Bing Image Search APIを使ってみた

画像認識のモデル作成用に大量に取得する必要があったため、[Bing Image Search API](https://azure.microsoft.com/ja-jp/services/cognitive-services/bing-image-search-api/)を呼び出して画像を一括で取得してみました。
[Bingの画像検索APIを使って画像を大量に収集する](https://qiita.com/ysdyt/items/49e99416079546b65dfc)に載っていたPythonのコードを少し修正して使わせてもらいました。
試した環境は Windows 10 / Python 3.8.2 です。

## コード

```py:bing-api.py
# -*- coding: utf-8 -*-
import http.client
import json
import re
import requests
import os
import math
import pickle
import urllib
import hashlib
import sha3


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


def make_correspondence_table(correspondence_table, original_url, hashed_url):
    """Create reference table of hash value and original URL.
    """
    correspondence_table[original_url] = hashed_url


def make_img_path(save_dir_path, url):
    """Hash the image url and create the path

    Args:
        save_dir_path (str): Path to save image dir.
        url (str): An url of image.

    Returns:
        Path of hashed image URL.
    """
    save_img_path = os.path.join(save_dir_path, 'imgs')
    make_dir(save_img_path)

    file_extension = os.path.splitext(url)[-1]
    file_extension_changed = re.sub('\?.*(\S+)', '', file_extension)
    if file_extension_changed.lower() in ('.jpg', '.jpeg', '.gif', '.png', '.bmp'):
        encoded_url = url.encode('utf-8') # required encoding for hashed
        hashed_url = hashlib.sha3_256(encoded_url).hexdigest()
        full_path = os.path.join(save_img_path, hashed_url + file_extension_changed.lower())

        make_correspondence_table(correspondence_table, url, hashed_url)

        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)


if __name__ == "__main__":
    save_dir_path = "save/directory/path"
    make_dir(save_dir_path)

    num_imgs_required = 20 # Number of images you want. The number to be divisible by 'num_imgs_per_transaction'
    num_imgs_per_transaction = 10 # default 30, Max 150
    offset_count = math.floor(num_imgs_required / num_imgs_per_transaction)

    url_list = []
    correspondence_table = {}

    headers = {
        # Request headers
        'Content-Type': 'multipart/form-data',
        'Ocp-Apim-Subscription-Key': 'api-key-value', # API key
    }

    for offset in range(offset_count):

        params = urllib.parse.urlencode({
            # Request parameters
            'q': 'xxxxxx',
            'mkt': 'ja-JP',
            'count': num_imgs_per_transaction,
            'offset': offset * num_imgs_per_transaction # increment offset by 'num_imgs_per_transaction' (for example 0, 150, 300)
        })

        try:
            conn = http.client.HTTPSConnection('api.cognitive.microsoft.com')
            conn.request("GET", "/bing/v7.0/images/search?%s" % params, "{body}", headers)
            response = conn.getresponse()
            data = response.read()

            save_res_path = os.path.join(save_dir_path, 'pickle_files')
            make_dir(save_res_path)
            with open(os.path.join(save_res_path, '{}.pickle'.format(offset)), mode='wb') as f:
                pickle.dump(data, f)

            conn.close()
        except Exception as err:
            print("[Errno {0}] {1}".format(err.errno, err.strerror))

        else:
            decode_res = data.decode('utf-8')
            data = json.loads(decode_res)

            pattern = r"(http.+)" # extract an URL of image

            for values in data['value']:
                unquoted_url = urllib.parse.unquote(values['contentUrl'])
                img_url = re.search(pattern, unquoted_url)
                if img_url:
                    url_list.append(img_url.group(1))

    for url in url_list:
        try:
            img_path = make_img_path(save_dir_path, url)
            image = download_image(url)
            save_image(img_path, image)
            print('saved image... {}'.format(url))
        except KeyboardInterrupt:
            break
        except Exception as err:
            print("%s" % (err))

    correspondence_table_path = os.path.join(save_dir_path, 'corr_table')
    make_dir(correspondence_table_path)

    with open(os.path.join(correspondence_table_path, 'corr_table.json'), mode='w') as f:
        json.dump(correspondence_table, f)
```
## 前提
- Bing Image Search APIのサブスクリプションが済んでいること
- Pythonがインストール済みであること
- 取得した画像ファイルの保管先としてパラメーター`save_dir_path`で指定するディレクトリのパスが作成済みであること

## 実行時に指定する内容
- `headers` の `Ocp-Apim-Subscription-Key` に取得したAPI keyを指定
- 検索したいクエリーを `params`中の `q`で指定（日本語も可）
- 取得したい画像枚数を `num_imgs_required` で指定
- 1トランザクションで取得する画像数は `params`中の`count`で指定（defaultは35枚で、Maxは150枚）
- 落としてきた画像を保存するpathを `save_dir_path` で指定

## 実行方法
`bing-api.py`を置いているディレクトリパスにて以下のコマンドを実行

```
py bing-api.py
```

## 元の記事のコードからの主な変更点
- APIのバージョンアップに対応して、エンドポイントやメソッドを変更

```py:変更前
conn.request("POST", "/bing/v5.0/images/search?%s" % params, "{body}", headers)
```
```py:変更後
conn.request("GET", "/bing/v7.0/images/search?%s" % params, "{body}", headers)
```

- 保管対象となる画像を拡大。元の記事のコードだと「.jpg」「.jpeg」「.gif」「.png」「.bmp」のいずれかで終わるURLの画像を保管する作りになっていましたが、 `https://xxxx.jp/image/assessment/2019/10/xxxxx.jpg?ts=12345678` のようにファイル拡張子の後にクエリ文字列がくっつく画像もそこそこあったので、クエリ文字列がついてても保管できるようにしました。

```py:変更前
    file_extension = os.path.splitext(url)[-1]
    if file_extension.lower() in ('.jpg', '.jpeg', '.gif', '.png', '.bmp'):
        encoded_url = url.encode('utf-8') # required encoding for hashed
        hashed_url = hashlib.sha3_256(encoded_url).hexdigest()
        full_path = os.path.join(save_img_path, hashed_url + file_extension.lower())

        make_correspondence_table(correspondence_table, url, hashed_url)

        return full_path
```
```py:変更後
    file_extension = os.path.splitext(url)[-1]
    # 次の行でクエリ文字列が含まれていた場合には削除するようにしています
    file_extension_changed = re.sub('\?.*(\S+)', '', file_extension)
    if file_extension_changed.lower() in ('.jpg', '.jpeg', '.gif', '.png', '.bmp'):
        encoded_url = url.encode('utf-8') # required encoding for hashed
        hashed_url = hashlib.sha3_256(encoded_url).hexdigest()
        full_path = os.path.join(save_img_path, hashed_url + file_extension_changed.lower())

        make_correspondence_table(correspondence_table, url, hashed_url)

        return full_path
```