пятница, 15 мая 2009 г.

Распределенный граббер своими руками

Иногда по долгу службы, иногда ещё каким-другим образом возникает задача сграбить (скачать) какой-либо контент. И зачастую суть задачи сводится к воровству этого самого контента: дело это конечно не очень благородное, но с начальством не поспоришь. В этой статье я хочу рассказать о том как справился с этой задачей с помощью распределенного скриптового граббера.

Итак, постановка задачи. На просторах интернета существует некий сайт с большим количеством хорошего контента (около 150 000 страниц), не предоставляющий удобные способы экспорта с этого сайта вкусной информации (xml, csv и прочее). Необходимо со страниц этого сайта извлечь нужные нам фрагменты контента.

Нужно было выявить закономерности.

Шаблонные URL-ы

В любом большом большом сайте практически всегда есть повторяющиеся URL-ы, служащие для вывода этой самой информации.

Приведу пример: есть сайт содержащий описание стран – их культурные традиции и прочую информацию. Вот адреса страниц сайта:

  1. http://example.com/country_info.php?id=12
  2. http://example.com/country_12.html

В первом адресе явно видно, что параметр id указывает нам на страницу со страной № 12. Во втором адресе все тоже не сложно – и только не наблюдательный человек не догадается, куда нужно подставлять id страны.

Даже если вы не можете найти шаблон для адреса нужных вам страниц, вы всегда сможете сгенерировать список ссылок на эти страницы путем парсинга собственно тех страниц, где эти ссылки есть — будь то карта сайта или другая навигационная страница. Так работает любой граббер сайтов.

После того как нашли, что будем парсить, нужно писать собственно парсер.

Пишем парсер

Как правило систематизированный контент выводится скриптом по определенному одному шаблону (или по ограниченному количеству шаблонов), а значит этот самый контент подвержен парсингу.

И снова пример:


Россия – великая могучая держава, только две головы ее идут в разные сторо…

Пишем парсер для выдирания текста (я буду использовать PCRE ибо привычка): /\\\(.*)\<\/td\>\<\/tr\>\<\/table\>/si Модификатор “s” – нужен, чтобы текст между табличными тегами вырезался полностью, без учета переноса строк. Модификатор “i” нужен для игнорирования регистра символов (на всякий случай). И не забываем экранировать служебные символы. Подробнее о синтаксисе PCRE можно прочитать на http://pcre.org.

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

Ну и что?

Казалось бы, а что сложного? Парсеры мы и до этого умели писать, про шаблонные URL-ы тоже слышали, как граббер работает знаем… Сложность в том, что на том сайте, который мы собрались грабить, стоит суровый фаервол или администратор – по вкусу. Эти два момента могут присутствовать одновременно и даже перманентно. Это означает, что вы не сможете вот так махом скачать с чужого сайта нужную информацию – это заметят и пресекут.

Как вас могут заметить? Если вы пишите на php, то для скачивания страницы вы скорее всего не задумываясь будите использовать встроенную функцию file_get_contents(), перебирая в цикле все ссылки, и это первая ошибка. При просмотре логов сервера (той же статистики сайта) будет четко видно как с одного ip шел поток http-запросов (примерно 150 000 :) без заголовка User-Agent. Это однозначно какой-то скрипт, либо самопальный браузер – для админа без разницы, залочит ip будите сидеть куковать. А даже если и не заметят, то скорее всего на ваш скрипт сработает защита от DDoS атак, выполненная в виде фаервола заботливым администратором. Кстати, варианты с использованием dial-up, когда ip у нас динамический я не рассматриваю – попробуйте через dial-up подключение скачать 150 000 страниц примерно по 80 Кб каждая. Вторая ошибка заключается именно в отсутствии пауз между запросами к нашему контент-сайту, это может порождать большую нагрузку на сервер: так что раз уж мы собрались воровать контент, давайте делать это красиво :)

