Python Selenium 動的なダウンロード完了待機

#はじめに
Seleniumを利用してファイルをダウンロードする際に、完了するまでの待機を考えなければなりません。
よくあるエラーや課題として以下のようなものが有るかとおもいます。
・**ダウンロードが完了しておらず、ファイルを扱おうとしたらエラーになった**
・**通常は数秒でダウンロード出来るけれど、余裕をもって15秒待機したり、余計な待機時間がある**

そこで
・**ダウンロード完了したらすぐに後続処理が開始される**
・**待機のタイムアウト時間を1秒単位で設定できる**
・**タイムアウト時のエラーハンドリングも出来る**
そんな待機処理を作ってみました。

環境：Windows10, Python3.8.3, Selenium3.141.0
ブラウザ：GoogleChrome ChromeDriver83.0.4103.39

#Chromeのダウンロード仕様
今回の機能を実装するにあたり、GoogleChromeの仕様(2020/07/14時点)を利用していますので
Chromeのダウンロード開始してから完了するまでの仕様を記載します。

※クリックすると「test.csv」というファイルがダウンロードされるリンクがあるとします。
**１、リンクをクリック ダウンロード開始**
**２、ダウンロードフォルダに「test.csv.crdownload」ファイルが生成**
**３、ダウンロードが完了すると「.crdownload」が取り除かれ「test.csv」が扱えるようになる**

このような仕様になっています。

なので、**ダウンロードしたファイルの拡張子が
「.crdownload」：ダウンロード中
「.crdownload」以外 ：ダウンロード完了**
と判断することが出来るので、拡張子「.crdownload」の有無を監視して待機するようにしていきます。

#一時ダウンロードフォルダの作成
SeleniumでChromeを起動した際、ダウンロード先はWindowsだと「C:\Users\username\Downloads」フォルダになっています。
このままデフォルトのフォルダを利用すると過去にダウンロードされたファイルが入っているなど
拡張子の監視をしづらいため、今回ダウンロードするファイルのみを格納する一時ダウンロードフォルダを作成します。

本Pythonファイルが入っているプロジェクトフォルダを作業フォルダとして、そこに一時フォルダを作成します。

```python:
import os

# カレントディレクトリの取得
current_dir = os.getcwd()

# 一時ダウンロードフォルダパスの設定
tmp_download_dir = f'{current_dir}\\tmpDownload'

# 一時フォルダの作成
os.mkdir(tmp_download_dir)
```

