SeleniumでGoogle検索した画像を取得する方法

スクレイピングでGoogle検索した画像を持ってくる方法です。
スクレイピングの違法性は他のところでもいろいろ言われているので、実施する際はご自身でご注意ください。
SeleniumでGoogleChromeを動かします。Pythonの環境構築、Anacondaのインストール方法などは、他の方もたくさん記事を書いているのでそちらを参照ください。
## 環境
```
Windows10
anaconda=4.13.0
python=3.10.5
GoogleChrome
```

## ディレクトリ構成
```
root
├ data
├ script
│  └ scraping_from_google.py
└ chromewebdriver.exe
```
「data」フォルダにスクレイピングした画像を保存します


## コード全文
先にコード全文を載せます。
```scraping_from_google.py
import os
from pathlib import Path
import random
import re
import requests
import string
import sys
import time

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.common.exceptions import NoSuchElementException
from selenium.common.exceptions import ElementClickInterceptedException


def collect_images():
    QUERY = <検索したいワード>
    LIMIT_DL_NUM = <取得したい画像数>

    project_dir = Path(__file__).resolve().parent.parent
    DRIVER_PATH = project_dr
    RETRY_NUM = 3
    TIMEOUT = 3

    # フルスクリーンにする
    options = webdriver.ChromeOptions()
    options.add_argument("--start-fullscreen")
    options.add_argument('--headless')
    options.add_argument('--disable-gpu')  # headlessモードで暫定的に必要なフラグ(そのうち不要になる)
    options.add_argument('--disable-extensions')  # すべての拡張機能を無効にする。ユーザースクリプトも無効にする
    options.add_argument('--proxy-server="direct://"')  # Proxy経由ではなく直接接続する
    options.add_argument('--proxy-bypass-list=*')  # すべてのホスト名
    options.add_argument('--start-maximized')  # 起動時にウィンドウを最大化する

    # 指定したURLに移動
    url = f'https://www.google.com/search?q={QUERY}&tbm=isch'
    driver = webdriver.Chrome(DRIVER_PATH, options=options)

    # タイムアウト設定
    driver.implicitly_wait(TIMEOUT)

    # グーグルの画像検索を行う。
    driver.get(url)

    # 表示されるサムネイル画像をすべて取得する
    thumbnail_elements = driver.find_elements(By.CLASS_NAME, 'Q4LuWd')

    # 取得したサムネイル画像数を数える
    count = len(thumbnail_elements)
    print(count)

    # 取得したい枚数以上になるまでスクロールする
    while count < LIMIT_DL_NUM:
        driver.execute_script('window.scrollTo(0, document.body.scrollHeight);')
        time.sleep(2)

        # サムネイル画像の取得
        thumbnail_elements = driver.find_elements(By.CLASS_NAME, 'Q4LuWd')
        count = len(thumbnail_elements)
        print(count)

    # HTTPヘッダーの作成
    HTTP_HEADERS = {'User-Agent': driver.execute_script('return navigator.userAgent;')}
    print(HTTP_HEADERS)

    # 画像をダウンロードするためのURLを格納
    image_urls = []

    # サムネイルをクリックしたときに表示される画像から、画像のURLを取得
    # エラーの処理は苦し紛れ
    # うまく取得できるまで、最大3回はトライする
    for index, thumbnail_element in enumerate(thumbnail_elements):
        is_clicked = False
        
        for i in range(RETRY_NUM):
            # サムネイル画像をクリックする。場合によっては失敗するのでtry-exceptでエラー処理
            try:
                if is_clicked == False:
                    thumbnail_element.click()
                    time.sleep(2)
                    is_clicked = True
            except NoSuchElementException:
                print(f'****NoSuchElementException*****')
                continue
            except Exception:
                print('予期せぬエラーです')
                break

            try:
                # サムネイル画像をクリックしたときに表示される大きい画像のimgタグを取得し、画像のURLをsrcから取得
                image_element = driver.find_element(By.CLASS_NAME, 'KAlRDb')
                image_url = image_element.get_attribute('src')

                # サムネイル画像をクリックした直後は、画像のURLがサムネイルURLのまま。そのURLだった場合、再度画像URLの取得を行う。
                if re.match(r'data:image', image_url):
                    print(f'URLが変わるまで待ちましょう。{i+1}回目')
                    time.sleep(2)
                    if i+1 == RETRY_NUM:
                        print(f'urlは変わりませんでしたね。。。')
                    continue
                else:
                    print(f'image_url: {image_url}')
                    extension = get_extension(image_url)

                    if extension:
                        image_urls.append(image_url)
                        print(f'urlの保存に成功')

                    # jpg jpeg png 以外はダウンロードしない
                    else:
                        print('対象の拡張子ではありません')
                    break
            except NoSuchElementException:
                print(f'****NoSuchElementException*****')
                break

            except ElementClickInterceptedException:
                print(f'***** click エラー: {i+1}回目')
                driver.execute_script('arguments[0].scrollIntoView(true);', thumbnail_element)
                time.sleep(1)
            else:
                break

        if index+1 % 20 == 0:
            print(f'{index+1}件完了')
        time.sleep(1)

    # 出力フォルダの作成
    project_dir = Path(__file__).resolve().parent.parent
    save_dir = os.path.join(project_dir, 'data', QUERY)
    os.makedirs(save_dir, exist_ok=True)

    for image_url in image_urls:
        down_load_image(image_url, save_dir, 3, HTTP_HEADERS)

    driver.quit()

def get_extension(url):
    url_lower = url.lower()
    extension = re.search(r'\.jpg|\.jpeg|\.png', url_lower)
    if extension:
        return extension.group()
    else:
        return None

def randomname(n):
   randlst = [random.choice(string.ascii_letters + string.digits) for i in range(n)]
   return ''.join(randlst)

def down_load_image(url, save_dir, loop, http_header):
    result = False
    for i in range(loop):
        try:
            r = requests.get(url, headers=http_header, stream=True, timeout=10)
            r.raise_for_status()

            extension = get_extension(url)
            file_name = randomname(12)
            file_path = save_dir + '/' + file_name + extension

            with open(file_path, 'wb') as f:
                f.write(r.content)

            print(f'{url}の保存に成功')

        except requests.exceptions.SSLError:
            print('*****SSLエラー*****')
            break

        except requests.exceptions.RequestException as e:
            print(f'***** requests エラー ({e}): {i+1} 回目')
            time.sleep(1)
        else:
            result = True
            break
    return result

if __name__ == '__main__':
    collect_images()
```

