PythonでGoogle Flightsをスクレイピングする方法:コード作成から価格アラートまで

最終更新日 June 3, 2026
PythonでGoogle Flightsをスクレイピングする方法:コード作成から価格アラートまで

国内線のたった1ルートでも、48時間のあいだに価格が17回も付け替わる——そんなことが現実に起きます。Google が Flights API を畳んだのは 2018 年ですが、航空券の値段はそれ以降も一向に落ち着く気配がありません。https://mightytravels.com/ この絶え間ない揺れをプログラムで掴まえたいなら、今のところ実質的にスクレイピング以外の道はないのです。

Google から航空券データを引き出す方法を、私はこれまで時間をかけて色々と試してきました。とりわけ潮目が変わったのが、2025 年 1 月に Google が SearchGuard を導入した瞬間です。本ガイドでは、Playwright を軸に Google Flights の実用的な Python スクレイパーを組み立てる手順、多くの人がつまずく bot 対策のかわし方、そしてそれを自動の価格追跡ツールとアラート機能へと育て上げるところまでを通しで解説します。コードを書かずに済ませたい方のために、Thunderbit を使って約 2 分で同じ結果に行き着くノーコードの手順も用意しました。

なぜ Python で Google Flights をスクレイピングするのか?

航空券検索の中心に陣取っているのが Google Flights です。米国のモバイル表示シェアは 47.6% まで一気に伸び、主要な OTA をことごとく追い抜きました。その土台となる旅行メタサーチ市場は、年平均 30.2% という勢いで成長を続け、2024 年には 83.3 億ドル の規模に達しています。ところが肝心の QPX Express API はというと、2018 年 4 月 10 日をもって完全に終了 してしまい、このデータを公式にプログラムで取得する手立ては残されていません。

価格の振れ幅も見逃せません。まったく同じ旅程であっても運賃は 最大 50% 変動し、最安値と最高値の差は平均でおよそ 20 ドルにのぼります。たとえば Delta は、ダイナミックプライシングのために 77 もの運賃バケットを使い分けているほどです。2026 年初頭の段階で、米国往復航空券の平均は 408 ドル、運賃は前年同期と比べて 14.9% も高い 水準にあります。

圧倒的なプラットフォーム、消えてしまった API、そして激しい価格変動。この3つが折り重なった結果、Python で Google Flights をスクレイピングするというテーマは、GitHub でも旅行系フォーラムでも根強い人気を集めています。

どんな人に、どんなメリットがあるのか。整理すると次のとおりです。

ユーザータイプ活用例主なメリット
個人旅行者特定ルートの価格を継続的に追跡1便あたり平均 $50 節約
旅行代理店競合価格のインテリジェンス収集リアルタイムの運賃パリティ監視
企業出張チームルート別のコスト最適化出張費を 10〜30% 削減
開発者運賃比較アプリの構築価格データへのプログラム的アクセス
研究者航空運賃の変動分析学術研究・市場調査

フォーラムをのぞくと、スクレイピングに踏み切った動機がかなり率直につづられています。「Google Flights API が終わったので、代わりに Web スクレイピングを使うべきだ」 といった趣旨の投稿は、繰り返し目に入ってきます。しかも、見返りは決して小さくありません。Hopper は毎日 50 億件を超える価格情報を分析し、95% の予測精度をうたっています。一方、Expedia の 2026 年データによれば、国内線は出発の 8〜15 日前に予約すると 約 25 ドル安く なるとされています。

Google Flights からどんなデータが取得できるのか?

Google Flights の検索結果ページには、見た目の印象以上に多彩なデータ項目が詰まっています。一般に取得できるのは、次のような情報です。

  • 航空会社名(ロゴ含む)
  • 出発時刻 と空港コード
  • 到着時刻 と空港コード
  • 総所要時間
  • 乗継回数 と乗継詳細(空港、所要時間、深夜帯かどうか)
  • 航空券価格(通貨別)
  • CO2 排出量(kg CO2e、標準的な便との比較差分付き)
  • 座席クラス、便名、機材モデル
  • 足元スペース の仕様
  • 設備(Wi-Fi、電源、機内ストリーミング)
  • 価格帯指標(低・標準・高)
  • 遅延警告(「30 分以上遅延しやすい」など)

実際に取れる項目は、ルート・日付・チケット種別(片道か往復か)によって入れ替わります。1回ぶんのスクレイピング結果を JSON に落とすと、おおよそ次のような姿になります。

