※ この記事にはアフィリエイトリンクが含まれています。リンク経由で購入しても読者の皆さんに追加費用は発生しません。収益は本サイトの運営費に充てています。
Playwrightの入門記事を読んで「これなら自分でも書けそう」と思った方、いませんか。僕もそうでした。でも実際にブラウザ自動化のシステムを組んでみると、入門記事には出てこない壁がいくつもあります。クリックしたのに30秒固まる、たまにSSLエラーで落ちる、ログインフォームが見つからない。よくある現象ばかりです。
この記事では、僕がアフィリエイトリンクの自動取得とInstagramのプロフィール自動化を作る過程で踏んだ罠を、6つに整理してまとめました。原因と回避策をコード付きで紹介します。これから書く方が同じ場所で詰まらないように、できるだけ平易に書いたつもりです。
こんな方に読んでほしい
- Playwrightのチュートリアルは触ったが、実用で動かそうとすると詰まる
- 「タイムアウト」や「セレクタが見つからない」エラーで時間を溶かしている
- SNSやASP(広告主と提携するサービス)の管理画面を自動化したい
- 「自動化していい範囲」の線引き基準を知っておきたい
事前に押さえておきたい用語
- Playwright:Microsoftが作ったブラウザ自動操作ライブラリ。Chromeなどを裏で動かしてクリックや入力ができる
- セレクタ:HTMLの中から「このボタン」を指定するための文字列(CSSセレクタなど)
- ASP:アフィリエイトを仲介するサービス。A8.netやもしもアフィリエイトなど
- SSLエラー:ブラウザがサイトとの暗号化通信に失敗するエラー全般

罠1|Playwrightの「自動待機」を信用しすぎると本番で詰まる
Playwrightの売りの1つが「自動で待ってくれる」ことです。ボタンをクリックする命令を出すと、要素が画面に出るまで待ってからクリックしてくれる。便利です。ただ、外部のサービス、特に広告計測タグが大量に動くサイトでは、この自動待機が想定通りに動きません。
そもそも「ページ読み込み完了」って何で決まる?
ブラウザは1つのページを開く間に何種類ものイベントを発火します。Playwrightはそのうちどれを「読み込み完了」とみなすかをwaitUntilオプションで指定できます。指定できる値は4つで、それぞれ厳密度が違います。
| 値 | 「完了」とみなす条件 | 使いどころ | ASP管理画面での挙動 |
|---|---|---|---|
load |
画像など全リソース読み込み完了 | 静的サイト | 広告タグで30秒タイムアウト頻発 |
domcontentloaded |
HTML本体のパース完了 | 多くのサイトで標準 | 動くが、操作直後にエラーが出やすい |
networkidle |
500msの間ネットワーク無通信 | SPAサイト | 計測タグが裏で送信続けると永遠に待つ |
commit |
サーバからのレスポンス受信 | とにかく速くしたい時 | 速いがDOM構築前で要素が見つからない |
結論、A8.netの管理画面ではどれもそのままでは安定しませんでした。広告計測タグが裏で動き続けるので、loadとnetworkidleはタイムアウト、domcontentloadedとcommitは早すぎてDOM(ブラウザが解釈した後のHTML構造)の準備が間に合わない。
結局 waitForTimeout(10000) が安定する
最終的に落ち着いたのは、domcontentloadedを指定した上で、追加で10秒の固定秒数待ちを入れる方法です。コードはこうなります。
// HTML本体の読み込みは待つ(ここまでは早く済む)
await page.goto(url, { waitUntil: "domcontentloaded" });
// 広告タグなどが落ち着くまで10秒だけ追加で待つ
await page.waitForTimeout(10000);
// ここから本来やりたい操作(クリックや入力)を始める
「setTimeoutに頼るのは良くない設計」とよく言われます。理屈はその通りですが、外部サイトの裏側がどこまで動いているかをスクリプトから厳密に判断する手段がない以上、固定秒数待ちが現実解になる場面があります。「お作法より動くこと」を優先する判断も、本番運用では時に必要です。
固定秒数待ちは「乱用しない」のがコツ
固定秒数待ちを連発するとスクリプト全体が重くなります。僕は、ループ内(プログラムが何度も繰り返す部分)には絶対に入れない、ページ遷移直後だけに限定する、という2つのルールにしています。1スクリプトで2〜3箇所までの目安です。
罠2|SSLエラーは「たまに出る」から本番で事故る
A8.netのスクリプトで一番悩んだのが、SSLエラーが10回に1回くらいの頻度で発生することでした。ignoreHTTPSErrors: trueというオプションは入れている。それでも出る。これが一番厄介でした。
ignoreHTTPSErrors だけでは握りつぶせない
ignoreHTTPSErrors: trueは「証明書の検証エラーを無視する」設定です。期限切れの自己署名証明書などを許容する用途。一方、僕が踏んでいたのはnet::ERR_SSL_PROTOCOL_ERRORという、暗号化通信を確立する手前で失敗するエラーで、オプションでは握りつぶせないタイプのものでした。同じURLにcurl(コマンドラインのHTTPクライアント)で投げると一発で繋がるので、Playwright側のネットワーク処理に何か原因がありそうだと推定しています。

