【Python】Selenium でタイピングの成績を自動記録
注意:一応利用規約をみて大丈夫だと判断した上で行っていますが、もし問題があるようならご一報いただけると幸いです。
タイピング、みなさん練習してますか?
私はしています。
やるべきことはあるけど、どうもやる気がでない、かといってサボるのはいけない、でもやる気が、、、というような状況でタイピング練習は猛威を振るいます。
「これはサボっているわけではない、長期的に考えるとタイピング速度向上は諸々の作業スピードを加速させるはずなのだ」
と自分を納得させることができるためです。ちなみに、画面が見えない側の周囲の人からすると恐ろしく仕事をしているように見えます。
練習を重ねていると、やはり「自分、タイピング速くなってるんか?」というのは当然気になるところです。「長期的に考えると作業スピードを加速させる」といっておきながらスピードに向上がないと言い訳の余地がないただのサボりです。
現在私は e-typing というサイトの「腕試しレベルチェック」を使って日々練習を行なっており、このサイトでは練習のたびに「入力文字数」「ミス入力数」「入力時間」や、それらを元に算出される「スコア」を確認することができます。
さらに、簡単な手続き済ませログインすると、成績を記録することができ、スコアの遷移グラフも生成してくれます。なるほどこれは便利だ。以下の画像は実際の記録表示画面の一部です。
最初は便利だな〜と使っていましたが、だんだんわがままになってきて
ローデータが欲しい
と思うようになってきました。それもそのはず、ちゃんと成長を確認したいのであれば週毎、月毎の平均や分散などを計算したりと、「自分なりの指標」を作りたくなったり系列を分解してみたくなるものです。
しかし、私の知る限り無料でローデータを csv なりなんなりで取得することはできなそうというのが現状です、したがって練習のたびに手動で結果を写すということをしなければなりません。
これは面倒だし、何より入力ミスなどのヒューマンエラーの温床となります。
ということで、前置きが長くなりましたが Python 様の出番ということです。
Selenium で成績を自動で csv に保存
これを実現するために以下のステップを踏みました。
- 成績をためている csv ファイルを読み込む、なければ作る
- ブラウザを立ち上げ、練習画面に遷移してスタート
- 練習の終了を検知し、結果画面からデータを取得
- 「もう一度」ボタンを押せばもう一度練習
- タイピングウィンドウやブラウザを閉じたら終了、練習した分のデータを追加保存
では順番にコードと実行の様子を見ていきます。
使ってる OS は MacOS Bigsur 11.6 です。
準備:Selenium を使うために
他のライブラリと同様、selenium は pip
でインストールするだけですが、chrome を自動操作するために必要となる ChromeDriver のインストールはちょっとだけ注意が必要です。
まず Chrome を立ち上げて右上の縦3点のやつをクリック→ヘルプ→「Google Chrome について」をクリックして、使っている Chrome のバージョンを確認します。このバージョンに最も近い ChromeDriver を公式サイトからダウンロードします。
ただ、基本的に「Chrome を最新にして、最新の ChromeDriver をインストール」すれば問題ないかと思います。
なお、ブラウザを自動操作する際に ChromeDriver がある場所を指定するので、わかりやすいところにダウンロードしておきましょう。
ちなみに、pip
でインストールする場合は以下の通り。
> pip install chromedriver-binary==バージョン
最初に、今回使うモジュールをまとめてインポートしておきます。
import pandas as pd
import numpy as np
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
from time import sleep
import datetime
import chromedriver_binary
1. 成績をためている csv ファイルを読み込む、なければ作る
これから記録をどんどんためる大事なファイルについての扱いです。このファイル名を仮に”typing_score.csv”とします。
まずは、typing_score.csv が指定したディレクトリに…
ある→初めてじゃないから追加書き込みするために読み込む
ない→初めてとみなし、新規にファイルを作成する
という流れを書きいます。
filepath = '記録ファイルを読み込む and 出力する場所'
isFirstPlay = False
try:
my_result = pd.read_csv(filepath + 'typing_score.csv', header = 0, encoding = 'cp932')
# 自動操作が変な挙動をした場合用に、その時だけのバックアップをとっておく
my_result.to_csv(filepath + 'typing_score_backup.csv', index = None, encoding = 'cp932')
except:
isFirstPlay = True
初めての場合は最初に取得したデータに基づいてファイルを作成するので、ここでは作っていません。
2. ブラウザを立ち上げ、練習画面に遷移してスタート
# Chrome を立ち上げる
driver = webdriver.Chrome(executable_path = 'chromedriver の場所')
# タイピングサイトに移動
driver.get("https://www.e-typing.ne.jp/roma/check/")
# 「腕試しチェック」をクリック
button_udedameshi = driver.find_element(By.XPATH, '/html/body/div[1]/div[1]/article/div[1]/a')
button_udedameshi.click()
sleep(2)
# アプリのフレームが出現するので、操作対象をそちらに切り替える
driver.switch_to.frame('typing_content')
# スタートボタンの位置を取得し、クリック
button_start = driver.find_element(By.XPATH, '/html/body/div/div/div[3]')
button_start.click()
sleep(2)
ここで、タイピング開始画面〜終了までの挙動を管理する関数を作成します。
「ゲーム中であること」「結果画面であること」をなんとかして検知し、それぞれの場面では処理を進めずユーザのアクションを待つということをします。
while True:
signal_start = None
try:
signal_start = driver.find_element(By.XPATH, '/html/body/div/div[3]/div/div/div[44]')
except:
break
if not signal_start:
break
else:
# タイピングをスタートさせる関数(後述)
start_typing(driver)
# スコアを取得する関数(後述)
result = get_score(driver)
# 初めて(記録ファイルがない)なら最初に取得したデータを記録ファイルとする
if isFirstPlay:
my_result = result
isFirstPlay = False
else:
my_result = my_result.append(result)
# スコア結果の画面で、「もう一度」ボタンが押されるかウィンドウを閉じられるまで待つ
while True:
try:
mouikkai = driver.find_element(By.XPATH, '/html/body/div/div/div/a[1]').text
if mouikkai == '':
break
sleep(1)
except:
sleep(1)
break
ユーザからのアクション(クリックなど)があるまでは、その画面にある特定の要素(文字列)を signal_start
や mouikkai
として取得し続け、取得できているなら画面遷移が起こっていないとしてループ、つまり何もしないという処理を行なっています。
ちなみにここまでは Selenium 初心者が書くゴミコードだと思うので、いかなるフィードバックでもいただけると大変喜びます。start_typing()
は以下の通りです。
def start_typing(driver):
body_element = driver.find_element(By.TAG_NAME, 'body')
# Start
#body_element.send_keys(Keys.SPACE)
sleep(4)
# Wait untill typing is done
while True:
try:
sentence = driver.find_element(By.XPATH, '/html/body/div/div[3]/div/div/div[44]').text
sleep(0.5)
except:
break
やっていることは非常に原始的で、0.5秒おきにゲーム中であるなら存在するはずの要素を取り出し続け、要素が存在しないのであればループを抜ける(=ゲームが終了したと判断する)という処理をしています。
get_score()
はこんな感じです。
def get_score(driver):
# 取得するデータとその名前を取得
name_score = driver.find_element(By.XPATH, '/html/body/div/div/article/section[1]/div[2]/ul/li[1]/div[1]').text
score = driver.find_element(By.XPATH, '/html/body/div/div/article/section[1]/div[2]/ul/li[1]/div[2]').text
name_level = driver.find_element(By.XPATH, '/html/body/div/div/article/section[1]/div[2]/ul/li[2]/div[1]').text
level = driver.find_element(By.XPATH, '/html/body/div/div/article/section[1]/div[2]/ul/li[2]/div[2]').text
name_time = driver.find_element(By.XPATH, '/html/body/div/div/article/section[1]/div[2]/ul/li[3]/div[1]').text
time = driver.find_element(By.XPATH, '/html/body/div/div/article/section[1]/div[2]/ul/li[3]/div[2]').text
name_num_input = driver.find_element(By.XPATH, '/html/body/div/div/article/section[1]/div[2]/ul/li[4]/div[1]').text
num_input = driver.find_element(By.XPATH, '/html/body/div/div/article/section[1]/div[2]/ul/li[4]/div[2]').text
name_num_miss = driver.find_element(By.XPATH, '/html/body/div/div/article/section[1]/div[2]/ul/li[5]/div[1]').text
num_miss = driver.find_element(By.XPATH, '/html/body/div/div/article/section[1]/div[2]/ul/li[5]/div[2]').text
name_wpm = driver.find_element(By.XPATH, '/html/body/div/div/article/section[1]/div[2]/ul/li[6]/div[1]').text
wpm = driver.find_element(By.XPATH, '/html/body/div/div/article/section[1]/div[2]/ul/li[6]/div[2]').text
name_accuracy = driver.find_element(By.XPATH, '/html/body/div/div/article/section[1]/div[2]/ul/li[7]/div[1]').text
accuracy = driver.find_element(By.XPATH, '/html/body/div/div/article/section[1]/div[2]/ul/li[7]/div[2]').text
# 時間を秒に変換
if '分' in time:
time_sep = time.split('分')
minute = int(time_sep[0])
second = int(time_sep[1].split('秒')[0])
decimal = int(time_sep[1].split('秒')[1])
time = 60 * minute + second + decimal / 100
else:
time_sep = time.split('秒')
second = int(time_sep[0])
decimal = int(time_sep[1])
time = second + decimal / 100
# 正解率の%を取り除く
accuracy = accuracy.replace('%', '')
date = datetime.datetime.now().strftime('%Y/%m/%d')
# データまとめ
columns = ['date', name_score, name_level, name_time, name_num_input, name_num_miss, name_wpm, name_accuracy]
data = [date, score, level, time, num_input, num_miss, wpm, accuracy]
result = pd.DataFrame(data, index = columns).T
return result
上から大まかに「欲しい要素の取得」→「データ整形」→「まとめ」の処理してます。
画面が閉じられるとループを抜け出し、データを保存して自動操作しているブラウザを閉じて終了です。
my_result.to_csv(filepath + 'typing_score.csv', encoding = 'cp932', index = None)
driver.close()
記録自動取得コードは以上です。
タイピングを連続3回やって、画面を閉じた場合に出力されたファイルがこんな感じになります。
ちゃんと記録されていますね!これでみなさんも快適なタイピング練習ライフを!
要修正なところ
- 今どの画面を表示しているかを「特定の要素を取得し、取得できなかったらページが変わったということ」としているが、
time.sleep()
の関係で結果画面になってすぐページ遷移すると謎情報を取得し続ける→スクリプトが終わらない→データが保存されない - ↑関連が原因だろうけど、たまに「閉じる」を押してもブラウザが閉じず、データが保存されない
- 「ミスだけ」ボタンを押すと、「もう一度」ボタンを押した時と同じ挙動をする(=ミスだけやった成績が保存されてまう)
- バックアップの方法が毎回ファイル開いて確認しなきゃ意味ない仕様になってる
おまけ:仮想環境でスクリプトを作った場合に、スクリプトをすぐ実行する方法(Mac)
ターミナルのデフォルトのディレクトリなどすぐ作業しやすい場所に、以下の .sh ファイルを作成しておきます。
#!/bin/sh
/Users/python実行で参照してる場所のパス 実行したいスクリプトのパス
#例:typing.py というファイルを実行したい場合
# /Users/myPC/typing/typing_env/bin/python /Users/myPC/typing/typing.py
python がある場所はターミナルで仮想環境に入った状態で実行します。
> which python
これを準備しておけば、ターミナルでこちらを実行すればタイピングが始まります。
> sh ファイル名.sh
まあ、もしグローバル環境の python を使っていてターミナルのデフォルトディレクトリに typing.py を置いちゃえば、ターミナルで python3 typing.py
ってやれば一発なんですけどね。