{
  "search_date": "2026-04-16",
  "route": "SFO-JFK",
  "departure_date": "2026-05-15",
  "flights": [
    {
      "airline": "United Airlines",
      "flight_number": "UA123",
      "departure_time": "08:00",
      "departure_airport": "SFO",
      "arrival_time": "16:35",
      "arrival_airport": "JFK",
      "duration_minutes": 335,
      "stops": 0,
      "price_usd": 287,
      "price_level": "low",
      "co2_kg": 156,
      "co2_vs_typical": "-12%",
      "travel_class": "Economy"
    }
  ]
}

Python 環境の準備

スクレイピングのコードに取りかかる前に、いくつか整えておくべきものがあります。

前提条件:

  • 難易度: 中級
  • 所要時間: チュートリアル全体で約 1〜2 時間
  • 必要なもの: Python 3.7 以上、Python の基礎知識、Chromium 系ブラウザ

必要なライブラリをインストールする

ブラウザ自動化には Playwright を採用します。というのも、Google Flights は中身が 100% JavaScript で描画される作りで、ふつうの HTTP リクエストでは使い物になる情報が返ってこないからです。これに加えて、いくつかの補助ツールも入れておきます。

pip install playwright playwright-stealth pandas
playwright install chromium
  • Playwright — ヘッドレスブラウザ自動化、JavaScript 描画対応、待機処理が標準搭載
  • playwright-stealth — 一般的な bot 検知シグナルを補正
  • pandas — 後でデータ分析と CSV 出力に使う

Selenium や requests ではなく Playwright を選ぶ理由

requestsBeautifulSoup の組み合わせだけでは、Google Flights には歯が立ちません。ページの中身がまるごと JavaScript で描かれるため、結局のところ実ブラウザが要るのです。

機能PlaywrightSeleniumrequests + BS4
JS 描画完全対応完全対応なし
速度全体で 42% 高速基準この用途では非対応
非同期対応標準対応逐次のみ非対応
メモリ使用量30% 少ない多め最小
bot 検知回避良好(stealth 利用時)検知されやすい非対応

速さ、モダンさ、そして非同期処理への強さ。Playwright はこの三拍子がそろっており、Google Flights という用途においては、もはや迷う余地のない第一候補です。

ステップごとの手順:Python で Google Flights をスクレイピングする方法

いよいよチュートリアルの本編です。ここからスクレイパーを一段ずつ積み上げていきます。

google-flights-scraping-workflow.webp

ステップ 1:データクラスを定義する

手始めに、検索条件と航空券データを Python の dataclass で型として切り出しておきます。こうしておくとコードの見通しがよくなり、後からの拡張もぐっと楽になります。

from dataclasses import dataclass, field
from typing import Optional, List

@dataclass
class SearchParams:
    origin: str          # 例: "SFO"
    destination: str     # 例: "JFK"
    departure_date: str  # 例: "2026-05-15"
    return_date: Optional[str] = None
    trip_type: str = "one-way"  # "one-way" または "round-trip"
    travel_class: str = "economy"

@dataclass
class FlightData:
    airline: str = ""
    departure_time: str = ""
    arrival_time: str = ""
    duration: str = ""
    stops: str = ""
    price: str = ""
    co2_emissions: str = ""

各フィールドは、ページから抜き取る情報に一対一で対応しています。最初にこの器をきちんと決めておけば、あとで雑な辞書をあちこち持ち回る羽目になりません。

ステップ 2:Google Flights の URL 構造を理解する

Google Flights は、検索条件を tfs URL パラメータに Base64 エンコード済みの Protobuf として詰め込んでいます。これを逆解析するアプローチもありますが、もっと気楽なのは、自然文の検索 URL を自分で組み立てるやり方です。

いちばん素朴な形は、こうなります。

https://www.google.com/travel/flights?q=flights+from+SFO+to+JFK+on+2026-05-15&curr=USD

もう少しきめ細かく制御したいなら、URL をプログラムで生成してしまいましょう。

def build_flights_url(origin: str, destination: str, date: str) -> str:
    base = "https://www.google.com/travel/flights"
    query = f"flights from {origin} to {destination} on {date}"
    return f"{base}?q={query.replace(' ', '+')}&curr=USD"

もう一方の Protobuf 逆解析は、より精密な制御ができる反面、Google が内部形式に手を入れた途端に壊れます。GitHub の fast-flights のようなライブラリは Protobuf をデコードして HTML パースを回避しますが、こちらはどちらかというと上級者向けの手段です。

ステップ 3:ブラウザを起動して Google Flights に移動する

Playwright のセットアップは以下のとおりです。最初の一歩から検知リスクを抑えるため、playwright-stealth を組み込んでいます。

import asyncio
from playwright.async_api import async_playwright
from playwright_stealth import Stealth