リトライ関数を全スクリプト共通化する
「たまに失敗する」エラーへの対処は、リトライ(再試行)を仕込むのが基本です。僕はgotoWithRetryという関数を作って、全Playwrightスクリプトでこれを使うようにしました。
async function gotoWithRetry(
page: Page,
url: string,
maxRetries = 5,
): Promise<void> {
for (let i = 0; i < maxRetries; i++) {
try {
await page.goto(url, {
waitUntil: "domcontentloaded",
timeout: 30000,
});
return; // 成功したら抜ける
} catch (err) {
// 最後の試行でも失敗したらエラーを投げる
if (i === maxRetries - 1) throw err;
console.log(`SSL/タイムアウト。${i + 1}/${maxRetries}回目リトライ`);
await page.waitForTimeout(2000); // 2秒待ってから再試行
}
}
}
ブラウザを起動するときの引数にも--ignore-certificate-errorsを渡しておくとさらに安定します。
const browser = await chromium.launch({
args: ["--ignore-certificate-errors"],
});
const context = await browser.newContext({ ignoreHTTPSErrors: true });
「たまに出る」エラーが本番で一番怖い
毎回同じエラーなら原因究明できますが、10回に1回しか出ないエラーは再現が取りにくく、本番のスケジュール実行で初めて踏むことになります。リトライ関数を後から追加する動機が「本番で事故ったから」になりがちなので、新しいPlaywrightスクリプトを書くときは最初から仕込むのがおすすめです。

罠3|page.click()がタイムアウトしたら form.submit() で迂回する
もう1つよくある詰まり方が、ログインボタンのpage.click()が30秒タイムアウトする現象です。よく見るとボタン自体は押されていてフォーム送信も走っている。それでもPlaywrightの命令は終わってくれない。これは仕様を理解すれば回避できます。
原因:page.click()はナビゲーション完了まで待つ
Playwrightのpage.click()は、クリック後にページ遷移(ナビゲーション)が発生する場合、その完了まで内部で待つ仕様になっています。ログイン後にリダイレクトが連鎖したり、広告タグの読み込み待ちでloadイベントが発火しなかったりすると、その待機がそのままタイムアウトになります。
解決策:ボタンを押す代わりにフォームを直接送信する
解決策は単純で、ボタンをclickするのではなく、フォームをJavaScriptから直接送信することでした。HTMLのフォームにはsubmit()というネイティブ関数があり、これを呼べば確実に送信が走ります。
// NG: ナビゲーション待ちでタイムアウトしやすい
await page.click('button[type="submit"]');
// OK: ブラウザ内のJSとしてフォームを直接送信
await page.evaluate(() => {
const form = document.querySelector("form") as HTMLFormElement;
form.submit();
});
// ページ遷移は別途待つ
await page.waitForLoadState("domcontentloaded");
ポイントは「クリックする」と「ナビゲーションを待つ」を別の処理に分けたこと。page.evaluate()はブラウザ内でJavaScriptを実行する機能で、Playwrightのセレクタが効きづらい場面の万能ツールです。
セレクタが不安定な時も page.evaluate() で攻略
同じ手法は、HTML構造が独特なサイトでも効きます。A8.netの提携プログラム一覧は<dl><dt><dd>という、定義リスト形式のタグで作られていました。これはPlaywrightのlocatorではややこしい書き方になります。page.evaluate()でブラウザ側のJSとして書けば、見た目通りに直感的に取れます。
const programs = await page.evaluate(() => {
return Array.from(document.querySelectorAll("dl.program-list")).map((dl) => ({
name: dl.querySelector("dt")?.textContent?.trim(),
detail: dl.querySelector("dd")?.textContent?.trim(),
}));
});

