Fli 底层原理:如何从 AF_initDataCallback 数据块中解析 Google Flights 页面数据(完整指南)
【免费下载链接】fliGoogle Flights MCP, CLI and Python Library项目地址: https://gitcode.com/gh_mirrors/fli2/fli
Fli是一个开源的 Google Flights 搜索工具,提供 Python 库、CLI 和 MCP Server 三种使用方式。它的核心能力是绕过 HTML 爬虫,直接解析 Google Flights 页面内嵌的AF_initDataCallback数据块(ds:1payload),把航班价格、时刻、机型等原始数据还原成结构化对象。本文带你完整走一遍这条解析链路 🛫
为什么不再直接调用 Google 的 RPC 接口?
Google Flights 背后有一个未公开的FlightsFrontendService服务(GetShoppingResults、GetCalendarGraph等 RPC 端点),早期工具直接 POST 到它拿数据。但自 2026-08 起,这些端点要求携带一个x-goog-batchexecute-bgr请求头,它只能由页面自身的 JavaScript 动态生成,且签名与请求体严格绑定——任何普通 HTTP 客户端都会被拒,返回 HTTP 200 加一个空的wrb.fr错误行(gRPC error 13)。
Fli 的应对方案:改走公开搜索页google.com/travel/flights。该页面把同样的航班结果内联在页面的AF_initDataCallback脚本块中,key 为ds:1,其中data[2]和data[3]就是原来 RPC 返回的航班行——于是旧的解析器几乎不用改动。这个决策记录在 fli/search/_tfs.py 的模块注释里。
第一步:构造 tfs 参数——用 protobuf 描述搜索条件
公开搜索页不再接收结构化的 JSON 请求,而是通过一个名为tfs的 URL 参数传递搜索条件。tfs是一个base64url 编码的 protobuf 消息,字段布局由逆向工程得出,例如(见 fli/search/_proto.py):
- 字段 19:行程类型(1=往返,2=单程)
- 字段 3:航段(重复),内含出发/到达机场、日期、中转上限
- 字段 8:乘客类型(1=成人,2=儿童,3/4=两种婴儿票)
- 字段 9:舱位等级
fli/search/_tfs.py 中的build_tfs函数把FlightSearchFilters对象编码成tfs值,再拼进 URL:
https://www.google.com/travel/flights?tfs=<base64>&hl=en&gl=US&curr=USD值得注意的是,多城市行程在此被明确拒绝——页面不为它内联任何航班行,硬发请求会静默返回错误结果,Fli 宁可直接报错(fli/search/_tfs.py#L119-L131)。
第二步:从 HTML 中精准抽取 ds:1 数据块
请求返回后,是一整页约 2.4MB 的 HTML,其中包含多个AF_initDataCallback块(地图数据、币种数据等)。Fli 需要挑出 key 为ds:1的那一块。核心是三个正则(fli/search/_tfs.py#L67-L70):
_DS_BLOB = re.compile(r"AF_initDataCallback\((\{.*?\})\);", re.S) _DS_KEY = re.compile(r"key:\s*'([^']+)'") _DS_DATA = re.compile(r"data:(.*?), sideChannel", re.S)extract_payload函数(fli/search/_tfs.py#L180-L205)逐个匹配脚本块、筛出ds:1、把data字段的 JSON 反序列化,返回一个嵌套数组——这正是 RPC 时代同样的结构。仓库里有一份真实的抓取样本,可以直观看到这个数据块的形态:search_page_jfk_lhr_nonstop_ds1.html(JFK→LHR 直飞、美元计价的完整 ds:1 载荷)。
第三步:解码——从嵌套数组到 FlightResult 模型
ds:1的data[2]/data[3]中每一行都是一条"位置式"数组:数字下标即字段,没有字段名。解码器 fli/search/_decoders.py 按固定下标逐层取值,例如parse_flight_row(fli/search/_decoders.py#L39-L79):
| 下标位置 | 含义 |
|---|---|
row[8] | 预订 token |
detail[9] | 总时长(分钟) |
detail[2] | 各航段数组 |
fl[3]/fl[6] | 出发 / 到达机场 |
fl[20]/fl[21] | 出发 / 到达时间戳 |
fl[22][1] | 航班号 |
fl[17] | 机型 |
fl[12] | 机上设施稀疏数组(电源/视频/Wi-Fi) |
解码后得到结构化的FlightResult:价格、货币、时长、经停次数、每段航班的航空公司/航班号/机型/设施/碳排放等,全部是强类型对象。解码器刻意不含任何 I/O,因此可以对着抓取的 fixture 做确定性单元测试(tests/search/test_parse_flights_data.py)。
第四步:容错——拒绝、缺块与重试
真实网络环境里,Fli 处理三类异常:
- 错误信封:
wrb.fr行第 5 位携带 gRPC 状态码(如 3=参数无效、13=拒绝服务),且不是合法正整数就不算拒绝(0是 gRPC 的 OK)——逻辑见 fli/search/_wire.py#L225-L248。 - 页面缺块:大约 1/60 的页面会 200 但唯独没有
ds:1块(瞬态现象)。fetch_payload对此精确重试 3 次、带 0.5s/1.5s 退避(fli/search/_tfs.py#L208-L243)——只重试这一种情况,避免放大硬失败。 - 稀疏乘客组合:带儿童/婴儿的行程中,Google 会在客户端计价,页面内联行数大幅减少甚至为 0。Fli 用
_RowCountTracker区分"Google 没内联"和"被用户过滤器滤空",前者会给出明确提示而不是空手而归(fli/search/flights.py#L82-L113)。
传输层同样讲究:fli/search/client.py 用curl_cffi模拟 Chrome 指纹、全局令牌桶限速 10 请求/秒、指数退避重试 3 次,并为 EU 用户的同意拦截页预置 SOCS cookie。
解析结果能做什么?
解析链路之上,Fli 暴露三种界面:Python API(SearchFlights().search(...))、CLI(fli flights search)和 MCP Server,后者可让 Claude 等 AI 助手直接调用search_flights/search_dates工具查航班、找最便宜日期:
关键文件清单
| 文件 | 职责 |
|---|---|
| fli/search/_tfs.py | 构造tfs参数、抽取ds:1块、页面重试 |
| fli/search/_proto.py | 手写 protobuf 编码器(varint / 长度前缀字段) |
| fli/search/_decoders.py | 位置式数组 → 强类型航班模型 |
| fli/search/_wire.py | wrb.fr多块流式读取与错误信封 |
| fli/search/client.py | 限速、重试、浏览器指纹 HTTP 客户端 |
| fli/search/flights.py | 搜索编排:取页 → 解码 → 客户端过滤 → 排序 |
小结:Fli 的 Google Flights 数据解析链路 = protobuf 编码请求(tfs)→ 正则定位AF_initDataCallback的ds:1块 → 按下标逐层解码 → 容错重试与过滤。理解这条链路,你也能看懂大多数"页面内联数据"类解析方案的设计取舍。
【免费下载链接】fliGoogle Flights MCP, CLI and Python Library项目地址: https://gitcode.com/gh_mirrors/fli2/fli
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考