Учимся парсить сайты с библиотекой PHP Simple HTML DOM Parser

Те, кто хоть раз писал парсер, знает, что не стоит этого делать с помощью регулярных выражений. Проиллюстрировать это утверждение поможет следующий пример.

Возьмем HTML код:

<div><a href="http://jcreator.ru"><div>Сайт по программированию парсеров</div><div> и многое другое</div></a></div>

К примеру, из него нам нужно получить описание и url сайта. Если брать исключительно этот кусок кода, то все решается достаточно просто:

$html = '<div><a href="http://jcreator.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://jcreator.ru,title:Сайт по программированию парсеров и многое другое

Проблемы начинаются тогда, когда описание сайта заполняют пользователи, и оно не имеет   определенного шаблона.

<div><a href=”http://jcreator.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('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://jcreator.ru');
if($data->innertext!='' and count($data->find('a'))){
foreach($data->find('a') as $a){
echo '<a href="http://jcreator.ru/'.$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

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, в дерево элемент присутствует

при желании  мы даже можем вернуть элемент на место

12
$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
$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;
// и еще дна тонкость, &amp; надо заменять на &
$a->href = str_replace('&amp;','&',$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_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 надо дополнять до полного
if( !preg_match('#^http://#',$a->href) )$a->href = 'http://images.yandex.ru'.$a->href;
// и еще дна тонкость, &amp; надо заменять на &
$a->href = str_replace('&amp;','&',$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('&amp;','&',$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. Почему нет, с хорошим кешированием будет очень удобная штука.



Учимся парсить сайты с библиотекой PHP Simple HTML DOM Parser - Генератор расширений Joomla и многое другое на нашем сайте посвященном работе расширений, компонентов, модулей, плагинов для линейки Joomla. Отправляйте ссылку на страницу своим друзьям и в социальные сети воспользовавшись графическими иконками выше.

Блог

Календарь приёма объявлений

Небольшой пример из повседневной практики. Организация меняет реквизиты, и они вступят в силу через  пару недель. На сайте идет приём объявлений и объявления принимаются на несколько месяцев в...

Дырка в com_content

Сегодня у товарища, сайт которого находится на старой  Joomla 1.0.3, была сильная нагрузка на сервере, просматривая логи обнаружил не вежливый запрос такого порядка: http://сайт/index2...

Лечим вирус на сайте

  Первоначальный осмотр сайта обнаружил, что у клиента закрыт доступ по фтп, что сузило круг подозреваемых уязвимостей, стало быть отверстие в какомто скрипте движка joomla. Касперский яро...

Troj/JSRedir-MH

Снова грянул гром зарежения сайтов новым вирусом  Troj/JSRedir-MH.  И так в коротко по вирусу Troj/JSRedir-MH. Вирус или троян куда интереснее чем свои предшествиники, тепер...

Доработка расширения для рассылки Send Email JK

Сегодня поделюсь немного интересной информацией для владельцев сайтов собранных ан joomla.  Рано и поздно возникает задача подписки и рассылки информации с сайта, это может быть разная постановка зада...

Привет Ie 10 регулярка

И вот и мне пришлось столкнулся с проблемами перехода на win 8 в придачу с ie 10, некоторых постоянных пользователей моих сайтов. На многих сайтах  имеющих раздел по приёму объявлений, использ...

Kunena заменяем ссылку на профиль пользователя

Всем привет, снова снова кунена, получил задание на допиливание популярного форума Kunena, сегодня потребовалось поменять ссылку на профиль пользователя. Это связано с тем, что на сайте стоит социа...

Работает на Kunena - зачем на каждой странице?

  Все у кого стоит форум kunena мешает надпись с двумя ссылкам внизу форума "Работает на Kunena". Сея надпись висит на каждой странице форма, а также страница разработчиков содержит множество вн...

Kunena избавимся от лишних quote

Для тех кто действительно делает сайт для людей, моя следующая заметка. Расскажу вам, как избавиться от лишних quote в постах на форуме kunena. У меня несколько тематических сайтов с нормальной посеща...

Kunena решаем проблему с пропажей аватаров

Так и вот мы обновись до последней версии кунены, сразу видно компонент внутри сильно изменился, и вот незадача у некоторых пользователей пропали аватары, физически они есть но неотображаются. ...

Джомлавикам

Обработка параметров расширений joomla

При разработке расширений для CMS Joomla, таких как плагины, модули, компоненты или шаблоны, часто возникает необходимость получить параметры разрабатываемого или любого другого расширения. В этой ...

Кunena крамсаем шаблон

1. В папке \components\com_kunena\template\ваш_шаблон\ находим файл template.xml и вставляем в него такой кусок (чтобы была возможность выбора показывать это поле или нет): <param name="l...

Подключить свои классы и методы в Joomla?

Иногда случается, что набор собственных функций или библиотек нужно подключить в Joomla. Но иногда подключить их нужно так, чтобы они присутсвовали во всей системе, вне зависимо...

3 новых полезных класса в Joomla 2.5

В платформе Joomla 11.3 разработчики добавлил несколько полезных новых классов для разработчиков. Рассмотрю некоторый из них - JGrid (для построения HTML таблиц), JWebClient (для получения информац...

Репозиторий файлов на Joomla средствами Apache

Создадим две вложенные директории /files/files в корне сайта. Если вы пожелаете использовать другие имена директорий, то вам необходимо будет внести соответствующие правки в дальнейшем. ...