async def scrape_flights(params: SearchParams) -> List[FlightData]:
    async with Stealth().use_async(async_playwright()) as pw:
        browser = await pw.chromium.launch(
            headless=True,
            args=[
                "--disable-blink-features=AutomationControlled",
                "--disable-dev-shm-usage",
                "--no-first-run",
            ]
        )
        context = await browser.new_context(
            viewport={"width": 1920, "height": 1080},
            user_agent=(
                "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                "AppleWebKit/537.36 (KHTML, like Gecko) "
                "Chrome/125.0.0.0 Safari/537.36"
            ),
            locale="en-US",
            timezone_id="America/New_York",
        )
        # 同意ポップアップを避けるため Cookie を先に設定
        await context.add_cookies([{
            "name": "SOCS",
            "value": "CAESHwgBEhJnd3NfMjAyNTAyMjctMF9SQzIaBXpoLUNOIAEaBgiAy6O-Bg",
            "domain": ".google.com",
            "path": "/"
        }])
        page = await context.new_page()

本番運用では headless で回します(デバッグ中だけ headless=False に切り替えると便利です)。あわせて、現実味のある viewport と user agent を指定し、さらに SOCS Cookie を先回りで仕込むことで、同意ポップアップを出させないようにしています。bot 対策の細部については、記事の後半でじっくり掘り下げます。

ステップ 4:検索結果に移動する

組み立てた URL を開き、航空券の結果が描画されるのを待ち受けます。

        url = build_flights_url(
            params.origin, params.destination, params.departure_date
        )
        await page.goto(url, wait_until="networkidle")

        # 航空券結果が読み込まれるまで待つ
        await page.wait_for_selector(
            "li.pIav2d", timeout=15000
        )

ここでタイムアウトに引っかかる原因は、たいてい2つです。同意ポップアップがページを覆い隠しているか(ステップ 3 の Cookie 対策を参照)、あるいは Google が CAPTCHA を突きつけてきているか。いずれも、後半の bot 対策パートで対処法を扱います。

ステップ 5:すべての航空券結果を読み込む

Google Flights は、追加の便を「さらに多くの便を表示」ボタンの裏側に隠しています。全部を引き出すには、このボタンを何度かクリックしてやる必要があります。

        # "さらに多くの便を表示" ボタンをクリックして全件読み込む
        while True:
            try:
                more_button = page.locator(
                    'button:has-text("Show more flights")'
                )
                if await more_button.is_visible(timeout=3000):
                    await more_button.click()
                    await page.wait_for_timeout(2000)
                else:
                    break
            except Exception:
                break

このループはボタンを押下し、新しい結果が出そろうまで 2 秒待ち、ボタンが見当たらなくなったところで抜けます。私が検証した範囲では、たいていのルートで 1〜3 ページぶんの結果が用意されていました。

ステップ 6:CSS セレクタで航空券データを抽出する

読み込み終えたページから、いよいよ実データを抜き取ります。以下のセレクタは 2026 年 4 月時点で動作を確認済みです(この日付がなぜ重要なのかは、保守の章であらためて説明します)。

        flights = []
        cards = await page.query_selector_all("li.pIav2d")

        for card in cards:
            flight = FlightData()

            # 航空会社名
            airline_el = await card.query_selector(
                "div.sSHqwe span:not([class])"
            )
            if airline_el:
                flight.airline = (await airline_el.inner_text()).strip()

            # 出発時刻
            dep_el = await card.query_selector(
                'span[aria-label*="Departure time"]'
            )
            if dep_el:
                flight.departure_time = (await dep_el.inner_text()).strip()

            # 到着時刻
            arr_el = await card.query_selector(
                'span[aria-label*="Arrival time"]'
            )
            if arr_el:
                flight.arrival_time = (await arr_el.inner_text()).strip()

            # 所要時間
            dur_el = await card.query_selector("div.gvkrdb")
            if dur_el:
                flight.duration = (await dur_el.inner_text()).strip()

            # 乗継回数
            stops_el = await card.query_selector("div.EfT7Ae span")
            if stops_el:
                flight.stops = (await stops_el.inner_text()).strip()

            # 価格
            price_el = await card.query_selector(
                "div.FpEdX span"
            )
            if price_el:
                flight.price = (await price_el.inner_text()).strip()

            # CO2 排出量
            co2_el = await card.query_selector("div.O7CXue")
            if co2_el:
                flight.co2_emissions = (
                    await co2_el.get_attribute("aria-label") or ""
                ).strip()

            flights.append(flight)

        await browser.close()
        return flights

ひとつ気をつけたいのは、pIav2dsSHqweFpEdX といった class 名です。これらは Google の Closure Compiler が吐き出すもので、ビルドのたびに変わりかねません。その点、aria-label を頼りにしたセレクタのほうが、ずっと安定します。保守のための具体策は、このあとまとめて取り上げます。

