TechFULの中の人

TechFULスタッフ・エンジニアによる技術ブログ。IT関連のことやTechFULコーディングバトルの超難問の深掘り・解説などを毎週更新

ブラウザ自動化フレームワークSeleniumを用いてコードを提出してみる

こんにちは、TechFULでアルバイトをしているSiiiecです!

今回はWebブラウザの自動化に用いられるSeleniumPythonで使い、 TechFULへコードの提出をしてみたいと思います。

注意

本記事で紹介する方法を用いて、 利用規約に反するような使用はしないようにしてください。

(修正:2021年7月2日 Dockerによる環境構築 の文中で誤字があったため修正しました。 Womdows→Windows

Seleniumとは

Seleniumブラウザー自動化を可能にし、それを支えるツール群とライブラリー群プロジェクトです。 出典:Seleniumブラウザー自動化プロジェクト :: Seleniumドキュメント

ブラウザの操作を自動化することで、Webアプリケーションの自動テストやWebスクレイピングが可能になります。

SeleniumJava, Python, C#等様々な言語で使用することができます。

今回は、Dockerを用いてSeleniumをセットアップし、Pythonを使用してTechFULへ接続します。

Dockerによる環境構築

DockerはLinux環境にコマンドでインストールする他、 WindowsMac用としてDocker Desktopが配布されています。

本記事では、Dockerはインストール済みである前提で進めていきます。

Seleniumの使い方として幾つかの方法がありますが、今回の方法では以下のような構成をとります。

  • Browser : Google ChromeFirefox等のブラウザ
  • Driver : ブラウザを制御する。ブラウザの提供元(Googleなど)が作成、配布する。
  • (Remote) WebDriver : Driverを経由してBrowserと通信する。

f:id:techful-444:20210628164553p:plain
Seleniumの構成 出典:コンポーネントを理解する :: Seleniumドキュメント

今回は、Google Chromeを動作させるコンテナ(selenium/node-chrome-debug)、Selenium Serverコンテナ(selenium/hub)、WebDriverを操作するためのPythonコンテナ(python:3.7)を使用します。

先程の図に対応させると、以下のような構成となります。

f:id:techful-444:20210628202840p:plain
本記事での構成

ここからは実際に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/selenium-python/Dockerfile

FROM python:3.7

# Python用のSeleniumライブラリをインストール
RUN apt update \
 && apt -y clean \
 && pip install selenium \
 && rm -rf /var/lib/apt/lists/*

Python用のSeleniumライブラリをインストールしています。

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 

Chromeを日本語で表示する設定と、VNCを使用するための設定をしています。

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となります。

接続すると、サーバのデスクトップが表示されます。

f:id:techful-444:20210628203729p:plain
「Finder > 移動 > サーバへ接続」から接続

f:id:techful-444:20210628203808p:plain
サーバのアドレスを入力後、パスワードを入力

f:id:techful-444:20210628203825p:plain
VNCサーバへ接続後の表示

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でブラウザの動作を確認すると、以下のように画面が切り替わります。

f:id:techful-444:20210628205000p:plain
②実行後の画面 ブラウザが起動する

f:id:techful-444:20210628205017p:plain
③実行後の画面 TechFULランディングページが表示される

ここからは、各部分をざっくりと説明していきます。

①ブラウザオプション設定
# ①ブラウザオプション設定
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()メソッドにより、取得した要素をクリックできます。

実行結果は、以下のようになります。

f:id:techful-444:20210628211354p:plain
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と一致する要素を探す

出典:要素を探す :: Seleniumドキュメント

待機

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公式サイトを参照してください。

www.selenium.dev

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でコード提出してみます。

今回は、プラクティス問題「エコーエコー」でコードを提出します。

techful-programming.com

まずは、コード全文を示します。 (長いため折り畳んでいます。)

コード全文

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)

実際に操作するとこのような表示になります。

f:id:techful-444:20210628225136p:plain
問題ページへアクセス、未ログイン状態なのでログイン画面が表示される

f:id:techful-444:20210628225139p:plain
手動でログイン後、問題ページが表示されたことを判定する

f:id:techful-444:20210628225143p:plain
言語の選択し、エディタへコードを入力する

f:id:techful-444:20210628225147p:plain
採点開始ボタンを押す

重要な部分を順に説明していきます。

(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を用いて自動でコード提出をしました。

記事では採点開始ボタンのクリックで終了していますが、結果を待つコードを追加することで提出結果も取得することができます。

再度となりますが、試す場合は利用規約違反とならないよう注意してください。

最後まで読んでくださりありがとうございました!