☰
基于Selenium的12306抢票脚本实战:环境搭建、登录态维持与反爬边界
2026/10/3 18:00:35 网站建设 项目流程

简介:这是一份基于Selenium的12306自动抢票脚本完整项目资料,面向计算机相关专业的在校学生、教师及企业员工,尤其适合用作毕业设计、课程设计、作业或项目初期立项演示,也适合具备一定Python基础的小白进阶学习。资源包共18个文件,以7个py脚本为核心,配合5个xml配置、3个txt说明、1个md文档、1个yaml配置文件及1个iml工程文件,整体约64KB,结构清晰、便于按模块查阅。项目围绕抢票主流程展开,涵盖车站信息获取、配置读取、邮件通知与购票逻辑等模块,代码经过实际运行测试,功能可用,并附有详细文档辅助理解。目前已有129人学习下载,可作为Selenium自动化实战的参考范例,读者既能直接运行体验,也能在此基础上修改扩展,实现其他自动化功能,适合作为学习进阶与项目开发的起点。

1. 从「抢票脚本」到可复现工程:Selenium 驱动 12306 的真实边界

每年春运前后,12306抢票脚本的搜索量都会暴涨,selenium配合python selenium的组合几乎成了新手入门自动化的第一课。但绝大多数人卡在同一个地方:本地跑起来能登录,一到查票就返回空列表,或者提交订单时被弹回登录页。这不是脚本写得不够长,而是没有理解 12306 的前端渲染节奏、请求校验链路和 Selenium 的等待模型之间的错位。这篇笔记围绕「基于 Selenium 的 12306 自动抢票脚本」这个方向,把环境搭建、登录态维持、车次查询、订单提交这条链路拆成可复现的步骤,同时把反爬边界和合规边界讲清楚。适合已经会 Python 基础语法、想用selenium自动化测试框架练手真实场景的读者,也适合做过半成品但一直不稳定、想找到根因的熟手。需要先说明:本文讨论的是技术学习与自动化测试思路,实际使用必须遵守 12306 官方规则,任何绕过官方正常购票流程的行为都不在讨论范围内。

2. 环境与依赖:把 Selenium 跑通的最小闭环

2.1 浏览器驱动版本对齐这件事为什么总翻车

selenium安装本身一条 pip 命令就够,真正让人头疼的是浏览器和驱动版本对不上。Chrome 从 115 版本之后改了分发方式,以前单独下载 chromedriver 的老教程大面积失效。现在的常见做法是用webdriver-manager自动匹配,或者直接用 Selenium 4.6 以上内置的 Selenium Manager。我一般会先确认三件事:Chrome 主版本号、Selenium 版本、以及是否走了代理下载驱动。三者任一不对,报错信息都是SessionNotCreatedException,看起来像代码问题,其实是环境问题。

# 查看 Chrome 主版本,Linux/macOS 通用 google-chrome --version # 或 macOS /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --version # 安装依赖,建议锁版本,避免自动升级踩坑 pip install "selenium>=4.16,<5" webdriver-manager

逻辑说明:把 Selenium 锁在 4.x 是因为 4.6 之后内置了驱动管理,能省掉手动下载。webdriver-manager作为兜底,在网络受限环境下可以指定国内镜像。参数上,selenium>=4.16是为了拿到较新的 BiDi 支持,<5是防止未来大版本破坏 API。

2.2 一个能启动、能退出的骨架脚本

不要一上来就写登录,先把「打开页面、等待元素、截图、退出」这条最小闭环跑通。很多人的脚本跑一半浏览器不关,进程越积越多,最后内存爆掉,还以为是 12306 封了 IP。

from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC options = Options() # 关闭自动化特征提示,减少被前端识别的概率 options.add_argument("--disable-blink-features=AutomationControlled") options.add_experimental_option("excludeSwitches", ["enable-automation"]) options.add_experimental_option("useAutomationExtension", False) # 固定窗口大小,避免响应式布局导致元素定位漂移 options.add_argument("--window-size=1280,900") driver = webdriver.Chrome(options=options) # 覆盖 navigator.webdriver 属性,这是最基础的指纹处理 driver.execute_cdp_cmd( "Page.addScriptToEvaluateOnNewDocument", {"source": "Object.defineProperty(navigator,'webdriver',{get:()=>undefined})"} ) try: driver.get("https://www.12306.cn/index/") # 显式等待,等搜索框出现再操作,别用 sleep 硬等 WebDriverWait(driver, 15).until( EC.presence_of_element_located((By.ID, "fromStationText")) ) driver.save_screenshot("step1_home.png") finally: driver.quit()

逻辑说明:excludeSwitches和useAutomationExtension是去掉 Chrome 启动时的自动化提示条,Page.addScriptToEvaluateOnNewDocument在每个新文档加载前注入脚本,把navigator.webdriver改成 undefined。参数上,WebDriverWait的超时设 15 秒是经验值,12306 首页在弱网下加载较慢,设太短会误判失败。save_screenshot是排查利器,元素定位失败时先看截图,比盯着日志猜快得多。

