この記事で切り分けられること
Playwright で持続プロファイル(launch_persistent_context)を使っていて、以下が同時に起きている場合の切り分けです。
- 本人が「ログインしている」と言っているのに、Playwright で開くと未ログイン扱いになる
- 複数サイトのうち、エンジンによって見えるログインと見えないログインが分かれる
TargetClosedError: Target page, context or browser has been closedで処理が落ちる
症状
別々の不具合だと思っていたが、実際は1つの原因から出ていた3つの症状だった。
- 盤面確認・ログイン確認・応募の処理が
TargetClosedErrorで落ちる。プロファイルが壊れたように見えた - 本人が「クラウディアはログインしているので見てください」と言っているのに、機械で開くと未ログイン
- ココナラも同じ。本人はログイン済み、機械は未ログイン
原因
本人がログインする窓と、機械が読む窓が、同じプロファイルフォルダを指しながら別のブラウザエンジンで動いていた。
- 本人の窓(
open_login_browser.py)はchannelの指定なしでp.chromium.launch_persistent_context(...)を呼んでいた。これは Playwright 同梱の素の Chromium - 読む側(
run_auto_applyrun_proposal_boardverify_apply_loginほか複数)はchannel="msedge"で実の Edge を使っていた
Chromium と Edge は Cookie の暗号鍵(Local State の os_crypt.encrypted_key)が別物。同じフォルダを指していても、片方が入れたログインはもう片方から復号できない。
実測では、Chromium で開くと2サイトが未ログイン、Edge で開くと別の2サイトが未ログイン。どちらのエンジンで開いても全サイト分は揃わない状態だった。
TargetClosedError の方は別の原因。open_login_browser.py は窓が閉じるまで待つ仕様なので、素の Chromium の窓を開いたままにしていると、そのプロセスがプロファイルを掴み続ける。Edge 側は同じフォルダを開けずに即終了していた。実測で掴んでいたのは特定の1プロセス(開始時刻が記録に残っていた)で、Last Browser にも Playwright 同梱 Chromium の実行ファイルパスが残っていた。
切り分け方(遠回りした所)
最初は「Edge のバージョンが古くてプロファイルを拒否している」と考え、プロファイル内の Last Version と Edge の実バージョンを比較した。結果は Edge の方が新しく、この筋は最初から外れていた。
決め手になったのは、プロファイル内の Cookie ファイルをコピーしようとした時の結果だった。
cp "<あなたのプロファイルパス>/Default/Network/Cookies" /tmp/check.db
# => cp: cannot open ... Device or resource busy
Device or resource busy は、誰かがそのファイルを掴んでいる合図。ブラウザが開けない・ログインが消えたように見える時は、バージョンを疑う前に、まず掴んでいるプロセスを探すべきだった。
Get-CimInstance Win32_Process -Filter "Name='msedge.exe' OR Name='chrome.exe'" |
Where-Object { $_.CommandLine -notmatch '--type=' } |
Select-Object ProcessId, CreationDate, CommandLine
--type= を含む子プロセスを除くと、プロファイルを実際に掴んでいる親プロセスだけが残る。
直し方
scripts/lib/apply_browser.py を新規に作り、持続プロファイルを開く処理を1箇所にまとめた。
launch()/holders()/assert_free()の3つを用意- エンジンは実 Edge(
CHANNEL = "msedge")に統一。本人の窓も機械も同じ Cookie を見るようにした holders()は上記のGet-CimInstance Win32_Process相当でプロファイルを掴んでいるプロセスを返す。取得できなければ空を返して処理は止めない- 掴まれていたら
SystemExitで、誰が掴んでいるかを日本語で1行出す。原因は本人の操作であることが多いので、追跡情報より読めることを優先した
from scripts.lib.apply_browser import launch, assert_free
assert_free(profile_dir) # 掴まれていればここで日本語1行を出して止まる
context = launch(profile_dir)
あわせて次の修正を行った。
open_login_browser.pyを実 Edge に変更し、assert_free()を先に通すようにした。画面には「閉じるまで、機械はこのプロファイルを使えません」と表示するrun_proposal_board.pyとverify_apply_login.pyをlib.apply_browser.launchを呼ぶ形に寄せた
再発防止
- 持続プロファイルを開く処理を自分で書かない。必ず
lib.apply_browser.launch()を呼ぶ。エンジンを変える時はCHANNELの1行だけを変えれば、本人の窓と機械の窓が同時に揃う - 残りのスクリプト(
run_auto_applyrun_import_from_urlrun_job_hunterrun_reply_checkrun_cw_portfoliorun_x_metricsrun_youtube_uploadupdate_x_header)も順次この入口に寄せる。X と YouTube は Google のログインが絡むので、寄せる時に1本ずつログインを確認してから進める - 「ログインした」は本人の申告ではなく
verify_apply_login.pyの出力で確かめる。エンジンが違えば、本人の申告が正しくても機械側にはログインが無い
それでも駄目なときに次に見る場所
| 症状 | 先に疑ったこと | 実際に効いた確認 |
|---|---|---|
| 機械だけ未ログイン | Edge のバージョンが古い | Last Version と実バージョンの比較(今回は外れ)より、本人の窓と機械の窓で channel が同じか |
| TargetClosedError で落ちる | プロファイル破損 | cp で Cookie ファイルを開き、Device or resource busy が出るか |
| 誰が掴んでいるか不明 | 再起動して様子を見る | Get-CimInstance Win32_Process で --type= を除いた親プロセスを確認 |
プロファイルが壊れたように見える時、まずエンジンの一致(Chromium か Edge か)とプロセスの掴み合いの2点を確認すると、原因の切り分けが早い。

Playwright でログインできない原因を切り分ける Chrome の既定プロファイル操作が塞がれた場合の対処
note自動投稿で記号が残る問題を正規表現の処理順序から直す方法
noteの投稿自動化はどこまでやっていいか、公開前の確認で線を引く考え方