<?xml version='1.0' encoding='UTF-8'?><?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:georss='http://www.georss.org/georss' xmlns:gd='http://schemas.google.com/g/2005' xmlns:thr='http://purl.org/syndication/thread/1.0'><id>tag:blogger.com,1999:blog-964245207178906174</id><updated>2011-08-17T03:43:14.276+04:00</updated><category term='Python'/><category term='функция обратного вызова'/><category term='текст'/><category term='генератор'/><category term='SQL'/><category term='unittest'/><category term='паук'/><category term='юлианская дата'/><category term='AJAX'/><category term='HTML-парсер'/><category term='Text Mining With Perl'/><category term='gzip'/><category term='парсер'/><category term='ООП'/><category term='Эдгар По'/><category term='CGI::Application'/><category term='chardet'/><category term='html5lib'/><category term='конечный автомат. WWW::Mechanize'/><category term='JQuery'/><category term='Text Mining'/><category term='DOM'/><category term='breadcrumbs'/><category term='язык'/><category term='seek'/><category term='тесты'/><category term='время'/><category term='unicode'/><category term='urllib'/><category term='логгинг'/><category term='JSON'/><category term='регулярные выражения'/><category term='парсеры HTML'/><category term='mechanize'/><category term='file-like object'/><category term='краулер'/><category term='urllib2'/><category term='# календарь'/><category term='BeautifulSoup'/><category term='карта сайта'/><category term='httplib'/><category term='сжатие'/><category term='мишка'/><category term='питон'/><category term='рекурсия'/><category term='cookies'/><category term='robots.txt'/><category term='DeprecationWarning'/><category term='календарь'/><category term='struct'/><category term='Perl'/><category term='callback'/><category term='Java'/><category term='трудоустройство'/><category term='StringIO'/><category term='тест'/><category term='minidom'/><category term='YAML'/><category term='cookielib'/><category term='слова'/><category term='socket'/><category term='Sanitizer'/><category term='обработка текста'/><category term='относительные и абсолютные ссылки'/><category term='скрапер'/><category term='HTTPCookieProcessor'/><category term='Django'/><category term='функциональное программирование'/><category term='аутентификация'/><category term='алгоритмы'/><category term='шаблоны проектирования'/><category term='HEAD'/><category term='ссылки'/><category term='астрономия'/><title type='text'>Pi-code</title><subtitle type='html'>Perl, Python и другие языки на букву "P"</subtitle><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://pi-code.blogspot.com/feeds/posts/default'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default?max-results=100'/><link rel='alternate' type='text/html' href='http://pi-code.blogspot.com/'/><link rel='hub' href='http://pubsubhubbub.appspot.com/'/><author><name>Наувул-Наувул</name><uri>http://www.blogger.com/profile/06702180511726415678</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/_3ifHLhKgDjk/TCEZ2cVuxJI/AAAAAAAAB2E/D5uehCRtVtI/S220/IMG_3814.JPG'/></author><generator version='7.00' uri='http://www.blogger.com'>Blogger</generator><openSearch:totalResults>24</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>100</openSearch:itemsPerPage><entry><id>tag:blogger.com,1999:blog-964245207178906174.post-7640376862008203480</id><published>2011-03-13T16:47:00.002+03:00</published><updated>2011-03-15T08:42:47.673+03:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='chardet'/><category scheme='http://www.blogger.com/atom/ns#' term='urllib'/><category scheme='http://www.blogger.com/atom/ns#' term='Perl'/><category scheme='http://www.blogger.com/atom/ns#' term='socket'/><category scheme='http://www.blogger.com/atom/ns#' term='unicode'/><category scheme='http://www.blogger.com/atom/ns#' term='struct'/><category scheme='http://www.blogger.com/atom/ns#' term='Python'/><title type='text'>Миф о продуктивности Python-а</title><content type='html'>Последние 2-3 года моя основная работа связана с Perl-ом и каждая попытка написать на Python-е что-нибудь "для души" заканчивается раздражением. Спотыкаешься там, где, казалось бы, никаких "коряг" быть не должно. Так произошло и в эти выходные. &lt;br /&gt;&lt;br /&gt;Мне понадобилось написать программку, которая разбирает &lt;b&gt;access.log&lt;/b&gt; и раскладывает его поля по таблицам базы данных &lt;b&gt;SQLite&lt;/b&gt;. Вполне тривиальная задачка, кто только ей ни занимался.&lt;br /&gt;&lt;br /&gt;Сам парсер не представляет из себя ничего сложного. Вот пример строки лог-файла:&lt;br /&gt;&lt;code&gt;&lt;br /&gt;89.179.242.83 - - [06/Mar/2011:07:09:48 +0000] "GET /themes/_pebble/handheld.css HTTP/1.1" 200 1020 "http://www.lunarium.ru/2011/02/19/1298158440000.html" "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; ru-ru) AppleWebKit/533.19.4 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4"&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;А вот регулярное выражение, которое прекрасно с ней справляется (я позаимствовал его из &lt;a href="http://seehuhn.de/blog/52"&gt;Johen's blog&lt;/a&gt; :&lt;br /&gt;&lt;code&gt;&lt;br /&gt;parts = [&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;r'(?P&lt;host&gt;\S+)',       # host %h&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;r'\S+',                 # indent %l (unused)&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;r'(?P&lt;user&gt;\S+)',       # user %u&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;r'\[(?P&lt;time&gt;.+)\]',    # time %t&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;r'"(?P&lt;request&gt;.+)"',   # request "%r"&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;r'(?P&lt;status&gt;[0-9]+)',  # status %&amp;gt;s&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;r'(?P&lt;size&gt;\S+)',       # size %b (careful, can be '-')&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;r'"(?P&lt;referer&gt;.*)"',   # referer "%{Referer}i"&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;r'"(?P&lt;agent&gt;.*)"',     # user agent "%{User-agent}i"&lt;br /&gt;]&lt;br /&gt;pattern = re.compile(r'\s+'.join(parts)+r'\s*\Z')&lt;br /&gt;&lt;/agent&gt;&lt;/referer&gt;&lt;/size&gt;&lt;/status&gt;&lt;/request&gt;&lt;/time&gt;&lt;/user&gt;&lt;/host&gt;&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;В одной из таблиц базы данных я решил хранить IP-адреса хостов, с которых приходят запросы. &amp;nbsp;Хранить их удобнее в виде целых чисел, а не 4 значений, разделенных точками. Системные библиотеки прекрасно понимают и тот и другой формат.&lt;br /&gt;&lt;code&gt;&lt;br /&gt;Berkana:~ sergey$ ping 3232235777&lt;br /&gt;PING 3232235777 (192.168.1.1): 56 data bytes&lt;br /&gt;64 bytes from 192.168.1.1: icmp_seq=0 ttl=255 time=6.288 ms&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;В Python-овской бибиотеке &lt;i&gt;urllib&lt;/i&gt; имеется функция&amp;nbsp;&lt;i&gt;inet_aton&lt;/i&gt;, возвращающая упакованное бинарное значение, которое может потребоваться при обмене данными с низкоуровневыми С-библиотеками. &amp;nbsp;Из него можно получить целочисленное значение, для этого его надо распаковать, используя &amp;nbsp;стандартный модуль &lt;i&gt;struct&lt;/i&gt;. Беда в том, что документация к&amp;nbsp;&lt;i&gt;inet_aton&lt;/i&gt; немногословна и мне понадобилось немало времени, чтобы понять, с какими ключами вызывать функцию &lt;i&gt;struct.unpack&lt;/i&gt;&amp;nbsp;для получения нужного результата.&lt;br /&gt;&lt;code&gt;&lt;br /&gt;import socket&lt;br /&gt;import struct&lt;br /&gt;struct.unpack('!L', socket.inet_aton(ip))&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;,&amp;nbsp;где ip представляет собой строку типа: '192.168.1.1'.&lt;br /&gt;&lt;br /&gt;Обратная операция:&lt;br /&gt;&lt;code&gt; socket.inet_ntoa(struct.pack('!L', i))&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;,&amp;nbsp;где i представляет собой целое число.&lt;br /&gt;&lt;br /&gt;Я в курсе, что можно плюнуть на библиотеки и поступить по-простому:&lt;br /&gt;&lt;code&gt;&lt;br /&gt;a &amp;lt;&amp;lt; 24 | b &amp;lt;&amp;lt; 16 | c &amp;lt;&amp;lt; 8 | d &lt;/code&gt;&lt;br /&gt;&lt;br /&gt;, где &lt;b&gt;a&lt;/b&gt;, &lt;b&gt;b&lt;/b&gt;, &lt;b&gt;c&lt;/b&gt;, &lt;b&gt;d&lt;/b&gt;&amp;nbsp;&lt;span class="Apple-style-span" style="font-family: 'Trebuchet MS', 'Lucida Grande', Tahoma, Verdana, Arial, sans-serif; font-size: 13px; line-height: 19px;"&gt;— &amp;nbsp;&lt;/span&gt;части Ip-адреса. Но не стал этого делать из принципа, дабы не плодить сущности.&lt;br /&gt;&lt;br /&gt;Следующая трудность возникла снова на пустом месте. В таблице &lt;b&gt;referer&lt;/b&gt; я решил хранить адреса, с которых посетители заходят на мой сайт. URL-адреса могут быть закодированы (URL-encoded). Типичный пример&amp;nbsp;&lt;span class="Apple-style-span" style="font-family: 'Trebuchet MS', 'Lucida Grande', Tahoma, Verdana, Arial, sans-serif; font-size: 13px; line-height: 19px;"&gt;—&lt;/span&gt;&amp;nbsp;адрес страницы поисковой системы с результатами, в которую включена поисковая фраза:&lt;br /&gt;&lt;code&gt;&lt;br /&gt;http://go.mail.ru/search?q=%E4%EE%EB%E3%EE%F2%E0%20%E4%ED%FF%20%ED%E0%20%EC%E0%F0%F2%202011%E3%EE%E4%E0&amp;amp;rch=e&amp;amp;num=10&amp;amp;sf=90%22&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;Тарабарщина со знаками процентов в данном случае&amp;nbsp;&lt;span class="Apple-style-span" style="font-family: 'Trebuchet MS', 'Lucida Grande', Tahoma, Verdana, Arial, sans-serif; font-size: 13px; line-height: 19px;"&gt;— &lt;/span&gt;ни что иное как фраза&lt;i&gt; "долгота дня на март 2011года"&lt;/i&gt;. Разумеется, хранить удобнее читаемый текст. В Perl-е, чтобы расшифровать закодированную таким методом строку, достаточно импортировать модуль &lt;i&gt;URI::Escape&lt;/i&gt; и вызвать его функцию &lt;i&gt;uri_unescape(&lt;/i&gt;). В Python-е для этой же цели служат&amp;nbsp;&lt;i&gt;urllib.unquote()&lt;/i&gt; и &lt;i&gt;urllib.unquote_plus()&lt;/i&gt;.&lt;br /&gt;&lt;br /&gt;&lt;code&gt;&lt;br /&gt;&amp;gt;&amp;gt;&amp;gt; import urllib&lt;br /&gt;&amp;gt;&amp;gt;&amp;gt; url = "http://go.mail.ru/search?q=%E4%EE%EB%E3%EE%F2%E0%20%E4%ED%FF%20%ED%E0%20%EC%E0%F0%F2%202011%E3%EE%E4%E0&amp;amp;rch=e&amp;amp;num=10&amp;amp;sf=90"&lt;br /&gt;&amp;gt;&amp;gt;&amp;gt; unquoted_url = urllib.unquote_plus(url)&lt;br /&gt;&amp;gt;&amp;gt;&amp;gt; unquoted_url&lt;br /&gt;'http://go.mail.ru/search?q=\xe4\xee\xeb\xe3\xee\xf2\xe0 \xe4\xed\xff \xed\xe0 \xec\xe0\xf0\xf2 2011\xe3\xee\xe4\xe0&amp;amp;rch=e&amp;amp;num=10&amp;amp;sf=90'&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;Поскольку SQLite работает с UTF-8, полученное значение необходимо перевести в unicode.&lt;br /&gt;&lt;code&gt;&lt;br /&gt;&amp;gt;&amp;gt;&amp;gt; unicode(unquoted_url)&lt;br /&gt;Traceback (most recent call last):&lt;br /&gt;File "&lt;stdin&gt;", line 1, in &lt;module&gt;&lt;br /&gt;UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 27: ordinal not in range(128)&lt;br /&gt;&lt;br /&gt;&lt;/module&gt;&lt;/stdin&gt;&lt;/code&gt; &lt;br /&gt;Что случилось? А дело в том, что функция &lt;i&gt;unicode&lt;/i&gt; не знает кодировку исходной строки. Раз запрос исходил из mail.ru, наверняка тут русская windows-кодировка. Так и есть!&lt;br /&gt;&lt;code&gt;&lt;br /&gt;&amp;gt;&amp;gt;&amp;gt; unicode(unquoted_url, encoding='cp1251')&lt;br /&gt;u'http://go.mail.ru/search?q=\u0434\u043e\u043b\u0433\u043e\u0442\u0430 \u0434\u043d\u044f \u043d\u0430 \u043c\u0430\u0440\u0442 2011\u0433\u043e\u0434\u0430&amp;amp;rch=e&amp;amp;num=10&amp;amp;sf=90'&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;Ну, а как быть с другими кодировками? ...Вспоминаю, что для угадывания кодировки текста существует питоновский модуль &lt;i&gt;chardet&lt;/i&gt;. Он считается достаточно надежным, однако для моей цели явно не подходит&amp;nbsp;&lt;span class="Apple-style-span" style="font-family: 'Trebuchet MS', 'Lucida Grande', Tahoma, Verdana, Arial, sans-serif; font-size: 13px; line-height: 19px;"&gt;—&lt;/span&gt;&amp;nbsp;уж слишком часто промахивается. Видимо потому, что исходный текст слишком короток. Пришлось написать вот такой костыль:&lt;br /&gt;&lt;code&gt;&lt;br /&gt;def decode_referer(url):&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;refurl = urllib.unquote_plus(url)&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;try:&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; refurl = unicode(refurl)&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;except UnicodeDecodeError:&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; logger.warn("Could not decode string: '%s'. Trying cp1251..." % refurl)&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; try:&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; refurl = unicode(refurl, encoding='cp1251')&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; logger.info('Success.')&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; except&amp;nbsp;&lt;/code&gt;&lt;span class="Apple-style-span" style="font-family: Monaco; font-size: 12px;"&gt;UnicodeDecodeError&lt;/span&gt;&lt;span class="Apple-style-span" style="font-family: monospace;"&gt;:&lt;/span&gt;&lt;br /&gt;&lt;code&gt; &amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; logger.warn("Could not decode string. Trying to guess encoding...")&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; enc = chardet.detect(refurl)['encoding']&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; logger.warn('Detected %s' % enc)&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; try:&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; refurl = unicode(refurl, encoding=enc)&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; logger.info('Success.')&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; except UnicodeDecodeError:&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; logger.error('Could not decode string. Will store it as is')&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;return refurl&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;...И кто придумал миф о продуктивности Python-а?&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/964245207178906174-7640376862008203480?l=pi-code.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://pi-code.blogspot.com/feeds/7640376862008203480/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=964245207178906174&amp;postID=7640376862008203480' title='Комментарии: 4'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/7640376862008203480'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/7640376862008203480'/><link rel='alternate' type='text/html' href='http://pi-code.blogspot.com/2011/03/python.html' title='Миф о продуктивности Python-а'/><author><name>Наувул-Наувул</name><uri>http://www.blogger.com/profile/06702180511726415678</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/_3ifHLhKgDjk/TCEZ2cVuxJI/AAAAAAAAB2E/D5uehCRtVtI/S220/IMG_3814.JPG'/></author><thr:total>4</thr:total></entry><entry><id>tag:blogger.com,1999:blog-964245207178906174.post-4846917153667937830</id><published>2010-11-19T14:02:00.013+03:00</published><updated>2010-11-19T16:31:31.210+03:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='шаблоны проектирования'/><category scheme='http://www.blogger.com/atom/ns#' term='Perl'/><category scheme='http://www.blogger.com/atom/ns#' term='# календарь'/><category scheme='http://www.blogger.com/atom/ns#' term='юлианская дата'/><category scheme='http://www.blogger.com/atom/ns#' term='ООП'/><title type='text'>Ленивая дата</title><content type='html'>Календарные функции, представленные в &lt;a href="http://pi-code.blogspot.com/2010/11/blog-post_10.html"&gt;предыдущей заметке&lt;/a&gt;, работают, исходя из наивного предположения, что на вход поступают правильные данные. В каких-то случаях ничего плохого не произойдет. К примеру, при вычислении юлианской даты на 29 февраля 1975г (в феврале 1975 было 28 дней), результат будет таким же как для 1 марта:&lt;br /&gt;&lt;blockquote&gt;&lt;pre&gt;$ perl julian.pl 1975 2 29&lt;br /&gt;JD: 2442472.500000&lt;br /&gt;JD at 0h: 2442471.500000&lt;br /&gt;Fri&lt;br /&gt;&lt;br /&gt;$ perl julian.pl 1975 3 1&lt;br /&gt;JD: 2442472.500000&lt;br /&gt;JD at 0h: 2442471.500000&lt;br /&gt;Fri&lt;br /&gt;&lt;/pre&gt;&lt;/blockquote&gt;Хорошим такое API никак не назовешь. Самое время задуматься над созданием удобного для использования класса.&lt;br /&gt;&lt;br /&gt;В одних случаях нужно получать из календарной даты юлианскую, в других -- наоборот.  Поэтому объект должен инициализироваться как из первой, так из второй даты. Если на входе год, месяц и число, то они сохраняются, а юлианская дата неопределена (&lt;i&gt;undef&lt;/i&gt;) и вычисляется &lt;i&gt;лениво&lt;/i&gt;, т.е только при вызове метода &lt;i&gt;djd&lt;/i&gt;. Точно так же, если объект создан с аргументом djd, календарная дата неопределена до тех пор, пока ее не запросили.&lt;br /&gt;&lt;br /&gt;В Perl-е не существует перегрузки методов, следовательно, нельзя создать два разных конструктора: один -- с календарной датой на входе, другой с юлианской. Можно было бы передавать в конструктор &lt;i&gt;new&lt;/i&gt; именованные аргументы, что-то вроде:&lt;br /&gt;&lt;blockquote&gt;&lt;pre&gt;Julian-&gt;new(date =&gt; \%ymd, djd =&gt; $scalar)&lt;br /&gt;&lt;/pre&gt;&lt;/blockquote&gt;Но тогда пришлось бы нагромождать проверки: не заданы ли сразу оба аргумента и какой именно задан. А потом еще проверять правильность либо первого либо второго. Проще создать два "фабричных" метода:&lt;br /&gt;&lt;ul&gt;&lt;li&gt;&lt;pre&gt;new_from_date(year =&gt; $scalar, month =&gt; $scalar, day =&gt; $scalar)&lt;/pre&gt;&lt;/li&gt;    &lt;li&gt;&lt;pre&gt;new_from_djd($djd)&lt;/pre&gt;&lt;/li&gt;&lt;/ul&gt;&lt;br /&gt;Как правило, я стараюсь не использовать модуль &lt;a href="http://search.cpan.org/%7Edrolsky/Params-Validate-0.95/lib/Params/Validate.pm#Callback_Validation"&gt;Params::Validate&lt;/a&gt;, который создан как раз для проверки входных параметров -- уж больно велики накладные расходы, да и ни к чему превращать динамический язык в подобие Java. Но тут как раз тот случай, когда без него не обойтись.&lt;br /&gt;&lt;br /&gt;Начнем с более простого метода &lt;span style="font-style: italic;"&gt;new_from_djd&lt;/span&gt;.&lt;br /&gt;&lt;blockquote&gt;&lt;pre&gt;...&lt;br /&gt;use Params::Validate qw/:all/;&lt;br /&gt;use Readonly;&lt;br /&gt;Readonly our $DJD_TO_JD =&gt; 2415020;&lt;br /&gt;&lt;br /&gt;...&lt;br /&gt;&lt;br /&gt;sub new_from_djd {&lt;br /&gt; my $class = shift;&lt;br /&gt;&lt;br /&gt; validate_pos(&lt;br /&gt;    @_,&lt;br /&gt;    {&lt;br /&gt;        type =&gt; SCALAR,&lt;br /&gt;        regex =&gt; qr/^[-+]?0*(\d+|(?:\d*\।\d+))$/, &lt;br /&gt;        callbacks =&gt; {&lt;br /&gt;            'djd range' =&gt; sub{ $_[0] &gt;= -$DJD_TO_JD }&lt;br /&gt;        }&lt;br /&gt;    }&lt;br /&gt; );&lt;br /&gt;&lt;br /&gt; bless {&lt;br /&gt;     _djd  =&gt; shift,&lt;br /&gt;     _date =&gt; undef,&lt;br /&gt; }, $class&lt;br /&gt;}&lt;br /&gt;...&lt;br /&gt;&lt;/pre&gt;&lt;/blockquote&gt;Поскольку параметры здесь не именованные, а позиционные, для валидации данных используется метод &lt;i&gt;validate_pos&lt;/i&gt; из пакета &lt;span style="font-style: italic;"&gt;Params::Validate&lt;/span&gt;. Имя класса предварительно убрано из списка аргументов ( @_ ). Проверка производится по трем критериям:&lt;br /&gt;&lt;ul&gt;&lt;li&gt;&lt;pre&gt;type =&gt; SCALAR&lt;/pre&gt; аргумент должен быть скаляром&lt;/li&gt;&lt;li&gt;&lt;pre&gt;qr/^[-+]?0*(\d+|(?:\d*\.\d+))$/&lt;/pre&gt; аргумент должен быть целым или десятичным числом, с любым количеством ведущих нулей и с необязательным ведущим  "плюсом" или "минусом"&lt;/li&gt;&lt;li&gt;&lt;pre&gt;'djd range' =&gt; sub{ $_[0] &gt;= -$DJD_TO_JD }&lt;/pre&gt; число не должно быть меньше нулевой стандартной юлианской даты (напомню, что видоизмененная юлианская дата, которую мы используем, больше стандартной на 2415020 суток. Функции обратного вызова (&lt;span style="font-style: italic;"&gt;callbacks&lt;/span&gt;) -- "тяжелая артиллерия" пакет &lt;span style="font-style: italic;"&gt;Params::Validate&lt;/span&gt;. Внутри них можно осуществлять любые проверки. На входе всегда два параметра: проверяемый аргумент и ссылка на все аргументы (хэш или массив).&lt;/li&gt;&lt;br /&gt;&lt;/ul&gt;В конце мы "благословляем" (bless) новорожденный экземпляр с инициализированным атрибутом djd и пустой (до поры до времени) календарной датой.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;Второй метод-фабрика сложнее:&lt;br /&gt;&lt;blockquote&gt;&lt;pre&gt;&lt;br /&gt;...&lt;br /&gt;use Params::Validate qw/:all/;&lt;br /&gt;use Readonly;&lt;br /&gt;&lt;br /&gt;Readonly our $DJD_TO_JD     =&gt; 2415020;&lt;br /&gt;Readonly our $MIN_YEAR      =&gt; -4713;&lt;br /&gt;Readonly our $MAX_YEAR      =&gt;  4713;&lt;br /&gt;&lt;br /&gt;...&lt;br /&gt;&lt;br /&gt;sub new_from_date {&lt;br /&gt; my $class = shift;&lt;br /&gt;&lt;br /&gt; my %args = validate(&lt;br /&gt;     @_,&lt;br /&gt;     {&lt;br /&gt;         year  =&gt; {&lt;br /&gt;             type      =&gt; SCALAR,&lt;br /&gt;             regex     =&gt; qr/^[\-+]?\d{1,4}$/,&lt;br /&gt;             callbacks =&gt; {&lt;br /&gt;                 'year range'    =&gt; sub{&lt;br /&gt;                     $MIN_YEAR &lt;= $_[0] &amp;amp;&amp;amp; $MAX_YEAR &gt;= $_[0]&lt;br /&gt;                 },&lt;br /&gt;                 'non-zero year' =&gt; sub{ 0 != $_[0] }&lt;br /&gt;             }&lt;br /&gt;         },&lt;br /&gt;&lt;br /&gt;         month  =&gt; {&lt;br /&gt;             type      =&gt; SCALAR,&lt;br /&gt;             regex     =&gt; qr/^0*([1-9]|(1[0-2]))$/,&lt;br /&gt;         },&lt;br /&gt;&lt;br /&gt;         day    =&gt; {&lt;br /&gt;             type      =&gt; SCALAR,&lt;br /&gt;             regex     =&gt; qr/^0*([1-9]|([0-2]\d)|(3[0,1]))(\।\d+)?$/,&lt;br /&gt;             callbacks =&gt; {&lt;br /&gt;                    'day range' =&gt; sub{&lt;br /&gt;                        my ($d, $arg) = @_;&lt;br /&gt;                        $d &amp;gt;= 1 &amp;&amp;&lt;br /&gt;                        $d &amp;lt; _days_per_month($arg-&gt;{year}, $arg-&gt;{month}) + 1;&lt;br /&gt;                    }&lt;br /&gt;             }&lt;br /&gt;         },&lt;br /&gt;     }&lt;br /&gt; );&lt;br /&gt;&lt;br /&gt;&lt;br /&gt; bless {&lt;br /&gt;     _djd  =&gt; undef,&lt;br /&gt;     _date =&gt; \%args,&lt;br /&gt; }, $class&lt;br /&gt;}&lt;br /&gt;...&lt;br /&gt;&lt;/pre&gt;&lt;/blockquote&gt;Здесь аргументы именованные, поэтому для их валидации используется метод &lt;i&gt;validate&lt;/i&gt;.&lt;br /&gt;&lt;ul&gt;&lt;li&gt;год проверяется по четырем критериям:&lt;ol&gt;&lt;li&gt;тип аргумента  -- скаляр&lt;/li&gt;&lt;li&gt;аргумент -- положительное или отрицательное целое число&lt;br /&gt;&lt;/li&gt;&lt;li&gt;диапазон: от 4713г. до н.э до 4713г. н.э.&lt;br /&gt;&lt;/li&gt;&lt;li&gt;ноль не допускается&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;br /&gt;&lt;li&gt;У месяца тип должен должен быть скаляром, а диапазон (1-12) проверяется регулярным выражением &lt;pre&gt;qr/^0*([1-9]|(1[0-2]))$/&lt;/pre&gt;&lt;/li&gt;&lt;li&gt;Проверка числа самая сложная.&lt;ol&gt;&lt;li&gt;тип аргумента -- скаляр.&lt;br /&gt;&lt;/li&gt;&lt;li&gt;при помощи регулярного выражения проверяем, что это число, возможно, с десятичными долями; при этом целая часть не выходит за рамки диапазона 1-31.&lt;/li&gt;&lt;li&gt;При окончательной проверке диапазона вызывается внешняя функция &lt;i&gt;_days_per_month&lt;/i&gt; (см. ниже)&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;/ul&gt;В конце мы "благословляем" (bless) новорожденный экземпляр с сохраненными в хэше годом, номером месяца и числом.&lt;br /&gt;&lt;h3&gt;Сколько дней в месяце?&lt;/h3&gt;Количество дней во всех месяцах, кроме февраля, в григорианском календаре неизменно.&lt;br /&gt;Чтобы узнать число дней в феврале, надо определить, является ли год високосным. Если да -- то 29, если нет -- то 28. Для определения "високосности" функция    &lt;i&gt;_leap_year&lt;/i&gt;. Год в григориансмком календаре является високосным, если он кратен 4 и при этом не кратен 100, либо кратен 400 (в отличие от "&lt;span style="font-style: italic;"&gt;старого стиля&lt;/span&gt;", где високосным считался каждый четвертый год).&lt;br /&gt;&lt;blockquote&gt;&lt;pre&gt;# является ли год високосным?&lt;br /&gt;sub _leap_year {&lt;br /&gt;  my $y = shift;&lt;br /&gt;  return 1 if $y % 400 == 0;&lt;br /&gt;  return 0 if $y % 100 == 0;&lt;br /&gt;  return 1 if $y %   4 == 0;&lt;br /&gt;  return 0;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;# количество дней в месяце&lt;br /&gt;sub _days_per_month {&lt;br /&gt;  my ($y, $m) = @_;&lt;br /&gt;  return _leap_year($y) ? 29 : 28 if $m == 2; # февраль&lt;br /&gt;  return 30 if grep{ $m == $_ } (4, 6, 9, 11); # апрель, июнь, сентябрь, ноябрь&lt;br /&gt;  return 31; # остальные месяцы&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;&lt;/blockquote&gt;&lt;h3&gt;Ленивые вычисления&lt;/h3&gt;Два &lt;span style="font-style: italic;"&gt;getter&lt;/span&gt;-а: &lt;span style="font-style: italic;"&gt;djd &lt;/span&gt;и &lt;span style="font-style: italic;"&gt;date &lt;/span&gt;служат для получения юлианской и календарной даты. Если либо одно либо другое не определено (&lt;span style="font-style: italic;"&gt;undef&lt;/span&gt;), оно вычисляется и сохраняется как аттрибут объекта.&lt;br /&gt;&lt;blockquote&gt;&lt;pre&gt;sub djd {&lt;br /&gt;  my $self = shift;&lt;br /&gt;  $self-&gt;{_djd} = _date2djd(%{$self-&gt;{_date}})&lt;br /&gt;      unless defined $self-&gt;{_djd};&lt;br /&gt;  $self-&gt;{_djd}&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;sub date {&lt;br /&gt;  my $self = shift;&lt;br /&gt;  $self-&gt;{_date} = _djd2date($self-&gt;{_djd})&lt;br /&gt;      unless defined $self-&gt;{_date};&lt;br /&gt;  $self-&gt;{_date}&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;&lt;/blockquote&gt;Функции &lt;i&gt;_date2djd&lt;/i&gt; и &lt;i&gt;_djd2date&lt;/i&gt;, которые заняты вычислениями, уже описаны в &lt;a href="http://pi-code.blogspot.com/2010/11/blog-post_10.html"&gt;предыдущей заметке&lt;/a&gt;. Здесь я только добавил к их названиям нижнее подчеркивания в качестве рекомендации не использовать их снаружи.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/964245207178906174-4846917153667937830?l=pi-code.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://pi-code.blogspot.com/feeds/4846917153667937830/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=964245207178906174&amp;postID=4846917153667937830' title='Комментарии: 0'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/4846917153667937830'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/4846917153667937830'/><link rel='alternate' type='text/html' href='http://pi-code.blogspot.com/2010/11/blog-post_19.html' title='Ленивая дата'/><author><name>Наувул-Наувул</name><uri>http://www.blogger.com/profile/06702180511726415678</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/_3ifHLhKgDjk/TCEZ2cVuxJI/AAAAAAAAB2E/D5uehCRtVtI/S220/IMG_3814.JPG'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-964245207178906174.post-7290535543015809951</id><published>2010-11-10T16:00:00.038+03:00</published><updated>2010-11-11T12:14:01.228+03:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='календарь'/><category scheme='http://www.blogger.com/atom/ns#' term='Perl'/><category scheme='http://www.blogger.com/atom/ns#' term='время'/><category scheme='http://www.blogger.com/atom/ns#' term='астрономия'/><category scheme='http://www.blogger.com/atom/ns#' term='юлианская дата'/><title type='text'>Юлианские даты</title><content type='html'>&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://2.bp.blogspot.com/_3ifHLhKgDjk/TNqmIfvQbMI/AAAAAAAAB5M/rfpPQLiwkt4/s1600/calendar.jpg"&gt;&lt;img style="float: left; margin: 0pt 10px 10px 0pt; cursor: pointer; width: 200px; height: 134px;" src="http://2.bp.blogspot.com/_3ifHLhKgDjk/TNqmIfvQbMI/AAAAAAAAB5M/rfpPQLiwkt4/s200/calendar.jpg" alt="" id="BLOGGER_PHOTO_ID_5537921356694318274" border="0" /&gt;&lt;/a&gt;&lt;span style="font-style: italic;"&gt;Гражданский календарь с его високосными годами и реформами крайне неудобен для расчетов. Сколько дней и часов осталось до начала мото-сезона? В какой день недели произошло памятное событие?... Вместо того, чтобы подсчитывать дни в годах и часы в сутках, удобнее пользоваться действительными числами. Я говорю, конечно, не о вычислениях в уме, а о программировании.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;h3&gt;Непрерывный счет дней&lt;/h3&gt;Одним из широко известных решений этой проблемы стало так называемое "&lt;a href="http://ru.wikipedia.org/wiki/UNIX-%D0%B2%D1%80%D0%B5%D0%BC%D1%8F"&gt;время Unix&lt;/a&gt;", измеряемое количеством секунд, прошедших от полуночи 1 января 1970 года. Прикладные языки программирования предоставляют собственные библиотечные средства. Помнится, в Delphi был тип &lt;span style="font-style: italic;"&gt;TDateTime&lt;/span&gt;, который хранил даты и время в виде десятичных чисел;  целая часть представляла количество дней, прошедших с полуночи 30 декабря 1899 года, а дробная часть -- время суток (стандарт OLE Automation). Базы данных, где проблема расчета времени весьма актуальна, делают это по-своему. Например, TIMESTAMP в MySQL 5.1 отсчитывается от 0ч 1 января 1000 года. Python умеет работать с датами в диапазоне от 1 до 9999 г.г. н.э. В языке Java... впрочем, довольно подробностей. Если нас интересуют события космического масштаба, как-то: восходы и заходы, равноденствия и солнцестояния, фазы Луны, координаты планет, наконец, интервалы времени, не ограничивающиеся нашей эрой -- ни одно из этих средств не подойдет. Что понадобится -- так это &lt;span style="font-weight: bold;"&gt;юлианские даты&lt;/span&gt;, система непрерывного счета времени, которую используют астрономы.&lt;br /&gt;&lt;br /&gt;За точку отсчета астрономы приняли средний гринвичский полдень 1 января 4713 г. до н.э. Очередная юлианская дата начинается в 12ч00м UT, что на полсуток расходится с началом гражданских суток. Целая часть числа представляет собой число суток, дробная -- доли суток, прошедшие от полудня. Например, 6ч 17 февраля 1985 года соответствует юлианской дате 2446113.75.&lt;br /&gt;&lt;h3&gt;Проблемы с точностью&lt;/h3&gt;В эпоху первых персональных компьютеров и программируемых калькуляторов возникали проблемы с точностью. В некоторых системах на хранение вещественных чисел отводилось всего 4 байта (то же самое, что сегодняшнее &lt;span style="font-style: italic;"&gt;float&lt;/span&gt;), что ограничивало точность 6-7 цифрами. Между тем, юлианские даты часто представлены 11-12 цифрами. Скажем, 11ч. 17 августа 1938 года = JD 2429127.9583&lt;br /&gt;&lt;h3&gt;Смещение точки отсчета&lt;br /&gt;&lt;/h3&gt;Можно принять за точку отсчета вместо даты, отстоящей от нас на 6 тысячелетий, момент поближе. Питер Даффетт-Смит в книге &lt;span style="font-style: italic;"&gt;"Astronomy With Your Personal Computer"&lt;/span&gt; (Cambridge University Press, 1986) предлагает использовать&lt;span style="font-weight: bold;"&gt; 12ч GMT 31 декабря 1899 года&lt;/span&gt;, т.е. ближайший полдень до начала XXв. Вычисления в рамках ближайших столетий становятся точнее, поскольку на хранение переменных потребуется меньше памяти. Даты до этого момента отрицательные. Чтобы перейти от такой даты (назовем ее вслед за Даффетт-Смитом &lt;span style="font-weight: bold;"&gt;DJD&lt;/span&gt;) к стандартной юлианской (&lt;span style="font-weight: bold;"&gt;JD&lt;/span&gt;), надо прибавить к первой &lt;span style="font-weight: bold;"&gt;2415020&lt;/span&gt;.&lt;br /&gt;&lt;br /&gt;Современное восьмибайтное представление вещественных чисел (&lt;span style="font-style: italic;"&gt;double&lt;/span&gt;), дает точность до 16 цифр, так что проблема, о которой идет речь, больше неактуальна, однако я намерен следовать методу из книги Даффетт-Смита. Тем более, что момент 12ч UT 31 декабря 1989 года взят не с потолка. Забегая вперед, скажу, что во множестве астрономических расчетов используется время в юлианских столетиях, прошедшее именно от этой даты (T). Формула такова:&lt;br /&gt;&lt;pre&gt;T = (JD - 2415020) / 36525&lt;br /&gt;&lt;/pre&gt;Здесь JD -- юлианская дата. То же самое, что DJD / 36525. Делитель, как нетрудно догадаться, представляет собой количество суток в условном юлианском столетии. &lt;br /&gt;&lt;br /&gt;Между прочим, класс &lt;span style="font-style: italic;"&gt;DateTime &lt;/span&gt;из одноименного пакета Perl содержит метод jd, выдающий стандартную юлианскую дату. В базе SQLite имеется функция для ее получения.&lt;br /&gt;&lt;h3&gt;От календаря к юлианской дате&lt;/h3&gt;Алгоритм пересчета календарной даты в юлианскую суров и непригляден. Его реализации на любых языках программирования &lt;br /&gt;выглядят примерно одинаково. Ниже представлен ее вариант на языке Perl.&lt;blockquote&gt;&lt;pre&gt;&lt;br /&gt;sub date2djd {&lt;br /&gt;    my %args = @_;&lt;br /&gt;    my $d = $args{date};&lt;br /&gt;    my $m = $args{month};&lt;br /&gt;    my $y = $args{year};&lt;br /&gt; &lt;br /&gt;    $y++ if $y &lt; 0;&lt;br /&gt;    if ($m &lt; 3) {&lt;br /&gt;        $m += 12;&lt;br /&gt;        $y--&lt;br /&gt;    }&lt;br /&gt;    my $b;&lt;br /&gt;    if (after_gregorian($y, $m, $d)) {&lt;br /&gt;        my $a = floor($y / 100);&lt;br /&gt;        $b = 2 - $a + floor($a / 4);&lt;br /&gt;    }&lt;br /&gt;    else {&lt;br /&gt;        $b = 0&lt;br /&gt;    }&lt;br /&gt;    my $f = 365.25 * $y;&lt;br /&gt;    my $c = floor( $y &lt; 0 ? $f - 0.75 : $f ) - 694025;&lt;br /&gt;    my $e = floor(30.6001 * ($m + 1));&lt;br /&gt;    return $b + $c + $e + $d - 0.5&lt;br /&gt;}&lt;/pre&gt;&lt;/blockquote&gt;На входе у нее именованные аргументы:&lt;br /&gt;&lt;ul&gt;&lt;li&gt;&lt;span style="font-style: italic;"&gt;year &lt;/span&gt;-- номер года, отрицательный для годов до н.э. Нулевое значение недопустимо, т.е. за -1 следует +1 вместо 0.&lt;/li&gt;&lt;li&gt;&lt;span style="font-style: italic;"&gt;month &lt;/span&gt;- номер месяца (1-12)&lt;/li&gt;&lt;li&gt;&lt;span style="font-style: italic;"&gt;day &lt;/span&gt;-- день месяца с долями суток. Например, 07ч 40м первого числа будет соответствовать 1 + (7 + 40 / 60.0) / 24.0&lt;/li&gt;&lt;/ul&gt;Правильность передаваемых аргументов не проверяется. Все проверки будут реализованы позже, при создании ООП-обертки.&lt;br /&gt;&lt;br /&gt;Календарная дата относится к  &lt;span style="font-style: italic;"&gt;пролептическому григорианскому календарю&lt;/span&gt;. Так называется григорианский&lt;br /&gt;календарь, который используется для обозначения дат, предшествовавших его введению. А применен он был впервые 15 октября 1582 года. Вспомогательная функция &lt;span style="font-style: italic;"&gt;after_gregorian&lt;/span&gt; как раз и помогает определить, приходится ли заданная дата на период до или после календарной реформы&lt;br /&gt;&lt;blockquote&gt;&lt;pre&gt;&lt;br /&gt;sub after_gregorian {&lt;br /&gt;    my ($y, $m, $d) = @_;&lt;br /&gt;    return 0 if $y &lt; 1582;&lt;br /&gt;    return 1 if $y &gt; 1582;&lt;br /&gt;    return 0 if $m &lt; 10;&lt;br /&gt;    return 1 if $m &gt; 10;&lt;br /&gt;    return 0 if $d &lt;= 15;&lt;br /&gt;    return 1;&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;&lt;/blockquote&gt;Использовать эту функцию самостоятельно, вне пролептического календаря, не имеет смысла, потому что григорианское летоисчисление не было принято везде одновременно. Наша страна перешла на &lt;span style="font-style: italic;"&gt;"новый стиль"&lt;/span&gt; в 1918 году. &lt;br /&gt;&lt;h3&gt;От юлианской даты к календарю&lt;/h3&gt;А вот как выглядит обратная функция, переводящая юлианскую дату в календарную.&lt;br /&gt;&lt;blockquote&gt;&lt;pre&gt;&lt;br /&gt;sub djd2date {&lt;br /&gt;    my $djd = shift;&lt;br /&gt;&lt;br /&gt;    my $d = $djd + 0.5;&lt;br /&gt;    my ($f, $i) = modf($d);&lt;br /&gt;&lt;br /&gt;    if ($i &gt; -115860) {&lt;br /&gt;        my $a = floor($i / 36524.25 + 9.9835726e-1) + 14;&lt;br /&gt;        $i += 1 + $a - floor($a / 4);&lt;br /&gt;    }&lt;br /&gt;    my $b = floor($i / 365.25 + 8.02601e-1);&lt;br /&gt;    my $c = $i - floor(365.25 * $b + 7.50001e-1) + 416;&lt;br /&gt;    my $g = floor($c / 30.6001);&lt;br /&gt;    my $dy = $c - floor(30.6001 * $g) + $f;&lt;br /&gt;    my $mn = $g - ($g &gt; 13.5 ? 13 : 1);&lt;br /&gt;    my $yr = $b + ($mn &lt; 2.5 ? 1900 : 1899 );&lt;br /&gt;    $yr-- if $yr &lt; 1;&lt;br /&gt;&lt;br /&gt;    return {year =&gt; $yr, month =&gt; $mn, day =&gt; $dy}&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;&lt;/blockquote&gt;На входе DJD -- число юлианских дней от начала XX века. На выходе -- хэш того же вида, что и на входе &lt;span style="font-style: italic;"&gt;date2jd&lt;/span&gt;.&lt;br /&gt;&lt;h3&gt;День недели&lt;/h3&gt;Система юлианских дней позволяет довольно просто вычислить день недели. Для этого достаточно:&lt;br /&gt;&lt;ol&gt;&lt;li&gt;Взять юлианскую дату на начало календарных суток (0ч UT).&lt;/li&gt;&lt;li&gt;Прибавить к ней &lt;span style="font-weight: bold;"&gt;1.5&lt;/span&gt;.&lt;/li&gt;&lt;li&gt;Найти остаток от деления результата на &lt;span style="font-weight: bold;"&gt;7&lt;/span&gt;&lt;/li&gt;&lt;/ol&gt;0 будет соответствовать воскресенью, 1 -- понедельнику, 2 -- вторнику и так до 6 -- субботы.&lt;br /&gt;&lt;br /&gt;Добавим две новые функции:&lt;br /&gt;&lt;ul&gt;&lt;li&gt;&lt;span style="font-style: italic;"&gt;jd_midnight&lt;/span&gt; для вычисления юлианской даты на календарную полночь, исходя из заданной юлианской даты&lt;/li&gt;&lt;li&gt;&lt;span style="font-style: italic;"&gt;weekday&lt;/span&gt; для вычисления дня недели по юлианской дате на начало суток.&lt;/li&gt;&lt;/ul&gt;&lt;blockquote&gt;&lt;pre&gt;&lt;br /&gt;#!/usr/bin/perl -w&lt;br /&gt;use strict;&lt;br /&gt;use warnings;&lt;br /&gt;use POSIX qw/modf floor/;&lt;br /&gt;use Readonly;&lt;br /&gt;&lt;br /&gt;Readonly our $DJD_TO_JD =&gt; 2415020;&lt;br /&gt;Readonly our @WEEKDAYS =&gt; qw/Sun Mon Tue Wed Thi Fri Sat/;&lt;br /&gt;&lt;br /&gt;....&lt;br /&gt;&lt;br /&gt;sub jd_midnight {&lt;br /&gt;    my $j = shift;&lt;br /&gt;    my $f = floor($j);&lt;br /&gt;    return $f + (abs($j - $f) &gt; 0.5 ? 0.5 : -0.5);&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;sub weekday {&lt;br /&gt;    my $j = shift;&lt;br /&gt;    my $j0 = jd_midnight($j);&lt;br /&gt;    return ($j0 + 1.5) % 7&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;if (@ARGV &lt; 3) {&lt;br /&gt;    print "Usage: perl julian.pl YEAR MONTH DAY";&lt;br /&gt;    exit(0)&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;my $djd = date2djd(year =&gt; $ARGV[0], month =&gt; $ARGV[1], day =&gt; $ARGV[2]);&lt;br /&gt;my $j = $djd + $DJD_TO_JD;&lt;br /&gt;printf "JD: %f\n", $j;&lt;br /&gt;my $j0 = jd_midnight($j);&lt;br /&gt;printf "JD at 0h: %f\n", $j0;&lt;br /&gt;my $w = weekday($j);&lt;br /&gt;print $WEEKDAYS[$w], "\n";&lt;br /&gt;&lt;/pre&gt;&lt;/blockquote&gt;На месте многоточий в скрипте должны фигурировать функции &lt;span style="font-style:italic;"&gt;date2djd&lt;/span&gt; и &lt;span style="font-style:italic;"&gt;after_gregorian&lt;/span&gt;.&lt;br/&gt;&lt;span style="font-weight: bold;"&gt;Важно:&lt;/span&gt; на входе &lt;span style="font-style: italic;"&gt;jd_midnight&lt;/span&gt; и &lt;span style="font-style: italic;"&gt;weekday&lt;/span&gt; стандартная юлианская дата, а не DJD. Иначе пришлось бы возиться с отрицательными значениями.&lt;br /&gt;&lt;br /&gt;Пример: в какой день недели был запущен первый советский спутник? Известно, что это произошло 4 октября 1957 года 19ч28м34с UT.&lt;br /&gt;Третьим аргументом (день) будет:((34 / 60 + 28) / 60 + 19) / 24 + 4 = 4.8115&lt;br /&gt;&lt;pre&gt;$ perl julian.pl 1957 10 4.8115&lt;br /&gt;JD: 2436116.311500&lt;br /&gt;JD at 0h: 2436115.500000&lt;br /&gt;Fri&lt;br /&gt;&lt;/pre&gt;Пятница, все правильно.&lt;br /&gt;&lt;br /&gt;Применение юлианской даты для вычисления дня недели -- стрельба из пушки по воробьям. Но это только начало. Дальше речь пойдет о решении куда более интересных календарных и астрономических задач, а уж там без юлианской даты никуда.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/964245207178906174-7290535543015809951?l=pi-code.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://pi-code.blogspot.com/feeds/7290535543015809951/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=964245207178906174&amp;postID=7290535543015809951' title='Комментарии: 0'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/7290535543015809951'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/7290535543015809951'/><link rel='alternate' type='text/html' href='http://pi-code.blogspot.com/2010/11/blog-post_10.html' title='Юлианские даты'/><author><name>Наувул-Наувул</name><uri>http://www.blogger.com/profile/06702180511726415678</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/_3ifHLhKgDjk/TCEZ2cVuxJI/AAAAAAAAB2E/D5uehCRtVtI/S220/IMG_3814.JPG'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://2.bp.blogspot.com/_3ifHLhKgDjk/TNqmIfvQbMI/AAAAAAAAB5M/rfpPQLiwkt4/s72-c/calendar.jpg' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-964245207178906174.post-1247598490658425197</id><published>2010-11-09T02:14:00.013+03:00</published><updated>2010-11-09T11:07:52.785+03:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='Perl'/><category scheme='http://www.blogger.com/atom/ns#' term='рекурсия'/><category scheme='http://www.blogger.com/atom/ns#' term='Python'/><title type='text'>Perl и рекурсия:  ура!</title><content type='html'>В &lt;a href="http://pi-code.blogspot.com/2010/11/blog-post.html"&gt;прошлой заметке&lt;/a&gt; выяснилось, что в Python-е рекурсия работает медленнее, чем for-цикл, а тот, в свою очередь -- медленнее, чем  while.  С циклами все понятно, while всегда быстрее. Что же касается рекурсии, то мне стало любопытно: специфична ли ее низкая производительность для Python-а или это везде так. И я решил ту же задачку -- получение из десятичного числа градусов, минут и секунд -- на Perl-е. Правда, ограничившись двумя&lt;div&gt; функциями: рекурсивной и циклом-while. Было интересно, насколько сильно здесь рекурсия отстает от цикла.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Результат удивил: рекурсия оказалась эффективнее.&lt;/div&gt;&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;br /&gt;&lt;pre style="margin-left: 1em; margin-right: 1em;"&gt;&lt;br /&gt;Benchmark: timing 1000000 iterations of Recursive, While-loop...&lt;br /&gt;Recursive: 136.521 wallclock secs (119.61 usr +  0.64 sys = 120.25 CPU) @ 8316.01/s (n=1000000)&lt;br /&gt;While-loop: 142.86 wallclock secs (123.91 usr +  0.66 sys = 124.57 CPU) @ 8027.61/s (n=1000000)&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;Получается, что на миллион вызовов рекурсивной функции понадобилось 136 секунд. На столько же вызовов нерекурсивной функции ушло 142 секунды. За секунду рекурсивная функция успела отработать примерно на 300 раз больше.&lt;br /&gt;Разница невелика -- и этот факт радует, потому что как система Perl ведет себя предсказуемо. Я не хочу вникать в устройство интерпретатора каждый раз когда приходится задумываться о реализации какого-то алгоритма.&lt;br /&gt;&lt;br /&gt;Вот скрипт целиком:&lt;br /&gt;&lt;/div&gt;&lt;br /&gt;&lt;br /&gt;&lt;script type="syntaxhighlighter" class="brush: perl"&gt;&lt;br /&gt;&lt;br /&gt;#!/usr/bin/perl -w&lt;br /&gt;use strict;&lt;br /&gt;use POSIX qw/modf/;&lt;br /&gt;use Benchmark qw/:hireswallclock/;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;sub _next_fi {&lt;br /&gt;    my $x = shift;&lt;br /&gt;    my ($f, $i) = modf(abs($x));&lt;br /&gt;    if ($x &lt; 0) {&lt;br /&gt;        if ($i == 0) {&lt;br /&gt;            $f = -$f&lt;br /&gt;        }&lt;br /&gt;        else {&lt;br /&gt;            $i = -$i&lt;br /&gt;        }&lt;br /&gt;    }&lt;br /&gt;    return ($f * 60.0, $i)&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;sub dms1 {&lt;br /&gt;    my $x = shift;&lt;br /&gt;    my %args = (nvals=&gt;3, _count=&gt;1, @_);&lt;br /&gt;&lt;br /&gt;    return $x if $args{_count} &gt;= $args{nvals};&lt;br /&gt;&lt;br /&gt;    my ($f, $i) = _next_fi($x);&lt;br /&gt;    $args{_count}++;&lt;br /&gt;    return ( $i, dms1($f, %args) )&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;sub dms2 {&lt;br /&gt;    my $x = shift;&lt;br /&gt;    my %args = (nvals=&gt;3, @_);&lt;br /&gt;    my $nvals = $args{nvals};&lt;br /&gt;    my @arr;&lt;br /&gt;    my $f = $x;&lt;br /&gt;    my $i;&lt;br /&gt;    while(@arr &lt;= $nvals) {&lt;br /&gt;        if (@arr == $nvals) {&lt;br /&gt;            push @arr, $f&lt;br /&gt;        }&lt;br /&gt;        else {&lt;br /&gt;            ($f, $i) = _next_fi($f);&lt;br /&gt;            push @arr, $i;&lt;br /&gt;        }&lt;br /&gt;    }&lt;br /&gt;    return @arr&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;timethese(1000000, {&lt;br /&gt;    'Recursive'  =&gt; sub { dms1(11.7666667) },&lt;br /&gt;    'While-loop' =&gt; sub { dms2(11.7666667) },&lt;br /&gt;});&lt;br /&gt;&lt;/script&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/964245207178906174-1247598490658425197?l=pi-code.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://pi-code.blogspot.com/feeds/1247598490658425197/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=964245207178906174&amp;postID=1247598490658425197' title='Комментарии: 1'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/1247598490658425197'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/1247598490658425197'/><link rel='alternate' type='text/html' href='http://pi-code.blogspot.com/2010/11/perl.html' title='Perl и рекурсия:  ура!'/><author><name>Наувул-Наувул</name><uri>http://www.blogger.com/profile/06702180511726415678</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/_3ifHLhKgDjk/TCEZ2cVuxJI/AAAAAAAAB2E/D5uehCRtVtI/S220/IMG_3814.JPG'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-964245207178906174.post-1841757568943888518</id><published>2010-11-08T15:25:00.014+03:00</published><updated>2010-11-09T11:14:32.330+03:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='генератор'/><category scheme='http://www.blogger.com/atom/ns#' term='рекурсия'/><category scheme='http://www.blogger.com/atom/ns#' term='Python'/><title type='text'>Python и рекурсия? Увы...</title><content type='html'>Задача пересчета градусов, выраженных десятичной дробью, в градусы, минуты и секунды для положительного числа x решается тривиально:&lt;div&gt;&lt;ol&gt;&lt;li&gt;Полагаем, что x = исходное число.&lt;/li&gt;&lt;li&gt;Находим целую (i) и дробную (f) часть x. &lt;/li&gt;&lt;li&gt;Целую часть сохраняем как градусы, минуты или секунды.&lt;/li&gt;&lt;li&gt;Если найдены три числа, конец.&lt;/li&gt;&lt;li&gt;Полагаем x = f * 60 и возвращаемся к шагу 2.&lt;/li&gt;&lt;/ol&gt;&lt;div&gt;Понятно, что эти вычисления производятся в цикле, однако подробности организации цикла пока не важны. &lt;/div&gt;&lt;div&gt;Как быть с отрицательными числами? В этом случае алгоритм немного усложняется:&lt;/div&gt;&lt;div&gt;&lt;ol&gt;&lt;li&gt;Полагаем, что x = исходное число.&lt;/li&gt;&lt;li&gt;Находим целую (i) и дробную (f) часть от абсолютного значения x.&lt;/li&gt;&lt;li&gt;Если x положительное, переходим к шагу 5. &lt;/li&gt;&lt;li&gt;Если i не равно 0, полагаем i = -i.  В противном случае, f = -f&lt;/li&gt;&lt;li&gt;Целую часть сохраняем как градусы, минуты или секунды.&lt;/li&gt;&lt;li&gt;Если найдены три числа, конец. &lt;/li&gt;&lt;li&gt;Полагаем x = f * 60 и возвращаемся к шагу 2.&lt;/li&gt;&lt;/ol&gt;&lt;/div&gt;&lt;div&gt;Теперь при отрицательном исходном числе, первое значащее число будет отрицательным. Так, &lt;/div&gt;&lt;div&gt;&lt;ul&gt;&lt;li&gt;11.75 -&gt; 11:45:0&lt;/li&gt;&lt;li&gt;-11.75 -&gt; -11:45:0&lt;/li&gt;&lt;li&gt;-0.75 -&gt; 0:-45:0&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;div&gt;Кроме того, удобнее будет секунды возвращать как вещественное, а не целое число, потому что в противном случае при обратном пересчете (градусов, минут и секунд в десятичные градусы) не будет получаться исходное число. &lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;ol&gt;&lt;li&gt;Полагаем, что x = исходное число.&lt;/li&gt;&lt;li&gt;Если при текущем повторении вычисляются секунды, сохраняем x как секунды и выходим из цикла. &lt;/li&gt;&lt;li&gt;Находим целую (i) и дробную (f) часть от абсолютного значения x.&lt;/li&gt;&lt;li&gt;Если x положительное, переходим к шагу 6&lt;br /&gt;&lt;/li&gt;&lt;li&gt;Если i не равно 0, полагаем i = -i.  В противном случае, f = -f&lt;/li&gt;&lt;li&gt;Целую часть сохраняем как градусы или минуты.&lt;/li&gt;&lt;li&gt;Полагаем x = f * 60 и возвращаемся к шагу 2.&lt;/li&gt;&lt;/ol&gt;&lt;/div&gt;&lt;div&gt;Когда в твоем распоряжении язык высокого уровня можно потратить немало времени в поисках наиболее красивой, эффективной и органичной для языка реализации этого алгоритма. В Python-е имеются как минимум три способа:&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;ol&gt;&lt;li&gt;рекурсивно;&lt;/li&gt;&lt;li&gt;при помощи циклов: for и while;&lt;/li&gt;&lt;li&gt;с использованием генераторов.&lt;/li&gt;&lt;/ol&gt;Начнем с рекурсии.&lt;/div&gt;&lt;/div&gt;&lt;br /&gt;&lt;br /&gt;&lt;script type="syntaxhighlighter" class="brush:python"&gt;&lt;br /&gt;def dms1(x, num_vals=3):&lt;br /&gt;    def mul_60(x, count=1):&lt;br /&gt;        if count == num_vals:&lt;br /&gt;            return [x]&lt;br /&gt;            f, i = modf(abs(x))&lt;br /&gt;        if x &lt; 0:&lt;br /&gt;            if i == 0:&lt;br /&gt;                f = -f&lt;br /&gt;            else:&lt;br /&gt;                i = -i&lt;br /&gt;        res = mul_60(f * 60.0, count=count+1)&lt;br /&gt;        res.insert(0, int(i))&lt;br /&gt;        return res&lt;br /&gt;&lt;br /&gt;    return tuple(mul_60(x))&lt;br /&gt;&lt;/script&gt;&lt;br /&gt;&lt;br /&gt;Функция возвращает кортеж из шестидесятиричных значений: (градусы, минуты, секунды). Именованный параметр &lt;i&gt;num_vals&lt;/i&gt;  указывает на то, сколько значений необходимо вернуть. Если секунды не нужны, &lt;i&gt;num_vals=2&lt;/i&gt;.&lt;br /&gt;&lt;br /&gt;&lt;div&gt;Рассмотрим альтернативные способы. Чтобы не повторять один и тот же код, вынесем общую часть в отдельную функцию:&lt;/div&gt;&lt;br /&gt;&lt;br /&gt;&lt;script type="syntaxhighlighter" class="brush:python"&gt;&lt;br /&gt;def _next_fi(f):&lt;br /&gt;    negative = f &lt; 0 &lt;br /&gt;    f, i = modf(abs(f))&lt;br /&gt;    if negative:&lt;br /&gt;        if i == 0:&lt;br /&gt;            f = -f&lt;br /&gt;        else:&lt;br /&gt;            i = -i     &lt;br /&gt;    return (f * 60.0, int(i))&lt;br /&gt;    &lt;br /&gt;&lt;br /&gt;def dms1(x, num_vals=3):&lt;br /&gt;    def mul_60(x, count=1):&lt;br /&gt;        if count == num_vals:&lt;br /&gt;            return [x]      &lt;br /&gt;        f, i = _next_fi(x)&lt;br /&gt;        res = mul_60(f, count=count + 1)&lt;br /&gt;        res.insert(0, i)&lt;br /&gt;        return res&lt;br /&gt;   &lt;br /&gt;    return tuple(mul_60(x))   &lt;br /&gt;&lt;/script&gt;&lt;br /&gt;&lt;div&gt;Функция, делающая то же самое при помощи &lt;span style="font-style: italic;"&gt;for&lt;/span&gt;-цикла, выглядит менее изящно:&lt;/div&gt;&lt;br /&gt;&lt;script type="syntaxhighlighter" class="brush:python"&gt;&lt;br /&gt;def dms2(x, num_vals=3):&lt;br /&gt;    arr = []&lt;br /&gt;    last_j = num_vals - 1&lt;br /&gt;    f = x&lt;br /&gt;    for j in range(0, num_vals):&lt;br /&gt;        if j == last_j:&lt;br /&gt;            arr.append(f)&lt;br /&gt;        else:        &lt;br /&gt;            f, i = _next_fi(f)&lt;br /&gt;            arr.append(i)&lt;br /&gt;           &lt;br /&gt;              &lt;br /&gt;    return tuple(arr)&lt;br /&gt;&lt;/script&gt;&lt;br /&gt;&lt;br /&gt;А вот вариант с циклом while:&lt;br /&gt;&lt;script type="syntaxhighlighter" class="brush:python"&gt;&lt;br /&gt;def dms3(x, num_vals=3):&lt;br /&gt;    arr = []&lt;br /&gt;    j = 1&lt;br /&gt;    f = x&lt;br /&gt;    while(j &lt;= num_vals):&lt;br /&gt;        if j == num_vals:&lt;br /&gt;            arr.append(f)&lt;br /&gt;        else:      &lt;br /&gt;            f, i = _next_fi(f)&lt;br /&gt;            arr.append(i)&lt;br /&gt;        j += 1&lt;br /&gt;              &lt;br /&gt;    return tuple(arr)&lt;br /&gt;&lt;/script&gt;&lt;br /&gt;На очереди -- генераторы. Первый из них использует &lt;i&gt;for&lt;/i&gt;, второй -- &lt;i&gt;while&lt;/i&gt;&lt;br /&gt;&lt;script type="syntaxhighlighter" class="brush:python"&gt;&lt;br /&gt;def dms4(x, num_vals=3):&lt;br /&gt;    &lt;br /&gt;    def mul_60(f):&lt;br /&gt;        last_j = num_vals - 1&lt;br /&gt;        for j in range(0, num_vals):&lt;br /&gt;            if j == last_j:&lt;br /&gt;                yield f&lt;br /&gt;            else:&lt;br /&gt;                f, i = _next_fi(f)&lt;br /&gt;                yield i&lt;br /&gt;              &lt;br /&gt;    return tuple([v for v in mul_60(x)])&lt;br /&gt;&lt;br /&gt;def dms5(x, num_vals=3):&lt;br /&gt;    &lt;br /&gt;    def mul_60(f):&lt;br /&gt;        j = 1&lt;br /&gt;        while(j &lt;= num_vals):&lt;br /&gt;            if j == num_vals:&lt;br /&gt;                yield f&lt;br /&gt;            else:&lt;br /&gt;                f, i = _next_fi(f)&lt;br /&gt;                yield i&lt;br /&gt;            j += 1&lt;br /&gt;              &lt;br /&gt;    return tuple([v for v in mul_60(x)])&lt;br /&gt;&lt;br /&gt;&lt;/script&gt;&lt;br /&gt;&lt;div&gt;Какая из функций работает быстрее всех? Для измерения я воспользовался стандартным методом &lt;a href="http://docs.python.org/library/timeit.html?highlight=timeit#module-timeit"&gt;Timer.timeit&lt;/a&gt;, которая по умолчанию запускает заданный код миллион раз.  Получилось вот что:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;&lt;br /&gt;Benchmarking X -&gt; D,M,S&lt;br /&gt;&lt;br /&gt;06.465 сек.: 'while'-цикл&lt;br /&gt;07.600 сек.: 'while'-генератор&lt;br /&gt;07.768 сек.: 'for'-цикл&lt;br /&gt;08.544 сек.: 'for'-генератор&lt;br /&gt;09.995 сек.: рекурсия&lt;br /&gt;&lt;/div&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;Увы, самый изящный, на мой взгляд, способ оказался самым медленным! А выиграла самая примитивная функция -- даже не генераторы.&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/964245207178906174-1841757568943888518?l=pi-code.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://pi-code.blogspot.com/feeds/1841757568943888518/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=964245207178906174&amp;postID=1841757568943888518' title='Комментарии: 0'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/1841757568943888518'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/1841757568943888518'/><link rel='alternate' type='text/html' href='http://pi-code.blogspot.com/2010/11/blog-post.html' title='Python и рекурсия? Увы...'/><author><name>Наувул-Наувул</name><uri>http://www.blogger.com/profile/06702180511726415678</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/_3ifHLhKgDjk/TCEZ2cVuxJI/AAAAAAAAB2E/D5uehCRtVtI/S220/IMG_3814.JPG'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-964245207178906174.post-6517153067439027239</id><published>2010-02-19T12:47:00.011+03:00</published><updated>2010-02-19T14:00:29.286+03:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='Perl'/><category scheme='http://www.blogger.com/atom/ns#' term='трудоустройство'/><title type='text'>Входной билет на собеседование</title><content type='html'>Письмо, полученное сегодня утром, показалось мне знакомым. Так и есть! В 2008 году та же &lt;span style="font-style: italic;"&gt;"&lt;a href="http://www.combats.ru/"&gt;ведущая игровая компания&lt;/a&gt;"&lt;/span&gt; уже искала Perl программистов. Требования к кандидатам  и условия работы стандартные. Далее шла  неуклюжая формулировка:&lt;br /&gt;&lt;br /&gt;&lt;span style="font-style: italic;"&gt;"Решением о дальнейшем сотрудничестве являются:&lt;/span&gt;&lt;br /&gt;&lt;span style="font-style: italic;"&gt; Успешно выполненное тестовое задание;&lt;/span&gt;&lt;br /&gt;&lt;span style="font-style: italic;"&gt; Положительные результаты собеседования.&lt;/span&gt;"&lt;br /&gt;&lt;br /&gt;Тестовое задание прилагалось -- даже не одно, а два.&lt;br /&gt;&lt;ol&gt;&lt;li&gt;В первом предлагалось написать сценарий регистрации посетителя веб-сайта, входа в систему и редактирования профиля. Дополнительные требования звучали несколько экзотично. Скажем, нельзя использовать модуль CGI (стандартную библиотеку, входящую в дистрибутивы Perl-а, которую обойдет мимо разве что любитель изобретать велосипеды).&lt;br /&gt;&lt;/li&gt;&lt;li&gt;Второе задание состояло из 6 вопросов по SQL. Для каждого рекомендовалось привести несколько решений.&lt;br /&gt;&lt;/li&gt;&lt;/ol&gt; Допустим, веб-приложение я напишу за вечер. Хотя нет, вечера не хватит, учитывая необходимость дублировать CGI. Хоть в условиях и сказано, что можно ограничиться GET-запросами (а POST, мол, является дополнительным плюсом), у меня рука не поднимется передавать логин и пароль пользователя так, чтобы они были видны в адресной строке браузера.  Так что, если в пятницу вечером я сяду, то закончу в субботу часикам к четырем. На второе задание уйдет как минимум полдня.&lt;br /&gt;&lt;br /&gt;И в том и другом случае особого ума не требуется, это рутина. Но рутина трудоёмкая. А на выходные у меня уже имеются кое-какие планы. Во-первых, мы с Мариной собирались на концерт, в кои-то веки.  Во-вторых, &lt;a href="http://krushinsky.blogspot.com/2008/01/blog-post_04.html"&gt;детям&lt;/a&gt; при моей занятости недостает внимания. Позавчера сижу в офисе, Марина звонит, жалуется: &lt;span style="font-style: italic;"&gt;"...Два сапога -- пара, только что поругались и принялись кидаться друг в друга едой"&lt;/span&gt;. В-третьих, есть пара собственных проектов, на которые катастрофически не хватает времени. Так что, увольте. Выходные -- слишком высокая цена для входного билета на собеседование.&lt;br /&gt;&lt;br /&gt;Два случая, когда человек не откажется тратить время на такие задания:&lt;br /&gt;&lt;ol&gt;&lt;li&gt;Он мечтает попасть в эту компанию. Мне самому ни разу не приходилось слышать о компании &lt;span style="font-style: italic;"&gt;"&lt;a href="http://www.combats.ru/"&gt;Адептус&lt;/a&gt;"&lt;/span&gt;. Но, наверное, я не в теме. Ясно, что существует она не меньше двух лет, причем -- кризисных, хороший признак. Может, программисты валят туда толпами и они выбирают лишь тех, кто готов на все лишь бы получить там место.&lt;/li&gt;&lt;li&gt;Нет выбора. С работы уволили, деньги кончились, больше никуда не берут.&lt;br /&gt;&lt;/li&gt;&lt;/ol&gt;Больше ничего не приходит в голову.&lt;br /&gt;&lt;br /&gt;PS. Кто-то метко сказал: &lt;span style="font-style: italic;"&gt;"тестовое задание я готов выполнить за тестовую зарплату"&lt;/span&gt;.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/964245207178906174-6517153067439027239?l=pi-code.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://pi-code.blogspot.com/feeds/6517153067439027239/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=964245207178906174&amp;postID=6517153067439027239' title='Комментарии: 5'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/6517153067439027239'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/6517153067439027239'/><link rel='alternate' type='text/html' href='http://pi-code.blogspot.com/2010/02/blog-post.html' title='Входной билет на собеседование'/><author><name>Наувул-Наувул</name><uri>http://www.blogger.com/profile/06702180511726415678</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/_3ifHLhKgDjk/TCEZ2cVuxJI/AAAAAAAAB2E/D5uehCRtVtI/S220/IMG_3814.JPG'/></author><thr:total>5</thr:total></entry><entry><id>tag:blogger.com,1999:blog-964245207178906174.post-8133049863051376857</id><published>2009-01-30T12:23:00.005+03:00</published><updated>2009-01-30T13:30:06.997+03:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='аутентификация'/><category scheme='http://www.blogger.com/atom/ns#' term='тесты'/><category scheme='http://www.blogger.com/atom/ns#' term='urllib2'/><category scheme='http://www.blogger.com/atom/ns#' term='краулер'/><category scheme='http://www.blogger.com/atom/ns#' term='Python'/><title type='text'>Краулер своими руками. Часть 12</title><content type='html'>&lt;h2&gt;Как работает базовая аутентификация&lt;/h2&gt;Наряду с &lt;a href="http://pi-code.blogspot.com/2009/01/11.html"&gt;аутентификацией при помощи формы&lt;/a&gt;, существует и более простая -- так наз. &lt;span style="font-style: italic;"&gt;"базовая аутентификация"&lt;/span&gt;:&lt;br /&gt;&lt;ul&gt;&lt;br /&gt;&lt;li&gt;При первой попытке зайти на сайт сервер проверяет, если среди заголовков запроса поле '&lt;span style="font-style: italic;"&gt;Authorization&lt;/span&gt;'. Если нет или там содержится неверное значение, возвращается ошибка 401.&lt;/li&gt;&lt;li&gt;Браузер открывает всплывающее окошко для ввода имени пользователя логин и пароля&lt;/li&gt;&lt;li&gt;Пользователь вводит имя и пароль, нажимает OK и браузер делает повторный запрос, вставив в заголовок запроса: &lt;span style="font-style: italic;"&gt;Authorization: Basic данные&lt;/span&gt;. &lt;/li&gt;&lt;li&gt;Сервер проверяет логин с паролем и если все в порядке, возвращает запрошенную страницу с кодом 200.&lt;br /&gt;&lt;/li&gt;&lt;br /&gt;&lt;/ul&gt;Пользователей заводит администратор веб-сервера. Их имена и пароли заносятся в конфигурацию. Они могут быть зашифрованы или храниться в виде текста -- зависит от требований к безопасности.&lt;br /&gt;&lt;br /&gt;Эта защита считается не самой надежной и применяется чаще всего в технических целях. Например, чтобы закрыть доступ к порталу всем, кроме заказчиков, разработчиков и тестировщиков, пока не закончена работа.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;В одной из заметок был продемонстрирован &lt;a href="http://pi-code.blogspot.com/2009/01/7.html"&gt;краулер, способный обходить сайт и проверять, все ли ссылки правильно работают&lt;/a&gt;. Поскольку использоваться он будет чаще всего на этапе тестирования сайтов, ему недостает умения заходить на защищенные таким способом страницы.&lt;br /&gt;&lt;br /&gt;&lt;h2&gt;Базовая аутентификация и urllib2&lt;/h2&gt;Для базовой аутентификации средствами питоновской библиотеки &lt;span style="font-style: italic;"&gt;urllib2 &lt;/span&gt;нужно использовать два встроенных класса:&lt;br /&gt;&lt;ul&gt;&lt;li&gt;HTTPPasswordMgr&lt;/li&gt;&lt;li&gt;HTTPBasicAuthHandler&lt;/li&gt;&lt;/ul&gt;Первый позволяет задавать имя пользователя и пароль, второй добавляется в набор &lt;span style="font-style: italic;"&gt;handler&lt;/span&gt;-ов (обработчиков запроса) для специализированного экземпляра &lt;span style="font-style: italic;"&gt;OpenDirector &lt;/span&gt;(см. &lt;a href="http://pi-code.blogspot.com/2008/12/2.html"&gt;подробности&lt;/a&gt;).&lt;br /&gt;&lt;br /&gt;&lt;h3&gt;Простейший вариант HTTPPasswordMgr&lt;/h3&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;class KnownPasswordMgr(HTTPPasswordMgr):&lt;br /&gt; """&lt;br /&gt; Хранит заранее заданную пару логин/пароль&lt;br /&gt; """&lt;br /&gt; def __init__(self, username, password):&lt;br /&gt;     HTTPPasswordMgr.__init__(self)&lt;br /&gt;     self.username = username&lt;br /&gt;     self.password = password&lt;br /&gt;&lt;br /&gt; def find_user_password(self, realm, authuri):&lt;br /&gt;     """&lt;br /&gt;     Возвращает заранее известную пару значений&lt;br /&gt;     """&lt;br /&gt;     retval = HTTPPasswordMgr.find_user_password(self, realm, authuri)&lt;br /&gt;     if not (retval[0] or retval[1]):&lt;br /&gt;         return (self.username, self.password)&lt;br /&gt;  &lt;br /&gt;     return retval&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;Этот класс рассчитан на использование одной единственной пары имя/пароль.&lt;br /&gt;&lt;h3&gt;HTTPBasicAuthHandler&lt;/h3&gt;В конструктор класса &lt;span style="font-style: italic;"&gt;UserAgent &lt;/span&gt;добавим необязательный параметр: &lt;span style="font-style: italic;"&gt;credentials&lt;/span&gt;. Если он присутствует, к набору &lt;span style="font-style: italic;"&gt;handler&lt;/span&gt;-ов будет добавлен &lt;span style="font-style: italic;"&gt;HTTPBasicAuthHandler&lt;/span&gt;.&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;&lt;br /&gt;class UserAgent(object):&lt;br /&gt; """&lt;br /&gt; Краулер.&lt;br /&gt;&lt;br /&gt; Именованные аргументы конструктора и значения по умолчанию:&lt;br /&gt;   agentname -- имя ('Test/1.0')&lt;br /&gt;   email -- адрес разработчика (пустая строка)&lt;br /&gt;   hdrs -- словарь HTTP-заголовков (DEFAULT_HEADERS)&lt;br /&gt;   ignore_robots -- True, если следует игнорировать robots.txt (False)&lt;br /&gt;&lt;br /&gt;   credentials -- словарь с ключами 'логин' и 'пароль', если нужна&lt;br /&gt;                  базовая аутентификация (None).&lt;br /&gt; """&lt;br /&gt;&lt;br /&gt; def __init__(self,&lt;br /&gt;              agentname=DEFAULT_AGENTNAME,&lt;br /&gt;              email=DEFAULT_EMAIL,&lt;br /&gt;              hdrs=None,&lt;br /&gt;              ignore_robots=False,&lt;br /&gt;              credentials=None):&lt;br /&gt;  &lt;br /&gt;     if not hdrs:&lt;br /&gt;         hdrs = {}&lt;br /&gt;     self.agentname = agentname&lt;br /&gt;     self.email = email&lt;br /&gt;&lt;br /&gt;     self.cookies_handler = SessionCookieHandler()&lt;br /&gt;     handlers = [ self.cookies_handler, ]&lt;br /&gt;     if not ignore_robots:&lt;br /&gt;         handlers.append(RobotsHTTPHandler(self.agentname))&lt;br /&gt;      &lt;br /&gt;     if credentials:&lt;br /&gt;         handlers.append(&lt;br /&gt;             HTTPBasicAuthHandler(KnownPasswordMgr(**credentials))&lt;br /&gt;         )&lt;br /&gt;      &lt;br /&gt;     self.opener = urllib2.build_opener(*handlers)&lt;br /&gt;&lt;br /&gt;     # переопределение заголовков по умолчанию&lt;br /&gt;     headers = copy(DEFAULT_HEADERS)&lt;br /&gt;     headers.update(hdrs)     &lt;br /&gt;     op_headers = [ (k, v) for k, v in headers.iteritems() ]&lt;br /&gt;     op_headers.append(('User-Agent', self.agentname))&lt;br /&gt;     # если email не задан, HTTP-заголовок 'From' не нужен&lt;br /&gt;     if self.email:&lt;br /&gt;         op_headers.append(('From', self.email))&lt;br /&gt;  &lt;br /&gt;     self.opener.addheaders = op_headers&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;Вот, собственно, и все. &lt;span style="font-style: italic;"&gt;TestCase &lt;/span&gt;для новой функции может выглядеть так:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;&lt;br /&gt;class TestAuth(unittest.TestCase):&lt;br /&gt; def setUp(self):&lt;br /&gt;     self.crawler = crawler.UserAgent(&lt;br /&gt;         agentname='Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)',&lt;br /&gt;         ignore_robots=True,&lt;br /&gt;         credentials = dict(username='ЛОГИН', password='ПАРОЛЬ')&lt;br /&gt;     )&lt;br /&gt;&lt;br /&gt; def _on_success(self, *args):&lt;br /&gt;     self.assertTrue(1)&lt;br /&gt;&lt;br /&gt; def _on_failure(self, url, err):&lt;br /&gt;     self.fail('Authentication failed: %s' % err) &lt;br /&gt;  &lt;br /&gt; def test_authentication(self):&lt;br /&gt;     self.crawler.visit('АДРЕС САЙТА',&lt;br /&gt;                        on_success=self._on_success,&lt;br /&gt;                        on_failure=self._on_failure )&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;Здесь, в отличие от &lt;a href="http://pi-code.blogspot.com/2009/01/11.html"&gt;авторизации через форму&lt;/a&gt;, в случае успешного ответа не надо дополнительно проверять содержание страницы. В случае ошибки будет возвращаться все тот же код 401, т.е. функция обратного вызова _on_&lt;span style="font-style: italic;"&gt;success &lt;/span&gt;не будет вызвана.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/964245207178906174-8133049863051376857?l=pi-code.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://pi-code.blogspot.com/feeds/8133049863051376857/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=964245207178906174&amp;postID=8133049863051376857' title='Комментарии: 6'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/8133049863051376857'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/8133049863051376857'/><link rel='alternate' type='text/html' href='http://pi-code.blogspot.com/2009/01/12.html' title='Краулер своими руками. Часть 12'/><author><name>Наувул-Наувул</name><uri>http://www.blogger.com/profile/06702180511726415678</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/_3ifHLhKgDjk/TCEZ2cVuxJI/AAAAAAAAB2E/D5uehCRtVtI/S220/IMG_3814.JPG'/></author><thr:total>6</thr:total></entry><entry><id>tag:blogger.com,1999:blog-964245207178906174.post-376610688795826501</id><published>2009-01-26T22:31:00.016+03:00</published><updated>2009-01-27T19:48:18.165+03:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='cookies'/><category scheme='http://www.blogger.com/atom/ns#' term='Perl'/><category scheme='http://www.blogger.com/atom/ns#' term='urllib2'/><category scheme='http://www.blogger.com/atom/ns#' term='краулер'/><category scheme='http://www.blogger.com/atom/ns#' term='mechanize'/><category scheme='http://www.blogger.com/atom/ns#' term='Python'/><category scheme='http://www.blogger.com/atom/ns#' term='скрапер'/><category scheme='http://www.blogger.com/atom/ns#' term='конечный автомат. WWW::Mechanize'/><title type='text'>Краулер своими руками. Часть 11</title><content type='html'>&lt;h2&gt;Лень как двигатель прогресса&lt;/h2&gt;В последнее время я пристрастился к порталу "&lt;a href="http://www.kinokopilka.ru/"&gt;Кинокопилка&lt;/a&gt;", откуда можно скачивать фильмы. Я захожу в разделы интересующих меня жанров, чтобы узнать, не появилось ли там что-нибудь интересное. Не все же время программы писать! Еще есть онлайн-библиотека технической литературы. Рассылка приходит раз в месяц, а новые книги появляются ежедневно. В отличие от "&lt;a href="http://www.kinokopilka.ru/"&gt;Кинокопилки&lt;/a&gt;", этот сайт предоставляет RSS-канал. Но среди первых заголовков, которые я вижу через программу просмотра каналов, далеко не всегда фигурируют книги по моей специальности. Так что, все равно приходится идти на сайт.&lt;br /&gt;&lt;br /&gt;Если добавить еще пяток ежедневно просматриваемых сайтов, то процедура становится уже несколько утомительной. А кому-то необходимо отслеживать информацию, меняющуюся каждый час: наблюдать за финансовыми событиями, наличием свободных мест и объявлений... Вот почему пользуются спросом так называемые &lt;span style="font-style: italic;"&gt;"скраперы"&lt;/span&gt;.&lt;br /&gt;&lt;br /&gt;&lt;h2&gt;Скрапер&lt;/h2&gt;&lt;span style="font-style: italic;"&gt;Скрапер &lt;/span&gt;-- это программа, которая, по &lt;a href="http://en.wikipedia.org/wiki/Web_scraping"&gt;определению Википедии&lt;/a&gt;, специализируется на извлечении той или иной информации с веб-страниц. Во многих случаях эта информация доступна только авторизованным посетителям.&lt;br /&gt;&lt;h3&gt;Типичный сценарий&lt;/h3&gt;&lt;ol&gt;&lt;li&gt;Клиент  заходит на страницу, где содержится форма авторизации.&lt;br /&gt;&lt;/li&gt;&lt;li&gt;Отправляет на сервер логин и пароль методом POST&lt;br /&gt;&lt;/li&gt;&lt;li&gt;Сервер проверяет данные и если все правильно, создает &lt;span style="font-style: italic;"&gt;сессию&lt;/span&gt;, возвращает клиенту ее уникальный код и перенаправляет его на другую страницу.&lt;/li&gt;&lt;li&gt;Клиент открывает нужные страницы, каждый раз передавая на сервер код сессии. По этому "ярлычку" сервер будет узнавать авторизованного клиента.&lt;/li&gt;&lt;li&gt;Сессия завершается (либо по таймеру либо клиент заходит на страницу "Выход"), сервер  удаляет код сессии и все данные, которые уже неактуальны.&lt;/li&gt;&lt;/ol&gt;Нельзя ли пропустить первый шаг, сразу отправляя на сервер логин и пароль? Иногда можно, но чаще -- нет, по двум причинам:&lt;br /&gt;&lt;ol&gt;&lt;li&gt;В ходе авторизации сервер может проверить наличие кода анонимной сессии.  А код этот можно получить только если вначале открыть страницу сайта (читай: сделать GET-запрос). У анонимной сессии тоже имеется  код, только сервер не связывает его с базой данных зарегистрированных пользователей.&lt;br /&gt;&lt;/li&gt;&lt;li&gt;Многие сайты, помимо  логина и пароля, проверяют еще и заголовок HTTP-запроса &lt;span style="font-style: italic;"&gt;"Referer"&lt;/span&gt;, ожидая увидеть там адрес страницы авторизации (а то ходят тут всякие...).&lt;br /&gt;&lt;/li&gt;&lt;/ol&gt;Второе ограничение можно, конечно, обойти, вбив нужный заголовок руками: &lt;span style="font-style: italic;"&gt;"Referer: адрес страницы авторизации"&lt;/span&gt;-- эта бесчестная практика называется &lt;span style="font-style: italic;"&gt;"&lt;a href="http://en.wikipedia.org/wiki/Referrer_spoofing"&gt;Referer spoofing&lt;/a&gt;"&lt;/span&gt;, но лучше (да и надежнее) все-таки соблюдать приличия.&lt;br /&gt;&lt;br /&gt;&lt;h2&gt;Авторизация средствами urllib2&lt;br /&gt;&lt;/h2&gt;Кодом сессии клиент и сервер обычно обмениваются через механизм &lt;span style="font-style: italic;"&gt;cookie&lt;/span&gt;, о котором шла речь в &lt;a href="http://pi-code.blogspot.com/2009/01/10.html"&gt;предыдущей заметке&lt;/a&gt;. Поэтому при  использовании Python-библиотеки &lt;span style="font-style: italic;"&gt;urllib2 &lt;/span&gt;в первую очередь надо позаботиться о том, чтобы в экземпляр &lt;span style="font-style: italic;"&gt;OpenDirector &lt;/span&gt;был включен &lt;span style="font-style: italic;"&gt;HTTPCookieProcessor&lt;/span&gt;. В &lt;a href="http://pi-code.blogspot.com/2009/01/10.html"&gt;заметке, посвященной cookies&lt;/a&gt;, использовался&lt;br /&gt;экземпляр стандартного &lt;span style="font-style: italic;"&gt;HTTPCookieProcessor&lt;/span&gt;-а. Он работает &lt;span style="font-style: italic;"&gt;out of the box&lt;/span&gt;. Но если требуется расширенная функциональность -- к примеру, возможность сохранять полученные коды авторизации в файле, удобнее определить класс-наследник:&lt;br /&gt;&lt;h3&gt;Поддержка cookies&lt;/h3&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;&lt;br /&gt;import cookielib&lt;br /&gt;from urllib2 import HTTPCookieProcessor&lt;br /&gt;COOKIEFILE = '...' # путь к файлу, где хранятся cookies&lt;br /&gt;class SessionCookieHandler(HTTPCookieProcessor):&lt;br /&gt;  """&lt;br /&gt;  Загружает и сохраняет куки.&lt;br /&gt;  """&lt;br /&gt;  def __init__(self):&lt;br /&gt;      cjar = cookielib.LWPCookieJar()&lt;br /&gt;      if os.path.isfile(COOKIEFILE):&lt;br /&gt;          cjar.load(COOKIEFILE)&lt;br /&gt;  &lt;br /&gt;      HTTPCookieProcessor.__init__(self, cjar)&lt;br /&gt;&lt;br /&gt;  def save_cookies(self):&lt;br /&gt;      """ Сохранение кук"""&lt;br /&gt;      self.cookiejar.save(COOKIEFILE)&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;br /&gt;В код инициализации краулера (напомню, что в самом начале этого цикла заметок был создан &lt;a href="http://pi-code.blogspot.com/2008/12/2.html"&gt;класс &lt;span&gt;UserAgent&lt;/span&gt;&lt;/a&gt;, который затем совершенствовался) следует включить такой фрагмент:&lt;br /&gt;&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;&lt;br /&gt;self.cookies_handler = SessionCookieHandler()&lt;br /&gt;handlers = [&lt;br /&gt;  self.cookieshandler,&lt;br /&gt;  # другие нестандартные handler-ы&lt;br /&gt;]&lt;br /&gt;self.opener = urllib2.build_opener(*handlers)&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;h3&gt;POST-запрос&lt;br /&gt;&lt;/h3&gt;Класс &lt;span style="font-style: italic;"&gt;urllib.Request&lt;/span&gt; имеет второй аргумент -- &lt;span style="font-style: italic;"&gt;data&lt;/span&gt;. При непустом значении предполагается, что там содержатся параметры POST-запроса, которые выглядят примерно так: &lt;span style="font-style: italic;"&gt;param1=value1&amp;amp;param2=value2&lt;/span&gt;... Для создания такой строки используется функция из "соседнего" пакета &lt;span style="font-style: italic;"&gt;urllib&lt;/span&gt;:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;import urllib # не путать с urllib2!&lt;br /&gt;data = urllib.urlencode({'param1':'param1', 'param2':'value2',})&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;h3&gt;ESCAPE-последовательности и кирилица&lt;/h3&gt;Не проще ли создать строку параметров запроса руками? Нет, потому что функция &lt;span style="font-style: italic;"&gt;urlencode&lt;/span&gt; при необходимости конвертирует строки в ESCAPE-последовательности.&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;from urllib import urlencode&lt;br /&gt;urlencode({'name':'sergey krushinsky'})&lt;br /&gt;'name=sergey+krushinsky'&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;br /&gt;Тонкость работы &lt;span style="font-style: italic;"&gt;urlencode &lt;/span&gt;с кириллицей в том, что уникодные и байтовые строки возвращают разные результаты.&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;from urllib import urlencode, unquote&lt;br /&gt;s1 = urlencode({'name':'вася'})&lt;br /&gt;s2 = urlencode({'name':u'вася'})&lt;br /&gt;print unquote(s1).encode('1251')&lt;br /&gt;name=РІР°СЃСЏ&lt;br /&gt;print unquote(s2).encode('1251')&lt;br /&gt;name=вася&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;br /&gt;Запрос будет выглядеть так:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;req = urllib2.Request(url, post_data)&lt;br /&gt;handle = self.opener.open(req, None, TIMEOUT)&lt;br /&gt;...&lt;br /&gt;self.self.cookies_handler.save_cookies()&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;br /&gt;&lt;h2&gt;Скрапер как "конечный автомат"&lt;/h2&gt;Скрапер удобнее всего написать, используя классическую идиому под названием "&lt;a style="font-style: italic;" href="http://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BD%D0%B5%D1%87%D0%BD%D1%8B%D0%B9_%D0%B0%D0%B2%D1%82%D0%BE%D0%BC%D0%B0%D1%82"&gt;конечный автомат&lt;/a&gt;".&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;&lt;br /&gt;#!/usr/bin/python&lt;br /&gt;# -*- coding:  utf8 -*-&lt;br /&gt;#########################################################################&lt;br /&gt;# Scrapper&lt;br /&gt;# author: Sergey Krushinsky&lt;br /&gt;# created: 2009-01-25&lt;br /&gt;#########################################################################&lt;br /&gt;from crawler import UserAgent&lt;br /&gt;from parsers import extract_text&lt;br /&gt;import urllib&lt;br /&gt;import logging&lt;br /&gt;logging.basicConfig(&lt;br /&gt;  level=logging.DEBUG,&lt;br /&gt;  format='%(asctime)s %(levelname)-8s %(message)s',&lt;br /&gt;  datefmt='%Y-%m-%d %H:%M:%S',&lt;br /&gt;)&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;class Scrapper(object):&lt;br /&gt;  """&lt;br /&gt;  Пример авторизации на сайте 'www.kinokopilka.ru'&lt;br /&gt;  через POST-форму.&lt;br /&gt;  """&lt;br /&gt;  def __init__(self):&lt;br /&gt;      self.crawler = UserAgent(&lt;br /&gt;          #agentname='Mozilla/5.0 (Windows; U; Windows NT 5.1; ru; rv:1.9.0.5) Gecko/2008120122 Firefox/3.0.5',&lt;br /&gt;          ignore_robots=True&lt;br /&gt;      )&lt;br /&gt;    &lt;br /&gt;      # страница с формой&lt;br /&gt;      self.signin_url = 'http://www.kinokopilka.ru/account/signin'&lt;br /&gt;      # адрес, куда отправляется POST-запрос&lt;br /&gt;      self.login_url = 'http://www.kinokopilka.ru/account/login'&lt;br /&gt;      # адрес, куда сервер должен перенаправить клиента&lt;br /&gt;      # после авторизации&lt;br /&gt;      self.expected_redirect = 'http://www.kinokopilka.ru/movies'&lt;br /&gt;&lt;br /&gt;      self.form = {&lt;br /&gt;          'login': 'ЛОГИН',&lt;br /&gt;          'password': 'ПАРОЛЬ',&lt;br /&gt;          'remember': 'true',&lt;br /&gt;          'commit_login': 'Войти', # кнопка&lt;br /&gt;      }&lt;br /&gt;    &lt;br /&gt;      # исходное стостояние "конечного автомата"&lt;br /&gt;      # по мере выполнения задач, значением становится очередное&lt;br /&gt;      # состояние.&lt;br /&gt;      self.state = 'signin'&lt;br /&gt;       &lt;br /&gt;       &lt;br /&gt;  def error(self):&lt;br /&gt;      """&lt;br /&gt;      Состояние ошибки. Прекращает выполнение.&lt;br /&gt;      """&lt;br /&gt;      logging.debug('Error state')&lt;br /&gt;      self.state = None&lt;br /&gt;&lt;br /&gt;  def completed(self):&lt;br /&gt;      """&lt;br /&gt;      Состояние успешного завершения действий.&lt;br /&gt;      Прекращает выполнение.&lt;br /&gt;      """      &lt;br /&gt;      logging.debug('Completed')&lt;br /&gt;      # TODO: на практике, должно включаться следующее состояние,&lt;br /&gt;      # которое выполняет что-то полезное -- например, поиск&lt;br /&gt;      # новых фильмов&lt;br /&gt;      self.state = None&lt;br /&gt;    &lt;br /&gt;  def login(self):&lt;br /&gt;      """&lt;br /&gt;      Логин при помощи формы&lt;br /&gt;      """&lt;br /&gt;      def _handle_success(url, data, info):&lt;br /&gt;          if (url != self.expected_redirect):&lt;br /&gt;              logging.error('Expected redirect to %s. Got: %s' % \&lt;br /&gt;                        (self.expected_redirect, url))&lt;br /&gt;              self.state = 'error'&lt;br /&gt;          else:&lt;br /&gt;              text = extract_text(data)&lt;br /&gt;              # print 'text: %s' % text&lt;br /&gt;              # В случае успешной авторизации на странице будет приветствие:&lt;br /&gt;              # "Здравствуйте, имя_пользователя", где имя пользователя&lt;br /&gt;              # совпадает со значением 'login' в форме.&lt;br /&gt;              if text.find(self.form['login']) &gt; -1:&lt;br /&gt;                  self.state = 'completed'&lt;br /&gt;              else:&lt;br /&gt;                  logging.debug('No greeting text found')&lt;br /&gt;                  self.state = 'error'&lt;br /&gt;             &lt;br /&gt;      def _handle_failure(url, err):&lt;br /&gt;          logging.error("Could not login: %s: %s" % (url, err))  &lt;br /&gt;          self.state = 'error'&lt;br /&gt;        &lt;br /&gt;      data = urllib.urlencode(self.form)&lt;br /&gt;      self.crawler.visit(self.login_url,&lt;br /&gt;                         on_success=_handle_success,&lt;br /&gt;                         on_failure=_handle_failure,&lt;br /&gt;                         post_data=data) &lt;br /&gt;      &lt;br /&gt;  def signin(self):&lt;br /&gt;      """&lt;br /&gt;      Получение страницы с формой и всех cookies.&lt;br /&gt;      """&lt;br /&gt;      def _handle_failure(url, err):&lt;br /&gt;          logging.error("Could not signin: %s: %s" % (url, err))&lt;br /&gt;          self.state = 'error'&lt;br /&gt;    &lt;br /&gt;      def _handle_success(url, data, info):&lt;br /&gt;          self.state = 'login'&lt;br /&gt;&lt;br /&gt;      self.crawler.visit(self.signin_url,&lt;br /&gt;                         on_success=_handle_success,&lt;br /&gt;                         on_failure=_handle_failure)&lt;br /&gt;&lt;br /&gt;  def run(self):&lt;br /&gt;      """&lt;br /&gt;      Главный метод "конечного автомата".&lt;br /&gt;      """&lt;br /&gt;      while self.state:&lt;br /&gt;          logging.info('-' * 50)&lt;br /&gt;          logging.info("Entering '%s' state" % self.state)&lt;br /&gt;          logging.info('-' * 50)&lt;br /&gt;          # поиск функции, чье название соответствует состоянию.&lt;br /&gt;          fn = getattr(self, self.state)&lt;br /&gt;          fn()&lt;br /&gt;&lt;br /&gt;if __name__ == '__main__':&lt;br /&gt;  Scrapper().run()&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;br /&gt;***&lt;br /&gt;&lt;br /&gt;Должен признаться, что используя Perl и библиотеку &lt;span&gt;WWW::Mechanize&lt;/span&gt;, я написал быстрее, чем за час, скрипт, который делает то же самое. На отладку скрапера, чей код приведен выше,  пришлось потратить  изрядное количество времени... не буду говорить, сколько, чтобы меня не заподозрили в непрофессионализме. При том, что для краулера имеется набор функциональных тестов. Строчек кода в питоновской версии значительно больше. ...Эй, кто там восхвалял продуктивность Питона?&lt;br /&gt;&lt;br /&gt;Другое дело, что в Perl-версии использовалась специализированная библиотека. Справедливости ради, надо отметить, что и в Python-е имеется ее аналог под названием  &lt;a href="http://wwwsearch.sourceforge.net/mechanize/"&gt;mechanize&lt;/a&gt;. Она состоит из довольно большого количества классов, которые встраиваются в &lt;span style="font-style: italic;"&gt;urlllib2&lt;/span&gt;, расширяя ее возможности, а иногда подменяют существующие там классы. Но если документацию к WWW::Mechanize достаточно бегло просмотреть в течение ровно пяти минут, чтобы начать писать что-то полезное, то с этим хозяйством предстоит разбираться долго. Когда-нибудь я это обязательно сделаю.  А пока лучше посмотрю фильм, полученное с "Кинокопилки"...&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/964245207178906174-376610688795826501?l=pi-code.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://pi-code.blogspot.com/feeds/376610688795826501/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=964245207178906174&amp;postID=376610688795826501' title='Комментарии: 0'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/376610688795826501'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/376610688795826501'/><link rel='alternate' type='text/html' href='http://pi-code.blogspot.com/2009/01/11.html' title='Краулер своими руками. Часть 11'/><author><name>Наувул-Наувул</name><uri>http://www.blogger.com/profile/06702180511726415678</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/_3ifHLhKgDjk/TCEZ2cVuxJI/AAAAAAAAB2E/D5uehCRtVtI/S220/IMG_3814.JPG'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-964245207178906174.post-6476411228054332500</id><published>2009-01-16T00:11:00.007+03:00</published><updated>2009-01-16T01:38:44.335+03:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='cookies'/><category scheme='http://www.blogger.com/atom/ns#' term='питон'/><category scheme='http://www.blogger.com/atom/ns#' term='HTTPCookieProcessor'/><category scheme='http://www.blogger.com/atom/ns#' term='cookielib'/><category scheme='http://www.blogger.com/atom/ns#' term='паук'/><category scheme='http://www.blogger.com/atom/ns#' term='urllib2'/><category scheme='http://www.blogger.com/atom/ns#' term='краулер'/><category scheme='http://www.blogger.com/atom/ns#' term='Python'/><title type='text'>Краулер своими руками. Часть 10</title><content type='html'>&lt;h2&gt;Ярлычки на чемодане&lt;/h2&gt;В некоторых случаях взаимодействие клиента с сервером невозможно без обмена порциями данных, получивших поэтическое имя &lt;span style="font-style: italic;"&gt;cookies &lt;/span&gt;-- "печенье". Сценарий таков:&lt;br /&gt;&lt;ol&gt;&lt;li&gt;При первом обращении к серверу клиент приходит с пустыми руками. Сервер "видит" это и передает ему HTTP-заголовок &lt;span style="font-style: italic;"&gt;"Set-Cookie:данные"&lt;/span&gt;. В данных может быть уникальный идентификатор или какие-то настройки -- скажем, языковые.&lt;/li&gt;&lt;li&gt;Клиент запоминает эти данные и при повторном обращении снова возвращает их серверу через заголовок &lt;span style="font-style: italic;"&gt;"Cookie:данные"&lt;/span&gt;.&lt;/li&gt;&lt;li&gt;Сервер читает заголовок &lt;span style="font-style: italic;"&gt;"Cookie"&lt;/span&gt; и распознает клиента.&lt;/li&gt;&lt;/ol&gt;Таким образом достигается постоянство сессии. Без этого было бы трудно, к примеру, при переходе с одной страницы интернет-магазина на другую видеть корзину покупателя в неизменном состоянии. На практике сервер часто при первом ответе "закрепляет" за клиентом куку (как ярлычок на чемодане), сразу перенаправляет его на ту же самую страницу и только после этого начинает полноценно взаимодействовать.&lt;br /&gt;&lt;br /&gt;***&lt;br /&gt;&lt;span style="font-style: italic;"&gt;OpenDirector&lt;/span&gt;, который используется в библиотеке &lt;span style="font-style: italic;"&gt;urllib2 &lt;/span&gt;по умолчанию, не работает с &lt;span style="font-style: italic;"&gt;Cookie&lt;/span&gt;. В наборе готовых handler-ов имеется &lt;span style="font-style: italic;"&gt;HTTPCookieProcessor&lt;/span&gt;, но если не подключить его руками, он не будет задействован. Подключить его можно таким образом:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;import cookielib&lt;br /&gt;COOKIEFILE = '...' # путь к файлу, где хранятся cookies&lt;br /&gt;&lt;br /&gt;cookie_jar = cookielib.LWPCookieJar()&lt;br /&gt;# загрузить сохраненные cookies, если файл существует&lt;br /&gt;if os.path.isfile(COOKIEFILE):&lt;br /&gt;  cookie_jar.load(COOKIEFILE)&lt;br /&gt;&lt;br /&gt;cookie_processor = urllib2.HTTPCookieProcessor(cookie_jar)&lt;br /&gt;opener = urllib2.build_opener(cookie_processor, другие handler-ы)&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;Теперь библиотека urllib2 будет автоматически читать и передавать назад данные, полученные через &lt;span style="font-style: italic;"&gt;cookies&lt;/span&gt;.&lt;br /&gt;После выполнения запроса надо сохранить полученные &lt;span style="font-style: italic;"&gt;cookies&lt;/span&gt;:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;req = urllib2.Request(url, None)&lt;br /&gt;...&lt;br /&gt;handle = self.opener.open(req, None, TIMEOUT)&lt;br /&gt;cookie_jar.save(COOKIEFILE)&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;Все эти операции легко включить в класс &lt;span style="font-style: italic;"&gt;UserAgent&lt;/span&gt;.&lt;br /&gt;&lt;br /&gt;Библиотека &lt;span style="font-style: italic;"&gt;cookielib&lt;/span&gt; появилась в Python 2.4. До этого использовался класс &lt;span style="font-style: italic;"&gt;ClientCookie&lt;/span&gt;. О том, как сделать работу с &lt;span style="font-style: italic;"&gt;cookies&lt;/span&gt; независимой от версий питона, подробно рассказано в статье &lt;a href="http://www.voidspace.org.uk/python/articles/cookielib.shtml"&gt;cookielib and ClientCookie на www.voidspace.org.uk&lt;/a&gt;.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/964245207178906174-6476411228054332500?l=pi-code.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://pi-code.blogspot.com/feeds/6476411228054332500/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=964245207178906174&amp;postID=6476411228054332500' title='Комментарии: 3'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/6476411228054332500'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/6476411228054332500'/><link rel='alternate' type='text/html' href='http://pi-code.blogspot.com/2009/01/10.html' title='Краулер своими руками. Часть 10'/><author><name>Наувул-Наувул</name><uri>http://www.blogger.com/profile/06702180511726415678</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/_3ifHLhKgDjk/TCEZ2cVuxJI/AAAAAAAAB2E/D5uehCRtVtI/S220/IMG_3814.JPG'/></author><thr:total>3</thr:total></entry><entry><id>tag:blogger.com,1999:blog-964245207178906174.post-3551171621480206415</id><published>2009-01-13T11:13:00.012+03:00</published><updated>2009-01-13T16:52:43.919+03:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='StringIO'/><category scheme='http://www.blogger.com/atom/ns#' term='питон'/><category scheme='http://www.blogger.com/atom/ns#' term='сжатие'/><category scheme='http://www.blogger.com/atom/ns#' term='gzip'/><category scheme='http://www.blogger.com/atom/ns#' term='callback'/><category scheme='http://www.blogger.com/atom/ns#' term='паук'/><category scheme='http://www.blogger.com/atom/ns#' term='краулер'/><category scheme='http://www.blogger.com/atom/ns#' term='Python'/><title type='text'>Краулер своими руками. Часть 9</title><content type='html'>Пора заняться усовершенствованием краулера.&lt;br /&gt;&lt;br /&gt;&lt;h2&gt;Сжатие контента&lt;/h2&gt;Большинство серверов умеют сжимать передаваемый контент, а браузеры, соответственно,-- разжимать. Делается это ради экономии трафика. Только вначале клиент и сервер должны договориться об этом между собой.&lt;br /&gt;&lt;ul&gt;&lt;li&gt;Клиент должен отправить в запросе заголовок&lt;span style="font-style: italic;"&gt; Accept-encoding&lt;/span&gt;: алгоритм сжатия(например, &lt;span style="font-style: italic;"&gt;gzip&lt;/span&gt;).&lt;br /&gt;&lt;/li&gt;&lt;li&gt;Если сервер  умеет сжимать страницу указанным алгоритмом, он это сделает и вернет заголовок &lt;span style="font-style: italic;"&gt;Content-Encoding&lt;/span&gt;: алгоритм сжатия.&lt;/li&gt;&lt;li&gt;Клиент проверяет заголовок Content-Encoding и если там указано сжатие, распаковывает данные.&lt;/li&gt;&lt;/ul&gt;До недавних пор распаковывать налету сжатый контент средствами питоновских библиотек было непросто. Xah Lee даже использовал официальную документацию к модулю &lt;span style="font-style: italic;"&gt;gzip &lt;/span&gt;как &lt;a href="http://bytes.com/groups/python/170666-python-doc-problem-example-gzip-module-reprise"&gt;пример неудачной документации&lt;/a&gt;. У меня год-два назад не получалось распаковывать сжатый web-контент, как ни старался.  Но, видимо, в текущей версии недоработки были исправлены. Сейчас простой рецепт приведен в &lt;a href="http://diveintopython.org/http_web_services/gzip_compression.html"&gt;Dive Into Python&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;Метод &lt;span style="font-style: italic;"&gt;visit&lt;/span&gt;, добавленный в класс &lt;span style="font-style: italic;"&gt;UserAgent&lt;/span&gt;, представляет собой обертку вокруг &lt;span style="font-style: italic;"&gt;open&lt;/span&gt;:&lt;br /&gt;&lt;span style="font-family:monospace;"&gt;&lt;/span&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;...&lt;br /&gt;from StringIO import StringIO&lt;br /&gt;import gzip&lt;br /&gt;&lt;br /&gt;class UserAgent(object):&lt;br /&gt; ...&lt;br /&gt;&lt;br /&gt; def open(self, url, add_headers=None):&lt;br /&gt;     """&lt;br /&gt;     Возвращает file-like object, полученный с заданного адреса.&lt;br /&gt;     В случае ошибки возвращает HTTPError, URLError или IOError.&lt;br /&gt;     """&lt;br /&gt;     logging.info('Opening %s...' % url)&lt;br /&gt;     req = urllib2.Request(url, None)&lt;br /&gt;     if add_headers:&lt;br /&gt;         for k, v in add_headers.iteritems():&lt;br /&gt;             req.add_header(k, v)&lt;br /&gt;     handle = self.opener.open(req, None, TIMEOUT)&lt;br /&gt;&lt;br /&gt;     return handle&lt;br /&gt;&lt;br /&gt; def gunzip(self, stream):&lt;br /&gt;     gz = gzip.GzipFile(fileobj=stream)&lt;br /&gt;     return StringIO(gz.read())&lt;br /&gt;&lt;br /&gt; def visit(self, url, add_headers={}, on_success=None, on_failure=None):&lt;br /&gt;     """&lt;br /&gt;     Возвращает последний пройденный URL, объект StringIO и info, полученные&lt;br /&gt;     с заданного адреса через callback-метод on_success.&lt;br /&gt;     В случае ошибки вызывает метод on_failure, передавая туда исклчение.&lt;br /&gt;  &lt;br /&gt;     Фактически, это обертка вокруг open. Важное отличие в том, что&lt;br /&gt;     этот метод автоматически разворачивает сжатый контент.&lt;br /&gt;     """&lt;br /&gt;     add_headers.update({'Accept-encoding': 'gzip'})&lt;br /&gt;     try:&lt;br /&gt;         r = self.open(url, add_headers)&lt;br /&gt;         s = StringIO(r.read())&lt;br /&gt;         info = r.info()&lt;br /&gt;         if info.get('Content-Encoding') == 'gzip':&lt;br /&gt;             logging.debug('Gzipped content received')&lt;br /&gt;             stream = self.gunzip(s)&lt;br /&gt;         else:&lt;br /&gt;             stream = s&lt;br /&gt;         stream.seek(0)&lt;br /&gt;         on_success(r.geturl(), stream, info)&lt;br /&gt;     except Exception, e:&lt;br /&gt;         on_failure(url, e)&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;ul&gt;&lt;li&gt;старый метод open принимает дополнительные заголовки.&lt;/li&gt;&lt;li&gt;новый метод &lt;span style="font-style: italic;"&gt;visit &lt;/span&gt;читает данные и "заворачивает" их в &lt;span style="font-style: italic;"&gt;StringIO&lt;/span&gt;&lt;/li&gt;&lt;li&gt;результат возвращается в callback-функции on_success и on_failure.&lt;br /&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h3&gt;Почему используются callback-функции?&lt;/h3&gt;В случае успешного запроса, могут понадобиться три результата:&lt;br /&gt;&lt;ol&gt;&lt;li&gt;содержание страницы&lt;br /&gt;&lt;/li&gt;&lt;li&gt;заголовки ответа&lt;/li&gt;&lt;li&gt;адрес, с которого были получены результаты (в случае редиректов он не совпадает с исходным адресом)&lt;/li&gt;&lt;/ol&gt;Можно, конечно, вернуть список результатов, но по-моему, это не очень красиво и удобно с точки зрения поддержки. Нехорошо когда функция возвращает больше одного значения.  И не всегда будут нужны сразу три результата. В последнем случае &lt;span style="font-style: italic;"&gt;callback&lt;/span&gt;-функция может быть объявлена без указания лишних аргументов:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;def handle_success(*args):&lt;br /&gt;ctype = args[2].get('Content-Type')&lt;br /&gt;...&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;Мне это больше нравится, чем:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;results = visit(аргументы)&lt;br /&gt;ctype = results[2].get('Content-Type')&lt;/div&gt;&lt;/div&gt;Впрочем, дело вкуса...&lt;br /&gt;&lt;br /&gt;Старый метод &lt;span style="font-style: italic;"&gt;open&lt;/span&gt; теперь снаружи практически не нужен.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/964245207178906174-3551171621480206415?l=pi-code.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://pi-code.blogspot.com/feeds/3551171621480206415/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=964245207178906174&amp;postID=3551171621480206415' title='Комментарии: 0'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/3551171621480206415'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/3551171621480206415'/><link rel='alternate' type='text/html' href='http://pi-code.blogspot.com/2009/01/9.html' title='Краулер своими руками. Часть 9'/><author><name>Наувул-Наувул</name><uri>http://www.blogger.com/profile/06702180511726415678</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/_3ifHLhKgDjk/TCEZ2cVuxJI/AAAAAAAAB2E/D5uehCRtVtI/S220/IMG_3814.JPG'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-964245207178906174.post-545390551219272686</id><published>2009-01-10T12:16:00.012+03:00</published><updated>2009-01-10T13:49:01.620+03:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='текст'/><category scheme='http://www.blogger.com/atom/ns#' term='minidom'/><category scheme='http://www.blogger.com/atom/ns#' term='BeautifulSoup'/><category scheme='http://www.blogger.com/atom/ns#' term='DOM'/><category scheme='http://www.blogger.com/atom/ns#' term='краулер'/><category scheme='http://www.blogger.com/atom/ns#' term='HTML-парсер'/><category scheme='http://www.blogger.com/atom/ns#' term='питон'/><category scheme='http://www.blogger.com/atom/ns#' term='html5lib'/><category scheme='http://www.blogger.com/atom/ns#' term='рекурсия'/><category scheme='http://www.blogger.com/atom/ns#' term='тест'/><category scheme='http://www.blogger.com/atom/ns#' term='паук'/><category scheme='http://www.blogger.com/atom/ns#' term='Sanitizer'/><category scheme='http://www.blogger.com/atom/ns#' term='Python'/><title type='text'>Краулер своими руками. Часть 8</title><content type='html'>&lt;h2&gt;Извлечение текста из HTML&lt;/h2&gt;В &lt;a href="http://pi-code.blogspot.com/2009/01/5.html"&gt;пятой части&lt;/a&gt; этой серии заметок HTML-парсер &lt;a href="http://www.crummy.com/software/BeautifulSoup/"&gt;BeautifulSoup&lt;/a&gt; был заменен на &lt;a href="http://code.google.com/p/html5lib/"&gt;html5lib&lt;/a&gt;. Справится ли новая библиотека с извлечением текста из HTML так же хорошо, как с извлечением сcылок? Ответ на этот вопрос будет критическим для всего проекта. Потому что от краулера, который умеет двигаться, но не умеет говорить, проку мало.&lt;br /&gt;&lt;br /&gt;Поскольку парсер возвращает DOM-дерево (точнее &lt;span style="font-style: italic;"&gt;minidom&lt;/span&gt;), для извлечения текста применяется тривиальный рекурсивный обход узлов:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;&lt;br /&gt;from html5lib import treebuilders&lt;br /&gt;&lt;br /&gt;IGNORED_ELEMENTS = ('script')&lt;br /&gt;&lt;br /&gt;...&lt;br /&gt;&lt;br /&gt;def build_dom(fileobj):&lt;br /&gt; """&lt;br /&gt; Читает fileobj и возвращает дерево minidom.&lt;br /&gt; """&lt;br /&gt; #HTMLSanitizer дает странные результаты&lt;br /&gt; #parser = html5lib.HTMLParser(tree=treebuilders.getTreeBuilder('dom'),&lt;br /&gt; #                             tokenizer=sanitizer.HTMLSanitizer)&lt;br /&gt; parser = html5lib.HTMLParser(tree=treebuilders.getTreeBuilder('dom'))&lt;br /&gt; return parser.parse(fileobj)&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;def extract_text(fileobj):&lt;br /&gt; """&lt;br /&gt; Извлекает текст из HTML-страницы. Результат в кодировке utf-8.&lt;br /&gt; fileobj -- файло-подобный объект.&lt;br /&gt; """&lt;br /&gt; def visit(node):&lt;br /&gt;     if node.nodeType == node.TEXT_NODE:&lt;br /&gt;         return node.data.strip()&lt;br /&gt;     elif node.nodeType == node.ELEMENT_NODE \&lt;br /&gt;          and not node.tagName in IGNORED_ELEMENTS \&lt;br /&gt;          and node.hasChildNodes():&lt;br /&gt;&lt;br /&gt;         resulttext = ''&lt;br /&gt;         for child in node.childNodes:&lt;br /&gt;             subtext = visit(child)&lt;br /&gt;             if subtext:&lt;br /&gt;                 resulttext = '%s %s' % (resulttext, subtext)&lt;br /&gt;         return resulttext&lt;br /&gt;     return None&lt;br /&gt;&lt;br /&gt; dom = build_dom(fileobj)&lt;br /&gt;&lt;br /&gt; text = visit(dom.getElementsByTagName('body')[0])&lt;br /&gt; dom.unlink()&lt;br /&gt; return text&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;ul&gt;&lt;li&gt;DOM-дерево получается тем же способом, что и при извлечении ссылок. Поэтому я вынес парсинг в отдельную функцию &lt;span style="font-style: italic;"&gt;'build_dom&lt;/span&gt;'. Только от &lt;span style="font-style: italic;"&gt;Sanitizer&lt;/span&gt;-а пришлось отказаться (см. закомментированные строки) -- с ним в результат, помимо чистого текста попадали HTML-теги, уж не знаю, почему.&lt;br /&gt;&lt;/li&gt;&lt;li&gt;Поиск начинается с содержимого элемента 'body'.&lt;/li&gt;&lt;li&gt;Если текущий узел -- текстовый (node.TEXT_NODE), возвращается его содержимое.&lt;/li&gt;&lt;li&gt;Если текущий узел -- элемент (node.ELEMENT_NODE),  имеющий потомков и не относящийся к числу игнорируемых элементов, потомки проверяются один за другим. Текст, добытый из каждой дочерней ветки, добавляется через пробел к уже собранному тексту.&lt;br /&gt;&lt;/li&gt;&lt;/ul&gt;Получается одна длинная строка.&lt;br /&gt;Вот простейший тест:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;class TestTextExtractor(unittest.TestCase):&lt;br /&gt; def setUp(self):&lt;br /&gt;     self.user_agent = crawler.UserAgent()&lt;br /&gt;&lt;br /&gt; def test_utf8_source(self):&lt;br /&gt;     page_url = 'http://krushinsky.blogspot.com/'&lt;br /&gt;     fileobj = self.user_agent.open(page_url)&lt;br /&gt;     txt = extract_text(fileobj)&lt;br /&gt;     print txt&lt;br /&gt;     self.assertTrue(txt, 'No text was extracted from %s' % page_url)&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;Результаты выглядят неплохо:&lt;br /&gt;&lt;blockquote&gt;&lt;span style="font-family:courier new;"&gt;Фото -субъектив     четверг, Декабрь 18, 2008   Вторая Табачная Экспедиция  Утром отправился в экспедицию за табаком. Шла метель, дороги стали скользкими. Я впервые познакомился с заносами. Ехал предельно осторожно, на поворотах замедлялся и страховался ногами... &lt;/span&gt;&lt;br /&gt;&lt;/blockquote&gt;&lt;br /&gt;Функцию  извлечения текста из HTML наверняка придется еще совершенствовать,  как и тесты, но главное: с этим уже можно работать.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/964245207178906174-545390551219272686?l=pi-code.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://pi-code.blogspot.com/feeds/545390551219272686/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=964245207178906174&amp;postID=545390551219272686' title='Комментарии: 0'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/545390551219272686'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/545390551219272686'/><link rel='alternate' type='text/html' href='http://pi-code.blogspot.com/2009/01/8.html' title='Краулер своими руками. Часть 8'/><author><name>Наувул-Наувул</name><uri>http://www.blogger.com/profile/06702180511726415678</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/_3ifHLhKgDjk/TCEZ2cVuxJI/AAAAAAAAB2E/D5uehCRtVtI/S220/IMG_3814.JPG'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-964245207178906174.post-4673719719559195781</id><published>2009-01-09T12:00:00.010+03:00</published><updated>2009-01-09T17:43:07.086+03:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='Java'/><category scheme='http://www.blogger.com/atom/ns#' term='file-like object'/><category scheme='http://www.blogger.com/atom/ns#' term='callback'/><category scheme='http://www.blogger.com/atom/ns#' term='httplib'/><category scheme='http://www.blogger.com/atom/ns#' term='seek'/><category scheme='http://www.blogger.com/atom/ns#' term='краулер'/><category scheme='http://www.blogger.com/atom/ns#' term='StringIO'/><category scheme='http://www.blogger.com/atom/ns#' term='питон'/><category scheme='http://www.blogger.com/atom/ns#' term='ссылки'/><category scheme='http://www.blogger.com/atom/ns#' term='urllib2'/><category scheme='http://www.blogger.com/atom/ns#' term='паук'/><category scheme='http://www.blogger.com/atom/ns#' term='HEAD'/><category scheme='http://www.blogger.com/atom/ns#' term='Python'/><title type='text'>Краулер своими руками. Часть 7</title><content type='html'>Программа для проверки ссылок, о которой впервые зашла речь в &lt;a href="http://pi-code.blogspot.com/2009/01/4.html"&gt;четвертой заметке&lt;/a&gt;, годится разве что в качестве иллюстрации&lt;span style="font-style: italic;"&gt;&lt;/span&gt;. Как инструмент она бесполезна.&lt;br /&gt;&lt;ul&gt;&lt;li&gt;Одного перечня неработающих ссылок недостаточно; важно знать, на каких страницах сайта находятся эти ссылки, чтобы можно было внести исправления.&lt;/li&gt;&lt;li&gt;Обход сайта и проверка ссылок в первой версии -- фактически, одно и то же. Значит, нет возможности проверить работоспособность внешних ссылок и вообще всего, что отбрасывается фильтром &lt;span style="font-style: italic;"&gt;is_valid_link&lt;/span&gt;.&lt;/li&gt;&lt;/ul&gt;Пускай это будет в ущерб производительности, но придется построить программу немного иначе. При успешном открытии страницы надо извлекать из нее ссылки, несмотря на то, что это уже делается один раз внутри функции &lt;span style="font-style: italic;"&gt;traverse&lt;/span&gt;. А потом проверять каждую запросом HEAD.&lt;br /&gt;&lt;h2&gt;&lt;br /&gt;&lt;/h2&gt;&lt;h2&gt;HEAD-запрос&lt;/h2&gt;Сделать HEAD-запрос в Python-е проще всего средствами &lt;span style="font-style: italic;"&gt;httplib&lt;/span&gt;. Библиотека &lt;span style="font-style: italic;"&gt;urllib2 &lt;/span&gt;это тоже позволяет, но придется писать много лишнего кода.  Функция, представленная ниже, возвращает код ответа на HTTP-запрос или 0, если соединение не состоялось. Этого достаточно, чтобы узнать, жива ли страница.&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;from urlparse import urlparse&lt;br /&gt;import httplib&lt;br /&gt;&lt;br /&gt;def request_head(url):&lt;br /&gt;parts = urlparse(url)&lt;br /&gt;conn = None&lt;br /&gt;try:&lt;br /&gt; conn = httplib.HTTPConnection(parts.netloc)&lt;br /&gt; conn.request('HEAD', parts.path, parts.params)&lt;br /&gt; return conn.getresponse().status&lt;br /&gt;except:&lt;br /&gt; return 0&lt;br /&gt;finally:&lt;br /&gt; if conn:&lt;br /&gt;     conn.close()&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;И методы, использующие эти запросы:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;&lt;br /&gt;# если при попытке открыть ссылку возвращается один из этих кодов,&lt;br /&gt;# ссылка считается неработающей&lt;br /&gt;BAD_CODES = (301, 303, 307, 404, 410, 500, 501, 502, 503, 504)&lt;br /&gt;&lt;br /&gt;for link in links_iterator(response, is_http_link ):&lt;br /&gt; status = passed.setdefault(&lt;br /&gt;     link,&lt;br /&gt;     request_head(link)&lt;br /&gt; )               &lt;br /&gt;                &lt;br /&gt; if not status or status in BAD_CODES:&lt;br /&gt;     print '%s --&gt; %s: %s ' % (url, hostname, status)&lt;br /&gt;...&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;Выглядит неплохо. Но не работает.  Корневая страница открывается, как положено, из нее извлекаются новые ссылки. После чего скрипт благополучно завершает свою работу. Обхода вглубь не происходит.&lt;br /&gt;&lt;br /&gt;Проблема в том, что после парсинга страницы внутри &lt;span style="font-style: italic;"&gt;callback&lt;/span&gt;-метода &lt;span style="font-style: italic;"&gt;test_links&lt;/span&gt; из файло-подобного объекта (&lt;span style="font-style: italic;"&gt;file-like object&lt;/span&gt;), который возвращает метод &lt;span style="font-style: italic;"&gt;opener.open()&lt;/span&gt;, уже невозможно ничего прочесть. Первая идея, приходящая в голову: применить метод файла &lt;span style="font-style: italic;"&gt;seek(0)&lt;/span&gt;, чтобы вернуться в начало. Однако, вызов response.seek(0) ни к чему ни приводит, хотя Python не ругается, как непременно сделала бы Java.   &lt;span style="font-weight: bold;"&gt;&lt;br /&gt;&lt;/span&gt;&lt;h2&gt;&lt;br /&gt;&lt;/h2&gt;&lt;h2&gt;Утка или не утка?&lt;br /&gt;&lt;/h2&gt;В динамических языках любят идиому: &lt;span style="font-weight: bold; font-style: italic;"&gt;"Если нечто ходит, как утка -- значит, это утка"&lt;/span&gt;. Именно так устроено в питоне все, что называется "&lt;span style="font-style: italic;"&gt;file-like object&lt;/span&gt;", в том числе, результат &lt;span style="font-style: italic;"&gt;urllib2.Request.open()&lt;/span&gt;. Однако, метод &lt;span style="font-style: italic;"&gt;seek&lt;/span&gt; не работает. Если задуматься, нет ничего удивительного в том, что сокет работает иначе, чем файл. Другой вопрос: хорошо это или плохо когда нечто, что называется уткой, ходит, как положено утке, но отказывается нести яйца -- не лучше бы ее тогда назвать как-то иначе? Эта тема уже обсужалась в списке рассылки &lt;a href="http://mail.python.org/pipermail/python-bugs-list/2007-April/038239.html"&gt;python-bugs-list&lt;/a&gt;. Там же был предложен рецепт: как все-таки заставить файло-подобный объект одноразового использования перематываться. Для этого достаточно прочесть  данные из потока и "завернуть" их в StringIO, чтобы получить своего рода виртуальный файл.&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;import StringIO&lt;br /&gt;&lt;br /&gt;response = self.open(url)&lt;br /&gt;data = StringIO.StringIO(response.read())&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;h2&gt;Большая стирка&lt;/h2&gt;Теперь &lt;span style="font-style: italic;"&gt;seek &lt;/span&gt;работает, но вместе с &lt;span style="font-style: italic;"&gt;response &lt;/span&gt;исчезли и его дополнительные методы, такие как &lt;span style="font-style: italic;"&gt;info()&lt;/span&gt; и &lt;span style="font-style: italic;"&gt;geturl()&lt;/span&gt;.  Заголовки HTTP-ответа, которые можно было получить через &lt;span style="font-style: italic;"&gt;response.info()&lt;/span&gt;, уже недоступны вне &lt;span style="font-style: italic;"&gt;traverse&lt;/span&gt;, поскольку в функцию обратного вызова &lt;span style="font-style: italic;"&gt;on_success&lt;/span&gt; передается другой объект -- &lt;span style="font-style: italic;"&gt;StringIO&lt;/span&gt;.  Придется изменить функцию обратного вызова, добавив туда новый аргумент:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;on_success(url, response.info(), data)&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;В модуль &lt;span style="font-style: italic;"&gt;parsers.py&lt;/span&gt; тоже надо внести изменения. Поскольку теперь мы не можем получить базовый адрес для преобразования относительных адресов в абсолютные, используя &lt;span style="font-style: italic;"&gt;response.geturl()&lt;/span&gt;, придется передавать этот адрес как аргумент:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;def links_iterator(base, response, link_filter=None):&lt;br /&gt; """&lt;br /&gt; Итератор по ссылкам, найденным в документе.&lt;br /&gt; Аргументы:&lt;br /&gt; base     -- URL страницы, с которой делается запрос&lt;br /&gt; response -- поток ввода&lt;br /&gt; filter   -- функция, которая может быть использована для&lt;br /&gt;             отбора нужных ссылок. На входе: url, на выходе&lt;br /&gt;             True, если проверка прошла, иначе -- False&lt;br /&gt; Если параметр 'filter' не задан, итератор возвращает&lt;br /&gt; все найденные ссылки.&lt;br /&gt; """&lt;br /&gt; ...&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;Новая версия &lt;span style="font-style: italic;"&gt;UserAgent.traverse()&lt;/span&gt; выглядит так: &lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;def traverse(self, start_url, links_filter=None, on_success=None, on_failure=None):&lt;br /&gt; """&lt;br /&gt; Обход сети.&lt;br /&gt;&lt;br /&gt; start_url    -- исходный адрес&lt;br /&gt; links_filter -- функция для оценки очередной ссылки, полученной со&lt;br /&gt;                 страницы. При результате False не включается в очередь.&lt;br /&gt; on_success   -- callback-функция, которая вызывается при успешном&lt;br /&gt;                 открытии страницы с аргументами (url, response)&lt;br /&gt; on_failure   -- callback-функция, которая вызывается в случае неудачи&lt;br /&gt;                 с аргументами: (url, exception)&lt;br /&gt; """&lt;br /&gt; queue = [ start_url ]&lt;br /&gt; passed = set()&lt;br /&gt; last_url = None&lt;br /&gt; while queue:&lt;br /&gt;     logging.debug('Queue size: %d, Passed: %d ' % \&lt;br /&gt;                   (len(queue), len(passed)) )&lt;br /&gt;     url = queue.pop(0)&lt;br /&gt;     try:&lt;br /&gt;         if last_url:&lt;br /&gt;             r = self.open(url, {'Referer': last_url})&lt;br /&gt;         else:&lt;br /&gt;             r = self.open(url)&lt;br /&gt;         data = StringIO(r.read())&lt;br /&gt;         logging.debug('Success')&lt;br /&gt;         if on_success:&lt;br /&gt;             on_success(url, r.info(), data)&lt;br /&gt;         # извлекаем со страницы новые ссылки и добавляем их в очередь&lt;br /&gt;         data.seek(0)&lt;br /&gt;         new_links = [&lt;br /&gt;             u for u in links_iterator(&lt;br /&gt;                 url,&lt;br /&gt;                 data,&lt;br /&gt;                 lambda u: False if (u in passed or u in queue) \&lt;br /&gt;                                 else links_filter(u)&lt;br /&gt;             )&lt;br /&gt;         ]             &lt;br /&gt;         queue.extend(new_links)&lt;br /&gt;         logging.debug('Added %d new links' % len(new_links))&lt;br /&gt;   &lt;br /&gt;     except Exception, ex:&lt;br /&gt;         logging.warn('Failure: %s' % ex)&lt;br /&gt;         if on_failure:&lt;br /&gt;             on_failure(url, ex)&lt;br /&gt;     finally:&lt;br /&gt;         last_url = url&lt;br /&gt;         passed.add(url)&lt;br /&gt;  &lt;br /&gt; logging.debug('Crawling completed. %d pages passed' % len(passed))&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt; Помимо "заворачивания" результата &lt;span style="font-style: italic;"&gt;self.open&lt;/span&gt; в &lt;span style="font-style: italic;"&gt;StringIO &lt;/span&gt;и изменения API callback-функции &lt;span style="font-style: italic;"&gt;on_success&lt;/span&gt;, есть и другие улучшения:&lt;br /&gt;&lt;ol&gt;&lt;li&gt;В очередной запрос, начиная со второго, добавляется заголовок '&lt;span style="font-style: italic;"&gt;Referer&lt;/span&gt;' для имитации поведения браузера. Некорые сайты проверяют его.&lt;/li&gt;&lt;li&gt;При извлечении ссылок со страницы вначале проверяется, не пройден ли уже адрес и нет ли его в очереди заданий и только потом, если эти условия выполняются, вызывается функция &lt;span style="font-style: italic;"&gt;link_filter&lt;/span&gt;. Раньше проверка происходила в другом порядке. Поскольку неизвестно заранее, сколько ресурсов потребуются на фильтрацию, лучше сразу отсекать лишнее и не дергать &lt;span style="font-style: italic;"&gt;link_filter&lt;/span&gt; лишний раз.&lt;/li&gt;&lt;li&gt;Добавился блок &lt;span style="font-style: italic;"&gt;finally&lt;/span&gt;.&lt;br /&gt;&lt;/li&gt;&lt;/ol&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/964245207178906174-4673719719559195781?l=pi-code.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://pi-code.blogspot.com/feeds/4673719719559195781/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=964245207178906174&amp;postID=4673719719559195781' title='Комментарии: 0'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/4673719719559195781'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/4673719719559195781'/><link rel='alternate' type='text/html' href='http://pi-code.blogspot.com/2009/01/7.html' title='Краулер своими руками. Часть 7'/><author><name>Наувул-Наувул</name><uri>http://www.blogger.com/profile/06702180511726415678</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/_3ifHLhKgDjk/TCEZ2cVuxJI/AAAAAAAAB2E/D5uehCRtVtI/S220/IMG_3814.JPG'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-964245207178906174.post-1906296343603893782</id><published>2009-01-05T13:30:00.008+03:00</published><updated>2009-01-05T14:04:03.466+03:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='относительные и абсолютные ссылки'/><category scheme='http://www.blogger.com/atom/ns#' term='html5lib'/><category scheme='http://www.blogger.com/atom/ns#' term='паук'/><category scheme='http://www.blogger.com/atom/ns#' term='краулер'/><category scheme='http://www.blogger.com/atom/ns#' term='Python'/><category scheme='http://www.blogger.com/atom/ns#' term='HTML-парсер'/><category scheme='http://www.blogger.com/atom/ns#' term='DeprecationWarning'/><category scheme='http://www.blogger.com/atom/ns#' term='логгинг'/><title type='text'>Краулер своими руками. Часть 6</title><content type='html'>&lt;h2&gt;Нормализация ссылок&lt;/h2&gt;Присмотревшись к логам, я заметил странные адреса, например:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;INFO Opening http://pi-code.blogspot.com/2008/12/\"http://pi-code.blogspot.com/\"&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;Дешевый способ преобразовывать относительные адреса в абсолютные:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;&lt;br /&gt;u = urlparse.urldefrag( # удаление фрагмента&lt;br /&gt; urlparse.urljoin(base, tag['href'], allow_fragments=False)&lt;br /&gt;)[0].encode('ascii')&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;явно ненадежен. Пришлось писать новый вариант:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;&lt;br /&gt;def normalize_url(base, url):&lt;br /&gt; """&lt;br /&gt; Нормализация URL. Используется для преобразования относительных адресов&lt;br /&gt; в абсолютные.&lt;br /&gt;&lt;br /&gt; base -- домен (напр. 'taxonomist.tripod.com'), вторая часть результата&lt;br /&gt;         urlparse.urlsplit result&lt;br /&gt; url  -- исходный URL, может быть относительным.&lt;br /&gt; """&lt;br /&gt; parts = urlparse.urlsplit(url)&lt;br /&gt; defaults = ('http', base, '/', parts[3], parts[4])&lt;br /&gt; norm = [ p if p else defaults[i]&lt;br /&gt;         for i, p in enumerate(parts) ]&lt;br /&gt; url = urlparse.urlunsplit(norm)&lt;br /&gt; url = urlparse.urldefrag(url)[0] # удаление фрагмента&lt;br /&gt; url = url.encode('ascii') # перекодировка из уникода в ascii&lt;br /&gt; return url&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;def links_iterator(response, link_filter=None):&lt;br /&gt; """&lt;br /&gt; Итератор по ссылкам, найденным в документе.&lt;br /&gt; Аргументы:&lt;br /&gt; response -- file-like object, возвращаемый&lt;br /&gt;             при открытии страницы библиотекой urllib2&lt;br /&gt; filter   -- функция, которая может быть использована для&lt;br /&gt;             отбора нужных ссылок. На входе: url, на выходе&lt;br /&gt;             True, если проверка прошла, иначе -- False&lt;br /&gt; Если параметр 'filter' не задан, итератор возвращает&lt;br /&gt; все найденные ссылки.&lt;br /&gt; """&lt;br /&gt; if not link_filter:&lt;br /&gt;     link_filter = lambda x: True&lt;br /&gt; base = response.geturl()&lt;br /&gt;&lt;br /&gt; parser = html5lib.HTMLParser(&lt;br /&gt;     tree=treebuilders.getTreeBuilder('dom'),&lt;br /&gt;     tokenizer=sanitizer.HTMLSanitizer)&lt;br /&gt; dom = parser.parse(response)&lt;br /&gt; for elem in dom.getElementsByTagName('a'):&lt;br /&gt;     if elem.hasAttribute('href'):&lt;br /&gt;         href = elem.getAttribute('href')&lt;br /&gt;         u = normalize_url(base, href)&lt;br /&gt;         if link_filter(u):&lt;br /&gt;             yield u&lt;br /&gt;&lt;br /&gt; dom.unlink()&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;Теперь относительные адреса стали преобразовываться в абсолютные по-человечески.&lt;br /&gt;&lt;br /&gt;&lt;h2&gt;"Минздрав предупреждает"&lt;/h2&gt;Еще один неприятный момент: при импорте &lt;span style="font-style: italic;"&gt;html5lib &lt;/span&gt;Python 2.6 выдавал DeprecationWarning:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;C:\Python26\lib\site-packages\html5lib-0.11.1-py2.6.egg\html5lib\&lt;br /&gt;inputstream.py:367:DeprecationWarning: object.__init__() takes no parameters&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;Эти предупреждения Минздрава ничего, кроме раздражения не вызывают. Чтобы избавиться от них, я добавил в parsers.py следующий код:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;from html5lib import treebuilders, sanitizer&lt;br /&gt;import warnings&lt;br /&gt;warnings.simplefilter("ignore",DeprecationWarning)&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/964245207178906174-1906296343603893782?l=pi-code.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://pi-code.blogspot.com/feeds/1906296343603893782/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=964245207178906174&amp;postID=1906296343603893782' title='Комментарии: 0'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/1906296343603893782'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/1906296343603893782'/><link rel='alternate' type='text/html' href='http://pi-code.blogspot.com/2009/01/6.html' title='Краулер своими руками. Часть 6'/><author><name>Наувул-Наувул</name><uri>http://www.blogger.com/profile/06702180511726415678</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/_3ifHLhKgDjk/TCEZ2cVuxJI/AAAAAAAAB2E/D5uehCRtVtI/S220/IMG_3814.JPG'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-964245207178906174.post-1479082466254940167</id><published>2009-01-04T16:38:00.009+03:00</published><updated>2009-01-04T22:53:24.484+03:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='Perl'/><category scheme='http://www.blogger.com/atom/ns#' term='html5lib'/><category scheme='http://www.blogger.com/atom/ns#' term='BeautifulSoup'/><category scheme='http://www.blogger.com/atom/ns#' term='паук'/><category scheme='http://www.blogger.com/atom/ns#' term='краулер'/><category scheme='http://www.blogger.com/atom/ns#' term='Python'/><category scheme='http://www.blogger.com/atom/ns#' term='HTML-парсер'/><title type='text'>Краулер своими руками. Часть 5</title><content type='html'>В 2007 году, когда я писал краулера на Питоне для поискового проекта, именно отсутствие надежного HTML-парсера заставила меня пересесть на Perl.  Ни &lt;span style="font-style: italic;"&gt;SGMLParser &lt;/span&gt;ни &lt;span style="font-style: italic;"&gt;HTMLParser &lt;/span&gt;из стандартных библиотек не в состоянии справиться со страницами, выходящими за рамки академического гипертекста. Альтернативная библиотека &lt;a href="http://www.crummy.com/software/BeautifulSoup/"&gt;BeautifulSoup&lt;/a&gt;, вроде бы хорошо себя зарекомендовавшая, оказалась, как выяснилось в &lt;a href="http://pi-code.blogspot.com/2009/01/4.html"&gt;предыдущей заметке&lt;/a&gt;, ненадежной.&lt;br /&gt;&lt;br /&gt;Прежде чем ставить вердикт, что Python -- неподходящий инструмент для написания простейшего краулера, дадим шанс еще одной библиотеке: &lt;a href="http://code.google.com/p/html5lib/"&gt;html5lib&lt;/a&gt;. Прежде всего, добавим в модуль test_&lt;span style="font-style: italic;"&gt;parsers &lt;/span&gt;новый тест:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;class TestLinks(unittest.TestCase):&lt;br /&gt; ...&lt;br /&gt; def test_blogspot(self):&lt;br /&gt;     page_url = 'http://krushinsky.blogspot.com/'&lt;br /&gt;     fileobj = self.user_agent.open(page_url)&lt;br /&gt;     test_link = 'http://krushinsky.blogspot.com/2007_12_01_archive.html'&lt;br /&gt;  &lt;br /&gt;     links = [ u for u in links_iterator(fileobj, lambda u: u == test_link) ]&lt;br /&gt;     self.assertTrue(len(links), "Link '%s' is absent" % test_link) &lt;br /&gt; ...&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;Первой версии функции &lt;span style="font-style: italic;"&gt;links_iterator&lt;/span&gt; не удавалось пройти этот тест, поскольку парсер &lt;span style="font-style: italic;"&gt;BeautifulSoup&lt;/span&gt; не справлялся со страницей гугловского блога.&lt;br /&gt;&lt;br /&gt;Альтернативная версия &lt;span style="font-style: italic;"&gt;links_iterator&lt;/span&gt;  опирается на парсер из библиотеки &lt;span style="font-style: italic;"&gt;html5lib&lt;/span&gt;.&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;import urlparse&lt;br /&gt;import html5lib&lt;br /&gt;from html5lib import treebuilders, sanitizer&lt;br /&gt;&lt;br /&gt;def links_iterator(response, link_filter=None):&lt;br /&gt;   """&lt;br /&gt;   Итератор по ссылкам, найденным в документе.&lt;br /&gt;   Аргументы:&lt;br /&gt;   response -- file-like object, возвращаемый&lt;br /&gt;               при открытии страницы библиотекой urllib2&lt;br /&gt;   filter   -- функция, которая может быть использована для&lt;br /&gt;               отбора нужных ссылок. На входе: url, на выходе&lt;br /&gt;               True, если проверка прошла, иначе -- False&lt;br /&gt;   Если параметр 'filter' не задан, итератор возвращает&lt;br /&gt;   все найденные ссылки.&lt;br /&gt;   """&lt;br /&gt;   if not link_filter:&lt;br /&gt;       link_filter = lambda x: True&lt;br /&gt;   base = response.geturl()&lt;br /&gt;  &lt;br /&gt;   parser = html5lib.HTMLParser(&lt;br /&gt;       tree=treebuilders.getTreeBuilder('dom'),&lt;br /&gt;       tokenizer=sanitizer.HTMLSanitizer)&lt;br /&gt;   dom = parser.parse(response)&lt;br /&gt;   for elem in dom.getElementsByTagName('a'):&lt;br /&gt;       if elem.hasAttribute('href'):&lt;br /&gt;           href = elem.getAttribute('href')&lt;br /&gt;           u = urlparse.urldefrag( # удаление фрагмента&lt;br /&gt;               urlparse.urljoin(base, href, allow_fragments=False)&lt;br /&gt;           )[0].encode('ascii')&lt;br /&gt;           if link_filter(u):&lt;br /&gt;               yield u  &lt;br /&gt;&lt;br /&gt;   dom.unlink()&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt; &lt;ul&gt;&lt;li&gt;&lt;span style="font-style: italic;"&gt;html5lib.HTMLParser&lt;/span&gt; способен возвращать разного типа деревья: &lt;span style="font-style: italic;"&gt;minidom&lt;/span&gt;, &lt;span style="font-style: italic;"&gt;elementTree&lt;/span&gt; и даже злополучный &lt;span style="font-style: italic;"&gt;BeautifulSoup&lt;/span&gt;. Я начал с &lt;span style="font-style: italic;"&gt;minidom&lt;/span&gt;-а как с простейшего варианта. Поэтому в конструкторе парсера присутствует аргумент: &lt;span style="font-style: italic;"&gt;tree=treebuilders.getTreeBuilder('dom')&lt;/span&gt;.&lt;/li&gt;&lt;li&gt;Второй аргумент: &lt;span style="font-style: italic;"&gt;tokenizer=sanitizer.HTMLSanitizer&lt;/span&gt; предписывает использовать стандартный класс для очистки HTML от двусмысленных элементов и CSS-объявлений.&lt;/li&gt;&lt;li&gt;Чтобы получить все теги &lt;span style="font-style: italic;"&gt;"a"&lt;/span&gt; применяется стандартный методы DOM: &lt;span style="font-style: italic;"&gt;getElementsByTagName&lt;/span&gt;.&lt;br /&gt;&lt;/li&gt;&lt;/ul&gt;Тест &lt;span style="font-style: italic;"&gt;test_blogspot&lt;/span&gt; выполняется. Ура! Удаляем &lt;span style="font-style: italic;"&gt;BeautifulSoup, &lt;/span&gt;работаем с &lt;span style="font-style: italic;"&gt;html5lib&lt;/span&gt; и продолжаем писать краулер на Питоне.&lt;br /&gt;&lt;br /&gt;Отмечу только, что установка &lt;span style="font-style: italic;"&gt;html5lib &lt;/span&gt;версии 0.11.1 из исходных кодов не проходит гладко -- по крайней мере, в среде Windows. Стандартная команда &lt;span style="font-style: italic;"&gt;python setup.py install&lt;/span&gt; не  перенесла библиотечные файлы в директорию &lt;span style="font-style: italic;"&gt;site-packages&lt;/span&gt;, а оставила их там, где лежали исходники. Пришлось копировать их вручную.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/964245207178906174-1479082466254940167?l=pi-code.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://pi-code.blogspot.com/feeds/1479082466254940167/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=964245207178906174&amp;postID=1479082466254940167' title='Комментарии: 0'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/1479082466254940167'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/1479082466254940167'/><link rel='alternate' type='text/html' href='http://pi-code.blogspot.com/2009/01/5.html' title='Краулер своими руками. Часть 5'/><author><name>Наувул-Наувул</name><uri>http://www.blogger.com/profile/06702180511726415678</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/_3ifHLhKgDjk/TCEZ2cVuxJI/AAAAAAAAB2E/D5uehCRtVtI/S220/IMG_3814.JPG'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-964245207178906174.post-6169632954013651139</id><published>2009-01-04T09:57:00.013+03:00</published><updated>2009-01-04T22:55:16.823+03:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='генератор'/><category scheme='http://www.blogger.com/atom/ns#' term='unittest'/><category scheme='http://www.blogger.com/atom/ns#' term='robots.txt'/><category scheme='http://www.blogger.com/atom/ns#' term='парсеры HTML'/><category scheme='http://www.blogger.com/atom/ns#' term='callback'/><category scheme='http://www.blogger.com/atom/ns#' term='BeautifulSoup'/><category scheme='http://www.blogger.com/atom/ns#' term='функция обратного вызова'/><category scheme='http://www.blogger.com/atom/ns#' term='паук'/><category scheme='http://www.blogger.com/atom/ns#' term='urllib2'/><category scheme='http://www.blogger.com/atom/ns#' term='краулер'/><category scheme='http://www.blogger.com/atom/ns#' term='парсер'/><category scheme='http://www.blogger.com/atom/ns#' term='Python'/><title type='text'>Краулер своими руками. Часть 4</title><content type='html'>&lt;h2&gt;Обход сети&lt;br /&gt;&lt;/h2&gt;Ниже представлен метод краулера (&lt;span style="font-style: italic;"&gt;UserAgent&lt;/span&gt;) &lt;span style="font-style: italic;"&gt;traverse&lt;/span&gt;, позволяющий обходить сеть.&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;def traverse(self, start_url, links_filter=None, on_success=None, on_failure=None):&lt;br /&gt;  """&lt;br /&gt;  Обход сети.&lt;br /&gt;&lt;br /&gt;  start_url    -- исходный адрес&lt;br /&gt;  links_filter -- функция для оценки очередной ссылки, полученной со&lt;br /&gt;                  страницы. При результате False не включается в очередь.&lt;br /&gt;  on_success   -- callback-функция, которая вызывается при успешном&lt;br /&gt;                  открытии страницы с аргументами (url, response)&lt;br /&gt;  on_failure   -- callback-функция, которая вызывается в случае неудачи&lt;br /&gt;                  с аргументами: (url, exception)&lt;br /&gt;  """&lt;br /&gt;  queue = [ start_url ]&lt;br /&gt;  passed = set()&lt;br /&gt;  last_url = None&lt;br /&gt;  while queue:&lt;br /&gt;      logging.debug('Queue size: %d, Passed: %d ' % \&lt;br /&gt;                    (len(queue), len(passed)) )&lt;br /&gt;      url = queue.pop(0)&lt;br /&gt;      try:&lt;br /&gt;          if last_url:&lt;br /&gt;              response = self.open(url, {'Referer': last_url})&lt;br /&gt;          else:&lt;br /&gt;              response = self.open(url)&lt;br /&gt;          if on_success:&lt;br /&gt;              on_success(url, response)&lt;br /&gt;          logging.debug('Success')&lt;br /&gt;          # извлекаем со страницы новые ссылки и добавляем их в очередь&lt;br /&gt;          new_links = [&lt;br /&gt;              u for u in links_iterator(response, links_filter)&lt;br /&gt;                if not u in passed and not u in queue ]              &lt;br /&gt;          queue.extend(new_links)&lt;br /&gt;      except Exception, ex:&lt;br /&gt;          logging.warn('Failure: %s' % ex)&lt;br /&gt;          if on_failure:&lt;br /&gt;              on_failure(url, ex)&lt;br /&gt;      last_url = url&lt;br /&gt;      passed.add(url)&lt;br /&gt;  logging.debug('Crawling completed.')&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;ul&gt;&lt;li&gt;Задания снимаются из "головы" очереди (&lt;span style="font-style: italic;"&gt;queue&lt;/span&gt;). Сперва туда помещается исходный адрес. Затем она пополняется ссылками, извлеченными с очередной страницы.&lt;/li&gt;&lt;li&gt;Открыв очередную страницу, краулер вызывает &lt;span style="font-style: italic;"&gt;callback&lt;/span&gt;-функцию &lt;span style="font-style: italic;"&gt;on_success&lt;/span&gt;, передавая туда адрес, а также файло-подобный (file-like) объект, из которого можно прочесть ее содержание. Если открыть страницу не удается, вызывается другой метод обратного вызова: &lt;span style="font-style: italic;"&gt;on_error&lt;/span&gt;.&lt;br /&gt;&lt;/li&gt;&lt;li&gt; Из текущей страницы извлекаются ссылки и помещаются в конец очереди заданий. Для их отбора применяется внешняя функция&lt;span style="font-style: italic;"&gt; links_filter&lt;/span&gt;. Как и было обещано в &lt;a href="http://pi-code.blogspot.com/2008/12/3.html"&gt;предыдущей части,&lt;/a&gt; сам &lt;span style="font-style: italic;"&gt;UserAgent &lt;/span&gt;не принимает решений относительно дальнейшего маршрута. Кроме того, выражение &lt;span style="font-style: italic;"&gt;list comprehension&lt;/span&gt; построено таким образом, что игнорируются как пройденные ссылки, так и те, что уже имеются в очереди (&lt;span style="font-style: italic;"&gt;...if not u in passed and not u in queue&lt;/span&gt;).&lt;br /&gt;&lt;/li&gt;&lt;li&gt;Обход завершается когда заданий не остается.&lt;br /&gt;&lt;/li&gt;&lt;/ul&gt;В набор тестов &lt;span style="font-style: italic;"&gt;TestUserAgent &lt;/span&gt;добавляется новая функция:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;def test_traverse(self):&lt;br /&gt;  """&lt;br /&gt;  Проверяет функцию обхода сети.&lt;br /&gt;  """&lt;br /&gt;  page_url = 'http://pi-code.blogspot.com'&lt;br /&gt;  hostname = urlsplit(page_url).hostname&lt;br /&gt;&lt;br /&gt;  def is_valid_link(u):&lt;br /&gt;      url_parts = urlsplit(u)&lt;br /&gt;      return False if url_parts.hostname != hostname \&lt;br /&gt;             else False if url_parts[0] != 'http' \&lt;br /&gt;                  else True&lt;br /&gt;    &lt;br /&gt;  passed = [] # успешно пройденные адреса&lt;br /&gt;  errors = [] # адреса, которые не удалось пройти&lt;br /&gt;&lt;br /&gt;  def on_success(url, response):&lt;br /&gt;      passed.append(url)&lt;br /&gt;    &lt;br /&gt;  def on_failure(url, error):&lt;br /&gt;      errors.append(url)&lt;br /&gt;&lt;br /&gt;  self.crawler.traverse(&lt;br /&gt;      page_url,&lt;br /&gt;      links_filter=is_valid_link,&lt;br /&gt;      on_success=on_success,&lt;br /&gt;      on_failure=on_failure)&lt;br /&gt;&lt;br /&gt;  self.assert_(passed &gt; 1, 'No nodes were passed')  &lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;h3&gt;Почему не генератор?&lt;br /&gt;&lt;/h3&gt;Я предпочел более традиционный подход с функциями обратного вызова. Можно было бы сделать &lt;span style="font-style: italic;"&gt;traverse &lt;/span&gt;генератором по образцу стандартной функции для обхода директорий &lt;span style="font-style: italic;"&gt;os.walk&lt;/span&gt;. Но тогда было бы сложнее с обработкой ошибок. Если в генераторе возникнет исключение,  цикл остановится.  Как сообщить "наверх" о том, что страницу не удалось открыть, не останавливая паука?&lt;br /&gt;&lt;br /&gt;В &lt;span style="font-style: italic;"&gt;os.walk&lt;/span&gt; для обработки ошибок может быть использована &lt;span style="font-style: italic;"&gt;callback&lt;/span&gt;-функция &lt;span style="font-style: italic;"&gt;onerror&lt;/span&gt;.  Если она не задана в качестве аргумента, ошибки игнорируются. Но это довольно некрасиво с точки зрения архитектуры. Любой генератор -- своего рода &lt;span style="font-style: italic;"&gt;callback&lt;/span&gt; наизнанку,  альтернатива функциям обратного вызова. Одновременное их использование явно избыточно.&lt;br /&gt;&lt;br /&gt;В &lt;span style="font-style: italic;"&gt;os.walk&lt;/span&gt; предполагается, что в большинстве случаев ошибки не будут обрабатываться, поэтому там такой "костыль" может быть и оправдан. При использовании краулера обработка ошибок почти всегда необходима.  Отрицательный результат не менее ценен, чем положительный. Ошибка регистрируется, а паучок ползет дальше.&lt;br /&gt;&lt;h2&gt;&lt;br /&gt;&lt;/h2&gt;&lt;h2&gt;Проверка ссылок&lt;br /&gt;&lt;/h2&gt;Пора сделать что-нибудь полезное. Попробуем приспособить краулер для решения довольно распространенной задачи: проверки "мертвых" внутренних ссылок на сайте.&lt;br /&gt;&lt;h3&gt;Игнорирование robots.txt&lt;br /&gt;&lt;/h3&gt;Для тестирования сайта учитывать ограничения &lt;span style="font-style: italic;"&gt;robots.txt&lt;/span&gt; ни к чему. В конструктор класса  &lt;span style="font-style: italic;"&gt;UserAgent &lt;/span&gt;стоит добавить необязательный параметр &lt;span style="font-style: italic;"&gt;ignore_robots&lt;/span&gt; со значением &lt;span style="font-style: italic;"&gt;False &lt;/span&gt;по умолчанию. При значении &lt;span style="font-style: italic;"&gt;True&lt;/span&gt;, &lt;span style="font-style: italic;"&gt;opener &lt;/span&gt;будет создаваться без &lt;span style="font-style: italic;"&gt;RobotsHTTPHandler &lt;/span&gt;(cм. &lt;a href="http://pi-code.blogspot.com/2008/12/2.html"&gt;"Краулер своими руками, часть 2"&lt;/a&gt;):&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;class UserAgent(object):&lt;br /&gt;def __init__(self,&lt;br /&gt;            agentname=DEFAULT_AGENTNAME,&lt;br /&gt;            email=DEFAULT_EMAIL,&lt;br /&gt;            new_headers=None,&lt;br /&gt;            ignore_robots=False):&lt;br /&gt;&lt;br /&gt;   ...&lt;br /&gt;   if ignore_robots:&lt;br /&gt;       self.opener = urllib2.build_opener()&lt;br /&gt;   else:&lt;br /&gt;       self.opener = urllib2.build_opener(&lt;br /&gt;           RobotsHTTPHandler(self.agentname))&lt;br /&gt;   ...&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;Любопытно, что попытка использовать метод &lt;span style="font-style: italic;"&gt;self.opener.add_handler(RobotsHTTPHandler)&lt;/span&gt; ни к чему ни приводит.&lt;br /&gt;&lt;br /&gt;Скрипт для проверки "мертвых" ссылок совсем короткий:&lt;span style="font-family:monospace;"&gt;&lt;br /&gt;&lt;/span&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;&lt;br /&gt;&lt;br /&gt;#!/usr/bin/python&lt;br /&gt;# -*- coding: cp1251 -*-&lt;br /&gt;#########################################################################&lt;br /&gt;# Tool for testing site links&lt;br /&gt;# author: Sergey Krushinsky&lt;br /&gt;# created: 2008-12-28&lt;br /&gt;#########################################################################&lt;br /&gt;&lt;br /&gt;from urlparse import urlunparse, urlsplit&lt;br /&gt;from crawler import UserAgent&lt;br /&gt;import logging&lt;br /&gt;logging.basicConfig(&lt;br /&gt;  level=logging.DEBUG,&lt;br /&gt;  format='%(asctime)s %(levelname)-8s %(message)s',&lt;br /&gt;  datefmt='%Y-%m-%d %H:%M:%S',&lt;br /&gt;  filename='%s.log' % __name__,&lt;br /&gt;  filemode='w'&lt;br /&gt;)&lt;br /&gt;&lt;br /&gt;# имитируем браузер&lt;br /&gt;AGENT_NAME = "Mozilla/5.0 (Windows; U; Windows NT 5.1; ru; rv:1.9.0.5) Gecko/2008120122 Firefox/3.0.5"&lt;br /&gt;&lt;br /&gt;def is_valid_link(u, hostname):&lt;br /&gt;  """&lt;br /&gt;  Фильтрация ссылок.&lt;br /&gt;  """&lt;br /&gt;  logging.debug("Validating link: '%s'" % u)&lt;br /&gt;  url_parts = urlsplit(u)&lt;br /&gt;  return False if url_parts.hostname != hostname \&lt;br /&gt;         else False if url_parts[0] != 'http' \&lt;br /&gt;              else True&lt;br /&gt;&lt;br /&gt;def main(hostname):&lt;br /&gt;  """&lt;br /&gt;  Обход хоста с целью проверки на наличие мертвых ссылок.&lt;br /&gt;  """&lt;br /&gt;  def on_failure(url, error):&lt;br /&gt;      """Вывод ошибки"""&lt;br /&gt;      print "%s: %s" % (url, error)&lt;br /&gt;&lt;br /&gt;  ua = UserAgent(agentname=AGENT_NAME, ignore_robots=True)&lt;br /&gt;  root = urlunparse(('http', hostname, '/', '', '', ''))&lt;br /&gt;  ua.traverse(&lt;br /&gt;      root,&lt;br /&gt;      links_filter=lambda u: is_valid_link(u, hostname),&lt;br /&gt;      on_failure=on_failure)&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;if __name__ == '__main__':&lt;br /&gt;  import sys&lt;br /&gt;  if len(sys.argv) &lt; 2:       &lt;br /&gt;      print 'Usage: python deadlinks.py HOSTNAME'       &lt;br /&gt;      sys.exit(1)      &lt;br /&gt;  main(sys.argv[1])&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt; Первым делом я напустил этот скрипт на собственный блог &lt;a href="http://krushinsky.blogspot.com/"&gt;krushinsky.blogspot.com&lt;/a&gt;. И очень удивился когда увидел, что краулер прошел всего 7 страниц -- это в блоге, который ведется с лета 2007 года. В число ссылок, извлеченных со страницы, не попала ни одна архивная.&lt;br /&gt;&lt;br /&gt;Как выяснилось в ходе тестов, часть ссылок &lt;a href="http://www.crummy.com/software/BeautifulSoup/"&gt;&lt;span style="font-style: italic;"&gt;BeautifulSoup &lt;/span&gt;&lt;/a&gt;просто молча игнорировал! Когда я попытался вместо того, чтобы использовать &lt;span style="font-style: italic;"&gt;SoupStrainer &lt;/span&gt;(см. &lt;a href="http://pi-code.blogspot.com/2008_12_01_archive.html"&gt;часть 3&lt;/a&gt;), парсить весь HTML, а потом методом &lt;span style="font-style: italic;"&gt;find_all&lt;/span&gt; искать нужные теги, как описано в документации, парсер просто начал умирать. Гугловский шаблон оказался ему не по зубам.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/964245207178906174-6169632954013651139?l=pi-code.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://pi-code.blogspot.com/feeds/6169632954013651139/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=964245207178906174&amp;postID=6169632954013651139' title='Комментарии: 1'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/6169632954013651139'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/6169632954013651139'/><link rel='alternate' type='text/html' href='http://pi-code.blogspot.com/2009/01/4.html' title='Краулер своими руками. Часть 4'/><author><name>Наувул-Наувул</name><uri>http://www.blogger.com/profile/06702180511726415678</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/_3ifHLhKgDjk/TCEZ2cVuxJI/AAAAAAAAB2E/D5uehCRtVtI/S220/IMG_3814.JPG'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-964245207178906174.post-3009812030103958768</id><published>2008-12-31T14:32:00.011+03:00</published><updated>2009-01-02T21:23:44.406+03:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='unittest'/><category scheme='http://www.blogger.com/atom/ns#' term='тесты'/><category scheme='http://www.blogger.com/atom/ns#' term='BeautifulSoup'/><category scheme='http://www.blogger.com/atom/ns#' term='паук'/><category scheme='http://www.blogger.com/atom/ns#' term='urllib2'/><category scheme='http://www.blogger.com/atom/ns#' term='краулер'/><category scheme='http://www.blogger.com/atom/ns#' term='парсер'/><category scheme='http://www.blogger.com/atom/ns#' term='Python'/><title type='text'>Краулер своими руками. Часть 3</title><content type='html'>&lt;h2&gt;Диспетчер и исполнитель&lt;br /&gt;&lt;/h2&gt;&lt;span style="font-style: italic;"&gt;UserAgent &lt;/span&gt;из &lt;a href="http://pi-code.blogspot.com/2008/12/2.html"&gt;предыдущей части&lt;/a&gt; не обладает интеллектом, его роль сводится к тому, чтобы открыть web-страницу, читать которую будет кто-то другой. Задания он тоже получает извне. Нетрудно добавить функцию, которая будет получать не один адрес, а список. Но принципиально это ничего не изменит. Гораздо интереснее будет, если программа сможет самостоятельно прокладывать свой маршрут через web-узлы, извлекая очередную порцию "пищи" с каждой пройденной страницы.&lt;br /&gt;&lt;br /&gt;Cам &lt;span style="font-style: italic;"&gt;UserAgent &lt;/span&gt;не должен этого делать. Во-первых, существует много ситуаций, с которыми он не в состоянии справиться Сайты, где страницы генерируются динамически, могут предоставлять почти бесконечное число потенциальных маршрутов. Взять к примеру календарики, где каждый месяц и год -- ссылки на соседние месяц и год. Там можно застрять навсегда.  Мало того, бывают специально созданные "паучьи ловушки". Так что, нужен механизм, наделенный эвристикой для оценки перспективности того или иного маршрута. Ходить куда попало, по всем подряд адресам нельзя.&lt;br /&gt;&lt;br /&gt;Есть еще одна причина, по которой лучше освободить "исполнителя" от принятия решений. В серьезных системах, как правило, предусмотрена возможность одновременного обхода многих страниц. Устроено это может быть по-разному: через механизм &lt;span style="font-style: italic;"&gt;thread&lt;/span&gt;-ов, как  параллельные процессы, в рамках распределенной вычислительной системы... зависит от задач и их масштаба.  В любом случае, диспетчер один, как и очередь заданий. И пускай в первой версии мы планируем ограничиться одним-единственным "агентом", возможность многозадачности лучше предусмотреть.&lt;br /&gt;&lt;br /&gt;&lt;h2&gt;Извлечение ссылок&lt;/h2&gt;Поскольку задач, связанных с разбором текста, предстоит решить немало, я создал в корне приложения отдельный модуль под названием parsers.py, куда поместил функцию-итератор&lt;span style="font-style: italic;"&gt; links_iterator&lt;/span&gt;.&lt;br /&gt;Чтобы она работала, необходимо установить популярную среди питонистов библиотеку для разбора HTML под названием &lt;a href="http://www.crummy.com/software/BeautifulSoup/"&gt;BeautifulSoup&lt;/a&gt;.&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;import urlparse&lt;br /&gt;from BeautifulSoup import SoupStrainer, BeautifulSoup&lt;br /&gt;import logging&lt;br /&gt;&lt;br /&gt;def links_iterator(response, link_filter=None):&lt;br /&gt;   """lm&lt;br /&gt;   Итератор по ссылкам, найденным в документе.&lt;br /&gt;   Аргументы:&lt;br /&gt;   response -- file-like object, возвращаемый&lt;br /&gt;               при открытии страницы библиотекой urllib2&lt;br /&gt;   filter   -- функция, которая может быть использована для&lt;br /&gt;               отбора нужных ссылок. На входе: url, на выходе&lt;br /&gt;               True, если проверка прошла, иначе -- False&lt;br /&gt;   Если параметр 'filter' не задан, итератор возвращает&lt;br /&gt;   все найденные ссылки.&lt;br /&gt;   """&lt;br /&gt;   if not link_filter:&lt;br /&gt;       link_filter = lambda x: True&lt;br /&gt;   base = response.geturl()&lt;br /&gt;   link_tags = SoupStrainer('a')&lt;br /&gt;   for tag in BeautifulSoup(response, parseOnlyThese=link_tags):&lt;br /&gt;       if ('href' in dict(tag.attrs)):&lt;br /&gt;           u = urlparse.urldefrag( # удаление фрагмента&lt;br /&gt;               urlparse.urljoin(base, tag['href'], allow_fragments=False)&lt;br /&gt;           )[0].encode('ascii')&lt;br /&gt;           if link_filter(u):&lt;br /&gt;               yield u&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;Обычно при работе с &lt;a href="http://www.crummy.com/software/BeautifulSoup/"&gt;BeautifulSoup &lt;/a&gt;(как и с большинством других подобных библиотек) необходимо получить из исходного документа (в нашем случае HTML) дерево, из которого потом извлекаются узлы. Но для нашей задачи есть более простой способ: вместо того, чтобы заставлять парсер строить все дерево, сразу же сказать, какого типа узлы нас интересуют. Делается это через объект &lt;span style="font-style: italic;"&gt;SoupStrainer&lt;/span&gt;.&lt;br /&gt;&lt;br /&gt;Unicode, возвращаемый парсером, не подходит. Если передать уникодную строку методу &lt;span style="font-style: italic;"&gt;RobotParser.can_fetch() &lt;/span&gt;возникнет &lt;span style="font-style: italic;"&gt;KeyError &lt;/span&gt;(это известная недоработка, см. http://bugs.python.org/issue1712522). Поэтому на последнем этапе строка перекодируется в &lt;span style="font-style: italic;"&gt;ascii&lt;/span&gt;.&lt;br /&gt;&lt;br /&gt;Нельзя предусмотреть все варианты использования этой функции. В одних случаях могут понадобиться только внешние ссылки, в других -- внутренние, в третьих -- только то, где используется &lt;span style="font-style: italic;"&gt;http&lt;/span&gt;-протокол... Поэтому вместо того, чтобы нагружать функцию лишним интеллектом, переложим бремя принятия решений на "вышестоящие" компоненты. Для этого вторым, необязательным аргументом итератору передается функция-фильтр. Если очередная ссылка годится, функция-фильтр должна вернуть &lt;span style="font-style: italic;"&gt;True&lt;/span&gt;. При отсутствии фильтра итератор просто возвращает одну за другой все найденные ссылки.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;Ниже представлены тесты, позволяющие "обкатать" API и проверить, все ли правильно работает.&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;import unittest&lt;br /&gt;from urlparse import urlsplit&lt;br /&gt;&lt;br /&gt;parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))&lt;br /&gt;src_dir = os.path.join(parent_dir, 'src')&lt;br /&gt;sys.path.append(src_dir)&lt;br /&gt;&lt;br /&gt;import crawler&lt;br /&gt;from parsers import links_iterator&lt;br /&gt;&lt;br /&gt;class TestLinks(unittest.TestCase):&lt;br /&gt;def setUp(self):&lt;br /&gt;  self.user_agent = crawler.UserAgent()&lt;br /&gt;  self.urls = (&lt;br /&gt;      'http://www.google.com',&lt;br /&gt;      'http://spintongues.msk.ru/',&lt;br /&gt;      'http://www.crummy.com/software/BeautifulSoup/documentation.html',&lt;br /&gt;      'http://pi-code.blogspot.com',&lt;br /&gt;      'http://krushinsky.blogspot.com'&lt;br /&gt;  )&lt;br /&gt;&lt;br /&gt;def test_alllinks(self):&lt;br /&gt;  """&lt;br /&gt;  Общая проверка.&lt;br /&gt;  """&lt;br /&gt;  links = []&lt;br /&gt;  for page_url in self.urls:&lt;br /&gt;      fileobj = self.user_agent.open(page_url)&lt;br /&gt;      links.extend([ u for u in links_iterator(fileobj) ])&lt;br /&gt;&lt;br /&gt;  self.assertTrue(links, 'No links from %d pages' % len(self.urls))&lt;br /&gt;&lt;br /&gt;def test_inbound_links(self):&lt;br /&gt;  """&lt;br /&gt;  Проверка фильтра.&lt;br /&gt;  Удается ли извлечь только внутренние ссылки?&lt;br /&gt;  """&lt;br /&gt;  outbound = [] # список внешних ссылок, должен остаться пустым&lt;br /&gt;  for page_url in self.urls:&lt;br /&gt;      hostname = urlsplit(page_url).hostname&lt;br /&gt;      fileobj = self.user_agent.open(page_url)&lt;br /&gt;&lt;br /&gt;      def is_inbound(u):&lt;br /&gt;          # является ли ссылка внутренней?&lt;br /&gt;          h = urlsplit(u)[1]&lt;br /&gt;          return h == hostname&lt;br /&gt;&lt;br /&gt;      links = [ u for u in links_iterator(fileobj, is_inbound) ]&lt;br /&gt;      # если среди результатов имеются внешние ссылки, они помещаются&lt;br /&gt;      # в массив outbound&lt;br /&gt;      outbound.extend(&lt;br /&gt;          [ u for u in links if urlsplit(u).hostname != hostname ]&lt;br /&gt;      )&lt;br /&gt;      for link in links:&lt;br /&gt;          print link&lt;br /&gt;&lt;br /&gt;  self.assertFalse(outbound, 'Unexpected outbound links: %s' % outbound)&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;def test_outbound_links(self):&lt;br /&gt;  """&lt;br /&gt;  То же самое, что test_inbound_links, но тут отбираются только внешние&lt;br /&gt;  ссылки.&lt;br /&gt;  """&lt;br /&gt;  inbound = [] # список внутренних ссылок, должен остаться пустым&lt;br /&gt;  for page_url in self.urls:&lt;br /&gt;      hostname = urlsplit(page_url).hostname&lt;br /&gt;      fileobj = self.user_agent.open(page_url)&lt;br /&gt;&lt;br /&gt;      def is_outbound(u):&lt;br /&gt;          # является ли ссылка внешней?&lt;br /&gt;          h = urlsplit(u).hostname&lt;br /&gt;          return h == hostname&lt;br /&gt;&lt;br /&gt;      links = [ u for u in links_iterator(fileobj, is_outbound) ]&lt;br /&gt;      inbound.extend(&lt;br /&gt;          [ u for u in links if urlsplit(u).hostname != hostname ]&lt;br /&gt;      )&lt;br /&gt;      for link in links:&lt;br /&gt;          print link&lt;br /&gt;&lt;br /&gt;  self.assertFalse(inbound, 'Unexpected inbound links: %s' % inbound)&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;if __name__ == '__main__':&lt;br /&gt;  module = __import__(__name__)&lt;br /&gt;  suite = unittest.TestLoader().loadTestsFromModule(__import__(__name__))&lt;br /&gt;  unittest.TextTestRunner(verbosity=2).run(suite)&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/964245207178906174-3009812030103958768?l=pi-code.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://pi-code.blogspot.com/feeds/3009812030103958768/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=964245207178906174&amp;postID=3009812030103958768' title='Комментарии: 1'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/3009812030103958768'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/3009812030103958768'/><link rel='alternate' type='text/html' href='http://pi-code.blogspot.com/2008/12/3.html' title='Краулер своими руками. Часть 3'/><author><name>Наувул-Наувул</name><uri>http://www.blogger.com/profile/06702180511726415678</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/_3ifHLhKgDjk/TCEZ2cVuxJI/AAAAAAAAB2E/D5uehCRtVtI/S220/IMG_3814.JPG'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-964245207178906174.post-8487825118026745489</id><published>2008-12-29T11:30:00.021+03:00</published><updated>2009-01-02T12:23:03.392+03:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='unittest'/><category scheme='http://www.blogger.com/atom/ns#' term='robots.txt'/><category scheme='http://www.blogger.com/atom/ns#' term='Java'/><category scheme='http://www.blogger.com/atom/ns#' term='обработка текста'/><category scheme='http://www.blogger.com/atom/ns#' term='тесты'/><category scheme='http://www.blogger.com/atom/ns#' term='Text Mining'/><category scheme='http://www.blogger.com/atom/ns#' term='паук'/><category scheme='http://www.blogger.com/atom/ns#' term='urllib2'/><category scheme='http://www.blogger.com/atom/ns#' term='краулер'/><category scheme='http://www.blogger.com/atom/ns#' term='Python'/><title type='text'>Краулер своими руками. Часть 2</title><content type='html'>&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://2.bp.blogspot.com/_3ifHLhKgDjk/SVij1mNV_9I/AAAAAAAABN4/FLN-2UQTfjY/s1600-h/tests-01.gif"&gt;&lt;img style="margin: 0pt 10px 10px 0pt; float: left; cursor: pointer; width: 320px; height: 231px;" src="http://2.bp.blogspot.com/_3ifHLhKgDjk/SVij1mNV_9I/AAAAAAAABN4/FLN-2UQTfjY/s320/tests-01.gif" alt="" id="BLOGGER_PHOTO_ID_5285154303904186322" border="0" /&gt;&lt;/a&gt;В &lt;a href="http://pi-code.blogspot.com/2008/12/1.html"&gt;предыдущей заметке&lt;/a&gt; речь шла о требованиях к краулеру и тестах, которые он должен проходить. Пора заняться кодом.&lt;br /&gt;&lt;h2&gt;&lt;br /&gt;&lt;/h2&gt;&lt;h2&gt;Обучение агента&lt;/h2&gt;Python предлагает много средств, позволяющих соорудить web-агента.&lt;br /&gt;&lt;ul style="list-style-type: none; list-style-image: none; list-style-position: outside;"&gt;&lt;li&gt;&lt;span style="font-style: italic;"&gt;socket &lt;/span&gt;-- низкоуровневый API к базовым сетевым службам операционной системы;&lt;br /&gt;&lt;/li&gt;&lt;li&gt;&lt;span style="font-style: italic;"&gt;httplib &lt;/span&gt;-- библиотека более высокого уровня, которую используют сейчас главным обазом в случаях, когда нужно или хочется  полностью все контролировать;&lt;br /&gt;&lt;/li&gt;&lt;li&gt;&lt;span style="font-style: italic;"&gt;urllib &lt;/span&gt;-- высокоуровневая библиотека, позволяющая быстро получить  результат, но не очень гибкая;&lt;/li&gt;&lt;li&gt;&lt;span style="font-style: italic;"&gt;urllib2 &lt;/span&gt;-- современный Java-образный фреймворк, главный недостаток которого -- плохая документированность при некоторой запутанности;&lt;br /&gt;&lt;/li&gt;&lt;/ul&gt;Такой переизбыток отнюдь не способствует продуктивности. То ли дело -- Perl, где стандартом де-факто давно стала прекрасно отлаженная и документированная библиотека &lt;span style="font-style: italic;"&gt;LWP&lt;/span&gt;.  Авторы-питонисты обычно выбирают какое-то одно из перечисленных средств и работают с ним. Поскольку одним из требований к краулеру была компактность, я решил разобраться с &lt;span style="font-style: italic;"&gt;urllib2&lt;/span&gt;.&lt;br /&gt;&lt;h3&gt;Вот что получилось&lt;/h3&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;&lt;br /&gt;#!/usr/bin/python&lt;br /&gt;# -*- coding: cp1251 -*-&lt;br /&gt;#########################################################################&lt;br /&gt;# Main UserAgent class&lt;br /&gt;# author: Sergey Krushinsky&lt;br /&gt;# created: 2008-12-28&lt;br /&gt;#########################################################################&lt;br /&gt;import sys&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;import urllib2&lt;br /&gt;from copy import copy&lt;br /&gt;from robotparser import RobotFileParser&lt;br /&gt;from urlparse import urlunsplit, urlsplit&lt;br /&gt;&lt;br /&gt;# Конфигурация по умолчанию&lt;br /&gt;# TODO: вынести в конфигурационный файл&lt;br /&gt;TIMEOUT = 5 # максимальное время ожидания ответа в секундах&lt;br /&gt;&lt;br /&gt;# HTTP-заголовки, которые используются по умолчанию и могут быть&lt;br /&gt;# переопределены в конструкторе UserAgent&lt;br /&gt;DEFAULT_HEADERS = {&lt;br /&gt;'Accept'           : 'text/html, text/plain',&lt;br /&gt;'Accept-Charset'   : 'windows-1251, koi8-r, UTF-8, iso-8859-1, US-ASCII',&lt;br /&gt;'Content-Language' : 'ru,en',&lt;br /&gt;}&lt;br /&gt;# Имя для HTTP-заголовка 'User-Agent' и проверки robots.txt&lt;br /&gt;DEFAULT_AGENTNAME = 'Test/1.0'&lt;br /&gt;# email автора; при пустом значении не используется&lt;br /&gt;DEFAULT_EMAIL = ''&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;class RobotsHTTPHandler(urllib2.HTTPHandler):&lt;br /&gt;"""&lt;br /&gt;Класс, который передается специализированному экземпляру&lt;br /&gt;OpenDirector.&lt;br /&gt;Прежде, чем произвести запрос, проверяет, нет ли запрета&lt;br /&gt;на посещение ресурса файлом robots.txt.&lt;br /&gt;&lt;br /&gt;Аргументы:&lt;br /&gt;agentname -- имя краулера&lt;br /&gt;"""&lt;br /&gt;# TODO: кэшировать один раз полученные данные, чтобы при повторных&lt;br /&gt;#       запросах к одному хосту не делать лишних запросов.&lt;br /&gt;def __init__(self, agentname, *args, **kwargs):&lt;br /&gt; urllib2.HTTPHandler.__init__(self, *args, **kwargs)&lt;br /&gt; self.agentname = agentname&lt;br /&gt;&lt;br /&gt;def http_open(self, request):&lt;br /&gt; """&lt;br /&gt; перегрузка родительского метода. Если в корне сервера&lt;br /&gt; имеется robots.txt c запретом на посещение заданного&lt;br /&gt; ресурса, генерируется исключение RuntimeError.&lt;br /&gt;&lt;br /&gt; request -- экземпляр urllib2.Request&lt;br /&gt; """&lt;br /&gt; url = request.get_full_url()&lt;br /&gt; host = urlsplit(url)[1]&lt;br /&gt; robots_url = urlunsplit(('http', host, '/robots.txt', '', ''))&lt;br /&gt; rp = RobotFileParser(robots_url)&lt;br /&gt; rp.read()&lt;br /&gt; if not rp.can_fetch(self.agentname, url):&lt;br /&gt;     # запрещено&lt;br /&gt;     raise RuntimeError('Forbidden by robots.txt')&lt;br /&gt; # не запрещено, вызываем функцию&lt;br /&gt; return urllib2.HTTPHandler.http_open(self, request)&lt;br /&gt;&lt;br /&gt;class UserAgent(object):&lt;br /&gt;"""&lt;br /&gt;Краулер.&lt;br /&gt;&lt;br /&gt;Именованные аргументы конструктора и значения по умолчанию:&lt;br /&gt;name -- имя ('Test/1.0')&lt;br /&gt;email -- адрес разработчика (пустая строка)&lt;br /&gt;headers -- словарь HTTP-заголовков (DEFAULT_HEADERS)&lt;br /&gt;"""&lt;br /&gt;def __init__(self,&lt;br /&gt;          agentname=DEFAULT_AGENTNAME,&lt;br /&gt;          email=DEFAULT_EMAIL,&lt;br /&gt;          new_headers={}):&lt;br /&gt;&lt;br /&gt; self.agentname = agentname&lt;br /&gt; self.email = email &lt;br /&gt; # для соединений будет использоваться OpenDirector,&lt;br /&gt; # лояльный к robots.txt.&lt;br /&gt; self.opener = urllib2.build_opener(&lt;br /&gt;     RobotsHTTPHandler(self.agentname),&lt;br /&gt; )&lt;br /&gt; # переопределение заголовков по умолчанию&lt;br /&gt; headers = copy(DEFAULT_HEADERS)&lt;br /&gt; headers.update(new_headers) &lt;br /&gt; opener_headers = [ (k, v) for k, v in headers.iteritems() ]&lt;br /&gt; opener_headers.append(('User-Agent', self.agentname))&lt;br /&gt; # если email не задан, HTTP-заголовок 'From' не нужен&lt;br /&gt; if self.email:&lt;br /&gt;     opener_headers.append(('From', self.email))&lt;br /&gt;&lt;br /&gt; self.opener.addheaders = opener_headers&lt;br /&gt;&lt;br /&gt;def open(self, url):&lt;br /&gt; """&lt;br /&gt; Возвращает file-like object, полученный с заданного адреса.&lt;br /&gt; В случае ошибки возвращает HTTPError, URLError или IOError.&lt;br /&gt; """&lt;br /&gt; return self.opener.open(url, None, TIMEOUT)&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;br /&gt;&lt;h3&gt;Новый директор&lt;/h3&gt;Ключом к использованию &lt;span style="font-style: italic;"&gt;urllib2 &lt;/span&gt;является класс &lt;span style="font-style: italic;"&gt;OpenDirector&lt;/span&gt;, отвечающий за всю последовательность операций по открытию страницы. Требуется что-нибудь не совсем стандартное? Назначьте нового директора.&lt;br /&gt;&lt;br /&gt;За каждую операцию, управляемую "директором", отвечает отдельный исполнитель -- handler.  За обработку редиректов -- &lt;span style="font-style: italic;"&gt;HTTPRedirectHandler&lt;/span&gt;, за коммуникацию через proxy -- &lt;span style="font-style: italic;"&gt;ProxyHandler&lt;/span&gt;, за открытие защищенного соединения -- &lt;span style="font-style: italic;"&gt;HTTPSHandler&lt;/span&gt; и т.д. Существует иерархия handler-ов. Некоторые используются по умолчанию, другие можно при желании подключить к директору, третьи придется доработать. Вся эта бюрократическая структура сильно напоминает то, что делается в библиотеках языка Java.&lt;br /&gt;&lt;br /&gt;&lt;span style="font-style: italic;"&gt;OpenDirector&lt;/span&gt; удобно создавать вспомогательным методом &lt;span style="font-style: italic;"&gt;build_opener(набор handler-ов)&lt;/span&gt;. После этого можно вызвать метод &lt;span style="font-style: italic;"&gt;urllib2.install_opener()&lt;/span&gt;, чтобы новый директор применялся в библиотеке по умолчанию (тогда &lt;span style="font-style: italic;"&gt;urllib2.Request.open()&lt;/span&gt; будет использовать только его). Однако, в этом случае есть риск неприятных побочных эффектов. Что, если понадобится одновременно запустить два краулера с разными конфигурациями?&lt;br /&gt;&lt;h3&gt;Правила вежливости&lt;br /&gt;&lt;/h3&gt; В нашем случае требуется &lt;span style="font-style: italic;"&gt;handler&lt;/span&gt;, который прежде, чем открывает страницу, проверяет, не запрещено ли ее посещение файлом &lt;span style="font-style: italic;"&gt;robots.txt&lt;/span&gt; (если таковой имеется). Этой цели служит класс &lt;span style="font-style: italic;"&gt;RobotsHTTPHandler &lt;/span&gt;-- наследник стандартного &lt;span style="font-style: italic;"&gt;urllib2.HTTPHandler&lt;/span&gt;.&lt;br /&gt;&lt;ul&gt;&lt;li&gt;если &lt;span style="font-style: italic;"&gt;robots.txt&lt;/span&gt; найден,  он проверяется при помощи стандатного класса &lt;span style="font-style: italic;"&gt;RobotFileParser&lt;/span&gt;. В случае запрета генерируется &lt;span style="font-style: italic;"&gt;RuntimeError&lt;/span&gt;. В противном случае страница открывается стандартным родительским методом &lt;/li&gt;&lt;li&gt;если &lt;span style="font-style: italic;"&gt;robots.txt&lt;/span&gt; не найден, страница открывается стандартным родительским методом.&lt;/li&gt;&lt;/ul&gt;Тут есть одна тонкость, не освещенная в документации. &lt;span style="font-style: italic;"&gt;RobotsFileParser &lt;/span&gt;может быть создан конструктором без аргументов. Задать адрес файла &lt;span style="font-style: italic;"&gt;robots.txt&lt;/span&gt; позволяет метод &lt;span style="font-style: italic;"&gt;set_url()&lt;/span&gt;. Например, &lt;span style="font-style: italic;"&gt;set_url('http://yandex.ru/robots.txt')&lt;/span&gt;. Такое  API склоняет к мысли, что можно один раз создать экземпляр &lt;span style="font-style: italic;"&gt;RobotsParser&lt;/span&gt;, а потом использовать его для разных хостов. Как выяснилось, это не работает. Попробуйте:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;&lt;br /&gt;&gt;&gt;&gt; rp = RobotFileParser()&lt;br /&gt;&gt;&gt;&gt; rp.set_url('http://spintongues.msk.ru/robots.txt')&lt;br /&gt;&gt;&gt;&gt; rp.read()&lt;br /&gt;&gt;&gt;&gt; rp.can_fetch('Test/1.0', 'http://spintongues.msk.ru/kafka2.html')&lt;br /&gt;True&lt;br /&gt;&gt;&gt;&gt; rp.set_url('http://yandex.ru/robots.txt')&lt;br /&gt;&gt;&gt;&gt; rp.read()&lt;br /&gt;&gt;&gt;&gt; rp.can_fetch('Test/1.0', 'http://yandex.ru/')&lt;br /&gt;True&lt;br /&gt;&gt;&gt;&gt; rp = rp = RobotFileParser('http://yandex.ru/robots.txt')&lt;br /&gt;&gt;&gt;&gt; rp.read()&lt;br /&gt;&gt;&gt;&gt; rp.can_fetch('Test/1.0', 'http://yandex.ru/')&lt;br /&gt;False&lt;br /&gt;&gt;&gt;&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;В первый раз -- когда был задан и прочтен &lt;span style="font-style: italic;"&gt;robots.url&lt;/span&gt; с Яндекса, метод &lt;span style="font-style: italic;"&gt;can_fetch&lt;/span&gt; вернул &lt;span style="font-style: italic;"&gt;True&lt;/span&gt;. Во второй раз -- после создания нового экземпляра с тем же яндексовским адресом -- &lt;span style="font-style: italic;"&gt;False&lt;/span&gt;.&lt;br /&gt;&lt;br /&gt;Поэтому приходится при каждом новом запросе создавать новый экземпляр &lt;span style="font-style: italic;"&gt;RobotsFileParser&lt;/span&gt;, что увы,  не способствует производительности. Зато тест &lt;span style="font-style: italic;"&gt;'test_robotrules'&lt;/span&gt; проходит.&lt;br /&gt;&lt;h3&gt;Редиректы&lt;/h3&gt;Вначале я думал, что нужно будет создавать еще и собственный обработчик перенаправлений -- в духе &lt;a href="http://diveintopython.org/http_web_services/redirects.html"&gt;примера из "Dive Into Python"&lt;/a&gt; -- чтобы ограничить максимальное число редиректов, скажем, до 7. В документации к urllib2 об этом ничего не сказано. Как выяснилось, в классе HTTPredirectHandler это уже предусмотрено: имеется недокументированный атрибут &lt;span style="font-style: italic;"&gt;max_redirections&lt;/span&gt; со значением 10. Ну, пускай будет столько...&lt;br /&gt;&lt;h3&gt;Заголовки&lt;/h3&gt;Наконец, третья важная вещь -- заголовки HTTP-запроса, при помощи которых мы сообщаем серверу, что нам от него надо. В начале модуля объявляются значения по умолчанию. И в конструкторе мы даем возможность переопределить любой из них или дополнить набор какими-то иными заголовками.&lt;br /&gt;&lt;br /&gt;Заголовок &lt;span style="font-style: italic;"&gt;'From'&lt;/span&gt; в примере пустой. Я не хочу помещать туда собственный почтовый адрес. Но любой вежливый краулер обязан предоставлять email создателя, куда возмущенный веб-мастер мог бы отправить гневное послание. А еще лучше -- адрес домашней страницы с подробной информацией о краулере, исходным кодом и подарками. Краулеры, не предоставляющие такой информации, обычно попадают в разряд подозрительных.&lt;br /&gt;&lt;h3&gt;Первое испытание&lt;/h3&gt;В &lt;a href="http://pi-code.blogspot.com/2008/12/1.html"&gt;предыдущей части&lt;/a&gt; приведен код unittest-ов. Результат тестов -- на картинке вверху страницы.&lt;br /&gt;&lt;br /&gt;Осталось сделать точку входа, чтобы программу можно было использовать из консоли.&lt;br /&gt;К модулю crawler.py добавляется:&lt;br /&gt;&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;&lt;br /&gt;if __name__ == '__main__':&lt;br /&gt;   # вызов из консоли&lt;br /&gt;   if len(sys.argv) &lt; 2:&lt;br /&gt;       print "Usage: python crawler.py URL"&lt;br /&gt;       sys.exit(1)&lt;br /&gt;      &lt;br /&gt;   import socket # требуется исключительно длч отлавливания socket.error&lt;br /&gt;  &lt;br /&gt;   ua = UserAgent()&lt;br /&gt;   try:&lt;br /&gt;       resp = ua.open(sys.argv[1])&lt;br /&gt;   except RuntimeError, e:&lt;br /&gt;       # ошибка в ходе выполнения, в т.ч. запрет в robots.txt&lt;br /&gt;       sys.stderr.write('Error: %s\n' % e)&lt;br /&gt;       sys.exit(4)&lt;br /&gt;  &lt;br /&gt;   except urllib2.HTTPError, e:&lt;br /&gt;       # сервер вернул код ошибки&lt;br /&gt;       sys.stderr.write('Error: %s\n' % e)&lt;br /&gt;       sys.stderr.write('Server error document:\n')&lt;br /&gt;       sys.stderr.write(e.read())&lt;br /&gt;       sys.exit(2)&lt;br /&gt;   except urllib2.URLError, e:&lt;br /&gt;       # другие ошибки&lt;br /&gt;       sys.stderr.write('Error: %s\n' % e)&lt;br /&gt;       sys.exit(3)&lt;br /&gt;&lt;br /&gt;   # чтение данных&lt;br /&gt;   bytes_read = long()&lt;br /&gt;   while 1:&lt;br /&gt;       try:&lt;br /&gt;           data = resp.read(1024)&lt;br /&gt;       except socket.error, e:&lt;br /&gt;           sys.stderr.write('Error reading data: %s' % e)&lt;br /&gt;           sys.exit(5)&lt;br /&gt;&lt;br /&gt;       if not len(data):&lt;br /&gt;           break&lt;br /&gt;       bytes_read += len(data)&lt;br /&gt;      &lt;br /&gt;   # проверка длины полученных данных; работает только если&lt;br /&gt;   # в ответном заголовке присутствует поле 'Content-Length'       &lt;br /&gt;   content_length = long(resp.info().get('Content-Length', 0))           &lt;br /&gt;   if content_length and (bytes_read != content_length):&lt;br /&gt;       print "Expected %d bytes, but read %d bytes" % \&lt;br /&gt;             (content_length, bytes_read)&lt;br /&gt;       sys.exit(6)&lt;br /&gt;      &lt;br /&gt;   # все в порядке; выводим данные&lt;br /&gt;   sys.stdout.write(data)   &lt;br /&gt;&lt;/div&gt;&lt;/div&gt;Методы чтения, проверки и обработки ошибок дают представление о том, как можно использовать краулер для более серьезных задач, о чем пойдет речь в дальнейшем.&lt;br /&gt;&lt;br /&gt;&lt;span style="font-style: italic;"&gt;(Продолжение следует...)&lt;/span&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/964245207178906174-8487825118026745489?l=pi-code.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://pi-code.blogspot.com/feeds/8487825118026745489/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=964245207178906174&amp;postID=8487825118026745489' title='Комментарии: 1'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/8487825118026745489'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/8487825118026745489'/><link rel='alternate' type='text/html' href='http://pi-code.blogspot.com/2008/12/2.html' title='Краулер своими руками. Часть 2'/><author><name>Наувул-Наувул</name><uri>http://www.blogger.com/profile/06702180511726415678</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/_3ifHLhKgDjk/TCEZ2cVuxJI/AAAAAAAAB2E/D5uehCRtVtI/S220/IMG_3814.JPG'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://2.bp.blogspot.com/_3ifHLhKgDjk/SVij1mNV_9I/AAAAAAAABN4/FLN-2UQTfjY/s72-c/tests-01.gif' height='72' width='72'/><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-964245207178906174.post-6313078183067342510</id><published>2008-12-29T01:17:00.023+03:00</published><updated>2008-12-29T11:03:39.555+03:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='unittest'/><category scheme='http://www.blogger.com/atom/ns#' term='robots.txt'/><category scheme='http://www.blogger.com/atom/ns#' term='обработка текста'/><category scheme='http://www.blogger.com/atom/ns#' term='тесты'/><category scheme='http://www.blogger.com/atom/ns#' term='Text Mining'/><category scheme='http://www.blogger.com/atom/ns#' term='паук'/><category scheme='http://www.blogger.com/atom/ns#' term='urllib2'/><category scheme='http://www.blogger.com/atom/ns#' term='краулер'/><category scheme='http://www.blogger.com/atom/ns#' term='Python'/><title type='text'>Краулер своими руками. Часть 1</title><content type='html'>&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://3.bp.blogspot.com/_3ifHLhKgDjk/SVgrP7jFHVI/AAAAAAAABNw/YN7qwbPF7zw/s1600-h/spider.jpg"&gt;&lt;img style="margin: 0pt 10px 10px 0pt; float: left; cursor: pointer; width: 300px; height: 225px;" src="http://3.bp.blogspot.com/_3ifHLhKgDjk/SVgrP7jFHVI/AAAAAAAABNw/YN7qwbPF7zw/s320/spider.jpg" alt="" id="BLOGGER_PHOTO_ID_5285021715402136914" border="0" /&gt;&lt;/a&gt;&lt;br /&gt;Чтобы исследовать тексты, их нужно откуда-то брать, и в немалом количестве. Программы, добывающие ресурсы из Всемирной Паутины, называются &lt;span style="font-style: italic;"&gt;краулерами&lt;/span&gt; (от английского &lt;span style="font-style: italic;"&gt;crawl&lt;/span&gt; -- ползать) или &lt;span style="font-style: italic;"&gt;пауками&lt;/span&gt;.&lt;br /&gt;&lt;br /&gt;&lt;h2&gt;Бумажный кораблик&lt;/h2&gt;Существуют готовые продукты, способные выдерживать промышленные нагрузки и снабжать данными поисковые системы. Например, &lt;a href="http://lucene.apache.org/nutch/"&gt;Nutch&lt;/a&gt; -- краулер, использующийся с открытой поисковой системой &lt;a href="http://lucene.apache.org/"&gt;Lucene&lt;/a&gt;. Однако изучать такие системы -- все равно что получать новую специальность и использовать их для относительно скромных задач -- стрельба из пушки по воробьям.&lt;br /&gt;&lt;br /&gt;С другой стороны, современные языки программирования предоставляют готовые библиотеки, позволяющие свести задачу скачивания документа к одной строчке кода. Например, на Питоне:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt; &lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;import urllib&lt;br /&gt;tmpfile, headers = urllib.urlretrieve('http://www.python.org/')&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;Это работает и выглядит весьма заманчиво. Можно даже, следуя примеру из книги Т.Сегарана &lt;span style="font-style: italic;"&gt;"Программируем коллективный разум"&lt;/span&gt; ("Символ-плюс", 2008), написать "обертку" вокруг этой строчки, которая позволит идти вглубь и вширь, по ссылкам, добытых с каждой загружаемой страницы. Если запустить такой  краулер на ночь, то к утру вас могут ожидать один или несколько неприятных  сюрпризов из серии:&lt;br /&gt;&lt;ul&gt;&lt;li&gt;многие документы обрываются в самом начале;&lt;/li&gt;&lt;li&gt;неоправданно большое количество сообщений об ошибках, в том числе с ресурсов, которые прекрасно видны через браузер;&lt;br /&gt;&lt;/li&gt;&lt;li&gt;диск заполнен мусором: вместо текстов пришли какие-то бинарные файлы из каких-то неприкаянных потоков;&lt;br /&gt;&lt;/li&gt;&lt;li&gt;краулер всю ночь провисел в ожидании ответа от какого-то хоста;&lt;/li&gt;&lt;li&gt;компьютер впал в ступор после того как процесс скушал все доступные ресурсы;&lt;/li&gt;&lt;li&gt;ваш IP-адрес заблокирован и помещен в черные списки веб-мастеров, потому что краулер ходит куда не положено и делает запросы с частотой дятла;&lt;br /&gt;&lt;/li&gt;&lt;/ul&gt;Это все равно, что пустить в Москву-реку бумажный кораблик, надеясь, что рано или поздно он попадет в Каспийское море.&lt;br /&gt;&lt;br /&gt;Между тем, Python позволяет вырастить вполне жизнеспособного паучка -- пускай не промышленного уровня, но вполне пригодного для прототипов и решения частных задач,  таких как исследование и обработка текстов. Достаточно порыться в стандартной документации и исходном коде библиотек -- первой, увы, не всегда хватает.&lt;br /&gt;&lt;br /&gt;Вот предварительные требования к программе:&lt;br /&gt;&lt;ol&gt;&lt;li&gt;Переносимость. Модуль должен работать одинаково из под разных операционных систем и по возможности ограничиваться стандартными библиотеками.&lt;br /&gt;&lt;/li&gt;&lt;li&gt;Компактность. Очень не хочется городить очередной фреймворк, который к концу проекта будет провисать под собственным весом. Достаточно того, что &lt;span style="font-style: italic;"&gt;urllib2&lt;/span&gt; -- скорее не библиотека, а Java-образный фреймворк.&lt;br /&gt;&lt;/li&gt;&lt;li&gt;Вежливость. Вежливыми принято называть краулеры, лояльные к &lt;span style="font-style: italic;"&gt;'robots.txt'&lt;/span&gt;. Так называется специальный файл, где веб-мастера объявляют правила поведения пауков на сайте. Скажем: таком-то краулеру не ходить в раздел '/news', никому не ходить в /weather/...  Пример можно увидеть прямо через браузер: &lt;a href="http://tv.yandex.ru/robots.txt"&gt;http://tv.yandex.ru/robots.txt &lt;/a&gt;.&lt;/li&gt;&lt;/ol&gt;Задача краулера -- попытаться открыть страницу, после чего либо сообщить "наверх" об ошибке либо вернуть полученные данные и как можно быстрее двигаться дальше. Он не должен заниматься ничем посторонним. Сохранение данных, извлечение текста из HTML, прокладывание дальнейшего маршрута и даже проверка заголовков ответа -- все это не его дело.&lt;br /&gt;&lt;h2&gt;&lt;br /&gt;&lt;/h2&gt;&lt;h2&gt;Начнем, как водится, с конца&lt;/h2&gt;Простейший способ использования будет из командной строки:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt; &lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;python crawler.py URL&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;В ответ краулер должен вернуть содержание документа либо выдать сообщение об ошибке и прекратить работу с кодом выхода больше нуля.&lt;br /&gt;&lt;br /&gt;Какого рода могут быть ошибки? Их можно свести к нескольким категориям:&lt;br /&gt;&lt;ol&gt;&lt;li&gt;Соединение не состоялось. Например, кошка поиграла с сетевым кабелем и сети не стало.  Или вместо &lt;span style="font-style: italic;"&gt;http://&lt;/span&gt; задано&lt;span style="font-style: italic;"&gt; реез://&lt;/span&gt;. Или сервер долго не отвечает.&lt;br /&gt;&lt;/li&gt;&lt;li&gt;Соединение состоялось, но посещение страницы запрещено &lt;span style="font-style: italic;"&gt;robots.txt&lt;/span&gt;&lt;br /&gt;&lt;/li&gt;&lt;li&gt;Соединение состоялось, но сервер вместо запрошенных данных вернул код ошибки (401 -- "требуется авторизация", 404 -- "документ не найден", 500 -- "внутренняя ошибка" и т.д ).&lt;/li&gt;&lt;li&gt;Вместо целой страницы пришла только ее часть&lt;/li&gt;&lt;/ol&gt;Сразу же установим несколько ограничений -- по крайней мере, для первой версии краулера:&lt;br /&gt;&lt;ol&gt;&lt;li&gt;используется только HTTP-протокол&lt;br /&gt;&lt;/li&gt;&lt;li&gt;интересны только html -документы и простой текст&lt;/li&gt;&lt;/ol&gt;Теперь можно попытаться написать минимальный набор тестов, которые должен будет проходить краулер первой версии.&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt; &lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;&lt;br /&gt;#!/usr/bin/python&lt;br /&gt;# -*- coding utf8 -*-&lt;br /&gt;#########################################################################&lt;br /&gt;# UserAgent tests&lt;br /&gt;# author: Sergey Krushinsky&lt;br /&gt;# created: 2008-12-28&lt;br /&gt;#########################################################################&lt;br /&gt;import sys, os&lt;br /&gt;import urllib2&lt;br /&gt;import unittest&lt;br /&gt;import crawler&lt;br /&gt;&lt;br /&gt;class TestUserAgent(unittest.TestCase):&lt;br /&gt;   def setUp(self):&lt;br /&gt;       self.crawler = crawler.UserAgent()&lt;br /&gt; &lt;br /&gt;   def tearDown(self):&lt;br /&gt;       pass&lt;br /&gt;&lt;br /&gt;   def test_default_agentname(self):&lt;br /&gt;       """&lt;br /&gt;       Если имя не задано в конструкторе, он должно соответствовать&lt;br /&gt;       имени по умолчанию.&lt;br /&gt;       """      &lt;br /&gt;       msg = "Default agent name should be '%s', not '%s'" % \&lt;br /&gt;           (crawler.DEFAULT_AGENTNAME, self.crawler.agentname)&lt;br /&gt;       self.assertEqual(self.crawler.agentname, crawler.DEFAULT_AGENTNAME, msg)&lt;br /&gt;&lt;br /&gt;   def test_custom_agentname(self):&lt;br /&gt;       """&lt;br /&gt;       Если имя задано в конструкторе, оно должно таким и быть.&lt;br /&gt;       """&lt;br /&gt;       name = 'Other Test/2.0'&lt;br /&gt;       c = crawler.UserAgent(agentname=name)&lt;br /&gt;       self.assertEqual(&lt;br /&gt;           c.agentname,&lt;br /&gt;           name,&lt;br /&gt;           "Custom agent name should be '%s', not '%s'" % \&lt;br /&gt;           (name, c.agentname))&lt;br /&gt;&lt;br /&gt;   def test_htmlget(self):&lt;br /&gt;       """&lt;br /&gt;       Краулер открывает заданный ресурс и в заголовке ответа возвращается&lt;br /&gt;       text/html.&lt;br /&gt;       """&lt;br /&gt;       resp = self.crawler.open('http://spintongues.msk.ru/kafka2.html')&lt;br /&gt;       ctype = resp.info().get('Content-Type')&lt;br /&gt;       # В заголовке может быть что-нибудь вроде 'text/html; charset=windows-1251',&lt;br /&gt;       # поэтому обычное сравнение не подходит.&lt;br /&gt;       self.assert_(ctype.find('text/html') != -1, 'Not text/html')&lt;br /&gt;&lt;br /&gt;   def test_urlerror(self):&lt;br /&gt;       """&lt;br /&gt;       Если задан неверный адрес, должны генерироваться ошибка IOError.&lt;br /&gt;       """&lt;br /&gt;       self.assertRaises(IOError, self.crawler.open, 'http://foo/bar/buz/a765')&lt;br /&gt;             &lt;br /&gt;   def test_robotrules(self):&lt;br /&gt;       """&lt;br /&gt;       Если выяснилось, что robots.txt запрещает посещение адреса,&lt;br /&gt;       должно генерироваться исключение.&lt;br /&gt;       """&lt;br /&gt;       # Яндекс, как известно, не любит пауков&lt;br /&gt;       self.assertRaises(&lt;br /&gt;           RuntimeError,&lt;br /&gt;           self.crawler.open,&lt;br /&gt;           'http://yandex.ru/')&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;if __name__ == '__main__':&lt;br /&gt;   suite = unittest.TestLoader().loadTestsFromTestCase(TestUserAgent)&lt;br /&gt;   unittest.TextTestRunner(verbosity=2).run(suite)&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;h3&gt;Пояснения к тестам:&lt;/h3&gt;&lt;br /&gt;&lt;ul&gt;&lt;li&gt;существует пакет по имени &lt;span style="font-style: italic;"&gt;crawler &lt;/span&gt;и в нем -- класс &lt;span style="font-style: italic;"&gt;UserAgent&lt;/span&gt;. Почему 'UserAgent', а не 'Crawler' или 'Spider'? Потому что последние два имени скорее ассоциируются с обходом сети. Позже мы решим и эту задачу.&lt;/li&gt;&lt;li&gt;Экземпляр может быть создан без аргументов либо с аргументом '&lt;span style="font-style: italic;"&gt;agentname&lt;/span&gt;'. Это имя включается в заголовки HTTP-запроса, а кроме того, используется при анализе &lt;span style="font-style: italic;"&gt;robots.txt&lt;/span&gt;.&lt;/li&gt;&lt;li&gt;Основной рабочий метод -- &lt;span style="font-style: italic;"&gt;open(адрес)&lt;/span&gt;. Почему не '&lt;span style="font-style: italic;"&gt;get&lt;/span&gt;'?-- потому  что сам &lt;span style="font-style: italic;"&gt;UserAgent&lt;/span&gt; не читает содержание страницы, он открывает ее, возвращая 'file-like object', проверять и обрабатывать который будет уже кто-то другой.&lt;/li&gt;&lt;li&gt;При попытке открыть страницу с несуществующего адреса, выбрасывается исключение &lt;span style="font-style: italic;"&gt;IOError&lt;/span&gt;.&lt;/li&gt;&lt;li&gt;Если пауку вход запрещен, генерируется &lt;span style="font-style: italic;"&gt;RuntimeError&lt;/span&gt;.&lt;br /&gt;&lt;/li&gt;&lt;/ul&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style="font-style: italic;"&gt;(продолжение следует)&lt;/span&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/964245207178906174-6313078183067342510?l=pi-code.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://pi-code.blogspot.com/feeds/6313078183067342510/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=964245207178906174&amp;postID=6313078183067342510' title='Комментарии: 5'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/6313078183067342510'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/6313078183067342510'/><link rel='alternate' type='text/html' href='http://pi-code.blogspot.com/2008/12/1.html' title='Краулер своими руками. Часть 1'/><author><name>Наувул-Наувул</name><uri>http://www.blogger.com/profile/06702180511726415678</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/_3ifHLhKgDjk/TCEZ2cVuxJI/AAAAAAAAB2E/D5uehCRtVtI/S220/IMG_3814.JPG'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://3.bp.blogspot.com/_3ifHLhKgDjk/SVgrP7jFHVI/AAAAAAAABNw/YN7qwbPF7zw/s72-c/spider.jpg' height='72' width='72'/><thr:total>5</thr:total></entry><entry><id>tag:blogger.com,1999:blog-964245207178906174.post-5490729024812455941</id><published>2008-12-24T22:04:00.003+03:00</published><updated>2008-12-29T03:57:46.161+03:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='unittest'/><category scheme='http://www.blogger.com/atom/ns#' term='карта сайта'/><category scheme='http://www.blogger.com/atom/ns#' term='Perl'/><category scheme='http://www.blogger.com/atom/ns#' term='Django'/><category scheme='http://www.blogger.com/atom/ns#' term='тест'/><category scheme='http://www.blogger.com/atom/ns#' term='breadcrumbs'/><category scheme='http://www.blogger.com/atom/ns#' term='YAML'/><category scheme='http://www.blogger.com/atom/ns#' term='Python'/><title type='text'>Меняющаяся карта</title><content type='html'>&lt;span style="font-weight: bold; font-style: italic;"&gt;Никогда, никогда больше не буду писать код без тестов!&lt;/span&gt;-- пообещал я себе сегодня утром после того, как исправил мерзкую ошибку.&lt;br /&gt;&lt;br /&gt;Мерзость ее заключалась в том, что появлялась она время от времени. Да, я замечал, что изредка &lt;span style="font-style: italic;"&gt;breadcrumbs&lt;/span&gt; -- так называется маршрут к текущей странице ('вы здесь...') на веб-портале не появлялся, но винил в этом браузер, memcached, скомпилированные Django-шаблоны -- все, что угодно, только не свой модуль, который отвечает за построение breadcrumbs.&lt;br /&gt;&lt;br /&gt;Модуль работает следующим образом:&lt;br /&gt;&lt;ul&gt;&lt;li&gt;Из YAML-файла читается карта сайта. В результате получается дерево. Это происходит один раз, при старте Django-приложения&lt;br /&gt;&lt;/li&gt;&lt;li&gt;При выводе очередной страницы вызывается функция 'search' с текущим адресом в качестве аргумента. Она ищет в дереве узел, соответствующий заданному адресу.&lt;br /&gt;&lt;/li&gt;&lt;li&gt;Если узел найден, то в шаблон возвращается маршрут к нему. Каждый  пункт маршрута содержит адрес и заголовок. В результате получается что-то вроде: &lt;span style="font-style: italic;"&gt;"Вы здесь: Главная/Сообщения/Непрочитанные"&lt;/span&gt;. &lt;/li&gt;&lt;/ul&gt;В любом веб-приложении имеется множество динамических адресов. В Django особенно любят дурачить пользователей и краулеры красивыми псевдо-статическими адресами. Если для доступа к событию в каком-нибудь календарике используется URL &lt;span style="font-style: italic;"&gt;/calendar/2008/12/24/&lt;/span&gt;, это отнюдь не означает, что выдаваемый документ хранится на сервере в   соответствующей структуре каталогов. Фреймворк знает из своей конфигурации, что последние три сегмента являются параметрами функции, отвечающей за обработку запросов по адресу&lt;span style="font-style: italic;"&gt; /calendar&lt;/span&gt;.&lt;br /&gt;&lt;br /&gt;Понятно, что карту сайта нельзя ограничивать статическими адресами. В YAML-файл следовало включить шаблоны, а модуль, который сопоставляет узлы дерева с текущим адресом, должен уметь их понимать.&lt;br /&gt;&lt;br /&gt;YAML получился примерно такой:&lt;br /&gt;&lt;pre&gt;--- # Карта сайта&lt;br /&gt;url   : /&lt;br /&gt;title : Главная&lt;br /&gt;nodes :&lt;br /&gt; - url   : accounts/&lt;br /&gt;   title : Пользователи&lt;br /&gt;   nodes :&lt;br /&gt; - url   : accounts/{USER_ID}/&lt;br /&gt; - url   : accounts/{USER_NAME}/&lt;br /&gt; - url   : blog/&lt;br /&gt;   title : Блог&lt;br /&gt;   nodes :&lt;br /&gt;  - url   : blog/{ENTRY_ID}/&lt;br /&gt;  - url   : blog/add/&lt;br /&gt;           title : Новая запись&lt;br /&gt;...&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;Наряду  со статическими адресами (например:&lt;span style="font-style: italic;"&gt; "/"&lt;/span&gt;, &lt;span style="font-style: italic;"&gt;"accounts/"&lt;/span&gt;), здесь присутствуют и динамические. Когда функция, отвечающая за обход дерева, встречает узел &lt;span style="font-style: italic;"&gt;accounts/{USER_ID}/&lt;/span&gt; и проверяет, соответствует ли этому узлу текущий адрес &lt;span style="font-style: italic;"&gt;account/1111/&lt;/span&gt;, она говорит: &lt;span style="font-style: italic;"&gt;"Ага, это то что мне надо!"  &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;Чтобы найти маршрут для &lt;span style="font-style: italic;"&gt;breadcrumbs&lt;/span&gt;, достаточно пройти от найденного узла к вершине дерева. Получится:&lt;br /&gt;&lt;ul&gt;&lt;li&gt;/ - "Главная"&lt;br /&gt;&lt;/li&gt;&lt;li&gt;/accounts - "Пользователи "&lt;br /&gt;&lt;/li&gt;&lt;li&gt;/accounts/1111 - ...&lt;/li&gt;&lt;/ul&gt;...Стоп. Нельзя писать в &lt;span style="font-style: italic;"&gt;breadcrumbs &lt;/span&gt;id пользователя, это некрасиво и не информативно. Надо заменить его именем. Значит, на последнем этапе, прежде чем возвращать результат, следует пройтись по цепочке и где надо, преобразовать id в названия.&lt;br /&gt;&lt;br /&gt;Вот как выглядела основная функция search до того, как я исправил ту самую ошибку, с которой начал эту заметку:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt; &lt;div style="margin-left: 1em; white-space: pre; font-family:monospace;"&gt;&lt;br /&gt; def translate(node, goal):&lt;br /&gt;     """&lt;br /&gt;     Преобразует узел node карты в структуру, пригодную для использования.&lt;br /&gt;     Если в адресе имеются шаблоны, они заменяется динамическими данными     &lt;br /&gt;     (например: {USER_ID} --&gt; 1111&lt;br /&gt;     Если существует правило замены заголовка, то node получает атрибут&lt;br /&gt;     'title' с новым значением.&lt;br /&gt;&lt;br /&gt;     node -- узел карты сайта&lt;br /&gt;     goal -- текущий URL&lt;br /&gt;     """&lt;br /&gt;     ...&lt;br /&gt;&lt;br /&gt; def search(url):&lt;br /&gt;     """&lt;br /&gt;     Поиск адреса в карте сайта.&lt;br /&gt;     Аргументы:&lt;br /&gt;       url -- искомый адрес (строка)&lt;br /&gt;     Возвращает:&lt;br /&gt;       True, маршрут -- если адрес найден&lt;br /&gt;       False, пустой список -- если адрес не найден.&lt;br /&gt;&lt;br /&gt;     Маршрут -- это список узлов, каждый из которых представлен словарем&lt;br /&gt;     c теми же ключами, что заданы в конфигурационном файле.&lt;br /&gt;     """&lt;br /&gt;     path = [] # маршрут&lt;br /&gt;     url = normalize_url(url) # приводит искомый URL к правильному виду&lt;br /&gt;  &lt;br /&gt;     # рекурсивный обход дерева&lt;br /&gt;     if visit(SITE_MAP, url, path):&lt;br /&gt;         # узел найден&lt;br /&gt;         return True, map(&lt;br /&gt;             lambda node: translate(node, url), path)&lt;br /&gt;     else:&lt;br /&gt;         # неудача&lt;br /&gt;         return False, []&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;Вроде, работало. Стоило прописать новый узел в карте сайта, перезапустить Django-приложение, как на странице появлялся новый маршрут.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;***&lt;br /&gt;И вот  я написал серию тестов для модуля. Самое простое -- проверить сразу дюжину адресов, про которые заранее известно, что они прописаны в карте сайта, и убедиться, что маршруты найдены.&lt;br /&gt;&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt; &lt;div style="margin-left: 1em; white-space: pre; font-family:monospace"&gt;import unittest&lt;br /&gt;&lt;br /&gt;class TestCase(unittest.TestCase):&lt;br /&gt;def setUp(self):&lt;br /&gt;self.urls = (&lt;br /&gt;    # адреса, про которые заранее известно,&lt;br /&gt;    # что они прописаны в карте сайта&lt;br /&gt;    ...&lt;br /&gt;)&lt;br /&gt;&lt;br /&gt;def test_search_results(self):&lt;br /&gt;"""&lt;br /&gt;Прогоняем через search self.urls;&lt;br /&gt;Если хотя бы один не найден, тест провален.&lt;br /&gt;"""&lt;br /&gt;print 'Searching %d entries' % len(self.urls)&lt;br /&gt;search_all = ((url, search(url)[0])&lt;br /&gt;   for url in self.urls)&lt;br /&gt;negative = [ url for url, result in search_all if not result ]&lt;br /&gt;msg = 'Some urls were not found: %s' % ', '.join(negative)&lt;br /&gt;self.assertEqual(len(negative), 0, msg)&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;span style="font-style: italic;"&gt;Один узел&lt;/span&gt; почему-то не находился. Это был динамический маршрут (из серии .../{ШАБЛОН}). Как ни странно, с другим узлом, попадающим в тот же шаблон, все было в порядке. Сколько я ни вчитывался, никаких опечаток, пробелов -- ничего этого не было.&lt;br /&gt;&lt;br /&gt;Я добавил в тест функцию, которая искала этот единственный проблематичный узел:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt; &lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;&lt;br /&gt;&lt;br /&gt;def test_idpattern(self):&lt;br /&gt; print 'Id pattern'&lt;br /&gt; url = u'/accounts/2227/'&lt;br /&gt; result, path = search(url)&lt;br /&gt; self.assertEqual(result, True, "URL '%s' not found" % url)&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;Тест прекрасно отрабатывал.&lt;br /&gt;&lt;br /&gt;Наконец, я понял, в чем дело. Я попался на типичное питоновское &lt;span style="font-style: italic;"&gt;'gotcha'&lt;/span&gt;.&lt;br /&gt;&lt;br /&gt;На вход функции "visit" подается исходное дерево, полученное YAML-парсером:&lt;br /&gt;&lt;div  style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;font-family:courier new;"&gt; &lt;div style="margin-left: 1em; white-space: pre; font-family: monospace;"&gt;visit(SITE_MAP, url, path)&lt;/div&gt;&lt;/div&gt;Время жизни этого дерева совпадает со временем жизни приложения -- а  Django-приложения, в отличие от CGI, живут долго. В ходе очередного поиска используется одно и то же дерево.&lt;br /&gt;&lt;br /&gt;Когда 'visit ' возвращает маршрут, оно берет его из исходного SITE_MAP. Причем возвращается не &lt;span style="font-style: italic;"&gt;копия&lt;/span&gt;, а &lt;span style="font-style: italic;"&gt;ссылка&lt;/span&gt;.  Perl, между прочим, вернул бы копию, там операция присвоения работает иначе:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt; &lt;div  style="margin-left: 1em; white-space: pre; font-weight: bold;font-family:monospace;"&gt;&lt;span style="font-size:100%;"&gt;&lt;br /&gt;my %a = (a =&gt; 'foo', b =&gt; 'bar');&lt;br /&gt;my %b = %a;&lt;br /&gt;$b{'a'} = 'buz';&lt;br /&gt;&lt;br /&gt;printf "Original: %s\n", $a{'a'};&lt;br /&gt;printf "Copy: %s\n", $b{'a'};&lt;br /&gt;Original: foo&lt;br /&gt;Copy: buz&lt;br /&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;br /&gt;На вход функции 'translate' подавался все тот же узел (точнее, ссылка на него) и преобразовывала она его же.  Из-за этого преобразования и возникала ошибка. После благополучного нахождения узла &lt;span style="font-style: italic;"&gt;/accounts/1/&lt;/span&gt;, шаблон  &lt;span style="font-style: italic;"&gt;/accounts/{USER_ID}&lt;/span&gt; переставал существовать,  &lt;span style="font-style: italic;"&gt;"{USER_ID}"&lt;/span&gt; заменялось на &lt;span style="font-style: italic;"&gt;"1"&lt;/span&gt;. Не удивительно, что другой адрес такого же типа: &lt;span style="font-style: italic;"&gt;accounts/2227&lt;/span&gt; уже не имел шансов. Это все равно, что пытаться ориентироваться по карте, которая по мере твоего продвижения сама меняется.&lt;br /&gt;&lt;br /&gt;Чтобы поправить ошибку, достаточно было отредактировать одну-единственную строчку в функции 'search':&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt; &lt;div style="margin-left: 1em; white-space: pre; font-weight: bold; font-family: monospace;"&gt;from copy import copy&lt;br /&gt;&lt;br /&gt;# рекурсивный обход дерева&lt;br /&gt;if visit(SITE_MAP, url, path):&lt;br /&gt; # узел найден&lt;br /&gt; return True, map(   &lt;br /&gt;     lambda node: translate(copy(node), url), path)&lt;br /&gt;...&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;Не знаю, через сколько времени была бы обнаружена причина изредка появляющихся ошибок в веб-приложении, если бы не тест.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/964245207178906174-5490729024812455941?l=pi-code.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://pi-code.blogspot.com/feeds/5490729024812455941/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=964245207178906174&amp;postID=5490729024812455941' title='Комментарии: 0'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/5490729024812455941'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/5490729024812455941'/><link rel='alternate' type='text/html' href='http://pi-code.blogspot.com/2008/12/blog-post_24.html' title='Меняющаяся карта'/><author><name>Наувул-Наувул</name><uri>http://www.blogger.com/profile/06702180511726415678</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/_3ifHLhKgDjk/TCEZ2cVuxJI/AAAAAAAAB2E/D5uehCRtVtI/S220/IMG_3814.JPG'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-964245207178906174.post-8212136144472304605</id><published>2008-12-19T20:57:00.000+03:00</published><updated>2008-12-24T23:18:07.727+03:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='текст'/><category scheme='http://www.blogger.com/atom/ns#' term='слова'/><category scheme='http://www.blogger.com/atom/ns#' term='Perl'/><category scheme='http://www.blogger.com/atom/ns#' term='регулярные выражения'/><category scheme='http://www.blogger.com/atom/ns#' term='unicode'/><category scheme='http://www.blogger.com/atom/ns#' term='язык'/><category scheme='http://www.blogger.com/atom/ns#' term='Эдгар По'/><category scheme='http://www.blogger.com/atom/ns#' term='Text Mining With Perl'/><category scheme='http://www.blogger.com/atom/ns#' term='Python'/><title type='text'>Как разбить текст на слова</title><content type='html'>&lt;span style="font-style: italic; font-weight: bold;"&gt;"Practical Text Mining With Perl" -- заметки на полях&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;Как часто нужные книги попадают в руки когда они уже не так актуальны! Вот первое, что я подумал, открыв "Practical Text Mining With Perl" (Roger Bilisoly, Wiley &amp;amp; Sons, 2008). А не повод ли это вернуться к старым увлечениям?&lt;br /&gt;&lt;br /&gt;Одна из первых глав посвящена проблеме разбиения текста на слова. Задачка не такая уж и простая.  Разбить строку по пробелам и убрать знаки препинания несложно:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace; font-weight: bold;"&gt;@tokens = split(/\s+/, $line);&lt;br /&gt;foreach $tk (@tokens) {&lt;br /&gt;    if ( $x =- /(\w+)/ ) {&lt;br /&gt;        # $1 содержит слово&lt;br /&gt;    }&lt;br /&gt;}&lt;/div&gt;&lt;/div&gt;&lt;br /&gt;Но результат будет немного неожиданным.&lt;br /&gt;&lt;h4&gt;Внутренняя пунктуация&lt;/h4&gt;От слова "&lt;span style="font-style: italic;"&gt;инженер-программист&lt;/span&gt;" останется лишь &lt;span style="font-style: italic;"&gt;"инженер"&lt;/span&gt;.  От английского &lt;span style="font-style: italic;"&gt;"o'clock"&lt;/span&gt; -- &lt;span style="font-style: italic;"&gt;"o"&lt;/span&gt;. Издатель O'Reilly не узнает своей фамилии... Шаблон &lt;span style="font-weight: bold;"&gt;\w+&lt;/span&gt; набирает буквы, цифры и нижнее подчеркивание (_). Как только в тексте встречается другой символ, дверца (круглая скобка) закрывается. Слово закончилось.&lt;br /&gt;&lt;br /&gt;Чтобы не отсекать правые части составных слов, автор модифицирует регулярное выражение следующим образом:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace; font-weight: bold;"&gt;$x =~ /(([a-zA-Z']+-)*[a-zA-Z']+)/&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;Теперь слово может состоять из нескольких сегментов, отделенных друг от друга дефисами. Каждый такой сегмент состоит из одной или более букв или апострофов. В предыдущем примере слово могло состоять только из букв, цифр и знаков подчеркивания (\w+) .&lt;br /&gt;&lt;br /&gt;Включение апострофа в квадратные скобки -- не лучшее решение, потому что теперь словом может считаться, например: '-'-'-'-'-'-'-'-'-'-'-'-'-'-' . И разве бывают слова, где за апострофом следует дефис? Не лучше ли принять, что разделителем составных слов может быть &lt;span style="font-style: italic;"&gt;либо дефис либо апостроф&lt;/span&gt;?&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt; &lt;div style="margin-left: 1em; white-space: pre; font-family: monospace; font-weight: bold;"&gt;$x =~ /(([a-zA-Z]+[-']))*[a-zA-Z]+'?)/&lt;br /&gt;&lt;/div&gt; &lt;/div&gt;Второй очевидный недостаток: диапазон a-zA-Z не оставляет никакого шанса не только цифрам, но и кириллическим символам. Одно из решений -- использовать уникодные расширения регулярных выражений:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt; &lt;div style="margin-left: 1em; white-space: pre; font-family: monospace; font-weight: bold;"&gt;$x =~ /(([[:alpha:]]+['-])*[[:alpha:]]+'?)/&lt;/div&gt; &lt;/div&gt;Разумеется, придется позаботиться о том, чтобы текст был в кодировке utf-8.&lt;br /&gt;&lt;br /&gt;***&lt;br /&gt;Всегда ли имеет смысл считать составное слово чем-то отдельным? Вот ряд примеров:&lt;br /&gt;&lt;ul&gt;&lt;li style="font-style: italic;"&gt;&lt;span style="font-size:85%;"&gt;бледно-оранжевый&lt;/span&gt;&lt;/li&gt;&lt;li style="font-style: italic;"&gt;&lt;span style="font-size:85%;"&gt;красно-коричневый&lt;/span&gt;&lt;/li&gt;&lt;li style="font-style: italic;"&gt;&lt;span style="font-size:85%;"&gt;давным-давно&lt;/span&gt;&lt;/li&gt;&lt;li style="font-style: italic;"&gt;&lt;span style="font-size:85%;"&gt;когда-нибудь&lt;/span&gt;&lt;/li&gt;&lt;li style="font-style: italic;"&gt;&lt;span style="font-size:85%;"&gt;высоко-высоко&lt;/span&gt;&lt;/li&gt;&lt;li style="font-style: italic;"&gt;&lt;span style="font-size:85%;"&gt;северо-западный&lt;/span&gt;&lt;/li&gt;&lt;li style="font-style: italic;"&gt;&lt;span style="font-size:85%;"&gt;мяу-мяу&lt;/span&gt;&lt;/li&gt;&lt;li style="font-style: italic;"&gt;&lt;span style="font-size:85%;"&gt;иван-да-марья&lt;/span&gt;&lt;/li&gt;&lt;li style="font-style: italic;"&gt;&lt;span style="font-size:85%;"&gt;человек-амфибия&lt;/span&gt;&lt;/li&gt;&lt;li style="font-style: italic;"&gt;&lt;span style="font-size:85%;"&gt;самолет-разведчик&lt;/span&gt;&lt;/li&gt;&lt;li style="font-style: italic;"&gt;&lt;span style="font-size:85%;"&gt;советско-американский&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style="font-style: italic;font-size:85%;" &gt;авто-мастерская&lt;/span&gt;&lt;br /&gt;&lt;/li&gt;&lt;/ul&gt;Определить, является ли составное слово отдельной смысловой единицей или нет, способен исключительно носитель языка. Иногда ответ зависит от контекста (&lt;span style="font-style: italic;"&gt;"красно-коричневый"&lt;/span&gt;).&lt;br /&gt;&lt;br /&gt;Автор "Practical Text Mining With Perl" считает, что проще все же не разбивать составные слова, и я с ним согласен. Некоторое количество лишних словообразований в лексиконе не так вредно с точки зрения анализа смысла, как дробление. Что останется от понятия &lt;span style="font-style: italic;"&gt;"mother-in-law"&lt;/span&gt; (свекровь) -- пишет Bilisoly -- если разбить его на &lt;span style="font-style: italic;"&gt;"mother"&lt;/span&gt;, &lt;span style="font-style: italic;"&gt;"in"&lt;/span&gt; и &lt;span style="font-style: italic;"&gt;"law"&lt;/span&gt; ("мать", "в", "закон")?&lt;br /&gt;&lt;br /&gt;&lt;h4&gt; Тире&lt;/h4&gt;Тире -- еще один камень преткновения. Оно может быть обозначено разными символами, отделяться или не отделяться от слов пробелами.  Тут частично помогает предварительная нормализация. Например, если в тексте встретились два идущих подряд дефиса, возле которых нет пробела,  обеспечим пробелы с двух сторон:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt; &lt;div style="margin-left: 1em; white-space: pre; font-family: monospace; font-weight: bold;"&gt;s/--/ -- /g&lt;/div&gt; &lt;/div&gt;&lt;h4&gt;Скрипт&lt;br /&gt;&lt;/h4&gt;Ниже код программы, адаптированный к русскому языку. В качестве аргументов задается имя исходного файла и файла с результатом (списком слов). Предполагается, что исходный файл в Windows-кодировке. При другой кодировке следует отредактировать строчку:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt; &lt;div style="margin-left: 1em; white-space: pre; font-family: monospace; font-weight: bold;"&gt;use open ':encoding(cp1251)';&lt;/div&gt; &lt;/div&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt; &lt;div style="margin-left: 1em; white-space: pre; font-family: monospace; font-weight: bold;"&gt;###############################################&lt;br /&gt;# split_words.pl&lt;br /&gt;# Usage: perl split_words.pl FROM_FILE TO_FILE&lt;br /&gt;###############################################&lt;br /&gt;use strict;&lt;br /&gt;use warnings;&lt;br /&gt;use open ':encoding(cp1251)';&lt;br /&gt;&lt;br /&gt;open (my $I, '&lt;', $ARGV[0]) or die $!; &lt;br /&gt;open (my $O, '&gt;', $ARGV[1]) or die $!;&lt;br /&gt;while (&lt;$I&gt;) {&lt;br /&gt;    chomp;&lt;br /&gt;    s/--/ -- /g;&lt;br /&gt;    my @words = split /\s+/;&lt;br /&gt;    foreach my $w (@words) {&lt;br /&gt;        print $O "$1\n"&lt;br /&gt;            if $w =~ /(([[:alpha:]]+['-])*[[:alpha:]]+'?)/ )&lt;br /&gt;    }&lt;br /&gt;}&lt;br /&gt;close $O;&lt;br /&gt;close $I;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;h4&gt;Python-версия&lt;/h4&gt;А вот версия для Питона:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt; &lt;div style="margin-left: 1em; white-space: pre; font-family: monospace; font-weight: bold;"&gt;&lt;br /&gt;#!/usr/bin/python&lt;br /&gt;# -*- coding: utf-8 -*-&lt;br /&gt;#&lt;br /&gt;# from __future__ import with_statement # Python2.5&lt;br /&gt;import sys&lt;br /&gt;import re&lt;br /&gt;&lt;br /&gt;pattern = re.compile("(([\w]+[-'])*[\w']+'?)", re.U)&lt;br /&gt;with open(sys.argv[1]) as f:&lt;br /&gt;  for line in f:&lt;br /&gt;      line = unicode(line, 'cp1251')&lt;br /&gt;      line = line.replace('--', ' -- ')&lt;br /&gt;      for token in line.split():&lt;br /&gt;          m = pattern.match(token)&lt;br /&gt;          if m:&lt;br /&gt;              print m.group()&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;Код выглядит полегче. Меньше системных операций. Но есть один существенный недостаток: регулярным выражениям в Питоне недостает уникодных расширений. Поэтому пришлось использовать \w. Значит, в слово попадут цифры и символ подчеркивания, с которыми придется разбираться позже. Можно, наверное, как-то иначе с этим справиться, но питоновский код не хочется усложнять.&lt;br /&gt;&lt;h4&gt;Везде свои тонкости&lt;/h4&gt;Локализованная версия испытывалась на русском переводе рассказа Эдгара По &lt;a href="http://www.lib.ru/INOFANT/POE/heart.txt"&gt;"Сердце-обличитель"&lt;/a&gt;. В книге "Text Mining With Perl" с этой же целью используется оригинал: &lt;a href="http://en.wikisource.org/wiki/The_Tell-Tale_Heart"&gt;"Tell-Tale Heart"&lt;/a&gt;. А что если мы обрабатываем текст, по которому не прошелся корректор? Авторы блог-постов, например, часто забывают поставить пробел после точки в конце предложения или после запятой. Предположение, что слова всегда разделены пробелами, может оказаться неверным.&lt;br /&gt;&lt;br /&gt;Вот почему на практике часто используют несколько иной подход. Исходная строка разбивается на слова не только пробелами, а всем, что не входит в слово. Простейший пример:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt; &lt;div style="margin-left: 1em; white-space: pre; font-family: monospace; font-weight: bold;"&gt;@tokens = split(/\W+/, $line);&lt;/div&gt; &lt;/div&gt;Это работает, но к сожалению, все составные слова будут разбиты.&lt;br /&gt;&lt;br /&gt;На Питоне:&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt; &lt;div style="margin-left: 1em; white-space: pre; font-family: monospace; font-weight: bold;"&gt;splitter = re.compile(r'\W*', re.U)&lt;br /&gt;words = [ s for s in splitter.split(text) if s ]&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;br /&gt;А вот более интересный  пример из книги "An Introduction to Language Processing with Perl and Prolog" (Pierre M. Nugues, Springer, 2006):&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt; &lt;div style="font-family: monospace; font-weight: bold;"&gt;$text = &lt;&gt;;&lt;br /&gt;while ($line = &lt;&gt;) {&lt;br /&gt;    $text .= $line;&lt;br /&gt;}&lt;br /&gt;$text =~ tr/a-zåàâäæçéèêëîïôöoeùûüßAZÅÀÂÄÆÇÉÈÊËÎÏÔÖOEÙÛÜ’()\-,.?!:;/\n/cs;&lt;br /&gt;$text =~ s/([,.?!:;()’\-])/\n$1\n/g;&lt;br /&gt;$text =~ s/\n+/\n/g;&lt;br /&gt;print $text;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;Здесь вместо того, чтобы обрабатывать строчку за строчкой, все прочитанные из файла строки, наоборот, сливаются в один текст. Затем:&lt;br /&gt;&lt;ol&gt;&lt;li&gt;Если символ не является ни буквой ни знаком пунктуации, он заменяется символом новой строки&lt;/li&gt;&lt;li&gt;Каждый знаки пунктуации помещается на отдельную строку&lt;/li&gt;&lt;li&gt;Повторяющиеся символы новой строки заменяются одним&lt;/li&gt;&lt;/ol&gt;Чтобы адаптировать этот код к кириллице, придется дополнить длинную цепочку не-ASCII символов, переданных оператору tr. К сожалению, этот оператор не понимает уникодных расширений. Впрочем, того же результата можно добиться более привычным оператором замены (s///).&lt;br /&gt;&lt;br /&gt;Первую программу можно тоже научить работать с неправильными текстами, не меняя алгоритма. Для этого можно расширить оператор разбиения слов. Или предварительно обрабатывать входящие строки -- примерно так же, как мы поступали с "тире".&lt;br /&gt;&lt;br /&gt;Ясно одно: трудно написать универсальную процедуру на все случаи жизни. Надо учитывать, с каким материалом придется иметь дело: с литературными текстами, блогами, юридическими документами... везде свои тонкости.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/964245207178906174-8212136144472304605?l=pi-code.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://pi-code.blogspot.com/feeds/8212136144472304605/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=964245207178906174&amp;postID=8212136144472304605' title='Комментарии: 4'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/8212136144472304605'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/8212136144472304605'/><link rel='alternate' type='text/html' href='http://pi-code.blogspot.com/2008/12/blog-post.html' title='Как разбить текст на слова'/><author><name>Наувул-Наувул</name><uri>http://www.blogger.com/profile/06702180511726415678</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/_3ifHLhKgDjk/TCEZ2cVuxJI/AAAAAAAAB2E/D5uehCRtVtI/S220/IMG_3814.JPG'/></author><thr:total>4</thr:total></entry><entry><id>tag:blogger.com,1999:blog-964245207178906174.post-532820820868608852</id><published>2007-11-20T13:08:00.000+03:00</published><updated>2008-12-11T13:46:40.903+03:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='Perl'/><category scheme='http://www.blogger.com/atom/ns#' term='тесты'/><category scheme='http://www.blogger.com/atom/ns#' term='мишка'/><category scheme='http://www.blogger.com/atom/ns#' term='SQL'/><title type='text'>Спасибо тебе, Мишка!</title><content type='html'>&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://1.bp.blogspot.com/_3ifHLhKgDjk/R0LIKAyEOsI/AAAAAAAAAj8/j31Yr5ZI5E8/s1600-h/Vinnie.jpg"&gt;&lt;img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="http://1.bp.blogspot.com/_3ifHLhKgDjk/R0LIKAyEOsI/AAAAAAAAAj8/j31Yr5ZI5E8/s200/Vinnie.jpg" alt="" id="BLOGGER_PHOTO_ID_5134886599490026178" border="0" /&gt;&lt;/a&gt;&lt;br /&gt;В книге "&lt;a href="http://www252.pair.com/comdog/mastering_perl/"&gt;&lt;span style="font-weight: bold;"&gt;Mastering Perl&lt;/span&gt;&lt;/a&gt;" есть глава "Философия борьбы с неразрешимыми проблемами от Брайена ди Фоя". Один из рецептов звучит так: &lt;span style="font-style: italic;"&gt;"&lt;span style="font-weight: bold;"&gt;А пытались ли вы поговорить об этом с Мишкой?&lt;/span&gt;"&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style="font-style: italic;"&gt;На одной из работ&lt;/span&gt;,-- рассказывает автор,-- &lt;span style="font-style: italic;"&gt;у меня был  коллега, к которому я всегда обращался за советом как только что-то не получалось.  Большинство таких консультаций заканчивалось одинаково: уже после третьего предложения я понимал, в чем дело, и обрывал свой монолог. Теперь&lt;/span&gt;,-- продолжает  Брайен де Фой -- &lt;span style="font-style: italic;"&gt;у меня возле компьютера живет плюшевый Мишка, с которым я вслух обсуждаю рабочие проблемы, дабы не беспокоить коллег.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;hr /&gt;&lt;br /&gt;Я вспомнил этот совет когда ломал голову над тестовым заданием следующего содержания:&lt;br /&gt;&lt;blockquote&gt;Есть две таблицы:&lt;br /&gt;table1 (table1_id integer, name varchar(255)),&lt;br /&gt;table2 (table2_id integer, table1_id integer, alias varchar(255))&lt;br /&gt;&lt;br /&gt;Выбрать записи из table1, для которых количество подчинённых записей в table2 максимально.&lt;br /&gt;&lt;/blockquote&gt;У меня сложные отношения с SQL. Периодически в него погружаюсь и решаю довольно сложные задачи. Но стоит полгода не позаниматься -- все забывается чуть ли не начисто. (Нечто подобное, но еще в большей степени происходит с XSLT). Ясно было, что надо использовать GROUP и MAX, но как объединить все это в один запрос?...&lt;br /&gt;&lt;br /&gt;Я прокрался в спальню, к детской кроватке. У пятилетнего Степы есть несколько медведей, в том числе два белых брата &lt;span style="font-style: italic;"&gt;Кнут&lt;/span&gt; и &lt;span style="font-style: italic;"&gt;Олаф Хаммундсены&lt;/span&gt;. Интуиция подсказывала, что для моих целей подойдет &lt;span style="font-style: italic;"&gt;Винни-Пух&lt;/span&gt;. Через пять минут я четко и с расстановкой объяснял ему, в чем проблема. А через 10 минут решение было готово. Вот оно:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;br /&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace; font-weight: bold;"&gt;SELECT table1_id FROM table2&lt;br /&gt;GROUP BY table1_id&lt;br /&gt;HAVING COUNT(*) =&lt;br /&gt;(&lt;br /&gt;SELECT MAX(table2_count)&lt;br /&gt;FROM (&lt;br /&gt;SELECT COUNT(*) AS table2_count&lt;br /&gt;FROM table2&lt;br /&gt;GROUP BY table1_id&lt;br /&gt;) AS inner_table&lt;br /&gt;);&lt;br /&gt;&lt;/div&gt;&lt;br /&gt;&lt;/div&gt;Ответ находится в 3 этапа:&lt;br /&gt;&lt;ol&gt;&lt;li&gt;Узнать, сколько дочерних записей приходится на каждую родительскую запись.&lt;/li&gt;&lt;li&gt;Найти максимум.&lt;/li&gt;&lt;li&gt;Выбрать строки, где количество дочерних записей равно найденному максимуму.&lt;/li&gt;&lt;/ol&gt;Моя ошибка заключалась в том, что я пытался отыскать какой-то волшебный метод, который позволил бы добиться результата сразу.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/964245207178906174-532820820868608852?l=pi-code.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://pi-code.blogspot.com/feeds/532820820868608852/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=964245207178906174&amp;postID=532820820868608852' title='Комментарии: 7'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/532820820868608852'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/532820820868608852'/><link rel='alternate' type='text/html' href='http://pi-code.blogspot.com/2007/11/blog-post_20.html' title='Спасибо тебе, Мишка!'/><author><name>Наувул-Наувул</name><uri>http://www.blogger.com/profile/06702180511726415678</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/_3ifHLhKgDjk/TCEZ2cVuxJI/AAAAAAAAB2E/D5uehCRtVtI/S220/IMG_3814.JPG'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://1.bp.blogspot.com/_3ifHLhKgDjk/R0LIKAyEOsI/AAAAAAAAAj8/j31Yr5ZI5E8/s72-c/Vinnie.jpg' height='72' width='72'/><thr:total>7</thr:total></entry><entry><id>tag:blogger.com,1999:blog-964245207178906174.post-4307975238904125238</id><published>2007-11-20T01:58:00.000+03:00</published><updated>2007-11-20T03:22:02.997+03:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='JSON'/><category scheme='http://www.blogger.com/atom/ns#' term='JQuery'/><category scheme='http://www.blogger.com/atom/ns#' term='Perl'/><category scheme='http://www.blogger.com/atom/ns#' term='AJAX'/><category scheme='http://www.blogger.com/atom/ns#' term='CGI::Application'/><title type='text'>CGI::Application  и AJAX</title><content type='html'>В выходные решил попрактиковаться с &lt;a href="http://search.cpan.org/%7Emarkstos/CGI-Application-4.06/lib/CGI/Application.pm"&gt;CGI::Application&lt;/a&gt;, &lt;a href="http://jquery.com/"&gt;библиотекой JQuery&lt;/a&gt; и &lt;a href="http://ru.wikipedia.org/wiki/AJAX"&gt;AJAX&lt;/a&gt;, потому что  за год погружения в краулеры/парсеры подзабыл, как пишутся CGI-приложения.  Получился вот такой  &lt;a href="http://slovarik.s2dio.info/"&gt;словарик онлайн&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;Больше всего времени угрохал на то, чтобы разобраться, как передавать клиенту в ответ на асинхронный запрос данные в &lt;a href="http://json.org/"&gt;JSON&lt;/a&gt;-формате -- то есть ни HTML ни XML, а JavaScript. Соответственно, content-type в заголовке HTTP-ответа должен быть "&lt;span style="font-weight: bold;"&gt;text/javascript&lt;/span&gt;".  Между тем, фреймворк CGI::Application предполагает, что каждый "run-mode" возвращает HTML.&lt;br /&gt;Вот как выглядит решение:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace; font-weight: bold;"&gt;$self-&gt;header_type('none');&lt;br /&gt;print "Content-Type: text/javascript; charset=utf-8\n\n";&lt;br /&gt;return objToJson($self-&gt;_get_dictionaries() );&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;ul&gt;&lt;li&gt;&lt;span style="font-weight: bold;"&gt;$self-&gt;header_type(none)&lt;/span&gt;  "сообщает" фреймворку: не заботься о HTTP-заголовках.  &lt;/li&gt;&lt;li&gt; затем мы выводим свой собственный, нестандартный заголовок&lt;/li&gt;&lt;li&gt;метод &lt;span style="font-weight: bold;"&gt;$self-&gt;_get_dictionaries() &lt;/span&gt;возвращает структуру данных, которая  преобразуется в JSON-формат функцией &lt;span style="font-weight: bold;"&gt;objToJson&lt;/span&gt; из &lt;a href="http://search.cpan.org/%7Emakamaka/JSON-1.15/lib/JSON.pm"&gt;CPAN-библиотеки JSON&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;На стороне клиента:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace; font-weight: bold;"&gt;function resetDictionaries() {&lt;br /&gt; $.getJSON('/cgi-bin/words.cgi',&lt;br /&gt;           {rm: 'update_dicts',&lt;br /&gt;            dicttype: document.SearchForm.DictType.value&lt;br /&gt;           },&lt;br /&gt;           function(data) {&lt;br /&gt;             populateCombo('#Dict', data);&lt;br /&gt;           });    &lt;br /&gt;}&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;Эта функция вызывается когда пользователь меняет категорию словаря в верхнем выпадающем списке.&lt;br /&gt;&lt;ul&gt;&lt;li&gt;методом &lt;span style="font-weight: bold;"&gt;JQuery getJSON &lt;/span&gt;мы отправляем на сервер асинхронный запрос&lt;/li&gt;&lt;li&gt;ответные данные -- те самые, которые были упакованы методом &lt;span style="font-weight: bold;"&gt;objToJson &lt;/span&gt;-- приходят в анонимную  &lt;span style="font-weight: bold;"&gt;callback-функцию&lt;/span&gt;. Ее аргумент data преставляет собой  готовый к использованию JavaScript-объект. Функция &lt;span style="font-weight: bold;"&gt;populateCombo&lt;/span&gt; (которая здесь не приводится) динамически меняет содержание  выпадающего списка словарей.&lt;br /&gt;&lt;/li&gt;&lt;/ul&gt;Разобраться с JQuery мне, помимо официальной документации, помогла статья на RSDN: &lt;a href="http://rsdn.ru/article/inet/jQuery.xml"&gt;JQuery -- Javascript нового поколения&lt;/a&gt;.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/964245207178906174-4307975238904125238?l=pi-code.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://pi-code.blogspot.com/feeds/4307975238904125238/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=964245207178906174&amp;postID=4307975238904125238' title='Комментарии: 16'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/4307975238904125238'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/4307975238904125238'/><link rel='alternate' type='text/html' href='http://pi-code.blogspot.com/2007/11/cgiapplication-ajax.html' title='CGI::Application  и AJAX'/><author><name>Наувул-Наувул</name><uri>http://www.blogger.com/profile/06702180511726415678</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/_3ifHLhKgDjk/TCEZ2cVuxJI/AAAAAAAAB2E/D5uehCRtVtI/S220/IMG_3814.JPG'/></author><thr:total>16</thr:total></entry><entry><id>tag:blogger.com,1999:blog-964245207178906174.post-936476701372636851</id><published>2007-11-14T22:46:00.000+03:00</published><updated>2007-11-20T02:41:04.430+03:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='Perl'/><category scheme='http://www.blogger.com/atom/ns#' term='функциональное программирование'/><category scheme='http://www.blogger.com/atom/ns#' term='Python'/><title type='text'>List Comprehension средствами Perl</title><content type='html'>Решая один из вопросов теста&lt;a href="http://www.blogger.com/post-edit.g?blogID=964245207178906174&amp;amp;postID=2060037777963072874"&gt;&lt;/a&gt;, понял, как средствами Perl сделать подобие питоновского '&lt;span style="font-weight: bold;"&gt;list comprehension&lt;/span&gt;'.&lt;br /&gt;&lt;br /&gt;Задача: имеется строка запроса вида: 'param1=foo&amp;amp;param2=bar...'&lt;br /&gt;Необходимо получить хэш: (param1 =&gt; 'foo', param2=&gt;'bar'...)&lt;br /&gt;&lt;br /&gt;Прямолинейное решение:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace; font-weight: bold;"&gt;sub query2hash {&lt;br /&gt;my  $query = shift;&lt;br /&gt;my @arr;&lt;br /&gt;foreach (split  /&amp;amp;/, $query) {&lt;br /&gt;    push @arr, split /=/, $_;&lt;br /&gt;}&lt;br /&gt;@arr;&lt;br /&gt;}&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;Каждому, кто знаком  с "&lt;span style="font-weight: bold;"&gt;list comprehension&lt;/span&gt;" в языке Python, столь многословная инициализация массива @arr покажется каменным веком. Выполнив на Python-е:&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace; font-weight: bold;"&gt;query = 'param1=foo&amp;amp;param2=bar&amp;amp;param3=zoo'&lt;br /&gt;arr = [ pair.split('=') for pair in query.split('&amp;amp;') ]&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;мы получим список списков:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace; font-weight: bold;"&gt;[['param1', 'foo'], ['param2', 'bar'], ['param3', 'zoo']]&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;Можно ли написать на Perl столь же лаконичную конструкцию?...&lt;br /&gt;Проще простого:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace; font-weight: bold;"&gt;sub query2hash {&lt;br /&gt; map { split /=/ } split /&amp;amp;/, shift;&lt;br /&gt;}&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;Всего строчка! Более того,  в Python-е предстоит еще повозиться, чтобы преобразовать список списков в словарь.  А тут мы  получаем хэш даром. Достаточно присвоить список, возвращаемый функцией, хэш-переменной:&lt;br /&gt;&lt;div style="background-color: rgb(230, 230, 230); margin-top: 1em; margin-bottom: 1em;"&gt;&lt;div style="margin-left: 1em; white-space: pre; font-family: monospace; font-weight: bold;"&gt;my %params&lt;br /&gt;  = query2hash( 'param1=foo&amp;amp;param2=bar&amp;amp;param3=zoo' );&lt;/div&gt;&lt;/div&gt;&lt;br /&gt;См также:&lt;br /&gt;&lt;ul&gt;&lt;li&gt;&lt;a href="http://ru.wikipedia.org/wiki/%D0%A4%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5_%D0%BD%D0%B0_%D0%9F%D0%B8%D1%82%D0%BE%D0%BD%D0%B5#.D0.A1.D0.BF.D0.B8.D1.81.D0.BE.D1.87.D0.BD.D1.8B.D0.B5_.D0.B2.D1.8B.D1.80.D0.B0.D0.B6.D0.B5.D0.BD.D0.B8.D1.8F"&gt;Функциональное программироване на Питоне&lt;/a&gt; -- статья из Wikipedia&lt;/li&gt;&lt;li&gt;&lt;a href="http://en.wikipedia.org/wiki/List_comprehension"&gt;List Comprehension&lt;/a&gt; -- о List Comprehension  c примерами на разных языках программирования&lt;/li&gt;&lt;/ul&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/964245207178906174-936476701372636851?l=pi-code.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://pi-code.blogspot.com/feeds/936476701372636851/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=964245207178906174&amp;postID=936476701372636851' title='Комментарии: 3'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/936476701372636851'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/936476701372636851'/><link rel='alternate' type='text/html' href='http://pi-code.blogspot.com/2007/11/list-comprehension-perl.html' title='List Comprehension средствами Perl'/><author><name>Наувул-Наувул</name><uri>http://www.blogger.com/profile/06702180511726415678</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/_3ifHLhKgDjk/TCEZ2cVuxJI/AAAAAAAAB2E/D5uehCRtVtI/S220/IMG_3814.JPG'/></author><thr:total>3</thr:total></entry><entry><id>tag:blogger.com,1999:blog-964245207178906174.post-2060037777963072874</id><published>2007-11-14T20:48:00.000+03:00</published><updated>2007-11-15T19:08:19.733+03:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='тесты'/><category scheme='http://www.blogger.com/atom/ns#' term='алгоритмы'/><category scheme='http://www.blogger.com/atom/ns#' term='трудоустройство'/><title type='text'>Уверенные знания и большие плюсы</title><content type='html'>Компании &lt;a href="http://www.iscann.ru/"&gt;Инфо-скан&lt;/a&gt; требовался ведущий web-разработчик, обладающий списком добродетелей, который я не цитирую, дабы не выглядеть занудой. Все эти &lt;span style="font-style: italic;"&gt;"уверенные знания"&lt;/span&gt;, &lt;span style="font-style: italic;"&gt;"большие плюсы"&lt;/span&gt;...&lt;br /&gt;&lt;br /&gt;Тестовое задание состояло из вопросов по Perl, SQL и Unix. Я отдавал себе отчет, что 24 вопроса -- многовато для первого знакомства. Но работа обещала быть интересной (сбор информации, анализ текста -- близко к поисковым задачам, которыми я занимался почти год).  К тому же, хотелось освежить память по SQL. Вот почему я посвятил этому тесту целый выходной.&lt;br /&gt;&lt;br /&gt;Ответа не последовало -- даже когда через неделю я написал HR-менеджеру, которая со мной связывалась, и попросил дать какой-либо ответ.  Похоже, это закономерность: &lt;span style="font-weight: bold;"&gt;чем длиннее тест, тем меньше шансов узнать о его результатах&lt;/span&gt;.&lt;br /&gt;&lt;br /&gt;Впервые я столкнулся с этим явлением лет 5 назад. Присылаю в одну фирму резюме, в ответ задачка -- вариант классической &lt;a href="http://ru.wikipedia.org/wiki/%D0%97%D0%B0%D0%B4%D0%B0%D1%87%D0%B0_%D0%BA%D0%BE%D0%BC%D0%BC%D0%B8%D0%B2%D0%BE%D1%8F%D0%B6%D1%91%D1%80%D0%B0"&gt;проблемы коммивояжера &lt;/a&gt;. Не дождавшись ответа, я сильно расстроился. Все  думал, что же я сделал не так. Звонить или писать повторно стеснялся: мол, нет -- ну и не надо... Так получилось, что через пару лет я по стечению обстоятельств  совершенно другим  путем устроился на работу  в&lt;a href="http://ptc.ru/"&gt; ту самую фирму&lt;/a&gt; -- о чем вначале не подозревал, так как ее название сменилось. Оказалось, человек, рассылавший эти тесты, просто уволился -- и, как говорится, пускай мертвецы сами хоронят собственных мертвецов.&lt;br /&gt;&lt;br /&gt;Позапрошлым летом я пытался устроиться в Гугл.  После, наверное, месячного молчания оттуда ответили, что у них, к сожалению,  нет подходящей для меня вакансии в Москве. И ни слова о 8-клеточных &lt;a href="http://ru.wikipedia.org/wiki/%D0%9F%D1%8F%D1%82%D0%BD%D0%B0%D1%88%D0%BA%D0%B8"&gt;пятнашках&lt;/a&gt;, которые я вполне корректно решил методом "&lt;a href="http://en.wikipedia.org/wiki/A*_search_algorithm"&gt;A-Star&lt;/a&gt;" -- таково было тестовое задание.&lt;br /&gt;&lt;br /&gt;...Ладно, буду потихоньку публиковать  интересные места из тестов,  может, кому пригодится.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/964245207178906174-2060037777963072874?l=pi-code.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://pi-code.blogspot.com/feeds/2060037777963072874/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=964245207178906174&amp;postID=2060037777963072874' title='Комментарии: 0'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/2060037777963072874'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/964245207178906174/posts/default/2060037777963072874'/><link rel='alternate' type='text/html' href='http://pi-code.blogspot.com/2007/11/blog-post.html' title='Уверенные знания и большие плюсы'/><author><name>Наувул-Наувул</name><uri>http://www.blogger.com/profile/06702180511726415678</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/_3ifHLhKgDjk/TCEZ2cVuxJI/AAAAAAAAB2E/D5uehCRtVtI/S220/IMG_3814.JPG'/></author><thr:total>0</thr:total></entry></feed>
