Одним из наиболее часто используемых мною модулем при парсинге 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*buttons+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*buttons+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*buttons+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().
Автор: kshiian