Те, кто хоть раз писал парсер, знает, что не стоит этого делать с помощью регулярных выражений. Проиллюстрировать это утверждение поможет следующий пример.
Возьмем HTML код:
< div >< a href = "http://xdan.ru" >< div >Сайт по программированию парсеров</ div >< div > и многое другое</ div ></ a ></ div > |
К примеру, из него нам нужно получить описание и url сайта. Если брать исключительно этот кусок кода, то все решается достаточно просто:
$html = '<div><a href="http://xdan.ru"><div>Сайт по программированию парсеров</div><div> и многое другое</div></a></div>' ; preg_match( '#<div><a href="([^"]+)"><div>([^<]+)</div><div>([^<]+)</div></a></div>#U' , $html , $list ); echo 'url:' . $list [1]. ',title:' . $list [2]. $list [3]; // выведет url:http://xdan.ru,title:Сайт по программированию парсеров и многое другое |
Проблемы начинаются тогда, когда описание сайта заполняют пользователи, и оно не имеет определенного шаблона.
< div >< a href=”http://xdan.ru”>< div >Сайт по < b >программированию</ b > парсеров</ div >< div > и многое < div > многое </ div > другое </ div ></ a ></ div > |
Такой код регулярному выражению не по зубам.
Обычно, в вузах на этот случай учат писать конечный автомат. Суть его в том, что мы перебираем, посимвольно, весь html текст, находим начало тега, и строим дерево документа. Так называемое DOM (Document Object Model)
Сейчас, писать такое самому нет необходимости.
В php, начиная с версии 5, есть встроенные методы работы с деревом документа (класс DOMDocument), но основан он на XML парсере.
А HTML и XML это хоть и очень похожие, но в тоже время абсолютно разные технологии.
К примеру, непременное требование к XML это закрытые теги и отсутствие ошибок.
Отсюда вытекает условие: ошибок в html, который мы парсим с помощью нативных средств php, быть не должно.
К сожалению, на сайтах донорах, ошибки не редки, а значит этот метод отпадает.
Для корректного разбора таких сайтов, на помощь придут php библиотеки PHPQuery, Simple HTML DOM, Zend DOM Query, Nokogiri .
Некоторые из них, после небольших манипуляций скармливают html тому же DOMDocument. Мы не будем их рассматривать.
В этой статье я расскажу про SimpleHTMLDOM. Этой библиотекой я пользуюсь уже несколько лет, и она меня еще ни разу не подводила.
Скачиваем последнюю версию здесь.
Пусть Вас не смущает то, что она не обновлялась с 2008 года, то, что она умеет, полностью покроет Ваши нужды в разборе html текстов.
В архиве, который вы скачали, две папки (примеры работы и документация) и файл simple_html_dom.php.
simple_html_dom.php это и есть вся библиотека, больше ничего для работы не потребуется. Кидаем этот файл в папку с проектом и в своем скрипте просто подгружаем его.
include 'simple_html_dom.php' ; |
Кроме документации, которую вы скачали с архивом, доступна еще online версия, ее вы найдете здесь
Файл подключен и готов к работе.
Для того, чтобы начать разбирать HTML, его сперва нужно получить. Обычно, я делаю это при помощи библиотеки CURL.
В simplehtmldom есть методы для удаленной загрузки страниц. После подключения файла библиотеки, нам доступны 2 функции для обработки HTML строк.
str_get_htm(str) и file_get_html(url)
Они делают одно и тоже, преобразуют HTML текст в DOM дерево, различаются лишь источники.
str_get_htm – на вход получает обычную строку, т.е. если вы получили HTML прибегнув к curl, или file_get_contents то вы просто передаете полученный текст этой функции.
$html = str_get_html( '<html><body>Привет!</body></html>' ); |
file_get_html – сама умеет загружать данные с удаленного URL или из локального файла
|
или
$html = file_get_html( 'data/test.htm' ); |
К сожалению, file_get_html загружает страницы обычным file_get_contents. Это значит если хостер, выставил в php.ini allow_url_fopen = false (т.е. запретил удаленно открывать файлы), то загрузить что-то удаленно, не получится. Да и серьезные веб сайты таким способом парсить не стоит, лучше использовать CURL с поддержкой proxy и ssl. Однако для наших опытов, вполне хватит и file_get_html.
|
в результате, в переменной $html будет объект типа simple_html_dom.
При больших объемах данных, в библиотеке происходит утечка памяти. Поэтому после окончания одного цикла надо ее чистить.
Делает это метод clear.
К примеру грузим 5 раз сайт www.yandex.ru с разными поисковыми запросами
$k = 5; while ( $k >0){ // как-то их обрабатываем $html ->clear(); // подчищаем за собой unset( $html ); $k --; } |
Эти две строчки $html->clear(); и unset($html); лучше писать сразу же после того, как Вы создали объект. Иначе забудете, и скрипт отвалится, забив всю память.
После того, как html текст упакован в объект, можно приступать непосредственно к поиску нужных элементов.
Большинство поисковых функций выполняет метод find(selector, [index]). Если второй аргумент не задан, метод возвращает массив элементов. Если же задан то элемент этого массива с индексом index.
Пример: скачаем главную страницу моего блога, и выведем все ссылки, которые встретим на своем пути.
require_once 'simple_html_dom.php' ; if ( $data ->innertext!= '' and count ( $data ->find( 'a' ))){ foreach ( $data ->find( 'a' ) as $a ){ } } |
В примере, в качестве селектора я воспользовался названием тега <a>. Но можно использовать и другие CSS селекторы. Элемент на странице можно найти по его атрибутам. В первую очередь, это название тега, id и class. Также могут быть использованы и второстепенные атрибуты, к примеру, href ссылки или width картинки. Если и этих атрибутов нет, то не грех воспользоваться и регулярными выражениями.
Поиск по названию тега вы уже видели
$html ->find( 'div' ) |
поиск по id
$html ->find( '#preview' ) |
поиск по классу
$html ->find( '.myclass' ) |
или комбинированный вариант
$html ->find( '#preview div.myclass' ) |
в данном случае, сначала найдется элемент с id= preview затем в нем найдутся все теги div, и уже среди них фильтруются те у которых class=”myclass”
Если метод find ничего не нашел и index не задан, то он возвращает пустой массив. Если же index задан, то метод возвращает null.
Поэтому верным решением будет проверить
if ( count ( $html ->find( '#preview div.myclass' ))) foreach ( $html ->find( '#preview div.myclass' ) as $div ) echo $div ->innertext; |
Поиск по наличию атрибута
$html ->find( ' img [width]' ); // найдет нам все изображения у которых задан атрибут ширина |
или более конкретный поиск по значению атрибута
$ret = $html ->find( 'img[width=400px]' ); // найдет все изображения, у которых задана ширина равная 400px |
Такая нотация позволяет искать по двум и более смежным классам
$ret = $html ->find( 'img[class=active myclass]' ); //<img/> |
Поиск нескольких тегов
$html ->find( 'a, img, br,span' ); |
Поиск вложенных тегов
$es = $html ->find( 'ul.myclass li' ); // найдет все li который является потомком ul(возможно и не прямым) $es = $html ->find( 'div.myclass li' ); // найдет все li в div.myclass |
У каждого найденного элемента также есть метод find
$html ->find( 'div.myclass li' ); //найдет все div.myclass а потом все li лежащие в них |
если нам нужно найти все li только первого div’а то мы можем написать так
$html ->find( 'div.myclass' ,0)->find( 'li' ); |
Поиск по значению атрибута не ограничивается только равенством. Вот доступные условия
[атрибут] – проверяет есть ли у элемента данный атрибут
[атрибут=величина] — проверяет, есть ли у элемента данный атрибут и равно ли его значение величине.( div[class=myclass] – найдет все div’ы у которых class равен myclass)
[атрибут!=величина] — проверяет, есть ли у элемента данный атрибут и не равно ли его значение величине.( div[class!=myclassok] – найдет все div’ы у которых class не равен myclassok)
[атрибут^=величина] — проверяет, есть ли у элемента данный атрибут и начинается ли его значение с величины ( div[class^=my] – найдет все div’ы у которых class начинается с my, к примеру myclass и myclassok)
[атрибут$=величина] — проверяет, есть ли у элемента данный атрибут и заканчивается ли его значение величиной( div[class$=ok] – найдет все div’ы у которых class заканчивается на ok, к примеру myclassok, yok, okно не oki)
[атрибут*=величина] — проверяет, есть ли у элемента данный атрибут и содержит ли его значение в себе величину, в любом месте(div[class*=sok] – найдет все div’ы у которых class содержит sok, к примеру myclassok, ysoki, sok)
Обычный текст можно искать как тег text
1
|
$es = $html ->find( 'text' ); // найдет все текстовые блоки в html |
Комментарии находим по тегу comment
1
|
$es = $html ->find( 'comment' ); |
Каждый найденный элемент и сам $html имеют 5 полей
$html = str_get_html( "<div>foo <b>bar</b></div>" ); echo $html ; // выведет <div>foo <b>bar</b></div>; $e = $html ->find( "div" , 0); echo $e ->tag; // Вернет: "div" echo $e ->outertext; // Вернет: <div>foo <b>bar</b></div> echo $e ->innertext; // Вернет: foo <b>bar</b> echo $e ->plaintext; // Вернет: foo bar |
$e->tag Читает или записывает имя тега элемента.
$e->outertext Читает или записывает весь HTML элемента, включая его самого.
$e->innertext Читает или записывает внутренний HTML элемента
$e->plaintext Читает или записывает простой текст элемента, это эквивалентно функции strip_tags($e->innertext). Хотя поле доступно для записи, запись в него ничего не даст, и исходный html не изменит
$html = str_get_html( "<div>foo <b>bar</b></div" ); $div = $html ->find( 'div' ,0); $div ->plaintext = 'gooo' ; echo $div ->innertext; // вернет <div>foo <b>bar</b></div> |
Как Вы могли догадаться, для удаления ненужного элемента из HTML можно затереть его поле outertext
$html = str_get_html( "<div>foo <b>bar</b></div" ); $b = $html ->find( 'b' ,0); $b ->outertext = '' ; echo $html ->innertext; // вернет <div>foo</div> |
Тут следует помнить, что хоть элемент и не виден в html, из дерева DOM он никуда не делся
$html = str_get_html( "<div>foo <b>bar</b></div" ); $b = $html ->find( 'b' ,0); $b ->outertext = '' ; echo $html ->innertext; // вернет <div>foo</div>, элемент удален из HTML // но echo count ( $html ->find( 'b' )); // вернет 1, в дерево элемент присутствует |
при желании мы даже можем вернуть элемент на место
1
2
|
$b ->outertext = '<span>bar</span>' ; echo $html ->innertext; // вернет <div>foo<span>bar</span></div> |
Для более эффективной навигации по дереву документа доступны методы
$e->children ( [int $index] ) Возвращает объект N-го прямого потомка, если индекс установлен, в противном случае возвращает массив всех дочерних элементов
$e->parent() Возвращает родительский элемент.
$e->first_child() Возвращает первый дочерний элемент, или null, если ничего не найдено
$e->last_child() Возвращает последний дочерний элемент, или null, если ничего не найдено
$e->next_sibling() Возвращает следующий родственный элемент, или null, если ничего не найдено
$e->prev_sibling() Возвращает предыдущий родственный элемент, или null, если ничего не найдено
пример
$html ="<div> <b>bar</b> <b>foo</b> <span>arg</span> <div> <b>tor</b> </div> </div>"; |
Все дочерние элементы разные, как-то подобрать к ним селектор проблематично. Поэтому воспользуемся описанными методами.
$html = str_get_html( $html ); $div = $html ->find( 'div' ,0); $i = 0; while ( $item = $div ->children( $i ++)){ echo $item ->innertext; } |
либо так
$item = $div ->children(0); echo $item ->innertext; while ( $item = $item -> next_sibling()){ echo $item ->innertext; } |
Данные методы полезны при разборе таблиц, элементы которых, как правило, структурированы, но не имеют идентифицирующих атрибутов.
Ну и последняя фишка это вызов callback функции на найденный элемент
function my_callback( $element ) { if ( $element ->tag== 'span' ) $element ->outertext = '<b>' . $element ->innertext. '</b>' ; // заменим все span элементы на b } $html = str_get_html( '<span>bar</span><span>pole</span><span>sushi</span><a>okno</a>' ); // Регистрация функции обратного вызова с ее именем $html ->set_callback( 'my_callback' ); // вызов функции произойдет при конвертации объекта в строку echo $html ; // на самом деле, при этом вызывается магический метод __toString, он и запускает наши калбяки |
На экране мы увидим
1
|
< b >bar</ b >< b >pole</ b >< b >sushi</ b >< a >okno</ a > |
Доступ к атрибутам элементов осуществляется напрямую
foreach ( $html ->find( 'img' ) as $img ) echo $img ->src; //или echo $html ->find( 'img' ,0)->src; |
Хватит теории, перейдем к практике
Загрузим n фотографий из поисковой выдачи Yandex Картинок. http://images.yandex.ru/
require_once 'simple_html_dom.php' ; // поисковый URL $n = 2; // загружаем данный URL $data = file_get_html( $url ); // очищаем страницу от лишних данных, это не обязательно, но когда HTML сильно захламлен бывает удобно почистить его, для дальнейшего анализа foreach ( $data ->find( 'script,link,comment' ) as $tmp ) $tmp ->outertext = '' ; // находим все изображения на странице if ( count ( $data ->find( 'div.b-image img' ))){ $i = 1; foreach ( $data ->find( 'div.b-image img' ) as $img ){ // выводим на экран изображение echo '<img src="' . $img ->src. '"/>' ; // и скачиваем его в файл file_put_contents ( 'data/' .( $i ++). '.jpg' , file_get_contents ( $img ->src)); if ( $i > $n ) break ; // выходим из цикла если скачали достаточно фотографий } } $data ->clear(); // подчищаем за собой unset( $data ); |
Как быть если нам нужно больше фото, чем лежит на одной странице?
Ответ прост: Код, приведенный выше, заключается в функцию, в html помимо фото находим еще и URLвсех страниц, и рекурсивно вызываем данную функцию для этих страниц.
require_once 'simple_html_dom.php' ; function getYandexImages( $url , $findpages = true){ static $i = 1; $n = 200; // загружаем данный URL $data = file_get_html( $url ); // очищаем страницу от лишних данных, это не обязательно, но когда HTML сильно захламлен бывает удобно почистить его, для дальнейшего анализа foreach ( $data ->find( 'script,link,comment' ) as $tmp ) $tmp ->outertext = '' ; // находим URL страниц только для первого вызова функции if ( $findpages and count ( $data ->find( 'div.b-pager__pages a' ))){ foreach ( $data ->find( 'div.b-pager__pages a' ) as $a ){ // довольно распространенный случай - локальный URL. Поэтому иногда url надо дополнять до полного // и еще дна тонкость, & надо заменять на & $a ->href = str_replace ( '&' , '&' , $a ->href); // вызываем функцию для каждой страницы getYandexImages( $a ->href,false); } } // находим все изображения на странице if ( count ( $data ->find( 'div.b-image img' ))){ foreach ( $data ->find( 'div.b-image img' ) as $img ){ // выводим на экран изображение echo '<img src="' . $img ->src. '"/>' ; // и скачиваем его в файл file_put_contents ( 'data/' .( $i ++). '.jpg' , file_get_contents ( $img ->src)); if ( $i > $n ) exit ; // завершаем работу если скачали достаточно фотографий } } $data ->clear(); // подчищаем за собой unset( $data ); } // поисковый URL getYandexImages( $url ); |
Все хорошо, 200 картинок лежат в папке data. Но их размер слишком мал.
Поэтому завершающим аккордом нашей практики будет загрузка увеличенной фотографии.
Для этого определим еще одну функцию
function getBigImage( $url ){ $data = @ file_get_contents ( $url ); if (trim( $data )== '' ) return false; // бывает что сайт недоступен, его фото мы не грузим $data = str_get_html( $data ); // находим фото if ( count ( $data ->find( '#i-main-pic' )) ){ $dataimg = @ file_get_contents ( $data ->find( '#i-main-pic' ,0)->src); // собачка нужна в если сервер нам вернул 404, это выозвет Warning:, поэтому экранируем ошибки if (trim( $dataimg )== '' ) return false; // фото не доступно, его не грузим file_put_contents ( 'data/' .md5( $url ). '.jpg' , $dataimg ); // сохрпаняем в файл } $data ->clear(); // подчищаем за собой unset( $data ); } |
и слегка поправим getYandexImages
function getYandexImages( $url , $findpages = true){ global $i , $n ; // загружаем данный URL $data = @ file_get_contents ( $url ); $data = str_get_html( $data ); // очищаем страницу от лишних данных, это не обязательно, но когда HTML сильно захламлен бывает удобно почистить его, для дальнейшего анализа foreach ( $data ->find( 'script,link,comment' ) as $tmp ) $tmp ->outertext = '' ; // находим URL страниц только для первого вызова функции if ( $findpages and count ( $data ->find( 'div.b-pager__pages a' ))){ foreach ( $data ->find( 'div.b-pager__pages a' ) as $a ){ // довольно распространенный случай - локальный URL. Поэтому иногда url надо дополнять до полного // и еще дна тонкость, & надо заменять на & $a ->href = str_replace ( '&' , '&' , $a ->href); // вызываем функцию для каждой страницы getYandexImages( $a ->href,false); } } // находим все изображения на странице if ( count ( $data ->find( 'div.b-image img' ))){ foreach ( $data ->find( 'div.b-image a' ) as $a ){ $a ->href = str_replace ( '&' , '&' , $a ->href); getBigImage( $a ->href); if ( $i ++>= $n ) exit ; // завершаем работу если скачали достаточно фотографий echo '<script>document.getElementById("counter").innerHTML = "Загружено: ' . $i . ' из ' . $n . ' фото";</script>' ; flush (); } } $data ->clear(); // подчищаем за собой unset( $data ); } // поисковый URL $i = 1; $n = 20; // будем грабить 20 картинок getYandexImages( $url ); |
Вот и все, наслаждаемся фото великолепной Джессики Альбы. Надеюсь меня простит Яндекс, ведь по сути фото грабится не с их серверов, а с прямиком с сайтов, где они лежат.
Кроме того это всего лишь демонстрация работы. Думаю никому в здравом уме, не придет в голову парсить Яндекс с помощью file_get_content. Данную библиотеку можно применять и в мирном программировании. К примеру в качестве шаблонизатора для CMS. Почему нет, с хорошим кешированием будет очень удобная штука.
При больших объемах сайтов доноров, неплохо бы разбить все на потоки.
А используя описанный мой скрипт сортировки изображений по цвету, можно собрать неплохую отсортированную базу, фотографий знаменитостей.
И, как всегда, выкладываю все исходники
Юрий says:
Привет!
Пользуюсь HTML Dom Parser уже с год, но на сервере столкнулись с проблемкой: в некоторых случаях вываливается 502 Bad Gateway.
Причем вычистили все, оставили только file_get_html — все равно ошибка лезет при парсинге некоторых страниц…:(
Не подскажешь в чём может быть дело?
Памяти PHP — 1Gb, сервер очень крутой, быстрый, выделенный, но ошибка всё равно лезет! 🙁
Спасибо!
admin says:
apache + nginx ?
Юрий says:
Да, но уже решили проблему.
Оказалось надо просто делать
$html->clear();
unset($html);
Даже если всего один запрос за крон происходит, память всё равно забивалась.
В любом случае — спасибо за наводку и статью! 🙂