历史代码分析。 本文讨论 2022 年携程 Selenium 初版的页面选择器、请求捕获与异常恢复,并提供自有 HTML 测试页示例。Selenium Wire 已归档,相关私有接口不作为当前可用的数据来源。
一、初版解决的任务
2022 年初版源码让 Selenium 打开页面,输入出发城市、到达城市和日期,点击搜索,再从浏览器的网络请求取得结构化响应。这与直接拼接 Requests 请求相比,把一部分会话和页面行为交给浏览器,但仍然受到页面状态、网络错误和服务规则约束。


当时观察到的接口是 batchSearch,这是历史 Web 请求名称,不是已确认供第三方使用的稳定 API。改变城市后没有触发限制的一次结果,不能证明频繁搜索永远不会被限制。
二、初版代码中的具体错误
| 位置 | 初版问题 | 修正原则 |
|---|---|---|
| 浏览器创建 | 旧 executable_path、chrome_options 参数 |
现代 Selenium 用 Service 和 options,先确认驱动版本 |
| 元素定位 | By.CLASS_NAME 填 basic-alert.alert-giftinfo 等复合值 |
两个 class 用 CSS,如 .basic-alert.alert-giftinfo |
| 日期选择 | 缩进混用,异常恢复调用未定义 click_btn() |
先修语法和函数边界,不通过加 sleep 掩盖 |
| 恢复页面 | driver.close() 后递归调用 getpage() |
恢复应明确新建实例,重试有次数上限 |
| 更换城市 | while 与递归没有稳定终止边界 |
显式等待确认输入和提交结果,失败独立记录 |
| 捕获请求 | 第一个匹配 URL 就读取,未充分核对日期 | 触发前清理旧捕获,核对城市、日期、舱位 |
| 响应解码 | 一律当 gzip,缺少响应时继续 | 按 Content-Encoding 解码,缺响应明确失败 |
| 错误处理 | 宽泛异常后继续 | 解析失败、要求验证、无航班、浏览器故障分别记录 |
疫情提示删除代码依赖当时页面和 jQuery,不适用于其他版本的页面。删除遮罩也不等于完成页面要求的交互;出现验证应结束本轮自动任务并按允许流程处理。


Selenium 等待策略解释了隐式与显式等待。固定 sleep 不验证业务状态,混用等待还可能使总时间难以预测。
三、可独立学习的本地页面示例
下面只操作自己创建的 HTML,不访问携程,演示当前初始化、查询身份、显式等待、有限重试和 finally 关闭。运行需要本机 Chrome 与可用驱动。
python -m pip install selenium
保存为 local_browser_demo.py:
from pathlib import Path
import tempfile
import time
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException
HTML = """<!doctype html><meta charset="utf-8">
<input id="departure"><input id="arrival"><input id="date">
<button id="search" onclick="submitQuery()">Search</button>
<div id="result" data-query=""></div>
<script>
function submitQuery() {
const query = ['departure', 'arrival', 'date']
.map(id => document.getElementById(id).value).join('|');
const result = document.getElementById('result');
result.dataset.query = query;
result.textContent = '模拟结果:' + query;
}
</script>"""
def query_local(driver, departure, arrival, departure_date, attempts=3):
if attempts < 1:
raise ValueError('attempts 必须至少为 1')
expected = '|'.join((departure, arrival, departure_date))
wait = WebDriverWait(driver, 5)
for attempt in range(attempts):
try:
for name, value in (('departure', departure), ('arrival', arrival),
('date', departure_date)):
field = wait.until(EC.element_to_be_clickable((By.ID, name)))
field.clear()
field.send_keys(value)
wait.until(EC.element_to_be_clickable((By.ID, 'search'))).click()
result = wait.until(lambda d: (
d.find_element(By.ID, 'result')
if d.find_element(By.ID, 'result').get_attribute('data-query') == expected
else False))
return {'status': 'ok', 'query': expected, 'text': result.text}
except TimeoutException:
if attempt + 1 == attempts:
raise
time.sleep(min(2 ** attempt, 5))
raise RuntimeError('未取得本地结果')
if __name__ == '__main__':
with tempfile.TemporaryDirectory() as temp:
page = Path(temp) / 'query.html'
page.write_text(HTML, encoding='utf-8')
driver = webdriver.Chrome(service=Service())
try:
driver.get(page.resolve().as_uri())
print(query_local(driver, '上海', '北京', '2026-10-01'))
finally:
driver.quit()
选择器只属于本地页面。移植时重新确定输入控件、自动补全、日期状态、提交与响应身份;不能仅替换 URL 就将本地演示用于真实网站。Selenium Service 类
四、初版的数据模型教训
初版源码分别处理首个航段和报价,以航班号合并。多航段、共享航班、同航班多票种会使键不够唯一;应保留行程标识、日期和航段,按源数据的关系关联。
经济舱和商务舱各自收集价格与折扣,独立取最大值推算全价,可能组合不同票种;最低价格与最小折扣也不一定来自同项。缺少明确字段时标为未知,不制造看似准确的原价。


图表对应历史采样,不能作为当前售价。数据应包含获取时间、出发日期、查询条件和解析状态,CSV 写出不等于采集完整。
五、历史源码与后续阅读
历史初版源码供追溯,不代表最新实现。Selenium Wire 上游已停止维护,固定旧依赖不能保证现代浏览器兼容。
V2 代码分析讨论恢复、解码和价格关联,Requests 协议分析说明请求签名与数据模型。三篇讨论不同实现的历史结构,都不提供当前真实查询接口的可用性保证。
评论区