ブログに戻ります

競争力のある価格設定パイプラインの構築方法:1日で8つの競合他社にわたって5,000のSKUを追跡する

Ethan Brown
Ethan Brown

Advanced Bot Mitigation Engineer

28-May-2026

主なポイント:

  • 競争価格設定は、製品問題ではなくバスケット問題である。 8つの競合他社で4つの市場にわたる5,000のSKUを追跡する価格設定チームは、毎日160,000のリードを処理している。拡張可能なアーキテクチャは、URLごとに1回のレンダリングコールと市場ごとに固定されたエグレス、さらに単一の標準出力スキーマであり、160,000のアドホックフェッチではない。
  • 市場がエグレスを決定する。 価格、通貨、供給状況は地域やIPの評判によって変動する。測定中の市場に対してプロキシ国を固定することで、記録されたすべての価格が比較可能になる; 同じSKUに対して米国とEUのエグレスを混合すると、何の意味もない価格履歴が生成される。
  • 競合他社間での一貫したスキーマ。 各小売業者のDOMは異なるが、倉庫テーブルは同じである。抽出時に標準化する: {your_sku, competitor, market, price_value, price_currency, availability, promo_state, captured_at}。意思決定は生のHTMLではなく、倉庫を読み取る。
  • アンチ検出はサーバーサイドで処理される。 各リクエストは、住宅用のエグレス、JavaScriptの実行、フィンガープリンティングのランダム化を利用してScrapelessクラウド内でレンダリングされる。パイプラインはURLと国を送り、レンダリングされたHTMLを受け取る。ブラウザのバイナリ、プロキシのローテーションロジック、あなたのマシン上でのサードパーティのCDPクライアントは不要。
  • パイプラインはHTMLではなくdiffで終了する。 生のレンダリングページはスクラッチストレージである。価格チームが作用するシグナルは、あなたの価格と競合他社の価格のdiffであり、市場ごと、SKUごとに — 再価格設定ルール、Slackアラート、またはアナリストダッシュボードに表示される。
  • 無料で開始できる。 新しいScrapelessアカウントには無料のランタイムが含まれる — app.scrapeless.comでサインアップ。

はじめに: ウェブデータから競争価格設定の決定へ

競争価格設定チームは、長年同じ制約に悩まされてきた: 価格は価格設定の決定を通知するデータフィードよりも早く変化する。小売業者は一晩で価格シールを改訂する; BIタイルは48時間後に更新される; アナリストがギャップを見たときには、プロモーションウィンドウは終了している。ウェブデータはそのループを閉じるが、収集層が変化のペースに追いつき、倉庫が結合できるスキーマにフィードする場合のみ。

構造的な課題は「製品ページをスクレイピングする」ことではない。SKUのバスケット、競合他社のバスケット、市場のバスケット全体で、毎日、すべての市場、すべての小売業者で、同じ精度でスクレイピングを運営すること — 各小売業者のDOMは回転し、各市場の価格はローカライズされ、各リクエストは小売業者のアンチボット層をクリアし、クリーンでレンダリングされたHTMLを返す必要がある。OctoparseのOptiGroupケーススタディは、このパターンをスケールで捉えた: 50の子会社、数十の競合サイト、地域価格、集中型の価格決定層。

このガイドは、Scrapeless上の価格インテリジェンスパイプラインの収集層のアーキテクチャとPythonコードを通じて説明する。出力は倉庫テーブルにフィードする標準化されたNDJSONストリーム; 入力はアナリストが定義するバスケットファイルである。パターンのために一度読み、その後は小売業者ごとのエクストラクタを変更してすべての競合他社に再利用する。


これを使ってできること

  • 日次競争バスケットのリード。 4市場にわたる8つの競合他社で5,000のSKUを日次スケジュールで、制限されたランタイムと1つの標準スキーマで追跡する。
  • 市場特有の再価格設定。 各市場にエグレス国を固定し、地元の買い物客が実際に見るローカライズされた価格を取得する。ジオフォールバック価格ではない。
  • プロモ状態の監視。 表示価格とプロモーション状態(セール中、割引率、時間制限のバッジ)の両方をキャプチャし、倉庫が通常の価格とクリアランスプッシュの違いを認識できるようにする。
  • MAPコンプライアンス監査。 小売業者の表示価格をあなたのMAP(最低広告価格)ポリシーと比較し、違反をチャネル管理チームに通知する。
  • 新製品発売の追跡。 カテゴリー内で競合SKUの初登場を監視する; パイプラインは「競合他社はXを発売しようとしているか?」というシグナルとして機能する。
  • 価格弾力性データセット。 90日間の毎日のスナップショットが、SKUレベルで弾力性を計算するために収益管理が使用する時系列を生成する。

