Bing + Python, поиск изображений

    Bing+PythonИногда бывает нужно набрать картинок по определённой тематике, чтобы иметь возможность выбрать из существующего набора нужную и т.д. Текущие поисковики дают такую возможность, но надо открывать браузер, переходить по страницам, работать мышкой и, вообщем, заниматься этим. Хотелось бы иметь консольную утилиту «запустил и забыл» для набора нужных картинок. Рассматривается Bing API, начало работы на Python и их связка для поиска изображений.


    Введение


    Это моя первая более-менее большая программа на Python'е, который я стал изучать недавно (кстати, огромное спасибо kossmak за его переводы статей). Примеры использования в конце статьи.

    ТЗ


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

    Почему Bing


    Некоторое время назад нужно было протестировать асинхронный загрузчик под ActionScript. Для нагрузки был выбран Google, однако, в результате оказалось, что Google выдаёт не больше 64 результатов по запросам через API. Этого (на тот момент) хватило, но осадок остался. После поисков было найдено: Yahoo (с комментариями, что многие выдаваемые им данные устарели) и Bing (который на своей странице обещает до 1000 результатов). Был выбран Bing, т.к., помимо самого запроса, позволяет накладывать на него фильтры (см. ниже)

    Bing


    Разработка под Bing начинается со страницы Bing Developer Center. Там необходимо получить APP_ID для подписывания каждого запроса, регистрация минутная. Я не очень разбирался с накладываемыми ограничениями (может быть их просто нет) так что публикую вместе с примерами свой тестовый APP_ID (если намерены использовать, то рекомендую завести и вбить свой APP_ID в код).

    Bing API


    API существует для VB/C#/C++/F#/JS, но в данном примере используется конечный http request. Описание API для поиска изображений здесь
    Итак, минимальный запрос для поиска картинок и ответа в формате JSON выглядит так:
    api.search.live.net/json.aspx?appid=APP_ID&sources=image&query=SEARCH_QUERY
    Пример запроса (поиск по слову apple):
    http://api.search.live.net/json.aspx?appid=4EFC2F2CA1F9547B3C048B40C33A6A4FEF1FAF3B&sources=image&query=apple

    Python


    Тут всё просто и кросс-платформерно. Сам питон (версии 2.6.x) ставится отсюда. В качестве среды разработки мне очень понравился PyDev. Ставим Eclipse(если ещё нет) и из под него ставим PyDev

    Алгоритм


    Не буду комментировать поблочно, в коде много комментариев, к тому же он не так велик, чтобы не поместить его одним блоком. Коротко:
    • В главном цикла отсылается запрос на Bing API и увеличивается параметр image.offset пока либо не наберётся требуемое количество изображений, либо Bing API не выдаст, что мол результаты кончились.
    • Каждый запрос запрашивает по 8 картинок (остановился на таком размере, 4 слишком мало, для 16 иногда долго ждать ответа, максимум 50).
    • Для каждой найденной картинки извлекается URL, и заводится нитка, которая скачивает картинку в память и сохраняет на диск. Здесь столкнулся с проблемой — картинки очень часто называются одинаково. Так что функция сохранения «блокирует» остальные нити, и добавляет к имени файла "_" спереди, пока не окажется что такого файла ещё нет. Далее сохранение и раблокировка.


    Код


    # import used libraries
    import urllib, json, sys, os, threading

    def load_url(url, filename, filesystem_lock):
        try:
            # open connection to URL
            socket = urllib.urlopen(url)
            # read data
            data = socket.read()
            # close connection
            socket.close()
        # on all exceptions
        except:
            print "error loading", url
        # if no exceptions
        else:
            # save loaded data
            save_to_file(data, filename, filesystem_lock)
            
    def save_to_file(data, filename, filesystem_lock):
        # wait for file system and block it
        filesystem_lock.acquire()
        try:
            # while already have file with this name        
            while os.path.isfile(filename):
                # append '_' to the beginning of file name
                filename = os.path.dirname(filename) + "/_" + os.path.basename(filename)
            # open for binary writing
            with open(filename, 'wb'as f:
                # and save data
                f.write(data)
                f.close()
            print filename
        except:
            print "error saving", filename
        # release file system
        filesystem_lock.release()
        
    def main():
        # Bing search URL
        SERVICE_URL = "http://api.search.live.net/json.aspx"
        # request parameters dictionary (will append to SERVICE_URL) 
        params = {}
        params["appid"]         = "4EFC2F2CA1F9547B3C048B40C33A6A4FEF1FAF3B"
        params["sources"]       = "image"
        params["image.count"]   = 8
        params["image.offset"]  = 00

        # try to read command line parameters
        try:
            params["query"= sys.argv[1]
            images_count = int(sys.argv[2])
            if len(sys.argv) > 3:
                params["image.filters"= sys.argv[3
        # if have less than 2 parameters (IndexError) or
        # if second parameter cannot be cast to int (ValueError)
        except (IndexErrorValueError):
            # print usage string
            print "Bing image search tool"
            print "Usage: bing.py search_str images_count [filters]"
            # end exit
            return 1

        # make directory at current path
        dir_name = "./" + params["query"+ "/"
        if not os.path.isdir(dir_name):
            os.mkdir(dir_name)
            
        # list to store loading threads
        loaders = []
        # file system lock object
        filesystem_lock = threading.Lock()
        
        try:
        
            # loop for images count
            while(params["image.offset"< images_count):
                
                # combine URL string, open it and parse with JSON
                response = json.load(urllib.urlopen(SERVICE_URL + "?%s" % urllib.urlencode(params)))
                # extract image section
                images_section = response["SearchResponse"]["Image"]
        
                # if current search offset greater or equal to returned total files  
                if "Total" not in images_section or params["image.offset">= images_section["Total"]:
                    # then break search loop
                    break
                
                # extract image results section 
                results = images_section["Results"]
                # loop for results
                for result in results:
                    # extract image URL
                    image_url = result["MediaUrl"]
                    # create new loading thread  
                    loader = threading.Thread(\
                        target = load_url,\
                        args=(\
                              image_url,\
                              dir_name + os.path.basename(str(image_url)),\
                              filesystem_lock))
                    # start loading thread
                    loader.start()
                    # and add it to loaders list
                    loaders.append(loader)
                    # advance search offset
                    params["image.offset"+= 1
                    # break if no more images needed
                    if params["image.offset">= images_count:
                        break;            
        
        # on all exceptions
        except:
            print "error occured"
            return 1
        
        # wait for all loading threads to complete 
        for loader in loaders:
            loader.join()

        # all done
        print "done"
        return 0;

    if __name__ == '__main__':
        status = main()
        sys.exit(status)

    Примеры запросов


    Для уточнения запроса можно использовать фильтры Bing API, разделённые пробелом.
    • bing.py apple 1000 — найти 1000 картинок по запросу «apple».
    • bing.py "obama" 16 "size:large style:graphics face:face" — найти 16 портретов Обамы, большого размера в стиле иллюстрации.
    • bing.py "warhammer wallpaper" 16 "size:width:1280 size:height:1024" — найти 16 обоев по теме «warhammer», размерами 1280x1024

    Создание single-exe под win32


    Для этого понадобится py2exe, установить можно отсюда. Далее, в папке с программой создаётся файл setup.py со следующим содержимым (программа в файле bing.py):
    from distutils.core import setup
    import py2exe, sys, os

    sys.argv.append('py2exe')

    setup(
        console=['bing.py'],
        options = {'py2exe': {'bundle_files'1}},
        zipfile = None,
    )

    И запускается на выполнение командой «python setup.py». В результате выполнения в папке ./dist оказывается «скомпилированная» программа (файл w9xpopen.exe можно стереть)
    Далее её можно пожать UPX'ом (с 5182Kb ужалось до 4061Кb)

    Что хотелось бы улучшить


    • Запросы на русском
    • Общий индикатор прогресса по всем файлам
    • Прогресс загрузки по каждого файла
    • Использование time-out при попытке загрузки изображения (он по-умолчанию вроде бы минутный)
    • Нормальная обработка ошибок

    P.S.


    Странный хабра-глюк.
    <code><font color="#666666">0</font></code>
    Не выводит ничего.
    Также ссылки вида
    <code>http://api.google.com</code>
    Выводятся без http://

    P.P.S.


    Скомпированный exe под Win32 тут.
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 29
    • +2
      вкусно и компактно пишете, спасибо, годная заметка — ничего лишнего!

      и отдельно спасибо за UPX, как раз вот-вот понадобится упаковщик.
      • +3
        и отдельное спасибо за ваш appid ;)
      • –13
        вы совсем с ума посходили? это же все равно, что святым мячом на стороне дьявола воевать!
        • –4
          мало минусов, хочу еще
          • –2
            Никогда не откажу тебе в просьбе, поэтому воткнул минусик :-)
        • 0
          Обычно так и получается ;( Сатана льстив… Ну и черт с ним. И в аду будут яблони цвести (В смысле и MS будет опенсорсом :).
        • +1
          О, неожиданно. Прочитал с удовольствием. Автор, спасибо.
          • +4
            Для .exe попробуйте еще PyInstaller — он сразу и .exe умеет делать.

            Для скачивания вместо threading лучше пользовать pycurl и можно мою функцию multi_get (описание на Хабре, исходник)

            Тогда весь код становится примерно таким (и умеет хорошо обрабатывать таймауты и не нужно lock и threads):

            import multi_get
            import json
            import urllib
            import sys
             
            urlencode = lambda a:urllib.urlencode({'':a})[1:]
             
            surl = "api.search.live.net/json.aspx?appid=4EFC2F2CA1F9547B3C048B40C33A6A4FEF1FAF3B" + \
                               "&sources=image&query=" + urlencode(sys.argv[2])
             
            # Берем результат Bing
             
            res = {}
            multi_get.multi_get(res, urls,  timeout = 30)
             
            response = json.load(res[surl])
            results = response["SearchResponse"]["Image"]["Results"]
            urls = list(result["MediaUrl"] for result in results)
             
            # Качаем все изображения
             
            res = {}
            multi_get.multi_get(res, urls,  timeout = 10)
             
            # Сохраняем их
             
            n = 
            for url, txt in res.iteritems():
              n += 1
              f = open('%s.jpg' % n, 'wb')
              f.write(txt)
              f.close()

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

            pycurl под 2.6 (судя по тому, что json используете) лежит здесь longsincefaded.com/curl/, под линукс следите чтобы была сборка с библиотекой c-ares — по умолчанию это не так и будет жутко тормозить из-за не асинхронных dns запросов.
            • 0
              Там n = 0, нолик куда-то Хабр скушал.
              • 0
                да и вместо urls в первом должно быть [surl,]
                • 0
                  Спасибо за PyInstaller и multi_get, буду пробовать.
            • 0
              понравился комментарий:
              • +1
                # import used libraries
                import urllib, json, sys, os, threading
              • +1
                А можно скомпилированный exe?
                • 0
                  Добавил ссылку в конце поста
                • 0
                  Интересно, насколько легальна такая автоматическая сборка изображений. И сразу два подвопроса:

                  1. Насколько нелегальна сборка изображений с сайтов. Ясно что нельзя собирать изображения от куда попало, ибо на изображениях могут быть копирайты. Вопрос как бы сделать так, чтобы сборка проходила только с сайтов где изображения без коммерческих лицензий. Например Google API позволяет указать тип лицензии на изображения при запросе (но я сомневаюсь что механизм надежен). Еще можно указать в запросе что сбор должен идти только с определенного сайта (с заведомо свободными изображениями), но API позволяют указать только один сайт, но не список.

                  2. Насколько поисковики Bing, Google и п.р… вообще согласны с таким использованием своего API. Думаю что они явно API открывали не для таких целей и у них могут быть притенении к его нецелому использованию.
                  • +1
                    1. Ничего нелегального тут нет. Если кто-то что-то выложил в сеть на общий доступ, то понятное дело, что он соглашается с тем, что это могут скачать. А уж чем — руками через браузер или программой — всё равно. Другой вопрос — использование.
                    2. API как раз для таких целей и выкладывается. Для крайних случаев как раз и вводится APP_ID (на который можно повесить по кол-ву использований в день) или просто сильно ограничивается число ответов (как поступил Google)
                    • 0
                      1. На многих сайта явно указано, что любое копирование любой информации без разрешения владельца запрещено. На счет того, что «раз выложил значит согласен что могут скачать» это врядли. Понимают что могут скачать, но не факт что согласен, и не факт что не будет предпринимать действий против скачавшего.

                      2. Может быть я и параноюсь, но судя по лицензионному соглашению, они рассчитывают что API будет в основном использовано для выдачи поисковых результатов релевантных текущей странице. И обязательно с указанием логотипа поисковика. Есть много ограничений, хотя явно не сказано что запрещено делать грабберы. Но сказано, что API не должно быть использовано для ресурсов, основной целью которого является поиск.
                  • +1
                    На ubuntu 9.10 при запросе содержащим русские символы валится с "'ascii' codec can't decode byte 0xd0 in position 2: ordinal not in range(128)" в 100 строке. Лечится если os.path.basename(image_url),\ заменить на os.path.basename(str(image_url)),\
                    • +4
                      Приятно видеть, когда человек старается узнать что-то новое и при этом не боится выставлять свои изыскания на показ.

                      Несколько замечаний по стилю Python кода:

                      — не надо стараться писать комментарии к каждой строчке кода, особенно
                      # close
                      something.close()

                      — стоит писать код так, чтобы он сам себе был комментарием

                      — в Питоне есть удобная штатное средство docstring, очень желательно в них коротко описывать параметры функции и возвращаемые значения

                      — очень плохо прятать исключения в слепой try: except:

                      — сообщения об ошибках лучше выводить в STDERR, а не в STDOUT

                      — внимательно прочитайте что такое with и почему в нем не имеет смысле f.close()

                      Вообще, на Питоне надо стараться не писать как на «еще одном Бэйсике» или «на Паскале с отступами», чем раньше переступите через эти шаблоны, тем лучше. Особенно это касается ООП.

                      Утверждение для затравки — операторы class и def в Питоне по сути являются операторами присваивания.
                      (тут надо сделать умное лицо и попытаться понять что и куда они присваивают :)

                      • 0
                        try:
                        # open connection to URL
                        socket = urllib.urlopen(url)
                        # read data
                        data = socket.read()
                        # close connection
                        socket.close()
                        # on all exceptions
                        except:
                        print «error loading», url
                        # if no exceptions

                        )))

                        1. вас на 10-11 запросе просто забянят.
                        2. где треды то ) вы же не на пыхе сидите
                        3. а чем гугло плох? там картинок то в разы больше
                        • +1
                          1. Забирал по 1000, как-то не банят.
                          2. Треды в коде.
                          3. Вы пост читали?
                        • 0
                          Сам учу Python, полезно, понравилось.
                          Только почитайте PEP 8, есть пара недочетов. :)
                          • 0
                            упс, вверху уже есть комментарий. извиняюсь.
                          • +1
                            with open(filename, 'wb') as f:
                                        # and save data
                                        f.write(data)
                                        f.close()
                            


                            а разве with сам не закроет файл?
                          • +1
                            <code>
                                            loader = threading.Thread(\
                                                target = load_url,\
                                                args=(\
                                                      image_url,\
                                                      dir_name + os.path.basename(str(image_url)),\
                                                      filesystem_lock))
                            </code>


                            В этом случае не обязательно ставить обратные слеши.

                            Написали скриптец, да. Вы молодец, но в статье абсолютно ничего интересного. Если бы реализовали pyBing или типа того, а так…
                            • 0
                              О полезная тулза! :)

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