Playwrightで7サービスを自動操作して詰まった箇所(実運用の記録)

Playwrightで7サービスを自動操作して詰まった箇所(実運用の記録)

note・X・Instagram・Bluesky・Tumblr・Amazonアソシエイト・YouTube の7サービスをPlaywrightで自動操作しています。この記事では、実際に運用して詰まった4つの箇所(headless、セレクタの部分一致、仕様変更の見逃し、無限スクロールの終了条件)と、その対処を記録として残します。

headless=True のときだけログイン画面に飛ばされた

Amazonアソシエイトの成果レポート取得が4回連続で失敗しました。ログには毎回『ログインが切れています』と出ていて、素直に読めばセッション切れです。

ところが実際にはログインは切れていませんでした。headless=True のときだけログイン画面に強制送還されていたのです。同じブラウザプロファイルのまま headless=False にすると、成果ページがそのまま開きました。プロファイルもCookieも一切触っていません。変えたのは headless のフラグだけです。

なぜヘッドレスだと弾かれるのか、その仕組みまでは分かっていません。分かったのは「ログが言っている原因と、実際の原因が違うことがある」という一点だけです。

失敗したこと

ログの『ログインが切れています』を信じて、再ログイン処理の側ばかり4回いじっていました。切り分けとして最初に headless=False で同じ処理を走らせていれば、1回目で気づけたはずです。

has-text() は部分一致。掴んではいけないボタンを掴む

Playwrightの has-text() は部分一致です。ここで実害が2回出ました。

  • button:has-text('保存') が『下書き保存』を掴んだ
  • has_text('追加') が『追加済』を掴み、再実行で登録を解除してしまった

特に後者が厄介でした。「追加する」処理のつもりが、2回目の実行ではすでに追加済みのものを外して回る動きになります。エラーは一切出ません。

:text-is() では解決しなかった

完全一致なら :text-is() ですが、これは子要素の文字を見ません。アイコン付きボタンのように、テキストが内側の要素に入っている構造では引っかからず、使えませんでした。

最終的に、候補を列挙して inner_text().strip() で完全一致を取る方式に落ち着いています。セレクタ一発で決めるのをやめ、取ってから絞るという順番に変えた形です。

注意

部分一致セレクタの怖いところは、間違った要素を掴んでも処理は成功することです。「追加」が「追加済」を掴んでも、クリックは成功しログにも成功と残ります。

押せているのに解除されない。3日間気づけなかった

noteのフォロー解除が、確認ダイアログからドロップダウンメニューに仕様変更されていました。こちらは変わる前の流れのまま動いているので、押せてはいるものの解除はされません。

そしてエラーにならないため、処理は毎回「成功」で終わります。3日間、1件も解除されていないことに気づけませんでした。

原因は自分のログの書き方

ログには『対象なし』とだけ書いていました。これだと「本当に対象が0件だった日」と「掴めていないだけの日」が区別できません。0件という数字で残していれば、3日連続0という並びで気づけたはずです。文字列ではなく数値でログを残す、というのがここでの教訓です。

ポイント

自動化の失敗は、落ちてくれる失敗より成功で終わる失敗のほうが危険です。「何件処理したか」を必ず数値で出し、0が続いたら疑う。それだけで発見までの日数が変わります。

無限スクロールの終了条件を間違えて2時間走り続けた

無限スクロールのページで、終了条件を「ページの高さが変わらなくなったら終了」にしていました。結果、2時間の実行上限まで走り続けました。高さが期待どおりに止まってくれないと、判定は永遠に成立しません。

これを「新しい対象が一定回数見つからなかったら終了」に直したところ、36分で正常終了しました。見るべきはページの高さではなく、こちらが欲しい対象が増えているかどうかでした。

無限スクロールの終了条件を変えた結果
無限スクロールの終了条件を変えた結果

プロファイル706MBを持ち運ぶのをやめた

ログイン状態を保持しているブラウザプロファイルは706MBありました。別サーバーへ運ぶには重すぎます。

storage_state を使うと、CookieとlocalStorageだけを4KBのJSONとして書き出せます。運びたかったのは結局この部分だけでした。

実測

706MB → 4KB
プロファイル全体ではなく storage_state で必要な分だけ書き出した場合
うまくいったこと

ログイン状態の受け渡しが4KBのJSONで済むようになり、プロファイルごとコピーする必要がなくなりました。

7サービス動かして分かったこと

  • ログの文言より、条件を1つだけ変えた再現テストのほうが早い(headless切り替え)
  • テキスト一致は inner_text().strip() で自前に取る
  • 件数は数値でログに残す
  • ログイン状態の持ち運びは storage_state
  • headless で弾かれる仕組みそのものは解明できていない
  • :text-is() はアイコン付きボタンには使えない
  • サービス側の仕様変更を事前に検知する方法は、まだ持てていない
まとめ

  • Amazonアソシエイトの4連続失敗は、ログの『ログインが切れています』ではなく headless=True が原因だった。headless=False では同じプロファイルで開けた
  • has-text() は部分一致。『保存』が『下書き保存』を、『追加』が『追加済』を掴み、再実行で登録解除まで起きた
  • :text-is() は子要素の文字を見ないので、候補を列挙して inner_text().strip() で完全一致させる方式にした
  • noteの仕様変更はエラーにならず、『対象なし』としか書いていなかったため3日間気づけなかった。0件は数字で残す
  • 無限スクロールの終了条件を高さ基準から「新しい対象が見つからない回数」に変えたら、上限2時間から36分になった
  • 706MBのプロファイルを運ぶ代わりに、storage_state で4KBのJSONにした
広告