() translation by (you can also view the original English article)
Якщо вам потрібно розібрати HTML, тоді регулярні вирази це не той шлях, який ви маєте обрати. У цьому навчальному матеріалі ви дізнаєтеся, як використовувати простий до вивчення парсер з відкритим вихідним кодом, щоб аналізувати і отримувати дані з HTML-документів, отриманих із зовнішніх джерел. Використовуюси сайт nettuts як приклад, ви дізнаєтеся, як отримати список всіх статей, опублікованих на сайті і відобразити їх на екрані.
Крок 1. Підготовка
Перше, що вам потрібно зробити, це завантажити копію бібліотеки simpleHTMLdom, що знаходиться у вільному доступі на sourceforge.
Для завантаження доступно декілька файлів, але вам потрібно завантажити лише файл simple_html_dom.php; все інше це приклади та документація.

Крок 2. Основи парсингу
Ця бібліотека дуже проста у використанні, але є деякі основи, що потрібно переглянути, перш ніж діяти.
Завантаження HTML
1 |
|
2 |
$html = new simple_html_dom(); |
3 |
|
4 |
// Load from a string
|
5 |
$html->load('<html><body><p>Hello World!</p><p>We're here</p></body></html>'); |
6 |
|
7 |
// Load a file
|
8 |
$html->load_file('http://net.tutsplus.com/'); |
Можна створити початковий об'єкт або шляхом завантаження HTML з рядка, або з файлу. Файл можна завантажити з мережі за посиланням, або взяти з вашої локальної файлової системи.
Зверніть увагу: метод load_file() делегує свою роботу функції РНР file_get_contents(). Якщо у вашому файлі налаштувань php.ini немає запису "allow_url_fopen = true", ви не зможете відкрити віддалений файл таким чином. Проте, ви завжди можете використати бібліотеку CURL для завантаження віддалених сторінок, а потім прочитати їх за допомогою методу load().
Доступ до інформації

Коли ви вже маєте DOM об'єкт, ви можете почати працювати з ним за допомогою find() для створення колекцій. Колекція це група об'єктів, які знайдені за допомогою селекторів, синтаксис яких дуже схожий на синтаксис селекторів у jQuery.
1 |
|
2 |
<html>
|
3 |
<body>
|
4 |
<p>Hello World!</p> |
5 |
<p>We're Here.</p> |
6 |
</body>
|
7 |
</html>
|
У цьому прикладі ми розберемо, як отримати доступ до інформації в другому параграфі, змінити його, і вивести результат.
1 |
|
2 |
# create and load the HTML
|
3 |
include('simple_html_dom.php'); |
4 |
$html = new simple_html_dom(); |
5 |
$html->load("<html><body><p>Hello World!</p><p>We're here</p></body></html>"); |
6 |
|
7 |
# get an element representing the second paragraph
|
8 |
$element = $html->find("p"); |
9 |
|
10 |
# modify it
|
11 |
$element[1]->innertext .= " and we're here to stay."; |
12 |
|
13 |
# output it!
|
14 |
echo $html->save(); |
Виклик методу find() з селектором у першому параметрі завжди повертає колекцію (масив) HTML-тегів. Вказавши число у другому параметрі, ви отримуєте енний дочірний елемент.
Рядки 2-4: Завантаження HTML із рядка, як описано раніше.
Рядок 7: Цей рядок знаходить всі теги <p> у вихідному HTML і повертає їх як масив. Перший параграф буде мати індекс 0, і відповідно буде проіндексовано наступні абзаци.
Рядок 10: Отримуємо доступ до 2-го пункту в нашій колекції параграфів (індекс 1) і змінюємо його атрибут innertext. Innertext представляє вміст між відкриваючими і закриваючими тегами, в той час як outertext представляє вміст, включаючи теги. Ми можемо замінити весь тег із його змістом за допомогою outertext.
Зараз ми додамо ще один рядок коду, щоб змінити клас другого параграфу.
1 |
|
2 |
$element[1]->class = "class_name"; |
3 |
echo $html->save(); |
У результаті виклику методу save() ми отримаємо такий HTML-код:
1 |
|
2 |
<html>
|
3 |
<body>
|
4 |
<p>Hello World!</p> |
5 |
<p class="class_name">We're here and we're here to stay.</p> |
6 |
</body>
|
7 |
</html>
|
Інші селектори
Ось інші приклади селекторів. Якщо ви використовували jQuery, вони здадуться вам добре знайомими.
1 |
|
2 |
# get the first occurrence of id="foo"
|
3 |
$single = $html->find('#foo', 0); |
4 |
|
5 |
# get all elements with class="foo"
|
6 |
$collection = $html->find('.foo'); |
7 |
|
8 |
# get all the anchor tags on a page
|
9 |
$collection = $html->find('a'); |
10 |
|
11 |
# get all anchor tags that are inside H1 tags
|
12 |
$collection = $html->find('h1 a'); |
13 |
|
14 |
# get all img tags with a title of 'himom'
|
15 |
$collection = $html->find('img[title=himom]'); |
Набір параметрів у першому прикладі не зовсім інтуїтивний - всі селектори за замовчуванням повертають масив — навіть за використання селектору за ідентифікатором, якому за визначенням має відповідати лише один елемент. Однак, вказавши другий параметр, ми говоримо: "поверни тільки перший пункт цієї колекції".
Це означає, що значенням $single стане один елемент, а не масив, у який входить лише цей елемент.
Інші приклади говорять самі за себе.
Документація
Повну документацію бібліотеки ви можете знайти на сторінці документації проекту.

Крок 3. Реальний приклад
Щоб спробувати цю бібліотеку в дії, ми напишемо простий скрипт для збору контенту веб-сайту Nettuts, діставши перелік матеріалів, присутніх на сайті, у вигляді заголовку і опису... тільки для прикладу. Пам'ятайте, що парсинг це мудрована ділянка вебу і цього краще не робити без дозволу.

1 |
|
2 |
include('simple_html_dom.php'); |
3 |
|
4 |
$articles = array(); |
5 |
getArticles('http://net.tutsplus.com/page/76/'); |
Почнемо, вклавши файл бібліотеки і викликавши функцію getArticles() зі сторінкою, яку ми хочемо розібрати. У цьому випадку ми почнемо з останньої сторінки каталогу статей Nettuts.
Також ми оголосимо глобальний масив, щоб зробити збір всієї інформації в одному місці простішим. Перш ніж ми почнемо парсинг, давайте поглянемо, як огляд статті виглядає на Nettuts+.
1 |
|
2 |
<div class="preview"> |
3 |
<!-- Post Taxonomies -->
|
4 |
<div class="post_taxonomy"> ... </div> |
5 |
<!-- Post Title -->
|
6 |
<h1 class="post_title"><a>Title</a></h1> |
7 |
<!-- Post Meta -->
|
8 |
<div class="post_meta"> ... </div> |
9 |
<div class="text"><p>Description</p></div> |
10 |
</div>
|
Так виглядає відображення статті на сайті, разом із коментарями у коді. Чому коментарі це важливо? Бо синтаксичний аналізатор також враховує і їх.
Крок 4. Починаємо функцію парсингу
1 |
|
2 |
function getArticles($page) { |
3 |
global $articles; |
4 |
|
5 |
$html = new simple_html_dom(); |
6 |
$html->load_file($page); |
7 |
|
8 |
// ... more ...
|
9 |
}
|
Починаючи з найпростішого, ми оголосимо нашу глобальну змінну, створимо новий об'єкт simple_html_dom, а потім завантажимо сторінку, яку ми хочемо розібрати. Пізніше ця функція буде викликатися рекурсивно, тож у першому параметрі вона буде приймати URL-адресу.
Крок 5. Знаходимо потрібну інформацію

1 |
|
2 |
$items = $html->find('div[class=preview]'); |
3 |
|
4 |
foreach($items as $post) { |
5 |
# remember comments count as nodes
|
6 |
$articles[] = array($post->children(3)->outertext, |
7 |
$post->children(6)->first_child()->outertext); |
8 |
}
|
Тут вся сіль функції getArticles(). Давайте подивимося уважніше, щоб дійсно зрозуміти, що відбувається.
Рядок 1: Створює масив елементів <div> з класом "preview". Тепер у змінній $items ми маємо колекцію статей.
Рядок 5: $post тепер вказує на один елемент <div> з класом "preview". Оглянувши вихідний HTML, ми можемо побачити, що третій дочірний елемент це <h1>, що містить назву статті. Візьмемо його і запишемо у $articles[індекс][0].
Пам'ятайте, що треба починати рахувати з нуля та не забувати про коментарі, визначаючи відповідний індекс дочірнього вузла.
Рядок 6: Шостий дочірний елемент у $post це <div class="text">. Нам потрібен текст опису всередині тегу, тож візьмемо перший дочірний елемент від outertext - це буде включати тег абзацу. Один запис у масиві зібраних статей тепер має такий вигляд:
1 |
|
2 |
$articles[0][0] = "My Article Name Here"; |
3 |
$articles[0][1] = "This is my article description" |
Крок 6. Пагінація
Спочатку визначимо, як знайти нашу наступну сторінку. На сайті Nettuts+ це легко зрозуміти з URL-адреси, але ми зробимо вигляд, що не знаємо цього і отримаємо адресу наступної сторінки за допомогою парсингу.

Якщо ми подивимося у HTML, то побачимо наступне:
1 |
|
2 |
<a href="http://net.tutsplus.com/page/2/" class="nextpostslink">»</a> |
Якщо є наступна сторінка (а так буде не кожного разу), ми знайдемо якір з класом "nextpostslink". Тепер використаємо цю інформацію.
1 |
|
2 |
if($next = $html->find('a[class=nextpostslink]', 0)) { |
3 |
$URL = $next->href; |
4 |
|
5 |
$html->clear(); |
6 |
unset($html); |
7 |
|
8 |
getArticles($URL); |
9 |
}
|
В першому рядку ми бачимо, чи є якір з класом "nextpostslink". Зверніть увагу на другий параметр методу find(). Ми визначаємо, що хочемо взяти лише перший елемент колекції (з індексом 0). Так змінна $next буде зберігати тільки один елемент, а не масив елементів.
Далі ми покладемо у змінну $URL значення з атрибуту "href", що містить посилання. Це важливо, бо зараз ми будемо знищувати HTML-об'єкт. З огляду на циклічні виклики, що призводять до витіку пам'яті у PHP5, поточний об'єкт simple_html_dom повиннен бути очищений і знищений перед тим, як буде створений наступний. Це допоможе вам не з'їдати всю доступну пам'ять.
Нарешті, ми можемо викликати getArticles() з URL-адресою наступної сторінки. Ця рекурсія завершиться, коли не залишиться сторінок для парсингу.
Крок 7. Відображаємо результат
Для початку створимо декілька основних стилів. Це залишається на ваш розсуд — ви можете зробити виш вивід таким, яким ви захочете його бачити.

1 |
|
2 |
#main { |
3 |
margin:80px auto; |
4 |
width:500px; |
5 |
} |
6 |
h1 { |
7 |
font:bold 40px/38px helvetica, verdana, sans-serif; |
8 |
margin:0; |
9 |
} |
10 |
h1 a { |
11 |
color:#600; |
12 |
text-decoration:none; |
13 |
} |
14 |
p { |
15 |
background: #ECECEC; |
16 |
font:10px/14px verdana, sans-serif; |
17 |
margin:8px 0 15px; |
18 |
border: 1px #CCC solid; |
19 |
padding: 15px; |
20 |
} |
21 |
.item { |
22 |
padding:10px; |
23 |
} |
Далі ми вставимо невеликий шмат PHP-коду на сторінці, щоб вивести попередньо зібрану інформацію.
1 |
|
2 |
<?php
|
3 |
foreach($articles as $item) { |
4 |
echo "<div class='item'>"; |
5 |
echo $item[0]; |
6 |
echo $item[1]; |
7 |
echo "</div>"; |
8 |
}
|
9 |
?>
|
Остаточним результатом буде сторінка HTML, що містить список всіх статей, починаючи зі сторінки, що буде зазначена у першому виклику функції getArticles().
Крок 8. Висновок
Якщо ви зібрались парсити багато сторінок (скажімо, весь сайт), це може тривати довше за максимально дозволений вашим сервером час. Для прикладу, робота цього скрипту на моїй локальній машині займає близько однієї секунди на сторінку.
На такому сайті, як Nettuts, за існуючих 78 сторінок, це буде працювати більше за одну хвилину.
Цей навчальний матеріал має допомогти вам ознайомитись із HTML-парсингом. Є й інші способи роботи з DOM, включаючи вбудований у РНР, що дозволяє працювати з потужними xpath-селекторами для знаходження елементів. Але я вважаю, що завдяки простоті використання ця бібліотека є однією з кращих для швидкого старту. На завершення я хочу знову звернути увагу, що треба завжди пам'ятати про отримання дозволу, перш ніж парсити сайти — це дуже важливо. Дякую за увагу!