Scrapelessでは、適用される法律、規制、およびウェブサイトのプライバシーポリシーを厳格に遵守しながら、公開されているデータのみをアクセスします。この投稿の内容はデモ目的のみです。


なぜ競争価格設定にScrapelessなのか

Scrapelessは、自己開発したChromiumを使用してアンチ検出クラウドブラウザ内で各ターゲットURLをレンダリングし、完了したHTMLを単一のAPIコールで返します。特に価格インテリジェンスパイプラインにおいて、それは次のものを提供します:

  • 195か国以上の住宅用プロキシ, リクエストごとに固定された国コードで — エグレスの地理は市場ごとに1フィールドである。
  • クラウド側のJavaScriptレンダリング。 小売業者の商品ページはReactまたはNext.jsアプリであり、価格要素はハイドレーションの後に表示される。js_render=Trueは、あなたのパイプラインがペイント後のDOMを読み取ることを意味し、SSRシェルではない。
  • サーバーサイドのアンチデetection。 UA、タイムゾーン、WebGL、キャンバス、およびヘッドレスフラグは、リクエストごとにクラウドでランダム化されます。ローカルのステルスプラグインのメンテナンスは不要で、ブラウザのバイナリをインストールする必要もありません。
  • ステートレスリクエスト形状。 各製品ページは独立した読み取りです: URLと国を送信すると、レンダリングされたHTMLが返されます。これは数千の独立したSKUの読み取りのバスケットにクリーンにマップされます。
  • 全パイプライン用の1つのAPIキー。 レンダリング、住宅プロキシ、およびSDKはすべて同じScrapelessアカウントに対して請求されます; ティアごとの統合は不要です。

無料プランでAPIキーを取得するにはapp.scrapeless.comにアクセスしてください。


前提条件

  • Python 3.10以降
  • ScrapelessアカウントとAPIキー — app.scrapeless.comでサインアップ
  • requestsスタイルのHTTPおよびCSSセレクタライブラリに精通していること
  • 競合他社リストとSKUバスケットファイル

パイプラインアーキテクチャの概要

Copy
basket.yaml                      (アナリスト定義の入力)
       │
       ▼
┌──────────────────┐
│   オーケストレーター   │  (市場、競合他社、SKU)ごとに1つのタスク; 有限の同時実行
└──────┬───────────┘
       │
       ▼
┌──────────────────┐
│   Scrapeless     │  client.universal.scrape(url, country) — 住宅用イグレス,
│  (クラウドレンダー)  │  JSレンダリング、アンチデetection、すべてサーバーサイド
└──────┬───────────┘
       │  レンダリングされたHTML
       ▼
┌──────────────────┐
│   ノーマライザー     │  小売業者ごとの抽出器 → 標準スキーマ
└──────┬───────────┘
       │
       ▼
prices.ndjson          ((製品、競合他社、市場、日)の各行)
       │
       ▼
倉庫の読み込み + あなたの価格との違い + アラート

各ステージはPythonモジュールです; 以下の7ステップで下から上に構築します。


ステップ1 — Scrapeless SDKのインストール

bash Copy
pip install scrapeless lxml pyyaml

scrapelessは公式Python SDKです; ページをクラウド側でレンダリングし、HTMLを返すため、ブラウザのバイナリやサードパーティの自動化ライブラリをインストールする必要はありません。lxmlはパーサーです; pyyamlはバスケット設定を読み取ります。


ステップ2 — バスケットの定義

価格設定チームがこのファイルを所有します。退屈に保ってください — 市場、競合他社、SKUマッピング。各行は(your_sku、競合他社、競合他社のURL、市場)ごとに1つです:

yaml Copy
# basket.yaml
markets:
  - US
  - GB
  - DE
  - JP

