Claude CodeでPlaywrightのダウンロードを自動化 — 請求書PDFの待ち方と保存名
expect_downloadでダウンロード完了を待ち、保存名を規則化して請求書PDFを集めるスクリプトを、認証情報をClaude Codeに渡さない権限設計と合わせて示します。
取引先の管理画面から毎月の請求書PDFを集める作業は、Playwrightのダウンロード処理に置き換えられます。要になるのは、クリックの直前に待機を始めるexpect_downloadと、サーバーが付けた名前をそのまま信じない保存名の規則です。
Claude Codeに書かせるときは、もう1つ決めることがあります。ログイン情報を読ませない設定と、スクリプトが書き込める場所の制限です。この記事ではPython版を例に、ダウンロードの待ち方、保存名の付け方、権限設計の順に組み立てます。
ダウンロードはクリックの前から待つ
Playwrightは、ページがダウンロードを発生させるたびにdownloadイベントを出します。ファイルは一時フォルダに落ち、イベントからDownloadオブジェクトを受け取れます。URL、ファイル名、中身のストリームはこのオブジェクトから取ります。
最も素直な書き方は、expect_downloadをコンテキストマネージャーとして使う形です。
with page.expect_download() as download_info:
page.get_by_text("Download file").click()
download = download_info.value
download.save_as("/path/to/save/at/" + download.suggested_filename)ポイントは順序です。待機を先に始めてから、ダウンロードを起こす操作をブロックの中に書きます。クリックしてから待ち始めると、イベントが先に出てしまう可能性があります。
expect_downloadは、操作を実行して新しいダウンロードを待つメソッドです。ページがダウンロード開始前に閉じられると、エラーを投げます。predicateを渡せば、受け取ったDownloadが条件を満たすまで待てます。請求書一覧に複数のリンクがあり、別のファイルも同時に落ちる画面では、この条件が選別に使えます。
同時に落ちるファイルを選り分ける
画面によっては、1回のクリックで請求書PDFと明細CSVのように複数のファイルが落ちます。predicateに条件を渡すと、条件が真になるダウンロードだけを受け取れます。
with page.expect_download(
predicate=lambda d: d.suggested_filename.lower().endswith(".pdf")
) as info:
row.get_by_role("link", name="一括取得").click()
pdf = info.value条件に合わないダウンロードは待機の対象から外れるだけで、止める処理ではありません。拡張子で選り分けても、取り逃したファイルが必要ならイベントハンドラー方式との併用を検討します。
save_asは完了を待ってくれる
save_asは、ダウンロードを指定のパスへコピーします。ダウンロード中に呼んでも安全で、必要なら完了まで待ちます。つまり、保存の前に自前で完了待ちを書く必要はありません。
失敗の確認にはfailure()を使います。エラーがあれば文字列、なければNoneを返し、これも必要なら完了を待ちます。path()は成功したダウンロードのパスを返しますが、失敗や中断では例外になります。ファイル名はランダムなGUIDなので、人が読む名前にはsuggested_filenameを使います。
イベントハンドラー方式は業務スクリプトに向かない
何がダウンロードを起こすのか分からないときは、page.on("download", ...)でも受けられます。ただしPlaywrightの説明では、この方式は制御の流れを分岐させ、スクリプトを追いにくくします。ダウンロード中に本流が終わってしまう場面もありえます。
請求書の収集は、「この行のこのボタンを押すと、このファイルが落ちる」と対応が分かっている作業です。expect_downloadで1件ずつ待つ形が向いています。
保存名を規則化する
suggested_filenameは、サーバーが提案する名前です。取引先ごとにinvoice.pdfやdownload.pdfのように同じ名前が返ると、そのまま保存して上書きが起きます。そこで、保存名はスクリプト側の規則で組み立てます。
次の形にそろえると、月次の突合や再実行の確認が楽になります。
| 要素 | 例 | 役割 |
|---|---|---|
| 対象月 | 例2026-09 | 役割並べ替えと月次の取りまとめ |
| 取引先の識別子 | 例acme | 役割取引先ごとの絞り込み |
| 請求番号 | 例INV-0042 | 役割突合のキー |
| 拡張子 | 例.pdf | 役割suggested_filenameから取得 |
import re
from pathlib import Path
OUT_DIR = Path("downloads/invoices")
def safe(text: str) -> str:
return re.sub(r"[^A-Za-z0-9_-]", "_", text)
def save_invoice(download, month: str, vendor: str, number: str) -> Path:
suffix = Path(download.suggested_filename).suffix or ".pdf"
dest = OUT_DIR / f"{month}_{safe(vendor)}_{safe(number)}{suffix}"
OUT_DIR.mkdir(parents=True, exist_ok=True)
if dest.exists():
return dest # 取得済みの月はスキップして再実行を安全にする
download.save_as(dest)
return destサーバー由来の文字列をそのままパスに使わないのが、この関数の設計の核です。suggested_filenameに../のような区切りが含まれていても、保存名は月、取引先、請求番号と拡張子だけで決まります。画面から読み取った取引先名や請求番号も、safeで英数字と-_に落としてから使います。
存在確認を入れると、途中で止まった月次処理を再実行しても二重に保存しません。内容の同一性までは見ないため、同じ請求番号で再発行された請求書を取り直したいときは、削除してから再実行する運用になります。
一覧画面を回す全体像
行ごとにリンクを押す部分は、取引先の画面に合わせて変わります。骨格は次のとおりです。
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(downloads_path="tmp/pw-downloads")
context = browser.new_context(storage_state="playwright/.auth/vendor.json")
page = context.new_page()
page.goto("https://billing.example.com/invoices")
for row in page.locator("table.invoices tbody tr").all():
number = row.locator("td.number").inner_text()
month = row.locator("td.month").inner_text()
with page.expect_download() as info:
row.get_by_role("link", name="PDF").click()
download = info.value
if download.failure():
print(f"失敗: {number}")
continue
save_invoice(download, month, "acme", number)
context.close()
browser.close()セレクターとURLはダミーです。実際の画面に合わせた調整は、Claude Codeに実画面の構造を見せながら進めるのが早い部分です。見せる範囲は、後の「認証情報をClaude Codeに渡さない構成」で決めます。
ダウンロードの置き場所と寿命
ダウンロードされたファイルは、いったん一時フォルダに入ります。launch()のdownloads_pathで、その一時的な置き場所を指定できます。ただし、ブラウザコンテキストを閉じると、そのコンテキストで落としたファイルは削除されます。
この寿命が、先のsave_asを必須にしています。context.close()の後でパスを参照しても、ファイルは残っていません。業務スクリプトでは、閉じる前にすべてOUT_DIRへコピーしておく構成にします。
書き込み先を1か所に固めると、次の節の権限設計もしやすくなります。一時置き場と保存先を別のディレクトリにしておけば、「保存先だけを人が確認する」運用が成り立ちます。
認証情報をClaude Codeに渡さない構成
請求書の管理画面にはログインが要ります。この記事のスクリプトは、パスワードを直接扱わず、ログイン済みの状態ファイルを読む形にしています。状態ファイルの作り方と、読ませないための考え方はClaude CodeでPlaywrightのログイン状態を使い回すにまとめています。ここでは請求書の収集に必要な設定だけを置きます。
プロジェクトの.claude/settings.jsonに、状態ファイルと.envの読み取りを拒否するルールと、スクリプト実行の許可を書きます。
{
"permissions": {
"allow": [
"Bash(python scripts/fetch_invoices.py *)"
],
"deny": [
"Read(./.env)",
"Read(./playwright/.auth/**)"
]
}
}Readのdenyルールは、Claudeの組み込みのファイルツールを止めます。Read(./.env)やRead(./secrets/**)のようにパスを指定する形は、権限の説明に例として載っています。さらにReadのdenyは、同じパスへのEditとWriteも止め、新しいファイルの作成も含みます。
許可側のBashルールは、コマンド全体の文字列に対するパターンです。Bash(python scripts/fetch_invoices.py *)は、そのスクリプトを引数付きで実行する呼び出しだけを承認します。&&や;でつないだ別のコマンドは、それぞれ個別に判定されます。
権限ルールが止めるもの、止めないもの
ここで注意が要ります。ReadとEditのdenyルールは、組み込みのファイルツールと、catやsedのようにClaude Codeが認識するファイルコマンド、リダイレクトの対象を扱います。PythonやNodeのスクリプトが自分でファイルを開く動作は、対象外と説明されています。
つまり、Read(./playwright/.auth/**)を拒否しても、許可したfetch_invoices.py自体は状態ファイルを読めます。それが狙いなので問題ありません。逆に言えば、Editの許可範囲をdownloads/に絞っても、スクリプトの書き込み先は制限されません。スクリプトがdownloads/invoices以外へ書かないことは、コードの側で保証します。上のOUT_DIR固定とsafeによる名前の無害化が、その役目です。
OSレベルで、すべてのプロセスから特定のパスを遮断したい場合は、サンドボックスを有効にします。権限は「Claudeがどのツールで何をできるか」、サンドボックスは「実行されたコマンドが何に触れられるか」を受け持つ、別の層です。
ネットワークの出口も絞る
請求書の取得先は決まっています。Bashのdenyでcurlやwgetを止める方法もありますが、同じプログラムを別の形で呼ぶ場合は一致しません。確実に絞る必要があるなら、サンドボックスのネットワーク許可リストと組み合わせます。許可リストに請求書の取得先ドメインだけを置けば、スクリプトの通信先もそこに限られます。
つまずきやすい点
落とし穴
クリック後にexpect_downloadを書いて、待機が間に合わない
ダウンロードを起こす操作をwith page.expect_download()のブロックの外に書くと、イベントが待機の開始より先に出る場合があります。操作は必ずブロックの内側に置きます。
- コンテキストを閉じた後にファイルがない: 一時フォルダのファイルは、コンテキストを閉じると削除されます。
save_asは閉じる前に済ませます。 - 画面が新しいタブでPDFを開くだけでダウンロードにならない: この場合は
downloadイベントが出ず、expect_downloadがタイムアウトする可能性があります。ボタンの挙動は、ブラウザで一度手動で確かめておきます。 - 失敗が見えない:
failure()がNoneでない行は、取得に失敗したものとして記録します。件数の突合は、保存した件数と画面の行数を比べるだけで足ります。
Claude Codeに書かせる指示の例
実画面を見ながら調整する段階では、指示に範囲を書き込むと安定します。
scripts/fetch_invoices.py を作って。
- expect_download のブロック内でリンクをクリックする
- 保存名は 対象月_取引先_請求番号.拡張子 に固定し、保存先は downloads/invoices のみ
- .env と playwright/.auth は読まない。ログインは storage_state のパスだけを使う
- 実行後、保存した件数と失敗した請求番号を表示する最初は一覧の1行だけで実行させ、保存名と保存先が意図どおりか確かめてから全件に広げると、取り直しの手間を抑えられます。実行結果は、保存した件数と失敗の一覧を読ませます。取得した請求書の突合は別の工程で、入金データとの照合は入金消込をClaude Codeで自動化する手順から続けられます。Playwrightのスクリプトをhookから自動で走らせる設計は、PlaywrightのE2Eテストをhookで自動実行する記事と同じ考え方で組めます。
業務スクリプトとして運用する前に
取引先の規約が自動取得を禁じていないか、画面の操作を代行してよいかは、スクリプトの外の確認事項です。コードでは判断できません。管理画面にAPIやメール配信による請求書の受け取りがあるなら、そちらを先に検討する価値があります。
そのうえで画面操作が必要なら、次の3点を満たしておくと運用できます。
- ダウンロードの待機は
expect_downloadで1件ずつ行う - 保存名と保存先はスクリプト側の規則で固定する
- 認証情報はdenyルールで読ませず、状態ファイルのパスだけを渡す
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。