HTML::TokeParser

от автора

Одним из наиболее часто используемых мною модулем при парсинге HTML является HTML::TokeParser. Этот модуль разбивает весь HTML документ на токены, с которым позже можно удобно работать.

Давайте рассмотрим какой-либо пример на практике. Возьмем сайт habrahabr.ru/

Пример 1. Необходимо спарсить список ссылок на полные статьи.

Первое. Определяем используемую кодировку. Для этого достаточно посмотреть тег meta, для хабра это – UTF-8

<meta http-equiv="content-type" content="text/html; charset=utf-8" /> 

Второе. Сохраняем веб страницу в файл. Пишем небольшой скрипт

use strict; use warnings; use HTML::TokeParser; use Data::Dumper;  open (my $f,"<", $ARGV[0]) ; my $p = HTML::TokeParser->new($f);  while (my $token = $p->get_token())  { 	print Dumper ($token);	 } 

Передаем ему на вход наш сохраненный файл и перенаправляем данные из STDOUT в файл. Мы должны получить что-то на подобии

$VAR1 = [           'T',           ' ',           ''         ]; $VAR1 = [           'D',           '<!DOCTYPE html>'         ]; $VAR1 = [           'T',           '     ',           ''         ]; $VAR1 = [           'S',           'html',           {             'xmlns' => 'http://www.w3.org/1999/xhtml',             'xml:lang' => 'ru'           },           [             'xmlns',             'xml:lang'           ],           '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ru">'         ]; 

и т.д. Этот файл будет использоваться для отладки.

Третье. Используем Firebug и смотрим, что из себя представляет ссылка на полную версию статьи. Вот что мы получаем в нашем случае

<a href="http://habrahabr.ru/post/163525/#habracut" class="button habracut">Читать дальше →</a> 

Догадываемся, что мы можем легко найти все ссылки благодаря class=«button habracut». Ищем в файлике, созданном на шаге 2 строку button habracut. Пишем свой парсер, я обычно оформляю его в виде отдельного класса. Парсер должен получать данные в HTML. Вот что получаем

Test.pl

