Pull to refresh

multi_get — качаем сайты оптом

Reading time4 min
Views1.7K
Топик будет интересен тем, кто хочет индексировать Интернет-сайты на предельных скоростях (самодельные поисковики, анализы частоты слов, сервисы по анализу html'я и т.п.) Threading тут не дает предельных скоростей, urllib — тем более… Решение здесь в использовании асинхронных запросов из libcurl.

Скорость?
На 500MHZ (очень-очень слабенький VPS) — около 100 URLов в секунду (100 соединений, 2 процесса).
На Amazon EC2 «High-CPU Medium Instance» (.2$/час) ~ 1200 URLов в секунду (300 соединений, 5 одновременных процессов). В один процесс до 660 URLов в секунду.

Для выкачивания множества сайтов и дальнейшей обработки, хочу поделиться одной своей полезной функцией — multi_get — по сути она — удобный wrapper для CurlMulti (libcurl), модифицированный из их примера CurlMulti.

>>> urls = ['http://google.com/', 'http://statcounter.com/']
>>> res = {}
>>> multi_get(res, urls, num_conn = 30, timeout = 5, percentile = 95)
>>> res['http://google.com/']
'<html><title>Google....
# тут обрабатываем res, который содержит HTML всех для URL'ок


Вот этот код скачает два сайта в 30 соединений. Точнее, конечно в два соединения, но мне тут просто места не хватило 10000 url'ов вписать.

Вкусности и полезности:

0. При num_conn=1 функция превращается в последовательный (не параллельный) скачиватель, но со всеми преимуществами ниже (куки, юзер-адженты, безусловные таймауты)

1. Если допустим мы в res заранее определим 'http://google.com/' как имеющий какое-то значение — скачиваться этот адрес не будет (пропущен будет). Суть в том, что если у Вас res не просто обычный dict, а каким-то образом persistent (например, хранится в файле или в SQLе каком-то) — то скачиваться при каждом вызове будут только те сайты, которые уже не скачивались раньше.

2. multi_get(res, url, debug = 1) — выводить информацию о ходе скачивания (консоль замедляет процесс, так что на production лучше отключать).

3. multi_get(res, url, percentile = 95) — частенько 90-99% сайтов из большого списка скачиваются практически за микросекунды каждый, но 1-2 сайта из большого списка будут очень медленными. В результате у Вас пролетает 9990 сайтов за минуту, допустим, а оставшихся 10 вы будете ждать еще минуту — это жутко снижает эффективность — поэтому есть такой параметр — скачать 95% (или сколько надо — 99, 50, 75) самых быстрых URLов и выйти, не ожидая медленных.

4. multi_get(res, url, timeout = 5) — таймаут на отедельный URL — 5 секунд (в отличии от таймаута встроенных сокетов в Python — всегда работает и не зависает без причин).

5.… ua = 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)'… — так мы и поверили, но все же — какой передать User-Agent.

6.… ref = 'http://google.com/bot.html'… — какой передать Referer
ref_dict = {'http://google.com/': 'http://mysite.com/?url-google.com'} — dict значений какому URLу какой Referer передать.

7.… cf = 'cookiefile.txt'… — использовать куки, хранить их в этом файле.

8.… follow = 0… — не следовать редиректами (по умолчанию следует).
Если, например, индексировать все .com домены — многие из них на одно и то же редиректят, так что редиректы лучше просто игнорировать.

9. res — не обязательно dict, например Вы можете определить новый класс MyDict, где сделать def __setitem__(self, url, html): и обрабатывать html асинхронно, прямо во время скачивания — не дожидаясь конца вызова multi_get, только определите еще def keys(self): return [] — возвращайте пустой список или список URLов, которые не надо скачивать.

Код


К сожалению, Хабр убивает whitespace (indentation), без которого код на python не будет работать, так что код здесь: rarestblog.com/py/multi_get.py.txt (или rarest.s3.amazonaws.com/multi_get.py.txt)

Там же в коде есть пример, который делать 10 YQL-запросов, чтобы получить 1000 случайных ссылок и скачать 80% из них и замерить скорость.

ВАЖНЫЕ МОМЕНТЫ


Вам нужно будет установить pycurl ( pycurl.sourceforge.net/download )
> easy_install pycurl
если easy_install не установлен, то сначала:
> python -murllib http://peak.telecommunity.com/dist/ez_setup.py | python - -U setuptools
а потом уже вышеприведенную строчку.

Для тестового скрипта еще понадобится
> easy_install cjson
(можно при желании заменить cjson.decode на simplejson.loads — если вы понимаете зачем)

Установка c-ares под Linux/FreeBSD

Под Windows все уже в порядке (.exe сетапник уже включает в себя вкомпилированную поддержку 'c-ares'), однако на сервере (Linux/FreeBSD) Вам нужно будет обязательно установить поддержку 'c-ares' (асинхронные DNS запросы), иначе скорость pycurl/multi_get падает в десятки раз — у Вас не получится использовать больше 20-30 соединений без c-ares.

# wget http://curl.haxx.se/download/curl-7.19.4.tar.gz
# tar zxvf curl-7.19.4.tar.gz
# cd curl-7.19.4
Под Linux: # ./configure --enable-ares --with-ssl --enable-ipv6 --with-libidn
Под FreeBSD: # ./configure --enable-ares=/usr/local --with-ssl --enable-ipv6 --with-libidn

"--with-ssl --enable-ipv6 --with-libidn" - по желанию можно убирать.

# make
# make install

[linux only]
Под линуксом надо будет еще пошаманить с библиотекой:
# rm -rf /usr/lib/libcu*
# ln -s /usr/local/lib/libcurl.so.4 /usr/lib/libcurl.so.4
# ln -s /usr/local/lib/libcurl.so.4 /usr/lib/libcurl.so
# ldconfig
[конец linux only]

# cd ..
# rm -rf curl-7*

# python -c "import pycurl;print pycurl.version"
убеждаемся, что в списке фич присутствует c-ares


Anti-DOS

2. Поскольку скрипт довольно тупой, но мощный — можно в легкую случайно начать DOSить чей-нибудь сайт, чтобы избежать этого — там включена маленькая функция reduce_by_domain, которая сжимает список, так чтобы с одного домена был только 1 URL — мера предосторожности, чтобы не положить чей-нибудь сайт.

short_list_of_urls = reduce_by_domain(urls)

Как скачать все URLы, без убивания сайтов? Вызывать reduce_by_domain, multi_get несколько раз подряд — помните, что если res не очищать, то те же URLы не будут качаться второй раз (см. 1. во «Вкусности и полезности»), остается только убирать из списка urls то, что уже скачали и снова делать short_list_of_urls = reduce_by_domain(urls); multi_get(res, short_list_of_urls).

Еще нюансы:

Ошибочные URLы будут возвращены со значением "---".
Файлы размером более 100 000 байт не будут скачиваться.
Файлы .pdf не будут скачиваться.

Это сделано как меры предосторожности — все можно изменить очень легко в коде функции, чтобы не индексировать то, что не нужно (картинки, .pdf'ки).


Йои Хаджи,
вид с Хабра
Tags:
Hubs:
Total votes 16: ↑14 and ↓2+12
Comments11

Articles