Webスクレイピング」タグアーカイブ

Python3 + Selenium + Firefox でチャットワークの表示名を自動で切り替える

前提

以下の環境で実装、動作確認済み

要素 バージョン
debian 8.6
python 3.6.2

概要

非常にニッチな目的に感じる題だが、以下の流れでこうなった

  • 諸事情でチャットワークの表示名を自動で変更する手段を作りたい
  • チャットワークAPIを使えばいけそう
  • ユーザ情報を変更するAPIが存在しなかった(見落としているだけであったら教えてください)
  • 仕方ないのでスクレイピングで強引に変更しよう
  • チャットワークにログインする必要もあるのでSeleniumを使うのが良いのかな
  • せっかくだから最近勉強してるPython3を使おう

以上より、Seleniumを用いてFirefoxを自動操作し、チャットワークにログイン、表示名を任意のに変更するスクリプトを実装する。

なお、今回はGUIを持たない環境で実装するので、Xvfbを用いた仮想画面を使って動かす(この辺理解が怪しい)

下準備

まずFirefoxを入れる。画面無くて良いならPhantomJSとかヘッドレスブラウザ使えばいいじゃんと思い最初はそうしたのだが、どうしてもチャットワークが正しく表示できなかったので安定を取ってFirefoxにした。その分容量が大きいけど仕方ない。

$ apt-get install firefox

Firefoxのレンダリングエンジンであるgeckoをseleniumから利用するためのドライバーが別途必要。
こちらからダウンロードできるので、OSの種類に応じてダウンロードする。
今回は64ビット版のLinuxなので以下のように。パスが通ってるところで展開したほうが後々楽。

$ cd /usr/local/bin
$ wget https://github.com/mozilla/geckodriver/releases/download/v0.18.0/geckodriver-v0.18.0-linux64.tar.gz
$ tar -zxvf geckodriver-v0.18.0-linux64.tar.gz
$ rm geckodriver-v0.18.0-linux64.tar.gz

仮想モニタが必要になるのでインストール

$ apt-get install xvfb

必要なPythonモジュールをpipでインストール

$ pip install selenium
$ pip install PyVirtualDisplay

ログイン情報を定義

チャットワークにログインするためのメールアドレスをパスワードが必要になるが、ソースコード中にハードコーディングしてしまうとコードの共有ができなくなってしまうので、今回はJSONファイルに別途書き出すようにする。

以下のようなsecret.jsonを作成した。

{
  "email":    "hogehoge@fugafuga.com",
  "password": "qawsedrftgyhujikol"
}

実装

今回は一つのファイルで全て完結させるので、前項で作成したsecret.jsonと同じフォルダに、main.pyを作成する。

モジュールインポート

今回使うモジュールは以下の通りなのでimportする。

import time
import json
import sys
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.common.by import By
from pyvirtualdisplay import Display

ログイン情報を取得する

secret.jsonを開いて、jsonモジュールでデシリアライズ。メールアドレスとパスワードを抜き出す

with open('secret.json', 'r') as file:
  secret   = json.load(file)
  email    = secret['email']
  password = secret['password']

Seleniumの初期設定

仮想モニタを立ち上げ、SeleniumDriverをFirefoxで初期化。画面サイズがやたらと大きいのはデバッグ時のスクリーンショットを見やすくするためなので最終的に意味はない。

display = Display(visible=0, size=(1920, 1080))
display.start()
driver = webdriver.Firefox()

チャットワークへのアクセス

SeleniumDriverのgetメソッドで簡単にWebサイトにアクセスできる。

driver.get('https://www.chatwork.com/login.php?lang=ja&args=')

メールアドレス/パスワードを入力

find_element_by_css_selectorメソッドで、CSSセレクターを用いてDOMを取得し、それに対してsend_keysメソッドを用いてキー入力を行うことができる。JSONファイルから取得しておいたメールアドレス/パスワードを入力する。今後他にも出てくるけど、SeleniumDriverって何かとメソッド名が冗長だなと思った。

driver.find_element_by_css_selector('form[name="login"] input[name="email"]').send_keys(email)
driver.find_element_by_css_selector('form[name="login"] input[name="password"]').send_keys(password)