## スクレイピングの環境構築
スクレイピングを行うには、コマンドプロンプトからGoogleChromeを開けないといけません。
そのためには、ChromeWebDriverをダウンロードする必要があります。
まずはご自身のChromeを開いて、「設定」→「ヘルプ」→「Google Chromeについて」からバージョンを確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108046/be0a003d-5906-6738-7e41-f44d791c2204.png)
バージョンは「103.0.5060.66」ですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108046/1b509fd6-9956-83b9-9322-47d0ff41f5b0.png)
そして、[こちらのページ](https://chromedriver.chromium.org/downloads)より、ChromeのWebDriverをダウンロードします。
バージョンと完全に合うものはないこともあるみたいです。
僕は「103.0.5060.53」をダウンロードしました。
ダウンロードしたWebDriverはプロジェクトディレクトリ直下に配置します（直下でなくても大丈夫ですが、その場合、`DRIVER_PATH`はご自身で変更してください。）

基本的な準備はこちらで終了です。
以下は、コードのそれぞれで何をやっているかを解説していきます。
## Seleniumを立ち上げる
```python
    # フルスクリーンにする
    options = webdriver.ChromeOptions()
    options.add_argument("--start-fullscreen")
    options.add_argument('--headless')
    options.add_argument('--disable-gpu')  # headlessモードで暫定的に必要なフラグ(そのうち不要になる)
    options.add_argument('--disable-extensions')  # すべての拡張機能を無効にする。ユーザースクリプトも無効にする
    options.add_argument('--proxy-server="direct://"')  # Proxy経由ではなく直接接続する
    options.add_argument('--proxy-bypass-list=*')  # すべてのホスト名
    options.add_argument('--start-maximized')  # 起動時にウィンドウを最大化する

    # 指定したURLに移動
    url = f'https://www.google.com/search?q={QUERY}&tbm=isch'
    driver = webdriver.Chrome(DRIVER_PATH, options=options)

    # タイムアウト設定
    driver.implicitly_wait(TIMEOUT)

    # グーグルの画像検索を行う。
    driver.get(url)
```
`add_argument`でいろんな設定を追加していきます。今記述しているものすべてを書く必要はなさそうですが、よく分かっていないので僕も確認していません。

`url`はGoogleの画像検索のURLです。ここに検索ワードである`QUERY`を渡すことで、画像検索ができます。
`driver.implicitly_wait(TIMEOUT)`はタイムアウト設定です。指定したドライバの要素が見つかるまでの待ち時間を設定します。
`TIMEOUT=3`で設定をしていますが、適宜値は変更して問題ないと思います。
## サムネイル画像を取得したい枚数以上表示する
Googleで画像検索をかけると、画像一覧が表示されます（とりあえず推しのれなひょん。深い意味はない）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108046/2bd2cbd6-db30-3c99-5594-63b588e72c43.png)
下にスクロールしていくとページが読み込まれて、画像の枚数が増えますよね。
以下のコードでSeleniumから同じことをしています。
```python
    # 表示されるサムネイル画像をすべて取得する
    thumbnail_elements = driver.find_elements(By.CLASS_NAME, 'Q4LuWd')

    # 取得したサムネイル画像数を数える
    count = len(thumbnail_elements)
    print(count)

    # 取得したい枚数以上になるまでスクロールする
    while count < LIMIT_DL_NUM:
        driver.execute_script('window.scrollTo(0, document.body.scrollHeight);')
        time.sleep(2)

        # サムネイル画像の取得
        thumbnail_elements = driver.find_elements(By.CLASS_NAME, 'Q4LuWd')
        count = len(thumbnail_elements)
        print(count)
```
サムネイル画像を数える
→取得したい枚数以下だったら、画面をスクロールして表示数を増やす
→サムネイル画像を数える
→取得したい枚数以下だったら、画面をスクロールして表示数を増やす
ということを繰り返して、目標の枚数以上表示されるまでスクロールします。
サムネイル画像は、サムネイルの画像のクラス名`rg_i Q4LuWd`のうち、後者を指定しています。
前者でもうまくいくかもしれませんが、試していないので分かりません。このクラス名は時々で変わるらしいので、Chromeの検証ツールから確認してください。
## サムネイル画像をクリックして表示される画像のURLを取得する
サムネイル画像をクリックすると写真のようにその画像のアップが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108046/f215a78c-c729-8bcb-5829-143ed3cef211.png)
元の画像のURLはこの画像のimgタグの中のsrcに記述されています。

