こんにちは、TechFULでアルバイトをしているSiiiecです!
今回はWebブラウザの自動化に用いられるSeleniumをPythonで使い、 TechFULへコードの提出をしてみたいと思います。
注意
本記事で紹介する方法を用いて、 利用規約に反するような使用はしないようにしてください。
(修正:2021年7月2日 Dockerによる環境構築 の文中で誤字があったため修正しました。 Womdows→Windows)
Seleniumとは
Seleniumはブラウザー自動化を可能にし、それを支えるツール群とライブラリー群プロジェクトです。 出典:Seleniumブラウザー自動化プロジェクト :: Seleniumドキュメント
ブラウザの操作を自動化することで、Webアプリケーションの自動テストやWebスクレイピングが可能になります。
SeleniumはJava, Python, C#等様々な言語で使用することができます。
今回は、Dockerを用いてSeleniumをセットアップし、Pythonを使用してTechFULへ接続します。
Dockerによる環境構築
DockerはLinux環境にコマンドでインストールする他、 Windows、Mac用としてDocker Desktopが配布されています。
本記事では、Dockerはインストール済みである前提で進めていきます。
Seleniumの使い方として幾つかの方法がありますが、今回の方法では以下のような構成をとります。
- Browser : Google ChromeやFirefox等のブラウザ
- Driver : ブラウザを制御する。ブラウザの提供元(Googleなど)が作成、配布する。
- (Remote) WebDriver : Driverを経由してBrowserと通信する。
今回は、Google Chromeを動作させるコンテナ(selenium/node-chrome-debug)、Selenium Serverコンテナ(selenium/hub)、WebDriverを操作するためのPythonコンテナ(python:3.7)を使用します。
先程の図に対応させると、以下のような構成となります。
ここからは実際にSeleniumを使用できるようセットアップしていきます。
まずは、以下のような階層になるようファイルを用意します。
また、echo_echo.py
はTechFULで提出するファイルとして、コードを用意しておきます。
. ├── docker │ ├── chrome │ │ └── Dockerfile │ └── selenium-python │ └── Dockerfile ├── docker-compose.yml └── work └── echo_echo.py # 提出に使用するファイル
コンテナdocker-compose.yml
version: '3.7'
services:
selenium-hub:
image: selenium/hub
container_name: 'selemium-hub'
ports:
- '4444:4444'
chrome:
container_name: 'selenium-chrome'
build: ./docker/chrome
environment:
- HUB_PORT_4444_TCP_ADDR=selenium-hub
- HUB_PORT_4444_TCP_PORT=4444
depends_on:
- selenium-hub
ports:
- '5900:5900'
python:
container_name: selenium-python
build: ./docker/selenium-python
working_dir: /app
tty: true
volumes:
- './work:/app'
selenium-python
とホストマシンでファイルを共有します。
docker/chrome/Dockerfile
FROM selenium/node-chrome-debug
USER root
# Locale settings
ENV LANGUAGE ja_JP.UTF-8
ENV LANG ja_JP.UTF-8
# 日本語フォントをインストール
RUN locale-gen ja_JP.UTF-8 \
&& dpkg-reconfigure --frontend noninteractive locales \
&& apt-get update -qqy \
&& apt-get -qqy --no-install-recommends install \
language-pack-ja \
&& apt-get install -y --no-install-recommends fonts-ipafont \
&& echo "*** INSTALLED: ja_JP locale & font ***" \
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/*
USER seluser
EXPOSE 5900
echo_echo.py
s = input()
print(s)
print(s)
コンテナの起動
ここからは実際にコンテナを起動してみます。
docker-compose.yml
があるフォルダで作業します。
初回はビルドが必要なため、以下のコマンドを実行します。
docker compose build
コンテナを起動するには、以下のコマンドを実行します。
docker compose up -d
コンテナを終了するには、以下のコマンドを実行します。
docker compose down
また、コンテナを再起動するには、以下のコマンドを実行します。
docker compose restart
VNCの起動
今回用いるWebブラウザ用コンテナでは、localhost:5900
でDockerのVNCに接続できます。
VNCに接続することで、Webブラウザ用コンテナをGUI環境でリモート操作できます。
Macの場合は、「Finder > 移動 > サーバへ接続」から確認できます。
サーバのアドレスはvnc://localhost:5900
、パスワードはsecret
となります。
接続すると、サーバのデスクトップが表示されます。
Windowsの場合はUltraVNCやVNC ViewerなどのVNCクライアントをインストールすることで、 Macと同様に確認できます。
Seleniumを動かす
ここからはSeleniumを用いてブラウザを操作します。
以下のコマンドでPythonコンテナに入って、Seleniumを用いるコードを書いていきます。
docker exec -i -t selenium-python bash
ブラウザの起動とページ遷移
先程入ったPythonコンテナで、以下のコードを実行してみます。
import time from selenium import webdriver from selenium.webdriver.common.desired_capabilities import DesiredCapabilities from selenium.webdriver.chrome.options import Options # ①ブラウザオプション設定 chrome_options = webdriver.ChromeOptions() # Chromeの言語を日本語に設定 chrome_options.add_argument('--lang=ja-JP') # Chromeがメモリ不足でクラッシュするのを防ぐ chrome_options.add_argument('--disable-dev-shm-usage') # Capabilitiesの設定(ブラウザの設定情報) capabilities = DesiredCapabilities.CHROME.copy() # ②WebDriverの作成(ブラウザの新規ウィンドウを開く) driver = webdriver.Remote( command_executor='http://selenium-hub:4444/wd/hub', desired_capabilities=DesiredCapabilities.CHROME, options=chrome_options) # 5秒間待機 time.sleep(5) # ③TechFULサイトへアクセス driver.get('https://techful-programming.com') # 5秒間待機 time.sleep(5) # ④WebDriverの終了 driver.quit()
上記のコードを実行しVNCでブラウザの動作を確認すると、以下のように画面が切り替わります。
ここからは、各部分をざっくりと説明していきます。
①ブラウザオプション設定
# ①ブラウザオプション設定 chrome_options = webdriver.ChromeOptions() # Chromeの言語を日本語に設定 chrome_options.add_argument('--lang=ja-JP') # Chromeがメモリ不足でクラッシュするのを防ぐ chrome_options.add_argument('--disable-dev-shm-usage') # Capabilitiesの設定(ブラウザの設定情報) capabilities = DesiredCapabilities.CHROME.copy()
今回はGoogle Chrome(selenium/node-chrome-debug
)を操作するため、Chrome用の設定を用います。
②WebDriverの作成(ブラウザの新規ウィンドウを開く)
# ②WebDriverの作成(ブラウザの新規ウィンドウを開く) driver = webdriver.Remote( command_executor='http://selenium-hub:4444/wd/hub', desired_capabilities=DesiredCapabilities.CHROME, options=chrome_options)
WebDriverを作成し、ブラウザの新規ウィンドウを開きます。
③TechFULサイトへアクセス
# ③TechFULサイトへアクセス driver.get('https://techful-programming.com')
get()
メソッドにより、指定のurlにアクセスします。
④WebDriverの終了
# ④WebDriverの終了
driver.quit()
WebDriverはquit()
メソッドで終了する必要があります。
with webdriver.Remote( command_executor='http://selenium-hub:4444/wd/hub', desired_capabilities=DesiredCapabilities.CHROME, options=chrome_options) as driver: driver.get('https://techful-programming.com') # driver.quit()
また、with構文
を使うことでquit()
メソッドの呼び出しを省略できます。
要素の取得
要素を取得するには、WebDriverクラスのfind_element()
メソッドを使用します。
例として、TechFULランディングページ(https://techful-programming.com/account/landing)の言語選択ドロップダウンを取得、クリックしてみます。
試しにホスト(Mac, Windows)のChromeのデベロッパーツールで確認(F12キーを押し、Elementsを選択)すると、
言語選択ドロップダウンにはid属性
としてpg_id
が設定されていることがわかります。
このため、id属性
としてpg_id
を持つ要素を取得し、クリックしてみます。
# import 省略 from selenium.webdriver.common.by import By # driver作成部分は省略 driver.get('https://techful-programming.com/account/landing') pg_id_element = driver.find_element(By.ID, 'pg_id') time.sleep(5) pg_id_element.click()
click()
メソッドにより、取得した要素をクリックできます。
実行結果は、以下のようになります。
要素を取得する同様のメソッドとして、以下のものも使用できます。
driver.find_element_by_id('pg_id')
該当する要素を複数取得するには、find_elements()
メソッドを用います。
また、Seleniumではidを含めて以下の8種類の要素を取得することができます。
ロケータ 詳細 class name class名に値を含む要素を探す (複合クラス名は使えない) css selector CSSセレクタが一致する要素を探す id id属性が一致する要素を探す name name属性が一致する要素を探す link text a要素のテキストが一致する要素を探す partial link text a要素のテキストが部分一致する要素を探す tag name タグ名が一致する要素を探す xpath XPathと一致する要素を探す
待機
Webページは、アクセスするまで、表示するまでに時間がかかることがあります。
このため、Seleniumはページを読み込むまでのタイムアウトを設定する機能や、 ページの特定の要素・全体の要素が読み込まれるまで待機する機能があります。
# import 省略 from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # driver作成部分は省略 # タイムアウト時間を5秒に設定 wait = WebDriverWait(driver, 5) # TechFULサイトへアクセス driver.get('https://techful-programming.com') # ページの全要素が見えるまで待機 wait.until(EC.visibility_of_all_elements_located)
多くの条件がありここでは説明しきれないため、詳しくはSelenium公式サイトを参照してください。
JavaScriptの実行
WebDriverクラスのexecute_script()
メソッドにより、JavaScriptを実行することができます。
こちらは、ページのスクロール等に用います。
driver.get('https://techful-programming.com/account/landing') # 要素が読み込まれるまで待つ WebDriverWait(driver, 3).until(EC.visibility_of_all_elements_located) # 最初のh4タグを探し h4 = driver.find_element(By.TAG_NAME, 'h4') # 見つけた要素までスクロールする driver.execute_script("arguments[0].scrollIntoView(true);", h4) # 最初のh4タグの内容を出力 print(h4.text)
この例では、最初のh4タグである「世界中のITエンジニアのプログラミングスキルを見える化」を取得し、その要素が画面上端に来るようスクロールします。
TechFULに接続・コードを提出する
ここからは実際にSeleniumを用いてTechFULでコード提出してみます。
今回は、プラクティス問題「エコーエコー」でコードを提出します。
まずは、コード全文を示します。 (長いため折り畳んでいます。)
コード全文
import time
from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
# (A)ファイルの内容を文字列に変換する
def read_file(file : str) -> str:
with open(file, 'r') as data:
contents = data.read()
return contents
# ブラウザオプション設定
chrome_options = webdriver.ChromeOptions()
# Chromeの言語を日本語に設定
chrome_options.add_argument('--lang=ja-JP')
# Chromeがメモリ不足でクラッシュするのを防ぐ
chrome_options.add_argument('--disable-dev-shm-usage')
# Capabilitiesの設定(ブラウザの設定情報)
capabilities = DesiredCapabilities.CHROME.copy()
# Chromeのwebdriverを作成
with webdriver.Remote(
command_executor='http://selenium-hub:4444/wd/hub',
desired_capabilities=DesiredCapabilities.CHROME,
options=chrome_options) as driver:
# (B)ログイン判定
# 手動でログインするためにタイムアウトを長時間に設定
wait = WebDriverWait(driver, 120)
# プラクティス「エコーエコー」に接続
# ここでは、未ログインなのでログインページにリダイレクトされる
problem_url = 'https://techful-programming.com/user/practice/problem/coding/428'
driver.get(problem_url)
# ページの全要素が見えるまで待機する
wait.until(EC.visibility_of_all_elements_located)
# VNCを操作し手動でログインする
# 入力フォーム要素に対して、ログイン情報を入力することも可能
print('手動でログインしてください')
# ログイン完了まで待つ
# ここでは、問題ページに遷移したらログイン完了とする
wait.until(EC.url_to_be(problem_url))
print('ログイン完了')
wait.until(EC.visibility_of_all_elements_located)
time.sleep(3)
print('言語変更ドロップダウンまでスクロール')
# (C)提出言語の変更
# 言語変更ドロップダウンがクリックできるまで待つ
wait.until(EC.element_to_be_clickable((By.ID, 'pg_id')))
# 言語変更ドロップダウンを取得し
pg_id = driver.find_element_by_id('pg_id')
# 言語選択ドロップダウンがページ上端に来るようにスクロール
driver.execute_script(f'arguments[0].scrollIntoView(true);', pg_id)
# 2秒待つ
time.sleep(2)
# 言語を変更する
print('言語をC++に変更')
language_cpp = driver.find_element_by_id('cpp')
language_cpp.click()
time.sleep(3)
print('言語をPython3に変更')
language = driver.find_element_by_id('python')
language.click()
time.sleep(3)
print('エディタへ入力')
# (D)エディタのテキストを更新する
# エディタがクリックできるまで待つ
wait.until(EC.element_to_be_clickable((By.ID, 'editor')))
# エディタのテキストを更新する
submit_text = read_file('echo_echo.py')
driver.execute_script('this.monaco.editor.getModels()[0].setValue(arguments[0]);', submit_text)
time.sleep(3)
print('採点開始')
# (E)採点開始ボタンを押す
# 採点開始というテキストが含まれるボタンを取得
submit_button = driver.find_element_by_xpath('//button[contains(text(), "採点開始")]')
# 採点開始ボタンが画面に入るようスクロール
driver.execute_script(f'arguments[0].scrollIntoView(true);', submit_button)
time.sleep(2)
# 採点開始ボタンをクリック
submit_button.click()
time.sleep(10)
実際に操作するとこのような表示になります。
重要な部分を順に説明していきます。
(A)ファイルの内容を文字列に変換する
# (A)ファイルの内容を文字列に変換する def read_file(file : str) -> str: with open(file, 'r') as data: contents = data.read() return contents
提出ファイル(echo_echo.py)を読み取り、文字列に変換するメソッドを定義しておきます。
(B)ログイン判定
# (B)ログイン判定 # 手動でログインするためにタイムアウトを長時間に設定 wait = WebDriverWait(driver, 120) # プラクティス「エコーエコー」に接続 # ここでは、未ログインなのでログインページにリダイレクトされる problem_url = 'https://techful-programming.com/user/practice/problem/coding/428' driver.get(problem_url) # ページの全要素が見えるまで待機する wait.until(EC.visibility_of_all_elements_located) # VNCを操作し手動でログインする # 入力フォーム要素に対して、ログイン情報を入力することも可能 print('手動でログインしてください') # ログイン完了まで待つ # ここでは、問題ページに遷移したらログイン完了とする wait.until(EC.url_to_be(problem_url)) print('ログイン完了') wait.until(EC.visibility_of_all_elements_located)
まずは、問題ページへ接続します。 Selenium起動直後はTechFULにログインしていないため、VNC経由で手動ログインします。
手動ログイン後は問題ページに遷移するので、
wait.until(EC.url_to_be(problem_url))
により、問題ページへ遷移するまで待ちます。
また、wait.until(EC.visibility_of_all_elements_located)
により、ページの要素が読み込まれるのを待ちます。
(C)提出言語の変更
# (C)提出言語の変更 # 言語変更ドロップダウンがクリックできるまで待つ wait.until(EC.element_to_be_clickable((By.ID, 'pg_id'))) # 言語変更ドロップダウンを取得し pg_id = driver.find_element_by_id('pg_id') # 言語選択ドロップダウンがページ上端に来るようにスクロール driver.execute_script(f'arguments[0].scrollIntoView(true);', pg_id) # 2秒待つ time.sleep(2) # 言語を変更する print('言語をC++に変更') language_cpp = driver.find_element_by_id('cpp') language_cpp.click() time.sleep(3) print('言語をPython3に変更') language = driver.find_element_by_id('python') language.click()
ドロップダウン内の要素を取得、クリックすることで言語を変更します。
元々Python3を選択していた場合は言語変更しているか見えないため、言語をC++に変更後、Python3に変更しています。
(D)エディタのテキストを更新する
# (D)エディタのテキストを更新する # エディタがクリックできるまで待つ wait.until(EC.element_to_be_clickable((By.ID, 'editor'))) # エディタのテキストを更新する submit_text = read_file('echo_echo.py') driver.execute_script('this.monaco.editor.getModels()[0].setValue(arguments[0]);', submit_text)
JavaScriptによりエディタ内のテキストをecho_echo.py
の内容に変更しています。
(E)採点開始ボタンを押す
# (E)採点開始ボタンを押す # 採点開始というテキストが含まれるボタンを取得 submit_button = driver.find_element_by_xpath('//button[contains(text(), "採点開始")]') # 採点開始ボタンが画面に入るようスクロール driver.execute_script(f'arguments[0].scrollIntoView(true);', submit_button) time.sleep(2) # 採点開始ボタンをクリック submit_button.click()
採点開始が含まれるボタンを取得、画面内に収めたあとでクリックします。
終わりに
今回は、ブラウザ自動化フレームワークSeleniumを用いて自動でコード提出をしました。
記事では採点開始ボタンのクリックで終了していますが、結果を待つコードを追加することで提出結果も取得することができます。
再度となりますが、試す場合は利用規約違反とならないよう注意してください。
最後まで読んでくださりありがとうございました!