PHP Simple HTML DOM Parser – библиотека для парсинга сайтов

17 ноября 2011 г.

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

Возьмем 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_htmll('<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 

Комментарии находим по тегу comment1

$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; 
            // и еще дна тонкость, &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_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; 
            // и еще дна тонкость, &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. Почему нет, с хорошим кешированием будет очень удобная штука.
Исходники
Источник xdan.ru

Теги: рубрика PHP
  • Похожие статьи
  • Предыдущие из рубрики