侧边栏壁纸
博主头像
いちごみるく

行动起来,活在当下

  • 累计撰写 70 篇文章
  • 累计创建 48 个标签
  • 累计收到 55 条评论

目 录CONTENT

文章目录

Selenium 携程机票采集初版:历史流程与状态管理

历史代码分析。 本文讨论 2022 年携程 Selenium 初版的页面选择器、请求捕获与异常恢复,并提供自有 HTML 测试页示例。Selenium Wire 已归档,相关私有接口不作为当前可用的数据来源。

一、初版解决的任务

2022 年初版源码让 Selenium 打开页面,输入出发城市、到达城市和日期,点击搜索,再从浏览器的网络请求取得结构化响应。这与直接拼接 Requests 请求相比,把一部分会话和页面行为交给浏览器,但仍然受到页面状态、网络错误和服务规则约束。

2022 年更换城市的界面

2022 年请求观察

当时观察到的接口是 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 类

四、初版的数据模型教训

初版源码分别处理首个航段和报价,以航班号合并。多航段、共享航班、同航班多票种会使键不够唯一;应保留行程标识、日期和航段,按源数据的关系关联。

经济舱和商务舱各自收集价格与折扣,独立取最大值推算全价,可能组合不同票种;最低价格与最小折扣也不一定来自同项。缺少明确字段时标为未知,不制造看似准确的原价。

2022 年结构化表格

当时的票价变化图

图表对应历史采样,不能作为当前售价。数据应包含获取时间、出发日期、查询条件和解析状态,CSV 写出不等于采集完整。

五、历史源码与后续阅读

历史初版源码供追溯,不代表最新实现。Selenium Wire 上游已停止维护,固定旧依赖不能保证现代浏览器兼容。

V2 代码分析讨论恢复、解码和价格关联,Requests 协议分析说明请求签名与数据模型。三篇讨论不同实现的历史结构,都不提供当前真实查询接口的可用性保证。

4

评论区