ログインする

同じくfind_element_by_css_selectorメソッドを用いてログインボタンを取得し、clickメソッドでクリックすることでログインする。

driver.find_element_by_css_selector('form[name="login"] input[name="login"]').click()

ログイン処理には時間がかかる上、チャットワークはJavaScriptで画面を構築していくので、構築の完了まで待機する必要がある。
Seleniumでは、以下のようにWebDriverWaitを用いることで、「特定の何かが確認できるまで待機する」という記述を行うことができる。
以下では、IDが_myStatusNameのDOMが確認できるまで待機するという処理(10秒でタイムアウト)

wait = WebDriverWait(driver, 10)
wait.until(expected_conditions.visibility_of_element_located((By.ID, "_myStatusName")))

_myStatusNameは、この部分。これが表示されたら読み込み完了と捉える。

読み込み完了でようやくチャットワークの画面に到達する。

プロフィール画面を開く

ヘッダのプルダウンメニューを開いて、編集メニューをクリックしてプロフィール編集画面を表示する。
かなりまどろっこしい事してる気がする。可能であれば最初からプロフィール編集画面を表示するためのJavaScriptコードを実行させれば一発だと思うが、Seleniumの練習も含めて地道に行う。

0.5秒のスリープを毎回挟んでるのは、UIのアニメーションがあるのでスグに次のUIを操作できないため。これもっと良い方法あったら教えて下さい。

driver.find_element_by_css_selector('#_myStatusName').click()
time.sleep(0.5)
driver.find_element_by_css_selector('#_myProfile .myAccountMenu__anchor').click()
time.sleep(0.5)

表示名を編集する

編集ボタンをクリックし、テキストボックスの内容を書き換え、保存ボタンをクリックする。
変更後の表示名は、コマンドライン引数から取ることを想定し、sys.argvを参照する。

driver.find_element_by_css_selector('#_profileContent ._profileEdit').click()
time.sleep(0.5)
driver.find_element_by_id('_profileInputName').clear()
driver.find_element_by_id('_profileInputName').send_keys(sys.argv[1])
driver.find_element_by_css_selector("div[aria-label='保存する']").click()

実行

以下コマンドで実装できる。ネットワーク環境の影響を大きく受けるが、手元の環境で実行時間は10秒程度。APIがあれば1秒で終わるというのに。

$ python main.py "ふー ばーのすけ"

最新のソースコードはこちら

所感

  • Seleniumの入門としてはえらくニッチな使い方になったが、個人的にやりたかったことが実現できたので良かった
  • 全体的冗長なコードになりやすそう。ラッパーライブラリが多いのも頷ける
  • JavaScriptで動いてるページをうまく制御したければ関連ライブラリ/フレームワークを使うべきか
  • このやり方だとチャットワーク側がちょいとHTML変更したら動かなくなったりするのでやっぱりAPIが欲しい

Python3.6.2をインストールして、Wikipediaをクロールするスクリプトを書く

前提

本記事は以下の環境で動作確認済み

要素 バージョン
Debian 9.1
pyenv 1.1.3-7
python 3.6.2

概要

Pythonを使ったことのない私が、pyenvを用いて現在の最新版であるPython3.6.2をインストールし、Wikipediaの特定ページからランダムにリンクを辿り続けるスクリプトを書いたお話。

Python3.6.2のインストール

pyenvについて

pyenvは、複数のPythonのバージョンを容易にインストール、切り替えを行うツール。Rubyのrbenvや、nodeのnvm,nなどと同じようなもの。

pyenvのインストール

以下ページをそのまま参考にした。
PyenvによるPython3.x環境構築(CentOS, Ubuntu) – Qiita

3.6.2のインストール

前述のページだと若干古いPythonを使っているので、pyenvを用いて以下のように3.6.2をインストール

$ pyenv install -v 3.6.2
$ pyenv global 3.6.2

確認

$ python --version
Python 3.6.2

Python3入門

初Pythonだったのでまずは基本的な使い方を確認。以下のページが広く浅くしっかり丁寧だったので大変お世話になった。
Python3基礎文法 – Qiita

