PHP Simple HTML DOM Parser – библиотека для парсинга сайтов
Те, кто хоть раз писал парсер, знает, что не стоит этого делать с помощью регулярных выражений. Проиллюстрировать это утверждение поможет следующий пример.
Возьмем HTML код:
<div><a href="http://korzh.net"><div>Сайт по программированию парсеров</div><div> и многое другое</div></a></div>
К примеру, из него нам нужно получить описание и url сайта. Если брать исключительно этот кусок кода, то все решается достаточно просто:
$html = '<div><a href="http://korzh.net"><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://korzh.net,title:Сайт по программированию парсеров и многое другое
Проблемы начинаются тогда, когда описание сайта заполняют пользователи, и оно не имеет определенного шаблона.
<div><a href=”http://korzh.net”><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_html(str) и file_get_html(url)
Они делают одно и тоже, преобразуют HTML текст в DOM дерево, различаются лишь источники.
str_get_html – на вход получает обычную строку, т.е. если вы получили HTML прибегнув к curl, или file_get_contents то вы просто передаете полученный текст этой функции.
$html = str_get_html('<html><body>Привет!</body></html>');
file_get_html – сама умеет загружать данные с удаленного URL или из локального файла
$html = file_get_html('http://www.yandex.ru/');
или
$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 = file_get_html('http://www.yandex.ru/');
в результате, в переменной $html будет объект типа simple_html_dom.
При больших объемах данных, в библиотеке происходит утечка памяти. Поэтому после окончания одного цикла надо ее чистить.
Делает это метод clear.
К примеру грузим 5 раз сайт www.yandex.ru с разными поисковыми запросами
$k = 5; while($k>0){ $html = file_get_html('http://yandex.ru/yandsearch?text=hi'.$k.'&lr=11114'); // загружаем данные // как-то их обрабатываем $html->clear(); // подчищаем за собой unset($html); $k--; }
Эти две строчки $html->clear(); и unset($html); лучше писать сразу же после того, как Вы создали объект. Иначе забудете, и скрипт отвалится, забив всю память.
После того, как html текст упакован в объект, можно приступать непосредственно к поиску нужных элементов.
Большинство поисковых функций выполняет метод find(selector, [index]). Если второй аргумент не задан, метод возвращает массив элементов. Если же задан то элемент этого массива с индексом index.
Пример: скачаем главную страницу моего блога, и выведем все ссылки, которые встретим на своем пути.
require_once 'simple_html_dom.php'; $data = file_get_html('http://korzh.net'); if($data->innertext!='' and count($data->find('a'))){ foreach($data->find('a') as $a){ echo '<a href="http://korzh.net/'.$a->href.'">'.$a->plaintext.'</a></br>'; } }
В примере, в качестве селектора я воспользовался названием тега <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 class="active myclass"/>
Поиск нескольких тегов
$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
$es = $html->find('text'); // найдет все текстовые блоки в html
Комментарии находим по тегу comment
$es = $html->find('comment');
Каждый найденный элемент и сам $html имеют 5 полей
$html = str_get_htmll("<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_htmll("<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_htmll("<div>foo <b>bar</b></div"); $b = $html->find('b',0); $b->outertext = ''; echo $html->innertext; // вернет <div>foo</div>
Тут следует помнить, что хоть элемент и не виден в html, из дерева DOM он никуда не делся
$html = str_get_htmll("<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, в дерево элемент присутствует
при желании мы даже можем вернуть элемент на место
$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_htmll($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_htmll('<span>bar</span><span>pole</span><span>sushi</span><a>okno</a>'); // Регистрация функции обратного вызова с ее именем $html->set_callback('my_callback');// вызов функции произойдет при конвертации объекта в строку echo $html; // на самом деле, при этом вызывается магический метод __toString, он и запускает наши калбяки
На экране мы увидим
<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 $url = 'http://images.yandex.ru/yandsearch?text='.urlencode('Джессика Альба').'&rpt=image'; $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 надо дополнять до полного if( !preg_match('#^http://#',$a->href) )$a->href = 'http://images.yandex.ru'.$a->href; // и еще дна тонкость, & надо заменять на & $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 $url = 'http://images.yandex.ru/yandsearch?text='.urlencode('Джессика Альба').'&rpt=image'; getYandexImages($url);
Все хорошо, 200 картинок лежат в папке data. Но их размер слишком мал.
Поэтому завершающим аккордом нашей практики будет загрузка увеличенной фотографии.
Для этого определим еще одну функцию
function getBigImage($url){ $data = @file_get_contents($url); if(trim($data)=='')return false; // бывает что сайт недоступен, его фото мы не грузим $data = str_get_htmll($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_htmll($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 надо дополнять до полного if( !preg_match('#^http://#',$a->href) )$a->href = 'http://images.yandex.ru'.$a->href; // и еще дна тонкость, & надо заменять на & $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){ if( !preg_match('#^http://#',$a->href) )$a->href = 'http://images.yandex.ru'.$a->href; $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 картинок $url = 'http://images.yandex.ru/yandsearch?text='.urlencode('Джессика Альба').'&rpt=image'; getYandexImages($url);
Вот и все, наслаждаемся фото великолепной Джессики Альбы. Надеюсь меня простит Яндекс, ведь по сути фото грабится не с их серверов, а с прямиком с сайтов, где они лежат.
Кроме того это всего лишь демонстрация работы. Думаю никому в здравом уме, не придет в голову парсить Яндекс с помощью file_get_content. Данную библиотеку можно применять и в мирном программировании. К примеру в качестве шаблонизатора для CMS. Почему нет, с хорошим кешированием будет очень удобная штука.
Исходники