WordPress の保守作業で「更新前後のサイトのスクリーンショットを比べて異常検知する」というアプローチがあります。レイアウト崩れ・404 になった画像・テーマの崩壊などを目視に頼らず自動で拾えれば、運用がだいぶ楽になる。
ビフォアを撮る → 更新する → アフターを撮る → 差分が大きければ異常、と書くとシンプル。
実際に自動化ツール【Playwright】で組んでみたら、想像していたより問題が次々と出てきました。実装中に踏んだ3つを残しておきます。
問題① wait_until='networkidle' は地雷
ページの読み込み完了を待つ設定として、まず思いつくのが Playwright の wait_until='networkidle'。
page.goto(url, wait_until='networkidle')
ネットワーク通信が一定時間途絶えるまで待つ、いかにも丁寧な待機方法。
…これが特定の WordPress サイトで毎回タイムアウトする問題が出てきました。
原因は Google Analytics と Google Tag Manager。これらはページが開いている間ずっと裏で通信し続けているので、networkidle に到達する瞬間が来ません。30秒のタイムアウトで強制終了されます。
しかも、タイムアウトすると Playwright は HTTP ステータスを「0」として返してきました。その値を「異常 → ロールバック」と判断する処理を組んでいたので、サイトは普通に表示されているのにロールバックが走るという二次災害まで発生。
WordPress のように GA/GTM が入っているのが当たり前のサイトでは、networkidle は使い物にならない、と早めに割り切る方が安全です。
問題② wait_until='load' でも撮影タイミングがブレる
networkidle をやめて wait_until='load' に切り替え。HTML・CSS・画像の読み込みが終わった時点を指す、より素直な待機条件です。
page.goto(url, wait_until='load')
page.screenshot(path=...)
これで毎回30秒タイムアウトする問題は解消。
ところが今度は、ビフォアとアフターで撮影タイミングがブレる問題が出てきました。同じサイト、同じ手順なのに、毎回少しずつ差分が検知される。
調べてみると、load が来るタイミングは毎回微妙にズレていました。
- スライダー初期化が
load前に終わっているケース - スライダー初期化が
load後にずれ込むケース - Web フォントの描画が間に合っているケース/いないケース
- 遅延読み込み画像が間に合っているケース/いないケース
JS の進み具合は毎回違う。load で揃えたつもりでも、実際の描画状態は揃わない。
解決策は、load 完了後にさらに 1秒待つ。
page.goto(url, wait_until='load')
page.wait_for_timeout(1000)
page.screenshot(path=...)
ポイントは「タイミングを揃える」ではなく「両方とも落ち着いた状態になるまで待つ」と発想を変えること。JS の初期化はだいたい load 後 200〜800ms 以内に終わるので、1秒待てば、load が早かろうが遅かろうが、両方とも初期化完了後の状態になります。絶対時刻は揃わなくても、状態が揃えば比較は成立する。
地味な1行ですが、誤検知が劇的に減りました。
問題③ ランダム要素はピクセル比較を壊す
タイミング問題が片付いたあとに残る、もう一段大きな壁。それがランダム要素でした。
webサイトには、ロードするたびに見た目が変わる領域がそこらじゅうにあります。
- スライダー / カルーセルの開始スライドがランダム
- 「関連記事」「人気記事」「おすすめ記事」のシャッフル表示
- お客様の声のローテーション
- AdSense・アフィリエイト広告の毎回違うバナー
- 「X分前」「X時間前」などの相対時刻、現在時刻ウィジェット
- アニメーション GIF・自動再生動画(撮影タイミングでフレームが違う)
- フェードイン・パララックスアニメ(進捗が毎回違う)
- CAPTCHA・トークン埋め込み
サイト側の挙動が決定的でないので、ピクセル単位の差分検知では「どれが本物の異常で、どれがランダムなだけか」を区別できません。閾値を緩めれば誤検知は減りますが、本来検知したいレイアウト崩れまで見逃しやすくなる。
対処パターン
実装側でやれることは、おおむね4種類。
1. Playwright の mask パラメータで該当領域をマスクする
slider = page.locator('.slick-slider')
ads = page.locator('.ad-banner')
page.screenshot(path=..., mask=[slider, ads])
撮影時にマスクしておけば、ランダム要素は単色で塗りつぶされ、比較から除外されます。一番素直な対処。
2. CSS 注入で該当要素を非表示にする
page.add_style_tag(content='.slider, .ad-banner, .related-posts { visibility: hidden !important; }')
page.wait_for_timeout(100)
page.screenshot(path=...)
要素自体を消すと隣接レイアウトも詰まることがあるので、display: none ではなく visibility: hidden(場所だけ確保)が無難。
3. JS でランダム要素を固定状態にする
たとえばスライダーなら、初期スライドを必ず1枚目に固定するスクリプトを撮影前に注入する:
page.evaluate("document.querySelectorAll('.slider').forEach(s => s.scrollLeft = 0)")
要素のセレクタや API に依存するので、サイトごとにメンテが必要。
4. 差分閾値を緩める
ピクセル差分の許容率を 8% → 15% などに引き上げる。実装は楽ですが、本物のレイアウト崩れまで通してしまうので、最後の手段。
ランダム要素は WordPress テーマやプラグインの仕様であって、こちら側で「無くす」ことはできません。スクショ比較というアプローチには本質的な限界があり、対象サイトの構造に応じて補正が必要、と理解した上で導入するのが現実的です。
まとめ
スクショ比較を実装するときに踏みやすい3つのポイント:
-
wait_until='networkidle'は GA/GTM のあるサイトで永遠にアイドルにならない。'load'を使う -
'load'後にさらに1秒待つ。「タイミングを揃える」ではなく「状態を揃える」 - ランダム要素はマスク・非表示・固定化のどれかで除外する。閾値で逃げるのは最後の手段
「ビフォアとアフターを比べるだけ」と思っていた処理が、実装してみると思った以上に複合的でした。同じことをしようとしている方の参考になれば嬉しいです。