Четыре метода загрузки изображений с веб-сайта с помощью Python

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

    Вот эти методы:

    1-ый метод

    Первый метод использует модуль urllib (или же urllib2). Пусть имеется ссылка на некое изображение img. Метод выглядит следующим образом:

    import urllib
    
    resource = urllib.urlopen(img)
    out = open("...\img.jpg", 'wb')
    out.write(resource.read())
    out.close()
    


    Здесь нужно обратить внимание, что режим записи для изображений — 'wb' (бинарный), а не просто 'w'.

    2-ой метод

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

    import urllib
    urllib.urlretrieve(img, "...\img.jpg")
    


    Притом стоит заметить, что функция urlretrieve в библиотеке urllib2 по неизвестным мне причинам (может кто подскажет по каким) отсутствует.

    3-ий метод

    Третий метод использует модуль requests. Метод имеет одинаковый порядок скорости выгрузки картинок с первыми двумя методами:

    import requests
    
    p = requests.get(img)
    out = open("...\img.jpg", "wb")
    out.write(p.content)
    out.close()
    

    При этом при работе с веб в питоне рекомендуется использовать именно requests вместо семейств urllib и httplib из-за его краткости и удобства обращения с ним.

    4-ый метод

    Четвертый метод по скорости кардинально отличается от предыдущих методов (на целый порядок). Основан на использовании модуля httplib2. Выглядит следующим образом:

    import httplib2
    
    h = httplib2.Http('.cache')
    response, content = h.request(img)
    out = open('...\img.jpg', 'wb')
    out.write(content)
    out.close()
    


    Здесь явно используется кэширование. Без кэширования (h = httplib2.Http()) метод работает в 6-9 раза медленнее предыдущих аналогов.

    Тестирование скорости проводилось на примере скачивания картинок с расширением *.jpg c сайта новостной ленты lenta.ru. Выбор картинок, подпадающих под этот критерий и измерение времени выполнения программы производились следующим образом:

    import re, time, urllib2
    
    url = "http://lenta.ru/"
    content = urllib2.urlopen(url).read()
    imgUrls = re.findall('img .*?src="(.*?)"', сontent)
    
    start = time.time()
    for img in imgUrls:
        if img.endswith(".jpg"):
            """реализация метода по загрузке изображения из url"""
    
    print time.time()-start
    


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

    Таблица сравнения скоростей методов
    Метод 1, с Метод 2, с Метод 3, с Метод 4, с (без кэширования, с)
    0.823 0.908 0.874 0.089 (7.625)

    Данные представлены как результат усреднения результатов семи измерений.
    Просьба к тем, кто имел дело с библиотекой Grab (и с другими), написать в комментариях аналогичный метод по скачиванию изображений с помощью этой и других библиотек.
    Метки:
    Поделиться публикацией
    Комментарии 18
    • 0
      Если принимаются сторонние модули, то я бы ещё посоветовал посмотреть на pycurl.
      В нём, например, поддерживается keep-alive, который при скачивании нескольких картинок с одного сервера может положительно способствовать в плане скорости. Например, urllib.urlopen это не поддерживает когда я последний раз это проверял.
      А если будет скачиваться текстовая информация, то pycurl так же может прозрачно это сжимать (если сервер поддерживает, конечно).
      • +10
        Весь пост — это один из ответов на SO, который я недавно встречал.
        • –6
          И что? Значит он абсолютно бесполезен? Я вот думаю над одним проектом, где надо парсить и сохранять странички. Мне этот пост попался вовремя.
          Спасибо автору!
          • +8
            Я ничего против автора не имею, но хабр тем и ценен, что статьи в нем отличаются от вопросов на stackoverflow. Формат хабра скорее: «Как я написал парсер», или «Используем requests + lxml для парсинга».
            • –7
              Хабр ценен своими комментариями.
          • +6
            Не хватает sock.connect(...) ....write('GET /img.jpg HTTP/1.0\r\nHost: ...') ...read()
            • +2
              Я около полугода назад начал использовать python для аналогичных целей — массового парсинга страниц, поэтому мне тоже было интересно, какой способ работает быстрее. Для этого я набросал небольшой тест: pastebin.com/mH2ASEGX. Скрипт в 100 итераций получает главную страницу vk.com и ищет на ней наличие паттерна — типичные действия при парсинге. Резульаты следующие:
              ('testUrllib()', 19.59859853472629)
              ('testUrllib2()', 22.586007300934412)
              ('testHttplib()', 16.670537860489773)
              ('testSocket()', 1.5129479809538537)
              ('testRequests()', 9.380710576092)
              ('testPycurl()', 17.76420596649031)
              

              Из выводов: видно, что urllib-функции и httplib работают приблизительно в два раза медленнее, чем популярная библиотека Requests. Это вызвано тем, что urllib* не поддерживают keep-alive и на каждый запрос открывают-закрывают новый сокет (в третьей версии питона это исправили). Нужно скзаать, что с httplib кипэлайвы использовать, в принципе, можно, но контролировать их нужно вручную, через хедеры, тогда скорость работы будет приблизительно в 2 раза выше. Pycurl по скорости тоже ничем не отличается от других высокоуровневых библиотек, не знаю, правда, поддерживает ли он keep-alive.
              Ну а сокеты, как самый низкоуровневый доступ к сети, рвут все библиотеки с огромным отрывом.

              Поэтому если стоит вопрос максимальной производительности и нет сложных http-запросов, то лучше все оформить в виде какой-нибудь своей обертки над сокетами.
              • 0
                На самом деле curl достаточно эффективная библиотека, просто вы не используйте multicurl, который на больших объёмах отлично себя показывает.
                Однопоточные парсеры прошлый век, количество данных с каждым годом только растёт.
                • 0
                  Ну если уже говорить о действительно больших масштабах, то pyCurl в многопоточных приложениях себя плохо ведет, так как использует блокирующие функции, тот же getaddrinfo для резолвинга домена в IP. Поэтому лучший вариант — это gevent и подобные асинхронные решения.
                  • 0
                    Нет такой проблемы, если libcurl собрана с поддержкой c-ares. По умолчанию, ни в одном популярном дистре линукса (кроме Archlinux, кажется) этого нет, но можно самому собрать нужные пакеты.
                    • +1
                      Ну а статья эта — ну совсем для самых маленьких.
                • 0
                  Только что запустил этот тест у себя локально на libcurl 7.34.0 и получил, что testPycurl примерно на 40% быстрее, чем testUrllib.
                  Видимо, действительно, зависит от сборки как выше уже заметили.

                  Кстати, если добавить сжатие, то testPycurl будет ещё вдобавок где-то в два раза быстрее (у меня в результате получилось примерно в три раза быстрее testUrllib'a):
                  curlHandler.setopt(pycurl.ENCODING, 'gzip')
                  


                  • 0
                    Я в urllib2 добавлял поддержку gzip так:
                    class GZipProcessor(urllib2.BaseHandler):
                        """A handler to add gzip capabilities to urllib2 requests
                        http://techknack.net/python-urllib2-handlers/
                        """
                        def http_request(self, req):
                            req.add_header("Accept-Encoding", "gzip")
                            return req
                        https_request = http_request
                    
                        def http_response(self, req, resp):
                            if resp.headers.get("content-encoding") == "gzip":
                                gz = GzipFile(
                                            fileobj=StringIO(resp.read()),
                                            mode="r"
                                          )
                                old_resp = resp
                                resp = urllib2.addinfourl(gz, old_resp.headers, old_resp.url,
                                                          old_resp.code)
                                resp.msg = old_resp.msg
                            return resp
                        https_response = http_response
                    
                    
                    opener = urllib2.build_opener()
                    opener.add_handler(GZipProcessor())
                    opener.open("http://example.com/")
                    

                    С keep-alive у urllib к сожалению всё печально. Рецепты есть, но старые и не поддерживаемые.
                • +1
                  Не понятно на что тут смотреть, даже проверки валидности изображения нет. Отдаст сервер 404 и будет битое изображение где-то потом выдаваться.
                  • +1
                    Сравнивать Grab не имеет смысла, граб это удобный фрэймворк поверх pycurl, данная операция может быть выполнена как синхронным грабом from grab import Grab так и асинхронным Spider — from grab.spider import Spider. Смысла особого нет замерять время, в конечном итоге все упирается в ширину канала и нестабильный пинг до цели.
                    • +1
                      Здесь явно используется кэширование. Без кэширования (h = httplib2.Http()) метод работает в 6-9 раза медленнее предыдущих аналогов.


                      Метод 4, с (без кэширования, с)
                      0.089 (7.625)


                      Сперва не заметил «с» перед скобочкой… И подумал, что с кешированием медленнее.
                      • 0
                        Для Python3 надо написать первые 2 способа так:

                        Способ 1

                        from urllib.request import urlopen
                        
                        resource = urlopen(img)
                        out = open("...\img.jpg", 'wb')
                        out.write(resource.read())
                        out.close()
                        


                        Способ 2

                        from urllib.request import urlretrieve
                        urlretrieve(img, "...\img.jpg")
                        
                        • 0
                          Может я чего-то не знаю, но я действительно не понимаю, почему эта новость находится на главной странице. Скоро, наверное, будут выкладывать на главную способы вывести «хеллоу ворлд», и пост длиной в абзац.

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.