Контакти

Пишемо парсер контенту php. Парсер на PHP – це просто. Пишемо скрипт парсера

Потроху вивчаю можливості PHP для створення парсерів. Я вже писала про те, як парсити. Зараз розповім про один із способів парсингу html (він підійде і для xml теж, до речі). Повторю, що в php я не гуру, тому буду дуже вдячна, якщо ви залишите свої коментарі до теми.

Поблукаючи нашими та англомовними форумами, зрозуміла, що суперечка про те, чи краще парсити html регулярними виразами або використовувати для цих цілей можливості PHP DOMє холіваром. Сама ж я дійшла висновку, що все залежить від складності структури даних. Адже якщо структура досить складна, то за допомогою регулярок доводиться ширяти в кілька етапів: спочатку виділити великий шматок, потім розділити його на більш маленькі і т.д.. У результаті, якщо дані складні (або їх дуже багато), то процес парсингу може значно затягнутися. Ресурсоємність у цьому випадку ще залежатиме, звичайно ж, від самих регулярних виразів. Якщо в регекспах багато ". *" (Вони є найбільш ресурсомісткими, тому що "прочісують" вихідний код з максимальною жадібністю), то уповільнення буде помітним.

І ось саме в цьому випадку дуже доречно доводиться PHP DOM. Це зручний інструмент для парсингу як XML, так і HTML. Деякі дотримуються думки, що парсить html регексп взагалі не можна, і люто захищають PHP DOM.

У свою чергу, я ознайомилася з цим розширенням, написавши простенький скрипт. Який і наводжу тут, щоб наочно показати, як це все легко і просто. У прикладі розбирається html із частиною карти сайту цього блогу. Він присвоєний змінній прямо всередині коду. У " бойових " умовах вихідні дані слід отримувати, наприклад, через file_get_contents().


$html = "
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

сайт Map


Останні теми блогу















http://сайт/2009/08/blog-post_06.html Бази
MySQL та Delphi. Express-метод
http://сайт/2009/08/blog-post.html Пост про те, що краще сто разів перевірити





";
/** створюємо новий dom-об'єкт **/
$dom = новий domDocument;

/** завантажуємо html в об'єкт **/
$dom->loadHTML($html);
$dom->preserveWhiteSpace = false;

/** елемент за тегом **/
$tables = $dom->getElementsByTagName("table");

/** отримуємо всі рядки таблиці **/
$rows = $tables->item(0)->getElementsByTagName("tr");

/** цикл по рядках **/
foreach ($rows as $row)
{
/** всі осередки по тегу **/
$cols = $row->getElementsByTagName("td");
/** виводимо значення **/
echo $cols->item(0)->nodeValue."
";
echo $cols->item(1)->nodeValue."
";
echo "


";
}
?>

В результаті після запуску скрипта отримуємо таку картину:

Upd: Без сумніву, для зручнішої роботи зі структурою HTML у PHP вам треба познайомитися з бібліотекою

Для того, щоб спарсити сторінку сайту (тобто розібрати її HTML-код), її для початку слід отримати. А потім вже отриманий код можна розібрати за допомогою регулярних виразів і якимось чином його проаналізувати, або зберегти в базу даних, або і те, і інше.

Отримання сторінок сайтів за допомогою file_get_contents

Отже, спочатку давайте повчимося отримувати сторінки веб-сайтів в змінну PHP. Це робиться за допомогою функції file_get_contents, яка найчастіше використовується для отримання даних із файлу, однак, може бути використана для отримання сторінки сайту - якщо передати їй параметром не шлях до файлу, а url сторінки сайту.

Врахуйте, що ця функція не ідеальна і існує потужніший аналог - бібліотека CURLяка дозволяє працювати з куками, із заголовками, дозволяє відправляти форми і переходити по редиректах. Все це file_get_contentsробити не вміє, проте спершу нам зійде і вона, а роботу з CURLми розберемо у наступному уроці.

Отже, давайте для прикладу отримаємо головну сторінку мого сайту та виведемо її на екран (зробіть це):

Що ви отримаєте в результаті: у себе на екрані ви побачите сторінку мого сайту, проте, швидше за все без CSS стилів і картинок (чи працюватимуть CSS і картинки - залежить від сайту, чому так - розберемо пізніше).

Тепер виведемо не сторінку сайту, а її вихідний код. Запишемо його до змінної $strі виведемо на екран за допомогою var_dump:

Врахуйте, що var_dumpмає бути налаштований коректно у конфігурації PHP (див. попередній урок для цього). Коректно - це означає, що ви повинні бачити теги і не повинно бути обмеження на довжину рядка (код сторінки сайту може бути дуже великим і бажано бачити його весь).

