Google Flights 是大多数旅行者在预订前比较机票的首选平台。它并排展示各家航空公司的价格、出发和到达时间、飞行时长以及经停次数,这使得其公开结果对于长期追踪票价、研究航线或观察各航空公司价格变动的人来说是一个有价值的数据源。旅行者从页面上读取的数字,正是票价监控工具希望以结构化表格形式存储的数字。

本指南演示如何以可靠的方式用 Python 抓取 Google Flights。你将构建一个小型可运行的爬虫,通过 Crawling API 获取渲染后的 Google Flights 结果页面,用 BeautifulSoup 解析每个航班,并将记录导出为 JSON 和 CSV 格式以供票价监控使用。整个流程仅限于任何人无需账号即可看到的公开航班结果数据,结尾处的合法性章节并非走过场,请在大量抓取前认真阅读。

你将构建什么

一个 Python 脚本:接受一个公开的 Google Flights 搜索 URL,通过 Crawling API 获取渲染后的 HTML,并为页面上的每个航班列表提取结构化记录。我们以一条示例航线作为运行示例,从每个列表中提取以下字段:

  • 航空公司 列表上显示的运营承运人(或多家承运人)。
  • 价格 该行程显示的票价。
  • 出发时间 航班的当地出发时间。
  • 到达时间 当地到达时间,包括任何 +1/+2 天标记。
  • 飞行时长 该行程的总旅行时间。
  • 经停 航班是直飞还是有几个经停。

为什么普通请求在 Google Flights 上会失败

如果从脚本对 Google Flights URL 发出裸 HTTP 请求,你得到的不是浏览器中看到的页面。Google Flights 在客户端构建其结果列表:初始 HTML 大部分是空 shell,航班卡片在页面加载后由 JavaScript 填充。普通的 requests.get 从不运行 JavaScript,因此它返回的标记中没有任何航班,你写的每个选择器都会返回空值。

此外,Google 还会监测自动化流量。不像真实浏览器的请求会被挑战、收到验证页面,或在到达列表之前被封锁。因此,一个可用的 Google Flights 爬虫在单次请求中需要两件事:一个实际渲染页面的浏览器,以及 Google 认为是真实访客的 IP。你可以自己搭建无头浏览器加上轮换住宅代理池,但保持这套系统正常运行才是主要工作。Crawling API 将两者合并为一次调用:你把 URL 发给它,它在受信任的住宅 IP 后面的真实浏览器中渲染页面,并返回供你解析的完整 HTML。

渲染在这里不是可选的

与传统的服务器端渲染结果页面不同,Google Flights 的原始 HTML 中几乎没有任何有用内容。航班卡片只有在 JavaScript 运行后才会存在。这就是为什么本教程从一开始就使用 JavaScript 渲染 token,而不是事后才用。如果你的请求返回一个没有任何列表的页面,渲染几乎一定是缺失的部分。

前提条件

在编写任何代码之前,你需要准备好以下几项。都不会花太长时间。

基础 Python 知识。 你应该能够编写和运行 Python 脚本,并使用 pip 安装包。如果你不熟悉 BeautifulSoup,我们的Python 中使用 BeautifulSoup 指南涵盖了本教程所依赖的解析基础知识。

Python 3.8 或更高版本。python --version 确认版本。如果没有,从 python.org 安装,或通过 Anaconda 等发行版安装。

Crawlbase 账号和 token。 注册后打开控制台,从账号文档页面复制你的 token。Google Flights 需要渲染,因此你将使用 JavaScript token 而非普通 token。前 1000 次请求免费,在用完之前添加账单信息可额外解锁 9000 次免费请求。请像对待密码一样保管 token:它用于验证你的请求,不要放入版本控制。

搭建项目

创建虚拟环境以隔离项目依赖,然后安装爬虫所需的库。

bash
python --version

python -m venv google_flights_env
source google_flights_env/bin/activate

pip install crawlbase beautifulsoup4

在 Windows 上,用 google_flights_env\Scripts\activate 替换 source 那行来激活环境。两个依赖各司其职:crawlbase 是调用 Crawling API 并处理渲染的官方 Python 库,beautifulsoup4 解析返回的 HTML,让你通过 CSS 选择器提取各个字段。

