Bing Image Searchで推しメン画像を自動取得する

[Bing Search API](https://azure.microsoft.com/ja-jp/pricing/details/cognitive-services/search-api/)には何種類かのAPIがあります。今回は画像を検索するBing Image Searchを使って、推しメン画像の自動収集をしてみました。

実行結果はこんな感じです。フォルダ内が可愛い推しメン（乃木坂46の大園桃子さん）画像で溢れて幸せになれます。

![スクリーンショット 2021-05-05 20.31.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47737/7df2f200-b39b-1239-54f4-2cd913b6a981.png)


# Bing Search API v7

まずはBing Search v7を検索して選びます。

![スクリーンショット 2021-05-05 17.53.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47737/cb194567-f860-b730-1f06-83b2620b5955.png)

Bingリソースを作成します。

![スクリーンショット 2021-05-05 17.53.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47737/003064ef-4865-1fc1-6c61-c4fdea4317a7.png)

準備はこれだけです。キーが発行されるので１か２いずれかをコピーします。

![スクリーンショット 2021-05-05 17.54.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47737/d81d7d19-42bf-90ef-480a-c5b2d5a30749.png)


# ハマったこと

先にハマったことを書きたいと思います。最初、以下の記事を参考に進めていました。

https://docs.microsoft.com/ja-jp/azure/cognitive-services/bing-image-search/quickstarts/client-libraries

ライブラリがあるんだな、と何も考えずに以下を追加しました。

```xml
<dependency>
    <groupId>com.microsoft.azure.cognitiveservices</groupId>
    <artifactId>azure-cognitiveservices-imagesearch</artifactId>
    <version>1.0.2</version>
</dependency>
```

記事のコードを参考にキーを設定したのですが、以下のエラーで認証が通りませんでした。

```
Exception in thread "main" com.microsoft.azure.cognitiveservices.search.imagesearch.models.ErrorResponseException: Status code 401, {"error":{"code":"401","message":"Access denied due to invalid subscription key or wrong API endpoint. Make sure to provide a valid key for an active subscription and use a correct regional API endpoint for your resource."}}
```

キーの指定方法は正しく、何が原因がさっぱりわかりませんでした。仕方なく認証の部分で使っているBingImageSearchManagerクラスのソースを確認したところ、ライブラリの中で使っているエンドポイントが以下になっていました。

```
https://api.cognitive.microsoft.com/bing/v7.0/
```

Bingリソースの画面上では以下のエンドポイントになっています。

```
https://api.bing.microsoft.com/
```

ドキュメントを見ると以下のように「Bing Search API は、Cognitive Services プラットフォームから Microsoft.com の下の新しい領域に移行されています。」と警告があるので、古い情報をみてしまっていたようです。

![スクリーンショット 2021-05-05 21.54.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47737/7a4f1644-5167-c5d6-fb2b-e2f7de59006d.png)


# HttpClientでAPIを呼び出す

先ほどのライブラリを使うのは諦めて、HttpClientで呼び出すことにしました。

レスポンスのJSONは以下のような形で色々な情報が含まれています。

```json
{
	"_type": "Images",
	"instrumentation": {
		"_type": "ResponseInstrumentation"
	},
	"readLink": "images/search?q=大園桃子",
	"webSearchUrl": "https://www.bing.com/images/search?q=大園桃子&FORM=OIIARP",
	"queryContext": {
		"originalQuery": "大園桃子",
		"alterationDisplayQuery": "大園桃子",
		"alterationOverrideQuery": "+大園桃子",
		"alterationMethod": "AM_JustChangeIt",
		"alterationType": "CombinedAlterationsChained"
	},
	"totalEstimatedMatches": 997,
	"nextOffset": 38,
	"currentOffset": 0,
	"value": [
		{
			"webSearchUrl": "https://www.bing.com/images/search?view=detailv2&FORM=OIIRPO&q=%E5%A4%A7%E5%9C%92%E6%A1%83%E5%AD%90&id=C98F2558D4D0436AFB3E601D2AF41B57CA13EDEC&simid=608037150088111611",
			"name": "大園玲のかわいい性格の秘密や大園桃子との関係は？推しメン ...",
			"thumbnailUrl": "https://tse1.mm.bing.net/th?id=OIP.GazlCdtTHvJoKftuEEnDbgHaEu&pid=Api",
			"datePublished": "2020-05-16T22:19:00.0000000Z",
			"isFamilyFriendly": true,
			"contentUrl": "https://img.ananweb.jp/2019/12/29195005/2183-beauty1.jpg",
			"hostPageUrl": "https://idolevery.com/oozonorei-12180",
			"contentSize": "286430 B",
			"encodingFormat": "jpeg",
			"hostPageDisplayUrl": "https://idolevery.com/oozonorei-12180",
			"width": 1300,
			"height": 831,
			"hostPageDiscoveredDate": "2020-04-20T00:00:00.0000000Z",
			"thumbnail": {
				"width": 474,
				"height": 302
			},
			"imageInsightsToken": "ccid_GazlCdtT*cp_15E9A25A3E3827AB4EAE1B6B87C4DB2C*mid_C98F2558D4D0436AFB3E601D2AF41B57CA13EDEC*simid_608037150088111611*thid_OIP.GazlCdtTHvJoKftuEEnDbgHaEu",
			"insightsMetadata": {
				"pagesIncludingCount": 11,
				"availableSizesCount": 4
			},
			"imageId": "C98F2558D4D0436AFB3E601D2AF41B57CA13EDEC",
			"accentColor": "A1382A"
		},
		{
        ..省略..
        }
     ]
}
```

とりあえず、contentUrlだけ引っ張れれば良いので、GSONでマッピングするための簡単なクラスを2つ作りました。

```java
package com.example;

public class Value {
    private String contentUrl;

    public Value(String contentUrl) {
        this.contentUrl = contentUrl;
    }

    public String getContentUrl() {
        return contentUrl;
    }

    public void setContentUrl(String contentUrl) {
        this.contentUrl = contentUrl;
    }
}
```

```java
package com.example;

import java.util.List;

public class Result {
    private List<Value> value;

    public Result() {
        
    }

    public Result(List<Value> value) {
        this.value = value;
    }

    public List<Value> getValue() {
        return value;
    }

    public void setValue(List<Value> value) {
        this.value = value;
    }
    
}
```

実装は以下のとおりです。Bing Image Searchで取得したcontentUrlの画像ファイルをダウンロードします。

```java
package com.example;

import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpClient.Version;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.file.Paths;

import com.google.gson.Gson;

public class App 
{
    public static void main( String[] args ) throws URISyntaxException, IOException, InterruptedException 
    {
        HttpClient client = HttpClient.newBuilder()
        .version(Version.HTTP_1_1)
        .build();

        HttpRequest req = HttpRequest.newBuilder(new URI("https://api.bing.microsoft.com/v7.0/images/search?q="
         + URLEncoder.encode("大園桃子", "UTF-8")))
        .header("Ocp-Apim-Subscription-Key", "xxxxxxxx")
        .GET()
        .build();

        HttpResponse<String> ret = client.send(req, HttpResponse.BodyHandlers.ofString());

        Gson gson = new Gson();
        Result result = gson.fromJson(ret.body(), Result.class);

        result.getValue().stream().forEach(v -> {
            try {
                URL url = new URL(v.getContentUrl());
                String fileName = Paths.get(url.getPath()).getFileName().toString();
                ReadableByteChannel readableByteChannel = Channels.newChannel(url.openStream());
                FileOutputStream fileOutputStream = new FileOutputStream(fileName);
                fileOutputStream.getChannel().transferFrom(readableByteChannel, 0, Long.MAX_VALUE);
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        });
    }
}
```


実行すると最初に紹介したように画像が取得できます。