Wikipediaをクロールするスクリプト作成

概要

Pythonの基本を学んだので、手始めに、以下のような簡単なWebクロールスクリプトを書いてみる。

  • 特定のWikipediaのページを開始地点とする
  • 記事内の、他の単語へのリンク一覧を取得する
  • ノイズとなるリンクを排除し、関連する単語へのリンクのみを取り出す
  • 残ったリンク一覧から、ランダムに1件選出し、そのリンク先へ移動する
  • リンク先のページタイトルを標準出力し、同様の操作を繰り返す

Wikipediaを使った連想ゲームのような物?

使用ライブラリ

調べてみると、PythonにはWebクロールのためのフレームワーク/ライブラリも充実してるようだが、今回はPythonのお試しも含んでいるので、HTTPにrequests、HTML/XMLパーサにBeautifulSoupというライブラリを用いることにする。

実装

そんなに長くないので先に全文

BASEURL = 'https://ja.wikipedia.org'

# 指定したWikipediaページ内のaタグからランダムで一つ戻す
def get_wiki_a_tag(url):
  response = requests.get(url)
  soup = BeautifulSoup(response.text, 'lxml')
  print(soup.find('h1').text)
  a_tags = filter(
    lambda a: 'href' in a.attrs and
               a.text and
               a.attrs['href'].startswith('/wiki') and
               a.attrs['href'].find('Wikipedia') and
               a.text.find('年') == -1,
    soup.select('.mw-parser-output p a')
  )
  a_tags_list = list(a_tags)
  if not len(a_tags_list):
    print("end")
    sys.exit()
  return random.sample(a_tags_list, 1)[0]

# 開始地点を設定
word  = 'Python'
url   = f"{BASEURL}/wiki/{word}"
# ランダムにWikipediaのリンクを飛び続ける
while True:
  a_tag = get_wiki_a_tag(url)
  url  = f"{BASEURL}{a_tag.attrs['href']}"
  time.sleep(1)

まず、以下の部分で特定URLのHTTPレスポンスを取得する。BeautifulSoupを用いることで、レスポンスのXML/HTMLをparseできるようにする。

response = requests.get(url)
soup = BeautifulSoup(response.text, 'lxml')

関連する単語へのリンクは概ねdiv.mw-parser-output配下のp要素に含まれているので、その中のaタグの一覧を取得する。あくまで概ねなので正確ではない。

soup.select('.mw-parser-output p a')

ノイズとなるリンクはpythonのfilter関数で排除する。排除対象は以下の通り

  • href属性が設定されてないaタグ
  • テキストが設定されていないaタグ
  • リンク先が/wiki で始まらないaタグ
  • リンク先にWikipediaが含まれるaタグ(要出典、検証可能性など)
  • テキストに「年」が含まれるaタグ(‘2010年’などの記事が頻出するため)
lambda a: 'href' in a.attrs and
           a.text and
           a.attrs['href'].startswith('/wiki') and
           a.attrs['href'].find('Wikipedia') and
           a.text.find('年') == -1,

randomパッケージのsample関数を用いて、aタグの一覧からランダムに一つ戻す。RubyだとArrayにsampleメソッドがあるのでちょっと不便だと思った。

return random.sample(a_tags_list, 1)[0]

動作確認

Pythonスタートでやってみたところ、初めのうちは良い感じだったが、トレインチャンネルあたりからおかしくなった。

もう一度。Googleから何故サウナ風呂につながるのか‥‥。
※確認した所、社内にサウナがあるらしい

試しに開始地点を阿部寛にしたらやたらとグローバリゼーションになった。阿部寛だしそんなものか。

所感

  • とりあえず何かを作ることを通じてPythonに世界に飛び込んでみたが、まだまだ魅力は掴めてない。インデントブロックがPythonの特徴の一つだが、これもまだ見やすいとは思えない(インデント幅を4字にしたらまた違うかも)
  • Rubyに慣れてて、Rubyが一番好きな分、Pythonとはウマが合わない可能性もあるが、もっと使ってみて自分に合うとこを見つけていきたい。
  • 今回作ったスクリプト、ずっと眺めてられそう。