Selenium（4.1.3）のまとめ書き

# はじめに
どうも。最近Selenium 4.1.3（Python 3.9.7）をよく使っていたので、そのときに気づいたことなどをまとめました。

# 注意事項
スクレイピングしている方ならご存知だと思いますが、念のため置いておきます。
[岡崎市立中央図書館事件](https://ja.wikipedia.org/wiki/%E5%B2%A1%E5%B4%8E%E5%B8%82%E7%AB%8B%E4%B8%AD%E5%A4%AE%E5%9B%B3%E6%9B%B8%E9%A4%A8%E4%BA%8B%E4%BB%B6)

# ドライバーはwebdriver_managerを使用する
バージョンが変わるごとにドライバーを入れ替えるのは大変ですよね。
そこで役立つのが、この**webdriver_manager**です。
```
pip install webdriver_manager
```

Chromeのドライバーを入れたい場合、このように記述します。

``` python
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager

driver = webdriver.Chrome(ChromeDriverManager().install(), options=options)
```
非常に便利です。


# class化したほうが使い勝手がよい
classにしたほうが圧倒的に使いやすい。オブジェクト指向に基づいて考えると当たり前ですが。
以下が私がよく使用しているclassです。

``` Python
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.by import By
from selenium.common.exceptions import *

import time
import os
from datetime import datetime
from logging import getLogger, Formatter, FileHandler, StreamHandler, INFO, DEBUG

class Driver_Format():
    """
    Driver Format
    """
    def __init__(self, headless:bool=False, class_name:str=__name__) -> None:

        if not os.path.exists('log'):
            os.makedirs('log')

        self.logger = getLogger(class_name)
        self.logger.setLevel(INFO)

        self.file_name = './log/log_' + datetime.now().strftime('%Y%m%d_%H%M%S')

        self.fh = FileHandler(self.file_name+'.log')
        self.fh.setLevel(INFO)
        fh_formatter = Formatter('%(asctime)s - %(levelname)s - %(name)s - %(funcName)s - %(message)s')
        self.fh.setFormatter(fh_formatter)

        self.ch = StreamHandler()
        self.ch.setLevel(INFO)
        ch_formatter = Formatter('%(asctime)s - %(levelname)s - %(name)s - %(funcName)s - %(message)s')
        self.ch.setFormatter(ch_formatter)

        self.logger.addHandler(self.fh)
        self.logger.addHandler(self.ch)

        self.logger.info('Initialization Logger')
        self.Initialization_Driver(headless)        

    def Initialization_Driver(self, headless:bool=False):

        options = Options()
        options.add_argument("--window-size=1920,1080")
        if headless:
            options.add_argument('--headless')
        self.driver = webdriver.Chrome(ChromeDriverManager().install(), options=options, service_log_path=self.file_name+'_driver.log')        
        self.driver.implicitly_wait(10)
  
    
    def close_driver(self, removeHandler_tf = True):
        if removeHandler_tf:
            self.logger.removeHandler(self.fh)
            self.logger.removeHandler(self.ch)
        self.driver.quit()

    def wait_and_get_element(self, tag, path:str):
        """
        Wait and return element from any tags
        """
        for _ in range(10):
            try:
                WebDriverWait(self.driver, 10).until(
                    EC.visibility_of_element_located((tag, path))
                )
                return self.driver.find_element(tag, path)
            except:
                self.driver.refresh()
        else:
            st = 'Time OUT : ' + path
            raise Exception(st)
    
    def wait_and_click(self, tag, path:str):
        
        for _ in range(10):
            try:
                WebDriverWait(self.driver, 10).until(
                    EC.element_to_be_clickable((tag, path))
                )
                element = self.wait_and_get_element(tag, path)
                ActionChains(self.driver).move_to_element(element).perform()
                element.click()
                break
            except ElementClickInterceptedException:
                time.sleep(1)
            except StaleElementReferenceException:
                time.sleep(1)
            except TimeoutException:
                time.sleep(1)
        else:
            st = 'Time OUT : ' + path
            raise Exception(st)
```
スクレイピングをしていると、やはりログが必要であることが大半（個人的感想）なのでこのようにログを混ぜて定義すると、非常に使いやすいです。

# 要素の選択にはWebDriverWaitを使うべき
スクレイピングが速すぎると要素を取得できず、よくエラーが発生します。time.sleep()を使用してやっていたとしても、ページのロードに時間が掛かると結局はエラーが発生します。そこで、このWebDriverWaitを使用することでわざわざtime.sleep()を使用しなくても待ってくれます。
以下のように使用します。

``` Python
# 要素が見つかるまで待つ
WebDriverWait(self.driver, 待機時間).until(EC.visibility_of_element_located((tag, path)))
```

``` Python
# クリックできるようになるまで待つ
WebDriverWait(self.driver, 待機時間).until(EC.element_to_be_clickable((tag, path)))
```

``` Python
# フレームが使用可能になるまで待機し、移動する
WebDriverWait(self.driver, 待機時間).until(EC.frame_to_be_available_and_switch_to_it((tag, path)))
```

他にもありますが、私が最近使用したのはこの３つです。これで大体のことは可能かと思われます。
フレーム移動に関する注意事項ですが、私がスクレイピングしていたページの問題かもしれませんが、`EC.frame_to_be_available_and_switch_to_it`を使用した後、すぐにframe内の要素を取得しようとする（WebDriverWaitを使用）と`selenium.common.exceptions.NoSuchWindowException`というエラーが発生しました。結局原因は分からず、`time.sleep(1)`を使用することでなんとか解消しました。

# frame内にいるときにrefreshを行うともとに戻る
ただの豆知識程度ですが、iframe内などにいるときに`driver.refresh()`を行うと一番外側のframeに戻されます。

# Google Chromeのユーザーデータ（履歴、ブックマーク、パスワード等）を使用する方法
optionsでユーザーデータのフォルダやProfileを指定することで、ログイン情報などを引き継いで使用することが出来ます。
次のように設定します。

```python
from selenium.webdriver.chrome.options import Options

options = Options()
options.add_argument('--user-data-dir=E:\python\Google_Chrome\locals') # Profileが格納されているフォルダの指定
options.add_argument('--profile-directory=Profile1') # Profileの指定
```

注意事項として、`--user-data-dir=`はフルパスで指定する必要があります。（相対パスではエラーが発生する）
Windowsでしか確認が取れていませんが、`--user-data-dir=`や`--profile-directory=`で指定したフォルダが存在しない場合、自動でフォルダが作成されます。
自分が使用しているユーザーデータを使用することも可能ですが、同時に使用するとエラーが発生するためそこは注意しましょう。

# 最後に
最後まで読んでいただきありがとうございます。
何かご指摘や質問などありましたら、コメントをよろしくお願いいたします。
それでは失礼いたします。