暇なので『RPAツール』を作る 番外編

#はじめに
皆さん、どうも。enp（えん）です。冬より夏が好きです。
この記事は今から『RPAツール』を作ろうとする人の進捗報告になります。
RPAやRPAツールの作り方が書いてあるものではございませんので、ご了承ください。
今回は番外編ということで#3の延長線上で出来ちゃったRPAのご紹介です。
大変私欲を極めたRPAなのでグレーゾーンのような気もしますが、なにとぞよろしくお願いいたします。

#面倒くさいんじゃ！
はい。タイトルそのまんまです。多分、同じようなことを考える人は多いと思うんです。
###マビノギって、なぜブラウザから立ち上げなきゃならないの？
マビノギに限らずブラウザからでのみ立ち上げるゲーム全てに言えます。
<font color=red>__いちいちホームページに行くの面倒！ どうにかしたい！__</font>
そこで私は思います。__自動化できるんじゃね？__
 #3で自動ログインは出来るようになりました。そのため、マビノギへログインすることは出来ます。
あとはゲームを立ち上げるボタンをクリックすれば出来る！　そう思い作り始めました。

#実装に必要なモノ
早速自動化を始めるのですが、今回必要なものがいくつかあります。
そのため、ここで列挙しておきたいと思います。

__【実装に必要なものリスト】__
　　__・ Python__
　　__・ selenium__
　　__・ ChromeDriver__
　　__・ PyOTP__
　　__・ 一度ゲームを立ち上げたことのあるユーザープロファイル（重要）__

PythonからPyOTPまでは普通にブラウザを自動で操作する上で必要なモノたちです。
最後の『一度ゲームを立ち上げたことのあるユーザープロファイル』はある問題を解決するために使います。
ユーザープロファイルとは__Chromeと同期しているユーザーのファイル__です。

#最大の壁『モーダルダイアログ』
さて、早速実装していきましょう。しかし、一つ大きな問題があります。
それは__モーダルダイアログの存在__です。
モーダルダイアログとは__ダイアログ以外操作できなくなるダイアログ__のことです。
ちょっとダイアログがゲシュタルト崩壊しそうな感じですね。ちょっと分かりやすくしましょう。
![モーダルダイアログ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/641928/ae0c959c-a42f-d8e1-dbcf-e1e7421fa289.png)
上の図が問題のモーダルダイアログです。このダイアログが表示されている間、マビノギのファンアートやお知らせなどをクリックしても何も起きません。このように__ダイアログ以外操作できなくなるヤツをモーダルダイアログと言います。__
しかし、__このダイアログはseleniumで認識しませんでした。__seleniumの機能でダイアログのボタンを押すものがありますが、反応せず。javaScriptを利用しようと思いましたが、そもそもクラス名など分からないためボタンを取得することができませんでした。
さて、ここでマビノギをプレイしている皆さんに質問です。
__このダイアログ見たことあります？__
初めてマビノギをプレイした人は新しい記憶として残っていると思いますが、マビノギを何年と続けている方はもう忘却の彼方だと思います。
そう、普通このダイアログは__二回目以降表示されないのです。__まぁ、チェックボックスにチェックした人だけですが。
__では、なぜ表示されなくなるのでしょうか？__
ネクソン側が保存してるんじゃないの？　と思った方、<font color=red>__不正解__</font>です。
かといって__ウェブページに保存されているわけでもありません。__ウェブページを作っている__<font color=red>HTMLやjavaScriptなどは情報を保存する機能を持っていないから</font>__です。データベースと組み合わせていれば保存することも可能だと思いますが、一般的ではありません。
__じゃあ、何？__
答えは__クッキー__と呼ばれる小さなファイルです。聞いたことがある人もいるかもしれません。
仕組み等を詳しくは話しませんが__<font color=red>『私、一回ゲーム立ち上げたことあるよ』というクッキーさえあれば表示されません。</font>__
もうモーダルダイアログとか言う訳分かんないものを相手にしなくていいのです。
そこで__『一度ゲームを立ち上げたことのあるユーザープロファイル』が重要性を帯びてきます。__
なぜなら、必要なクッキーを持っているからです。

#Chromeさんを知ろう
クッキーを使いモーダルダイアログを克服するのは分かりました。しかし、こう思った人がいるかもしれません。
###わざわざユーザープロファイルを使わなきゃならないの？
答えはYesです。なぜなら__Chromeでクッキーはユーザープロファイルごとで管理されているから__です。
しかし、この『ユーザープロファイルごとで管理されている』というのは__私の所感でしかありません。__
なので、正しいという訳ではありませんのでご了承ください。
なぜ『ユーザープロファイルごとで管理されている』と感じたのかというと、__seleniumで単純にクッキーを取得しただけではダイアログ問題は解決しなかったから__です。
また、__ユーザープロファイルごとで所持しているクッキーが違った__のでそう思いました。
さらに、デフォルトで用意されているユーザープロファイルを使おうとしましたが、少々不都合なことが起きるので使いませんでした。
その不都合な事とは__もともとChromeが開いてる状態ではプログラムが壊れてしまう__というものです。
どうやら参照するディレクトリが被ってしまうとエラーを吐くようです。__なんてこったい。__
そのため、個別にユーザープロファイルを置くディレクトリを用意しました。