basket:
  - your_sku: SKU-1001
    name: "Acme Widget Pro"
    competitors:
      - retailer: target_competitor_a
        url:
          US: "https://competitor-a.com/p/widget-pro"
          GB: "https://competitor-a.co.uk/p/widget-pro"
          DE: "https://competitor-a.de/p/widget-pro"
          JP: "https://competitor-a.co.jp/p/widget-pro"
      - retailer: target_competitor_b
        url:
          US: "https://competitor-b.com/products/widget-pro"
          GB: "https://competitor-b.co.uk/products/widget-pro"

5,000 SKUのバスケットは同じ形状で、倉庫はあなた自身の価格フィードに対してyour_skuで結合します。


ステップ3 — Scrapelessを介して製品ページをレンダリング

各(市場、SKU)ごとに1つのレンダーコール。国のピンが住宅用イグレスを設定します; js_render=Trueは、ポストハイドレーションDOMを返します:

python Copy
import os
from scrapeless import Scrapeless
from scrapeless.types.universal import (
    UniversalScrapingRequest, UniversalJsRenderInput, UniversalProxy,
)

client = Scrapeless()  # 環境からSCRAPELESS_API_KEYを読み取ります

def scrape_rendered(url: str, market: str) -> str:
    """Scrapelessクラウドで1つの製品ページをレンダリングし、HTMLを返します。"""
    request = UniversalScrapingRequest(
        actor="unlocker.webunlocker",
        input=UniversalJsRenderInput(url=url, js_render=True, headless=True),
        proxy=UniversalProxy(country=market),
    )
    return client.universal.scrape(request)  # レンダリングされたHTML(str)を返します

国のピンは主要なフィールドです。同じ製品URLが地域ごとに異なる価格、通貨、可用性の状態をレンダリングするため、イグレスをピンニングすることで、記録されたすべての価格が同じ市場に保たれます。js_render=Trueは、ページが描画されるまで待機し、React/Vue/Next.jsの小売業者が空のシェルではなく価格要素を返します。


ステップ4 — バスケットを歩く

各SKUは独立したレンダーコールであるため、バスケットウォークは単純なループ(または並行処理用の制約のあるスレッドプール)です。セッションを保持する必要はなく、ホームページを温める必要もありません — クラウドレンダーはリクエストごとに小売業者のアンチボット層をクリアします:

python Copy
import yaml

def load_basket(path: str = "basket.yaml") -> dict:
    with open(path, encoding="utf-8") as f:
        return yaml.safe_load(f)

def walk_basket(basket: dict):
    """すべてのバスケットエントリに対して (your_sku, retailer, market, url, html) を生成します。"""
    for item in basket["basket"]:
        for comp in item["competitors"]:
            for market, url in comp["url"].items():
                html = scrape_rendered(url, market)
                yield item["your_sku"], comp["retailer"], market, url, html

5,000 SKU バスケットの場合、scrape_renderedconcurrent.futures.ThreadPoolExecutor でラップし、ワーカーの数をアカウントプランが許可するレベルに制限します。各呼び出しは無状態なので、ワーカーを追加することで並行処理がスケールします — 競合する共有セッションはありません。

無料プランの API キーを取得してください: app.scrapeless.com


ステップ 5 — 標準スキーマに抽出する

各小売業者の DOM は異なりますが、倉庫テーブルは同じです。エキストラクターの仕事は、小売業者がレンダリングするものを毎回同じ形に変えることです。出力スキーマは (your_sku, competitor, market, captured_at) ごとに1行です:

python Copy
from dataclasses import dataclass, asdict
from datetime import datetime, timezone
from typing import Optional
from lxml import html as lxml_html

@dataclass
class PriceRecord:
    your_sku: str
    competitor: str
    market: str
    url: str
    price_value: Optional[float]
    price_currency: Optional[str]
    availability: Optional[str]       # "in_stock" | "out_of_stock" | "preorder" | None
    promo_state: Optional[str]        # "none" | "on_sale" | "clearance" | None
    promo_discount_pct: Optional[float]
    captured_at: str                  # ISO-8601 UTC

小売業者ごとのエキストラクターは、同じ戻り値の型に接続します:

