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

Блог Никиты Синицы


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

2015-12-21 13:26:40

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

 Возьмем 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_html(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://xdan.ru');
if($data->innertext!='' and count($data->find('a'))){
	foreach($data->find('a') as $a){
echo '<a href="http://xdan.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

$es = $html->find('text'); // найдет все текстовые блоки в html

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

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

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

$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, 
он и запускает наши калбяки

На экране мы увидим 

<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. Почему нет, с хорошим кешированием будет очень удобная штука.

 При больших объемах сайтов доноров,  неплохо бы разбить все на потоки.

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

исходники на xdan.ru исходники зеркало Комментарий к архиву: В архиве 5 файлов + архив с документацией на PHP Simple HTML DOM Parser. Файл pars_page.php содержит пример загрузки одной страницы яндекс картинок. Файл pars_all_page.php содержит пример загрузки всех найденных страниц. Файл parse_all_page_and_big_photo.php содержит готовый парсер для скачивания больших фото с поисковой выдачи яндекс картинок.

Источник: http://xdan.ru/Uchimsya-parsit-saity-s-bibliotekoi-PHP-Simple-HTML-DOM-Parser.htm




Добавить комментарий:













Смотрите также:

Simple HTML DOM: взять атрибут data-src
Иконки для сайтов
MySQL: cравнение даты (datetime) через оператор BETWEEN
PHP: отнять от даты один год
PHP: взять из строки только латиницу
Таблица нулевых вилок
Что такое МОС в Sape
Заменить <br/> на \n
Mysql: добавить уникальный индекс для столбца типа text
Бесплатная проверка «Ping» из различных частей мира
PHP: взять все ссылки
PHP: обрезать строку до определенного символа
Php: превью новости (оставить энное количество символов)
Php: взять информацию из тэга
PHP: вырезать из строки тэг img
Включить показ предупреждений и ошибок PHP
В Испании создали очки для селфи
Продажи Apple Watch рухнули на 55%
«ВКонтакте» вернула функцию граффити
Facebook разрешил пользователям сохранять видео на смартфоны
Microsoft признала провал Windows10
"Яндекс" рассказал тонкости построения мобильного раздела на поддомене
Debian mysql restart
Как удалить в Dreamweaver разный кусок кода во всех файлах
Бесплатная проверка битых ссылок