第一步:获取渲染后的结果

先获取完整页面。导入 CrawlingAPI 类,用你的 JavaScript token 初始化它,然后开启渲染请求搜索 URL。在解析之前读取状态码,可以让失败信息清晰可见,这对于会进行限速的目标来说非常重要。

python
from crawlbase import CrawlingAPI

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"})

def crawl(page_url):
    options = {"ajax_wait": "true", "page_wait": 5000}
    response = api.get(page_url, options)
    status = response["headers"].get("pc_status")
    if status == "200":
        return response["body"].decode("utf-8")
    print(f"Request failed: {status}")
    return None

if __name__ == "__main__":
    page_url = "https://www.google.com/travel/flights/search?tfs=CBwQAhopEgoyMDI0LTA3LTE0ag0IAxIJL20vMDFmMDhycgwIAxIIL20vMDZ5NTcaKRIKMjAyNC0wNy0yMGoMCAMSCC9tLzA2eTU3cg0IAxIJL20vMDFmMDhy&hl=en-US&curr=EUR"
    html = crawl(page_url)
    print(html[:500] if html else "No HTML returned")

两个等待选项对这类客户端渲染目标非常重要。ajax_wait 告诉 API 等待异步内容加载完成,page_wait 在加载后再等待固定毫秒数,让延迟渲染的航班卡片在页面被捕获前出现。5 秒是一个合理的起点;如果航班列表返回为空或不完整,可以增大该值。注意 pc_status 请求头:这是 Crawlbase 对底层请求的状态码,在判断请求是否成功时,它比外层 HTTP 状态码更可信。运行脚本,你应该能看到真实的列表标记,而不是普通请求返回的空 shell。

Crawlbase Crawling API

Google Flights 需要在受信任 IP 后面渲染页面,通过一次调用完成。这正是上面代码片段中 JavaScript token 为你带来的:Crawling API 在真实浏览器中运行页面,等待航班卡片填充完成,在服务器端轮换住宅 IP,并将完整 HTML 交给你,省去了自己运行无头浏览器队列和代理池的麻烦。先用免费套餐指向一个公开搜索试试。

第二步:用 BeautifulSoup 解析每个航班

拿到渲染后的 HTML,将其加载到 BeautifulSoup 并从列表元素中提取每个航班。Google 将每个航班包裹在一个列表项中,因此你找到所有列表项,然后从每个列表项中读取航空公司、飞行时长、价格、出发和到达时间以及经停情况。下面的选择器直接来自实时布局;在长时间运行之前,在浏览器开发者工具中(右键点击,然后检查)确认当前类名。

python
from bs4 import BeautifulSoup

def text_or_none(listing, selector):
    el = listing.select_one(selector)
    return el.get_text(strip=True) if el else None

def scrape_dates(listing):
    dep = listing.select_one(
        'span.mv1WYe span:first-child [jscontroller="cNtv4b"] span')
    arr = listing.select_one(
        'span.mv1WYe span:last-child [jscontroller="cNtv4b"] span')
    departure = dep.get_text(strip=True) if dep else None
    arrival = arr.get_text(strip=True) if arr else None
    return departure, arrival

def scrape_flights(html):
    soup = BeautifulSoup(html, "html.parser")
    flights = []
    for listing in soup.select("li.pIav2d"):
        departure, arrival = scrape_dates(listing)
        flights.append({
            "airline": text_or_none(listing, "div.Ir0Voe div.sSHqwe"),
            "price": text_or_none(listing, "div.U3gSDe div.FpEdX span"),
            "departure": departure,
            "arrival": arrival,
            "duration": text_or_none(listing, "div.AdWm1c.gvkrdb"),
            "stops": text_or_none(listing, "div.EfT7Ae span.ogfYpf"),
        })
    return flights

包装选择器 li.pIav2d 匹配页面上的每个航班列表,每个字段都从该列表内的选择器读取。div.Ir0Voe div.sSHqwe 包含航空公司名称,div.U3gSDe div.FpEdX span 包含价格,div.AdWm1c.gvkrdb 包含飞行时长,div.EfT7Ae span.ogfYpf 包含经停文本(例如"Nonstop"或"1 stop")。出发和到达时间位于共享的 span.mv1WYe 块中,其中第一个子元素是出发,最后一个子元素是到达,每个都包裹在 [jscontroller="cNtv4b"] 的 span 中。小型辅助函数 text_or_none 使缺失字段返回 None 而不是导致循环崩溃,这正是你在不是每张卡片都包含所有字段的页面上需要的行为。

