webページをPythonとSeleniumで自動入力する

あるページに定期的に情報を入力する機会があったのでこれを自動化しました


# 下準備
## 環境
<!-- 
aws上のEC2にAMIを定期的にデプロイするか、Docker hubとかでコンテナ化するかはまだ考え中。
コンテナ内に認証情報も含めることになるので、AMIを時間になったらデプロイするのが良さそう。
-->
AWSにてEC2インスタンスを用意する。

```
ubuntu20.04(ARM64)
t4g.small
GP3 8GB
22/tcpと3389/tcpを開放
```

t3aなどのx86_64ではなくARMなのは今回の構築はARMですべて動作可能であり、ARMの方が安くパフォーマンスが高いからである。
デスクトップ環境を構築するものの余計なソフトウェアをインストールしなければ8GBで問題ない
セキュリティーグループではインバウンドルールで22/tcpと3389/tcpを開放する。アウトバウンドルールはデフォルトのまま。
22/TCPはSSH,3389/TCPはRDPにそれぞれ利用する。

## パスワードの設定
後述するXRDP接続を行うために、ユーザーubuntuにパスワードを設定する。
EC2インスタンスを立ち上げたときにダウンロードした秘密鍵を用いてSSHを行った後、以下コマンドでパスワードを設定する

```
$ sudo passwd ubuntu
New password: 
Retype new password: 
passwd: password updated successfully
```

パスワードは入力後にコンソールに出力されないが、そのままEnterでよい。

## デスクトップ環境構築
ブラウザを実行するにはデスクトップ環境が必須となる。
Ubuntuでは比較的簡単にデスクトップ環境がインストール出来る。

ソフトウェアの更新を行う

```
sudo apt update
sudo apt upgrade
```

デスクトップ環境のインストール。インストールにしばらく時間がかかる。

```
sudo apt -y install ubuntu-desktop
```
処理完了後に再起動を行う。

```
sudo reboot
```

## XRDPの導入
デスクトップを構築したので、GUIにアクセスできる方法で遠隔操作する必要がある。
VNCやNICE DCVなどと比較したが、XRDPが導入の手間と接続品質のバランスが丁度良かった。

xrdpのインストールとサービスの再起動

```
sudo apt -y install xrdp
sudo systemctl restart xrdp
```
サービスの再起動後にstatusを確認する。`Active: active (running)`となっていればOK

```
systemctl status xrdp
```
```

● xrdp.service - xrdp daemon
     Loaded: loaded (/lib/systemd/system/xrdp.service; enabled; vendor preset: enabled)
     Active: active (running) since Tue 2021-09-14 06:42:01 UTC; 8s ago
       Docs: man:xrdp(8)
             man:xrdp.ini(5)
    Process: 2083 ExecStartPre=/bin/sh /usr/share/xrdp/socksetup (code=exited, status=0/SUCCESS)
    Process: 2091 ExecStart=/usr/sbin/xrdp $XRDP_OPTIONS (code=exited, status=0/SUCCESS)
   Main PID: 2092 (xrdp)
      Tasks: 1 (limit: 2204)
     Memory: 716.0K
     CGroup: /system.slice/xrdp.service
             └─2092 /usr/sbin/xrdp
```

## XRDP接続
接続に利用するクライアントPCがwindowsなら、検索バーにrdpと入力し表示される`リモートデスクトップ接続`を起動する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593714/882c4514-3b49-929c-f262-d48b0a3ebc88.png)

コンピューター名にインスタンスのアドレスを入力する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593714/1e8f1ce8-f70f-9934-102f-3ee6f275a1a9.png)

接続先がwindowsではなくLinuxなので警告が表示されるが、意図した用途のため`はい`を選択し続行する

ログイン画面が表示されるため、ユーザー名に`ubuntu`、パスワードは先程設定したものを入力する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593714/047a9d5a-d6d1-4915-e4e0-74388c7a96b6.png)

以下のような初期設定画面が表示される。キーボードやプライバシーの設定があるが、リモートアクセスのためそのまま次へを選択してよい。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593714/d2be55d6-28b9-caac-103f-d80a67149943.png)

ソフトウェアアップデーターからアップデートの通知が来る場合があるが、内容を確認しセキュリティーアップデートで無い限りはアップデートは行わないことにする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593714/97799d9e-e46b-2572-af7b-bc67b16ba778.png)

# Seleniumでブラウザ操作の自動化
## 開発機にSelenium IDE導入
ここからはクライアントマシンで作業する。
クライアントはwindows10で作業しているが、macやlinuxでもほぼ同様に作業できる。
ubuntuではデスクトップ環境のインストールに伴いfirefoxがデフォルトでインストールされているため、今回はfireoxを用いて自動化を行う。
pythonで最初からプログラミングを行うのは大変なので、一度Selenium IDEにてコードを作成した後にpythonへと変換することにする。