ステップ 7:結果を JSON または CSV に保存する

仕上げに、スクレイピングしたデータをタイムスタンプ付きで書き出します。これは、のちのち価格を追跡していくうえで決定的に効いてくる工程です。

import json
from datetime import datetime
from dataclasses import asdict

async def main():
    params = SearchParams(
        origin="SFO",
        destination="JFK",
        departure_date="2026-05-15"
    )
    flights = await scrape_flights(params)

    output = {
        "search_date": datetime.now().isoformat(),
        "params": asdict(params),
        "flights": [asdict(f) for f in flights],
    }

    with open("flights.json", "w") as f:
        json.dump(output, f, indent=2)

    # CSV としても保存
    import pandas as pd
    df = pd.DataFrame([asdict(f) for f in flights])
    df["search_date"] = datetime.now().isoformat()
    df["route"] = f"{params.origin}-{params.destination}"
    df.to_csv("flights.csv", index=False)

    print(f"Scraped {len(flights)} flights")

asyncio.run(main())

これを走らせると、結果が flights.jsonflights.csv に書き出されます。私のテストでは、SFO-JFK の検索でだいたい 30〜80 件の候補が返り、完了までおよそ 15〜20 秒という具合でした。

Google Flights スクレイピングのための bot 対策サバイバルガイド

たいていのチュートリアルは、この手前で筆を置きます。ところが現実には、多くのスクレイパーがまさにこの地点で動かなくなるのです。Google は 2025 年 1 月に SearchGuard を投入 し、世の SERP スクレイパーのほぼすべてを一夜にして無力化しました。Google 自身はこれを「何万時間もの人件費と数百万ドル規模の投資の成果」と表現しています。Google Flights のスクレイピング難易度は、5 段階中 4 と評されています。

競合記事の多くはこの領域に深く踏み込みませんが、実のところ、スクレイパーが止まる最大の元凶はここにあります。いったい何が起きていて、どう手を打てばいいのか——順に見ていきましょう。

anti-bot-survival-guide.webp

リクエスト間にランダムな遅延を入れる

レート制限対策として、いちばん基礎にあたる一手です。たった 2 行で書けて、効き目はほどほど、といったところです。

import time
import random

time.sleep(random.uniform(3, 7))

これをページ移動の合間に挟みます。毎回きっかり 5 秒、といった固定間隔はいかにも機械的で見破られやすいので、必ずランダムにばらつかせてください。

User-Agent のローテーション

毎回まったく同じ user-agent を送り続けるのは、かなりあからさまな足跡になります。いくつか候補を用意して、その都度切り替えましょう。

import random

USER_AGENTS = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/125.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 Safari/605.1.15",
    "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/124.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0",
]

user_agent = random.choice(USER_AGENTS)

ヘッドレス検知の回避

Google は navigator.webdriver フラグをはじめ、各種の自動化シグナルに目を光らせています。playwright-stealth がその多くを打ち消してくれますが、ステップ 3 で挙げた起動引数も忘れずに設定してください。とくに効くフラグは以下です。

args=[
    "--disable-blink-features=AutomationControlled",
    "--disable-dev-shm-usage",
    "--no-first-run",
]

これで初歩的な検知はかわせます。SearchGuard はさらに踏み込んで、マウスの動く速さ、キー入力のタイミング、スクロールの癖までうかがっていますが、件数が中程度にとどまるうちは、stealth モードと自然な遅延だけで切り抜けられることがほとんどです。

プロキシのローテーション:データセンター型 vs. 住宅型

検索が数件を超えてくると、プロキシはもはや実質的に必須です。しかも、その差は決して小さくありません。

機能データセンタープロキシ住宅プロキシ
速度100〜1,000 Mbps10〜100 Mbps
Google での成功率20〜40%85〜95%
コスト1 IP あたり月額 $0.10〜$0.50$5〜$15/GB
検知リスク高い非常に低い

保護の堅いサイトを相手にする場合、住宅プロキシは成功リクエストあたりのコストが 約 180 倍も安く済む とされています。2026 年の提供価格を並べると、Smartproxy が $7/GB から、Bright Data が $8.40/GB、Oxylabs が $8/GB です。

Playwright にプロキシを噛ませるには、こう書きます。

browser = await pw.chromium.launch(
    proxy={"server": "http://proxy-host:port",
           "username": "user", "password": "pass"}
)

Cookie 同意ポップアップの処理

「利用規約への同意」ポップアップを障害として挙げる人は、とにかく多いです。「最初に Google が '利用規約に同意しますか' のポップアップを出してくる」 という嘆きもよく耳にします。いちばんきれいに片づける方法は、ステップ 3 で示した SOCS Cookie を事前に仕込んでおくこと。それでも突破されないときは、クリックで押し通します。

