Олег Макаренко (olegmakarenko.ru) wrote,
Олег Макаренко
olegmakarenko.ru

Category:

Пишем робота на LISP


(картинка: nmap.org)

Френды выдали мне кучу ссылок на срывание покровов с последних выборов. Графики, таблицы, конспирологические выводы… Возник естественный вопрос — кому верить?

Вот если я вижу фотографию ОМОН’овца с буквами «SS» на каске — я могу лично открыть её в графическом редакторе и решить, реальный ли это снимок или коллаж. А как мне убедиться в правильности графика?

Очевидно, лично построить этот график в Excel. Вот только есть маленькая проблема. Данные хоть и выложены в Интернет, но собирать их вручную с трёх тысяч участков — занятие сильно на любителя.

Поэтому в воскресенье я потратил час времени и написал робота, который собирает эти данные за меня.

Если у Вас есть свободная пара часов и желание научиться писать таких роботов самостоятельно — читайте дальше. Я расскажу как. От Вас потребуется знание компьютера на уровне продвинутого пользователя и определённая сила воли, чтобы напрягать в течение этих двух часов свой мозг. Писать робота, кстати, мы будем на LISP.

Почему именно на LISP?

Потому что есть определённая иерархия программистов.

VBasic, PHP: гопники
Perl, Python: гастарбайтеры
C/Cpp: рабочий класс, мужики
Assembler: инженеры
LISP: элита

На всякий случай: я шучу, конечно же. Никакой иерархии программистов нет. Эту забавную таблицу я увидел на одном форуме и, думаю, её автор тоже не был особо серьёзен. Как хорошие, так и плохие программы можно писать на любом языке.

Но мы всё же будем писать на LISP. Точнее, на newLISP: бесплатной среде программирования, разработанной одним гениальным товарищем по имени Lutz. Так как этот язык, на мой взгляд, как нельзя более лучше подходит для решения возникающих в повседневной жизни задач.

Итак, начнём.

1. Установка NewLisp под Windows

Идём вот сюда. Cкачиваем из раздела «Win32 installer» собственно «installer» и версии файлов с включённым UTF-8. UTF-8 будет нужен для поддержки русских букв.



Дальше устанавливаем newLISP, а потом кидаем в c:\Program Files\newlisp два скачанных UTF-8 файла: newlisp.exe и newlisp.dll. На всякий случай, перегружаемся.

1a. Установка newLISP под Linux

Если Вы сидите под Linux'ом, на той же странице есть дистрибутивы и для Вас. Проблем с установкой быть не должно никаких: просто скачиваем и ставим. Тоже UTF-8 версию, да.

Кстати, размер дистрибутива NewLisp — около одного мегабайта. Поэтому ждать скачивания часами не нужно, какой бы тонкий канал у Вас ни был.

2. Установка Java

Для работы в оболочке нам потребуется Java. Более конкретно, JRE 1.6. Если у Вас Windows, скачать её можно вот отсюда. Нужный файл называется "JRE 6 Update 16".

В Убунту, если мне не изменяет память, я ставил java командой «sudo apt-get install sun-java6-jre». В других дистрибутивах установка тоже не должна вызвать проблем. Проверить, что java установилась правильно, можно командой «java --version». После выполнения команды java должна сказать, что текущая версия — 1.6.

3. Работа без Java

Для тех, кто не хочет ставить себе Java (неважно по каким причинам). Вполне можно работать и без неё. Просто запустите файл c:\Program Files\newlisp.exe (или /usr/bin/newlisp), после чего вводите там свой код.

Также можно набить программу в любом текстовом редакторе, сохранить её с расширением .lsp и запустить из командной строки выполнив «newlisp имяпрограммы.lsp».

4. Основы LISP

Запустите Пуск — Программы — newLISP — newLISP-GS (Линуксолюбы: «newlisp-edit» в терминале). Должно появиться вот такое окошко:



Если не появилось — пишите в комментарии, попытаемся разобраться, что не так. Или, чтобы не терять времени, запустите просто «newlisp», он должен работать в любом случае.

Введите снизу:

(+ 2 3)

Нажмите Enter. LISP сложит два и три и выдаст результат — пять. Теперь введите:

(* 3 6 37)

LISP перемножит эти три числа. Идея его работы очень проста: первое слово в скобочках — это команда. Остальные слова в скобочках — это параметры команды.

То есть, те же самые (* 3 6 37) можно было бы записать на каком-нибудь другом языке программирования как 3*6*37.

Собственно, на этом основы языка LISP можно считать изученными. Переходим к практической части.

5. Учим LISP скачивать файл из Интернет