use strict; use warnings; use habr_parse; use LWP::UserAgent; use Data::Dumper;  my $ua = LWP::UserAgent->new();  my $res = $ua->get("http://habrahabr.ru");  if ($res->is_success()) { 	my $parser = habr_parse->new();  #	print Dumper ($res);  	my $conf = {}; 	$conf->{content} = $res->content;	 	$conf->{cp} = 'utf8'; 	my $r = $parser->get_page_links($conf);	 	 	print Dumper ($r); }  

Habr_parse.pm

package habr_parse; use strict; use warnings; use HTML::TokeParser; use HTML::Entities; use Data::Dumper; use Encode;  sub new { 	my $class = shift;  	my $self = {};  	bless ($self, $class);  }  sub get_page_links { 	my $self = shift; 	my $conf = shift;  	my @data; 	# get internal format 	$conf->{content} = decode($conf->{cp},$conf->{content}); #	print Dumper ($conf); 	decode_entities($conf->{content});  	my $p = HTML::TokeParser->new(\$conf->{content});  	while (my $token = $p->get_token()) 	{ 		# we found our link 		if ($token->[0] eq 'S' && $token->[1] eq 'a' && defined ($token->[2]->{class}) && $token->[2]->{class}=~/^\s*button\s+habracut$/i) 		{ 			push @data, $token->[2]->{href};			 		} 	} #	print Dumper ($p);	 	 	return \@data; }     return 1; 

Для написания строки кода ниже, очень помогает наличие файла, созданного на шаге 2 (особенно если условий много)

	if ($token->[0] eq 'S' && $token->[1] eq 'a' && defined ($token->[2]->{class}) && $token->[2]->{class}=~/^\s*button\s+habracut$/i)  

В принципе это простой пример, потому что каждая ссылка имеет уникальный атрибут (значение class), которого нет больше нигде. Но сила HTML::TokeParser не в этом. Рассмотрим пример 2.

Пример 2. Необходимо для каждой статьи получит список категорий. С помощью Firebug мы замечаем, что категории находятся внутри тега div с атрибутом class=’hubs’.

Поскольку мы заходим на сайт без куков и какой-либо аутентификации то мы не можем быть подписаны ни на один хаб, поэтом для нас выводятся ссылки с title = ‘Вы не подписаны на этот хаб’

Если посмотреть на наш дамп, созданный на шаге 2 (пример 1), вот какой фрагмент нам нужен

$VAR1 = [           'S',           'a',           {             'href' => 'http://habrahabr.ru/hub/photo/',             'title' => 'Вы не подписаны на этот хаб',             'class' => 'hub '           },           [             'href',             'class',             'title'           ],           '<a href="http://habrahabr.ru/hub/photo/" class="hub " title="Вы не подписаны на этот хаб" >'         ]; $VAR1 = [           'T',           'Фототехника',           ''         ];  

Все получается просто, если мы вначале найдем ссылку с title =‘Вы не подписаны на этот хаб’ получим следующий токен и если это текст, сохраним.

Я покажу немного другую технику, которая базируется на том, что мы запихываем токены в стек, проверяя самый последний токен, до тех пор пока не встретим то, что нужно. Если же нам не встретился нужный токен мы используем unget_token().

Обратим внимание на другую закономерность после нужных нам данных идет токен с закрывающим тегом a

$VAR1 = [           'T',           'Гаджеты. Устройства для гиков',           ''         ]; $VAR1 = [           'E',           'a',           '</a>'         ];  

Изменим habr_parse.pm

package habr_parse; use strict; use warnings; use HTML::TokeParser; use HTML::Entities; use Data::Dumper; use Encode;  sub new { 	my $class = shift;  	my $self = {};  	bless ($self, $class);  }  sub get_page_links { 	my $self = shift; 	my $conf = shift;  	my @data; 	# get internal format #	$conf->{content} = decode($conf->{cp},$conf->{content}); #	print Dumper ($conf); #	decode_entities($conf->{content});  	my $p = HTML::TokeParser->new(\$conf->{content});   	my $tmp_conf = {};  	while (my $token = $p->get_token()) 	{  		# we found our link 		if ($token->[0] eq 'S' && $token->[1] eq 'a' && defined ($token->[2]->{class}) && $token->[2]->{class}=~/^\s*button\s+habracut$/i) 		{ 			$tmp_conf->{href} = $token->[2]->{href};			 		} 		elsif ($token->[0] eq 'S' && $token->[1] eq 'div' && defined ($token->[2]->{class}) && $token->[2]->{class}  eq 'hubs') 		{ 		 	my @next; 			my $found=0;  			# вначале идет информаци по категориям 			$tmp_conf = {};  			my $token = $p->get_token();  			push @next, $token; 			 			# пока нет закрывающегося тега div (вложенных div не должно быть). 			while ($next[$#next][1] ne 'div') 			{ 				push @next, $p->get_token();  #				print Dumper ($next[$#next][1]);  				# закрывающийся тег а 				if ($next[$#next][0] eq 'E' && $next[$#next][1] eq 'a') 				{ 					# предыдущий тег T с нужным нам тегом 					if ($next[$#next-1][0] eq 'T') 					{ 						# print	$next[$#next-1][1] . "\n";  						push @{$tmp_conf->{cats}}, $next[$#next-1][1];   						$found = 1; 					} 				} 			}  			if (!$found) 			{ 				# возращаемся на исходную позицию мы не нашли категории 				$p->unget_token(@next); 			}  			push @data, $tmp_conf;  		}   	} #	print Dumper ($p);	 	 	return \@data; }   return 1; 

Результат

$VAR1 = [           {             'cats' => [                         'Исследования и прогнозы в IT',                         'Будущее здесь'                       ],             'href' => 'http://habrahabr.ru/post/162053/#habracut'           },           {             'cats' => [                         'Фототехника',                         'Будущее здесь'                       ],             'href' => 'http://habrahabr.ru/post/163433/#habracut'           },           {             'cats' => [                         'Электроника для начинающих',                         'Гаджеты. Устройства для гиков',                         'Будущее здесь'                       ],             'href' => 'http://habrahabr.ru/post/163493/#habracut'           },           {             'cats' => [                         'HTML',                         'CSS'                       ],             'href' => 'http://habrahabr.ru/post/163429/#habracut'           },           {             'cats' => [                         'Железо',                         'Блог компании Intel'                       ],             'href' => 'http://habrahabr.ru/company/intel/blog/162293/#habracut'           },           {             'cats' => [                         'Хабрахабр — Анонсы',                         'Фриланс',                         'Блог компании Тематические Медиа'                       ],             'href' => 'http://habrahabr.ru/company/tm/blog/163483/#habracut'           },           {             'cats' => [                         'Веб-разработка',                         'Open source'                       ],             'href' => 'http://habrahabr.ru/post/163425/#habracut'           },           {             'cats' => [                         'Переводы',                         'Операционные системы',                         'Open source'                       ],             'href' => 'http://habrahabr.ru/post/148911/#habracut'           },           {             'cats' => [                         'Программирование'                       ],             'href' => 'http://habrahabr.ru/post/163445/#habracut'           },           {             'cats' => [                         'Работа со звуком',                         'Ненормальное программирование'                       ],             'href' => 'http://habrahabr.ru/post/163525/#habracut'           }         ];  

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

При таком подходе как в HTML::TokeParser не сохраняется информация о вложенности, поэтому как вариант можно использовать массив с токенами и unget_token().

ссылка на оригинал статьи http://habrahabr.ru/post/163567/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *