The OpenNET Project / Index page

[ новости /+++ | форум | теги | ]

Разработка Perl скрипта для разбора web-страниц (perl tree web)


<< Предыдущая ИНДЕКС Исправить src / Печать Следующая >>
Ключевые слова: perl, tree, web,  (найти похожие документы)
From: Арсений Чеботарев Date: Mon, 16 Dec 2007 14:31:37 +0000 (UTC) Subject: Разработка Perl скрипта для разбора web-страниц Оригинал: http://www.cpp.com.ua/?in=kpp_show_article&kpp_art_ID=568 Прелюдия Данный текст - это моя запоздалая реакция на несколько писем, пришедших в разное время, в которых встречались такие ключевые слова, как "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 "$1\n" 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 "$param\n"; } 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 "$1\n" 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("http://aviso.ua/a/Rb.aspx")=~/i=(-?\d+)\s/; %used={}; sub linx { my ($nId,$list,$spacer)=@_; my $content=$obj->get("http://aviso.ua/a/Tr.aspx?Id=${nId}&i=${sid}"); while ($content=~m/class="(Collapse|Leaf)".+(Id|r)=(\d+)\&amp.+">(.+)<\/a>/g) { $type,$id,$title)=($1,$3,$4); if (not exists($used{$id})) { $title=~s/\&nbsp;/ /g; $title=win2utf($title); my @child=(); if ($type=~/Collapse/) { push(@$list,{Id=>$id,Title=>$title,Type=>"+",Sublist=>\@child}); $used{$id}=1; print "${spacer}<node type=Tr id=$id title=\"$title\">\n"; linx($id,\@child,"$spacer\t"); print "${spacer}</node>\n"; } else { push(@$list,{Id=>$id,Title=>$title,Type=>"-",Sublist=>\@child}); print "${spacer}<node type=Cn id=$id title=\"$title\">\n"; print "${spacer}\tpages "; lines($id,\@child,1,"$spacer\t"); print "\n"; print "${spacer}</node>\n"; } } } } sub lines { my ($nId,$notes,$pageNo,$spacer)=@_; print "${pageNo} "; my $content=win2utf($obj->get("http://aviso.ua/a/Cn.aspx?r=${nId}&i=$ {sid}&pg=${pageNo}")); # while (...) push (@notes,...); $pageNo++; if ($content=~m/<a class=\"Page\" href=\"Cn.aspx\?r=${nId}\&amp;i=${sid}\&amp;pg=${pageNo}\">/){ lines($nId,$notes,$pageNo,$spacer); } } @bigtree; linx("Top",\@$bigtree,""); store(\@bigtree); Я не использую здесь какие-то специальные средства для разбора html - не потому, что мне лень набрать LinkExtor, например, а потому, что это дольше, но не удобнее. Лучше обратите внимание на некоторые особенности синтаксиса, вроде присвоения "списком", передачи параметров в виде списка @_ и т.д. В остальном программа не слишком сложна - только не пытайтесь представить получаемое пространство с количеством измерений около шести :). Действительно, эту программу легче написать, чем представить, что она делает. Вкратце: на каждой странице ищутся ссылки особого типа. Мы перебираем все вхождения regexp с помощью модификатора g. Вообще, один раз задействовав механизм regexp, старайтесь нагрузить его посильнее - он выдержит. Естественно, при этом нужно избегать алгоритмов, известных как "вешалки regexp", но мы не будем вдаваться в такие детали. Для каждой записи типа Tr закачивается дочерняя страница и т.д. Само "прохождение" не было бы таким сложным, если бы мы по ходу не строили рекурсивный список хешей. Узлу соответствует запись (хеш) с перечнем данных, таких как Id и Title. Поле "дочерние элементы" является указателем на новый список и т.д. Загрузка Cn-узла представлена только "болванкой", чтобы не превращать эту программу в коммерческий продукт. Как видите, контрольный вывод представлен в XML-виде. В генерации XML нет никаких проблем: немного сложнее разбор, верификация и модификация. К сожалению, формат не позволяет нам как следует "проехаться" по этой теме, можно только сказать, что Perl не изобретает в этой сфере ничего нового. Существуют две-три книги по сабжу "Perl и XML", в частности у O'Reilly ( www.oreilly.com/catalog/perlxml - возможно уже есть и перевод). Базы данных? Это возможно... но здесь - не нужно Итак, наша программа составляет дерево в памяти, соответствующее классификатору AVISO. Записывать это дерево в виде плоской базы данной - варварство и непроизводительная трата килокалорий. Ни одна плоская база не представит наше дерево в таком красивом виде, как наш список хешей. Вообще-то, в Perl есть, минимум, четыре способа "заюзать" базы данных (естественно, под базой данных можно понимать любой текстовый или бинарный файл, так что метод номер раз уже есть). Второй, самый штатный метод доступа: использовать DBI - модуль, сопоставляющий (привязывающий) с помощью tie() файл базы данных к виртуальному хешу. Впрочем, насколько этот хеш виртуален - тяжело судить, мы ведь не знаем, что такое "не виртуальный" хеш, реализация остается за областью нашего понимания. Существует четыре реализации таких "хеш-баз", из которых на Linux самая популярная - Berkeley DB. У этого метода есть все ограничения, присущие хешам. То есть метод хорош, когда данные легко представляются в виде ключ=>значение. Третий метод - это SQL базы данных. Поскольку в этом методе нет ничего Perl-специфичного, то мы его тоже пропустим. Вместо всего перечисленного используем устойчивые структуры: просто сохраним наш список со всеми вложенными структурами. А для этого используем хак-модуль Storable. "Хак" - потому что это не Perl-модуль, он написан на C и использует внутренние двоичные представления наших структур. Для того чтобы сохранить всю структуру как целое нужно добавить всего две строки (представьте, как бы это выглядело на SQL...): вначале - use Storable;, а в самом конце программы - store(\@bigtree). Все! Не зря, к примеру, Sun так ратует за хранение объектных структур Java в противовес всем остальным методам хранения. Конечно, нужно смотреть в корень - но даже если мы храним миллион записей по 100 байт, то это "всего" 100 Мб, плюс накладные расходы (ну, о'кэй - 150 Мб). Не так уж и много, особенно если учесть, что недостающая память всегда может быть позаимствована из виртуальной памяти. Если вам нужна база данных с мгновенной реакцией, то такой вариант будет работать стремительно - к тому же вы можете "достраивать" деревья и дополнительные хеши для более быстрого поиска по "вторичным ключам". В конце концов, простой бесперебойник UPS (который по-любому должен стоять на сервере) может довести аптайм вашей базы до 99,99%. Другая программа будет делать @bigtree=@{retrieve("aviso.dat")}; и использовать полученные данные для просмотра нашей базы. Почему другая? По логике база AVISO меняется раза два в неделю, закачивается в консольном приложении - и, вероятно, наша программа будет стоять на каком-то сервере, все время подключенном к сети, на котором даже не установлен X-Windows. Просмотр же будет производиться на пользовательских станциях, с фронт-эндом в Perl-Gtk, причем средство просмотра будет запускаться любым количеством пользователей в асинхронном режиме (плюс функции retreive - файл данных практически не блокируется). Так что это по определению другая программа. Вы реально сэкономите трафик и свое время, закачивая данные раз в неделю. Для ориентира: закачка всей базы AVISO дает примерно 30 Мб трафика в сети и такой же файл на диске - и там, и там есть накладные расходы. В результате В результате: нашей цели ("рассказать обо всем Perl") мы не достигли. Остались за бортом (из существенного) указатели на функции, привязки (tie-s), файловый ввод-вывод, пользовательские интерфейсы и еще кое-что. Но для тех, кто разобрался в исходниках, все остальное уже не будет составлять проблем. Кстати, Александр Македонский тоже не завоевал весь мир, как ему хотелось. Также, как и Володя Ульянов не построил свой левый коммунизм. Но пытаться достичь недостижимого периодически все-таки нужно - чего и вам желаю. Автор: Арсений Чеботарев

<< Предыдущая ИНДЕКС Исправить src / Печать Следующая >>

 Добавить комментарий
Имя:
E-Mail:
Заголовок:
Текст:




Партнёры:
PostgresPro
Inferno Solutions
Hosting by Hoster.ru
Хостинг:

Закладки на сайте
Проследить за страницей
Created 1996-2024 by Maxim Chirkov
Добавить, Поддержать, Вебмастеру