Немного о том, как грамотно использовать GUI. Экран у оболочки LISP разделён на две части, верхнюю и нижнюю. План работы таков:

1. Сверху пишем программу.
2. Нажимаем зелёную кнопку «Play».
3. Наблюдаем снизу результат.

Итак, пишем сверху: (get-url "http://lenta.ru/info/"). Жмём зелёный «Play». Видим внизу скачанный с сайта текст в HTML-формате.

Лирическое отступление. Пользователи Windows увидят текст сразу в правильной кодировке. Пользователям же Linux придётся файл сначала перекодировать из win-1251 в UTF. Для перекодировки нужно будет подключить небольшой модуль русификации. (Примечание от 15 апреля 2014 — модуль называется "russian.lsp" и ссылка на него за давностью лет уже устарела. Если вы паче чаяния найдёте эту древнюю статью и захотите увидеть модуль вживую, пишите мне, я постараюсь найти и выслать. Адрес в профайле). Этот модуль нужно просто скачать и положить к остальным модулям. Которые лежат в «c:\Program Files\Newlisp\modules» или в «/usr/shared/newlisp/modules», соответственно. Пользователи Linux, полагаю, сами догадаются правильно применить на скачанный файл команду «chmod 644».

После того, как скачанный файл будет подложен к остальным модулям, можно подключать его в программе, командой (module "russian.lsp"). А перекодировать получаемый файл в UTF можно будет командой cyr-win-utf. Как на скриншоте:



6. Команда в команде

Вся суть LISP'а в том, чтобы объединять команды таким хитрым образом, чтобы они создавали единое работающее целое. Для объединения двух (или нескольких) команд служат скобочки. Например, чтобы подставить в команду умножить результат команды сложить, надо написать:

(* 8 (+ 70 7))

Кстати, что-нибудь выполнить или посмотреть при работе в LISP довольно просто — достаточно набрать команду внизу экрана. Либо, если команда занимает несколько строчек, открыть новую вкладку (File — New Tab) и выполнить команду там.

Так вот. LISP выполняет команды точно так же, как это делали бы мы, решая математический пример. То есть, раскрывает скобочки по очереди, в порядке вложенности. Сначала складывает семьдесят и семь, а потом умножает результат на восемь.

7. Немного хакерской магии

Знаете, что такое шифрование md5? Я покажу на примере.

Пользователям Linux скачивать дополнительно ничего не надо, а Windows-кунам нужно будет проследовать вот сюда и установить файл GetGnuWin32-0.6.22.exe. Ставить его надо непременно в папку «с:/Program Files/gnuwin32»:



После установки пишем в тексте программы (сверху) следующий код:

(module "crypto.lsp")
(crypto:md5 "Washington")


Первой командой мы подключаем шифровальный модуль, а второй командой мы зашифровываем слово «Washington». В качестве результата LISP выдаёт нам готовый хэш слова «Washington»: 6d69689d0056a27bce65398abc70297a.

Спросите, кому это нужно?

Допустим, у Вас есть сайт. На этом сайте есть пользователь Pupkin с паролем Washington. Если хранить его пароль в открытом виде, как «Pupkin:Washington», любой подлец, похитивший этот файл, сможет узнать пароль Василия. Если же мы храним пароль в зашифрованном виде, негодяй увидит только бессмысленный набор букв: «Pupkin: 6d69689d0056a27bce65398abc70297a».

С другой стороны, мы всегда сможем узнать, верный ли пароль ввёл Pupkin. Так как команда (crypto:md5 "Linkoln") и (crypto:md5 "Obama") выдаст другие хэши. А вот команда (crypto:md5 "Washington") будет всегда выдавать ровно «6d69689d0056a27bce65398abc70297a».

Впрочем, не думайте, что md5 нельзя сломать. Любопытные могут попытаться ввести эти хэши в поиск Гугла и посмотреть на результат.

8. Функции, кэширование файлов

Правила вежливости робототехников предписывают им не скачивать файлы из Интернета по нескольку раз. Скачал файл — положи в кэш. Понадобился файл ещё раз? Не лезь в Интернет: вынь из кэша.

Поэтому мы начнём создание нашей волшебной программы с функции кэширования.

Для начала изучим оператор append. Этот оператор «склеивает» свои аргументы. Например, результатом выполнения (append "pen" "island") будет являться слово «остров ручек»: «penisland». Мы будем использовать оператор append, чтобы указать программе путь к файлам кэша.

На всякий случай: руководство по newLISP, где рассказывается обо всех операторах подробно, лежит вот здесь.

