Pull to refresh

HTML::TokeParser

Reading time 5 min
Views 12K
Одним из наиболее часто используемых мною модулем при парсинге 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().
Tags:
Hubs:
+4
Comments 15
Comments Comments 15

Articles