用 Python 调用 Skyscanner 机票价格日历 API:筛选低价出行日期
做低价出行推荐时,用户往往先知道目的地,却还没确定哪天出发。逐日搜索具体航班既慢,也容易混用不同币种或舱位。Skyscanner 机票价格日历 API 可以先比较一段日期内的报价,再针对候选日期查航班。
本文用 Python 筛选“本次返回中有报价的低价日期”,而不是承诺全网最低价。日历、航班和酒店数据都是搜索快照;选中日期后仍需复查,不能把报价当成已锁定的库存。
接口怎么选?
以下接口均使用 GET,请求路径为 https://api.opendata-api.com/api/project/skyscanner/<operationId>?key=YOUR_API_KEY。
| 目标 | operationId | 关键输入 |
|---|---|---|
| 查城市与机场标识 | flights-search-airport | query |
| 比较日期范围 | flights-get-price-calendar | originSkyId、destinationSkyId、fromDate,可选 toDate |
| 查询整月单程报价 | flights-get-cheapest-oneway | 起终点 skyId、month(YYYY-MM) |
| 比较往返日期组合 | flights-get-price-calendar-return | 起终点 skyId、fromDate,可选 returnFromDate |
| 复查具体航班 | flights-search-flights | 起终点 skyId/entityId、date,可选 returnDate |
整月单程和往返日历的响应结构尚未独立验证,不要直接套用下面的日历解析器;往返接口也没有文档支持的任意结束日期参数。
第一步:准备密钥并确认地点
在控制台获取自己有权使用的 API 密钥。Python 需要安装 requests,并在本地或服务端设置环境变量 OPENDATA_API_KEY。不要把密钥放入前端代码、公开仓库或完整请求日志。
下面是地点查询的安全示例,替换占位密钥后手动执行。执行会真实请求接口,并可能消耗账户点数;打开本文或复制代码不会发起数据查询。
curl --get 'https://api.opendata-api.com/api/project/skyscanner/flights-search-airport?key=YOUR_API_KEY' \
--data-urlencode 'query=London' \
--data-urlencode 'market=US' \
--data-urlencode 'locale=en-US'
从 places 中按名称、国家和 placeType 选择城市或机场,再保存同一条结果的 skyId 与 entityId。文档示例中伦敦城市为 LOND / 27544008,希思罗机场为 LHR / 95565050。城市搜索可能返回该城市不同机场的航班,不要混搭标识,也不要用 IATA 三字码替代 entityId。
第二步:请求未来日期的价格日历
以下使用文档中的伦敦—纽约城市示例。正式使用时请先确认起终点选择,并将过期日期改为未来日期,结束日期不早于开始日期。
curl --get 'https://api.opendata-api.com/api/project/skyscanner/flights-get-price-calendar?key=YOUR_API_KEY' \
--data-urlencode 'originSkyId=LOND' \
--data-urlencode 'destinationSkyId=NYCA' \
--data-urlencode 'fromDate=2026-11-15' \
--data-urlencode 'toDate=2026-11-22' \
--data-urlencode 'currency=USD' \
--data-urlencode 'market=US' \
--data-urlencode 'cabinClass=economy'
Python 示例自动选择未来八天的区间,没有预设成功输出。它检查 HTTP 状态、响应包装中的 code 和实际字段;结构不符时停止,不把错误响应当成空报价。
import math
import os
from datetime import date, timedelta
import requests
start = date.today() + timedelta(days=35)
end = start + timedelta(days=7)
params = {
"key": os.environ["OPENDATA_API_KEY"],
"originSkyId": "LOND",
"destinationSkyId": "NYCA",
"fromDate": start.isoformat(),
"toDate": end.isoformat(),
"currency": "USD",
"market": "US",
"cabinClass": "economy",
}
url = "https://api.opendata-api.com/api/project/skyscanner/flights-get-price-calendar"
try:
response = requests.get(url, params=params, timeout=30)
except requests.RequestException:
raise RuntimeError("Request failed; inspect redacted diagnostics") from None
if not 200 <= response.status_code < 300:
raise RuntimeError(f"HTTP {response.status_code}")
try:
data = response.json()
except ValueError:
raise RuntimeError("Expected a JSON response") from None
# Check application codes before unwrapping response data.
while isinstance(data, dict) and "code" in data:
if data["code"] != 200:
raise RuntimeError("Application error; inspect the redacted message")
data = data.get("data")
if not isinstance(data, dict) or not isinstance(data.get("outboundDates"), list):
raise RuntimeError("Unexpected calendar schema")
currency = data.get("currency")
if not isinstance(currency, str) or not currency:
raise RuntimeError("Missing quote currency")
quotes = []
for item in data["outboundDates"]:
if not isinstance(item, dict) or item.get("hasQuote") is not True:
continue
price = item.get("price")
if isinstance(price, bool) or not isinstance(price, (int, float)):
continue
if not math.isfinite(price) or price < 0:
continue
try:
departure = date.fromisoformat(item["date"])
except (KeyError, TypeError, ValueError):
continue
if start <= departure <= end:
quotes.append((price, departure.isoformat()))
for price, departure in sorted(quotes)[:3]:
print(departure, price, currency)
if not quotes:
print("No usable quoted dates in this response")
第三步:理解报价并复查航班
已观察到的日历字段如下,其他航线的日期覆盖可能不同。
| 字段 | 使用方式 |
|---|---|
currency | 保留报价币种,不与其他币种直接排序 |
outboundDates[].date | 出发日期 |
outboundDates[].price | 数值报价,不用缺省的零替代缺失值 |
outboundDates[].hasQuote | 筛选有报价记录 |
outboundDates[].group | 如 low、medium 的相对分组,不代表全网最低 |
缺失日期或无报价不等于当天没有航班。已观察的响应没有 directPrice 或 isDirect,不能据此筛选直飞。
选择候选日后调用 flights-search-flights,同时传入起终点的配对标识和 date;往返再加较晚的 returnDate。检查 itineraries 中的 price.amount/currency、各段实际机场、时刻、stopCount 和 bookingUrl。HTTP 200 或外层 code=200 不代表航班已搜索完成,业务状态仍要单独判断。
可以落地哪些场景?
- 弹性出行推荐:同一路线、币种、市场和舱位内比较候选日期,展示查询时间。
- 旅游内容选题:给线路攻略或促销邮件提供可复查的低价日期线索,不宣传保证成交价。
- 降价提醒:自行安排定时任务,存储价格观测并发送通知;接口不自带历史价格档案或提醒服务。
常见问题
如何找一段日期或整个月中的最低报价?
日期范围用 flights-get-price-calendar,将有报价、日期有效、价格为数值的记录排序。整月单程可用 flights-get-cheapest-oneway,但先确认实际响应字段和覆盖天数。往返应比较出发与返回日期组合,不能把两个独立单程日历的最低价直接拼成往返报价。
日历里的最低价就是最终可预订价格吗? 不是。它只是本次查询中可比较的日期报价,不保证所有日期、供应商或直飞航班都被覆盖。重新搜索选中日期,并核实人数、舱位、机场和币种;展示时注明“查询时参考价”。如果需要行程详情,应使用自己搜索返回的 itineraryId 和同一次查询的私有会话,不能自行构造。
机场查询 HTTP 200 却没有结果,或 IATA 为空怎么办?
places:[] 是正常无匹配,应修改地点关键词;不要编造 ID。有效地点的 iataCode 也可能为空,以返回的 skyId/entityId 为准。
航班结果未完成,如何继续?
初始结果可能带 RESULT_STATUS_INCOMPLETE、部分行程和 sessionToken。把本次查询的 token 作为 flights-search-incomplete 的 sessionId,保持币种和 countryCode 一致。会话仅留服务端,不公开或硬编码。续查响应尚未独立验证,应检查结构、按行程标识去重,并设置退避、次数和超时上限。
多城市的 legs 怎么传?
flights-search-flights-multi-stops 的 legs 是含 originSkyId、destinationSkyId、date 的 JSON 数组字符串,用 json.dumps() 序列化后放入 GET 的 params,不是请求体。日期按行程排序;不要由组合报价推断联程保护。
酒店怎么查?房型、评价和坐标从哪里取?
先用 hotels-search-destination 确认地点 entityId,再调用 hotels-search-hotels,传未来 checkIn、更晚的 checkOut,以及 adults、rooms。保留人数、房间数和币种,核实金额是每晚还是整段住宿、是否含税。
搜索结果提供 hotelId 和坐标;详情、房型报价、评价、相似酒店及地图查询分别有独立接口。房型报价需 hotelId、目的地 entityId 和入住退房日期;评价支持可选 page/limit,但分页起点、上限及后续接口结构仍需验证。不要把返回列表当成全部酒店库存。
能直接出票、订酒店或查租车价格吗?
不能创建预订,只提供搜索数据及返回的预订链接。cars-search-location 仅查询取还车地点,不返回车辆、租赁报价或订单。
market、locale、currency 和汇率日期有何影响?
market 是搜索市场,locale 是支持时的语言地区设置,currency 是报价币种;并非每个接口都接受全部参数。通过 config-get-locale 查询支持配置并验证组合。换算时检查 config-get-exchange-rates 的 base、date、rates,保留原价与汇率日期,不把估算当成结算金额。
下一步
查看 Skyscanner 接口文档和价格日历接口,先确认地点,再筛选日期,最后复查行程。如需类似旅游应用的接口,可通过联系页面提出需求;也可咨询源码私有部署。