Давайте рассмотрим какой-либо пример на практике. Возьмем сайт 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/
Добавить комментарий