一旦 Selenium 向你表明搜索页面由 /dml/graphql,更快捷的做法是直接调用该端点并跳过浏览器。这正是 Booking.com 网页抓取真正实现可扩展性的关键。
发现过程与处理任何 [隐藏的 JavaScript API] 时相同:打开开发者工具(F12),切换至“网络”标签页,按 Fetch/XHR 过滤,随后触发一次真实搜索并点击进入第二页。你会看到一个 POST 请求发送到 /dml/graphql ,其中包含一个带有 operationName,一个 variables 对象(包含目的地、日期、入住人数以及一个 offset),以及一个 query 或 extensions 字段用于固定查询哈希。右键单击该请求并选择“复制为 cURL”,这就是您的起点。
在发布前,请根据您自己的开发者工具捕获结果重新核对确切的字段名称;Booking.com 会定期重命名 GraphQL 操作,而最安全的参考依据是当前前端发送的数据。
import httpx
ENDPOINT = 'https://www.booking.com/dml/graphql'
HEADERS = {
'content-type': 'application/json',
'origin': 'https://www.booking.com',
'referer': 'https://www.booking.com/searchresults.html',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/124.0 Safari/537.36',
'accept-language': 'en-US,en;q=0.9',
}
def search_page(client, payload, offset):
body = {**payload}
body['variables']['input']['pagination'] = {'offset': offset, 'rowsPerPage': 25}
r = client.post(ENDPOINT, json=body, headers=HEADERS, timeout=30)
r.raise_for_status()
return r.json()
def search_all(payload, max_results=1000):
results = []
with httpx.Client(http2=True) as client:
for offset in range(0, max_results, 25):
page = search_page(client, payload, offset)
hits = (page.get('data', {})
.get('searchQueries', {})
.get('search', {})
.get('results', []))
if not hits:
break
results.extend(hits)
return results
有两个细节容易让人踩坑。该端点每次调用返回 25 条结果,由一个偏移量变量控制,该变量以 25 条结果为增量进行调整。此外,请求必须看起来像是来自网站本身: origin 以及 referer 并设置为 booking.com, content-type: application/json,并设置 accept-language 与您的 IP 区域相符的。若移除这些标头,您将在数次请求内收到通用 400 错误或遭遇软封锁。请使用 HTTP/2(当您传入 http2=True时,httpx 会自动支持此协议),因为 Booking.com 的边缘服务器似乎会识别仅协商 HTTP/1.1 的客户端。