尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Selenium自动化实战:从12306购票流程拆解页面自动化与容错策略

发布时间:2026/9/17 16:44:03

资讯中心
01
ARTICLE

Selenium自动化实战:从12306购票流程拆解页面自动化与容错策略

Selenium自动化实战:从12306购票流程拆解页面自动化与容错策略
简介基于Selenium的12306自动抢票脚本资料包主要面向计算机相关专业的在校学生、开发者以及自动化测试初学者用于解决12306官网手动刷新抢票效率低这一常见问题。资源完整覆盖模拟登录、查询余票、提交订单等自动化流程适合作为课程设计、毕业设计或项目演示的基础也便于在源码基础上进行二次开发与功能扩展。压缩包共18个文件以7个Python脚本为核心辅以YAML/XML配置文件和md/txt文档分别承担参数配置、站点信息维护、邮件通知与使用说明等功能整体仅64KB结构轻量、修改便捷。已有129人学习浏览资料内含可直接运行的抢票源码和详细项目文档可帮助读者快速理解Selenium操作逻辑并进一步拓展多线程抢票、验证码识别等高级功能是一款兼具实战与教学价值的开源资料。1. Selenium 自动化的边界12306 抢票脚本到底在抢什么网上流传的“12306 抢票脚本”十有八九是用 Selenium 打开一个真实浏览器把查车次、选乘客、提交订单这些动作按顺序重复执行。它并不比手快多少Selenium 解决的只是“稳定地、按顺序地”完成操作真正的延迟仍在请求网络的往返时间与验证码处理上。对做自动化测试的人来说这个场景是 WebDriver 面对动态渲染页面的典型样本元素异步出现、弹窗状态变化、会话被服务端校验。本文从 Selenium 自动化测试框架的常规用法出发把一个购票流程拆成可复现的脚本结构并说明每一处容易翻车的地方适合想理解 Python Selenium 在复杂表单站点上落地方式的工程师参考。同时要明确边界12306 有公开用户协议脚本不应被用于干扰正常购票秩序。下面讨论的是 Selenium 如何处理异步页面、维护会话和设计容错属于自动化测试框架研究而不是生产抢票工具。2. 把 12306 购票流程拆成 Selenium 能理解的状态机2.1 购票流程里哪些动作适合自动驱动完整的购票链路可以拆成登录、查询、选择车次、选择乘车人、提交订单、确认支付几个阶段。这中间并不是连续的 HTTP 请求而是页面状态切换输入出发地后会弹出城市候选面板选完日期后车次表格才通过异步渲染出来点“预订”后乘车人列表可能还在加载。用 Selenium 写脚本时最忌讳把整个过程当成一长串find_element().click()顺序执行因为任何一步的网络抖动都会让真实页面比脚本预期晚半拍。一个更稳妥的建模方式是把每个阶段看作状态机中的状态进入下一个状态前必须确认当前状态的“稳定信号”。例如状态常见 DOM 特征适合的等待条件首页/查询页加载城市输入框可点击element_to_be_clickable城市候选面板出现城市列表可见visibility_of_element_located车次表格加载完成存在若干行数据自定义until(lambda d: ...)乘车人列表加载完成复选框可勾选element_to_be_clickable订单确认页价格与车次信息可见text_to_be_present_in_element每个状态只认“信号”不认固定秒数。这样页面快时脚本快页面慢时脚本也不会误操作。2.2 元素定位的四个层次和选择selenium 中定位元素的方式很多但在 12306 这类快速改版的站点上我一般按这个优先级选择稳定的id例如fromStationText、toStationText带语义的CSS_SELECTOR例如.btn72、#queryLeftTable tr可读性好的XPath例如//span[text()北京]页面结构层级尽可能不用因为它一改版就失效。还有两个细节容易被忽略。城市候选框里的可点击项通常不是 input而是自定义渲染的div/span直接send_keys不一定能触发联动更常见的是先点击输入框再用execute_script设置 value 并手动派发input事件。另一个细节是元素可能被弹窗或浮层遮挡find_element能定位到但click()会抛ElementClickInterceptedException因此需要显式等待可点击状态。2.3 显式等待是抢票脚本的第一层盾牌很多人一上来就写time.sleep(3)这不是技术方案是赌运气。Selenium 官方推荐的做法是WebDriverWait配合expected_conditions把“等多久”和“等什么”交给浏览器来判断。下面是一个通用的等待函数from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By def wait_for(driver, locator: tuple, timeout: float 10, state: str clickable): wait WebDriverWait(driver, timeout) if state clickable: return wait.until(EC.element_to_be_clickable(locator)) if state visible: return wait.until(EC.visibility_of_element_located(locator)) if state present: return wait.until(EC.presence_of_element_located(locator)) raise ValueError(funknown state: {state})调用时传一个(By.ID, fromStationText)形式的元组避免在多个条件之间反复拼接字符串。element_to_be_clickable会同时检查元素存在、可见且未被禁用三层判断一次完成适合抢票流程里的绝大多数点击场景。presence_of_element_located只检查 DOM 是否存在适合那些不需要用户交互的隐藏字段visibility_of_element_located则适合等待异步渲染出来的列表。2.4 登录态与验证码不要指望一套脚本全自动12306 的登录流程包含图片验证码、滑块验证甚至短信验证纯 Selenium 脚本很难做到全自动也没有必要。工程上常见的做法是“人机协作”用脚本打开浏览器人工完成验证码登录然后脚本把登录态保存下来供后续使用。这样既绕开了验证码识别的不确定性又不会因为频繁登录触发风控。保存和恢复 Cookie 是 Selenium 会话保持的基础操作import json # 登录成功后保存 cookies driver.get_cookies() with open(cookies.json, w, encodingutf-8) as f: json.dump(cookies, f) # 新会话恢复 driver.get(https://kyfw.12306.cn) with open(cookies.json, r, encodingutf-8) as f: for cookie in json.load(f): driver.add_cookie(cookie) driver.refresh()注意add_cookie前必须先访问一次目标域名否则浏览器会因为“没有域名上下文”而拒绝写入。保存时不要过滤、不要只取name和valueexpiry、path、domain这些字段直接影响恢复后的会话完整性。Cookie 本身有时效过期后页面会跳回登录页后续代码要能识别这种状态并停下等待人工处理。3. 最小可用余票查询脚本Selenium Python 从零到出结果3.1 环境准备Selenium 安装和驱动匹配先装 Python 依赖。Python 版本建议 3.8 以上pip装 Seleniumpython -m pip install seleniumSelenium 4.6 以上自带 Selenium Manager会自动匹配并下载 ChromeDriver省去了手动处理驱动版本的步骤如果还在用 Selenium 3就需要手动下载与浏览器版本匹配的chromedriver并放到PATH中。验证安装是否成功一行代码即可python -c from selenium import webdriver; webdriver.Chrome()如果这行命令能弹出一个空白浏览器窗口说明驱动链接没有问题如果报SessionNotCreatedException基本是浏览器和驱动版本不一致先用chrome://version查看浏览器版本再去下载对应主版本号的驱动。3.2 查询页面最小脚本填车站、选日期、触发查询下面的脚本以查询页https://kyfw.12306.cn/otn/leftTicket/init为目标核心是把“填写出发到达城市”和“设置日期”做成可复用函数。12306 的车站输入框是自定义控件直接send_keys后不会自动填充候选面板需要用 JavaScript 设置值并派发事件import time from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def set_station(driver, input_id: str, station_name: str): input_el driver.find_element(By.ID, input_id) input_el.click() input_el.clear() # 触发搜索候选列表 driver.execute_script( arguments[0].value arguments[1]; arguments[0].dispatchEvent(new Event(input, {bubbles: true}));, input_el, station_name ) time.sleep(1) # 在候选列表里精确点击目标车站 option WebDriverWait(driver, 5).until( EC.element_to_be_clickable((By.XPATH, f//span[text(){station_name}])) ) option.click() driver webdriver.Chrome() wait WebDriverWait(driver, 10) driver.get(https://kyfw.12306.cn/otn/leftTicket/init) set_station(driver, fromStationText, 北京) set_station(driver, toStationText, 上海) date_input wait.until(EC.element_to_be_clickable((By.ID, train_date))) driver.execute_script(arguments[0].value 2024-01-01;, date_input) search_btn wait.until(EC.element_to_be_clickable((By.ID, query_ticket))) search_btn.click()这段代码里最重要的不是点击查询而是execute_script派发input事件。很多前端框架监听的是 JavaScript 事件而不是原生输入直接send_keys后虽然输入框有字但框架内部的状态还是空的点击查询自然查不出结果。station_name要严格匹配车站全称例如“北京”“北京南”是不同对象不能写简称如果有多个城市名匹配XPath 里的text()精确定位会失效需要改成包含匹配。3.3 从车次表格提取余票信息并做简单过滤查询按钮点击后车次表格异步加载。此时不能立刻解析表格最好等列表行数大于 1wait.until(lambda d: len(d.find_elements(By.CSS_SELECTOR, #queryLeftTable tr)) 1) rows driver.find_elements(By.CSS_SELECTOR, #queryLeftTable tr) matched [] for row in rows: cells row.find_elements(By.TAG_NAME, td) if len(cells) 5: continue train_no cells[1].text.strip() second_class cells[4].text.strip() if train_no.startswith(G1) and second_class in (有, 10张): matched.append((train_no, second_class)) for train_no, second_class in matched: print(f{train_no} 二等座余票: {second_class})列索引不是固定的12306 改版后车次列、余票列的顺序可能变化因此这里把它做成一个可配置项更合理。cells[1]是车次号cells[4]是二等座余票文本实际使用时要先手动打开页面检查“二等座”在第几列再填到代码里。更稳定的做法是遍历th表头定位包含“二等座”的表头列索引再用该索引去取同行的td这样即便拖动列顺序也不会错。另外表格中有时会出现隐藏的空行row.find_elements(By.TAG_NAME, td)返回空列表因此代码里加了len(cells) 5的过滤。余票文本可能是“有”“无”“10张”或“18张”判断时不要用! 无因为空字符串也会混进来。4. 抢票脚本的容错工程元素定位、Cookie 与会话保持4.1 元素不稳定时的自愈机制写一点就能跑的脚本容易写能在页面改版时多撑几天的脚本难。最常见的错误是StaleElementReferenceException第一次定位到元素后页面局部刷新旧引用就失效了再调用click()或.text就报错。解决方案是重新查找并重试。我一般把查找动作封装成一个带重试的函数import time from selenium.webdriver.common.by import By from selenium.common.exceptions import StaleElementReferenceException def find_with_retry(driver, by, value, retries: int 3): for attempt in range(retries): try: return driver.find_element(by, value) except StaleElementReferenceException: if attempt retries - 1: raise time.sleep(0.3)使用时要清楚这只是一个兜底不能替代显式等待。更合理的顺序是先WebDriverWait等元素可交互再在随后的操作中捕获StaleElementReferenceException并重走“定位-操作”这个原子步骤。不要在一个元素引用上反复重试那个引用已经失效重试也没有意义。4.2 Cookie 与会话保持的边界第 2 章里保存和恢复 Cookie 的做法已经覆盖基础场景但生产级的会话保持还需要处理三个细节恢复 Cookie 后必须刷新页面否则driver.get_cookies()里虽然能看到 Cookie但页面上下文还没有带上某些 Cookie 标记为 session cookie不写expiry保存后如果浏览器退出它就没了需要重新登录登录失效后页面通常会跳转到某个公共登录页可以通过driver.current_url判断是否包含login关键字一旦发现就停止自动操作并告警。如果脚本需要长时间运行建议每次成功查询后顺手把 Cookie 重新写回文件避免中途过期后只能白等。4.3 频率控制与并发不是浏览器开得越多越好很多人直觉上觉得抢票快就是并发多开几十个浏览器窗口但真实站点风控看的是 IP、浏览器指纹、操作节奏和行为特征。多个 Chrome 实例如果共用同一个代理 IP高频请求会更容易触发滑块验证反而把自己锁在外面。我一般会控制单浏览器实例的轮询频率并把请求间隔做成随机抖动import random import time def human_pause(min_seconds: float 1.0, max_seconds: float 2.5): time.sleep(random.uniform(min_seconds, max_seconds))随机间隔不是为了“拟人”而是避免固定的sleep(1)形成可被识别的节律。要并发优先考虑 Selenium Grid 或容器化方案把浏览器分散到多个节点但每个节点的间隔仍然要控制。这里的关键不是代码能开多少线程而是站点允许你在多长时间内完成多步操作。4.4 日志与截图脚本出问题最怕不知道在哪一步Selenium 脚本报错时只打印堆栈往往不够因为真正的问题在页面状态不在 Python 代码。常见的做法是在主流程里包一层异常捕获把页面源代码和截图一起落盘import logging logging.basicConfig(filenameticket.log, levellogging.INFO) def safe_step(driver, step_name: str, func): try: logging.info(start: %s, step_name) return func() except Exception: ts int(time.time()) driver.save_screenshot(ffail_{step_name}_{ts}.png) with open(ffail_{step_name}_{ts}.html, w, encodingutf-8) as f: f.write(driver.page_source) logging.exception(failed at %s, step_name) raisefail_*.png用于肉眼确认页面卡在哪fail_*.html用于离线分析 DOM 结构。两者配合才能判断是元素选择器失效、Cookie 过期、还是弹窗遮挡。下表是几个高频异常和最短应对路径异常触发条件对策NoSuchElementException元素还没渲染或选择器失效先加显式等待无效就检查页面快照StaleElementReferenceException页面刷新后旧引用失效重新查找并重试TimeoutException等待条件超时检查当前 URL 和页面提示ElementClickInterceptedException弹窗或浮层遮挡按钮等待弹窗消失或先关闭浮层这四种异常覆盖了绝大多数脚本挂掉的情况。日志里除了记录异常还要记录当前 URL 和最近一次操作名方便回放。5. 从“能跑”到“可交付”Selenium 项目资料包的落地规范5.1 资料包目录结构和依赖锁如果要把脚本交出去或者隔几个月再回来看代码本身只是一半另一半是依赖和配置。我一般会按这个结构组织project/ ├── config.yaml ├── requirements.txt ├── main.py ├── core/ │ ├── driver_factory.py │ ├── wait_utils.py │ └── ticket_query.py ├── logs/ └── README.mdrequirements.txt里不要只写包名至少固定主版本号selenium4.10 pyyaml6.0config.yaml存放车站名、可配置列索引、查询间隔、Cookie 文件路径等不要把这些参数散落在代码里。这样别人拿到资料包后改配置就能跑不需要读懂每一个函数。5.2 下载文件时的下一步判断抢票脚本不一定只解析页面也可能需要导出结果报表或保存车次截图。Selenium 点击下载按钮后文件是浏览器进程在下载Python 代码不会自动等它完成。“selenium 怎样使文件下载完成之后才进行下一步”是高频搜索问题标准做法是配置浏览器默认下载目录然后轮询目录里是否出现目标文件并判断.crdownload临时后缀是否消失import os import time def wait_for_download(download_dir: str, filename: str, timeout: float 30): file_path os.path.join(download_dir, filename) deadline time.time() timeout while time.time() deadline: if os.path.exists(file_path) and not os.path.exists(file_path .crdownload): return True time.sleep(0.5) return Falsedownload.default_directory需要在创建浏览器时设置options webdriver.ChromeOptions() prefs { download.default_directory: /tmp/ticket_exports, download.prompt_for_download: False, } options.add_experimental_option(prefs, prefs)如果忽略了.crdownload后缀代码可能会在一个写了一半的文件上做解析结果读到残缺数据。设置download.prompt_for_download为False可以避免弹窗卡住自动化流程。5.3 把脚本改造成可复用的自动化框架最后一个具体技巧是把浏览器启动封装成上下文管理器并让页面操作函数只依赖传入的driver实例。这样同一个脚本既能跑查询、又能跑测试还能和pytest结合做日常回归from contextlib import contextmanager from selenium import webdriver contextmanager def create_driver(headless: bool True): options webdriver.ChromeOptions() if headless: options.add_argument(--headlessnew) driver webdriver.Chrome(optionsoptions) try: yield driver finally: driver.quit() with create_driver(headlessFalse) as driver: query TicketQuery(driver) query.search(北京, 上海, 2024-01-01)把 driver 创建放在finally里能保证脚本每次异常退出时浏览器进程都会被回收不会留下几十个残留 Chrome。所有页面对象接受driver参数后测试代码里替换成pytest的 fixture 就是顺手的事这也是 Selenium 自动化测试框架沉淀资料包时最值得做的一步。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。