罠4|Playwrightのログインフォーム自動化|Instagramで踏んだ罠
SNSログインの自動化はPlaywrightの定番ですが、Instagramは特に難物でした。ネット記事を参考に「input[name='username']を待つ」コードを書いたら、待っても待っても要素が見つからずタイムアウト。原因はInstagram側のフォーム実装でした。
name属性がデバイスで違う
調べた結果、Instagramのログインフォームはデスクトップ版とモバイル版でname属性(フォーム要素の識別名)が違いました。
| 環境 | ID入力欄のname | パスワード入力欄のname |
|---|---|---|
| デスクトップ | email |
pass |
| モバイル | username |
password |
公式ドキュメントには載っていません。これはブラウザの開発者ツール(F12キー)で実際のHTMLを見て初めて気づきました。Playwrightで知らないサイトを操作するときは、まずHTMLを取得してダンプするのが最初の一手です。
// 自動化前にまずHTMLを覗いてみる
const html = await page.content();
console.log(html); // ファイルに保存してエディタで読むのも有効

そもそもデスクトップではフォームが描画されないことがある
もう1つの罠が、Playwrightからアクセスした場合のInstagramデスクトップ版で、ログインフォーム自体が表示されないことがある点です。Instagramの画面はReact(モダンなWebサイトでよく使われるJavaScriptライブラリ)で動的に描画されるため、Playwrightの操作環境ではうまく初期化されないケースがあります。これも事前にHTMLダンプを見て初めて分かります。
解決策はモバイルエミュレーション
これらをまとめて回避できるのが、Playwrightの「モバイルエミュレーション」機能です。iPhoneのSafariを擬装してアクセスすると、フォームが安定して表示され、name属性もusername/passwordに揃います。
const context = await browser.newContext({
// iPhoneっぽいUserAgentを使う
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0...)",
viewport: { width: 390, height: 844 }, // iPhoneサイズ
isMobile: true,
hasTouch: true,
ignoreHTTPSErrors: true,
});
SNSやモバイル前提のサイトを自動化するなら、Playwrightのデバイスエミュレーション公式ドキュメントに最初から目を通しておくと迷いません。
罠5|Playwrightで「自動化しない判断」をするとき
ここが入門記事ではほぼ触れられない領域です。Playwrightは強力なので「やろうと思えば全部自動化できる」と錯覚しやすい。でも実際は自動化のコストが見合わない場面があり、そこで線を引かないと時間ばかり溶けます。
Instagramの画像アップロードは断念しました
Instagramのプロフィール画像変更を自動化しようとして、最終的に手動運用に戻しました。流れはこうです。
1. ファイル選択ダイアログ → 安定して動く
2. プレビュー画面 → 安定して表示される
3. フィルター画面の「保存する」ボタン → 毎回挙動が違う ← ここで詰む
4. 完了確認 → 表示される時とされない時がある
「保存する」ボタンが表示されたりされなかったり、テキストが日本語と英語で揺れたり、表示位置が変わったり。page.evaluate()で全ボタンのテキストを書き出して何度もデバッグしましたが、安定するパターンが見つかりませんでした。
テキスト入力は安定、画像系UIは不安定という傾向
同じInstagramでも、自己紹介文の自動入力は安定して動きます。inputやtextareaのような標準的なHTMLタグで作られた部品は、Playwrightと相性が良い。一方、独自に作り込まれた画像エディタやドラッグ&ドロップUIは、内部に別のフレームが入っていたり、独自のWebコンポーネント(カスタムHTML要素)になっていたりして、毎リリースで構造が変わって安定運用が難しい。
判断基準:「月の手作業時間」と「自動化にかかる時間」を比べる
僕の判断基準は単純で、月1回しか発生しない作業のために何時間も自動化に費やすなら、手動でいいというものです。プロフィール画像なんて年に数回しか変えません。「できる」と「やる価値がある」は別の話なので、ここで線を引いて時間を節約します。