try:
    accept_btn = page.locator('button:has-text("Accept all")')
    if await accept_btn.is_visible(timeout=3000):
        await accept_btn.click()
        await page.wait_for_timeout(1000)
except Exception:
    pass  # ポップアップがない場合

なお、ボタンの文言はロケールしだいで変わります。ドイツ語なら "Alle akzeptieren"、フランス語なら "Tout accepter" です。

bot 対策のクイックリファレンス

手法難易度効果コードは必要?
ランダム遅延(2〜7 秒)2 行
User-Agent ローテーション5 行
ヘッドレス検知回避Playwright の起動引数
playwright-stealth プラグイン基本サイトで 60〜80%pip install
プロキシローテーション(データセンター)設定
プロキシローテーション(住宅)成功率 85〜95%設定
Cookie 同意の事前設定(SOCS)必須1 行

安全圏とされるリクエスト速度の目安は、IP をローテーションしながら 1 リクエストにつき 10〜20 秒の遅延を挟むあたりです。Google 側の閾値としては、1 IP あたりおよそ 100 リクエスト/分を超えると 429 エラーが顔を出し始め、1 日 1,000 リクエストを超える継続的な利用は一時的なブロックを招きかねません。

なぜ Google Flights のセレクタは壊れ続けるのか、そしてどう直すのか

これは本当によくある悩みです。フォーラムには 「返ってくるのは空のリストばかり」 という投稿があふれ返っています。どのチュートリアルもセレクタ自体は教えてくれますが、なぜそれが壊れるのかまで踏み込むものはあまりありません。

Google Flights のセレクタが変わる理由

主な原因は、次の3つです。

  1. Closure Compiler による難読化。 Google は Closure Stylesheets を用い、goog.setCssNameMapping() 経由で BVAVmfYMlIz といった class 名を機械的に生成します。これらはビルドのたびに、ときには週単位で姿を変えます。

  2. A/B テスト。 同じ時間帯であっても、ユーザーごとに別々の HTML 構造があてがわれることがあります。自分の環境では問題なく動いていたのに、別地域のユーザーでは失敗する——そんな事態が起こり得ます。

  3. ロケール差。 EU のユーザーには、米国のユーザーとは異なる文言・レイアウト・データ項目が提示される場合があります。

壊れにくいセレクタを書く

見た目ではなく、意味に紐づいたセレクタを優先しましょう。

# 壊れやすい — ビルドごとに壊れる
price_el = await card.query_selector("div.BVAVmf > div.YMlIz")

# より安定 — アクセシビリティラベルに紐づく
dep_el = await card.query_selector('span[aria-label*="Departure time"]')

# こちらも安定 — テキストベースの一致
more_btn = page.locator('button:has-text("Show more flights")')

セレクタの安定性の序列(安定度が高い順):

  1. aria-label 属性 — アクセシビリティ用で、変わりにくい
  2. data-* 属性 — 機能のために明示的に付与される
  3. role 属性 — ARIA role は意味情報
  4. テキストベースのセレクタ — 表示文言に一致
  5. 部分一致の class 名 — 例: [class*="price"]
  6. 完全な難読化 class 名 — できるだけ避ける

バリデーション関数を追加する

壊れたセレクタが、何食わぬ顔で空データを返してくる事態は避けたいところです。とにかく、早い段階で異変に気づける仕組みを入れておきましょう。

import logging

logger = logging.getLogger(__name__)

def validate_flight(data: FlightData) -> bool:
    required = ["airline", "price", "departure_time",
                "arrival_time", "duration"]
    valid = True
    for field_name in required:
        if not getattr(data, field_name, ""):
            logger.warning(
                f"Missing '{field_name}' — selectors may need updating"
            )
            valid = False
    return valid

スクレイピングした便ひとつひとつに、このチェックを通してください。警告がぽつぽつ出始めたら、それがページ構造を見直してセレクタを差し替えるサインです。

セレクタ保守の方針

  • セレクタは毎月点検するか、出力品質が落ちた時点ですぐに見直す
  • セレクタは別の設定辞書にまとめ、更新の手間を減らす
  • この記事のセレクタ最終確認日は 2026 年 4 月
  • 代替案として fast-flights ライブラリ も検討する。CSS セレクタではなく Protobuf 解析を使うため、この問題そのものを回避できる(ただし Google が内部データ形式を変えれば、別種の脆さは残る)

1 回きりのスクレイピングから、自動 Google Flights 価格トラッカーへ

多くのチュートリアルは「JSON に保存して、はい終わり」で完結します。けれど、この記事の看板に掲げているのは「価格アラート」です。いよいよ、ここからが本題になります。

