Python自動化 #4 — Webスクレイピング ②: Playwright で動的ページを扱う

読了 5分

#3 では、requests で HTML を取得して BeautifulSoup でパースする方法を身につけました。ところが同じコードをあるサイトに適用すると、おかしなことが起きます。ブラウザでは確かにデータが見えるのに、コードで受け取った HTML は空の骨組みです。今回は、こうした JavaScript レンダリングページを Playwright で扱う方法を整理します。

空の HTML が返ってくる理由 #

練習サイト quotes.toscrape.com には、JavaScript でレンダリングされるバージョンの /js/ パスがあります。#3 の方式そのままで取得してみます。

#3 の方式で取得してみる
import requests
from bs4 import BeautifulSoup

html = requests.get("https://quotes.toscrape.com/js/").text
soup = BeautifulSoup(html, "html.parser")
print(len(soup.select(".quote")))  # 0

ブラウザで開くと名言が 10 件見えるのに、コードは 0 件と答えます。このページのサーバーは空の骨組みの HTML と JavaScript コードだけを送り、実際のデータはブラウザがその JavaScript を実行したあとに画面へ描かれるからです。requests は JavaScript を実行できないので、空の骨組みだけを受け取ります。解決策は 1 つです。JavaScript を実行できる本物のブラウザをコードで操縦することで、その道具が Playwright です。

Playwright のインストール #

インストールは 2 段階です。Python パッケージをインストールしたあと、操縦するブラウザのバイナリを別途ダウンロードします。

インストール
pip install playwright
playwright install chromium

uv を使っているなら、uv add playwright のあとに uv run playwright install chromium を実行すれば OK です。playwright install chromium は Playwright 専用の Chromium ブラウザを取得するステップです。普段使っている Chrome とは別に管理されるので、既存のブラウザ設定には影響しません。

最初のスクリプト: ページを開いてスクリーンショット #

first_browser.py
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()  # headless=True がデフォルト
    page = browser.new_page()
    page.goto("https://quotes.toscrape.com/js/")
    page.screenshot(path="page.png")
    browser.close()

実行しても画面には何も表示されませんが、page.png には名言 10 件がレンダリングされた画面が写っています。デフォルトが headless モード、つまり画面なしでバックグラウンドで動くブラウザだからです。動作を目で確認しながらデバッグしたいときは、p.chromium.launch(headless=False) に変えると実際のブラウザウィンドウが開き、コードが操縦する過程がそのまま見えます。

セレクタと待機: wait_for_selector #

動的ページスクレイピングの核心となる概念は待機です。ページを開いても、データは JavaScript の実行が終わったあとにようやく現れるので、コードが早く読みすぎるとまた空の結果を受け取ります。Playwright の答えが wait_for_selector です。

要素が現れるまで待つ
page.goto("https://quotes.toscrape.com/js/")
page.wait_for_selector(".quote")       # .quote が現れるまで待機
print(page.locator(".quote").count())  # 10

time.sleep(5) のような固定待機との違いが重要です。sleep はデータが 1 秒で表示されても 5 秒を待ち切り、5 秒以内に表示されなければそのまま失敗します。wait_for_selector は要素が現れた瞬間に次の行へ進み、デフォルトの 30 秒までは待ち続けてくれます。速いときは速く、遅いときは粘り強く動くわけです。なお locator でクリックやテキスト抽出をするときは Playwright が自動で待機してくれるので、明示的な待機はページに入った直後のように基準点が必要な場所で使えば十分です。

クリック・入力・ログイン #

ログインが必要なページも同じ方式で処理します。入力は fill、クリックは click です。

ログイン自動化
import os

USER = os.environ["QUOTES_USER"]
PASSWORD = os.environ["QUOTES_PASSWORD"]
page.goto("https://quotes.toscrape.com/login")
page.fill("#username", USER)
page.fill("#password", PASSWORD)
page.click("input[type=submit]")
page.wait_for_selector("a[href='/logout']")  # ログイン成功を確認

1 つセキュリティ上の注意が必要です。パスワードをコードに直接書いてはいけません。コードは Git に上がり、画面で共有されるからです。上のコードのように環境変数に分離し、実行前にターミナルで export QUOTES_PASSWORD=パスワード のように渡す方式が基本です。最後の行の wait_for_selector にも注目してください。ログアウトリンクが現れたということはログインが実際に成功したという意味なので、次のステップへ進む前の確認装置の役割を果たします。

無限スクロール・「もっと見る」の処理 #

スクロールするたびにデータが追加で読み込まれるページもよくあります。練習サイトの /scroll パスがまさにこの構造です。コツは、スクロールして、データの件数がそれ以上増えなくなるまで繰り返すことです。

無限スクロールを最後まで読む
page.goto("https://quotes.toscrape.com/scroll")
page.wait_for_selector(".quote")
prev = 0
while True:
    page.mouse.wheel(0, 5000)    # 下へスクロール
    page.wait_for_timeout(1000)  # 読み込み待ち 1 秒
    count = page.locator(".quote").count()
    if count == prev:            # 増えなくなったら終了 (100 件)
        break
    prev = count

「もっと見る」ボタンを押すと次のデータが出るページなら、構造はもっと単純です。ボタンが存在する間、page.click("text=もっと見る") を繰り返せば済みます。

データを抽出して CSV に保存 #

仕上げは #3 と同じです。レンダリングが終わったページから locatorinner_text() でデータを取り出し、CSV に保存します。BeautifulSoup の selectget_text に対応する流れなので、構造がそのまま重なって見えるはずです。

scrape_quotes.py
import csv
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto("https://quotes.toscrape.com/js/")
    page.wait_for_selector(".quote")
    rows = []
    for q in page.locator(".quote").all():
        text = q.locator(".text").inner_text()
        author = q.locator(".author").inner_text()
        rows.append([text, author])
    browser.close()

with open("quotes.csv", "w", newline="", encoding="utf-8-sig") as f:
    writer = csv.writer(f)
    writer.writerow(["quote", "author"])
    writer.writerows(rows)

どちらを使うか #

Playwright のほうが強力だからといって、常に Playwright を使う理由はありません。ブラウザを丸ごと立ち上げる分、遅くて重いからです。

requests + BeautifulSoup (#3)Playwright (今回)
速度・リソース速くて軽い (HTML を 1 回受信)遅くて重い (ブラウザを起動)
JavaScript レンダリング不可可能
ログイン・クリック・スクロール限定的可能

見分け方も簡単です。ブラウザでページのソースを表示(Ctrl+U)したとき目的のデータが見えるなら静的ページなので、#3 の方式が軽くて速いです。ソースにはないのに画面には見えるなら動的ページなので、今回の Playwright が必要です。

まとめ #

今回扱った内容です。

  • 空の HTML の原因と選択基準: データを JavaScript が描くなら requests は空の骨組みしか受け取れないので、ページのソースにデータがないときだけ Playwright を使用
  • Playwright の基本: インストール 2 段階、headless ブラウザで goto とスクリーンショット
  • 核心概念: sleep の固定待機の代わりに wait_for_selector で要素が現れるまで待機
  • 実戦パターン: ログイン自動化(パスワードは環境変数)、無限スクロール、CSV 保存

次回(#5 メール・通知)では、ここまで作ったスクリプトの結果を人に届ける方法を扱います。スクレイピングしたデータをメールで送り、Slack のようなメッセンジャーへ通知を飛ばす流れまでつなげます。

X