(картинка: 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><b>"1400"</b></td><td><b>"1300"</b><
</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></n
(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. У меня сейчас намечается несколько средних размеров заказов.
← Ctrl ← Alt
Ctrl → Alt →
← Ctrl ← Alt
Ctrl → Alt →