Обработка HTML кода на Perl, HTML::TagReader

ArticleCategory: [Choose a category, do not translate this]

Webdesign

AuthorImage:[Here we need a little image from you]

[Photo of the Author]

TranslationInfo:[Author + translation history. mailto: or http://homepage]

original in en Guido Socher

en to ru Pukhlyakov Kirill 

AboutTheAuthor:[A small biography about the author]

Guido нравится Perl за его мощь и скорость. Девиз Perl "Есть более одного способа сделать это" - полностью отражает возможности тех, кто пользуется свободно распространяемым программным обеспечением.

Abstract:[Here you write a little summary]

Если ваш сайт достаточно объемен, то рано или поздно приходит мысль об автоматизации управления им, то есть о поиске какого-либо инструмента или приложения.
Большинство приложений работают с файлами построчно или посимвольно. К сожалению строки не имеют никакого смысла в SGML/XML/HTML файлах. Эти файлы основаны на тэгах. Модуль, о котором пойдет речь в этой заметке, обрабатывает файлы по тэгам.

Надеюсь, что вы знакомы с Perl достаточно хорошо. В любом случае советую взглянуть на мои предыдущие заметки об этом языке (Январь 2000).

ArticleIllustration:[This is the title picture for your article]

[Illustration]

ArticleBody:[The article body]

Вступление

Обычно файлы имеют строковую структуру - например конфигурационные файлы Unix - /etc/hosts, /etc/passwd ... Даже есть старые ОС, в которых вы можете найти функции для построчного чтения/записи данных.
Интересующие нас SGML/XML/HTML файлы основаны на тэгах, строки не имеют никакого значения в них, но текстовые редакторы и люди до сих пор ориентируются на строки.

Например большие HTML файлы, возвращаемые сервером могут иметь большую длину строк и тут мы можем сделать их более читаемыми например таким инструментом как "Tidy". Мы используем строковую структуру, несмотря на то, что HTML основан на тэгах. Вы можете сравнить это с С-кодом - теоретически можно писать программу в одну строку, но так никто не делает - код будет нечитаемым.
Но с другой стороны любой инструмент для проверки HTML кода выдаст вам сообщение об ошибке на основе отсчета строк, но не тэгов - вы наверное не видели никогда такое сообщение как "ОШИБКА после тэга 4123". Это потому, что ваш текстовый редактор легко перейдет на заданную вами строку.

Идеальным инструментом был бы тот, который обрабатывает HTML файл тэг за тэгом и при этом запоминает еще номера строк.

Возможное решение

Обычно для чтения файла в Perl используют оператор while(<FILEHANDLE>). В результате файл будет читаться построчно и очередная строка будет помещена в переменную $_. Почему так делает Perl? Дело в том, что в Perl есть служебная переменная INPUT_RECORD_SEPARATOR ($RS или $/), в которой определяется символ "\n" как конец строки. Если вы определите $/=">", то Perl будет считать ">" концом строки. Следующий однострочный Perl скрипт преобразует html текст таким образом, что каждая строка будет заканчиваться на ">":

perl -ne 'sub BEGIN{$/=">";} s/\s+/ /g; print "$_\n";' file.html

такой html файл

<html><p>some text here</p></html>
преобразуется к такому виду
<html>
<p>
some text here</p>
</html>
Но это по-прежнему не очень читаемо. Для разработчика важно, чтобы данные передавались тэг за тэгом. Чтобы, например, можно было легко найти "<a href= ..." даже если "a" и "href" расположены на разных строках.

Замена переменной "$/" (INPUT_RECORD_SEPARATOR) не замедлит работу приложения. Также можно воспользоваться регулярными выражениями - это немного сложнее и может привести к замедлению приложения, но этот способ широко используется.

В чем проблема?? В заголовке заметки говорится что-то про HTML::TagReader, но я почему-то до сих пор говорю о каких-то других способах без привлечения дополнительных модулей. Посмотрим где тут может возникнуть проблема: Так что делаем вывод, что только в некоторых случаях можно использовать переменную "$/" (INPUT_RECORD_SEPARATOR).

У меня есть одна программка, которая использует то, что мы уже так долго обсуждаем. В ней меняется "$/" на "<". Браузеры не так хорошо отслеживают некорректный "<", как ">", поэтому гораздо меньше некорректных страниц с "<", чем с ">". Программа называется tr_tagcontentgrep ( посмотреть ), вы можете по коду посмотреть как запоминать номер строки. Это программа может быть использована для "grep'а" строки ( например "img" ) в тэге даже если тэг расположен на разных строках, например:

tr_tagcontentgrep -l img file.html
index.html:53: <IMG src="../images/transpix.gif" alt="">
index.html:257: <IMG SRC="../Logo.gif" width=128 height=53>

HTML::TagReader

HTML::TagReader решает проблему с заменой INPUT_RECORD_SEPARATOR, а также предлагает более красивый способ отделения текста от тэгов. Он не так тяжеловесен как HTML::Parser и предлагает именно то, что надо при обработке html кода - возможность чтения тэг за тэгом.

Достаточно разговоров. Посмотрим как использовать модуль в реальной жизни. Сначала объявляем его:
use HTML::TagReader;
Затем:
my $p=new HTML::TagReader "filename";
чтобы открыть файл и получить объектную ссылку на него в $p. Теперь используя $p->gettag(0) или $p->getbytoken(0) получаем доступ к следующему тэгу. gettag возвращает только названия тэгов, т.е. то, что находится между < и >, а getbytoken также возвращает текст, который присутствует между тэгами и сообщает вам, что это - текст или тэг. Используя эти функции очень легко обрабатывать html файлы, что вообщем и необходимо при поддержке больших сайтов. Полное описание синтаксиса вы можете найти здесь (man страница HTML::TagReader.
Приведем реальный пример использования модуля - выведем заголовки некоторого количества документов:
#!/usr/bin/perl -w
use strict;
use HTML::TagReader;
#
die "USAGE: htmltitle file.html [file2.html...]\n" unless($ARGV[0]);
my $printnow=0;
my ($tagOrText,$tagtype,$linenumber,$column);
#
for my $file (@ARGV){
  my $p=new HTML::TagReader "$file";
  # read the file with getbytoken:
  while(($tagOrText,$tagtype,$linenumber,$column) = $p->getbytoken(0)){
  if ($tagtype eq "title"){
    $printnow=1;
    print "${file}:${linenumber}:${column}: ";
    next;
  }
  next unless($printnow);
  if ($tagtype eq "/title" || $tagtype eq "/head" ){
    $printnow=0;
    print "\n";
    next;
  }
  $tagOrText=~s/\s+/ /; #kill newline, double space and tabs
  print $tagOrText;
  }
}
# vim: set sw=4 ts=4 si et:
Как это работает? Читая html файл тэг за тэгом при помощи $p->getbytoken(0) мы ищем <title> или <Title> или <TITLE> ( $tagtype eq "title" ) и когда находим устанавливаем флаг ( $printnow ) и начинаем выводить заголовок пока не встретим </title>.
Используем следующий вызов программы:

htmltitle file.html somedir/index.html
file.html:4: the cool perl page
somedir/index.html:9: joe's homepage

Конечно можно использовать и tr_tagcontentgrep вместе с HTML::TagReader. Тогда получится немного короче и проще:

#!/usr/bin/perl -w
use HTML::TagReader;
die "USAGE: taggrep.pl searchexpr file.html\n" unless ($ARGV[1]);
my $expression = shift;
my @tag;
for my $file (@ARGV){
  my $p=new HTML::TagReader "$file";
  while(@tag = $p->gettag(0)){
    # $tag[0] is the tag (e.g <a href=...>)
    # $tag[1]=linenumber $tag[2]=column
    if ($tag[0]=~/$expression/io){
      print "$file:$tag[1]:$tag[2]: $tag[0]\n";
    }
  }
}
Скрипт получился достаточно коротким, без избытка обработки ошибок, но он полностью функционален. Чтобы найти тэги с вхождением строки "gif" используем его следующим образом:

taggrep.pl gif file.html
file.html:135:15: <img src="images/2doc.gif" width=34 height=22>
file.html:140:1: <img src="images/tst.gif" height="164" width="173">

Хотите еще пример? Напишем программу, которая вырежет все тэги <font...> и </font> из html файла. Эти тэги ( font ) используются в огромном количестве некачественными приложениями для верстки html кода и потом возникают проблемы при просмотре таких файлов браузерами. Это упрощенная версия программы - она удаляет все тэги font, но вы можете немного усовершенствовать ее и удалять только аттрибуты fontface или size, а color оставлять.
#!/usr/bin/perl -w
use strict;
use HTML::TagReader;
# strip all font tags from html code but leave the rest of the
# code un-changed.
die "USAGE: delfont file.html > newfile.html\n" unless ($ARGV[0]);
my $file = $ARGV[0];
my ($tagOrText,$tagtype,$linenumber,$column);
#
my $p=new HTML::TagReader "$file";
# read the file with getbytoken:
while(($tagOrText,$tagtype,$linenumber,$column) = $p->getbytoken(0)){
  if ($tagtype eq "font" || $tagtype eq "/font"){
    print STDERR "${file}:${linenumber}:${column}: deleting $tagtype\n";
    next;
  }
  print $tagOrText;
}
# vim: set sw=4 ts=4 si et:
Как видите достаточно легко написать полезную программу всего в несколько строк.
В пакете с исходниками HTML::TagReader ( см. ссылки ) приведены несколько примеров использования модуля. tr_xlnk и tr_staticssi полезны при копировании веб-сайта на CD. Веб сервер выдаст вам страницу http://www.linuxfocus.org/index.html даже если вы просто наберете http://www.linuxfocus.org/ ( без index.html ). Если в случае копирования сайта на диск вы просто переносите все файлы и потом обращаетесь к диску напрямую ( file:/mnt/cdrom ) то вы увидите просто список файлов каталога вместо index.html. Это произошло в первый раз при создании _LF_ CD - компания выполнявшая заказ так и сделала и пользоваться диском стало очень неудобно. Теперь они используют tr_xlnk и все ОК.

Уверен, что HTML::TagReader будет полезен для вас. Удачного программирования!

Ссылки