选择器会发生变化

Google 的类名(如 pIav2dsSHqwe)经过混淆处理,会在 Google 重新部署前端时轮换。请将上述选择器视为起始模板,而非固定合约。当某个字段对每个航班都返回空值时,在浏览器开发者工具中重新检查实时页面并更新选择器。定期维护选择器是任何生产爬虫的正常工作,并非哪里出错了的信号。

第三步:整合代码并导出

现在将获取和解析整合为一个可运行的脚本,然后将结构化输出写入 JSON 和 CSV 两种格式。JSON 便于进一步处理;CSV 可以直接放入电子表格或票价监控表格,每天进行差异比较。

python
import csv
import json
from crawlbase import CrawlingAPI
from bs4 import BeautifulSoup

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"})

def crawl(page_url):
    options = {"ajax_wait": "true", "page_wait": 5000}
    response = api.get(page_url, options)
    if response["headers"].get("pc_status") == "200":
        return response["body"].decode("utf-8")
    return None

def text_or_none(listing, selector):
    el = listing.select_one(selector)
    return el.get_text(strip=True) if el else None

def scrape_dates(listing):
    dep = listing.select_one(
        'span.mv1WYe span:first-child [jscontroller="cNtv4b"] span')
    arr = listing.select_one(
        'span.mv1WYe span:last-child [jscontroller="cNtv4b"] span')
    departure = dep.get_text(strip=True) if dep else None
    arrival = arr.get_text(strip=True) if arr else None
    return departure, arrival

def scrape_flights(html):
    soup = BeautifulSoup(html, "html.parser")
    flights = []
    for listing in soup.select("li.pIav2d"):
        departure, arrival = scrape_dates(listing)
        flights.append({
            "airline": text_or_none(listing, "div.Ir0Voe div.sSHqwe"),
            "price": text_or_none(listing, "div.U3gSDe div.FpEdX span"),
            "departure": departure,
            "arrival": arrival,
            "duration": text_or_none(listing, "div.AdWm1c.gvkrdb"),
            "stops": text_or_none(listing, "div.EfT7Ae span.ogfYpf"),
        })
    return flights

def save_json(flights, path="google_flights.json"):
    with open(path, "w", encoding="utf-8") as f:
        json.dump(flights, f, ensure_ascii=False, indent=2)

def save_csv(flights, path="google_flights.csv"):
    fields = ["airline", "price", "departure",
              "arrival", "duration", "stops"]
    with open(path, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=fields)
        writer.writeheader()
        writer.writerows(flights)

def main():
    page_url = "https://www.google.com/travel/flights/search?tfs=CBwQAhopEgoyMDI0LTA3LTE0ag0IAxIJL20vMDFmMDhycgwIAxIIL20vMDZ5NTcaKRIKMjAyNC0wNy0yMGoMCAMSCC9tLzA2eTU3cg0IAxIJL20vMDFmMDhy&hl=en-US&curr=EUR"
    html = crawl(page_url)
    if not html:
        print("No HTML returned")
        return
    flights = scrape_flights(html)
    save_json(flights)
    save_csv(flights)
    print(f"Saved {len(flights)} flights")

if __name__ == "__main__":
    main()

python main.py 运行完整脚本。它获取示例航线的渲染结果页面,为每个航班提取一条记录,并写出 google_flights.jsongoogle_flights.csv 两个文件。要监控票价,可以按计划运行并将每次运行的 CSV 追加到历史文件中:价格列随时间的变化就是你的票价曲线。要扫描不同的航线或日期,替换 URL 中的 tfs 值和日期即可;解析器会处理返回的任何内容。

输出示例

你会得到一个整洁的航班记录列表,每个列表项一个对象,可以直接写入 JSON、CSV 或数据库。以下是一条长途航线的 JSON 样本(有所截减):