Как быть? Создавать миниатюрную распределенную систему для граббинга. Примечание – в общем случае эта система может служить для чего угодно, не только для граббинга.

Итак, распределяемся

В отсутствии у меня большого числа компьютеров с выделенным подключением к интернету, будем использовать удаленные сервера с бесплатным виртуальным хостингом и поддержкой одного из скриптовых языков для организации проксирующих точек. Для примера, возьмем PHP. Важно учитывать, что на этих серверах должно быть разрешено открытие удаленных файлов на чтение (allow_url_fopen).

Теперь схема:
Я –(запрос)-> проксирующая_точка –(запрос)-> страница_сайта_с_контентом –(ответ)-> проксирующая_точка –(ответ)-> Я

При наличии большого количества проксирующих точек и при случайном алгоритме выбора проксирующей точки для запроса той или иной страницы сайта схема сильно разветвиться в своем среднем звене. Проксирующая точка — это ничто иное как компьютер с уникальным ip и установленным скриптом, выполняющий роль прокси-сервера.

Код проксирующей точки: // Проксирующая точка
if (md5($_POST["pass"]) == "7cf4c18ca666d477") require $_POST["url2include"];
?>

Скрипт проверяет правильность пришедшего пароля pass и выполняет другой скрипт из файла, URL которого пришел в переменной url2include. Чтобы сразу было понятно, что это за файл с таким URL-ом, уcловимся что URL=http://myhost.com/somecode.txt

Так как скрипт somecode.txt подключается внутри проксирующей точки — фактически мы получаем единую систему. Таким образом, меняя лишь один параметр url2include мы можем на всех наших прокси-точках выполнять произвольный код. Это очень удобно, когда количество прокси-точек доходит до 100, а вам неожиданно требуется внести изменение в проксирующий скрипт: изменения вносятся в один файл somecode.txt, а не в 100 его копий на разных серверах. Кроме того, если в последствии у вас появится задача, требующая распределенного решения, то вам опять же нужно лишь создать один файл с рабочим кодом и дальше только передать путь до этого файла вашим прокси-скриптам. Для чего в коде проксирующей точки стоит авторизация по паролю, думаю, объяснять не нужно.

Имитация браузера

Чтобы кто-то случайно не подумал, что ваши действия вызваны скриптом, ботом, стихийным бедствием или НЛО мы будем прятаться под обычных пользователей.

По сути обычный пользователь отличается от всего вышеперечисленного двумя вещами:

  1. Он не враждебно настроен к контент-сайту
  2. Он смотрит сайт через браузер, а браузер в HTTP запрос к серверу подсовывает свой идентификатор — User-Agent. Кто не знает, что это такое — стыд и срам, идем гуглить.
Собственно для имитации браузера мы напишем код, который и положми в файл somecode.txt

Вот этот код: /* Проксирующий http-шлюз */
require "http://mikhail.voronkov.biz/pantry/httprequest.class.php5.txt";

$request = new HTTP_Request($_POST["url2import"]);
$request->setHeader("User-Agent", $_POST["ua"]);
$request->setHeader("Connection", "Close");
echo
$request->sendRequest();
?>

В данном примере я использую класс HTTP_Request, его исходник можно взять http://mikhail.voronkov.biz/pantry/httprequest.class.php5.txt

Теперь разберемся что собственно делает этот код. Он посылает GET запрос на адрес из переменной url2import, сопровождая этот запрос заголовком User-Agent, берущимся из переменной ua. Причем запрос на контент-сервер идет именно с проксирующей точки и именно с нужными нам заголовками.

Паузы

Хорошим тоном будет добавить паузу между скачиванием отдельных страниц – это снизит нагрузку на целевой сервер и снизит вероятность срабатывания защитных систем на этом самом сервере.

Ну вот и всё. Имея парсер, квази-браузер и несколько проксирующих точек вы без особого труда сможете запустить процесс граббинга сайта в цикле.