Seleniumライブラリを含めて、pyinstallerでEXE化する 

## はじめに
pyinstallerを用いてSeleniumライブラリを含めてEXE化を行い、Python環境がインストールされていないPC環境でも実行する方法を記載します。
本記事では、Chromeブラウザー、ChromeDriver含めてEXE化します。

また、pyinstallerは、Pythonおよび関連するファイル一式をまとめて実行ファイル形式に変換し、実行時に以下のフォルダに展開、実行しています。
`%USERPROFILE%/AppData/Local/Temp/_[ランダムな英数字]/`

## 動作確認環境
* Windows OS : 21H2 (64bit)
* Python Version : Python 3.10.5
* Chrome : 103.0.5060.53
* Chrome Driver Version : 103.0.5060.53

## プロジェクト構成
プロジェクト構成は以下のとおり。

```
　project
　　|
　　|-browser
　　| 　|-103.0.5060.53
　　| 　|-chrome.exe
　　|
　　|-driver
　　| 　|-chromedriver.exe
　　|
　　|-main.py
　　|-selenium-automation.spec
```

## 手順

1. プロジェクト構成のbrowserフォルダーの各種ファイルを以下のパスから取得します
    * C:\Program Files\Google\Chrome\Application\103.0.5060.53\
    * C:\Program Files\Google\Chrome\Application\chrome.exe

1. プロジェクト構成のdriverフォルダーにあるChromeDriverを以下のURLから取得します
    * ブラウザーのドライバーをインストールする
        - [https://www.selenium.dev/documentation/webdriver/getting_started/install_drivers/]

1. 以下のコマンドを実行して、`selenium-automation.spec`ファイルを生成します
    * pyinstaller main.py --onefile --noconsole --name selenium-automation

1. specファイルのAnalysisのdatasとhiddenimportsを編集します
    * datas=[("./browser", "./browser"), ("./driver", "./driver")],
    * hiddenimports=["selenium"],

1. EXE化コマンドで実行します
    * pyinstaller --clean selenium-automation.spec

## ソースコード
`main.py`ファイルの内容を以下に記載しました。

proxyがある場合は、proxyを通す設定が必要になります。
proxyを通して、実行する場合は以下のコメントを外してください。

```
    # PROXY_AUTH = '{userid}:{password}'
    # set_proxy_setting(PROXY)
    # options.add_argument(f"--proxy-server={PROXY}")
    # options.add_argument(f"--proxy-auth={PROXY_AUTH}")
```

``` python
# main.py

import os
import sys
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import time


def set_proxy_setting(proxy_server: str):
    # スクリプト内で環境変数をセットする
    # no_proxyの設定はカンマ区切りで実施する
    # Seleniumのlocalhostに対する通信はproxyを経由せずに通信させるために、no_proxy設定を行う
    os.environ["http_proxy"] = proxy_server
    os.environ["https_proxy"] = proxy_server
    os.environ['no_proxy'] = "127.0.0.1,localhost"
    os.environ['NO_PROXY'] = "127.0.0.1,localhost"


def resource_path(relative_path):
    try:
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.dirname(__file__)
    return os.path.join(base_path, relative_path)


def main():

    browser_full_path = resource_path("./browser/chrome.exe")
    chrome_driver_full_path = resource_path("./driver/chromedriver.exe")

    # WebDriver のオプションを設定する
    options = Options()
    options.binary_location = browser_full_path
    options.add_argument("start-maximized")
    options.add_argument("-disable-gpu")
    options.add_argument("--ignore-certificate-errors")

    # Selenium Chrome Proxy の設定
    # PROXY Example : "http://192.168.0.100:8080"
    # PROXY = "http://192.168.0.100:8080"
    # PROXY_AUTH = '{userid}:{password}'
    # set_proxy_setting(PROXY)
    # options.add_argument(f"--proxy-server={PROXY}")
    # options.add_argument(f"--proxy-auth={PROXY_AUTH}")

    driver = webdriver.Chrome(
        executable_path=chrome_driver_full_path, chrome_options=options)

    # スクレイピングを実行する
    URL = "https://stackoverflow.com/"
    driver.get(URL)

    # 3秒ウェイトする
    time.sleep(3)

    driver.close()
    driver.quit()


if __name__ == "__main__":
    main()
```

## specファイル
`selenium-automation.spec`の内容を以下に記載しました。

```
# -*- mode: python ; coding: utf-8 -*-


block_cipher = None


a = Analysis(
    ['main.py'],
    pathex=[],
    binaries=[],
    datas=[("./browser", "./browser"), ("./driver", "./driver")],
    hiddenimports=["selenium"],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=[],
    excludes=[],
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.zipfiles,
    a.datas,
    [],
    name='selenium-automation',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    upx_exclude=[],
    runtime_tmpdir=None,
    console=False,
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
)
```
## トラブルシューティング
Seleniumライブラリを含めて、pyinstallerでEXE化するときはエラーにはまりやすいです。
私がはまったエラーの対処方法について記載します。

### PythonライブラリのEXE化・もしくはEXEの起動に失敗する
* specファイルのAnalysisのdatasとhiddenimportsの記載を再度確認する
* 余計なPythonライブラリを含めない。最小限の構成で作成する。仮想環境で一度すべてのPythonライブラリをuninstallしてから作業してください
1. Pythonライブラリ取得]
    * pip freeze > requirement.txt

1. Pythonライブラリ全削除]
    * pip uninstall -r requirement.txt

1. 手動のpip installで復旧
    * 1つずつ復旧する

* 「build」、「dist」フォルダーを削除してから再度pyinstallerコマンドでEXE化を実施する

### EXEの起動中にSeleniumライブラリ内部でエラーとなる
SeleniumのChromeオプションでproxy設定を行うと、Seleniumのlocalhost(127.0.0.1)に対する通信もproxyを通るように通信してしまうため、no_proxyの設定を行うこと。


## 補足
* specファイルのhiddenimportsとは
specファイルにおいてhiddenimportsを指定することで、pyinstallerによるEXEコマンド実行時にライブラリを同梱できる

* sys._MEIPASSとは
https://stackoverflow.com/questions/22472124/what-is-sys-meipass-in-python

## 参考リンク
本記事の記載には以下のサイトを参考にさせていただきました。

* Seleniumまとめサイト
    - https://www.selenium.dev/ja/

* SeleniumをEXEで動かす
    - https://qiita.com/middle_aged_rookie_programmer/items/dd82febac13072ec148e/

* specファイルの取り扱いについて詳細に記載されている
    - https://jerrynsh.com/how-to-package-python-selenium-applications-with-pyinstaller/

* Pythonスクリプトでプロキシを利用する
    - https://dev.classmethod.jp/articles/python-script-proxy/

* Selenium Chrome Proxy の設定
    - https://algorithm.joho.info/programming/python/selenium-proxy/

* PyInstallerを使ってみた
    - https://qiita.com/TakamiChie/items/8dba8459343db898b335