怠慢mirenn所感

描けば強くなる?

Bing Image Search API v7で画像をちょっと保存する方法

f:id:mirenn:20171111205408p:plain
やりたいことがあってけものフレンズの画像を収集することに決めて、それにBing Image Searchを用いることにしました。しかしいざ使おうとすると古いバージョン(v5)がなくてv7しか見当たらずしょうがないからあまり情報がない最新バージョンをやることに。
v5だったら巷にコードがいっぱい落ちていてそのまま使えたんですが、v7は最近も最近だから記事がない。のでv7で、指定した検索ワードで検索された画像を手元に保存するコードを書きました。
 以下全コード。実行環境はpython3.6、windowsです。

# -*- coding: utf-8 -*-
import http.client
import urllib.parse
import requests
import json
import os

# Replace the subscriptionKey string value with your valid subscription key.
subscriptionKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

# Verify the endpoint URI.  At this writing, only one endpoint is used for Bing
# search APIs.  In the future, regional endpoints may be available.  If you
# encounter unexpected authorization errors, double-check this value against
# the endpoint for your Bing search instance in your Azure dashboard.
host = "api.cognitive.microsoft.com"
path = "/bing/v7.0/images/search"

term = "クラピカ"
save_dir_path = "./hunterxhunter"
count = 0


def make_dir(path):
    if not os.path.isdir(path):
        os.mkdir(path)


def BingImageSearch(search):
    "Performs a Bing image search and returns the results."
    headers = {'Ocp-Apim-Subscription-Key': subscriptionKey}
    conn = http.client.HTTPSConnection(host)
    query = urllib.parse.quote(search)
    conn.request("GET", path + "?q=" + query, headers=headers)
    response = conn.getresponse()
    headers = [k + ": " + v for (k, v) in response.getheaders()
                   if k.startswith("BingAPIs-") or k.startswith("X-MSEdge-")]
    data = response.read()
    conn.close()
    return headers, data.decode("utf8")


def make_img_path(save_dir_path, url, term):
    save_img_path = os.path.join(save_dir_path, term)
    make_dir(save_img_path)
    global count
    count += 1

    file_extension = os.path.splitext(url)[-1]
    if file_extension.lower() in ('.jpg', '.jpeg', '.gif', '.png', '.bmp'):
        full_path = os.path.join(save_img_path, str(count)+file_extension)
        return full_path
    else:
        raise ValueError('Not applicable file extension')


def download_image(url, timeout=10):
    response = requests.get(url, allow_redirects=True, timeout=timeout)
    if response.status_code != 200:
        error = Exception("HTTP status: " + response.status_code)
        raise error

    content_type = response.headers["content-type"]
    if 'image' not in content_type:
        error = Exception("Content-Type: " + content_type)
        raise error

    return response.content


def save_image(filename, image):
    with open(filename, "wb") as fout:
        fout.write(image)


def main():
    if len(subscriptionKey) == 32:
        try:
            make_dir(save_dir_path)
            url_list = []

            print('Searching images for: ', term)
            headers, result = BingImageSearch(term)
        except Exception as err:
            print("[Errno {0}] {1}".format(err.errno, err.strerror))
        else:
            data = json.loads(result)

            for values in data['value']:
                    unquoted_url = urllib.parse.unquote(
                            values['contentUrl'])
                    url_list.append(unquoted_url)

        for url in url_list:
            try:
                img_path = make_img_path(save_dir_path, url, term)
                image = download_image(url)
                save_image(img_path, image)
                print('saved image... {}'.format(url))
            except KeyboardInterrupt:
                break
            except Exception as err:
                print("%s" % (err))
    else:
        print("Invalid Bing Search API subscription key!")
        print("Please paste yours into the source code.")


if __name__ == '__main__':
    main()

 基本的に以下のURLのbing image search v5でのコードを参考にしています。
Bingの画像検索APIを使って画像を大量に収集する - Qiita

使い方

 まず、subscriptionKeyのところにAzureで作成したBing Image API v7のkeyを代入してください。keyはダッシュボードで確認できます。ここは上記参考URLに詳しいです。
 次に集めたい画像の検索ワードをtermに代入して、保存したいディレクトリをsave_dir_pathに代入してください。今だとデフォルトで次のようになっています。