json
[
  {
    "airline": "Cebu Pacific",
    "price": "€924",
    "departure": "10:10 PM",
    "arrival": "9:45 AM+2",
    "duration": "29 hr 35 min",
    "stops": "1 stop"
  },
  {
    "airline": "Etihad",
    "price": "€2,038",
    "departure": "10:25 PM",
    "arrival": "6:10 PM+1",
    "duration": "13 hr 45 min",
    "stops": "Nonstop"
  },
  {
    "airline": "Emirates",
    "price": "€2,215",
    "departure": "9:30 PM",
    "arrival": "5:20 PM+1",
    "duration": "13 hr 50 min",
    "stops": "Nonstop"
  }
]

同样的记录会出现在 google_flights.csv 中,带有标题行 airline,price,departure,arrival,duration,stops,每个航班一行。这正是票价监控所需的格式:追加每天的运行结果,价格列就成为一个可绘图或设置提醒的时间序列。

跨航线和日期扩展

单条航线单天只是演示;票价监控需要对多条航线和一段出发日期范围运行相同的解析。结构保持不变:构建每个搜索 URL,启用渲染通过 Crawling API 获取,并用同一个 scrape_flights 函数解析。保持长时间运行健康的唯一关键习惯是控制请求节奏,在请求之间暂停,而不是在紧密循环中发出请求。

python
import time

route_urls = [
    "https://www.google.com/travel/flights/search?tfs=...&curr=EUR",
    "https://www.google.com/travel/flights/search?tfs=...&curr=USD",
]

all_flights = []
for url in route_urls:
    html = crawl(url)
    if html:
        all_flights.extend(scrape_flights(html))
    time.sleep(3)

print(f"Collected {len(all_flights)} flights across {len(route_urls)} routes")

API 返回的任何 5XX 响应都不计费,因此重试被封锁或渲染超时的 URL 不需要额外付费。由于每次 Google Flights 请求都需要渲染,每次请求都使用 JavaScript token 层级;在大规模运行监控之前,请查阅定价页面了解渲染请求的计费方式。如果你更倾向于通过轮换池路由自己的流量而非使用托管 API,Smart AI Proxy(也称 AI Proxy)以直连代理端点的形式提供同等的住宅 IP 轮换能力。

保持不被封锁

即使渲染和受信任的 IP 已处理妥当,Google 对爬虫特征流量的监测比大多数网站更严格。养成几个好习惯能让运行保持健康。

  • 控制请求频率。 在紧密循环中频繁请求搜索页面是最快被挑战的方式。分散请求并变换航线,而不是以全速翻页一个搜索。
  • 依赖 IP 轮换。 住宅 IP 池将请求分散到众多真实用户地址,使单个 IP 不会触发限制。Crawling API 为你处理这一切;如果你自己搭建,这部分是关键。
  • 给页面足够的渲染时间。 如果航班返回为空,在假设选择器出问题之前先增大 page_wait。卡片需要一点时间来填充。
  • 字段为空时重新检查。 Google 会轮换其混淆后的类名。如果结果停止解析,在开发者工具中打开实时页面并更新选择器。

更广泛的策略请参阅如何在不被封锁的情况下抓取网站。由于 Google Flights 完全依赖客户端渲染,我们的爬取 JavaScript 网站指南解释了渲染为何重要以及如何开启它。如果你还需要追踪住宿价格,用 Python 抓取 Google Hotels 使用相同的渲染获取模式。

抓取 Google Flights 合法吗?

抓取 Google Flights 是否被允许,取决于 Google 的服务条款、你所在的司法管辖区以及数据的用途。Google 的条款对自动化访问设有限制,因此无论你的工具有多谨慎,抓取行为都可能违反这些条款。这里的代码不改变这一点,只是让技术部分得以运行。请阅读 Google 的条款及其 robots.txt,并将两者视为你采集内容的边界。

几条值得遵守的底线。只采集公开航班结果数据:任何人无需账号即可在结果页面上看到的航空公司、价格、时间、飞行时长和经停次数。保持请求量足够低,不给 Google 的服务器造成压力,并以合理节奏爬取,而不是全速运行。本指南有意将范围限定在这些公开列表,因为这是让工作具有可辩护性的边界。本指南不涵盖任何需要登录的内容、账号或个人数据、支付或预订流程,或你会作为自己内容重新分发的票价数据。