#ようやく実装
ここまでグダグダと話してきましたが、要は以下の二点を抑えてくれたらいいです。
　　__・ ダイアログ問題を解決するにはクッキーが必要__
　　__・ 必要なクッキーを持つユーザープロファイルを使えばダイアログ問題が解決できる__
はい。この二点を踏まえて実装していきます。
プログラムは以下の通りです。

```python:nexon.py
# モジュール等をインポート
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.chrome.options import Options
import chromedriver_binary
import pyotp
import time

# ドライバの設定
options = Options()
PROFILE_PATH = r"個別に用意したユーザープロファイルディレクトリ"
options.add_argument('--incognito')  # シークレットモードでブラウザを開く
options.add_argument('--no-sandbox')
options.add_argument('--disable-gpu')
options.add_argument("--user-data-dir=" + PROFILE_PATH)   # ユーザープロファイルディレクトリの指定
options.add_argument("--profile-directory=Profile 1")   # ユーザープロファイルの指定

# ドライバーの指定
chrome = webdriver.Chrome(options=options)

chrome.get("https://login.nexon.co.jp/login/?gm=mabinogi")   # ログインページへ移動
chrome.implicitly_wait(10)   # 必要な要素が表示されるまで待機（最大10秒間）

# ログインに必要な情報
user_name = "user_name"
password = "password"

# ログイン情報を入力するテキストボックスを取得
element_user = chrome.find_element_by_id("NexonID")
element_password = chrome.find_element_by_id("Password")
element_totp = chrome.find_element_by_id("OTP")

# ユーザー名などを入力
element_user.send_keys(user_name)
element_password.send_keys(password)

# ワンタイムパスワードを生成
totp = pyotp.TOTP("ワンタイムパスワードを設定するときに使用する英数字")
# ワンタイムパスワードはあらかじめ設定しておく
# 必要な英数字は控えておく

# ワンタイムパスワードを入力してEnterで確定
element_totp.send_keys(totp.now())
element_totp.send_keys(Keys.ENTER)

# ゲームスタートのボタンをクリック
btn = chrome.find_element_by_xpath("//div[@id='left']/div[@class='bt-login']/div[@class='btn-web-gamestart']/a")
btn.click()

# 待機（待機しないとゲームが起動しなかったため）
time.sleep(10)

# Chromeを閉じる
chrome.quit()
```

クッキーはユーザープロファイルで実行した時点で利用可能のようで特に何かする必要はないようです。
また、headlessモードを使用するとゲームが立ち上がりませんので注意してください。
あと、ゲームを起動するために管理者の許可を求められますが、そこは自分でクリックしてください。
理由はウェブブラウザで操作できる管轄外だからです。
しかし、残念ながら__このプログラムには致命的な欠点があります。__
__クッキーの有効期限が切れたらダイアログ問題が再発します。__
その時はまたクッキーを作るためにユーザープロファイルを使用して自分でゲームを立ち上げる必要があります。

【追記 2021.2.1】
しばらく上記のプログラムを使ってゲームを起動してきましたが、大きな問題が一つ浮上してきました。
それは__ワンタイムパスワードが発行してすぐ使えなくなった場合、プログラムが壊れる__という事です。
タイミングが悪いとワンタイムパスワードを乗り越えられなくなり、プログラミングがエンドレスループする事態になってしまいました。
現時点での解決策は強制終了してもう一度プログラムを動かす方法ですが、スマートではありません。
__ですので、改良の余地ができました。__
本日は不具合報告までですが、改良版が出来次第ご報告しようと思います。

#最後に
最後までお付き合いありがとうございます。番外編いかがだったでしょうか？
かなりグダグダとお話してしまいましたが、この記事が何かの一助になれば幸いです。
しかし、結構グレーゾーンなプログラムだと思いますので、ご了承ください。
以上、enpがお送りいたしました。


【追記 8/15】
ちょっとプログラムを変更しました。変更点はoptions.add_argument('--incognito')の部分です。
この設定を行うとChromeをシークレットモードで開いてくれます。
なぜシークレットモードで開く理由は『システムが誤動作しにくい』等の記事を読みましたが私は違います。
単純に履歴を残したくなかったからです。
私情ですが『私の情報が私の知らないところで勝手に流出している』という状況が大変嫌いです。
なので普段からあまり履歴を残したくないんです。知らないところで私の履歴見られたくないんで。
別に履歴を見られても構わないですよ。でも、違うじゃないですか。勝手に見られるのは。
なので、勝手に見られるというリスクを排除するために履歴を残さないようにしています。
ちょっと変なことをダラダラと喋りましたが『プログラムちょっと変えたよ』ということだけ分かって頂ければ大丈夫です！