Google 早在 2018 年就關閉了 Flights API,但機票價格依然天天在變——單一國內航線在 48 小時內甚至可能波動高達 17 次。若你想用程式化方式取得這些資料,抓取網頁幾乎是目前唯一可行的辦法。
我花了不少時間測試各種從 Google 取得航班資料的方法,而整個環境也在 2025 年 1 月 Google 推出 SearchGuard 後出現巨大變化。在這篇指南中,我會帶你用 Playwright 打造一個可運作的 Google Flights Python 爬蟲,示範如何應對大多數人都會卡住的反機器人機制,接著再把它延伸成可自動執行的票價追蹤器與提醒系統。如果你想完全跳過寫程式,我也會介紹一個用 Thunderbit 的免程式替代方案,大約兩分鐘就能得到相同成果。
為什麼要用 Python 擷取 Google Flights?
Google Flights 幾乎主導了機票搜尋市場。它在美國行動裝置上的曝光率已飆升至 47.6%,超越所有主要 OTA。背後的旅遊比價搜尋市場在 2024 年估值達 83.3 億美元,年複合成長率為 30.2%。但自從 QPX Express API 在 2018 年 4 月 10 日永久關閉後,官方就不再提供任何程式化存取這些資料的方法。
同時,機票價格在同一行程上可能波動高達 50%,最低與最高價平均差距約 20 美元。像 Delta 這類航空公司甚至使用 77 個票價區間來進行動態定價。到了 2026 年初,美國平均來回機票約為 408 美元,而機票價格年增幅達 14.9%。
平台強勢、沒有 API、價格又極度不穩。這就是為什麼用 Python 抓 Google Flights,成了 GitHub 和各大旅遊論壇上最熱門的專案之一。
以下這些族群最能受益:
| 使用者類型 | 使用情境 | 主要效益 |
|---|---|---|
| 一般旅客 | 長期追蹤特定航線票價 | 平均每張機票可省下 50 美元 |
| 旅行社 | 競爭票價情報分析 | 即時監控票價一致性 |
| 企業差旅團隊 | 跨航線成本最佳化 | 企業差旅可節省 10–30% |
| 開發者 | 建立票價比較應用程式 | 程式化取得票價資料 |
| 研究人員 | 分析航空票價波動 | 學術與市場研究用途 |
論壇上的使用者也很直接地說明他們為什麼改用抓取:像是「Google Flights API 已停用,所以我應該改用網頁爬取」這類說法很常見。而且投資報酬率確實存在——Hopper 表示他們每天分析超過 50 億筆票價報價,預測準確率高達 95%;Expedia 2026 年的資料則顯示,提前 8–15 天訂票可讓國內航線平均省下約 25 美元。
你可以從 Google Flights 抓到哪些資料?
Google Flights 的結果頁面其實包含相當豐富的欄位。通常可取得的資料包括:
- 航空公司名稱(含標誌)
- 起飛時間與機場代碼
- 抵達時間與機場代碼
- 總飛行時間
- 轉機次數與轉機細節(機場、時間、是否過夜)
- 票價(依幣別顯示)
- 二氧化碳排放量(kg CO2e,並與一般航班做百分比比較)
- 艙等、航班號、機型
- 座位空間資訊
- 附加服務(Wi‑Fi、插座、影音串流)
- 價格等級指標(低/一般/高)
- 延誤警示(例如「經常延誤超過 30 分鐘」)
實際可取得的欄位會因航線、日期與票種(單程或來回)而有所不同。以下是一筆抓取到的航班資料,轉成 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 完全由 JavaScript 渲染——只送 HTTP 請求是拿不到有用內容的),另外再加上幾個輔助套件:
pip install playwright playwright-stealth pandas
playwright install chromium
- Playwright — 無頭瀏覽器自動化工具,可處理 JavaScript 渲染,內建等待機制
- playwright-stealth — 修補常見的機器人偵測訊號
- pandas — 之後用來做資料分析與 CSV 匯出
為什麼選 Playwright,而不是 Selenium 或 requests
單靠 requests + BeautifulSoup 無法處理 Google Flights,因為頁面內容完全是 JavaScript 動態渲染的。你需要一個真正的瀏覽器。
| 功能 | Playwright | Selenium | requests + BS4 |
|---|---|---|---|
| JS 渲染 | 完整支援 | 完整支援 | 不支援 |
| 速度 | 整體快 42% | 基準 | 不適用 |
| 非同步支援 | 原生支援 | 只支援順序執行 | 不適用 |
| 記憶體使用 | 少 30% | 較高 | 最低 |
| 規避機器人偵測 | 表現佳(搭配 stealth) | 較容易被偵測 | 不適用 |
Playwright 更快、更現代,也更適合非同步流程。就 Google Flights 這個案例來說,它是最明確的首選。
逐步教學:如何用 Python 擷取 Google Flights
這是整篇教學的核心,我們會一步一步完成爬蟲。

