🧸
Pyppeteerしたい
Pyppeteer
を使ったのか
なぜ- とある入力フォームの動作確認をするために自動入力にトライすることにした
- 調べてみると
Python
のSelenium
を使った記事が大量にヒットしたが、その中にPyppeteer
を使った Qiita 記事があった - 他の
Pyppetter
に関する Qiita 記事を何本が読んでみて、使ってみることにした -
Pyppetter
はPuppeteer(Javascript)
を移植したものなので、Puppeteer
の記事も何本か確認した
参考にした記事
- Python でスクレイピング - Selenium なんてもう古い!?・・・Pyppeteer の使い方 - Qiita
- いろいろな言語に移植された Puppeteer っぽいライブラリまとめ - Qiita
インストール
pip3 install
$ pip3 install pyppeteer
インポート
モジュールのインポート
import asyncio
from pyppeteer import launch
事前準備
フォームの入力内容を確認
- この記事では下記の要素の入力項目があるとして話を進める
- 名前
テキストボックス
- ふりがな
テキストボックス
- メールアドレス
テキストボックス
- 年代
ドロップダウンリスト
- 本イベントをどこで知りましたか?【複数選択可】
チェックボックス
手動で入力して導線を確認する
- 入力画面 -> 送信
- 入力内容の確認 -> 確認
- 送信完了の画面
自動入力の手順を整理する
- ブラウザを起動する
- 入力フォームを開く
- フォームの内容を入力する
- フォームを送信 -> 入力内容の確認画面に移動
- 確認画面のスクリーンショットを保存
- 内容を送信
フォームの入力内容を用意する
- 今回の入力フォームにはメールアドレスを使った重複登録の確認機能がある
-
faker
パッケージを使って、それっぽいメールアドレスを自動生成した - ついでに他の内容(名前など)も自動生成した
関数の作り方/呼び出し方
-
pyppeteer
で行うウェブ操作はすべて非同期で実装してある - 関数を作成するときは
async def 関数名(...)
にする - 関数を呼び出すときは
await 関数名(...)
にする -
async
やawait
をつけ忘れた場合は、実行時に怒られるので修正すればよい
ブラウザの起動
- まずブラウザを起動します
-
headless Chrome
というのが起動するらしい - 初回の起動時にダウンロードされた
-
- ブラウザの画面は表示されない
ブラウザを非表示(headlessモード)
browser = await launch()
- ブラウザの画面を表示する場合は
headless=False
を指定する - 最初の方に動作の確認をしながらデバッグするときにとてもお世話になった
ブラウザを表示
browser = await launch(headless=False)
ページにアクセス
- ここはコードに書いてある通り
- 新しいページ(タブ?)を開いて URL を入力する、という操作
タブを開いてURLを入力
page = await browser.newPage()
await page.goto(url)
テキストボックスに入力
-
セレクタ
と入力する値
を指定すれば OK
基本形
await page.type('セレクタ', '入力する値')
- どのセレクタを使うかはページソースを確認して判断する
入力フォーム(テキストボックス)
<input type="text" name="name_last" id="name_last" value="">
<input type="text" name="name_first" id="name_first" value="">
<input type="text" name="kana_last" id="kana_last" value="">
<input type="text" name="kana_first" id="kana_first" value="">
<input type="text" name="mail" id="mail" value="">
<input type="text" name="mail_re" id="mail_re" value="">
- この場合は
id
属性を使って指定するのが簡単そう -
input[name="name"]
としても OK(なはず)
自動入力(テキストボックス)
await page.type('#name_last', '竈門')
await page.type('#name_first', '炭治郎')
await page.type('#kana_last', 'かまど')
await page.type('#kana_first', 'たんじろう')
await page.type('#mail', 'tanjiro.kamado@kimetsu.example')
await page.type('#mail_re', 'tanjiro.kamado@kimetsu.example')
ドロップダウンから選択
- テキストボックスと同じように
セレクタ
と選択する値
を指定すれば OK
基本形
await page.select('セレクタ', '選択する値')
- どの
セレクタ
を使うかはページソースを確認して判断する -
選択する値
はvalue
で設定されている値を確認する
入力フォーム(年代)
<select id="age" name="age">
<option value="" selected="selected">選択してください</option>
<option value="1">10代</option>
<option value="2">20代</option>
<option value="3">30代</option>
</select>
- この場合も
id
属性を使って指定する
入力テスト(年代)
await page.select('#age', '1')
- ランダムに入力したい場合
入力テスト(年代をランダムに入力)
import random
await page.select('#age', str(random.randint(1, 3)))
チェックボックスを選択
- セレクタを指定すれば OK
await page.click('セレクタ')
- どの
セレクタ
を使うかはページソースを確認して判断する
<input type="checkbox" name="web" id="web" value="1" />
<label for="web">ウェブサイト</label>
<br />
<input type="checkbox" name="tw" id="tw" value="2" />
<label for="tw">Twitter</label>
<br />
<input type="checkbox" name="fb" id="fb" value="3" />
<label for="fb">Facebook</label>
<br />
<input type="checkbox" name="mz" id="mz" value="4" />
<label for="magazine">メールマガジン</label>
- この場合も
id
属性を使って指定する - 複数選択も可能
await page.click('#web')
await page.click('#fb')
入力フォームを送信する
-
page.click
を使えば OK
入力フォーム(フォームの送信)
<input type="submit" name="__send" id="__send" value="次へ →">
-
name
属性を使って指定した - 今回は、遷移先のページのスクリーンショットを記録として保存したい
- ページ遷移が終わるのを待つようにする(
page.waitForNavigation()
)
非同期処理の待ち合わせ
await asyncio.wait([
page.click('input[name="__send"]'),
page.waitForNavigation(),
])
スクリーンショットの保存する
- エラーが生じた際などに確認できるよう、スクリーンショットを保存する
- ブラウザで開いたときに表示される部分しか保存されない点に注意する
- 全部を保存した場合、
page.setViewport
を使って画面サイズを設定する
await page.setViewport({'width': 800, 'height': 1000})
await page.screenshot(path='ss/screenshot.png', fullPage=True)
入力テストを実行する
- デバッグ中はサンプルにあるままで OK
サンプルの通り
if __name__ == "__main__":
asyncio.get_event_loop().run_until_complete(main())
- 複数回入力する場合は、ループを作っておく
if __name__ == "__main__":
N = 5 ## <== ここの値を直接書き換えて、様子をみながらテスト
for i in range(N):
print('=' * 30)
print(f'TEST [{i+1:3d}/{N:3d}]')
asyncio.get_event_loop().run_until_complete(main())
time.sleep(random.randint(1, 30)) ## <== 攻撃してるわけじゃないよという気持ちの表れ
おまけ:テスト用データの生成
- テスト用データを用意する必要がある
- 今回は
faker
、pykakasi
、random
パッケージを使って、なんちゃってデータ を生成した
モジュールをインストール
$ pip3 install faker
$ pip3 install pykakasi
数値の生成
- 今回の入力フォームだと年代 の入力部分
-
input
のvalue
範囲で整数をランダムに生成
1〜5の整数値をランダムに生成
import random
random.randint(1, 5)
名前の生成
-
faker.Faker
オブジェクトを日本語モード(ja_JP
)で作成する - 苗字(
last_name
)と名前(first_name
)を別々に生成する
苗字と名前をランダムに生成する
from faker import Faker
fakejp = Faker('ja_JP')
name_last = fakejp.last_name()
name_first = fakejp.first_name()
await page.type('#name_last', name_last)
await page.type('#name_first', name_first)
ふりがなの生成
-
問題点
-
faker
で生成されるフリガナはカタカナ - 上で生成した名前とは無関係に生成される
-
-
解決策
- 上で生成した名前を
pykakasi
を使ってひらがなに変換する
- 上で生成した名前を
漢字→ひらがなに変換
import pykakasi
kks = pykakasi.kakasi()
kana_last = kks.convert(name_last)[0]['hira']
kana_first = kks.convert(name_first)[0]['hira']
await page.type('#kana_last', kana_last)
await page.type('#kana_first', kana_first)
Discussion