🧸

Pyppeteerしたい

2020/11/01に公開

なぜPyppeteerを使ったのか

  • とある入力フォームの動作確認をするために自動入力にトライすることにした
  • 調べてみるとPythonSeleniumを使った記事が大量にヒットしたが、その中にPyppeteerを使った Qiita 記事があった
  • 他のPyppetterに関する Qiita 記事を何本が読んでみて、使ってみることにした
  • PyppetterPuppeteer(Javascript)を移植したものなので、Puppeteerの記事も何本か確認した

参考にした記事

  1. Python でスクレイピング - Selenium なんてもう古い!?・・・Pyppeteer の使い方 - Qiita
  2. いろいろな言語に移植された Puppeteer っぽいライブラリまとめ - Qiita

インストール

pip3 install
$ pip3 install pyppeteer

インポート

モジュールのインポート
import asyncio
from pyppeteer import launch

事前準備

フォームの入力内容を確認

  • この記事では下記の要素の入力項目があるとして話を進める
  1. 名前 テキストボックス
  2. ふりがな テキストボックス
  3. メールアドレス テキストボックス
  4. 年代 ドロップダウンリスト
  5. 本イベントをどこで知りましたか?【複数選択可】 チェックボックス

手動で入力して導線を確認する

  1. 入力画面 -> 送信
  2. 入力内容の確認 -> 確認
  3. 送信完了の画面

自動入力の手順を整理する

  1. ブラウザを起動する
  2. 入力フォームを開く
  3. フォームの内容を入力する
  4. フォームを送信 -> 入力内容の確認画面に移動
  5. 確認画面のスクリーンショットを保存
  6. 内容を送信

フォームの入力内容を用意する

  • 今回の入力フォームにはメールアドレスを使った重複登録の確認機能がある
  • fakerパッケージを使って、それっぽいメールアドレスを自動生成した
  • ついでに他の内容(名前など)も自動生成した

関数の作り方/呼び出し方

  • pyppeteerで行うウェブ操作はすべて非同期で実装してある
  • 関数を作成するときは async def 関数名(...) にする
  • 関数を呼び出すときは await 関数名(...) にする
  • asyncawaitをつけ忘れた場合は、実行時に怒られるので修正すればよい

ブラウザの起動

  • まずブラウザを起動します
    • 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))  ## <== 攻撃してるわけじゃないよという気持ちの表れ

おまけ:テスト用データの生成

  • テスト用データを用意する必要がある
  • 今回はfakerpykakasirandomパッケージを使って、なんちゃってデータ を生成した
モジュールをインストール
$ pip3 install faker
$ pip3 install pykakasi

数値の生成

  • 今回の入力フォームだと年代 の入力部分
  • inputvalue範囲で整数をランダムに生成
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