python Copy
def extract_competitor_a(html: str, your_sku: str, market: str, url: str) -> PriceRecord:
    doc = lxml_html.fromstring(html)

    price_el = doc.cssselect("[data-test='price'] .value")
    currency_el = doc.cssselect("[data-test='price'] .currency")
    availability_el = doc.cssselect("[data-test='availability']")
    promo_el = doc.cssselect("[data-test='promo-badge']")

    availability = (
        "in_stock" if availability_el and "In stock" in availability_el[0].text_content()
        else "out_of_stock" if availability_el
        else None
    )

    return PriceRecord(
        your_sku=your_sku,
        competitor="target_competitor_a",
        market=market,
        url=url,
        price_value=_to_float(price_el[0].text_content()) if price_el else None,
        price_currency=currency_el[0].text_content().strip() if currency_el else None,
        availability=availability,
        promo_state="on_sale" if promo_el else "none",
        promo_discount_pct=_to_float(promo_el[0].get("data-discount-pct")) if promo_el else None,
        captured_at=datetime.now(timezone.utc).isoformat(),
    )

def _to_float(text) -> Optional[float]:
    if not text:
        return None
    cleaned = "".join(c for c in text if c.isdigit() or c == ".")
    try:
        return float(cleaned)
    except (ValueError, TypeError):
        return None

すべての小売業者はそれぞれの extract_<name> 関数を持ち、すべての関数は同じ PriceRecord を返します。オーケストレーターは各小売業者が使用する DOM を知らず — 単に呼び出す関数名だけを知っています。

セレクタ設計の注意点:

  • 小売業者が公開する [data-test='...'] 属性を優先します。 これはビジュアルのクラス名のローテーションに耐えます; .text-lg.font-semibold のようなクラスは、毎回のリリースで変更されます。
  • 欠落しているフィールドは nullable として扱います。 在庫切れの商品に対する None 価格はデータであり、失敗ではありません。
  • 小売業者がレンダリングする通貨文字列をキャプチャします。 市場から通貨を推測しないでください — 一部の小売業者は、越境商品のために .de ドメインで USD を表示しています。ページに表示されている通りに保存しましょう。

ステップ 6 — 倉庫ロードのために NDJSON にストリーミングする

NDJSON にストリーム書き込みを行い、パイプラインが実行中の中断を生き延びてもレコードを失わないようにします。各行は1つのレンダリング SKU であり、ファイルは追記専用です:

python Copy
import json
from pathlib import Path

def append_records(records: list[PriceRecord], out_path: str = "prices.ndjson"):
    Path(out_path).parent.mkdir(parents=True, exist_ok=True)
    with open(out_path, "a", encoding="utf-8") as f:
        for r in records:
            f.write(json.dumps(asdict(r)) + "\n")

NDJSON は Snowflake (COPY INTO ... FILE_FORMAT = (TYPE = JSON))、BigQuery (bq load --source_format=NEWLINE_DELIMITED_JSON)、Redshift、ClickHouse、DuckDB に直接ロードされます。ビジネスインテリジェンススタックで既に使用されているものを選んでください; スキーマは同じです。


ステップ 7 — 差分を計算し、価格決定をルーティングする

価格チームが行動する信号は生の価格ではなく — 競合他社の価格とあなたの価格の差です。市場ごと、SKU ごとに。差分は倉庫にあり、スクレイパーにはありません:

sql Copy
-- SKU ごとの競合ごとのデイリー価格ギャップ
WITH yours AS (
  SELECT sku, market, list_price, currency, captured_date
  FROM your_internal_prices
  WHERE captured_date = CURRENT_DATE
),
theirs AS (
  SELECT your_sku, competitor, market, price_value, price_currency,
         availability, promo_state, CAST(captured_at AS DATE) AS captured_date
  FROM competitor_prices
  WHERE CAST(captured_at AS DATE) = CURRENT_DATE
)
SELECT
  t.your_sku,
  t.competitor,
  t.market,
  y.list_price                                  AS our_price,
  t.price_value                                 AS their_price,

ROUND(100.0 * (y.list_price - t.price_value) / NULLIF(t.price_value, 0), 2)
AS price_gap_pct,
t.availability,
t.promo_state
FROM theirs t
LEFT JOIN yours y
ON y.sku = t.your_sku AND y.market = t.market
WHERE y.list_price IS NOT NULL
AND t.price_value IS NOT NULL
ORDER BY price_gap_pct DESC;

