seleniumで読み込んだ画像をそのまま保存したい！！


# はじめに
ChromeのDevToolsなどで見るように、ブラウザ上で読み込んだファイルを読み込む方法を紹介します。
seleniumから直接読み込むことで、
- リクエスト数を減らせる
- 適切な方法でリクエストできる (ヘッダ、Cookieなど) 

といったメリットがあります。
特にリクエスト方法が複雑なサイトの場合、その条件をjsファイルなどから解析せずに自動化できるため便利です。

**※スクレイピングが禁止されているサイトでの利用は控えてください**

# 方法
以下の2つの方法があります。

1. `selenium-wire` を使う
    - メリット
        - 簡単に使える
    - デメリット
        - 実行中にエラーが出ることがある
        - 読み込んだデータが取得できないことがある
        - 「保護されていない通信」になる
1. 自力で実装する
    - メリット
        - 自分の使い方に合ったように作成できる
    - デメリット
        - エラーなどには自分で対応する必要がある

(私は最初 `selenium-wire` を知らなかったため、自力で実装することになりました...)

# 画像の保存
それでは、qiitaのページにある公式イベントの画像を取得してみましょう。
ファイル名: `original.jpg`

![qiita.com_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409132/6b7f8397-65fd-9611-b63b-02e82db9dfe6.png)

## 1. `selenium-wire` を使う

seleniumwireのdriverにはrequestsというインスタンス変数があり、
そこにはブラウザで行った通信が保持されます。

```python:selenium-wireを使った場合のコード
from seleniumwire import webdriver
from webdriver_manager.chrome import ChromeDriverManager

# driverの作成
driver = webdriver.Chrome(ChromeDriverManager().install())
driver.get('https://qiita.com/')

# URLに「original.jpg」が含まるリクエストを取得する
results = [item for item in driver.requests if 'original.jpg' in item.url]
if len(results) == 0:
    raise Exception('original.jpgが見つかりませんでした')

# リクエスト情報にある取得データを保存する
with open('original.jpg', 'wb') as f:
    f.write(results[0].response.body)
```

無事、画像が保存されていました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409132/26b9798d-729e-c017-e126-4262b81159da.png)

## 2. 自力で実装する
実装部分は長くなってしまうため、`selenium_load`というモジュールに書き、それを読み込んでいます。
1つ注意点として、使うseleniumのdriverには `caps["goog:loggingPrefs"] = {"performance": "ALL"}` という設定が必要です。

```python:
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from webdriver_manager.chrome import ChromeDriverManager

import selenium_load

# driverの作成
caps = DesiredCapabilities.CHROME
caps["goog:loggingPrefs"] = {"performance": "ALL"}
service = Service(ChromeDriverManager().install())
driver = webdriver.Chrome(service=service, desired_capabilities=caps)
driver.get('https://qiita.com/')

# original.jpgの情報を取得
network_dict = selenium_load.get_network_dict(driver)
results = [network_info for url, network_info in network_dict.items() if 'original.jpg' in url]
if len(results) == 0:
    raise Exception('original.jpgが見つかりませんでした')

# 画像として保存
with open('original2.jpg', 'wb') as f:
    f.write(results[0].body)
```

こちらも問題なく保存されていました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409132/8de3649f-f372-dd66-0d2a-8d7ddd95eb05.png)


### 自作モジュール
`driver.get_log('performance')` をすることでdriverのログを取得することができます。
そのログを種類ごとに分類し、`Network.getResponseBody` というコマンドからbodyを取得しています。
(詳しい説明は後日追加予定です。)


```python:selenium_load.py
import json

import cv2
import numpy as np
import selenium.common.exceptions


class NetworkInfo(object):
    def __init__(self, request_id):
        self.request_id = request_id

        self.url = None
        self.status_code = None
        self.content_type = None
        self.body = None
        self.request_headers = {}
        self.response_headers = {}
        self.is_full_request_headers = False
        self.is_full_response_headers = False

    def set_body(self, body):
        self.body = body

    def set_request_headers(self, headers, is_full):
        if not self.is_full_request_headers:
            self.request_headers = headers
            self.is_full_request_headers = is_full

    def set_response_headers(self, headers, is_full):
        if not self.is_full_response_headers:
            self.response_headers = headers
            self.content_type = self.get_content_type(headers)
            self.is_full_response_headers = is_full

    @staticmethod
    def get_content_type(response_headers):
        content_type_list = [value for key, value in response_headers.items() if key.lower() == 'content-type']
        if len(content_type_list) > 0:
            return content_type_list[0]
        else:
            return None


def get_network_dict(driver):
    events = _get_network_events(driver)
    network_dict = _get_network_dict(events, driver)
    return network_dict


def _get_network_events(driver):
    net_log = driver.get_log('performance')
    events = [json.loads(entry['message'])['message'] for entry in net_log]
    return events


def _get_network_dict(events, driver=None):
    results = {}
    for event in events:
        method = event['method']

        if method == 'Network.requestWillBeSent':
            request_id = event['params']['requestId']
            url = event['params']['request']['url']
            part_headers = _fix_headers_tuple(event['params']['request']['headers'])

            if request_id not in results.keys():
                results[request_id] = NetworkInfo(request_id)

            results[request_id].url = url
            results[request_id].set_request_headers(part_headers, False)
        elif method == 'Network.requestWillBeSentExtraInfo':
            request_id = event['params']['requestId']
            headers = _fix_headers_tuple(event['params']['headers'])

            if request_id not in results.keys():
                results[request_id] = NetworkInfo(request_id)

            results[request_id].set_request_headers(headers, True)
        elif method == 'Network.responseReceived':
            request_id = event['params']['requestId']
            url = event['params']['response']['url']
            part_headers = _fix_headers_tuple(event['params']['response']['headers'])

            if request_id not in results.keys():
                results[request_id] = NetworkInfo(request_id)

            results[request_id].url = url
            results[request_id].set_response_headers(part_headers, False)

            if driver is not None:
                try:
                    body_info = driver.execute_cdp_cmd(
                        'Network.getResponseBody', {'requestId': event['params']['requestId']})
                    results[request_id].set_body(body_info['body'])
                except selenium.common.exceptions.WebDriverException:
                    print(f"Failed to fetch: {url}")
                    pass

        elif method == 'Network.responseReceivedExtraInfo':
            request_id = event['params']['requestId']
            headers = _fix_headers_tuple(event['params']['headers'])
            status_code = event['params']['statusCode']

            if request_id not in results.keys():
                results[request_id] = NetworkInfo(request_id)

            results[request_id].status_code = status_code
            results[request_id].set_response_headers(headers, True)

    formatted_results = {result.url: result for result in results.values()}

    return formatted_results


def _fix_headers_tuple(headers):
    new_headers = {}
    for key in headers.keys():
        if type(headers[key]) is tuple:
            new_headers[key] = headers[key][0]
        else:
            new_headers[key] = headers[key]

    return new_headers
```

# さいごに
今回はseleniumで読み込んだデータを抽出する方法について説明しました。
方法は `selenium-wire`を使った方法、自作モジュールを使った方法の2種類がありますが、
それぞれメリット・デメリットがあるので使い分けてみてください。
頻繁に使う場合は自作モジュールを使ったほうが調整できるので便利ですが、
たまに使う程度であれば `selenium-wire` のほうが楽で良い印象です。

それでは