罠6|Playwrightの自動操作で守るべき注意事項
最後は技術論ではなく、運用上の心構えです。Playwrightは「やろうと思えばできること」が多いツールなので、「やっていいか」を毎回確認しないと事故ります。
自分のアカウントの自分のデータに限定する
僕がPlaywrightでアクセスするのは、自分のASPアカウントの提携情報や、自分のSNSアカウントのプロフィール情報だけです。他人のアカウントや、ログインしないと取れない第三者データを集めるのは別の議論になります。A8.netの会員規約のように、各サービスがスクレイピングについて何を許可しているかは必ず読みます。
リクエスト頻度を抑える
各操作の間に2〜10秒のウェイトを入れます。waitForTimeoutで簡単に挟めます。「動けばいい」と連打すると、サーバ側で異常検知に引っかかってアカウントロックされる可能性があります。これは技術的な話ではなく、サービス側のリソースを尊重する意味でも大事です。
「できる」と「やっていい」は別
Playwrightで自動化できることは、利用規約上やってはいけないこととしばしば重なります。自動フォロー・自動いいね・自動DMは、技術的には書けますが、SNSの利用規約で明確に禁止されています。記事化したくなるトピックほど、規約違反スレスレなことが多い。書く前に各サービスの利用規約を読み直すクセをつけておくと安全です。
まとめ|Playwrightで実運用する全体像
この記事で紹介した6つの罠と対処を、最後に1枚の表に整理しておきます。
| 罠 | 原因 | 対処 |
|---|---|---|
| 自動待機が効かない | 広告計測タグが裏で動き続ける | domcontentloaded+固定秒数待ち |
| SSLエラーがたまに出る | 暗号化通信の確立失敗 | リトライ関数で5回まで再試行 |
page.click()でタイムアウト |
ナビゲーション待ちが終わらない | page.evaluate()でform.submit() |
| ログインフォームが見つからない | name属性がデバイスで違う | モバイルエミュレーション |
| UIが安定せず詰まる | 独自画像エディタなどで構造が頻変 | 頻度が低いなら手動運用に切り替え |
| 規約違反のリスク | 「できる」と「やっていい」の混同 | 自分のデータ限定+頻度制御 |

Playwrightは便利な道具ですが、入門記事のサンプルコードをそのまま動かしても本番では止まります。「動くことを優先する」「たまに出る不具合に備える」「自動化のコストで線を引く」の3つを意識すると、実用システムに少しずつ近づきます。
次に書きたいテーマは、ここで触れたアフィリリンク自動取得の続編で、A8.netともしもアフィリエイトでリンク仕様がどう違うかを比較する内容です。できたら記事にします。
Playwrightをチームで運用する設計や、テスト用途を超えた業務自動化の書き方を体系的に学びたい方は、Web自動化系の書籍がまとまっています。気になる方はPlaywrightの関連書籍をチェックしてみてください。


コメント