price_gap_pctが価格ルールで定義された閾値を超える行をルーティングします:

  • あなたの価格閾値を超える (例えば、リーダーよりも5%以上高い) → 価格見直し。
  • MAP閾値未満 → チャンネル管理へのMAP違反アラート。
  • 昨日以降のプロモ状態の変更 → カテゴリマネージャーへの競合プロモ通知。

差分クエリは、コレクションと意思決定の間の契約です。倉庫スキーマが安定していれば、価格チームのダウンストリームBIタイル、アラート、および価格ルールは小売業者がDOMを回転させても変更されることはありません — 変更されるのはステップ5の小売業者ごとの抽出器だけです。


返される内容

(your_sku、競合、マーケット、日)ごとに1つのNDJSON行、次のような形状です:

json Copy
{
  "your_sku": "SKU-1001",
  "competitor": "target_competitor_a",
  "market": "US",
  "url": "https://competitor-a.com/p/widget-pro",
  "price_value": 79.99,
  "price_currency": "USD",
  "availability": "in_stock",
  "promo_state": "on_sale",
  "promo_discount_pct": 15.0,
  "captured_at": "<ISO-8601 UTC timestamp written at read time>"
}

パターンを実行した際の正直な観察:

  • レンダリングのタイミングはDOMの特異性よりも重要です。 SSRシェルに対して実行されるセレクタは、価格要素が描画される前に空文字列を返します。 js_render=Trueは、価格セレクタが解決されるものを持つ、後の水和DOMを返します。
  • 通貨は市場と重複しません。 国境を越えたSKUは、ローカライズされたドメイン上でも、非現地通貨を表示することがあります。 レンダリングされた文字列を保存し、倉庫レイヤーに正規化させます。
  • プロモ状態には少なくとも3つの値があります。 noneon_saleclearanceは、価格改定ルールで異なる動作をします — clearanceマークダウンはライフサイクルの終わりを示し、プロモーションのプッシュではありません。
  • 可用性は二番目にアクション可能なフィールドです。 在庫切れSKUの20%の価格差は、在庫ありSKUの同じ差とは異なる競争シグナルです。 どちらも意思決定レイヤーに表示します。
  • 1つのカノニカルスキーマが耐荷重の決定です。 小売業者ごとのフィールド、通貨の慣習、プロモ形式は異なりますが、倉庫テーブルはそうではありません。 変動を抽出関数に押し込み、スキーマを平坦に保ちます。

結論: 競争価格パイプラインをスケールさせましょう

パイプラインは6つのステップに縮小されます: バスケットを定義 → Scrapelessを通じて各SKUを市場ごとにエグレスピンでレンダリング → 標準的なスキーマに抽出 → NDJSONにストリーミング → 倉庫にロード → 自身の価格と差分を取る。 各ステップは読みやすいほど小さく、構成は1日1回のcronで8つの競合および4つの市場にわたり5,000SKUを処理します。

価格関連のスクレイピングのためのベンダー比較ビュー(特に不動産価格)のために、2026年の最高のZillowスクレイパーリストは、同様のローカライズ価格抽出の課題に対して8つのツールを評価します。 NDJSON出力をクラウド倉庫にロードするために、Scrapeless + SnowflakeデータインジェスチョンガイドはCOPY INTOとストリーミングパスの手順を解説します。

市場ごとに出国をピン留めし、各SKUを独立してレンダリングし、抽出時に正規化し、SKU/競合/市場/日ごとに1つのカノニカル行を保存し、倉庫で差分を取ります — スクレイパーではありません。


AI搭載データパイプラインを構築する準備はできましたか?

私たちのコミュニティに参加して無料プランを取得し、競争価格パイプラインを構築している開発者とつながりましょう: Discord · Telegram

app.scrapeless.com に無料のランタイムでサインアップし、上記のパターンを価格パイプラインが必要とする市場、競合、SKUバスケットに適応させてください。価格の詳細はscrapeless.com/en/pricing; プロキシソリューションの製品ページはscrapeless.com/en/product/proxy-solutions; 完全なSDKリファレンスはdocs.scrapeless.comにあります。


よくある質問

Q1: 競合の価格をスクレイピングすることは合法ですか?

