スクレイピングAPIを使ってフライト価格トラッカーを構築する
Specialist in Anti-Bot Strategies
TL;DR:
- フライトウォッチはルートと日付であり、URLではありません。 航空運賃には、小売SKUのような単一の標準ページはなく、同じ旅行は
departure_id、arrival_id、outbound_date、および(往復の場合)return_dateの組み合わせです。以下のパイプラインは、Google Flightsの独自インターフェースをレンダリングおよびスクレイピングする代わりに、Scrapeless Scraping APIのscraper.google.flightsアクターを通じてその組み合わせを直接クエリします。 - 旅行タイプは第一級パラメータであり、URLバリアントではありません。
data_typeは、同じリクエストを片道、往復、複数都市間旅行の間で切り替え、複数都市間旅行は自身のmulti_city_jsonレッグの配列を取ります。小売価格のウォッチではこの次元は必要ありませんが、フライトウォッチでは初日から必要です。 - Google Flightsは独自の変動シグナルを提供します。 応答の
price_insightsオブジェクトは、lowest_priceとprice_level分類(low、typical、high)を個々のオファーであるbest_flightsおよびother_flightsと共に持っています。これにより、パイプラインは「これは今良い運賃です」と、単に「これは前回より安いです」という警告を出すことができます。 - 履歴はルートと日付によってキー付けされ、チェックごとに一度追加されます。
(departure_id, arrival_id, outbound_date, return_date)の組み合わせごとに1レコードのJSONLログがあれば、ランニングロウを計算し、ドロップを検出するのに十分であり、ウォッチリストが数ルートを超えるまでデータベースは必要ありません。 - ドロップ、または
price_levelがlowに切り替わると、Webhookが発火します。 アラートの決定は数値比較をGoogle自身の定性的シグナルと組み合わせ、その後受信するエンドポイントに一度投稿されます。 - スケジューラがループを実行します。何も持続的に運転する必要はありません。 Cron、タスクスケジューラ、またはサーバーレスのタイマーがチェックを一定の間隔で実行し、終了します — パイプラインには長期間のプロセスはありません。
- 無料で開始可能。 新しいScrapelessアカウントには無料のScraping APIクレジットが含まれています — app.scrapeless.comにサインアップしてください。
はじめに:運賃は1次元ではなく2次元の動くターゲット
小売価格は単一のページに添付された単一の数値です。フライト運賃はルートと日付の関数であり、同じルートは同じ検索内のすべての出発日で異なる価格が付けられる場合があります。これが、Googleのフライト価格追跡機能が存在する理由です:航空会社は需要に対してシート在庫を継続的に再価格設定し、1回だけチェックして予約を行うショッパーはタイミングを推測しているのです。Googleのトラッカーは、追跡されたルートが移動するときの要約をメールで送信しますが、生データ、Webhook、またはそのシグナルを他の稼働中のものと組み合わせる方法は提供しません。
この記事では、Google Flightsのインターフェースではなく、特定のルートと日付ウィンドウの上にプログラム的にその仕事を行う小さなパイプラインを構築します。ScrapelessのScraping APIは、このサーフェス用に特別に設計されたアクターを提供します — scraper.google.flights — これはルートと日付の組み合わせを受け入れ、構造化された運賃データを返します:個々のオファー、オファーごとの航空会社と所要時間、そして運賃に関するGoogleの独自の価格レベルの読み取りです。パイプラインはそのアクターにクエリし、運賃シグナルを抽出し、それらをルートと日付でキー付けされた履歴ログに追加し、その読み取りがアラートの価値があるかどうかを判断し、必要な場合にWebhookを発火させます。スケジューラが一定の間隔でチェックを実行します。
アクターは、この記事の調査に使用されたアカウントに対してライブの disabled actor 応答を返しました — プランティアのギャップであり、壊れたパイプラインではありません — そのギャップは重要なポイントで文書化されており、軽視されていません。他のすべてのステージは、実行されたリアルなPythonに対して実行されています。
あなたができること
- 個人の運賃ウォッチ。 特定のルートと日付ウィンドウを追跡し、価格が実際に有用な方向に動いたときのみ通知を受け取ります。
- 柔軟な日付のショッピング。 候補となる日付の範囲で同じルートを実行し、範囲全体で
price_insights.price_levelを比較して、最も安い窓口を見つけます。単一の日付だけではありません。 - 法人旅行の監視。 旅行デスクが、定期的な通勤やクライアントのルートを監視し、予約された旅行のルートがその後ドロップした場合にフラグを立てて再予約を促します。
- 複数都市の旅程追跡。
multi_city_jsonは、同じリクエストで多レグの旅行を表現できるため、3つまたは4つのセグメントを含む旅程は、手作業でつなぎ合わせることなく1回の呼び出しで済みます。 - 運賃履歴データセット。 追加のみのログは、後のトレンド分析や「今すぐ予約するか待つか」というヒューリスティックに役立つ、ルートごとの時系列としても機能します。
なぜScrapeless Scraping APIなのか
Scrapeless Scraping APIは、与えられたターゲットに対して構造化されたJSONを返すサイト固有のアクター — scraper.<site> — を公開します。フライトデータに特に関しては、つまり:
- ルートと日付のリクエスト、ブラウザセッションではありません。 コールは、認証されたPOSTでJSONボディを持つものであり、レンダリングするページはなく、アンカーを配置するセレクターもなく、セッションを維持する必要もありません。
- リクエスト形状に組み込まれた旅行タイプ。
data_typeは、ドキュメント化されたパラメータとして片道(2)、往復(1)、および多都市(3、multi_city_json付き)をカバーし、ケースごとの別々のスクレイピングロジックではありません。 - ターゲットサイト自体が計算する価格分類。
price_insights.price_levelは、運賃がそのルートに対して低い、通常、または高いかどうかについてのGoogleの独自の判断であり、パイプラインはそれを基にして閾値をゼロから推測する代わりに利用できます。 - 分散スタックの代わりに1回のリクエスト。 航空運賃データは、GDSや航空会社の直接フィードの寄せ集めを通じてコール元に到達します。そのような断片化された配信に対して、IATAの新しい配信能力標準が航空業界側で標準化を目的としています。単一の構造化されたアクターコールは、そのスタックを完全に回避し、読み取り専用の価格ウォッチを提供します。
無料プランのAPIキーを取得するには、app.scrapeless.comにアクセスしてください。Scraping API製品ページには、このパイプラインが呼び出すアクターのファミリーが紹介されており、この特定のアクターのリクエスト形状は、Google Flights APIリクエストガイドで詳しく説明されています。
前提条件
- Python 3.10以降、さらに
requestsパッケージ(pip install requests)。 - ScrapelessアカウントとAPIキー、
SCRAPELESS_API_KEY環境変数から読み取られます。プロダクショントラフィックを向ける前に、そのアカウントのプランでscraper.google.flightsアクターが有効であることを確認してください — 下のFetchの注意事項を参照してください。 - アラートを受信するためのWebhookエンドポイント(Slack、Discord、またはJSON POSTを受け入れる任意のリレー)。
- チェックを定期的に実行するためのcron対応ホスト、Windowsタスクスケジューラ、またはサーバーレススケジューラ。
パイプラインの概要
パイプラインは、6つのステージで直線的に接続されています:フェッチ → ディスカバー → 抽出 → 変換 → ストレージ → 決定 → アラート。スケジューラが全体を定期的に駆動します。
- フェッチ — ルート、日付(または日付ウィンドウ)、旅行タイプを
scraper.google.flightsにPOSTします。 - ディスカバー — 旅行タイプのエンベロープを読み返します:応答が実際にどの
best_flights、other_flights、price_insightsを埋めたかを確認します。 - 抽出 — 最も安いオファーの価格とGoogle自身の
price_level分類をそのエンベロープから引き出します。 - 変換 — 読み取りをルートと日付でキー付けされた1つの標準レコードに正規化します。
- ストレージ — レコードを履歴ログに追加します。
- 決定とアラート — 履歴と比較し、読み取りが浮上する価値があるときにWebhookを発火させます。
フェッチ:ルートと日付ウィンドウをクエリする
リクエストは1つの認証されたPOSTです。アクター名はactorに、ルートと日付のパラメータはinputに入ります。data_typeは往復の場合は1、片道の場合は2です。往復の場合、return_dateも含まれます。
python
import os
import requests
API_URL = "https://api.scrapeless.com/api/v1/scraper/request"
def fetch_route(departure_id: str, arrival_id: str, outbound_date: str,
return_date: str | None = None, gl: str = "us", hl: str = "en") -> dict:
payload = {
"actor": "scraper.google.flights",
"input": {
"departure_id": departure_id,
"arrival_id": arrival_id,
"data_type": 1 if return_date else 2, # 1 = 往復、2 = 片道
"outbound_date": outbound_date,
"gl": gl,
"hl": hl,
},
}
if return_date:
payload["input"]["return_date"] = return_date
headers = {"Content-Type": "application/json", "x-api-token": os.environ["SCRAPELESS_API_KEY"]}
response = requests.post(API_URL, headers=headers, json=payload, timeout=60)
response.raise_for_status()
return response.json()
outbound_dateとreturn_dateは、RFC 3339が定義するインターネット日付と時刻の交換の平易なYYYY-MM-DDカレンダーデート形式に従います — 時間成分はなく、運賃検索は出発の瞬間ではなく、出発日を基にするからです。多都市の旅程は、単一のレグフィールドをdata_type: 3とmulti_city_json配列に置き換え、各レグごとにそれぞれのdeparture_id、arrival_id、および日付を持つオブジェクトを持ちます。
前提条件のギャップ: 上記の呼び出しを実際のルートに対して実行したところ、パイプラインの検証に使用したアカウントに対してリアルなHTTP 400が返されました:{"code": 14002, "message": "disabled actor: scraper.google.flights"}。このメッセージは、認識されていないアクター名が返すエラー("invalid actor: <name>" — 意図的にいくつかの虚構のバリエーションを要求することで確認済み)とは異なります。これは、アクター自体が実在し、カタログに登録されていることを意味します;ここで使用されている無料プランからは制限されているだけで、欠落しているわけではありません。アカウントの基盤となるリクエストメカニズムについては疑問の余地がありません — 同じキーと同じエンドポイントでの研究中に、兄弟アクターscraper.google.searchはリアルなHTTP 200を返し、オーガニック結果を提供しました。プロダクショントラフィックを構築する前に、アクターが目的のプランで有効であること(GET /api/v1/meがプランとクレジット残高を報告します)を確認し、Discover以降のすべてを、このセッションでライブでキャプチャしたページではなく、アクターの文書化されたレスポンス形状に対して実行されているものとして扱ってください。
Discover:トリップタイプエンベロープの読み取り
往復または片道の検索は、2つの配列のオファー — best_flights と other_flights — に加えて、price_insights オブジェクトを返します。多都市間の検索は、単一のレッグではなく、それぞれの旅程ごとに同等の構造を返します。何かを抽出する前に、パイプラインはレスポンスが実際にどのキーを populated したかを知る必要があります。狭いルートや奇妙な日付は、other_flights が空で best_flights のみがオファーを持つ場合があります。
python
def envelope_shape(payload: dict) -> dict:
return {
"has_best": bool(payload.get("best_flights")),
"has_other": bool(payload.get("other_flights")),
"has_insights": bool(payload.get("price_insights")),
}
if __name__ == "__main__":
sample = {"best_flights": [{"price": 312}], "other_flights": [], "price_insights": {}}
print(envelope_shape(sample))
best_flights のみが populated した狭いレスポンスに対して実行すると、envelope_shape は {'has_best': True, 'has_other': False, 'has_insights': False} を返します — これは下記の抽出ステージが耐えなければならない正確なケースです。
これは一行の健全性チェックですが、重要です:best_flights が populated されていないエンベロープから「最も安いオファー」を抽出すると、間違った数値ではなく None が生成されます。これは、抽出ステージが常に1つの配列が存在することを前提にせず、両方の配列を確認する場合に限ります。
Extract:レスポンスから運賃信号を引き出す
抽出ステージは、両方の配列とその隣の price_insights フィールドから最も安いオファーを引き出します。以下のフィクスチャは、このアクターファミリーのために文書化されたレスポンス形状を反映しています — best_flights と other_flights は {flights: [...], price, layovers} オブジェクトの配列として、price_insights は lowest_price、price_level、および typical_price_range を持ちます — リアルな呼び出しは、上記のギャップであるためであり、このセッションでキャプチャされたページではありません。
python
# 文書化されたレスポンス形状に一致する例示的フィクスチャ(フェッチの下の前提条件ギャップの注記を参照) — リアルなキャプチャではありません。
FIXTURE_RESPONSE = {
"best_flights": [
{
"flights": [{
"departure_airport": {"id": "JFK", "time": "2026-08-10 08:00"},
"arrival_airport": {"id": "LAX", "time": "2026-08-10 11:24"},
"airline": "Example Air",
"flight_number": "EX 101",
"duration": 384,
"travel_class": "Economy",
}],
"price": 312,
"layovers": 0,
}
],
"other_flights": [
{"flights": [{"airline": "Sample Airways", "flight_number": "SA 220"}], "price": 349, "layovers": 1}
],
"price_insights": {
"lowest_price": 289,
"price_level": "low",
"typical_price_range": [295, 430],
},
}
def extract_fares(payload: dict) -> dict:
best = payload.get("best_flights", [])
other = payload.get("other_flights", [])
insights = payload.get("price_insights", {})
cheapest_offer = min([*best, *other], key=lambda offer: offer["price"], default=None)
return {
"cheapest_price": cheapest_offer["price"] if cheapest_offer else None,
"cheapest_layovers": cheapest_offer["layovers"] if cheapest_offer else None,
"offer_count": len(best) + len(other),
"price_insights_lowest": insights.get("lowest_price"),
"price_level": insights.get("price_level"),
"typical_price_range": insights.get("typical_price_range"),
}
if __name__ == "__main__":
print(extract_fares(FIXTURE_RESPONSE))
フィクスチャに対して実行すると、extract_fares は以下を返します:
text
{'cheapest_price': 312, 'cheapest_layovers': 0, 'offer_count': 2,
'price_insights_lowest': 289, 'price_level': 'low', 'typical_price_range': [295, 430]}
次の英語のテキストを翻訳します:
cheapest_price(実際に返された最低の単独オファー)とprice_insights_lowest(Googleのルートに対する独自のフロア)との間のギャップに注意してください。これらはほとんどの場合、正確に一致することはありません。price_insightsは、この一つの応答の特定のオファーよりもルートについてのより広い読みを反映しているため、両方のフィールドを保持し、一つの数字にまとめるのではなく、両方を保持してください。
変換: ルート・日付レコードに正規化
フライトウォッチのアイデンティティは、(departure_id, arrival_id, outbound_date, return_date)のタプルであり、URLではありません。変換ステージでは、このキーを構築し、ストレージと比較が再び導出する必要がないようにカノニカルなレコード形状を作成します。
python
from datetime import datetime, timezone
def to_record(route: dict, fares: dict) -> dict:
return {
"departure_id": route["departure_id"],
"arrival_id": route["arrival_id"],
"outbound_date": route["outbound_date"],
"return_date": route.get("return_date"),
"trip_type": "round_trip" if route.get("return_date") else "one_way",
"price": fares["cheapest_price"],
"price_level": fares["price_level"],
"currency": "USD",
"checked_at": datetime.now(timezone.utc).strftime("%d-%b-%Y %H:%M UTC"),
}
def route_key(record: dict) -> str:
return f"{record['departure_id']}-{record['arrival_id']}:{record['outbound_date']}:{record.get('return_date')}"
if __name__ == "__main__":
sample_route = {"departure_id": "JFK", "arrival_id": "LAX",
"outbound_date": "2026-08-10", "return_date": "2026-08-17"}
sample_fares = {"cheapest_price": 312, "price_level": "low"}
record = to_record(sample_route, sample_fares)
print(record)
print(route_key(record))
フィクスチャールート(JFKからLAX、8月10日出発、8月17日帰り)と上記の抽出された運賃に対してto_recordを実行すると、price: 312、price_level: 'low'、trip_type: 'round_trip'を持つレコードが返され、route_keyはJFK-LAX:2026-08-10:2026-08-17を返します。同じルートと出発日での片道ウォッチは、瞬時にreturn_dateがない場合に異なるキーを生成します。これがポイントです。同じルートの2種類の旅行は、1つではなく2つの異なるウォッチです。
ストレージ: ルートと日付によるキー付きの追加のみのログ
ストレージは、リテール価格ウォッチが使用する追加のみのJSONLパターンと同じですが、唯一の違いは、ルックアップがURLではなくルート・日付キーにフィルターされることです。
python
import json
HISTORY_FILE = "flight_history.jsonl"
def append_history(record: dict) -> dict:
with open(HISTORY_FILE, "a", encoding="utf-8") as f:
f.write(json.dumps(record) + "\n")
return record
def load_history(key: str) -> list[dict]:
rows = []
try:
with open(HISTORY_FILE, encoding="utf-8") as f:
for line in f:
row = json.loads(line)
if route_key(row) == key:
rows.append(row)
except FileNotFoundError:
pass
return rows
if __name__ == "__main__":
open(HISTORY_FILE, "w").close() # このデモを空のログから始める
sample_record = {"departure_id": "JFK", "arrival_id": "LAX", "outbound_date": "2026-08-10",
"return_date": "2026-08-17", "trip_type": "round_trip",
"price": 312, "price_level": "low", "currency": "USD",
"checked_at": "13-Jul-2026 14:28 UTC"}
append_history(sample_record)
append_history(sample_record)
with open(HISTORY_FILE, encoding="utf-8") as f:
print(f"{len(f.readlines())} 行が {HISTORY_FILE} に2回のチェックの後に存在します")
同じレコードを2回追加し、キーで再読み込みすると、挿入順に両方の行が返されるため、ログは上書きされるのではなく蓄積されることが確認できます。これはURLキー付きウォッチが与える追加のみのファイルの保証と同じことです。ただし、異なるキーでフィルタリングされています。数件のルートを超えた場合、ファイルを小さなSQLiteテーブルに置き換え、(departure_id, arrival_id, outbound_date, return_date)を複合キーとして使用してください。読み取りおよび書き込みの形状は同じままです。
決定: 歴史およびGoogleの独自の価格シグナルと比較
リテール価格ウォッチには、1つの決定ルールがあります。それは、新しい価格がこれまでに見た最低の価格を下回っているかどうかです。運賃ウォッチには、2つ目のシグナルが利用可能です -- price_level -- したがって、決定ステージは両方をチェックします。実際の数値が現在の最低値を下回るか、price_level: "low"にすでに達している最初の読み取りかを確認します。
python
def is_price_drop(history: list[dict], current_price: float, current_level: str) -> dict:
prior_prices = [r["price"] for r in history if r.get("price") is not None]
previous_low = min(prior_prices) if prior_prices else None
numeric_drop = (
current_price is not None
and previous_low is not None
and current_price < previous_low
)
level_signal = current_level == "low"
return {
"dropped": numeric_drop,
"worth_alerting": numeric_drop or (level_signal and previous_low is None),
"current": current_price,
"previous_low": previous_low,
json
"price_level": 現在のレベル,
}
if __name__ == "__main__":
history = [{"price": 349}, {"price": 340}]
print(is_price_drop(history, current_price=312, current_level="低"))
349および340の2つの以前の読み値に対して、新しい読み値312と price_level: "低" は {'dropped': True, 'worth_alerting': True, 'current': 312, 'previous_low': 340, 'price_level': '低'} を返します。worth_alerting フィールドは、dropped よりも意図的に広い範囲を持ち、Google自身の分類がすでにそれを低と呼んでいる場合には、ルートの非常に最初の読み値にも発動します。これは、稼働中の低比較はまだ比較するものがないためですが、定性的シグナルは存在します。その区別は、航空券市場におけるオフライン動的価格設定に関する研究がカテゴリの構造的特徴として扱っています: 航空座席の在庫は、固定の販売カレンダーではなく、需要と残りのキャパシティに対して再価格設定されるため、単一のスナップショットでも価格履歴と比較せずにすでに情報を提供できます。
アラート:ドロップ時にWebhookを発火させる
アラートメカニズムは、小売ウォッチが使用するのと同じ単一の requests.post です — 一つのHTTP呼び出し、キューなし、ブローカーなし — メッセージは製品名の代わりにルートと日付のアイデンティティから構築されます。
python
def send_alert(route_label: str, decision: dict, webhook_url: str) -> int:
message = (
f"運賃ウォッチ: {route_label}\n"
f"現在 ${decision['current']} (以前は ${decision['previous_low']}), "
f"Googleの価格レベル={decision['price_level']}"
)
response = requests.post(webhook_url, json={"text": message}, timeout=15)
response.raise_for_status()
return response.status_code
raise_for_status() は、ミスコンフィギュレーションされたリレーでアラートを静かに飲み込む代わりに、壊れたWebhookエンドポイントを即座に浮上させます。{"text": message} ボディは、一般的なSlack/DiscordのインカミングWebhookの形状と一致しています。受信エンドポイントが期待する形にJSONを調整してください。
スケジュール: 旅行によって制約された周期でポーリングする
アラートへの取得を一つの関数に配線することで、スケジューラが実行できる単一の check_route() 呼び出しを作成します。
python
def check_route(departure_id, arrival_id, outbound_date, return_date=None, webhook_url=None):
payload = fetch_route(departure_id, arrival_id, outbound_date, return_date)
fares = extract_fares(payload)
route = {"departure_id": departure_id, "arrival_id": arrival_id,
"outbound_date": outbound_date, "return_date": return_date}
record = append_history(to_record(route, fares))
decision = is_price_drop(load_history(route_key(record)), record["price"], record["price_level"])
if decision["worth_alerting"] and webhook_url:
send_alert(f"{departure_id}-{arrival_id} {outbound_date}", decision, webhook_url)
return record, decision
ほとんどの個人のウォッチには、毎日の実行で十分です。複数の定期ルートを追跡する法人旅行デスクは、より頻繁に実行できます。小売SKUとは異なり、フライトウォッチには自然な終了もあります:出発日が過ぎると、そのルートはリアルタイムの検索ではなくなり、そのための履歴は完了します。 crontab エントリは、LinuxまたはmacOS上での最も簡単なドライバであり、プラットフォームの任意のスケジュールされたジョブが使用する同じ5フィールドのPOSIX crontab時間仕様を使用します:
bash
# crontab -e — 一日二回、07:00と19:00にチェック
0 7,19 * * * cd /opt/fare-watch && /usr/bin/python3 check.py >> watch.log 2>&1
Windowsのタスクスケジューラは、同等のトリガーで同じコマンドを実行し、サーバーレスタイマーでも機能します — スクリプトは履歴ファイル以外に状態を保持しないため、ステートレスな呼び出しが適しています。複数のルートのウォッチリストに対しては、リスト上で check_route() をループし、APIキーのレート制限が快適に保たれるように同時実行を控えめに保つことができます。
取得するもの
各チェックは、URLではなくルートと日付でキー付けされた履歴ログに1つのレコードを追加します。
json
// スキーマは、to_record()/append_history() が書き込む内容を正確に反映しています。
// フィールド値は、上記のフィクスチャと一致しており、ライブ読み値ではありません。
{
"departure_id": "JFK",
"arrival_id": "LAX",
"outbound_date": "2026-08-10",
"return_date": "2026-08-17",
"trip_type": "往復",
"price": 312,
"price_level": "低",
"currency": "USD",
"checked_at": "2026年7月13日 15:01 UTC"
}
このスケールで実行する前に知っておくべきいくつかのポイント:
priceとprice_insights_lowestは乖離する可能性があります。 前者はこの特定の応答が返した最も安いオファーです。後者はGoogleのルートに対するより広い見解です。ダウンストリームユースケースが違いを気にするなら、両方を保存してください。
- **保存された金額は、基本料金ではなく全額であるべきです。** アメリカの航空会社が運賃を広告する際は、<a href="https://www.govinfo.gov/content/pkg/CFR-2011-title14-vol4/xml/CFR-2011-title14-vol4-sec399-84.xml" rel="nofollow"><strong>連邦の全運賃広告規則</strong></a>に従い、乗客が支払う全体の価格を引用しなければならず、単に基本料金を割り引いたものではありません — パイプラインが保存するすべてのフィールドを`price`として扱うことで、チェック間の比較が、一日の基本運賃と別の日の全額合計を混同しないようにします。
- **複数都市は形状を変えるものであり、パイプラインを変えません。** `data_type: 3` と `multi_city_json` は、単一レグの応答ではなく、旅程レベルの応答を生成します; 各レグの価格を合計に折り込むために `to_record` を拡張し、旅程を一つのフラットな運賃として扱わないようにします。
- **`gl`と`hl`は単なる表現だけでなく、通貨と言語に影響を与えます。** 小売ウォッチが`proxy_country`をピン留めするのと同じ方法で両方をピン留めし、チェック間の比較が同じ市場に留まるようにします。
- **ヌル可能な価格はゼロではありません。** 欠落している `cheapest_price` を `None` として扱います; これは抽出ステージが既に行っている通りです — 時折の奇妙な応答は、実行中の最低価格に誤ったゼロを注入してはいけません。
## 結論: 一つのルート、一つの日、一つの決定
パイプラインは、他のどの価格ウォッチと同じ形状に簡素化されます — 取得、抽出、保存、決定、アラート、スケジュール — カテゴリに対して「ウォッチ」のアイデンティティが再定義され、URLの代わりにルートと日付のペアが、単一リクエストの形状の代わりに旅の種類パラメータが利用され、平易な数値比較と共に、第二のターゲット提供信号(`price_level`)が利用可能になります。 ウォッチリストをより多くのルート、日付、または複数都市の旅程に拡張することは、変更されずにすべてのステージを再利用します; 変更するのはFetchに渡されるパラメータのみです。
## AI駆動のデータパイプラインを構築する準備はできましたか?
コミュニティに参加して、Scrapelessでデータパイプラインを構築している開発者と情報交換しましょう: <a href="https://discord.gg/VU2vtbq7Q2">Discord</a> · <a href="https://t.me/scrapeless">Telegram</a>。
<a href="https://app.scrapeless.com/passport/login/?utm_source=website&utm_medium=blog&utm_campaign=scraperapi&utm_term=flight-price-tracker">app.scrapeless.com</a>にサインアップして無料のスクレイピングAPIクレジットを取得し、アカウントのプランで `scraper.google.flights` アクターが有効になっていることを確認し、上記のステージをウォッチリストに必要なルートに適応させます。 <a href="https://www.scrapeless.com/ja/pricing?utm_source=website&utm_medium=blog&utm_campaign=scraperapi&utm_term=flight-price-tracker">料金</a>でプランの詳細を確認し、現在のアクターリファレンスについては<a href="https://docs.scrapeless.com/en/scraping-api/quickstart/introduction/?utm_source=website&utm_medium=blog&utm_campaign=scraperapi&utm_term=flight-price-tracker">スクレイピングAPIドキュメント</a>を参照してください。
## FAQ
**Q: `scraper.google.flights`は実際の文書化されたアクターですか?**
はい。意図的に作られたアクター名は `"invalid actor: <name>"` を返します; `scraper.google.flights` をリクエストすると、 `"disabled actor: scraper.google.flights"` が返されます — これは識別されたカタログされたアクターにのみ適用される明確なエラーです。それは存在しないのではなく、いくつかのプランでは利用制限されています; プロダクショントラフィックをそれに集中させる前に、ターゲットアカウントのプランに対して有効であることを確認してください。
**Q: 小売ウォッチがするように、なぜルートと日付で履歴をキーにするのですか、URLではなく?**
小売SKUが持つような単一の公式ページがないためです。同じルートは出発日、帰り日、旅行タイプによって異なる価格になり、したがって「ウォッチ」のアイデンティティはその組み合わせでなければなりません。一つのURLではありません。
**Q: `price_insights.price_level`は単なる「前回より安い」という比較に何を追加しますか?**
それは、Google自身の、現在の運賃がそのルートにとって低い、典型的、または高いかを分類したもので、パイプラインが受け取った単一の応答を超えたデータから計算されたものです。これを実行中の低価格比較に組み合わせることで、ルートの最初の読み取りが行動可能となり、単にその後のものだけではなくなります。
**Q: これで複数都市の旅程を処理できますか?**
はい、リクエストレベルで — `data_type: 3` と、レグの `multi_city_json` 配列はこのアクターファミリーのための文書化されたパラメータ形状です。複数レグの旅程を一つの比較可能な合計に折り込むために `to_record` を拡張することは、複数都市ウォッチが必要とする唯一の追加ロジックです。
**Q: チェックはどのくらいの頻度で実行すべきですか?**
ほとんどの個人運賃ウォッチには、1日または1日2回のチェックが適しています。フライトウォッチには自然な停止もあります; 出発日が過ぎれば、そのルートはもはやライブ検索ではなく、小売SKUは無期限に監視可能です。
**Q: AIエージェントなしでこれを実行できますか?**
はい。FetchからScheduleまでのPythonは、最初から最後まで独立して実行されます — 呼び出し、抽出、保存、決定、アラートを行い、スケジューラーがリズムをドライブします。エージェントは自然言語でルートと日付の選択を促進する便利な方法ですが、パイプライン自体はPythonとスケジューラー以外は何も必要ではありません。
Scrapelessでは、適用される法律、規制、およびWebサイトのプライバシーポリシーを厳密に遵守しながら、公開されているデータのみにアクセスします。 このブログのコンテンツは、デモンストレーションのみを目的としており、違法または侵害の活動は含まれません。 このブログまたはサードパーティのリンクからの情報の使用に対するすべての責任を保証せず、放棄します。 スクレイピング活動に従事する前に、法律顧問に相談し、ターゲットウェブサイトの利用規約を確認するか、必要な許可を取得してください。