提示:驱动下载如果卡住,可以手动指定webdriver_manager的镜像源,或者提前把驱动放到 PATH 里,不要让脚本在启动阶段就超时。

3. 登录态与查询链路:Selenium 在 12306 上到底能做什么

3.1 扫码登录与 Cookie 复用:别每次都从头登

12306 的登录方式里,扫码登录对自动化最友好,因为它不涉及账号密码输入框的复杂校验。但扫码本身需要人工介入,所以工程上的常见做法是:第一次手动扫码,把 Cookie 存下来,后续查询复用 Cookie,直到失效再重新扫码。这样既避开了验证码,也减少了登录频率。

import pickle import time def login_and_save_cookie(driver, cookie_path="cookies.pkl"): driver.get("https://kyfw.12306.cn/otn/resources/login.html") # 切换到扫码 tab,具体选择器以实际页面为准 WebDriverWait(driver, 15).until( EC.element_to_be_clickable((By.CLASS_NAME, "login-hd-account")) ).click() # 这里留出人工扫码时间,轮询检测登录成功标志 for _ in range(60): if "index" in driver.current_url or driver.get_cookies(): break time.sleep(2) with open(cookie_path, "wb") as f: pickle.dump(driver.get_cookies(), f) def load_cookie(driver, cookie_path="cookies.pkl"): driver.get("https://www.12306.cn/index/") with open(cookie_path, "rb") as f: cookies = pickle.load(f) for c in cookies: # 部分字段不能直接 add_cookie,需要剔除 c.pop("sameSite", None) try: driver.add_cookie(c) except Exception: pass driver.refresh()

逻辑说明:pickle存的是 Cookie 列表,add_cookie之前必须先在目标域名下打开页面,否则会报InvalidCookieDomainException。sameSite字段在部分 Selenium 版本里不被接受,直接 pop 掉最省事。轮询检测登录成功不要用固定 sleep,因为扫码时间不可控,60 次乘 2 秒是两分钟上限,够用且不会死等。

3.2 车次查询:为什么你的结果总是空列表

查询接口是 12306 反爬最集中的地方。页面上的查询按钮点下去之后,前端会先请求一个queryU之类的接口拿 token,再带着 token 去查票。Selenium 直接点按钮,如果等待策略不对,拿到的 DOM 还是旧数据。更稳的做法是等结果表格的某个具体行出现,而不是等表格容器出现。

def query_tickets(driver, from_station, to_station, date): driver.get("https://kyfw.12306.cn/otn/leftTicket/init") # 出发站输入框有自动补全,输入后要等下拉列表出现再选 from_input = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, "fromStationText")) ) from_input.clear() from_input.send_keys(from_station) WebDriverWait(driver, 5).until( EC.element_to_be_clickable((By.CSS_SELECTOR, "#panel_cities .city")) ).click() to_input = driver.find_element(By.ID, "toStationText") to_input.clear() to_input.send_keys(to_station) WebDriverWait(driver, 5).until( EC.element_to_be_clickable((By.CSS_SELECTOR, "#panel_cities .city")) ).click() # 日期输入框通常是 readonly,需要用 JS 去掉只读再赋值 date_input = driver.find_element(By.ID, "train_date") driver.execute_script("arguments[0].removeAttribute('readonly')", date_input) date_input.clear() date_input.send_keys(date) driver.find_element(By.ID, "query_ticket").click() # 等具体车次行出现,而不是等表格 WebDriverWait(driver, 15).until( EC.presence_of_element_located((By.CSS_SELECTOR, "#queryLeftTable tr")) ) rows = driver.find_elements(By.CSS_SELECTOR, "#queryLeftTable tr") return [r.text for r in rows if r.text.strip()]

逻辑说明:出发站和到达站都有自动补全下拉,必须等.city元素可点击再点,否则输入的内容不会被前端状态接受。日期框是 readonly,用execute_script去掉属性再 send_keys,这是 12306 前端的一个固定套路。等待条件用#queryLeftTable tr而不是表格本身,是因为表格容器一直在,行才是新数据到来的标志。返回的文本列表可以进一步解析,但先确认能拿到非空结果,再谈解析。

注意:查询频率不要设得太高,常见做法是 5 到 10 秒一次,并且加随机抖动。固定间隔的高频请求既容易触发风控,也不符合正常用户行为。

4. 订单提交与反爬对抗:哪些能做,哪些不该做

4.1 提交订单前的校验链路

从查询结果到提交订单,中间隔着「选择车次、选择席别、选择乘客、确认订单」四步。每一步都有前端校验,Selenium 点击的节奏如果太快,会出现「点了没反应」的玄学现象。根因通常是前一步的异步请求还没回来,下一步的按钮虽然可见但绑定的 handler 还没就绪。解决办法不是加长 sleep,而是等某个状态标志,比如席别下拉框的选项数量大于零。

