Bing画像検索API v7で画像を収集する

# はじめに
[Bingの画像検索APIを使って画像を大量に収集する](https://qiita.com/ysdyt/items/49e99416079546b65dfc) を参考にBingで画像収集しようとしたところ、APIがv7になっていたので、少し修正することになりました。
また、url取得とダウンロードをわけて行うようしました。

Bingのアカウントの取得や、API Keyの取得などは上記の記事を参考に行ってください。大変お世話になりました。

python素人なので、コードがイマイチなところは目をつぶってください。

# コード
## 画像URL収集用
プログラム中に指定した検索ワードでBing画像検索を行い、結果のURLリストをファイルに保存します。

```python3:bing_image_url_collector.py
# coding: utf-8
import requests
import os
import math
import configparser # for Python3
import urllib
import re
import datetime

import bing_util

def get_headers(api_key):
    return {"Ocp-Apim-Subscription-Key" : api_key}

def get_params(search_term, num_imgs_per_transaction, offset):
    return urllib.parse.urlencode({
        "q": search_term,
        "license": "All",
        "imageType": "photo",
        "count":num_imgs_per_transaction,
        "offset": offset * num_imgs_per_transaction,
        "mkt":"ja-JP"})

def get_search_results(search_url, headers, params):
    response = requests.get(search_url, headers=headers, params=params)
    response.raise_for_status()
    search_results = response.json()
    return search_results

def save_urls(results, filepath):    
    with open(filepath, mode='a') as f:
        for values in results:
            if values['encodingFormat'] == 'jpeg':
                print(values['contentUrl'], file=f)
            
def get_filename(path, fn, ext):
    return os.path.join(path, '%s.%s' % (fn, ext))

def gen_url_save_file(search_term, url_dir_path, total_count):
    ext = 'txt'
    fn = bing_util.search_term2file_name(search_term)
    filename = get_filename(url_dir_path, fn, ext)
    dt = datetime.datetime.now().strftime("%Y%m%d%H%M%S")

    if os.path.isfile(filename):
        fn = '%s_%s' % (fn, dt)
        filename = get_filename(url_dir_path, fn, ext)
    with open(filename, mode='w') as f:
        print("date=%s, search_term=%s, totalEstimatedMatches=%d" % (dt, search_term, total_count), file=f)
    return filename
                                        

if __name__ == '__main__':
    config = configparser.ConfigParser()
    config.read('authentication.ini')
    bing_api_key = config['auth']['bing_api_key']

    save_dir_path = './bing'
    bing_util.make_dir(save_dir_path)
    url_dir_path = os.path.join(save_dir_path, 'url')
    bing_util.make_dir(url_dir_path)
    
    num_imgs_required = 1000 # Number of images you want.
    num_imgs_per_transaction =150 # default 30, Max 150 images
    search_term = "emeperor penguin"

    search_url = "https://api.cognitive.microsoft.com/bing/v7.0/images/search"
    
    headers = get_headers(bing_api_key)
    params = get_params(search_term, num_imgs_per_transaction, 0)

    first_search_results = get_search_results(search_url, headers, params)
    total_count = first_search_results["totalEstimatedMatches"]
    print("totalEstimatedMatches=%d" % total_count)
    
    filepath=gen_url_save_file(search_term, url_dir_path, total_count)

    print ("len=%d" % (len(first_search_results["value"])))
    save_urls(first_search_results["value"], filepath)
    
    if num_imgs_required > total_count :
        num_imgs_required = total_count
        
    offset_count = math.ceil(num_imgs_required / num_imgs_per_transaction)
    print('offset_count=%d' % offset_count)
    for offset in range(1, offset_count):
        params = get_params(search_term, num_imgs_per_transaction, offset)
        search_results = get_search_results(search_url, headers, params)
        
        print ("len=%d" % len(search_results["value"]))
        save_urls(search_results["value"], filepath)

```
URL収集と次の画像ダウンロードで共通する処理だけくくりだしたutil。

```python3:bing_util.py
import os
import re

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

def search_term2file_name(search_term):
    return re.sub('\s+', '_', search_term)
```
### 設定箇所
mainにある以下を編集します。

<dl>
  <dt>search_term</dt>
  <dd>検索キーワードをいれます。多分日本語いけるはずですが、このキーワードを使ったファイルでURL取得リストを作るので、ちょっと注意したほうが良いかと思います。</dd>
  <dt>save_dir_path</dt>
  <dd>URL取得リストを保存するディレクトリを指定。ディレクトリがなければ作ります。</dd>
  <dt>num_imgs_required</dt>
  <dd>ほしい画像枚数。取得するURLリストの上限。ものすごくたくさん欲しくても、検索にマッチした画像がなければ、取れないのです。。。</dd>
  <dt>num_imgs_per_transaction</dt>
  <dd>Bingリクエスト一回あたりに取得する画像URL。Image Search APIのcountに渡す値です。countのデフォルトは30。</dd>
</dl>

あと、Bing Search APIのkeyをこのスクリプトと同じディレクトリに、authentication.ini のファイル名で保存します。

```python3:authentication.ini
[auth]
bing_api_key = XXXXXXXXXXXXXXXXXXXXX
```
という感じに。
### 動きのざっくりした説明
一回のリクエストで取得できる結果がmax 150なので、たくさんほしいときは複数回投げます。複数回投げるときは、APIのパラメータのcountとoffsetを使います。
countとoffsetについては、[参考にさせていただいた記事](https://qiita.com/ysdyt/items/49e99416079546b65dfc#%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)を御覧ください。わかりやすいので。

1. 一回、検索パラメータを組み立てて、検索を投げる。レスポンスから、検索結果件数を取得。
2. URL格納ファイルを作成。
    - ファイル名は　"検索キーワード.txt"。ただし、検索キーワード中にスペースがある場合は"_"に変換。あと、出力ディレクトリに同じファイル名のファイルがあったら、"検索キーワード_YYYYMMDDhhmmss.txt"と日時をくっつけたファイル名にする。
    -  一行目に以下の情報を出力し、二行目以降に結果のURLリストを出力。

     ```date=YYYYMMDDhhmmss, search_term=検索キーワード, totalEstimatedMatches=検索結果件数```
 
3. 検索結果件数 < num_imgs_required の場合は、num_imgs_requiredに検索結果件数をセットし直す。
4. num_imgs_requiredとnum_imgs_per_transactionから、何回検索を投げるかを決めて、必要回数だけ投げて、結果をファイルに格納。

#### いくつか補足
- Image Searchのパラメーターでlicenseを指定できます。サンプルではAllにしているので、licenseを無視してガバガバ集めます。[APIドキュメント](https://docs.microsoft.com/ja-jp/rest/api/cognitiveservices/bing-images-api-v7-reference#query-parameters) これをPublicとかにしていると取得結果がすっごく少なくなる。king penguinだと150件以上あるのに、adelie penguinだと8件しかないとか。
- 取得する画像のタイプをjpegに制限しています。他の画像種類が必要でしたら、save_urlsを直してください。

## 画像ダウンロード用
前項で取得したURLリストのファイルを使って、画像をダウンロードするスクリプトです。動かし方は、

```
$ python3 bing_image_downloader.py URL格納ファイル
```

```python3:bing_image_downloader.py
# coding: utf-8
import requests
import os
import sys
import re
import hashlib
import json

import bing_util

def get_target(line):
    matchObj=re.match(r"date=(\d{14}), search_term=([^,]+)", line)
    return [matchObj.group(1), matchObj.group(2)]

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

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

    return response.content

def gen_image_md5(image_data):
    return hashlib.md5(image_data).hexdigest()

def save_image_file(img_save_dir, content):
    filename = "%s.jpg" % (gen_image_md5(content))
    filepath = os.path.join(img_save_dir, filename)
    with open(filepath, "wb") as fout:
        fout.write(content)
    return filename

if __name__ == '__main__':

    filepath = sys.argv[1]
    save_dir_path = './bing'
    bing_util.make_dir(save_dir_path)

    file = open(filepath)
    lines = file.readlines()
    file.close()
    
    [target_date, search_term] =get_target(lines[0].rstrip())
    print (search_term)
    img_save_dir=os.path.join(save_dir_path, '%s_%s' % (bing_util.search_term2file_name(search_term), target_date))

    bing_util.make_dir(img_save_dir)

    correspondence_table = {}

    for url in lines[1:]:
        try:
            url = url.rstrip()
            content = download_image(url)
            filename = save_image_file(img_save_dir, content)
            correspondence_table[url] = filename
            print ("filename:%s" % filename)
            
        except KeyboardInterrupt:
            break
        except Exception as err:
            print("%s : %s" % (err, url))

    with open(os.path.join(img_save_dir, 'corr_table.json'), mode='w') as f:
        json.dump(correspondence_table, f)

```

### 動きのざっくりした説明

1. URL格納ファイルを開いて、一行目の情報をとってきます。
2. 画像をためるディレクトリを作成します。``save_dir_path/URLファイル中の検索キーワード_URLファイル中のdate/`` です。
3. URLにリクエストを投げて画像を2.にためていきますが、同じ画像が別URLで置かれている場合があるので、画像をhash値にしてファイル名としています。
4. 最後にURLと画像ファイル名の対応をcorr_table.jsonというファイル名で、画像保存ディレクトリに書き出します。

# おわりに
大量に集めてきた画像を前に途方に暮れている現状。。。この先が大変。