步驟 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 的網址結構
Google Flights 會把搜尋參數編碼到 tfs URL 參數中,使用 Base64 編碼的 Protobuf。你可以嘗試逆向解析這段編碼,但更簡單的方法是直接建立一個自然語言查詢網址。
最簡單的格式如下:
https://www.google.com/travel/flights?q=flights+from+SFO+to+JFK+on+2026-05-15&curr=USD
如果你想更有彈性,也可以用程式動態產生網址:
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=False),同時設定合理的視窗大小與使用者代理,並先預設 SOCS cookie 以跳過同意彈窗——這部分在後面的反機器人章節會再細講。
步驟 4:前往搜尋結果頁
載入剛剛組好的網址,並等待航班結果出現:
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
)
如果這裡逾時,通常代表同意彈窗擋住了頁面(可回頭看步驟 3 的 cookie 解法),或者 Google 直接送出 CAPTCHA。我們會在反機器人章節同時處理這兩種情況。
步驟 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 selector 擷取航班資料
接著就來解析頁面上的實際航班資料。以下是目前有效的 selector(截至 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_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
要注意的是,像 pIav2d、sSHqwe、FpEdX 這類 class 名稱是 Google Closure Compiler 產生的,任何版本更新都可能改掉。相較之下,aria-label selector 會穩定得多。後面我會補充完整的維護策略。
步驟 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.json 與 flights.csv 兩個檔案。根據我的測試,一次 SFO-JFK 搜尋通常會回傳 30–80 個航班選項,整個流程大約需要 15–20 秒。
Google Flights 反機器人生存指南
大多數教學都會停在這裡,但大多數爬蟲也會死在這裡。Google 在 2025 年 1 月部署了 SearchGuard,幾乎一夜之間讓大部分 SERP 爬蟲失效。Google 自己也把它描述為「投入了數萬小時人力與數百萬美元資金」的成果。Google Flights 的抓取難度被評為 4/5。
幾乎沒有競品文章會深入講這一塊,但這正是大家的爬蟲失效的頭號原因。下面就來看你會遇到什麼,以及該怎麼處理。

在請求之間加入隨機延遲
對抗速率限制最簡單的方法。只要兩行程式碼,中等效果:
import time
import random
time.sleep(random.uniform(3, 7))
把它加在頁面跳轉之間。固定間隔(例如每次都剛好 5 秒)很容易被標記,務必加入隨機性。
使用者代理輪替
每次請求都送出同一組 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 Mbps | 10–100 Mbps |
| Google 成功率 | 20–40% | 85–95% |
| 成本 | 每 IP 每月 0.10–0.50 美元 | 每 GB 5–15 美元 |
| 被偵測風險 | 高 | 非常低 |
在抓取受保護網站時,家用代理的單次成功請求成本大約便宜 180 倍。2026 年的供應商價格:Smartproxy 從每 GB 7 美元起,Bright Data 為每 GB 8.40 美元,Oxylabs 為每 GB 8 美元。
在 Playwright 中加入代理的方式如下:
browser = await pw.chromium.launch(
proxy={"server": "http://proxy-host:port",
"username": "user", "password": "pass"}
)
處理 cookie 同意彈窗
不少使用者都提到「我同意條款」彈窗會擋路:像是「一開始 Google 會先跳出『我同意條款與條件』的視窗」。最乾淨的做法就是預先設定 SOCS cookie(步驟 3 已示範)。如果這招不行,就直接點掉:
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」。
反機器人快速參考
| 技術 | 難度 | 有效性 | 需要程式碼嗎? |
|---|---|---|---|
| 隨機延遲(2–7 秒) | 低 | 中 | 2 行 |
| 使用者代理輪替 | 低 | 中 | 5 行 |
| 繞過無頭偵測 | 中 | 高 | Playwright 啟動參數 |
| playwright-stealth 外掛 | 中 | 基礎網站可達 60–80% | pip install |
| 代理輪替(資料中心) | 中 | 中 | 設定即可 |
| 代理輪替(家用) | 中 | 成功率 85–95% | 設定即可 |
| 預設 cookie 同意(SOCS) | 低 | 必要 | 1 行 |
建議的安全頻率是:搭配 IP 輪替時,請求間隔維持 10–20 秒。Google 的門檻大約是每個 IP 每分鐘 100 次請求,若單一 IP 持續每天超過 1,000 次請求,可能會觸發暫時封鎖。
為什麼你的 Google Flights selector 一直壞掉,以及怎麼修
這幾乎是最常見的痛點。論壇上充滿了類似「我拿回來的全是 14 個空清單」這種抱怨。每篇教學都會給你 selector,但很少有人解釋為什麼它們會壞。
Google Flights 的 selector 為什麼會變
主要有三個原因:
-
Closure Compiler 混淆。 Google 使用 Closure Stylesheets 和
goog.setCssNameMapping()來產生像BVAVmf、YMlIz這樣的 class 名稱。這些名稱每次建置都可能變,甚至每週都換。 -
A/B 測試。 不同使用者會同時看到不同的 HTML 結構。你的爬蟲在自己電腦上能跑,不代表別的地區也能跑。
-
語系差異。 歐盟使用者看到的術語、版面,甚至資料欄位都可能與美國使用者不同。
寫出更穩健的 selector
優先選擇和「意義」綁定,而不是和外觀綁定的 selector:
# 很脆弱——每次建置都可能壞掉
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")')
selector 穩定性排序(由穩到不穩):
aria-label屬性——和無障礙設計綁定,較少變動data-*屬性——功能性用途明確標記role屬性——ARIA role 本身有語意- 文字型 selector——比對畫面上的文字
- 子字串 class 比對——例如
[class*="price"] - 完整混淆 class 名稱——能避就避
加入驗證函式
不要讓 selector 壞掉卻悄悄產生空資料。應該及早發現:
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"缺少 '{field_name}' —— selector 可能需要更新"
)
valid = False
return valid
每一筆抓到的航班都跑一次這個檢查。如果開始出現警告,就表示該打開頁面檢查並更新 selector 了。
selector 維護策略
- 每月檢查一次 selector,或者在輸出品質下降時立即檢查
- 把 selector 集中放在獨立設定檔,方便快速更新
- 本文中的 selector 最後驗證時間:2026 年 4 月
- 也可以考慮 fast-flights library 作為替代方案——它使用 Protobuf 解碼而不是 CSS selector,能完全避開這個問題(但當 Google 改變內部資料格式時,它也有自己的脆弱性)
從一次性抓取,升級成自動化的 Google Flights 票價追蹤器
大多數教學都停在「存成 JSON」這一步。但這篇文章的標題是「票價提醒」,所以我們現在把它做完。
![]()
排程讓爬蟲自動執行
方案 1:Python schedule 套件(最簡單、跨平台):
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 job(Linux/Mac):
# 每天早上 6 點與晚上 6 點執行
0 6,18 * * * cd /path/to/scraper && python scraper.py
方案 3:Windows Task Scheduler — 建立一個基本工作,依你想要的排程執行 python scraper.py。
這些方案的代價是:你需要一台永遠開著的機器。如果你放在會睡眠的筆電上,就會漏抓。
保存歷史票價資料
把原本覆蓋 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()
持續抓一週、每天兩次後,你就有足夠資料開始觀察趨勢了。
分析票價趨勢並設定提醒
從歷史資料中找出最便宜的選項:
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['price_usd']:.0f},"
f"日期:{cheapest['scrape_date']}({cheapest['airline']})"
)
當價格低於你的門檻時,就寄出 email 提醒:
import smtplib
from email.mime.text import MIMEText
def send_price_alert(route, price, threshold, recipient):
msg = MIMEText(
f"票價下跌通知!{route}:${price:.0f} "
f"(已低於你設定的 ${threshold:.0f} 門檻)"
)
msg["Subject"] = f"航班優惠:{route} 價格 ${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")
建議的抓取頻率:個人票價追蹤每天兩次就夠了(隨機時間可降低被偵測風險)。若是商業監控,可提升到每 4–6 小時一次。只有在短期特價促銷期間,才建議暫時提高到每小時一次。
更簡單的做法:Thunderbit 的排程爬蟲
如果你覺得 cron、長期運作的伺服器、以及代理設定太像一堆要維護的基礎設施,那麼 Thunderbit 的排程爬蟲可以不用這些額外負擔,完成同樣的需求。你只要用自然語言描述抓取頻率,貼上 Google Flights 網址,爬蟲就會在 Thunderbit 的雲端基礎架構上自動執行——內建反機器人處理,結果還能直接匯出到 Google Sheets、Excel 或 Airtable。它不完全等同於完整的 Python 方案(彈性較少),但如果你的目標只是做一份「票價追蹤表」,那它就是最快的路徑。你可以先試試看 免費方案。
當 Python 太重:不用寫程式也能抓 Google Flights
把前面那些都做完後,我必須老實說:這真的有不少零件。不是每個人都需要這麼高的控制度。selector 會壞、代理要輪替、cron 要監控。如果你的目標只是「定期把票價放進試算表」,其實有更快的選項。
比較:自己寫 Python、API 服務、Thunderbit
| 方案 | 設定時間 | 需要寫程式嗎 | 反機器人處理 | 排程 | 成本 |
|---|---|---|---|---|---|
| 自己動手寫 Playwright(本教學) | 1–2 小時 | 需要 Python(中等程度) | 需手動設定 | 手動(cron) | 免費 + 代理成本 |
| SerpApi Google Flights endpoint | 15 分鐘 | 只需 API 呼叫 | 已處理 | 透過 API | 每月約 50 美元以上 |
| Thunderbit Chrome 擴充功能 | 2 分鐘 | 不需要 | 雲端爬取 | 內建排程 | 有免費方案 |
關於 SerpApi,值得注意的是:Google 在 2025 年 12 月對 SerpApi 提起 DMCA 訴訟,理由是他們的請求量在兩年間成長了 25,000%。如果你正在評估 API 供應商,這樣的法律不確定性值得納入考量。
Thunderbit 如何抓取 Google Flights
在 Chrome 裡打開你的 Google Flights 搜尋結果,點擊 Thunderbit 的「AI 建議欄位」按鈕——AI 會讀取頁面並建議像航空公司、票價、起飛時間、轉機次數這些欄位;接著檢查建議欄位,按下「抓取」即可。結果會以表格顯示,你可以匯出到 Excel、Google Sheets、Airtable 或 Notion——而且免費方案就能用。
若是專注在票價追蹤這個用途,Thunderbit 的排程爬蟲與 雲端爬取(可同時處理 50 個頁面)可以直接取代整套 cron + 代理 + 伺服器基礎架構。
Python 的優勢是完全可控、客製化無上限;Thunderbit 的優勢是速度快、幾乎不用維護。請依照你的實際需求選擇。如果你想進一步了解免程式抓取方式,可以看看我們整理的 最佳免程式網頁爬蟲。