scraper-to-price-tracker-sfo-jfk.webp

スクレイパーを自動実行するようにスケジュールする

方法 1: Python の schedule ライブラリ(いちばん手軽で、OS を選ばない):

import schedule
import time

def run_scraper():
    asyncio.run(main())

schedule.every().day.at("06:00").do(run_scraper)
schedule.every().day.at("18:00").do(run_scraper)

while True:
    schedule.run_pending()
    time.sleep(60)

方法 2: cron ジョブ(Linux/Mac):

# 毎日 6:00 と 18:00 に実行
0 6,18 * * * cd /path/to/scraper && python scraper.py

方法 3: Windows タスク スケジューラpython scraper.py を任意のスケジュールで走らせる基本タスクを作成します。

ひとつ注意があります。これらはいずれも、常時稼働しているマシンが前提です。スリープに入るノートPCで回そうとすると、肝心のスクレイピングを取りこぼしてしまいます。

過去の価格データを保存する

JSON ファイルを毎回上書きする方式はやめて、SQLite データベースに追記していく形へ切り替えます。

import sqlite3
from datetime import datetime

def init_db():
    conn = sqlite3.connect("flights.db")
    conn.execute("""
        CREATE TABLE IF NOT EXISTS flights (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            scrape_date TEXT NOT NULL,
            route TEXT NOT NULL,
            airline TEXT,
            departure_time TEXT,
            arrival_time TEXT,
            duration TEXT,
            stops TEXT,
            price_usd REAL,
            co2_emissions TEXT
        )
    """)
    conn.execute(
        "CREATE INDEX IF NOT EXISTS idx_route_date "
        "ON flights(route, scrape_date)"
    )
    conn.commit()
    return conn

def save_flight(conn, route: str, flight: FlightData):
    price_num = float(
        flight.price.replace("$", "").replace(",", "")
    ) if flight.price else None
    conn.execute(
        "INSERT INTO flights "
        "(scrape_date, route, airline, departure_time, "
        "arrival_time, duration, stops, price_usd, co2_emissions) "
        "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
        (datetime.now().isoformat(), route, flight.airline,
         flight.departure_time, flight.arrival_time,
         flight.duration, flight.stops, price_num,
         flight.co2_emissions)
    )
    conn.commit()

1 日 2 回のスクレイピングを 1 週間も続ければ、価格の動きを読み取るのに十分なデータが手元にたまります。

価格トレンドを分析し、アラートを設定する

蓄積した過去データから、最安値を掘り起こします。

import pandas as pd
import sqlite3

conn = sqlite3.connect("flights.db")
df = pd.read_sql_query(
    "SELECT * FROM flights WHERE route = 'SFO-JFK'", conn
)
summary = df.groupby("scrape_date")["price_usd"].agg(
    ["min", "max", "mean"]
)
cheapest = df.loc[df["price_usd"].idxmin()]
print(
    f"Cheapest: ${cheapest['price_usd']:.0f} on "
    f"{cheapest['scrape_date']} ({cheapest['airline']})"
)

そして価格が決めておいた閾値を割り込んだら、メールでアラートを飛ばします。

import smtplib
from email.mime.text import MIMEText

def send_price_alert(route, price, threshold, recipient):
    msg = MIMEText(
        f"Price drop alert! {route}: ${price:.0f} "
        f"(below your ${threshold:.0f} threshold)"
    )
    msg["Subject"] = f"Flight Deal: {route} at ${price:.0f}"
    msg["From"] = "alerts@example.com"
    msg["To"] = recipient

    with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
        server.login("alerts@example.com", "your_app_password")
        server.send_message(msg)

# 各スクレイプ後にお得な価格を確認
min_price = df["price_usd"].min()
threshold = 250
if min_price < threshold:
    send_price_alert("SFO-JFK", min_price, threshold,
                     "you@email.com")

頻度の目安についても触れておきます。個人で価格を見張る用途なら、1 日 2 回で十分こと足ります(実行時刻をランダムにずらせば、検知リスクも下げられます)。業務利用なら 4〜6 時間ごと。1 時間おきの取得は、短期セールの期間に限って一時的に使う程度にとどめてください。

もっと簡単な方法:Thunderbit のスケジュールスクレイパー

cron や常時稼働サーバー、プロキシ設定といった面倒を抱え込みたくない——そんな人には、Thunderbit の Scheduled Scraper という選択肢があります。同じ用途を、はるかに身軽に実現できます。スクレイピングの間隔を自然な言葉で指定し、Google Flights の URL を入れる。あとは Thunderbit のクラウド基盤上で自動的に回り続けます。bot 対策は最初から組み込まれており、結果は Google Sheets、Excel、Airtable へ直接書き出せます。フル Python 実装の完全な置き換えにはなりませんが、「価格追跡用のスプレッドシートがほしいだけ」というゴールであれば、これがいちばん近道です。無料プラン からでも試せます。

