用 Python 调用 Skyscanner 机票价格日历 API:筛选低价出行日期

发布于 2026年10月12日

做低价出行推荐时,用户往往先知道目的地,却还没确定哪天出发。逐日搜索具体航班既慢,也容易混用不同币种或舱位。Skyscanner 机票价格日历 API 可以先比较一段日期内的报价,再针对候选日期查航班。

本文用 Python 筛选“本次返回中有报价的低价日期”,而不是承诺全网最低价。日历、航班和酒店数据都是搜索快照;选中日期后仍需复查,不能把报价当成已锁定的库存。

接口怎么选?

以下接口均使用 GET,请求路径为 https://api.opendata-api.com/api/project/skyscanner/<operationId>?key=YOUR_API_KEY。

目标operationId关键输入
查城市与机场标识flights-search-airportquery
比较日期范围flights-get-price-calendaroriginSkyId、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 接口文档和价格日历接口,先确认地点,再筛选日期,最后复查行程。如需类似旅游应用的接口,可通过联系页面提出需求;也可咨询源码私有部署。