Отже, якщо все зроблено добре, і ви бачите вихідний код сторінки сайту - час приступити до його парсингу за допомогою регулярних виразів.

Якщо ви не знаєте регулярних виразів або сумніваєтеся у своїх знаннях - саме час вивчити підручник з регулярних виразів, а потім повернуться до вивчення цього посібника з парсингу.

Директива повинна бути включена allow_url_fopen http://php.net/manual/ru/filesystem.configuration.php#ini.allow-url-fopen

Парсинг за допомогою регулярних виразів

При спробі розібрати HTML код за допомогою регулярних виразів на вас чекатимуть деякі підводні камені. Їх наявність найчастіше пов'язана з тим, що регулярні вирази не призначені для розбору тегів - для цього є більш просунуті інструменти, наприклад, бібліотека phpQuery, яку ми розбиратимемо в наступних уроках.

Проте, вміти використовувати регулярні вирази для парсингутеж важливо - по-перше, регулярки це простий (якщо ви їх вже знаєте - то простий) і популярний інструмент для парсингу, по-друге, регулярки працюють на порядок швидше, ніж будь-які бібліотеки (часто це критично), ну і в- третіх, навіть при використанні спеціальних бібліотек, потреба в регулярках все одно є.

Підводні камені

Першанесподіванка, яка чекає на вас при використанні preg_matchі preg_match_all- це те, що вони працюють тільки для тегів, які повністю розташовані на одному рядку (тобто, в них немає натиснутого ентеру). Якщо спробувати спарсити багаторядковий тег - у вас нічого не вийде, доки ви не ввімкнете однорядковий режимза допомогою модифікатора s. Ось таким чином:

Друганесподіванка чекає на вас, коли ви спробуєте попрацювати з кирилицею - у цьому випадку потрібно не забути написати модифікатор u(u маленьке, не плутати з великим), ось так:

Які ще підводні камені на вас чекають - розбиратимемо поступово протягом даного уроку.

Спробуємо розібрати теги

Нехай ми якимось чином (наприклад, через file_get_contents) отримали HTML код сайту Ось він:

Це заголовок тайтл Це основний вміст сторінки.

Давайте займемося його розбором. Для початку давайте отримаємо вміст тега , тега <head>, і тега <body>.</p> <p>Отже, отримаємо вміст тега <title>(у змінній <b>$str</b>зберігається HTML код, який ми розуміємо):</p> <p> <?php preg_match_all("#<title>(.+?)#su", $str, $res); var_dump($res); ?>

Вміст :

(.+?)#su", $str, $res); var_dump($res); ?>

Вміст :

(.+?)

#su", $str, $res); var_dump($res); ?>

Загалом нічого складного немає, тільки зверніть увагу на те, що як куточки тегів, так і зліш від закриває тега екранувати не треба (останнє вірно, якщо обмежувачем регулярки є не сліш /, а, наприклад, решітка #, як у нас зараз).

Однак насправді наші регулярки не ідеальні. За деяких умов вони просто відмовляться працювати. Ви повинні бути готові до цього – сайти, які ви будете парсити – різні (часто вони ще й застарілі), і те, що добре працює на одному сайті, цілком може перестати працювати на іншому.

Що ж у нас не таке? Насправді тег - Той самий тег, як і інші і в ньому цілком можуть бути атрибути. Найчастіше це атрибут class , але можуть бути інші (наприклад, onloadдля виконання JavaScript).

Отже, перепишемо регулювання з урахуванням атрибутів:

(.+?)

#su", $str, $res); var_dump($res); ?>

Але й тут ми помилилися, причому помилок кілька. Перша- слід ставити не плюс + , а зірочку * , так як плюс передбачає наявність хоча б одного символу- але атрибутів у тезі може і не бути - і в цьому випадку між назвою тега bodyі куточком не буде жодних символів – і наша регулярка рятує (не зрозуміло, що я тут написав – навчайте регулярки).

Виправимо цю проблему і повернемося до подальшого обговорення:

(.+?)

#su", $str, $res); var_dump($res); ?>

Другапроблема така: якщо всередині будуть інші теги (а так воно і буде в реальному житті) - то наше регулювання зачепить зайвого. Наприклад, розглянемо такий код:

Це заголовок тайтл

Регулярка знайде не , як очікувалося, а

Абзац(

) - тому що ми не обмежили їй жадібність. Зробимо це: місце напишемо - у цьому випадку буде все гаразд.

Але найкращим варіантом буде написати замість точки конструкцію [^>] (не закриває куточок), ось так - ]*?> - у цьому випадку ми повністю застрахуємо себе від проблем такого роду, тому що регулювання ніколи не зможе вийти за тег.

