Данный текст - это моя запоздалая реакция на несколько писем,
пришедших в разное время, в которых встречались такие ключевые слова,
как "Perl", "ботик", "html", "закачать порнуху", "какого хрена" и
"зарание спасиба".
Когда-то я написал статейку "Безполезный Perl и общая теория улучшения
мира", где речь шла об очень специфическом "движке" на Perl, который
помогал разобраться с ассемблерным кодом на примере Spedia.exe. В
результате один читатель попенял мне - мол, "я не собираюсь ковыряться
в ассемблере, мне нужно HTML разбирать, рассказывайте о чем нужно
людям". Ну, прямо скажем, люди бывают разные, и каждому нужно свое -
но да, согласен, разбирать HTML приходится чаще.
Другая статейка рассказывала о таком очень интересном (по крайней
мере, мне) языке программирования, как NQL - Network Query Language -
специально созданном для написания сетевых ботиков. Но поскольку с
производителем этой милой софтины что-то не сложилось, сайт "разлезся"
по разным порталам и найти закачку стало сложно, то несколько человек
обращались с просьбой скинуть им инсталяшку. Я, конечно, только за -
но чувствую, что то же самое, то есть "веб-краулинг" нужно показать и
на более доступном языке.
Последней каплей стало письмо одного моего американского друга,
который так и пишет "я понимаю в Perl все, кроме хешей, регулярных
выражений, работы с HTML и баз данных" :). Ну, с американцев спрос
невелик - но, с другой стороны, там же живут и самые "гикнутые
эггхеды", так что одним аршином родину индейцев не измерить. Для той
ее части, которая "понимает в Perl все, кроме хешей", и написана эта
статью.
Чего будем делать?
Делать будем следующее: писать программку, которая разбирает дерево
веб-страниц, общее количество которых неизвестно, собирать на
страничках данные и записывать их в локальный файл (базу данных). При
этом постараемся задействовать весь Perl, так чтобы оказались
использованными: регулярные выражения, хеши, в том числе и "хеши
хешей" или "хеши массивов", рекурсия и объекты Perl, а также обычные
модули и пакеты. Из библиотек: запросы и получение данных по http,
базы данных, XML. Ничего не пропустили?
Тогда поехали.
Немного о разном
В качестве объекта для закачки возьмем традиционную онлайн-газетку
"AVISO-Киев". Всем хороша газетка :кроме своего сайта. То ли
веб-мастер ленивый, то ли они клики накручивают, то ли баннера - но
только не предусмотрено на этом сайте никакого поиска. Вдумайтесь: на
сайте с тысячами объявлений просто непонятно почему, но нет поиска!
Люди просто вынуждают нас писать программку для разбора их контента.
Пропустим ту часть, где я щелкаю правой кнопкой мыши и получаю
страницу с html-текстом главной странички. Скажу только, что сайт
построен из нескольких вложенных фреймов, в которые вложены другие
фреймы. Что вы еще заметите, это десятизначный номер вашей сессии в
строке URL (&i=-xxxxxxxxxx), под которым вы ходите по сайту. Номер
сессии представляет собой "отбалденное" число, которое вы получаете
при визите на сайт. Будем считать, что вы крутите свой даун-бот на том
же компе, на котором у вас есть браузер, так что не будем особо
заниматься тем, как вы получаете номер сессии в первый раз - просто
будем считать, что вы уже побывали на этом сайте и получили этот
"волшебный пирожок".
Последняя страничка, которую вы можете получить без этого числа:
aviso.ua/a/Rb.aspx - и там это же число встречается впервые. Для
начала приведем небольшой код, который просто закачивает эту страничку
и выводит на экран номер сессии - на этом примере мы научимся
закачивать все, что нам нужно, из Сети - и первый раз встретим в нашей
программке регулярное выражение.
01 #!/usr/bin/perl
02 use LWP::UserAgent;
03 use HTTP::Request;
04 use HTTP::Response;
05 my $ua=LWP::UserAgent->new();
06 my $url="http://aviso.ua/a/Rb.aspx";
07 $ua->agent("Aviso Grabber/5.12 Professional");
08 my $req=HTTP::Request->new(GET=>$url);
09 $req->referer("http://aviso.ua");
10 my $resp=$ua->request($req);
11 print "$1n" if $resp->content()=~/i=(-?d+)s/;
Разберем этот текст подробнее. Первая строка указывает, где искать
интерпретатор Perl. Вообще говоря, это не обязательно указывать -
можно просто в командной строке писать perl < program-name >. Но если
вы хотите вызывать программы Perl как исполнительные файлы, то должны
включить эту строку. Это нужно только под *nix - под MS Windows
ассоциации работают через записи в реестре. Не забудьте задать бит
"исполнимый" для своего файла: chmod +x < programname >. Если perl
расположен в другом месте, найти его поможет команда which perl.
Следующие три строки - подключение библиотек. LWP, собственно,
обозначает libwww-perl, которая включает средства для работы с http. В
частности, мы импортируем в наше адресное пространство LWP::UserAgent.
Два других импорта из модуля HTTP позволяют создавать запросы к
серверу и анализировать его ответы.
С пятой строки мы стремительно катимся к счастью: создаем объект типа
"агент", устанавливаем для него поле "подписи" agent - это поле
останется в логах на сервере (как видите, мы честно сообщаем, что это
наша программа, а не какой-то "Нетшкаф 9"), формируем новый запрос с
парой значений GET и наш URL. Здесь, в строке 8, вы видите "перловскую
дичку" - оператор =>. На самом деле то, что вы видите,- хеш-массив,
сопоставляющий тексту GET наше урло. Фактически оператор => заменяет
запятую и кавычки вместе взятые. То же самое можно записать так:
('GET', $url).
Обратите внимание на слово my - так обозначаются локальные переменные
(лексические, в терминах Perl). На всякий случай не создавайте без
особой необходимости глобальные переменные, обозначайте все переменные
как локальные, чтобы они не "сцепились" с какими-то модулями. Хотя в
Perl "не создавать глобальные" значит "создавайте локальные",
поскольку все, что не отмечено my, само по себе становится глобальным
- даже если переменная определена внутри функции.
Хеши, как вы знаете, сопоставляют пары "ключ-значение", так что в
данном случае это значит "GET указывает на значение $url". На самом
деле в Perl хеши широко используются для реализации того, что в других
языках соответствует понятию "запись": вы просто добавляете в хеш
различные пары "имя поля - атрибут" и потом всегда можете получить эти
значение обратно. Конечно, это медленнее, чем записи в C, но зато куда
более гибко. Таким образом обычно передаются неформальные списки
необязательных параметров: ваша подпрограмма может проанализировать
наличие того или другого поля и использовать значение по умолчанию,
если параметр не передан.
В строке 9 мы задаем поле http-запроса, которое указывает, с какого
сайта пришел пользователь на данный ресурс (браузер должен "сдавать"
такую инфу серверу). Некоторые "шифровщики" шифруются, не позволяя
закачивать контент, если вы переходите к нему не с формы-запроса на
том же сайте, поэтому поставим тут тот самый домен, по которому и
будем "свинячиться".
Строка 10 делает то, чего мы от нее просим, строка 11 выводит ризалт.
Кстати, эта строка представляет интерес для всех, кто изучает
странности Perl. Во-первых, обратите внимание, что оператор print
следует перед if, который, собственно, выполняется первым и поставляет
значение переменной $1. Это, как говорится, исторически сложившийся
синтаксис - и поэтому любой "перловод" старается писать именно так,
чтобы в нем признали своего в доску.
Во-вторых, обратите внимание на оператор print: в Perl, в отличие от
других языков, двойные кавычки - не простое украшение, а оператор
форматированного вывода, наподобие sprintf(). В данном случае в строку
подставляется значение переменной $1. Это разновидность встроенных
переменных, в данном случае она обозначает "первый бэклог", то есть
первое выражение в скобках, найденное при разборе регулярного
выражения. Еще существует несколько таких переменных, вроде $', $& или
$+, значат они разное - позже разберемся.
Также обратите внимание, что все переменные начинаются с знака $ - это
признак скалярного значения (то есть "одно значение", в отличие,
скажем, от массивов или хешей). Собственно, это не было бы так нужно,
если бы не "кастинг" векторных типов к скалярным, о чем мы еще
поговорим.
Само регулярное выражение - в конце. Обратите внимание на операцию =~
- это выражение возвращает логическое значение, обозначающее "строка
полностью удовлетворила регулярному выражению". А вот что именно
совпало, это можно узнать только косвенно, по таким переменным, как
$1, $2 и т.д. - этим переменным присваиваются подстроки, заключенные в
regexp'е в круглые скобки. Переменная $& обозначает все совпавшее
выражение целиком; $+ - последний совпавший "бэклог", полезно в
выражениях, когда не понятно, что именно совпало: =~/(одно)|(другое)/.
В нашем случае regexp читается так: 'i=' (i=), необязательный знак
минус (-?), одна или более цифр (d+), и пробел, точнее - пробельный
символ, может быть и табуляция (w). Минус и цифры заключены в скобки,
так что к этому участку можно будет обратиться как к $1 - если,
конечно, весь оператор =~ закончится успешно для if.
Об объектах в Perl
В данный момент мы имеем замечательную, хотя и короткую, программу.
Главное ее достоинство в том, что она вообще работает. По горячим
следам, пока наш код нас не сожрал (а с кодом Perl это элементарно)
займемся рефакторингом - то есть сразу же упакуем наш код так, чтобы в
дальнейшем не видеть того, что уже работает, и концентрироваться лишь
на неработающих вещах.
Хотя объектная модель Perl не относится к врожденным свойствам этого
языка, тем не менее и она имеет своих поклонников. По крайней мере,
как вы могли убедиться, большинство библиотек, таких как LWP,
предоставляют свои сервисы как набор классов, так что и мы пойдем этим
путем.
Вообще-то, Perl содержит немало средств для инкапсуляции кода:
библиотеки, вызываемые оператором do, а также модули, подключаемые на
этапе компиляции оператором use или на этапе выполнения вызовом
require. К тому же в одном модуле может быть несколько пакетов - пакет
связан скорее не с файлом, как модуль, а с областью видимости
объектов. В результате вы всегда можете впасть "в детство" и
программировать так, как это делали пять лет назад. Как сказал
создатель Perl Ларри Уолл, "существует несколько здоровых субкультур
Perl". Мы не будет анализировать все варианты, рассмотрим только самые
хронологически последние - и, вероятно, самые совершенные.
Итак - объекты. Объектов в том смысле, в каком они существуют в C++, в
Perl не было никогда. И уже никогда не будет. Объект, в терминах
Perl,- это только маркер, помечающий нечто как объект. Сама "метка" -
или приведение к данному типу - вызывается оператором bless. Нечто,
что вы будете приводить - это обычно хеш. В результате приведения в
хеше окажется список полей объекта, а также список указателей на
методы. Вы правильно поняли: объект - это хеш, ключи - имена полей и
методов, а значения - это значения полей и указатели на методы. Таким
образом Perl смог стать "объектным" между делом, не меняя основного
синтаксиса, с помощью такого остроумного "хода лошадью".
Вот как выглядит "образцовый" конструктор:
sub new {
my $class=shift;
my $self={};
bless($self,$class);
$self->{NEW_FIELD}=0;
. . .
return $self;
}
Как видите - получили невидимый "следующий параметр" с помощью
оператора shift (при вызове конструктора имя текущего пакета
передается неявно как первый параметр, в смысле области видимости
пакет и класс одно и то же), создали хеш, дополнили его полями и
вернули созданный хеш. На самом деле в приведенном выше примере "много
текста" - тривиальный класс записывается так:
sub new{bless({},shift)}
Обратите внимание: мы возвращаем локальную переменную хеша - но она не
будет удалена после выхода из тела конструктора. Это потому, что
локальные переменные в Perl не являются автоматическими и не
освобождаются, если на них есть хоть одна ссылка. А такая ссылка в
данном случае остается даже после завершения процедуры.
Вообще-то, если честно, тяжело говорить о классах в Perl, лучше бы
называть это "фабрикой объектов", поскольку этот вот new() и является
"описанием" класса. Естественно, что ваш класс должен что-то
"домазать" в хеш, чтобы ваши объекты отличались от того, от чего они
происходят. Заметьте, что new() в Perl вызывается до распределения
памяти, в то время как в C++ - после. В этом смысле "конструктором"
следовало бы назвать именно new() в Perl, а первый вызываемый для
объекта метод в C++ - пост-инициатором ("пост" - поскольку объект в
основном уже инициирован до вызова "конструктора C++).
Как следствие такого "полиморфизма" классов и экземпляров, в Perl не
существует статических методов (класс - это тоже экземпляр), а также
частных методов и полей - в хеше все элементы равны. Впрочем, для
экземпляров первый параметр указывает на self, так что можно это
проверить:
sub {
my $self=shift;
die "non-static method called on class" unless ref $self;
. . .
}
Кроме того (что уже хуже), поскольку поиск методов и данных
производится "очень поздно", то до момента выполнения программы вы не
получите ошибки при обращении к неверному методу или данным - и в
некоторых случаях вы не сможете проверить этот факт до возникновения
ошибки.
Теперь создадим наш первый (но не последний) модуль, содержащий объект
"закачай по сети". Хотя модуль, пакет и объект-прототип могут
варьироваться как угодно, но люди педантичные следят за тем, чтобы это
было одним и тем же - то есть в один файл кладут одно описание
объекта. Так поступим и мы (хотя с одним объектом сложно поступить
иначе):
package GetAviso;
use LWP::UserAgent;
use HTTP::Request;
use HTTP::Response;
sub new {
my $classname=shift;
my $self={};
bless($self,$classname);
$self->{ua}=LWP::UserAgent->new();
$self->{ua}->agent("Aviso Grabber/5.12 Professional");
return $self;
}
sub test {
my $self=shift;
$param=shift;
return "$paramn";
}
sub get {
my $self=shift;
$url=shift;
my $req=HTTP::Request->new(GET=>$url);
$req->referer("http://aviso.ua");
return $self->{ua}->request($req)->content();
}
1;
После такого определения наша программа принимает следующий вид:
#!/usr/bin/perl
use GetAviso;
my $obj=GetAviso->new();
$url="http://aviso.ua/a/Rb.aspx";
print $obj->test($url);
print "$1n" if $obj->get($url)=~/i=(-?d+)a/;
Тут стоит остановиться для нескольких комментариев.
Во-первых, наш модуль больше не исполняемый файл. Из этого три
следствия: не нужна первая строка интерпретатора, бит исполнимого
файла можно опустить и, наконец, имя файла должно соответствовать
соглашениям об именовании модулей. В данном случае, если файл лежит в
том же каталоге, что и приложение, то его имя должно быть GetAviso.pm
(расширение обозначает Perl Module).
Далее: единичка в конце модуля - не опечатка, а "так надо".
Исторически Perl происходит от калькулятора, введя в который
последовательность 2+2< Enter >, вы получали 4. Если ввести 1 - то и
получите 1, так что строка с одной единичкой возвращает ее как
значение. В конце модуля исторически располагается блок инициализации
(вы тоже можете это использовать), который должен вернуть True как
символ того, что инициализация закончилась без проблем,- иначе
возникнет прерывание. А поскольку True в Perl - почти все что угодно
кроме того, что явно False (ноль и т.д.), то можете вместо единицы
записать любое "истинное" выражение, например "Эт0 СуппЕР Модуль";.
Традиционно там единичка.
И третье: не забывайте, что первый < shift > поставит в метод класса
имя пакета, а в экземпляр - указатель self. Это демонстрирует метод
test(). В остальном этот класс не слишком универсален и не очень
сложен - но, например, кэширует экземпляр типа UserAgent в собственном
self-хеше, чтобы не создавать его каждый раз. Главное, что он
действительно скрывает много деталей http-запроса и, кроме того,
демонстрирует технику создания собственных объектов, как и было
обещано.
Для того, чтобы окончательно "завязать" тему всяческих модулей и
пакетов, приведу канонический "древний" модуль, который нам тоже скоро
пригодится:
package win2utf;
use Exporter;
use Text::Iconv;
@ISA=('Exporter');
@EXPORT=qw(&win2utf);
sub win2utf {
$inline=shift;
$conv=Text::Iconv->new("windows-1251","utf-8");
return $conv->convert($inline);
}
1;
Делает он следующее - перекодирует символы из win-1251 в utf-8, так
чтобы я мог видеть строки на своей Unicode-консоли. Для этого
используется опциональный модуль Text::Iconv. Вообще-то, под Linux
утилита и интерфейс iconv существуют как штатное средство, а вот
модуль для Perl вам, возможно, придется закачать.
Большего внимания заслуживает то, как наш модуль экспортирует
интерфейс через Exporter (ISA обозначает "is a...", "некий..."). Когда
нам что-то понадобится в нашем пакете, будет вызван win2utf->import().
У нас такого, естественно, нет - так что поиск будет продолжен в
Exporter, а тот уж подсуетит создать для имен из списка @EXPORT
псевдонимы в том адресном пространстве, где мы будем их использовать.
В результате сможем вызывать win2utf без указания имени пакета в
качестве префикса.
Опять к хешам - на этот раз (очень) рекурсивным
Поскольку мы уже зашли в освоении Perl далеко, теперь мои комментарии
будут уменьшаться - а исходники усложняться. Если вы посмотрите на
htmp странички aviso.ua/a/Tr.aspx&i=..., то увидите там несколько
разделов, в которых обнаружите подразделы - и так далее, до тех пор
пока не доберемся к ссылкам на страницы с самими объявлениями.
Поскольку странички-каталоги называются Tr.aspx, а страницы с данными
- Cn.aspx, то и мы будем говорить о Tr-узлах нашего графа и Cn-узлах
(в программе они обозначаются знаками "+" и "-", как принято при
отображении раскрываемых и не раскрываемых узлов).
Для Tr-узлов характерны два атрибута: строка-описание на русском и
идентификатор Id=. Для страничек с данными идентификатор называется
r=, но мы его тоже будем называть ID. Важно: с каждым узлом связаны
другие узлы (массив). Для Cn-узлов также имеет смысл количество
страниц, на которых располагается данный Cn.
В результате код для рекурсивного разбора нашего дерева можно записать
так:
#!/usr/bin/perl
use Storable; use win2utf;
use GetAviso; $obj=GetAviso->new();
$sid=$1 if $obj->get("
1308 Прочтений • [Разработка Perl скрипта для разбора web-страниц (perl tree web)] [08.05.2012] [Комментариев: 0]