还值得了解的是,底层票价来自航空公司和全球分销系统,许多航空公司为这类访问发布了官方票价 API 或合作伙伴计划。如果你的项目需要有保障的、大量的、可再分发的票价数据,官方数据协议才是正确的途径,而不是更聪明的爬虫。将抓取用于公开列表的监控和研究,当你超越这个范围时,请转向经授权的 API。

回顾

核心要点

  • Google Flights 完全依赖客户端渲染。 普通请求返回空 shell,因此渲染是必不可少的,而非可选的,不渲染就看不到任何航班卡片。
  • 你需要同时具备渲染能力和受信任的 IP。 Crawling API 接受 JavaScript token,在真实浏览器中运行页面,在服务器端轮换住宅 IP,并返回完整 HTML。
  • BeautifulSoup 负责提取。 选取每个 li.pIav2d 列表项,然后从中读取航空公司、价格、出发时间、到达时间、飞行时长和经停情况,并预期混淆后的类名会发生变化。
  • 导出为 JSON 和 CSV。 每个航班一行的 CSV 是票价监控的格式:追加每次运行结果,价格列就成为时间序列。
  • 坚守公开数据。 尊重 Google 的服务条款和 robots.txt,保持请求量低,对于大量、可再分发的访问优先选择航空公司或 GDS 官方票价 API。

常见问题

为什么普通请求在 Google Flights 上返回不到航班?

Google Flights 通过 JavaScript 在客户端构建其结果卡片,因此普通 requests.get 下载的原始 HTML 大部分是空 shell,其中没有任何航班。要看到列表,你必须在真实浏览器中渲染页面。通过 Crawling API 使用 JavaScript token 获取,会为你运行这个渲染过程并返回完整 HTML,这就是为什么本指南中的每个选择器都假设页面已被渲染。

我可以用 Python 抓取 Google Flights 吗?

可以。使用 crawlbase 库和 BeautifulSoup,你可以获取渲染后的结果页面并提取航空公司、价格、出发和到达时间、飞行时长和经停情况。Crawling API 充当桥梁,渲染页面并从受信任的 IP 向 Google 发出你的请求,使请求得以顺利处理而不被封锁。更广泛的 Python 入门,请参阅我们的用 Python 抓取网站指南。

我可以从 Google Flights 列表中提取哪些字段?

本教程从每个航班中提取六个字段:航空公司、价格、出发时间、到达时间(包括 +1/+2 天标记)、总飞行时长,以及经停文本(如"Nonstop"或"1 stop")。你可以通过添加选择器将其扩展到其他可见字段,如经停机场或碳排放估算。保持在公开航班结果数据范围内,避免任何需要登录或预订流程的内容。

抓取 Google Flights 需要 JavaScript 渲染吗?

需要,这是与许多其他目标的关键区别。Google Flights 的原始 HTML 中几乎什么都没有;航班卡片只有在 JavaScript 运行后才会出现。Crawling API 提供了 JavaScript 渲染 token 以及 ajax_waitpage_wait 选项,像真实浏览器一样获取页面。我们的爬取 JavaScript 网站指南涵盖了何时需要渲染以及其工作原理。

如何随时间监控票价?

对固定航线和日期按计划运行爬虫,并将每次运行的 CSV 行(带时间戳)追加到历史文件中。跨运行的价格列就是你的票价曲线,你可以绘图或在其低于某个阈值时触发提醒。保持适度的频率,每条航线每天检查几次就足够了,以保持在上述节奏指导范围内。

我的选择器没有返回任何结果。是什么变了?

几乎可以肯定是 Google 的标记发生了变化。pIav2dsSHqwe 等类名经过混淆处理,会在 Google 重新部署前端时轮换,因此上个月有效的选择器可能会失效。首先通过增大 page_wait 确认页面确实已经渲染;如果已渲染但字段仍为空,在浏览器开发者工具中重新检查实时结果页面并更新选择器。定期维护选择器是任何生产爬虫的正常工作。

开始构建

大规模爬取任何站点,无需与基础设施对抗。

Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。

自助开通 · 无需销售通话 · 提供企业级爬取量