term = "クラピカ"
save_dir_path = "./hunterxhunter"

 だから上のコードを実行すると、コードが置いてある同じ階層にhunterxhunterというフォルダができて、さらにその下にクラピカというフォルダが生成されてその中に画像が連番で保存されていきます。
 hunterxhunter/クラピカフォルダはこんな感じ。「命をかける」
f:id:mirenn:20171111220342p:plain

ちょっとコードの解説

 今回作成したコードは上記参考サイトのようにひとつの検索ワードで1000件保存するといったものとは違い本当に単純なもので、検索したい画像を35個だけ保存するというものです(取得したURLの画像が指定した画像形式でない場合などには35個以下になります)。35個保存というのはv7のAPIがデフォルトでは検索1回に35個のURLを返してくれて、今回まさに何もいじらずに素直にデフォルトの35個だけ利用しているからです。
 今回のコードでは、APIを呼び出している関数はBingImageSearchという関数だけになります。これは以下のv7の公式ドキュメントから引っ張ってきたものになります。
docs.microsoft.com

def BingImageSearch(search):
    "Performs a Bing image search and returns the results."
    headers = {'Ocp-Apim-Subscription-Key': subscriptionKey}
    conn = http.client.HTTPSConnection(host)
    query = urllib.parse.quote(search)
    conn.request("GET", path + "?q=" + query, headers=headers)
    response = conn.getresponse()
    headers = [k + ": " + v for (k, v) in response.getheaders()
                   if k.startswith("BingAPIs-") or k.startswith("X-MSEdge-")]
    data = response.read()
    conn.close()
    return headers, data.decode("utf8")

 この関数はとりあえず検索したURLが入ったデータを返すものという程度の理解でいいです。とりあえず欲しいデータを返してくれて、それはJSONというデータ形式で渡してくるということが重要です。検索した画像を保存するために私達は、そこから欲しいURLの情報だけ抜き出す必要があります。
 URLの抜き出しの話をする前に、JSONがどういう形で帰ってきているかを見てみましょう。JSONとは以下のようなもので、{}でくくられているデータ形式です。
f:id:mirenn:20171111224239p:plain
 これは、json.load( )に入力してしまえばpythonの辞書配列として扱えるように簡単に変換できます。欲しいURLはvalueの中のcontentUrlというkeyの値なので以下のようなコードになっています。

            for values in data['value']:
                    unquoted_url = urllib.parse.unquote(
                            values['contentUrl'])
                    url_list.append(unquoted_url)

 こうしてurlがurl_listに格納されていきます。欲しい画像のurlが分かっていたらあとは実はpythonではあっという間で、以上が今回書いたコードの大雑把な流れです。

最後に私的反省

私がやったのはAPIを叩く部分を最新のバージョンに合わせたくらいがせいぜいで、他の部分は参考URLのコードをお手本にしています。他にももっと便利な関数がそちらではついていてもっと長いコードだったんですが、こちらではシンプルにBing Image Search API v7をまわしてみる取っ掛かりとして最低限必要なだけに絞ってみました。まぁこれは自分のタスク上1つのけものフレンズにつき1000件も集めることはないしそれぞれとりあえず35件でいいやという妥協がもとです。実際私はこのコードをもとに、最初にけものフレンズの動物名のリストを作ってfor文で回してけものフレンズの画像を収集しました。案の定それぞれ35件まるまる使えそうな画像ばかりというわけではないんですがそれは当たり前ですしまぁまぁ満足しています。
本当はgoogleの画像検索APIがいいな~なんて思っていて、けどそれは使い勝手が悪いらしいということでしょうがなくAzure登録してBing Image Search APIを使うことにしたのですが不都合なところはありませんでした。1ヶ月無料で使えるようだし良いですね。APIころころ変わってなければもっとやりやすかったと思います。まあ十二分に簡単でしたしpythonの勉強にもなって良かったと思います。
どうせそのうちまたAPIが変わってAPIの叩き方も変わるんでしょうが、JSONでデータが返ってくるという形式は変わらないだろうと思うのでそこさえちゃんと分かっていればあとの残りのコードは過去のものが参考になると思います。
でも私の場合画像集めはネットで集めるより動画から切り出したほうが良いよなとか思い始めているのでもう二度とBing Image Search APIについて調べることはないかもしれません……。