価格は小売業者の製品ページ上の公的情報であり、価格比較は確立された商業実践です。合法性は、何を、どこから、どのような条件でスクレイピングするかに依存します。公に見ることができるデータは一般的にアクセス可能ですが、サイトの利用規約、地域のプライバシー法(GDPR、CCPA)および著作権が適用されます。重大な使用例については法的助言を求めてください。Scrapelessは公に利用可能なデータのみにアクセスします。

Q2: 競争価格のためにプロキシは必要ですか?
はい、国のPINはIPのローテーションよりも重要です。小売業者は市場ごとに価格をローカライズします。アメリカから.co.ukドメインへのアクセス要求は、フォールバック価格、リダイレクト、または地理的ブロックを返す場合があります。測定対象の市場に国を固定するには、UniversalProxy(country=...)を使用します。195か国以上のスクレイプレスの住宅用プロキシは、別のプロキシプロバイダーをスタックに持ち込むことなく、一般的な価格バスケットをカバーします。

Q3: アンチボットチャレンジやボット検出にどう対処すればよいですか?

レンダリングは、住宅用エグレス、リアルなJavaScript実行、ランダマイズされたフィンガープリンティングを使用して、スクレイプレスのクラウドでサーバーサイドで実行されるため、小売業者に届くリクエストはターゲット市場の住宅IPからの普通のブラウザのように見えます。js_render=Trueを設定すると、レスポンスは事前レンダリングのシェルではなく、ハイドレーション後のDOMになります。そして、計測する市場に国を固定します。

Q4: パイプラインはどのくらいの頻度で実行すべきですか?

価格再設定の決定のための基本的な頻度は日次です。価格が日中に変わるプロモーションウィンドウの監視に対しては、時間毎の頻度が現実的です。SKUごとのコストは単一レンダリングコールに制約されているため、日次の頻度で5,000SKUのバスケットは単一のCronショップの予算内に収まります。より高い頻度はコストを線形で増加させます — 価格決定が実際に消費する頻度を選択してください。

Q5: 小売業者がDOMをローテーションすると何が起こりますか?

ステップ5の小売業者ごとの抽出器だけが変更されます。基本的なスキーマ、ウェアハウスのテーブル、BIタイル、差分クエリ、アラートルールはすべて影響を受けません。小売業者がリリースを行う際にセレクタを再チェックしてください。可能な場合は[data-test='...']属性を優先し、抽出器を変動するレイヤー、スキーマを安定したレイヤーとして扱います。

Q6: 複数の小売業者を並行して実行できますか?

はい。各レンダリングコールはステートレスであるため、オーケストレーターは(市場、競合、SKU)タスクをスレッドプール全体にファンアウトし、アカウントプランが許可するレベルにワーカー数を制限します。並行処理はワーカーを追加することでスケールし、セッションを共有することではありません — 競合する接続はありません。

Q7: プロモ状態と割引率をどうやってキャプチャしますか?

ステップ5の抽出器は、レンダリングされたDOMからプロモバッジを直接読み取り、promo_state"on_sale""clearance""none")とpromo_discount_pctを別々のフィールドとして保存します。ウェアハウスは両方を差分クエリに結合し、「競合は現在販売中か?」対「競合の通常価格は何ですか?」で価格ルールが分岐できるようにします。

Q8: 国際通貨とFXについてはどうですか?

レンダリングされた通貨文字列を各レコードごとに保存します(USDEURJPYGBP)。通貨変換はウェアハウスレイヤーに属し、スクレイパーには含まれません — 生の価格 + 生の通貨 + 市場をNDJSONに保持し、BI側で日次のFXクロスジョインを実行します。これにより、一つの悪いFXレートが全履歴を汚染しないようにします。

Scrapelessでは、適用される法律、規制、およびWebサイトのプライバシーポリシーを厳密に遵守しながら、公開されているデータのみにアクセスします。 このブログのコンテンツは、デモンストレーションのみを目的としており、違法または侵害の活動は含まれません。 このブログまたはサードパーティのリンクからの情報の使用に対するすべての責任を保証せず、放棄します。 スクレイピング活動に従事する前に、法律顧問に相談し、ターゲットウェブサイトの利用規約を確認するか、必要な許可を取得してください。

最も人気のある記事

カタログ