def select_train_and_submit(driver, train_no): # 找到目标车次行,点击「预订」 rows = driver.find_elements(By.CSS_SELECTOR, "#queryLeftTable tr") for row in rows: if train_no in row.text: # 预订按钮可能是 a 标签或 button,用相对定位更稳 book_btn = row.find_element(By.CSS_SELECTOR, "a.btn72, button.btn72") book_btn.click() break # 等席别选择区域出现 WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, "seatType_1")) ) # 选择第一个可用席别,实际使用要按需判断 driver.find_element(By.ID, "seatType_1").click() # 等乘客列表加载 WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CSS_SELECTOR, "#normal_passenger_id li")) ) passengers = driver.find_elements(By.CSS_SELECTOR, "#normal_passenger_id li") if passengers: passengers[0].click() # 提交订单 driver.find_element(By.ID, "submitOrder_id").click()

逻辑说明:车次行的定位用文本包含而不是精确匹配,因为行文本里混了时间、车站等冗余信息。席别和乘客列表都用显式等待,确保异步数据到位。submitOrder_id点击后可能弹出确认框,实际工程里还要处理弹窗,但这一步已经能验证链路是否通。参数上,所有超时设 10 秒是平衡值,太短误报,太长拖慢整体节奏。

4.2 反爬边界:python selenium反爬虫在 12306 上的真实水位

12306 的风控不是单一维度,它同时看请求频率、Cookie 完整性、浏览器指纹、以及行为轨迹。Selenium 能改的只是指纹里很小的一部分。navigator.webdriver只是入门级检测,更深的有 Canvas 指纹、WebGL 渲染器、字体列表、时区与语言一致性。指望靠几行 CDP 命令就完全隐身,是不现实的。我的判断是:Selenium 适合做「低频、有人工介入、以学习为目的」的自动化,不适合做「高频、全自动、绕过风控」的抢票。后者既技术上难以稳定,也触碰合规红线。

检测维度Selenium 默认表现可处理程度建议
navigator.webdrivertrue可覆盖启动时注入脚本
Chrome 启动参数含自动化标记可部分去除excludeSwitches
Canvas 指纹与真实浏览器有差异难完全一致不建议强行伪造
请求频率由脚本控制可控加随机间隔,降频
Cookie 完整性取决于登录流程可控复用真实登录态

这张表想说明的是:能改的改,改不动的不要硬刚。把精力放在「稳定拿到查询结果」和「人工确认后提交」上,比追求全自动更有实际价值。

5. 避坑与排查:那些让你白熬一晚上的具体问题

5.1 元素定位突然失效

现象:昨天还能跑的By.ID,今天报NoSuchElementException。原因:12306 前端会做 A/B 测试或灰度发布,同一功能的元素 ID 可能变化。解决:优先用相对稳定的 CSS 选择器,比如基于文本内容或层级关系,而不是依赖单一 ID;同时把定位逻辑抽成函数,方便集中替换。

5.2 登录后立刻跳回登录页

现象:Cookie 存了也 load 了,一查询就提示未登录。原因:Cookie 里的关键字段(如RAIL_EXPIRATION、RAIL_DEVICEID)有时效性,或者add_cookie时域名不匹配。解决:load 之后先访问一个需要登录的页面验证状态,失效就重新扫码;add_cookie前确认当前 URL 在.12306.cn域下。

5.3 查询结果一直是空的

现象:表格行数为零,截图显示页面正常。原因:查询接口返回了数据,但前端渲染被拦截,或者等待条件选错了元素。解决:打开浏览器开发者工具,看 Network 里查询接口的实际返回;把等待条件从表格容器改成具体行;必要时直接解析接口返回的 JSON,而不是依赖 DOM。

5.4 提交订单时弹出验证码

现象:点击提交后出现滑块或图形验证。原因:行为轨迹被判定为异常,或者提交频率过高。解决:降低操作速度,在关键步骤之间加随机延迟;不要试图用第三方打码平台绕过,这既不稳定也不合规。遇到验证码就转人工,这是最务实的选择。

5.5 浏览器进程残留

现象:脚本报错退出后,Chrome 进程还在,下次启动端口冲突。原因:driver.quit()没有放在 finally 里,异常时被跳过。解决:所有 driver 操作包在 try/finally 中,finally 里调 quit;或者用 context manager 封装。

提示:排查顺序永远是「先看截图,再看 Network,最后看代码」。大部分问题不在代码,在页面状态和网络返回。

6. 把脚本变成可维护工具的几个习惯

写到能跑通查询和提交之后,真正决定这个方向值不值得继续投入的,是它能不能被维护。我自己的习惯是:第一,把所有选择器集中到一个配置文件或常量模块里,12306 改版时只改一处;第二,给每个关键步骤加日志和截图开关,出问题时能回放;第三,把「查询」和「提交」拆成两个独立阶段,查询可以自动跑,提交必须人工确认,这样既保留了自动化的效率,又守住了合规底线。第四,定期清理 Cookie 和截图目录,别让临时文件堆成黑匣子。最后一条血泪经验:不要追求 100% 全自动,12306 的风控在进化,你的脚本也在和它赛跑,把目标定在「减少重复操作」而不是「完全替代人工」,心态会稳很多,代码也会干净很多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询