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

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


Учимся парсить сайты с библиотекой 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

Как в Total Commander посмотреть размеры папок?

2015-12-21 07:17:34

Для этого всего лишь необходимо выделить папку и нажать пробел.

GIF-хантер: самый полный гайд по гифкам

2015-12-05 11:13:15

Формат изображений GIF был придуман для экономии трафика в эру медленного интернета десятилетия назад. Сейчас, когда экономить уже не нужно даже на мобильном интернете, гифки служат для других целей — они дарят нам радость. Как максимально гармонично вписать их в свою жизнь и стать настоящим GIF-хантером, вы узнаете из этой статьи.

Как заработать новичку в интернете: сложный уровень

2015-02-21 13:13:31

ОБНОВЛЕНИЕ: В процессе эксплуатации всемирной выяснилось, что есть более легкий путь добывания дензнаков. Подробнее читайте здесь.

Все написанные статьи по "прежнему пути" оставляю как есть. Все кто желает идти по "новому пути" переходите по вышеприведенной ссылке.

Сложный уровень предполагает заработок на своих сайтах, будь-то автономный сайт или сайт, созданный на платформе сервиса сайтов.

Автономный сайт (также называемый Standalone, Стенделон) - это сайт, который находится полностью под управлением владельца и юридически ему принадлежит. Сайт, созданный при помощи сервиса, должен отвечать правилам платформы, на которой создан и юридически принадлежит владельцу платформы.

Самыми известными платформами для создания сайтов (а по сути блогов) являются Живой Журнал (livejournal), LiveInternet, уже закрытый Я.ру, WordPress, Blogger. А вот конструкторами сайтов (в т.ч. блогов) уже можно назвать Wix или Ucoz (которые, к тому же, разрешают привязать свой домен).

Еще разъясню такой момент: Домен (а точнее доменное имя) - это название сайта, а сайт - это наполнение домена. Можно привести пример с автомобильным номером. Домен как автомобильный номер сам по себе бесполезен без автомобиля (сайта) и его можно прицепить к любому автомобилю (сайту).

Домен можно получить у регистратора, оплатив определенную сумму, или получить бесплатно в определенной доменной зоне. В сервисе сайтов вы также получите бесплатный домен - это будет поддомен этой платформы (например, site.livejournal.com).

Для сайта нужен хостинг (по аналогии с автомобилем - гараж), где будут храниться ваши файлы. Хостинг - это определенным образом настроенный сервер (компьютер), который постоянно подключен к сети и показывает ваш сайт любому пользователю в любом уголке мира.

Создание автономного сайта возможно на "чистом хтмл" и подразумевает написание кода своими руками и знания html, css, php, javascript, а создавая сайт на движке (CMS, Content management system, Система управления содержимым), вы получаете готовый сайт даже без знаний html, css, php, javascript. На первый взгляд, движок - это проще и быстрее, но это только на первых порах. К тому же, я буду рассказывать как создавать сайт на "чистом хтмл".

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

Это был пролог, а подробности узнаете в следующих статьях.

РАЗДЕЛ LIVEJOURNAL

Рассмотрим создание сайта (блога) на примере блогохостинга livejournal. Я выбрал эту платформу, так как она очень хорошо монетизируется, а также имеет возможность зарабатывать внутри системы.

Регистрация и создание блога
Ведение блога и увеличение количества читателей
Монетизация ЖЖ

РАЗДЕЛ STANDALONE

1. Что такое HTML
2. Что такое CSS
3. Что такое PHP
4. Что такое JAVASCRIPT К сожалению, статья так и не дописана...

 

Страницы: 1   2   3   4   5   6   7   8  9   10   11   12   13   14   15   16   17   18   ...  20