Да, кстати, пришло время создать папку для нашего проекта. Для ясности, я буду предполагать, что Windows-пользователи создали папку «c:\election», а пользователи Linux работают в «/home/fritz/election». Создадим под кэш отдельную папку: «c:\election\cache». И укажем программе, что при запуске следует переходить в свою рабочую папку командой

(change-dir "c:/election")

Попробуйте выполнить эту команду (снизу). Если всё правильно, Вы увидите в качестве результата true.

Теперь можно вводить процедуру кэширования. Выглядеть целиком она будет вот так:

(module "crypto.lsp")
(change-dir "c:/election")

(define (cache-url url)
(let (file-name (append "cache/" (crypto:md5 url)))
(if (file? file-name)
(read-file file-name)
(begin
(set 'text (get-url url))
(write-file file-name text)
text))))


Посмотрим, что тут к чему.

Первые две строчки понятны: подгружаем шифровальный модуль и переходим в рабочую папку. Дальше идёт команда «define», которая определяет функцию. Чтобы проиллюстрировать работу define, попробуем выполнить (в нижнем окне) простой пример.

(define (triple x) (* x 3))

В качестве результата должно вернуться: (lambda (x) (* x 3)). Теперь попробуйте (triple 1) или (triple 3). Числа утраиваются?

Вот так вот define и работает. В первых скобочках — имя функции и её аргументы. Во вторых скобочках — команды, которые над этими аргументами надо выполнить.

Наша функция кэширования, как видите, называется cache-url, а в качестве параметра она принимает адрес веб-страницы — url.

Следующая команда, let, возможна известна Вам ещё по Бэйсику. Она присваивает значение переменной. В нашем случае мы кладём в переменную file-name имя файла: папка cache плюс хэш нужного адреса. Попробуйте выполнить команду:

(append "cache/" (crypto:md5 "http://www.google.com"))

LISP ответит: «cache/ed646a3334ca891fd3467db131372140». Такие имена и будут выдаваться кэшируемым файлам.

Дальше у нас идёт оператор if. Устроен он так:

(if (условие) a b)

Если условие верно — возвращается значение а. Если неверно — возвращается значение b.

Например, суды в LISP работают вот так:

(if (виновен? обвиняемый) посадить отпустить)

Вы можете проверить, как работает if введя, скажем,

(if (= 2 3) "равно" "не равно")

Оператор file? возвращает true, если соответсвующий файл есть и nil, если файла нет. Смысл проверки — выяснить, есть ли уже нужная нам страница в кэше на жёстком диске. Если такая страница есть — запросить её из кэша. Если такой страницы нет — прочесть прямо из Интернет.

Операторы read-file и write-file записывают и читают файл в переменную. А оператор begin позволяет объединить несколько команд в одну скобочку.

Чтобы проверить, работает ли функция кэширования, наберите снизу, например,

(cache-url "http://livejournal.com")

После этого в созданной Вами папочке «cache» должен появиться файл с именем "b2e194563cd8ee1b8dd4142489618b02".

9. Функция извлечения ссылок на избирательные комиссии

Приступаем теперь к исследованию сайта МосГорИзбиркома. Идём вот на эту страницу:

http://www.moscow_city.vybory.izbirkom.ru/region/region/moscow_city?action=show&root=1&tvd=2772000268687&vrn=2772000268682®ion=77&global=&sub_region=77&prver=0&pronetvd=0&vibid=2772000268687&type=379

И наблюдаем следующую картину — все результаты собраны в таблицу по 17 округам. Щёлкнем мышью, например, на ОИК № 1. Там будут уже ссылки на районы. «Район Арбат», «Басманный район»… Щёлкнем на район Арбат. По ссылке пойдут уже рядовые избирательные комиссии. УИК № 1 и так далее. Вот они-то нам и нужны.

Код сайта в IE можно посмотреть, нажав Вид — Источник, а в FireFox и Opere код доступен по клавишам Ctrl+U. Беглое исследование кода показывает: все нужные нам ссылки имеют вид:

http://...длинный_адрес...379

Объясним это интерпретатору LISP. Сделаем функцию extract-links, которая будет извлекать из файла html все нужные нам ссылки. Выглядеть она будет так:

(define (extract-links url)
(find-all {http://.*?379(?=")} (cache-url url)))




Работает функция следующим образом. Сначала LISP раскрывает последние скобки и выполняет команду (cache-url url) — скачивает указанный файл из Интернета. Потом LISP выполняет оператор find-all и находит все ссылки по указанной маске. Результаты функция возвращает в виде списка ссылок на результаты районных избирательных комиссий.

Чтобы убедиться, что функция работает, запустите:

(extract-links "http://www.moscow_city.vybory.izbirkom.ru/region/region/moscow_city?action=show&root=1&tvd=2772000268687&vrn=2772000268682®ion=77&global=&sub_region=77&prver=0&pronetvd=0&vibid=2772000268687&type=379")

В качестве результата должен появиться перечень ссылок.

10. Извлекаем ссылки

Теперь мы готовы извлечь ссылки. Для начала, чтобы каждый раз не набирать длинный адрес общих результатов, запишем его в переменную start-page:

(set 'start-page "http://www.moscow_city.vybory.izbirkom.ru/region/region/moscow_city?action=show&root=1&tvd=2772000268687&vrn=2772000268682®ion=77&global=&sub_region=77&prver=0&pronetvd=0&vibid=2772000268687&type=379")

Обратите внимание на одиночную кавычку перед переменной start-page. Она нужна. Наберите снизу экрана сначала 'start-page, а потом start-page, чтобы увидеть разницу.

Теперь извлечём ссылки. Сделаем это вот такой командой:

(map extract-links (extract-links start-page))

Если у Вас медленный канал, придётся подождать какое-то время. Как-никак, будут скачиваться 17 страниц, пусть и без картинок. В качестве результата Вы получите длинный список ссылок.

Ждать, впрочем, придётся только один раз: при следующем обращении программа не будет лезть в Интернет, а вынет результаты из кэша.

Чтобы проверить, что всё скачалось правильно, наберите снизу:

(length (flat (map extract-links (extract-links start-page))))

Должно получиться число 123. Оператор flat делает список из двумерного «плоским», а оператор length возвращает длину списка.

11. Обрабатываем данные в ячейках

Теперь посмотрим, что находится внутри файлов с результатами. Там находится таблица, такого примерно вида:

<table>
<tr>
<td><nobr>"УИК № 1"</nobr></td><td><nobr>"УИК № 2"</nobr></td>…
<td><b>"1983"</b></td><td><b>"1791"</b></td>…
<td><b>"1400"</b></td><td><b>"1300"</b></td>…
</tr>
</table>


При этом называние УИК выделено тэгами <nobr></nobr>, а нужные нам цифры выделены жирным: <b></b>. Пишем функцию:

(define (cut-data line)
(if (find "УИК" line)
(set 'tag "nobr>")
(set 'tag "b>"))
(if (find (append "(?si)(?<=<" tag ")(.*)(?=</" tag ")") line 1)
$it
nil))


Идея работы функции такова. Если в ячейке встречается слово «УИК», возвращаем то, что находится между тэгами <nobr> и </nobr>. Если не встречается — возвращаем то, что находится между <b> и </b>. Если не находим ничего, возращаем nil. Проверить функцию можно, скормив ей реальные кусочки кода:

(cut-data {<td style="color:black" align="center"><nobr>УИК №1</nobr></td>})

(cut-data {<td align="right" style="color:black"><nobr><b>1983</b></nobr><br></td>})

(cut-data {<td style="color:black"><br></td>})



Перед тем, как проверять, разумеется, надо дописать функцию к телу программы, после (set 'start-page …)

Возможно, у Вас возникнет вопрос, что это за странные символы находятся внутри оператора find. Это — регулярные выражения, с помощью которых сейчас принято обыскивать разного рода тексты. Подробнее про регулярные выражения можно прочесть, например, вот здесь.

12. Вынуть из файла таблицу.

Следующая наша задача — вынуть из файла html таблицу с интересующими нас данными. Создадим для этого функцию parse-html:

(define (parse-html text)
(map
(fn (x) (find-all "(?si)(<td)(.*?)(</td>)" x (cut-data $it)))
(find-all
"(?si)(<tr)(.*?)(</tr>)"
((regex {(?si)(0" align="left">)(.*?)(</table>)} text) 0))))


Вот здесь уже придётся напрячь мозг, чтобы представить себе порядок раскрытия скобок.

Сначала выпоняется вот этот оператор:

(regex {(?si)(0" align="left">)(.*?)(</table>)} text) 0)

Он ищет выдирает из текста нужную нам таблицу целиком. Полученная таблица отдаётся оператору find-all, который шинкует её на строчки. Строчки он опознаёт так: строчка должна начинаться с «<tr», а заканчиваться на «</tr>».

Полученные строчки делятся на ячейки (начинаются с «td», заканчиваются на «/td»).

И, наконец, содержимое ячеек обрабатывается только что написанной нами функцией cut-data. B итоге мы получаем вполне уже пригодную для работы таблицу. Мы можем проверить работу функции, натравив её на результаты района Арбат, например:



Важно. Если Вы работаете в Linux, надо будет добавить ещё и функцию перекодировки, cyr-win-utf. Вот так:

(parse-html (cyr-win-utf (cache-url "http://www.moscow_city.vybory.izbirkom.ru/region/region/moscow_city?action=show&tvd=2772000268688&vrn=2772000268682®ion=77&global=&sub_region=77&prver=0&pronetvd=0&vibid=2772000268715&type=379")))

Немного об операторе fn (x). Это одна из тех удобных мелочей, за которые я и люблю LISP. Этот оператор позволяет определять функции «на лету».

Допустим, нам надо взять список '(1 2 3 4 5) и возвести каждое число из списка в квадрат. Сделать это можно вот так:

(define (square x) (* x x))
(map square '(1 2 3 4 5))


Или вот так:

(map (fn (x) (* x x)) '(1 2 3 4 5))

Результат — тот же, однако вторая запись в два раза короче.

13. Обработка таблицы

Полученная таблица нуждается в обрабротке. Во-первых, её нужно повернуть на 90 градусов, чтобы мы могли склеить несколько таблиц друг с другом. Во-вторых, эту таблицу нужно распечатать, разделив значения запятыми, чтобы мы могли потом загрузить полученный файл в Excel.

Повернуть таблицу можно оператором «transpose». Распечатать — командой

(join (map string строка-таблицы) ",")

Команда string переводит число в строковой формат. Команда join объединяет несколько значений списка, вставляя разделитель. Например:

(join '("время" "не" "ждёт") "-")

Результат: «время-не-ждёт». Проверим, как всё вместе работает.

(set 'Arbat "http://www.moscow_city.vybory.izbirkom.ru/region/region/moscow_city?action=show&tvd=2772000268688&vrn=2772000268682®ion=77&global=&sub_region=77&prver=0&pronetvd=0&vibid=2772000268715&type=379")

(dolist (y (transpose (parse-html (cache-url Arbat))))
(println (join (map string y) ",")))


Если всё введено верно, мы должны получить разделённый запятыми текст:



(На Linux придётся, разумеется, добавить ещё и cyr-win-utf)

Новый оператор dolist пробегается по списку и выполняет определённое действие с каждым его значением.

14. Обработка всех районов

Теперь объединим всё вместе. Готовая программа выглядит вот так:

(set 'start-page "http://www.moscow_city.vybory.izbirkom.ru/region/region/moscow_city?action=show&root=1&tvd=2772000268687&vrn=2772000268682®ion=77&global=&sub_region=77&prver=0&pronetvd=0&vibid=2772000268687&type=379")

(dolist (x (flat (map extract-links (extract-links start-page))))
(dolist (y (transpose (parse-html (cache-url x))))
(println (join (map string y) ","))))


Для Linux одна строчка будет выглядеть иначе:

(dolist (y (transpose (parse-html (cyr-win-utf (cache-url x)))))

Запустите программу и идите пить чай. Робот сам обработает все 123 страницы и распечатает результат.

15. Запись результатов в файл

Теперь нам нужно загнать данные в csv (разделённый запятыми) файл, понятный Excel. Для этого мы ставим в конце программы оператор (exit), записываем готовую программу в файл «election.lsp» и выходим из неё. Пользователями Linux дополнительно надо будет сделать chmod +x на этот файл. Кроме того, пользователям Linux надо будет добавить в начало файла указание пути к интерпретатору LISP:

#!/usr/bin/newlisp

Дальше в командной строке набираем:

Для Windows: newlisp election.lsp > data.csv
Для Linux: ./election.lsp > data.csv

Компьютер немного подумает и запишет файл.



16. Обработка данных

Всё. Дальше можно открывать Excel и пытаться вывести кого-нибудь на чистую воду. Кстати, в Excel 2007 файлы «csv» открываются не вполне очевидным образом: через вкладку «Data» — «From Text».



Заключение

Если Вы дочитали до этого места и у Вас всё получилось, Вы умеете извлекать информацию из Интернет. Если у Вас получилось не всё — пишите в камменты. Я (и мои читатели) поможем разобраться. Также пишите мне в комментарии о найденных ошибках и опечатках.

Если же Вы программист LISP, у которого есть свободное время, пришлите мне письмецо на fritz.morgen@gmail.com. У меня сейчас намечается несколько средних размеров заказов.
Subscribe

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 394 comments
Previous
← Ctrl ← Alt
Next
Ctrl → Alt →
Previous
← Ctrl ← Alt
Next
Ctrl → Alt →