##※パスの区切り文字の注意
WindowsのChromeでダウンロードフォルダを指定する際、区切り文字は「/」スラッシュではなく、「\」バックスラッシュで指定します。
本記事では、パスの区切り文字をバックスラッシュ2つ「`\\`」でエスケープして記載します。
[【初心者向け】Pythonでパス設定するときに「\」が含まれていると予期せぬ動作になる](https://qiita.com/hajimejimejime/items/93a9c4bb3bf009e89924)

#Chromeのダウンロードフォルダ指定
ダウンロード先を、前項で作成した一時ダウンロードフォルダに変更します。
変更するにあたって、Chromeオプションを利用します。

```python:
from selenium import webdriver

# Chromeオプション設定でダウンロード先の変更
options = webdriver.ChromeOptions()
prefs = {'download.default_directory' : tmp_download_dir }
options.add_experimental_option('prefs',prefs)

# ドライバのパス設定
driver_path = 'webdriver\\chromedriver.exe'

# オプションを適用してChromeを起動
driver = webdriver.Chrome(executable_path = driver_path, chrome_options = options)
```
これで、Chrome起動時にダウンロードフォルダの設定が行われ、一時ダウンロードフォルダに保存されるようになります。

#ダウンロード完了まで待機
Seleniumでダウンロードを開始した後に、待機タイムアウトの時間を設定し、完了まで待機を行います。

```python:
from selenium import webdriver

import os
import sys
import glob
import time

# Seleniumでダウンロード開始処理(ダウンロードリンクのクリックなど)

# 待機タイムアウト時間(秒)設定
timeout_second = 10

# 指定時間分待機
for i in range(timeout_second + 1):
    # ファイル一覧取得
    download_fileName = glob.glob(f'{tmp_download_dir}\\*.*')
    
    # ファイルが存在する場合
    if download_fileName:
        # 拡張子の抽出
        extension = os.path.splitext(download_fileName[0])

        # 拡張子が '.crdownload' ではない ダウンロード完了 待機を抜ける
        if ".crdownload" not in extension[1]:
            time.sleep(2)
            break

    # 指定時間待っても .crdownload 以外のファイルが確認できない場合 エラー
    if i >= timeout_second:
        # == エラー処理をここに記載 ==
        # 終了処理
        driver.quit()
        # 一時フォルダの削除
        shutil.rmtree(tmp_download_dir)
        sys.exit()

    # 一秒待つ
    time.sleep(1)

# 以下 ダウンロード完了後の処理 正ダウンロードフォルダへの格納など
```


##解説
大まかに言うと、指定秒数分ループを行い
**「ダウンロード完了しているかを確認、完了していれば待機ループを抜け、完了していなければ1秒待機」**
を行っています。

ダウンロード完了後すぐに待機ループを抜けるため
例えばタイムアウト秒数を「10秒」に設定、実際には「3秒」後にダウンロードが完了
という場合、ダウンロード開始から「3-4秒」後にはダウンロード完了後の後続処理に進むことができます。

指定秒数経っても完了が確認できなければ、ループを抜けずにエラー処理に入ります。

###拡張子監視
拡張子を監視するにあたって、まず glob.glob で一時フォルダ内のファイル一覧を取得しています。

```python
# ファイル一覧取得
download_fileName = glob.glob(f'{tmp_download_dir}\\*.*')
```

ダウンロード状況に応じて以下のようなリストが返ってきます。

■**ダウンロードボタンをクリックしてもまだダウンロードが開始できていない場合など**
フォルダにファイルが存在しない：**\[](空のリスト)**

■**ダウンロード中**
test.csv.crdownload生成：**[test.csv.crdownload]**

■**ダウンロード完了**
「.crdownload」除去：**[test.csv]**

このリストを判定して、
**リストが空ではなく(ファイルが存在していて)、拡張子が「.crdownload」ではないとき**
をダウンロード完了として待機のループを抜けるようにしています。

拡張子だけを抽出する処理として os.path.splitext を利用しています。

```python
# 拡張子の抽出
extension = os.path.splitext(download_fileName[0])
```

ファイル名が「`test.csv.crdownload`」のとき
extension[0]に　**拡張子前のファイル名 [test.csv]**
extension[1]に　**拡張子[.crdownload]**

ファイル名が「`test.csv`」のとき
extension[0]に　**拡張子前のファイル名 [test]**
extension[1]に　**拡張子[.csv]**
が入ります。

拡張子が入ったextension[1]が `'.crdownload'` ではないかを判定しています。
※判定が早すぎると、.crdownload からの切り替わり途中でDriverを閉じてしまい、ファイルが消える場合があるので待機をいれています。

```python
# 拡張子が .crdownload ではない ダウンロード完了 処理を抜ける
if ".crdownload" not in extension[1]:
    time.sleep(2)
    break
```
#完成
上記の処理をまとめ、調整、一時ダウンロードフォルダから正ダウンロードフォルダに移動されるようにして完成です。
使うときは待機時間を変えるだけ！

```python:
from selenium import webdriver

import os
import sys
import glob
import shutil
import time

# カレントディレクトリの取得
current_dir = os.getcwd()

# 一時ダウンロードフォルダパスの設定
tmp_download_dir = f'{current_dir}\\tmpDownload'

# 一時フォルダが存在していたら消す(前回のが残存しているかも)
if os.path.isdir(tmp_download_dir):
    shutil.rmtree(tmp_download_dir)

# 一時ダウンロードフォルダの作成
os.mkdir(tmp_download_dir)

# Chromeオプション設定でダウンロード先を変更
options = webdriver.ChromeOptions()
prefs = {'download.default_directory' : tmp_download_dir }
options.add_experimental_option('prefs',prefs)

# ドライバのパス設定
driver_path = 'webdriver\\chromedriver.exe'

# オプションを適用してChromeを起動
driver = webdriver.Chrome(executable_path = driver_path, chrome_options = options)

# === 画面遷移 ===
# driver.get('https://xxxxxxx.co.jp/')

#ダウンロード開始するリンクをクリック
# driver.find_element_by_xpath('//*[@id="download"]').click()

# 待機タイムアウト時間(秒)設定
timeout_second = 10

# 指定時間分待機
for i in range(timeout_second + 1):
    # ファイル一覧取得
    download_fileName = glob.glob(f'{tmp_download_dir}\\*.*')

    # ファイルが存在する場合
    if download_fileName:
        # 拡張子の抽出
        extension = os.path.splitext(download_fileName[0])

        # 拡張子が '.crdownload' ではない ダウンロード完了 待機を抜ける
        if ".crdownload" not in extension[1]:
            time.sleep(2)
            break

    # 指定時間待っても .crdownload 以外のファイルが確認できない場合 エラー
    if i >= timeout_second:
        # == エラー処理をここに記載 ==
        # 終了処理
        driver.quit()
        # 一時フォルダの削除
        shutil.rmtree(tmp_download_dir)
        sys.exit()

    # 一秒待つ
    time.sleep(1)

# === ダウンロード完了後処理 ===
# Chromeを閉じる
driver.quit()

# 正ダウンロードフォルダへ格納
shutil.move(download_fileName[0], f'{current_dir}\\Download')

# 一時フォルダの削除
shutil.rmtree(tmp_download_dir)
```

ただ、このままだと同名のファイルを正ダウンロードフォルダに移動させるとエラーになるので、状況に応じて作り変える必要はあります。
※ `download_fileName[0]`　がダウンロードしたファイルのフルパスなので好きにリネームしてください。


#おわりに
ご覧いただきありがとうございました。

もっといい方法有るよ。など
ご指摘あればコメントいただけますと幸いです。

#参考
[Python + Selenium で Chrome の自動操作を一通り](https://qiita.com/memakura/items/20a02161fa7e18d8a693)
[SeleniumのChrome driverのデフォルトダウンロードフォルダを設定する](https://qiita.com/hikoalpha/items/fa8330391823aea2fbca)
[Pythonでフォルダ内のファイルリストを取得する](https://qiita.com/amowwee/items/e63b3610ea750f7dba1b)
[Pythonでパス文字列からファイル名・フォルダ名・拡張子を取得、結合](https://note.nkmk.me/python-os-basename-dirname-split-splitext/)