抓取 Google Flights 合法嗎?你需要知道的事
論壇使用者經常會問:「直接抓 Google Flights 會違反 Google 的服務條款嗎?」 這確實是合理的疑慮,尤其在 API 已停用、又沒有官方替代方案的情況下。
違反服務條款 vs. 法律責任
Google 服務條款(2024 年 5 月 22 日更新)明定,使用者不得「透過任何自動化方式(例如 robots、spiders 或 scrapers)存取或使用服務或內容」。違反 ToS 屬於違約(民事範疇),不等於犯罪。
最重要的法律先例是 hiQ v. LinkedIn(第九巡迴上訴法院,2022 年),該案確認抓取公開可得資料不違反《電腦詐欺與濫用法》(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 種語言 |
如果你想看完整脈絡,我們也寫過一篇更深入的 網頁爬取法律影響 分析。
結論與重點整理
內容很多,我幫你整理成最重要的幾點:
- Python + Playwright 是抓取 Google Flights 最有彈性的方案,但需要持續維護
- 反機器人措施(延遲、user-agent 輪替、家用代理)不是可選項,而是可靠運作的必要條件,尤其在 SearchGuard 上線後更是如此
- selector 很常壞——盡量使用
aria-label和文字型 selector,記得驗證輸出並安排維護時間 - 用
schedule或 cron 自動化,就能把一次性抓取變成真正的票價追蹤器,累積歷史資料並寄出 email 提醒 - Thunderbit 提供免程式替代方案,內建排程、雲端爬取與反機器人處理——如果你的目標是做一份票價追蹤表,而不是寫程式專案,它會非常適合
- 注意法律界線——控制頻率、只抓公開資料,商業用途則要考慮 API 替代方案
你可以直接使用這篇教學的程式碼,或安裝 Thunderbit Chrome 擴充功能 走快速路線。不管選哪種方式,你都不必再手動重整 Google Flights,而是讓票價追蹤自動跑起來。
若想學更多 Python 抓取技巧,歡迎參考我們的 如何用 Python 做網頁爬取 與 最佳 Python 網頁爬蟲工具 指南。
常見問題
1. 不用 Python,也能抓 Google Flights 嗎?
可以。像 SerpApi 和 Kiwi Tequila 這類 API 服務,可以透過 API 呼叫提供結構化的航班資料(不需要瀏覽器自動化)。如果你要完全不寫程式,Thunderbit 的 Chrome 擴充功能 也能直接從瀏覽器抓 Google Flights 結果,搭配 AI 建議欄位與一鍵匯出功能。
2. Google 會阻擋航班資料抓取嗎?
Google 會使用機器人偵測(SearchGuard)、CAPTCHA 與速率限制。只要搭配正確的反機器人措施——隨機延遲、user-agent 輪替、家用代理、以及 stealth 瀏覽器設定——中等流量下仍然可以維持穩定抓取。上面的反機器人章節有具體技巧與門檻。
3. 做票價追蹤時,多久抓一次 Google Flights 比較好?
每天兩次(時間隨機)就足以做個人票價觀察,而且能把被偵測風險降到較低。若是商業監控,可提高到每 4–6 小時一次並搭配代理輪替。除非是短期特價活動,否則不建議每小時抓一次,因為被封鎖的機率會明顯上升。
4. 有免費的 Google Flights API 嗎?
官方的 Google QPX Express API 已在 2018 年 4 月停用,沒有免費的官方替代方案。最接近的免費選項是 Kiwi Tequila API(聯盟模式,搜尋次數無上限)。SerpApi 每月提供 250 次免費搜尋。對多數使用者來說,抓取或像 Thunderbit 這樣的免程式工具,都是更實際的做法。
5. 為什麼我的 Google Flights CSS selector 一直抓到空資料?
Google 使用 Closure Compiler 產生會混淆的 class 名稱,而且每次建置都可能改變。A/B 測試和語系差異也會讓不同使用者看到不同的 HTML 結構。解法是:改用 aria-label 屬性與文字型 selector,而不是 class 名稱;加上驗證函式及早發現失效;並每月檢查一次 selector。詳細策略請參考 selector 維護章節。
延伸閱讀


