Pythonモジュールの自動更新

#前置き
Pythonのモジュールは、納品時のままにしていると納品物が動かなくなることがあります。

今回は、chromedriver-binaryというモジュールを例に話を進めます。
chromedriver-binaryの役割はPython経由でchromeブラウザを操作できうようにすることです。
chromedriver-binaryはchromeブラウザに対応したものをインストールしておく必要があります。
chromeブラウザは、勝手にアップデートしますので何もしないと
そのうちchromedriver-binaryが対応しなくなり納品物が止まるようになります。
こういうところにモジュールの自動更新が使えます。

#仕掛け①
以前は、pipの\_\_init\_\_.pyのコードを流用する方法が使えていたのですが
pipのバージョンが新しくなってからは使えなくなりました。
新しいやり方に関しては、Pycharmのコンソールなどを眺めてみると案内が出ていると思います。

訳して見ると
「pipコマンドを直接実行してください」
みたいな話だったと思います。

簡単に仕掛けを肝をまとめますと
subprocess辺りに「pip install ***」を実行させればよいということになります。
venvを利用していてもこの方法で使えるを確認済みです。

以上のことを関数にまとめてみるとこんな感じ。

```python:installer.py
import sys
import subprocess

    def install_module(module: str, ver: str = None):
        """
        指定したモジュール（バージョン指定可）をインストールする
        :param module: モジュール名
        :param ver: モジュールのバージョン
        :return:
        """
        # バージョンの指定がある場合はオプションを付け足す
        if ver is not None:
            ver = f"=={ver}"
        else:
            ver = ""

        # インストール作業を行う
        try:
            command = f"pip install {module}{ver}"
            subprocess.call(command, shell=True)
        except Exception:
            sys.exit(-1)
```

#仕掛け②
上記のコードをプログラムの中に仕込めば完成！！
・・・といえるほど単純じゃないです。
見出しが「仕掛け①」なので②、③もまだあります。

以下のような使い方をします。

```sample_1.py
try:
  driver = webdriver.Chrome()
except:
  install_module("chromedriver-binary", "86.0.4240.22.0")
```

exceptに流れたときに指定したモジュールをインストールをしてはくれますが・・・
driver変数の中身はまだ未設定のままです。

```sample_2.py
try:
  driver = webdriver.Chrome()
except:
  install_module("chromedriver-binary", "86.0.4240.22.0")
  driver = webdriver.Chrome()
```

とすればコード上は問題なさそうですが・・・
driver = webdriver.Chrome()このコードは、
どのchromedriver-binaryに基づいて実行されるのでしょうか？
・import chromedriver-binaryを宣言したタイミングのchromedriver-binaryなのか？
・新たにインストールしたchromedriver-binaryなのか？
実装方法にもよるのでしょうが、私が検証した限りは前者で動いてるようです。

importlib.reload(モジュール名)などを使えばリロードできますし、
pyファイルそのものを再起動させるみたいな方法もあるのですが、
プログラムの本筋と関係ない部分がやたらと面倒になります。
pyファイルを再起動させるということは一度pyファイルのプロセスが終了することになりますので
プロセスの終了を待つ系の何かがあると
pyファイルのプロセスが終わってないのに次の処理を始めるなど面倒なことがいろいろと発生します。

いろいろと考えた中で一番単純に落ち着いたのは、以下のようなバッチを作ること

```run.bat
py モジュールの確認用.py
py メイン処理用.py
```

バッチファイルを活用すると
①処理の終了を待って次に進むような場合、バッチファイルの終了を待ってから次に進んでくれます
②モジュールの確認とメインの処理を別々のpyファイルにすることにより、メイン処理がスリム化できます
③モジュールの確認用.pyでインストールを実行すると
　メイン処理用.pyを実行するときにはモジュールを最新の状態にできます

#仕掛け③

仕掛け①と仕掛け②が実践できていればモジュールをインストール／アップデートするという用途には使えます。
残念ながらchromedriver-binaryの更新に利用するにはまだ不十分です。
私が2018年辺りで検証した限り、chromedriver-binaryは最新をインストールしても動きません。
どのバージョンをインストールするのかを調べて、引数に与えてあげる必要があります。

##対応するchromedriver-binaryのバージョンの調べ方
↓のURLにchromeブラウザのメジャーバージョンを与えてURLを開くとわかります
https://chromedriver.storage.googleapis.com/LATEST_RELEASE_{chromeブラウザのメジャーバージョン}
例：
https://chromedriver.storage.googleapis.com/LATEST_RELEASE_86

ではバージョンをどうやってchromeブラウザのバージョンを調べるのか？
C:\Program Files (x86)\Google\Chrome\Application
もしくは、
C:\Program Files\Google\Chrome\Application
をのぞいてみるとバージョン番号の書かれたフォルダが見つかるかと
これをうまいこと切り出せばバージョン番号を取得できます。

ただ必要なのはメジャーバージョンなので文字列操作でうまいことメジャーバージョンだけを取り出す必要があります
まとめるとこんな感じ。

```python:installer.py
import sys
import re
import requests
import subprocess


   def get_lastrelease_chromedriver():
        """
        chromeブラウザに合う最新のchromedriverのバージョンを得る
        :return:
        """
        # インストール済みのchromeブラウザのバージョンを得る
        try:
            res = subprocess.Popen('dir /B /O-N "C:\Program Files (x86)\Google\Chrome\Application" | findstr "^[0-9].*\>"', stdout=subprocess.PIPE, shell=True).communicate()
            target = str(res[0])
            pattern = r'[0-9]+\.'
            match = re.search(pattern, target)
            if match is None:
                # [32bit用]
		res = subprocess.Popen('dir /B /O-N "C:\Program Files\Google\Chrome\Application" | findstr "^[0-9].*\>"', stdout=subprocess.PIPE, shell=True).communicate()

            if len(res) < 1:
                raise Exception("chromeブラウザのバージョン確認のコマンドが失敗しました")
            target = str(res[0])
            pattern = r'[0-9]+\.'
            match = re.search(pattern, target)
            if match is None:
                sys.exit(-1)
            chrome_ver = match.group(0).replace('.', '')
        except:
            return None

        # WEBサービスにchromedriverのバージョンを問い合わせする
        try:
            api = "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_{}"
            ver = requests.get(api.format(chrome_ver)).text
        except:
            return None

        return ver
```

それをモジュールの確認用.pyに組み込むと↓のようになります

```モジュールの確認用.py
try:
  driver = webdriver.Chrome()
except:
  ver = get_lastrelease_chromedriver()
  install_module("chromedriver-binary", ver)
```
これでchromedriver-binaryの自動更新もできるはず。