このURLをシンプルに取得できればいいのですが、うまくいかないことがよくあります。具体的にはこのような事象に対処しています。
- サムネイル画像をクリックしたが、読み込みが間に合わず、画像のimgタグを取得できない
- サムネイル画像をクリックしたときに表示される画像はクリック直後は「data:image」で始まるサムネイル画像のURLになっている。しばらく待つと、実際の画像のURLに変わる
- 表示される画像のimgタグのクラス名は`KAlRDb`が含まれているがたまにないものもある（先述したが、このクラス名は可変らしい）
これらに対応するために、ごちゃごちゃとエラー対応をしています。最大3回トライしてみて、ダメだったら諦めて次の画像に行く、という処理をしています。
また、拡張子が`jpg`, `jpeg`, `png`以外のものは取得しない、という処理もいれています。

正直全くきれいにかけていないので、もっとうまく書く方法はたくさんあると思います。
```python
    # サムネイルをクリックしたときに表示される画像から、画像のURLを取得
    # エラーの処理は苦し紛れ
    # うまく取得できるまで、最大3回はトライする
    for index, thumbnail_element in enumerate(thumbnail_elements):
        is_clicked = False

        for i in range(RETRY_NUM):
            # サムネイル画像をクリックする。場合によっては失敗するのでtry-exceptでエラー処理
            try:
                if is_clicked == False:
                    thumbnail_element.click()
                    time.sleep(2)
                    is_clicked = True
            except NoSuchElementException:
                print(f'****NoSuchElementException*****')
                continue
            except Exception:
                print('予期せぬエラーです')
                break

            try:
                # サムネイル画像をクリックしたときに表示される大きい画像のimgタグを取得し、画像のURLをsrcから取得
                image_element = driver.find_element(By.CLASS_NAME, 'KAlRDb')
                image_url = image_element.get_attribute('src')

                # サムネイル画像をクリックした直後は、画像のURLがサムネイルURLのまま。そのURLだった場合、再度画像URLの取得を行う。
                if re.match(r'data:image', image_url):
                    print(f'URLが変わるまで待ちましょう。{i+1}回目')
                    time.sleep(2)
                    if i+1 == RETRY_NUM:
                        print(f'urlは変わりませんでしたね。。。')
                    continue
                else:
                    print(f'image_url: {image_url}')
                    extension = get_extension(image_url)

                    if extension:
                        image_urls.append(image_url)
                        print(f'urlの保存に成功')

                    # jpg jpeg png 以外はダウンロードしない
                    else:
                        print('対象の拡張子ではありません')
                    break
            except NoSuchElementException:
                print(f'****NoSuchElementException*****')
                break

            except ElementClickInterceptedException:
                print(f'***** click エラー: {i+1}回目')
                driver.execute_script('arguments[0].scrollIntoView(true);', thumbnail_element)
                time.sleep(1)
            else:
                break
```
## 画像のURLからダウンロードしてフォルダに保存
スクレイピングして取得した画像URLから、最後実際にダウンロードしていきます。
```python
    # 出力フォルダの作成
    project_dir = Path(__file__).resolve().parent.parent
    save_dir = os.path.join(project_dir, 'data', QUERY)
    os.makedirs(save_dir, exist_ok=True)

    for image_url in image_urls:
        down_load_image(image_url, save_dir, 3, HTTP_HEADERS)

def randomname(n):
   randlst = [random.choice(string.ascii_letters + string.digits) for i in range(n)]
   return ''.join(randlst)

def down_load_image(url, save_dir, loop, http_header):
    result = False
    for i in range(loop):
        try:
            r = requests.get(url, headers=http_header, stream=True, timeout=10)
            r.raise_for_status()

            extension = get_extension(url)
            file_name = randomname(12)
            file_path = save_dir + '/' + file_name + extension

            with open(file_path, 'wb') as f:
                f.write(r.content)

            print(f'{url}の保存に成功')

        except requests.exceptions.SSLError:
            print('*****SSLエラー*****')
            break

        except requests.exceptions.RequestException as e:
            print(f'***** requests エラー ({e}): {i+1} 回目')
            time.sleep(1)
        else:
            result = True
            break
    return result
```
保存する画像の名前はランダムに付けました。

このpythonファイルを実行すると、`data`内に検索したクエリ名でディレクトリが作られ、その中に画像がダウンロードされます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108046/6ca448a0-0789-daa1-284d-233170dfba42.png)

これにてスクレイピング終了です！
## 参考
[PythonスクレイピングでGoogle画像検索ページから画像を取得](https://senmyou.xyz/python%E3%82%B9%E3%82%AF%E3%83%AC%E3%82%A4%E3%83%94%E3%83%B3%E3%82%B0%E3%81%A7google%E7%94%BB%E5%83%8F%E6%A4%9C%E7%B4%A2%E3%83%9A%E3%83%BC%E3%82%B8%E3%81%8B%E3%82%89%E7%94%BB%E5%83%8F%E3%82%92/)