Playwrightを使ったスーパーマーケット価格追跡
(sakisv.net)- 2022年12月の高インフレ下で、ギリシャの大手3スーパーの価格変動を毎日追跡するために pricewatcher.gr パイプラインを構築
- 商品一覧が JavaScript レンダリングと無限スクロールの背後にあるため、
curlやrequests.get()の代わりに Playwright でブラウザを制御し、DOM からデータを抽出 - 実行環境は古いノートPCだけでは遅く、AWS EC2 は高価だったため、Hetzner サーバーを必要なときだけ作成して 3 つのスクレイパーを並列実行した後に終了する方式へ変更
- Akamai が非住宅用 IP を遮断するスーパーでは Tailscale exit node を使って自宅 IP 経由にし、メール通知・厳格な変換・商品数ヒューリスティックで失敗を検知
- 8vCPU サーバーと画像リクエスト遮断で実行時間を短縮し、直近 31 回の実行コストは Hetzner サーバー €4.94 と IPv4 €0.09、Cloudflare R2 保存コストは €0.00 程度
JavaScript ベースのスーパーマーケットサイトをスクレイピング
- 2022年12月の高インフレ状況で、ギリシャの大手3スーパーの価格変動を追跡する pricewatcher.gr を構築
- 3 つのスーパーの e-shop はいずれも JavaScript レンダリング を使っており、一部の領域ではソーシャルメディアの無限スクロールのように、スクロール時に商品を追加で読み込んでいた
- 単純な
curlやrequests.get()では商品データを取得できないため、Playwright を使用 - Playwright はブラウザをプログラムから制御し、次の作業を API で処理する
- 新しいタブを開いて URL に移動
- DOM を検査
- 要素の詳細を取得
- リクエストのインターセプトと検査
- Playwright は Chromium、Safari、Firefox をサポートし、Node、Java、.NET、Python で利用できる
- スクレイパーは無限スクロールの
load more要素を見つけてスクロールを続けた後、li.product-itemの一覧から売り切れ表示のある商品を除外 - 最終的に各商品 `` から 商品名・価格・写真・リンク などをパースし、同じ作業を次の商品カテゴリへ繰り返す
毎日回す実行環境を選ぶ
- M1 MacBook Pro ではスーパー 1 社分を丸ごと処理するのに 50分から2時間30分 かかり、3 つのスクレイパーを並列実行しても目立った差はなかった
- 開発とテストにはノートPCで十分だったが、毎日実行する恒久的な環境が必要だった
-
古いノートPC単独実行
- 最初の試みは 2013 年製の古いノートPCだった
- スペックは 2.20GHz デュアルコア M 系列プロセッサと 4GB RAM で、その後 RAM を 12GB に増設
- しかし「速い」スーパーでも処理時間が 2時間以上 かかり、期待した性能には届かなかった
-
クラウド実行
- AWS で 4 コア・8GB RAM 級の EC2 インスタンスを検討したが、サイドプロジェクトのコストとしては負担が大きかった
- 執筆時点で
eu-north-1のc5a.xlargeは 1 時間あたり $0.1640、月 $118.08 または年 $1,416.96 程度 - Hetzner の同等サーバー
cpx31は月 $17.22(€15.72)、年 $206.64 で、AWS より約 7 倍安かった - 最終的な実行環境には Hetzner を選択
Concourse で構成した日次パイプライン
- 古いノートPCはスクレイピングを直接実行せず、Hetzner サーバーに作業を委任する CI サーバー として機能
- CI ツールには Concourse を使用
- Concourse は自らを “a continuous thing-doer” と紹介している
- 宣言的パイプラインモデルと入力のバージョン管理により、再現可能なビルドを志向
- パイプラインは毎晩、次の順序で実行される
- スクレイピングサーバーを作成
- 3 つのスーパーのスクレイピングジョブを並列実行
- すべてのジョブ完了後、コスト削減のためサーバーを終了
- 各スクレイパーの生出力を変換ジョブへ渡す
- 変換したデータを pricewatcher.gr にロード
- どの段階でも失敗したらメール通知を送信
IP 制限の回避と Tailscale exit node
- テストに使ったスーパーは正常に動作したが、別のスーパー 1 社は Akamai の背後にあり、非住宅用 IP からのリクエストを遮断するファイアウォールルールが有効になっていた
- 必要だった構成は通常の VPN とは逆で、実際の自宅 IP からリクエストが出ているように見せる方式だった
- Tailscale で複数デバイスを同じネットワーク上にあるかのように接続
- Tailscale では 1 台のデバイスを exit node に指定すると、他のデバイスのリクエストをそのノード経由で外へ出せる
- 古いノートPCはスクレイピングトラフィックの exit node の役割も兼ねていた
- 利用中の ISP が CGNAT を使っており、グローバル IP が特定個人に固定されず ISP の他の顧客と共有される点も変数として残っている
失敗の種類と検知方法
- この構成は 1 年半運用され、全体として信頼できる形で動作した
- スクレイピングプロジェクトは対象ウェブサイト側の変更の影響を避けられない
- 失敗は大きく 2 つに分かれる
-
壊れる変更
- サイト変更によってスクレイパーが即座に失敗するケース
- 例は次のとおり
- アンケートが追加され、ボタンを 1 回多く押す必要がある場合
- レイアウトが完全に変わり、スクレイパーを大幅にリファクタリングする必要がある場合
-
壊れない変更
- スクレイパー自体は正常に動き続けるが、データ解釈がずれるケースのほうが厄介
- たとえば価格表記方式が変わり、小数点以下の部分を `` に分離すると、€1.99 のポテトチップスが €199 のようにパースされる可能性がある
- こうした変更を捉えるため、変換段階では入力をできるだけ厳格に検証するよう構成
- 毎日実行されるため問題を確認する時間はあるが、休暇中に壊れると不安要素になりうる
実行時間と安定性の最適化
- 全体アーキテクチャは当初からほぼ維持しつつ、信頼性向上と手作業削減のために複数箇所を変更
- 実施した改善には次が含まれる
- 失敗時のメール通知
- 特定スーパーで商品数が多すぎる、または少なすぎるときに通知するヒューリスティック
- タイムアウト
- 最初からやり直さないリトライ
- 最大のボトルネックは スクレイピング実行時間 だった
- 長引くほどコストが増える
- 失敗後に最初から再試行しなければならない場合の不便さが大きい
-
より大きなサーバーを使う
- サーバーを 4vCPU・16GB RAM から 8vCPU・16GB RAM に変更
- 実行時間は約 20% 減少 し、MBP で得られる性能に近い水準になった
- スクレイピングサーバーは約 2 時間しか使わないため、価格差は無視できる程度だった
-
取得量を減らす
- Playwright の
page.routeを使って画像リクエストを遮断 .png、.jpgリクエストを abort 処理し、商品読み込み中に画像を取得しないようにした- この方法でスクレイピングを高速化でき、帯域幅と対象ウェブサイト側のコストもわずかに減らせる
- Playwright の
31 回実行時点のコスト
- 直近の Hetzner 請求書ベースのコストは次のとおり
- 作成したサーバー 31 台: €4.94
- それらのサーバーに割り当てられた IPv4 アドレス 31 個: €0.09
- スクレイピングデータは Cloudflare R2 に保存
- Cloudflare R2 の 10GB 無料枠をまだ超えていないため、保存コストは €0.00
- 全体のパイプラインは Playwright、Hetzner、Concourse、Tailscale、Cloudflare R2 を組み合わせ、スーパーマーケットの価格変動を毎日追跡する構成として動作している
1件のコメント
Hacker News のコメント
今年の初めから New Zealand を対象に似たようなことをしている。Playwright/Typescript でデータをスクレイピングし、Parquet ファイルとしてクラウドストレージに保存しているが、まだ画面には表示していない。
作業の大半は、Akamai や Cloudflare のようなリバースプロキシサービスを迂回することに費やしている。最初に作ったときは誰もやっていないと思っていたが、今では NZ で少なくとも3社のスタートアップが同じことをしているのを知っている。インフレがここでかなり多くのイノベーションを促したようだ。
パターンは予想どおり。スーパーマーケットは価格をできるだけ複雑にするいつもの手口を使い、「鋸歯状」の価格変動で時間のない人とお金のない人を切り分ける。ブランドロイヤルティの高い顧客と価格に敏感な顧客を分けることも多く、人気のチョコレートブランドが3つあれば、毎週そのうち1つだけを適正価格で売る、という具合だ。
「ブランドロイヤルティの高い顧客と価格に敏感な顧客を分ける。人気のチョコレートブランドが3つあれば、毎週そのうち1つだけが適正価格で売られる」という部分が特に気になる。
AU ではこうしたスクレイパーがたくさん現れては消えるが、たいてい大手スーパーマーケットにブロックされる。有用さと「なぜこういうものがないのか?」が繰り返される循環だが、実際にはすでに何度も存在していた。
クローラーとそれを支えるインフラを作るのに20時間もかからなかった。
しかも複数の店の価格をすべて持っているマーケットプレイスが1つしかないので、さらに有用だ。
良い記事。私が運営しているコンタクトレンズ価格比較サイト https://lenspricer.com/ でも似た問題を経験しており、約30か国で運営している。Webサイトが HTML を変更するのは本当に厄介だという点に同意する。
初期の最大の障害の一つは、100を超えるWebサイトで同じ製品をマッチングすることだった。製品名は一意に見えても、誰もが自分たちのやり方で少しずつ変えて書いている。大半は正規表現で処理できるが、かなりの数は手動でマッピングする必要があり、一部には AI を使ったものの、すべて自分で検証した。
スクレイパーとインフラを作るのは比較的簡単な方だ。難しいのは、すべてのスクレイパーを保守し、製品がサイトから消えたときに、それがスクレイパーのエラーなのか、ブロックなのか、サイト変更なのか、クロール時点でサイトがメンテナンス中だったのかを見極めることだ。
面白いプロジェクトだが、ときには難しく、直すのが面倒な問題も多い。
価格はばらばらで、補償もたいてい郵送のリベートで30%ほど処理されるような形だ。
店舗数も非常に少なく見えるので、もしかするとすべてのリンクがスポンサーリンクなのかもしれない。idealo.de ではもっと低い価格も見つけた。
Costco はこれでかなり有名で、店舗で売っている電子製品のほぼすべてと、他の多くの製品がカスタム SKU だ。製品構成も少し違うことがよくある。
私の街でかなり関心を集めた、似たようなWebサイトを作った。アプリとWebサイトのデータまでスクレイピングし、Linode の RAM 2GB の単一サーバーに IPv4 5個と無料の IPv6 1000個を付けて使っている。
すべての製品は最大40分間隔で収集しており、平均は25分ほど。curl-impersonate を使い、可能な限り JSON をスクレイピングしている。市場の90%は Ajax 呼び出しで価格を提供しており、残りの10%は正規表現で HTML を簡単にパースしている。
https://www.economizafloripa.com.br で見られる。
https://www.economizafloripa.com.br/?q=parceria-comercial
そのページを見ると、このプロジェクトが「生活必需品を売る企業から主導権を取り戻す人々のための有用なツール」から「また一つの金儲けの試み」に見えてしまう。もちろんそれは本人の自由だが、ホームページを読んだときは、もっと倫理的な動機を期待していた。
良い記事
スクレイピングとパースは別プロセスに分けるのが最善だと思う。元の JSON や HTML を保存しておけば、いつでも戻ってパーサーを直し、再適用できる
修士プロジェクトの一環として、オランダ向けに似たようなシステムとサイトを作った: https://www.superprijsvergelijker.nl/
自分のプロジェクトのスクレイピングの大半は、JSON API に単純な HTTP 呼び出しを送る方式。一部のサイトでは、有効なセッションクッキーを取得し、ボット対策や CAPTCHA を回避するために Playwright インスタンスを使っている。残りのクローラー/スクレイパー、パーサー、API は Haskell で作り、AWS ECS で動かしている。サイトは NextJS
ずっと解こうとしている主な課題は、異なるスーパーの商品をひも付けて、1つの画面に価格を並べること。例はこちら: https://www.superprijsvergelijker.nl/supermarkt-aanbieding/6...
商品に正しいバーコード番号が1つでも提供されていれば、たいていうまく動く
オーストラリアの二大スーパーは、双方で価格分析 AI アルゴリズムを運用するだけでも、反競争的な価格設定の複占構造を作れると思う。アルゴリズムは結局、利益を最大化するために協調する方向に進む可能性が高い
公開されている価格だけでも合法的にできるし、仕入れ原価や商品別の利益データを共有すれば違法にもできる。結果は似たようなものになりそう
学習済みの AI 2つは、多次元、もしかすると超多次元の回帰分析で、妙なやり方の利益最大化を行い、消費者は表向きは競合である企業たちの最大化された利益を負担することになる。こういう形で価格データを得られるなら、複占に焦点を当てた機械学習実装を2つ回すのに必要なものは多くない
すべての価格が公開されると、消費者はむしろより高い価格を払うことになる、という理屈。スーパー各社が、全員の利益が最大になる地点に価格を合わせるようになるということ
こちらのスーパーは何年も前から「価格ハンター」を雇っていて、競合店に行って全商品の価格を記録する人たち
ノルウェーでは、スーパー A がある週に特定の商品を割引すると、翌週またはその次の週にスーパー B が似たような割引をして客を呼び込む様子をよく見る
こうした変更を検知するには、自動チェックを追加できそう。たとえば常識チェックに失敗したら、価格/商品の変更を同期しないようにする
各価格は100%を超えて変動してはならず、アクティブな商品数は20%を超えて変動してはならない、という基準を置ける
代わりに、こうしたヒューリスティックはスクレイピングの成功可否を確認するのに使った。たとえば、今日取得した商品数が直近7日平均のおよそ10%以内に収まっているかを確認する方式
難しいのはスクレイピング自体ではなく、どんどん高度化するブロック機構の回避
住宅用プロキシを継続的にローテーションする必要があり、評価の高いものを使わなければならず、データスクレイピングのパターンを露呈しないようにしなければならない。スーパーによってはネットワークタブにネットワークリクエストを表示しないため、API レスポンスだけを取得する方式ではうまくいかない
それでもモバイルアプリに中間者攻撃を仕掛けてネットワークリクエストとデータを見ようとしても、きちんと偽装しなければブロックされる
試してみたが、コストと継続的な開発作業のため、価値がないと判断した。実際、一部のスーパー価格比較サービスは、単に低賃金の人手を使って取得させている
これはサイドプロジェクトなので、時間的負担があまりに頻繁になったら、そのまま中止してコードとデータをすべて公開するつもり
ところで、ネットワークリクエストがネットワークタブに表示されないというのは、どうやって可能なのか気になる
自分にとって一番難しい部分は、スーパー間で商品をひも付けて比較すること
商品の価格透明性があるとよい。店舗や地域ごとに、こうした流れをずっと簡単に追跡できる
たとえば牛乳の代替品であるオーツミルクの価格を、郵便番号や食料品店ごとに比較できる。また、価格は同じでも容量が減る「シュリンクフレーション」も追跡できる
そういう意味で価格は追跡しているようだが、グラム当たりまたはオンス当たりのコストも確認しているのか気になる。メーカーや店舗は価格を据え置きながら消費者に渡す量を減らせるが、ツールがそれを検知できるのか気になる
それでも十分追加できる機能で、シュリンクフレーションがあったなら発生時点を示せる
スウェーデン市場では、8年以上この取り組みを続けています。https://www.matspar.se/ というウェブサイトがあり、顧客は主要オンライン店舗のすべての商品を見て回り、価格を比較したうえで、欲しい商品をカートに入れられます。
最後に配送料まで含めたカート総額を比較し、希望する店舗へカートをエクスポートして注文できます。
共同創業者で現CTOなので、これまでスクレイピングとメンテナンスをかなりやってきました。毎日 3,000万件を超える価格を取得しています。
公開データ上の会社売上は約40万ドルで、従業員は6人です: https://www.allabolag.se/5590076351/matspar-i-sverige-ab
新しい地域に引っ越したときは価格追跡をしていましたが、今は常に低価格を維持している市場や大型店2か所で買い物するほうがずっと簡単だと感じています。
ヨーロッパなら、おそらく Aldi/Lidl でしょう。
米国なら Costco/Trader Joe's かもしれません。
オンラインでは CamelCamelCamel/Amazon があります。食品ではなく、健康・美容・一部の電子機器向けです。
メーカーから直接買えるなら、ときにはそのほうが良いこともあります。たとえば、気に入っている特定の石けんブランドを卸売サイトでまとめ買いしたところ、小売価格の半額以下でした。シャンプーもガロン単位で直接買うほうが、どの小売店よりもずっと安かったです。
Trader Joe's は品質は高いものの、全般的により高価です。