Отримання блоку з id

Давайте розглянемо наступний код:

Це заголовок тайтл

Контент
Ще див


Напишемо регулярку, яка отримає вміст блоку з id, рівним content.

Отже, спроба номер один (не зовсім коректна):

#(.+?)

#su

Що тут не таке? Проблема з пробілами - адже між назвою тега та атрибутом може бути скільки завгодно прогалин, так само, як і навколо одно в атрибутах.

Всі проблеми такого роду суттєві - навіть якщо ваша регулярка розбирає одну сторінку сайту - це не означає, що вона розбере іншу подібну сторінку: на ній цілком навколо в атрибуті id могли поставити прогалини - і тут ваша регулярка рятує.

Тому, регулярки парсера потрібно будувати так, щоб вони обходили якнайбільше проблем- у цьому випадку ваш парсер працюватиме максимально коректно на всіх сторінках сайту, а не лише на тих, які ви перевірили.

Давайте поправимо наше регулювання:

#

(.+?)
#su

Навколо рівно прогалини можуть бути, а можуть і не бути, тому там стоїть оператор повторення зірочка * .

Крім того, перед тегом, що закриває куточком, теж можуть бути прогалини (а можуть і не бути) - врахуємо і це:

#(.+?)

#su

Отже, вже краще, але ще далеко не ідеал - адже навколо атрибуту id можуть бути й інші атрибути, наприклад:

. У цьому випадку наше регулювання рятує. Вкажемо, що можуть бути ще й інші атрибути:

#

(.+?)
#su

Зверніть увагу, що після

стоїть регулярка .+? , а перед > стоїть регулярка .*? - це не помилка, так і задумано, адже після
обов'язково має йти пробіл (тобто хоча б один символ точно буде), а перед > може взагалі не бути інших атрибутів (крім нашого id) та пробілу теж може не бути.

Регулярка стала ще кращою, але є проблема: краще не використовувати крапку в блоках типу .*? - ми можемо вистачити зайвого вийшовши за наш тег (пам'ятаєте приклад вище з body?). Краще все-таки використати [^>] - це гарантія безпеки:

#

]+? id\s*?=\s*?"content" [^>]*? >(.+?)
#su

Наступна проблема: лапки-то в атрибутах можуть бути як одинарними, так і подвійними (їх навіть може взагалі не бути, якщо значення атрибута - одне слово, але цей випадок рідкісний - не враховуватимемо його, якщо вам зустрінеться такий сайт - простіше написати регулярку спеціально йому). Отже, врахуємо це:

#]+?id\s*?=\s*? ["\"] content ["\"] [^>]*?>(.+?)

#su

Одинарна лапка заекранована - ми це робимо, тому що зовнішні лапки від рядка PHP у нас теж одинарні, ось тут:

Загалом регулярка досить хороша, але іноді йдуть далі і роблять так, щоб перша лапка від тега збігалася з другою (виключаємо варіант id="content"). У цьому випадку роблять так - перша лапка лягає в кишеню, а друга лапка вказується кишенею, щоб збігалася з першою:

#]+?id\s*?=\s*? (["\"]) content \1 [^>]*?>(.+?)

#su

Для нашого завдання це особливо не потрібно (можна бути точно впевненим, що таке id="content" - навряд чи буде десь), але є атрибути, де це суттєво. Наприклад, у такому разі:

- в атрибуті title цілком може затесатися одинарна лапка і регулювання title\s*?=\s*?["\"](.+?)["\"]витягне текст " Розповідь про д- тому що пошук ведеться до першої лапки.

А ось регулярка title\s*?=\s*?(["\"])(.+?)\1коректно оброблятиме

і навіть
.

Проблема вкладених блоків

У нашому регулярні є ще одна проблема – вона не може працювати із вкладеними блоками. Наприклад, якщо всередині дива #content є ще один див - регулярка знайде текст до першого закриває

, а не для дива для #content. Приклад проблемного коду:

Це заголовок тайтл

Див всередині контенту
Контент


Наше регулювання витягне тільки

Див всередині контенту
- зупиниться на першому ж
. Що робити у цьому випадку?

Що робити у цьому випадку? По-перше, до цього випадку завжди потрібно бути готовим – навіть якщо на досліджуваних сторінках сайту немає вкладених блоків – вони цілком можуть бути і на інших сторінках або з'явитися потім (якщо сайт парситься не один раз, а періодично).

Ну, а що робити - потрібно просто прив'язуватися не до

а до того, що стоїть під нашим блоком (у нашому випадку під контентом). У наведеному нижче коді під ним стоїть