JavaScriptで一覧を描くページは、requestsで取得すると中身が空のHTMLしか返りません。Playwrightでスクレイピングすると、実際のブラウザで描画を終えたページから値を読み取れます。この記事では、取得前のrobots.txt判定から、ロケーターによる描画待ち、ページ送りと無限スクロールの打ち切り条件、API応答のJSONを直接受け取る方法、ログイン状態の保存と取得間隔の決め方までを、Pythonのコピーして動かせる形で示します。版と仕様はPyPIのPlaywright 1.65.2(2026年11月時点)と公式ドキュメントで確かめました。
まとめ:Playwrightスクレイピングは描画待ちと取得元の選び方で決まる
- 使う場面:JavaScriptで描画される一覧、ログイン後の画面、クリックやスクロールで追加読み込みされるページ。静的HTMLならrequestsとBeautifulSoupで足りる。
- 取得前:
urllib.robotparserで対象URLの可否とCrawl-delayを機械的に確かめ、利用規約で自動取得が禁じられていないかを人が読む。 - 待ち方:
time.sleepやnetworkidleで待たず、取りたい要素のロケーターで待つ。公式もnetworkidleを非推奨としている。 - 複数ページ:ページ送りは「次へ」が無くなったら終了、無限スクロールは「一定時間件数が増えない」を打ち切り条件にする。
- 取得元:画面が裏でJSONのAPIを呼んでいるなら、
expect_responseで応答を受け取るほうがHTML解析より壊れにくい。 - 負荷:1コンテキスト・逐次実行から始め、間隔はrobots.txtの指定と自分で決めた下限の大きいほうを取る。bot対策の回避が前提になる案件は、技術ではなく許諾の問題として扱う。
Playwrightでスクレイピングする前の判断:ブラウザの要否とrobots.txt
スクレイピングの定義や、クローリング・APIとの違い、著作権法上の整理はスクレイピングとは?クローリング・APIとの違いと実装・法務の判断で扱っています。ここでは、Playwrightを持ち出す前に確かめる2点に絞ります。
ブラウザが要るのはJavaScript描画とログイン後の画面に限るという基準
ブラウザを起動する方式は、HTTPリクエストを1本送る方式より桁違いに重くなります。判断の目安は単純で、ブラウザの「ページのソースを表示」で取りたい値が見えるなら、Playwrightは要りません。requestsで取得し、BeautifulSoupのfind・selectで値を取り出す方法のほうが速く、サーバーへの負荷も小さく済みます。
Playwrightを選ぶのは、ソースに値が無く開発者ツールの要素パネルにだけ現れるページ、ボタン操作やスクロールで追加読み込みされるページ、ログインして初めて見えるページの3種類です。数千ページ以上を定期巡回する用途なら、まずScrapyの使い方とScrapy Cloudへのデプロイ手順の枠組みを検討し、描画が必要なページだけをブラウザに回す構成にします。
urllib.robotparserによる取得可否と待機秒数の事前判定
robots.txtの読み方はRFC 9309で標準化されています。Pythonの標準ライブラリurllib.robotparserを使えば、追加のインストールなしで判定できます。
from urllib.parse import urlparse
from urllib.robotparser import RobotFileParser
USER_AGENT = "example-collector/1.0"
def check_robots(url: str):
"""取得可否とCrawl-delay(秒・未指定ならNone)を返す"""
parts = urlparse(url)
rp = RobotFileParser()
rp.set_url(f"{parts.scheme}://{parts.netloc}/robots.txt")
rp.read()
return rp.can_fetch(USER_AGENT, url), rp.crawl_delay(USER_AGENT)
allowed, delay = check_robots("https://example.com/items?page=1")
if not allowed:
raise SystemExit("robots.txtで不許可のため取得しない")
print("Crawl-delay:", delay)
crawl_delay() はPython 3.6で、site_maps() は3.8で追加されたメソッドです。robots.txtが返らなかったときの扱いも押さえておきます。CPython 3.14の RobotFileParser.read() のソースでは、401と403は全面不許可、それ以外の4xxは全面許可、5xxは全面不許可として処理されます。これはRFC 9309の「4xxなら任意のリソースにアクセスしてよい(MAY)」「5xxなら全面不許可とみなす(MUST)」に沿った実装です。
RFC 9309は、取得したrobots.txtを24時間を超えてキャッシュしないよう求めています(SHOULD NOT)。毎日動かすバッチなら、実行のたびに読み直す書き方で足ります。robots.txtは取得の可否を示すだけで、利用規約の禁止事項までは教えてくれません。規約は人が読み、書き方の基本はrobots.txtとは?クロール制御の書き方と効く場面・不要な場面で確認してください。
Python版Playwright 1.65のインストールと最小のスクレイピングコード
Playwrightの仕組み(Pythonのコードがドライバ経由でブラウザを動かす構造)はPlaywright(プレイライト)とは?仕組み・できること・Seleniumとの違いで解説しています。ここではスクレイピングに要る分だけを入れます。
pip install playwrightとChromiumのみの導入手順
python -m venv .venv
.venv/bin/activate # Windowsは .venv\Scripts\activate
pip install --upgrade pip
pip install playwright
# スクレイピングはChromiumだけで足りる
playwright install chromium
# Linuxサーバーでは依存パッケージも同時に入れる
# playwright install --with-deps chromium
python -c "import importlib.metadata as m; print(m.version('playwright'))"
公式のLibraryページの手順は引数なしの playwright install で、Chromium・Firefox・WebKitの3種を入れます。スクレイピングで複数ブラウザを使う理由は薄いので、ブラウザ名を指定してダウンロード量を抑えました。2026-11-15時点のPyPIの最新は1.65.2で、対応するPythonは3.10以上です。
公式のBrowsersページによると、ヘッドレス実行では通常のChromiumとは別の「chromium headless shell」が使われます。画面を出さないサーバー専用なら、--only-shell を付けて完全版のダウンロードを省けます。インストールで止まったときはPlaywrightのインストール方法とできない時の対処の切り分けを先に当ててください。
sync_apiで1ページを開いてタイトルとリンク一覧を取り出す最小コード
from playwright.sync_api import sync_playwright
URL = "https://example.com/"
with sync_playwright() as p:
browser = p.chromium.launch() # 既定はヘッドレス
context = browser.new_context(locale="ja-JP")
page = context.new_page()
page.goto(URL, wait_until="domcontentloaded")
print(page.title())
for link in page.get_by_role("link").all():
print(link.inner_text(), link.get_attribute("href"))
browser.close()
同期API(sync_api)は上から順に読めるので、1本のバッチを書くならこちらで十分です。asyncioで動くWebアプリやジョブ基盤に組み込む場合は、同じ構造を async_playwright と await で書きます。
動的ページの待機設計:networkidleとsleepに頼らずロケーターで待つ
スクレイピングの失敗の大半は「まだ描画されていない要素を読んで空振りする」か「念のため長く待って遅くなる」のどちらかです。待つ対象をページ全体ではなく、取りたい要素に置き換えると両方が消えます。
wait_untilの4つの値とnetworkidleが公式に非推奨とされる理由
| wait_untilの値 | 完了とみなす時点 | スクレイピングでの扱い |
|---|---|---|
| commit | 応答を受け取り文書の読み込みが始まった | リダイレクト先のURLだけ欲しいとき |
| domcontentloaded | DOMContentLoadedが発火した | 既定に推す。後はロケーターで待つ |
| load | loadイベントが発火した(既定値) | 画像の多いページで遅くなる |
| networkidle | 500ミリ秒以上通信が無い | 使わない |
公式のPageクラスのリファレンスは、networkidle に「DISCOURAGED」と明記しています。広告やアクセス解析が定期的に通信し続けるページでは、500ミリ秒の静寂が来ずに既定の30秒で TimeoutError になるからです。逆に通信が早く途切れると、一覧の描画前に完了扱いになることもあります。
一覧の1件目が表示されるまでロケーターで待ってから全件を読む書き方
page.goto(LIST_URL, wait_until="domcontentloaded")
items = page.locator("ul.results li")
items.first.wait_for(timeout=15_000) # 1件目が表示されるまで最大15秒
rows = items.evaluate_all(
"""els => els.map(e => ({
title: e.querySelector('h3')?.innerText.trim() ?? '',
url: e.querySelector('a')?.href ?? ''
}))"""
)
print(len(rows), rows[:3])
wait_for() の既定は「表示されるまで待つ」です。件数が多い一覧では、1件ずつ inner_text() を呼ぶとPythonとブラウザの往復が件数分発生します。evaluate_all でブラウザ側でまとめて辞書にすれば、往復は1回で済みます。
クリックや入力の前は、Playwrightが自動で待ちます。公式のAuto-waitingページによると、クリック前には要素が表示されていること、2フレーム続けて位置が動いていないこと、他の要素に覆われていないこと、無効化されていないことが確かめられます。読み取りの前だけ、自分で待つ対象を指定すればよいわけです。
ページ送りと無限スクロールで複数ページを取得するループの組み方
一覧が1画面で終わることはまれです。どちらの形式でも、ループの打ち切り条件を先に決めてから書くと、終わらないジョブを作らずに済みます。
次へリンクが消えたら終了するページ送りのループとURL変化の待ち方
import re
import time
MAX_PAGES = 50
results = []
for page_no in range(1, MAX_PAGES + 1):
page.locator("ul.results li").first.wait_for()
results += page.locator("ul.results li h3").all_inner_texts()
next_link = page.get_by_role("link", name="次へ")
if next_link.count() == 0:
break # 最終ページ
next_link.click()
page.wait_for_url(re.compile(rf"[?&]page={page_no + 1}\b"))
time.sleep(interval) # 間隔の決め方は後述
クリック直後に次の一覧を読むと、前のページの要素を読んでしまう場合があります。URLにページ番号が入るサイトなら、wait_for_url でURLの変化を確かめてから読むのが確実です。MAX_PAGES は、サイト側の不具合で「次へ」が消えない場合の安全弁として必ず置きます。
ロケーターは同じ条件に複数の要素が当たると、クリックの時点で例外を投げます。公式のLocatorsページがstrictと呼ぶ挙動で、上下2か所に「次へ」があるサイトでは get_by_role("navigation") で範囲を絞ってから指定します。
無限スクロールは一定時間件数が増えなければ末尾とみなす打ち切り条件
import time
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
MAX_SCROLLS = 30
cards = page.locator("div.card")
cards.first.wait_for()
for _ in range(MAX_SCROLLS):
before = cards.count()
page.mouse.wheel(0, 4000)
try:
page.wait_for_function(
"n => document.querySelectorAll('div.card').length > n",
arg=before,
timeout=8_000,
)
except PlaywrightTimeoutError:
break # 8秒増えなければ末尾
time.sleep(interval)
titles = cards.all_inner_texts()
無限スクロールには「最後のページ」という目印がありません。件数が増えるのを待ち、増えなければ終わりとみなすのが実務での定番です。8秒は応答の遅いサイトに合わせて調整してください。数万件が積み上がるページでは、ブラウザのメモリが先に尽きます。その場合は次の章の方法で、画面ではなく裏のAPIの応答を読みます。
画面ではなく通信を読む:expect_responseでJSONを直接受け取る方法
動的ページの多くは、ブラウザがJSONのAPIを呼び、その結果を画面に描いています。開発者ツールのネットワークパネルで「Fetch/XHR」を絞り込み、一覧の元データらしき応答があれば、HTMLを解析する必要はありません。
expect_responseでAPI応答のJSONを受け取りHTML解析を省く実装
with page.expect_response(
lambda r: "/api/items" in r.url and r.request.method == "GET" and r.ok
) as resp_info:
page.get_by_role("button", name="もっと見る").click()
data = resp_info.value.json()
for item in data.get("items", []):
print(item["id"], item["name"])
公式のNetworkページが示す expect_response は、ブロック内の操作で発生した応答のうち条件に合う最初の1件を待って返します。画面のクラス名はデザイン変更で頻繁に変わりますが、APIの項目名はそれより変わりにくく、取得コードの寿命が延びます。
ただし、このAPIを直接requestsで叩く方式に切り替えるかは別の判断です。認証トークンや署名パラメーターが画面のJavaScriptで組み立てられていることが多く、公開APIとして提供されていないエンドポイントへの直接アクセスは、利用規約上も扱いが分かれます。ブラウザで画面を操作し、その結果として届いた応答を読む範囲にとどめるのが安全です。
routeで画像・フォント・動画を止めて転送量と取得時間を減らす設定
BLOCK = {"image", "font", "media"}
context.route(
"**/*",
lambda route: route.abort()
if route.request.resource_type in BLOCK
else route.continue_(),
)
page = context.new_page()
テキストだけを集めるなら、画像とフォントの読み込みは不要です。route をコンテキストに設定すると、そのコンテキストで開くすべてのページに効きます。取得先のサーバーにとっても、転送量が減るぶん負荷が下がります。画像の有無で一覧の描画が変わるサイト(遅延読み込みの判定に画像を使う等)では、件数が減っていないかを最初の1回で確かめてください。
ログイン後のページと負荷対策:storage_state・取得間隔・並列数の決め方
自社の管理画面や契約しているSaaSから定期的にデータを書き出す用途では、ログインと取得間隔の扱いが品質を左右します。
storage_stateでログイン状態を保存して毎回のログイン操作を省く手順
import os
from pathlib import Path
from playwright.sync_api import sync_playwright
STATE = Path("playwright/.auth/state.json") # .gitignoreに追加しておく
with sync_playwright() as p:
browser = p.chromium.launch()
if STATE.exists():
context = browser.new_context(storage_state=str(STATE))
else:
context = browser.new_context()
page = context.new_page()
page.goto(LOGIN_URL)
page.get_by_label("メールアドレス").fill(os.environ["SITE_USER"])
page.get_by_label("パスワード").fill(os.environ["SITE_PASS"])
page.get_by_role("button", name="ログイン").click()
page.wait_for_url("**/mypage**")
STATE.parent.mkdir(parents=True, exist_ok=True)
context.storage_state(path=str(STATE))
公式のAuthenticationページは、この状態ファイルにはなりすましに使えるCookieやヘッダーが含まれうるとして、公開・非公開を問わずリポジトリに入れないよう強く求めています。ID・パスワードもコードに直接書かず、環境変数やシークレット管理から渡す形にしてください。セッションの期限が切れるとログイン画面に飛ばされるので、取得前にログイン後にしか無い要素があるかを確かめ、無ければ状態ファイルを消して取り直す分岐を入れておきます。
取得間隔はCrawl-delayと自前の下限の大きいほうを取り逐次で始める
import csv
MIN_INTERVAL_SEC = 3.0
allowed, delay = check_robots(LIST_URL)
interval = max(delay or 0, MIN_INTERVAL_SEC)
# ...ページ送りのループで rows を集める...
with open("items.csv", "w", newline="", encoding="utf-8-sig") as f:
writer = csv.DictWriter(f, fieldnames=["title", "url"])
writer.writeheader()
writer.writerows(rows)
本記事のコードでは、robots.txtに Crawl-delay があればその秒数を、無ければ3秒を間隔の下限にしています。3秒は法令や規格の数値ではなく、相手のサーバーに人の閲覧と同程度の負荷しかかけないための自前の線です。CSVは utf-8-sig で書き出すと、Excelで開いても文字化けしません。
並列化を検討する順番は、逐次での取得を確認したあとの最後です。公式のLibraryページは、PlaywrightのAPIはスレッドセーフではなく、スレッドごとにPlaywrightのインスタンスを作るよう求めています。asyncioで複数ページを同時に開く書き方もできますが、並列数を増やせば相手から見た負荷もその倍数になります。1コンテキスト・逐次で必要な時間内に終わるなら、並列化はしません。
Playwrightでスクレイピングを組むか見送るか:採用条件と断る案件
ここまでのコードで動くことと、業務で回し続けてよいことは別です。受託でデータ収集の仕組みを組む立場から、線を引いておきます。
Playwrightの採用とrequests・Scrapy・Seleniumへの切替基準
Playwrightを選ぶのは、取得対象がJavaScript描画で、1回の取得が数百ページ程度に収まり、画面のクリックやログインを含む場合です。この条件ならコード量が少なく、待機を自動で処理してくれる利点がそのまま効きます。
戻す場面も明確です。ページのソースに値があるならrequestsとBeautifulSoupに戻します。数万ページを毎日巡回するならScrapyを主にし、描画が要るページだけブラウザに渡します。既存の資産がSeleniumで書かれ、動いているなら、移行する理由は待機処理の書き直しで工数が減る場合に限られます(Seleniumの使い方とWebDriverWaitによる待機を参照)。独自ライセンスのライブラリまで候補に入れるなら、DrissionPageの使い方と商用利用の条件のようにライセンスの確認が先に要るものもあります。
bot対策の回避が前提の案件は技術でなく許諾の問題として扱う
見送るべき失敗パターンは、取得先がCAPTCHAやbot判定サービスで自動アクセスを拒否しているのに、プロキシの切り替えやユーザーエージェントの偽装で突破しようとする案件です。相手が明示的に拒否している以上、これは技術の工夫ではなく、利用規約違反や業務妨害が問われうる法務の論点として扱う必要があります。取得が止まるたびに回避策を足す保守も続き、費用は膨らむ一方です。
この場合は、相手先にAPIの提供やデータ連携の契約を打診するのが先です。社内外のデータを集めて分析に回す仕組みを、取得元との取り決めから設計し直したい場合は、データ分析基盤構築・MLOps構築支援のように、収集・蓄積・分析をまとめて設計できる体制に相談すると、スクレイピングに頼らない経路も含めて比較できます。取得コードを定期実行する環境をコンテナで組む場合は、外部サイトを開くときの pwuser とseccompの設定をPlaywright Docker実行ガイドで確認してください。
よくある質問
Playwrightでスクレイピングを組むときに出やすい疑問をまとめました。
PlaywrightとSeleniumはスクレイピングにどちらが向いていますか?
新しく書くならPlaywrightが扱いやすいです。クリックや入力の前に表示・安定・覆われていないことを自動で確かめるため、Seleniumで WebDriverWait を書き足していた待機処理の多くが不要になります。expect_response で通信の応答を直接受け取れる点も、データ取得では効きます。Seleniumで動いている既存のコードがあり、保守に困っていないなら、取得のためだけに移行する必要はありません。
Playwrightを使ったスクレイピングは違法になりますか?
使うツールで違法かどうかが決まるわけではありません。論点になるのは、取得先の利用規約、著作物の利用目的、サーバーへの負荷、個人情報の扱いです。robots.txtで不許可の範囲を取らない、規約で禁止されているサイトから取らない、間隔を空けて逐次で取得する、の3点は最低限守ってください。著作権法30条の4などの整理はスクレイピングとは?クローリング・APIとの違いと実装・法務の判断で解説しています。
BeautifulSoupと組み合わせて使えますか?
BeautifulSoupと組み合わせて解析する使い方も可能です。Playwrightで描画を待ったあと page.content() で描画後のHTMLを文字列として取り出し、BeautifulSoupに渡せば、慣れた select の書き方で解析できます。ただし、ロケーターと evaluate_all で必要な値だけをブラウザ側で取り出すほうが、HTML全体を渡すより速く、ライブラリも1つで済みます。既存の解析コードを流用したい場合に限って組み合わせるとよいでしょう。
ヘッドレスだと取れないのに画面を出すと取れるのはなぜですか?
原因は2つに分かれます。1つは画面サイズで、ヘッドレスの既定のビューポートではスマートフォン向けの表示に切り替わり、クラス名や要素の並びが変わるケースです。new_context(viewport={"width": 1280, "height": 800}) で幅を固定すると解消します。もう1つは取得先がヘッドレスブラウザを自動アクセスとして拒否しているケースで、この場合は回避策を探さず、相手先にAPIや許諾を求めるのが筋です。
PythonとNode.jsのどちらで書くべきですか?
集めたデータをpandasで加工したり、機械学習やデータ基盤に流したりするならPythonが向いています。APIはほぼ同じで、本記事のコードはNode.jsでも同じ構造で書けます。Node.jsを選ぶのは、取得処理をE2Eテストと同じリポジトリで管理したい場合や、既存のバックエンドがNode.jsの場合です。Pythonで使う場合、2026年11月時点のPyPIの最新は1.65.2、対応するPythonは3.10以上です。
関連記事
- スクレイピングとは?クローリング・APIとの違いと実装・法務の判断を解説:取得してよい範囲と法務の整理、APIとの使い分け
- Playwright(プレイライト)とは?仕組み・できること・Seleniumとの違いを解説:ブラウザ・コンテキスト・ページの関係と自動待機の仕組み
- Playwrightのインストール方法とできない時の対処|バージョン確認・Windows・Python:pip installやブラウザのダウンロードで止まったときの切り分け
- BeautifulSoupとは?pip installと使い方(find・select)・文字化け対策を解説【bs4 4.15】:ブラウザが要らない静的ページの解析
- DrissionPageの使い方|Pythonでの入力・iframe・通信取得と5.0ベータ・商用利用の条件:通信の直接受信ができる別のブラウザ操作ライブラリとの比較