简介:这是一份基于Selenium的12306自动抢票脚本完整项目资料,面向计算机相关专业的在校学生、教师及企业员工,尤其适合用作毕业设计、课程设计、作业或项目初期立项演示,也适合具备一定Python基础的小白进阶学习。资源包共18个文件,以7个py脚本为核心,配合5个xml配置、3个txt说明、1个md文档、1个yaml配置文件及1个iml工程文件,压缩包约64KB,结构清晰、便于按模块查阅。项目围绕抢票主流程展开,涵盖车站信息获取、配置读取、邮件通知与购票逻辑等模块,代码经过实际运行测试,功能可用,并附有详细文档辅助理解。目前已有129人学习下载,可作为Selenium自动化实战的参考案例,读者既能直接运行体验,也能在此基础上修改扩展,实现其他自动化功能,适合作为毕设、课设或自学练手项目使用。
1. 12306 抢票脚本到底在抢什么:从 Selenium 驱动浏览器到车票查询接口的真相
每年春运前一周,后台总会收到类似私信:“用 Selenium 写 12306 抢票脚本,到底能不能跑通?”问的人多了,我干脆把过去几年反复调试、翻车、再重构的经验整理成一份可复现的笔记。核心结论先放这里:Selenium 抢票脚本能跑通查询和下单流程,但真正决定成败的不是代码写得多花哨,而是对 12306 页面加载节奏、登录态维持和余票刷新频率的理解。它适合两类人:一是想学 Python Selenium 自动化实战的开发者,二是需要理解浏览器自动化边界在哪的测试工程师。不适合指望“挂上就能抢到”的人,因为 12306 的反爬和风控从来不是靠一个脚本就能绕过的。这篇内容会从环境搭建、登录态保持、余票监控、下单提交四个环节拆开讲,每一步都给可抄的代码和参数说明,最后收在几个我踩过的坑上。
2. 环境准备与 Selenium 驱动 12306 的最小可跑通方案
2.1 为什么选 Selenium 而不是直接怼接口
很多人第一反应是抓 12306 的查票接口直接发请求,但实际操作过的人知道,12306 的接口参数带动态签名,且频繁请求会触发风控。Selenium 的优势在于它驱动真实浏览器,页面渲染、JS 执行、Cookie 管理都由浏览器完成,脚本只需要模拟人的操作节奏。常见做法是用 Chrome + ChromeDriver,配合 Selenium 的 WebDriverWait 做显式等待,避免页面没加载完就点按钮导致元素找不到。
选版本时注意:Selenium 4.x 对 W3C 标准支持更好,ChromeDriver 版本必须和本地 Chrome 大版本一致,否则启动就报 session not created。我一般会先跑一个最小示例确认环境通了,再往上叠业务逻辑。
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) driver = webdriver.Chrome(options=options) driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", { "source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})" }) driver.get("https://kyfw.12306.cn/otn/resources/login.html") wait = WebDriverWait(driver, 15) login_btn = wait.until(EC.element_to_be_clickable((By.CLASS_NAME, "login-hd-account"))) print("页面标题:", driver.title) driver.quit()这段代码做了三件事:隐藏 webdriver 特征、打开登录页、等待账号登录入口可点击。--disable-blink-features=AutomationControlled是减少被识别为自动化的关键参数,Page.addScriptToEvaluateOnNewDocument在每个页面加载前注入脚本,把navigator.webdriver改成 undefined。WebDriverWait的超时设 15 秒,是因为 12306 登录页在高峰期加载会超过 10 秒,设太短会误报超时。
2.2 ChromeDriver 版本对齐与依赖安装的实操命令
环境翻车十次有八次是版本不对。先查本地 Chrome 版本,再下对应 ChromeDriver。Linux 和 macOS 用命令行查,Windows 在浏览器地址栏输入chrome://version看。
# 查看 Chrome 版本(macOS/Linux) google-chrome --version # 或 chromium --version # 安装 Python 依赖 pip install selenium==4.15.0 pip install requests pillow # 下载 ChromeDriver 后放到 PATH 或指定路径 # 验证驱动可用 python -c "from selenium import webdriver; d=webdriver.Chrome(); print(d.capabilities['browserVersion']); d.quit()"参数说明:selenium==4.15.0是我在多个项目里验证过相对稳定的版本,太新的版本偶尔和旧 ChromeDriver 有兼容问题。requests和pillow是为后续处理验证码和通知准备的。验证命令能打印出浏览器版本就说明驱动通了,如果报Message: session not created,九成是版本不匹配。
提示:不要把 ChromeDriver 硬编码在代码里,用
webdriver.Chrome()让它自动从 PATH 找,换机器时少改一处。
3. 登录态维持与车次查询:Selenium 操作 12306 的核心链路
3.1 扫码登录与 Cookie 持久化的取舍
12306 登录方式有账号密码和扫码两种。账号密码登录会触发滑块验证,扫码登录相对省事,但二维码有效期短。我一般用扫码登录后把 Cookie 存下来,后续查询复用,避免每次重新登录。
import pickle import time from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def login_and_save_cookie(driver, wait): driver.get("https://kyfw.12306.cn/otn/resources/login.html") # 切换到扫码 tab qr_tab = wait.until(EC.element_to_be_clickable((By.CLASS_NAME, "login-hd-code"))) qr_tab.click() print("请用 12306 App 扫码,等待登录完成...") # 等待跳转到个人中心,说明登录成功 wait.until(EC.url_contains("https://kyfw.12306.cn/otn/view/index.html")) time.sleep(2) cookies = driver.get_cookies() with open("12306_cookies.pkl", "wb") as f: pickle.dump(cookies, f) print("Cookie 已保存,共", len(cookies), "条") return cookies def load_cookie(driver): driver.get("https://kyfw.12306.cn/otn/resources/login.html") with open("12306_cookies.pkl", "rb") as f: cookies = pickle.load(f) for c in cookies: driver.add_cookie(c) driver.refresh()逻辑说明:扫码后等待 URL 跳到个人中心页,这是判断登录成功的可靠信号,比等某个元素出现更稳。Cookie 存成 pickle 文件,下次启动浏览器后先访问登录页再 add_cookie,最后 refresh 让 Cookie 生效。参数上time.sleep(2)是给页面写入 Cookie 留缓冲,太短会丢。
3.2 车次查询的显式等待与余票解析
查询页的核心是填出发站、到达站、日期,然后点查询,再从结果表格里解析余票。12306 的查询结果表格是动态渲染的,必须用显式等待等表格出现。
def query_tickets(driver, wait, from_station, to_station, date): driver.get("https://kyfw.12306.cn/otn/leftTicket/init") # 填写出发站 from_input = wait.until(EC.presence_of_element_located((By.ID, "fromStationText"))) from_input.clear() from_input.send_keys(from_station) # 填写到达站 to_input = driver.find_element(By.ID, "toStationText") to_input.clear() to_input.send_keys(to_station) # 填写日期 date_input = driver.find_element(By.ID, "train_date") driver.execute_script("arguments[0].value = arguments[1]", date_input, date) # 点查询 query_btn = driver.find_element(By.ID, "query_ticket") query_btn.click() # 等结果表格 table = wait.until(EC.presence_of_element_located((By.ID, "queryLeftTable"))) rows = table.find_elements(By.TAG_NAME, "tr") results = [] for row in rows: try: train_no = row.find_element(By.CSS_SELECTOR, ".number").text seats = row.find_elements(By.CSS_SELECTOR, ".cdz td") results.append((train_no, [s.text for s in seats])) except Exception: continue return results参数说明:fromStationText和toStationText是 12306 查询页的输入框 ID,直接 send_keys 会触发下拉联想,但这里简化处理,实际用的时候要等联想列表出现再选,否则可能填错站。日期用execute_script直接赋值,因为日期控件是只读的,send_keys 写不进去。queryLeftTable是结果表格 ID,等它出现再解析行。每行里.number是车次,.cdz td是各席别余票单元格。
注意:查询频率不要高于 5 秒一次,否则容易触发 12306 的请求限制,页面会返回“操作过于频繁”。
4. 余票监控与下单提交:Selenium 抢票脚本的最后一公里
4.1 轮询查票的间隔控制与异常重试
余票监控本质是定时轮询查询接口,但用 Selenium 轮询成本高,每次都要刷新页面。我的做法是查询到有票后,立刻进入下单流程,而不是一直刷。轮询间隔设 5 到 8 秒,带随机抖动,避免固定频率被识别。
import random import time def monitor_tickets(driver, wait, from_station, to_station, date, target_train): while True: try: results = query_tickets(driver, wait, from_station, to_station, date) for train_no, seats in results: if train_no == target_train and any(s not in ("--", "无", "") for s in seats): print("发现余票:", train_no, seats) return train_no, seats time.sleep(random.uniform(5, 8)) except Exception as e: print("查询异常,重试:", e) time.sleep(10) driver.refresh()逻辑说明:random.uniform(5, 8)让每次间隔不一样,降低被风控的概率。异常时 refresh 页面再重试,因为长时间运行后页面可能失效。target_train是目标车次,只盯这一趟,减少无效判断。
4.2 下单提交的点击链路与确认页处理
发现余票后,点“预订”按钮进入确认页,选乘客、选席别、提交订单。这一步最容易翻车,因为确认页元素多,加载慢。
def submit_order(driver, wait, train_no): # 找到对应车次的预订按钮 book_btn = wait.until(EC.element_to_be_clickable( (By.XPATH, f"//tr[.//a[contains(text(), '{train_no}')]]//a[text()='预订']") )) book_btn.click() # 等确认页 wait.until(EC.presence_of_element_located((By.ID, "confirmPassenger"))) # 勾选乘客(这里假设乘客已保存在常用联系人) passenger_checkbox = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "#normal_passenger_id input"))) if not passenger_checkbox.is_selected(): passenger_checkbox.click() # 提交订单 submit_btn = wait.until(EC.element_to_be_clickable((By.ID, "submitOrder_id"))) submit_btn.click() # 等确认弹窗 confirm_btn = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "#qr_submit_id"))) confirm_btn.click() print("订单已提交,请尽快支付")参数说明:XPath 里用contains(text(), train_no)定位车次行,再找该行里的“预订”链接。confirmPassenger是确认页容器 ID,等它出现说明页面加载完。乘客勾选框在#normal_passenger_id下,提交按钮submitOrder_id,最后确认弹窗的按钮qr_submit_id。每一步都用显式等待,不用 sleep 硬等。
提示:下单成功后 12306 会保留 30 分钟支付时间,脚本提交完就可以人工接管支付,不要试图自动化支付环节。
5. 避坑与排查:Selenium 抢票脚本最常见的 5 个翻车现场
5.1 元素定位不到,报 NoSuchElementException
现象:脚本跑着跑着突然报找不到元素,但手动打开页面元素明明在。原因:12306 页面有 iframe 嵌套,或者元素在动态加载的弹层里,直接 find_element 找不到。解决:先driver.switch_to.frame()切到对应 iframe,或者用 WebDriverWait 等元素可见再操作。我一般会在报错时截图,看当时页面到底停在哪。
5.2 登录态失效,查询页跳回登录页
现象:Cookie 加载后第一次查询正常,过几分钟再查就跳登录。原因:12306 的会话有有效期,且部分 Cookie 是 HttpOnly,pickle 存的时候可能丢。解决:不要指望 Cookie 永久有效,脚本里加一个检测,如果 URL 变成登录页就重新扫码。另外每次查询前先访问一次个人中心页确认登录态。
5.3 查询频率过高被限流,页面返回空白
现象:连续查询十几次后,结果表格不刷新,或者返回“操作过于频繁”。原因:请求间隔太短,触发风控。解决:间隔加到 5 秒以上,带随机抖动,并且每查 20 次就 refresh 一次页面,模拟人工操作节奏。
5.4 下单时乘客勾选失败,提交按钮灰色
现象:确认页乘客列表加载出来了,但勾选后提交按钮还是灰的。原因:乘客信息没同步,或者席别没选。解决:勾选乘客后等一秒,再检查席别下拉框是否已选,没选就手动触发 change 事件。提交按钮灰色时不要硬点,先看页面有没有提示未选乘客。
5.5 ChromeDriver 被识别,页面弹验证码
现象:脚本启动后访问 12306,直接弹滑块验证。原因:webdriver 特征没藏干净,或者 ChromeDriver 版本太旧。解决:用--disable-blink-features=AutomationControlled加 CDP 注入,并且 ChromeDriver 版本和 Chrome 大版本对齐。如果还弹,就换用 undetected-chromedriver 这类库,但注意它更新频繁,要锁版本。
6. 把抢票脚本跑稳的一个小技巧:用日志和截图做黑匣子
脚本跑在后台,翻车时你不在现场,事后全靠猜。我的习惯是给每个关键步骤加日志和截图,出问题时能回放。具体做法是封装一个snap函数,在查询、下单、提交三个节点各截一张图,文件名带时间戳。
import os import time def snap(driver, tag): ts = time.strftime("%Y%m%d_%H%M%S") path = f"logs/{tag}_{ts}.png" os.makedirs("logs", exist_ok=True) driver.save_screenshot(path) print(f"[snap] {tag} -> {path}")参数说明:tag用来区分步骤,比如query、confirm、submit。截图存到 logs 目录,时间戳避免覆盖。这个习惯帮我定位过好几次问题,比如有一次提交按钮一直点不到,截图一看是弹了个“请先选择乘客”的提示,而日志里只看到点击失败。
另外,日志里记录每次查询的车次和余票状态,方便事后分析哪个时间点放票。我一般用 Python 的 logging 模块,输出到文件和控制台,级别设 INFO。抢票这件事,脚本只是工具,真正值钱的是你对放票规律和页面行为的观察。希望帮到你。
本文还有配套的精品资源,点击获取