クライアントPCのfirefoxで以下ページより拡張機能をインストールする
https://addons.mozilla.org/ja/firefox/addon/selenium-ide/

## 操作の実装
実際にブラウザに対し操作する内容を実装していく。
今回はSelenium JPが公開しているデモサイト[example.selenium.jp](http://example.selenium.jp/reserveApp_Renewal/)を利用する。
これは説明用にデモサイトを利用しているだけなので、実際に対象のページを用いて実装しても良いだろう。

firefoxにインストールしたSelenium IDEをクリックする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593714/cbc8117d-3fcf-9af7-0f22-66cd687e3fce.png)

以下の画面が表示される。今回はイチから作成するため`Create a new project`を選択する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593714/26b1383e-a5af-768e-efaa-780ccddb4592.png)

プロジェクトの名前を付ける。特に指定はないが、今回は`sample_project1`とする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593714/626aa7c4-5eaf-3747-66f5-2d9fc9b26807.png)

Selenium IDEのホーム画面が表示される。
操作の記録を開始するため、右上の`REC`ボタンを選択する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593714/7c1ba05c-5497-e0df-66b7-7de38e5bb87b.png)

プロジェクトのURLを入力する。先程のデモサイトを利用する場合は`http://example.selenium.jp/reserveApp_Renewal/`と入力する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593714/69aa41ac-c88a-593d-dbb0-16e1976cae3d.png)

対象ページが表示される。自動化したい操作を最後まで入力する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593714/df712133-f433-367e-b9fa-de44ce77d4d5.png)

操作が完了したら起動したタブを閉じる。`Selenium IDE`の画面に戻るので、先程選択した右上の`REC`ボタンのところにある■をクリックする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593714/ca042f5b-3879-d797-df6a-8eec8a317f2a.png)

すると`TEST NAME`を入力する画面になる。これは今回の一連の操作に名前をつけてね、ということだ。
今回は`operation1`としておく。これも特に指定はないので自由に入力して問題ないが、わかりやすくするために先程のプロジェクト名とは別のものにしておくことが無難だろう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593714/e6001843-b8cd-88e7-c1db-2ff496049fa4.png)

今回記録した内容は、このSelenium IDEを閉じると消えてしまうためファイルに保存する。
右上の保存ボタンをクリックする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593714/c007e691-8f3e-1a94-4694-66d6e7afa3b5.png)

保存場所とファイル名を確認し、プロジェクトファイルを保存する。
プロジェクトファイルは複数テストケースを作成した場合でも、そのプロジェクト全体で保存し、次に読みだした場合はそのテストケースすべてが読み込まれる。
なお、この保存はwindowsならCtrl+Sでも可能だ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593714/d380bafd-0b3f-b5d7-c275-55f5d7c27b26.png)

保存が完了したので、試しに今記録した動作が問題なく実行できるか確認してみる。
Selenium IDEの上部真ん中あたりの三角の再生ボタンをクリックし実行する。
実行ボタンは2つ並んでいるが、左の実行ボタンはプロジェクトに保存されているすべてのテストケースを実行、右の実行ボタンは選択しているテストケースのみを実行する。今回は右の実行ボタンを選択する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593714/ccfa6e92-c4be-ca07-2a36-0a9e5bb01f39.png)

もし途中で意図しない動作を行った場合はここで修正しておく。

今回作成したテストケースは`Selenium IDE`から実行することを想定しているため、Pythonに変換する。
`Selenium IDE`左上の表示が`Tests`になっていることを確認し、該当するテストケースにマウスカーソルを合わせると`︙`が表示される。これを右クリックする
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593714/bc1a6540-e904-2df7-ab6a-99bd2c2ec0d7.png)

`Export`を選択する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593714/70fc3e8b-449a-249c-8e12-fd0c0e26cea5.png)

`Python`を選択し、表示を見やすくするために上から2つのチェックボックスを選択し`EXPORT`を選択する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593714/9b3c3d79-a59f-4ae3-3276-5025baefbf27.png)

保存場所とファイル名を確認し保存する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593714/444f4135-86f7-51c2-ae7e-bc368b87850b.png)

なお、ここで生成されるコードはそのままでは使えないので修正する必要がある。

生成直後の素のコード↓
https://github.com/Honahuku/Selenium-Automation/blob/main/test_operation1_original.py

修正したコード↓
https://github.com/Honahuku/Selenium-Automation/blob/main/test_operation1_edited.py


## Pythonのプログラムを実行、動作確認(クライアント)
Pythonで必要なライブラリをインストールする