Python までは不要? Google Flights をノーコードでスクレイピングする方法

ここまで一緒に組んできて、正直なところを言えば、部品の数がかなり多いのは否めません。そして、誰もがこれほどの制御を必要としているわけでもありません。セレクタは壊れる、プロキシのローテーションは要る、cron の見守りも欠かせない。もし目的が「航空券価格を定期的にスプレッドシートへ流し込むこと」に尽きるなら、もっと手早い道があります。

比較:自作 Python vs API サービス vs Thunderbit

方法準備時間コーディング必要bot 対策対応スケジュール機能料金
自作 Playwright(このチュートリアル)1〜2 時間Python(中級)手動設定手動(cron)無料 + プロキシ代
SerpApi の Google Flights エンドポイント15 分API 呼び出しのみ対応済みAPI 経由月額約 $50〜
Thunderbit Chrome 拡張機能2 分不要クラウドで対応内蔵スケジューラ無料プランあり

SerpApi について、ひとこと添えておきます。Google は 2025 年 12 月に SerpApi を DMCA で提訴 し、2 年間でリクエスト数が 25,000% も膨れ上がったと主張しました。API 事業者を選ぶ際には、こうした法的な不確実性も合わせて見ておく価値があります。

Thunderbit で Google Flights をスクレイピングする方法

Chrome で Google Flights の検索結果を開いたら、Thunderbit の「AI Suggest Fields」ボタンを押します。すると AI がページを読み解き、航空会社・価格・出発時刻・乗継といった列を提案してくれます。提案された項目に目を通したうえで「Scrape」をクリックすれば、それで完了です。結果はテーブルとして並び、Excel、Google Sheets、Airtable、Notion へエクスポートできます。これらはいずれも 無料プラン の範囲で使えます。

とりわけ価格追跡の用途では、Thunderbit の Scheduled Scraper と Cloud Scraping(同時に 50 ページまで処理可能)がそろっていれば、cron・プロキシ・サーバーという一式の構成がまるごと不要になります。

Python の持ち味は、自由度の高さと拡張性。Thunderbit の持ち味は、速さとメンテナンス不要の手軽さ。自分の目的に照らして選んでください。ノーコードのスクレイピング手法をさらに知りたい方は、おすすめのノーコード Web スクレイパー のガイドも参考になります。

flight-data-options-comparison.webp

Google Flights のスクレイピングは合法? 知っておくべきこと

フォーラムでは 「Google Flights を直接スクレイピングするのは Google の利用規約違反だ」 という声がしばしば上がります。もっともな懸念です。API が姿を消し、正規の代替も用意されていない今となっては、なおさらでしょう。

利用規約違反と法的責任は別物

Google の利用規約(2024 年 5 月 22 日更新)では、ユーザーは "robots, spiders, scrapers などの自動手段を用いてサービスやコンテンツにアクセスまたは利用してはならない" と定められています。ただし、利用規約に背くことは契約違反(民事)であって、それがそのまま違法行為に直結するわけではありません。

押さえておきたい先例が hiQ v. LinkedIn(第 9 巡回区、2022 年)です。この裁判では、公開データのスクレイピングは Computer Fraud and Abuse Act(CFAA)に違反しないと判断されました。もっとも、この件は最終的に和解で決着しています。そして Google が 2025 年 12 月に SerpApi へ起こした訴訟は、これとは別の法理——DMCA 第 1201 条(技術的保護手段の回避)——に立脚しており、より重い問題に発展する余地があります。

責任あるスクレイピングのベストプラクティス

  • リクエストをレート制限する — IP ローテーションを使い、10〜20 秒の遅延を入れる
  • 個人情報はスクレイピングしない — 航空券価格は公開されている集計データです
  • CAPTCHA をプログラムで回避しない — ここは DMCA リスク領域です
  • データは個人研究用途にとどめる。適切なライセンスなしに競合の商用製品を作らない
  • 使えるなら公式 API を検討する

代替データソース

用途によっては、スクレイピングよりも合法的な API のほうが向いている場合があります。

提供元料金無料枠備考
SerpApi月額 $75〜$3,750+月 250 検索Google Flights の JSON を直接取得(法的注目あり)
Kiwi Tequila無料(アフィリエイトモデル)無制限スタートアップや検証に最適
Amadeus従量課金月 2,000 リクエスト400+ 航空会社、予約機能あり
Skyscanner個別見積もり承認が必要52 市場、30 言語対応

