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

    Топик будет интересен тем, кто хочет индексировать Интернет-сайты на предельных скоростях (самодельные поисковики, анализы частоты слов, сервисы по анализу 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'ки).


    Йои Хаджи,
    вид с Хабра
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 11

      +1
      интересная функция, однако я бы на Вашем месте выделил бы куски кода в функции, а то тройные вложенные циклы это круто :) сложно понять. простота кода — залог успеха :)
        0
        Большая часть тела функции — это модифицированный пример использования pycurl.CurlMulti, который, к сожалению, правильно не работал.

        На самом деле, в саму функцию лезть особо не надо будет, практически все, что нужно для типичных задач покрывают параметры, передаваемые ей.
          0
          А двойные вложенные циклы — ну так pycurl устроен, вот чтобы не вспоминать каждый раз как это делать — я и сделал себе multi_get.
          0
          > К сожалению, Хабр убивает whitespace (indentation), без которого код на python не будет работать…

          Можно использовать тэг <pre>
            0
            А можно использовать source code highlighter
              0
              А можно еще использовать code2html прямо из дистрибутива, заменить лидирующие спейсы на
              &nobsp; и получится тоже неплохо:

              a = 1
              while a < 10:
                  a +=1
                  print a

                0
                Пожалуй, да. Как в анекдоте: «Можно и так». :)
            0
            c.setopt(pycurl.NOSIGNAL, 1) — это зачем?
            def removewww(a) — может лучше регуляркой, а то домен может быть examplewww.com
              0
              Только я сразу подумал что это отличная штука для DOS-а кого либо?
              :)
                0
                А уж стандартный апачевский ab, наверное, и вовсе жуть несусветная?
                0
                Всем привет.
                Спасибо автору за информацию про c-ares и некоторые пояснения по скрипту.
                Поскольку сюда будут заходить интересующиеся, позволю себе упростить им жизнь. В свободное от работы время, я иногда пишу обертку над pycurl, которая упрощает API работы с pycurl. Стараюсь максимально приблизиться к python-request.

                Думаю никто не будет спорить, что разобраться во всех принципах работы pycurl задача не очень простая, особенно в асинхронном модуле.

                Библиотека human_curl доступна на гитхабе.

                Вот пример использования асинхронного модуля human_curl:

                from urlparse import urljoin 
                from datetime import datetime
                
                from human_curl.async import AsyncClient 
                from human_curl.utils import stdout_debug
                
                def success_callback(response, **kwargs):
                    """This function call when response successed
                    """
                    print("success callback")
                    print(response, response.request)
                    print(kwargs)
                
                def fail_callback(request, opener, **kwargs):
                    """Collect errors
                    """
                    print("fail callback")
                    print(request, opener)
                    print(kwargs)
                
                with AsyncClient(success_callback=success_callback,
                                 fail_callback=fail_callback) as async_client:
                    for x in xrange(10000):
                        async_client.get('http://google.com/', params=(("x", str(x)),)
                        async_client.get('http://google.com/', params=(("x", str(x)),),
                                        success_callback=success_callback, fail_callback=fail_callback)
                
                


                Обработку данных можно реализовать через систему колбэков.

                Only users with full accounts can post comments. Log in, please.