```
pip install selenium
```
`WARNING: You are using pip version 21.1.3; however, version 21.2.4 is available.
You should consider upgrading via the 'c:\python39\python.exe -m pip install --upgrade pip' command.`
と怒られる場合がある
windowsなら以下コマンドを`管理者権限で起動した`powershellで実行することで解決する

```
python -m pip install --upgrade pip
```

以下のソフトをクライアントの環境に応じたもとをダウンロードし、パスの通るディレクトリに展開する

https://github.com/mozilla/geckodriver/releases

windowsの場合はCドライブ直下に`geckodriver-v0.29.1-win64`でディレクトリを作成し`geckodriver.exe`をその中に入れる。

## CLIで2FA
該当ページにアクセスするためにはログインが必要な場合がある。
ログインが必要ない場合は次の[リモートマシン(ubuntu)にSelenium導入](#リモートマシン(ubuntu)にSelenium導入)まで読み飛ばして問題ない

ログインするときの要素としてメールアドレスとパスワードが要求されるが、最近はこれに加えSMSやTOTPによる2FA(二段階認証)/MFA(多要素認証)が実施される。
今回は毎回TOTP(時間ベース)で認証するシステムを自動化することとする。

メールアドレスとパスワードは、

```py
driver.find_element(By.ID, "guestname").send_keys("hoge")
```
とすることで入力可能だが、問題はTOTPの認証である。
これは秘密鍵と時間をベースに6桁のワンタイムパスワードを発行する仕組みである。
これをCLIから実行する仕組みがあるのでこれを行う。

AWSのubuntuマシンにSSHを行い、以下コマンドを実行する

```sh
sudo apt install oathtool
```
oathtoolというプログラムでTOTP秘密鍵からワンタイムパスワードを発行することが出来る。
使い方は以下の通り。`private_key`には実際の秘密鍵をそのまま入力する。

```
oathtool --totp private_key --base32
```
以下実行例のスクショ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593714/42b0120d-5d62-9abd-7cc1-c2ec80533cc5.png)

これをPythonで実行すると以下の例となる。先の生成したコードと組み合わせて利用してほしい。

```python
import subprocess

#subprocessコマンドでホストOSのコマンドを実行する
#ここではoathtoolコマンドで2FAの秘密鍵からコードを生成し、変数resultへ格納する
result = subprocess.run(["oathtool", "--totp", "private_key", "--base32"],stdout=subprocess.PIPE)
print(result.stdout.decode("utf-8"))
#生成した値に文字列操作し入力可能な形にし、変数numberへ格納する
number = result.stdout.decode("utf-8")
# 13 | type | id=idTxtBx_SAOTCC_OTC | 2FAコードの入力
driver.find_element(By.ID, "idTxtBx_SAOTCC_OTC").send_keys(number)
```

## リモートマシン(ubuntu)にSelenium導入

```
sudo apt install xvfb python3-pip firefox-geckodriver
pip3 install selenium
```


## 動作確認
セットアップしたシェルスクリプトを実行する。正常に実行できなかった場合はXRDP接続を行いトラブルシューティングを行う

なお、

```
(EE) 
Fatal server error:
(EE) Server is already active for display 99
        If this server is no longer running, remove /tmp/.X99-lock
        and start again.
(EE)
```
というエラーは

```
pkill -f "Xvfb :99"
```
で解消されるので良ければ参考にしてほしい。
# インスタンス起動時にプログラムを自動実行
プログラムの実行にはブラウザを起動する必要があるが、これにはGUIが有効な環境が必要なため仮想ディスプレイを用意する。
デスクトップ環境がすでにインストールしてある場合でも、自動実行時にはCUIで実行されてしまうため仮想ディスプレイを有効にしGUIで実行する。

## 仮想ディスプレイの有効化
```
sudo apt install xvfb
Xvfb :99 -screen 0 1280x720x16 &
export DISPLAY=:99
```

## 自動実行スクリプトの作成
先程作成したpythonのプログラムを自動実行する必要がある。

```sh:autofill.sh
#!/bin/sh
pkill -f "Xvfb :99"
Xvfb :99 -screen 0 1280x720x16 &
export DISPLAY=:99
python3 ./fill.py
```
# 記事の誤植、抜けについて
記事中に一部説明が不足しているところがあった場合は気軽にtwitterのリプライかDMでお問い合わせください。[@yosuke_yamakawa](https://twitter.com/yosuke_yamakawa)

# 参考文献
## 下準備
デスクトップ環境のインストールとXRDPのインストール
https://qiita.com/hitobb/items/b44e87e04133dc845265#desktop%E7%92%B0%E5%A2%83%E3%82%92%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB

## Seleniumでブラウザ操作の自動化
コマンドリファレンス
https://qiita.com/oh_rusty_nail/items/77782973b4152992017b

導入について
https://qiita.com/oh_rusty_nail/items/5b584c95e01759c00869