もっと踏み込んだ内容は、Web スクレイピングの法的論点 を扱った解説記事でも紹介しています。

まとめと重要ポイント

ずいぶんと盛りだくさんでした。要点をおさらいしておきます。

  • Python + Playwright は Google Flights をスクレイピングするうえで最も柔軟な方法ですが、その分、継続的な保守がついて回ります
  • bot 対策(遅延、User-Agent ローテーション、住宅プロキシ)は欠かせません。とりわけ SearchGuard 以降は、信頼性を保つうえで必須です
  • セレクタは頻繁に壊れる ため、できる限り aria-label とテキストベースのセレクタを使い、出力を検証し、保守スケジュールをあらかじめ決めておきましょう
  • schedule や cron で自動化 すれば、単発のスクレイピングを、履歴データとメールアラートまで備えた本格的な価格トラッカーへと仕立て直せます
  • Thunderbit は、ノーコードで、スケジュール機能・クラウドスクレイピング・bot 対策を備えた代替手段です。コードを書くより価格追跡用のスプレッドシートが欲しい、という人に向いています
  • 法的な境界はしっかり尊重する こと。レート制限を守り、公開データだけを対象にし、商用利用では API 代替も視野に入れてください

このチュートリアルのコードをそのまま使うのもよし、手早く済ませたいなら Thunderbit Chrome 拡張機能 を導入するのもよし。どちらを選んでも、Google Flights を手で更新し続ける日々から抜け出し、航空券価格を自動で追えるようになります。

Python でのスクレイピング技術をもっと深めたいなら、Python で Web スクレイピングする方法おすすめの Python Web スクレイピングツール もあわせてご覧ください。

FAQ

1. Python なしで Google Flights をスクレイピングできますか?

はい。SerpApi や Kiwi Tequila のような API サービスを使えば、構造化された航空券データを API 呼び出しで取得できます(ブラウザ自動化は不要です)。完全ノーコードで進めたいなら、Thunderbit の Chrome 拡張機能 を使えば、AI が提案する項目をもとにブラウザ上の Google Flights 結果を直接スクレイピングし、ワンクリックでエクスポートできます。

2. Google は航空券スクレイピングをブロックしますか?

Google は bot 検知(SearchGuard)、CAPTCHA、レート制限を使っています。ランダムな遅延、User-Agent ローテーション、住宅プロキシ、stealth ブラウザ設定などをうまく組み合わせれば、中くらいの件数なら安定してスクレイピングできます。具体的な手法と閾値は上の bot 対策セクションを見てください。

3. 価格追跡のために Google Flights をどのくらいの頻度でスクレイピングすべきですか?

個人用途なら、ランダムな時間帯に 1 日 2 回で十分です。これなら検知リスクも低めに抑えられます。業務監視なら 4〜6 時間ごとに、プロキシをローテーションして実行してください。短期セールを除き、1 時間ごとのスクレイピングは避けたほうがよく、ブロックされる可能性がかなり上がります。

4. 無料の Google Flights API はありますか?

公式の Google QPX Express API は 2018 年 4 月に終了 しました。無料の公式代替はありません。いちばん近い無料の選択肢は Kiwi Tequila API(アフィリエイトモデル、検索無制限)です。SerpApi には月 250 回の無料検索があります。多くの人にとっては、スクレイピングか Thunderbit のようなノーコードツールが現実的な選択肢です。

5. Google Flights の CSS セレクタが空データばかり返すのはなぜですか?

Google は Closure Compiler を使って、ビルドごとに変わる難読化された class 名を生成しています。A/B テストやロケール差によっても HTML 構造がユーザーごとに変わります。対策は、class 名ではなく aria-label 属性やテキストベースのセレクタを使うこと、壊れを早めに見つけるバリデーション関数を追加すること、そしてセレクタを毎月見直すことです。詳しくはセレクタ保守の章をご覧ください。

さらに読む

Ke
Ke
Thunderbit の CTO | シニアデータサイエンティスト & ML エキスパート 機械学習とデータサイエンスで約10年の経験を持つ Ke Shen は、コロンビア大学の卒業生であり、Walmart Labs の元シニアデータサイエンティストです。Python、R、Java、統計学における深い専門性は同業者からも高く評価されており、理論段階の複雑な AI アルゴリズムを本番運用レベルのアーキテクチャへと落とし込むための、実践に裏打ちされた知見を共有しています。
目次

ただ伝えるだけで、Webページをスクレイピング

必要なことをそのまま英語で伝えるだけ。いや、何も言わなくてもOKです。

Thunderbitを試す 無料
AIでデータを抽出
Google Sheets、Airtable、Notionへ簡単にデータを移行できます
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week