1. 抢票脚本的真实定位与核心思路拆解
1.1 先泼一盆冷水:没有100%成功的抢票脚本
先把话说在前头。标题里写的“100%成功”,是吸引你点进来的钩子,不是技术承诺。我做了这么多年自动化,可以负责任地讲:任何声称能100%抢到票的脚本,要么是在骗你,要么是在骗自己。抢票这件事的最终决定权在12306的服务器端,脚本能做的只是把你手动操作的流程自动化,把“人盯着屏幕点鼠标”变成“程序按规则高频查询并提交”。它能提高的是你的操作效率,不是改变票池的供需关系。
那脚本到底能解决什么问题?简单说,它帮你干三件事:高频查询余票、自动提交订单、多车次多日期并行监控。手动抢票时你一次只能盯一个车次,刷新慢了就没了;脚本可以同时监控几十个车次组合,一旦有票在毫秒级发起请求。这就是它的价值所在。
适合谁来参考这篇内容?有Python基础、想理解自动化抢票原理的开发者;正在学selenium想做点实战项目练手的人;以及被抢票折磨过、想自己动手折腾一套工具的技术爱好者。如果你完全没写过代码,建议先补一补Python基础语法再来看,不然配置环境那一步就会卡住。
1.2 为什么选selenium而不是直接调接口
热词里出现了“12306抢票算法”“python爬虫”这些词,很多人第一反应是直接抓包调HTTP接口。这条路理论上最快,但实际坑最多。12306的接口有复杂的加密参数、动态token、请求签名,而且风控策略一直在变。你今天逆向出来的接口,明天可能就失效了。对于非专业逆向的人来说,维护成本极高。
selenium的思路完全不同。它驱动一个真实的浏览器,模拟人的点击、输入、跳转。浏览器能做的事,selenium都能做,而且走的是和真人一模一样的请求链路。你不需要去逆向任何加密算法,只需要把“人怎么操作”翻译成“代码怎么操作”。这就是为什么大量抢票脚本都基于selenium——它不是最快的方案,但它是最容易理解和维护的方案。
当然,selenium也有代价:启动慢、资源占用高、执行速度不如直接调接口。但对于抢票这个场景,瓶颈往往在服务器响应和网络延迟,不在本地执行速度。所以selenium的这点性能损失,完全可以接受。
1.3 整体架构:查询、判断、提交三段式
一个抢票脚本的核心逻辑,拆开来看就三段:
- 查询段:定时刷新车次余票页面,解析出当前有哪些车次、哪些席别有票。
- 判断段:根据预设的优先级(比如“G1234二等座 > G5678一等座 > 其他”),决定要不要下单。
- 提交段:选中车次和席别,点击预订,填写乘客信息,提交订单。
这三段循环执行,直到抢到票或者到达设定的截止时间。听起来简单,但每一段都有大量细节要处理:登录态怎么保持、页面元素怎么定位、验证码怎么应对、订单提交后怎么确认。后面我会逐个拆解。
注意:本文所有内容仅用于技术学习和自动化原理探讨。实际使用中请遵守12306的用户协议,合理控制请求频率,不要对服务器造成压力。
2. 环境搭建与工具选型的关键细节
2.1 Python环境配置:别在版本上踩坑
热词里“python安装教程”“vscode配置python”“pycharm配置python环境”出现频率很高,说明很多人卡在环境这一步。我直接给结论:用Python 3.9到3.11之间的版本,不要用3.12+。原因很简单,selenium和一些依赖库对最新版Python的兼容性往往滞后,你可能会遇到莫名其妙的安装报错。3.10是我实测最稳的版本。
安装Python时有一个关键选项:一定要勾选“Add Python to PATH”。这个选项如果不勾,后面在命令行里敲python会提示找不到命令,很多新手就卡在这里。如果你已经装了但没勾,重新运行安装程序选“Modify”补上就行。
编辑器用VSCode还是PyCharm?都行。VSCode轻量,装个Python插件就能跑;PyCharm功能全,但启动慢。我个人抢票脚本这种小项目用VSCode就够了。关键是配好解释器路径,在VSCode里按Ctrl+Shift+P,输入“Python: Select Interpreter”,选中你装的那个Python版本。
2.2 selenium与WebDriver版本匹配:最容易翻车的地方
热词里有人问“有没有谷歌浏览器版本为138.0.7204.169的webdriver”,这个问题问到了痛点上。selenium驱动浏览器的核心是WebDriver,而WebDriver的版本必须和浏览器版本匹配。浏览器自动更新后,旧版WebDriver就失效了,脚本会报“session not created”之类的错误。
传统做法是去下载对应版本的WebDriver,手动放到PATH里。但现在更推荐用webdriver-manager这个库,它会自动检测你的浏览器版本,下载匹配的WebDriver。安装命令:
pip install webdriver-manager使用时代码这样写:
from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager service = Service(ChromeDriverManager().install()) driver = webdriver.Chrome(service=service)这样每次运行都会自动检查并下载匹配的驱动,省去了手动对版本的麻烦。实测下来很稳,强烈推荐。
2.3 依赖库清单与安装
除了selenium和webdriver-manager,还需要几个辅助库:
| 库名 | 用途 | 安装命令 |
|---|---|---|
| selenium | 浏览器自动化核心 | pip install selenium |
| webdriver-manager | 自动管理WebDriver | pip install webdriver-manager |
| requests | 辅助HTTP请求 | pip install requests |
| pillow | 处理验证码图片 | pip install pillow |
| ddddocr | 验证码识别(可选) | pip install ddddocr |
ddddocr是一个开源的验证码识别库,对12306的图形验证码有一定识别率,但不是100%。后面会详细讲验证码的处理策略。
提示:安装ddddocr时如果报错,可能是缺少编译依赖。Windows用户一般直接pip install就能装上,Linux用户可能需要先装libgl等系统库。
3. 核心功能实现:从登录到提交的完整链路
3.1 登录环节:扫码登录是最省事的方案
12306的登录方式有账号密码登录和扫码登录两种。账号密码登录需要处理滑块验证,难度较大。扫码登录是脚本最友好的方式,因为二维码本身就是给手机扫的,脚本只需要把二维码截图保存下来,你手动用手机扫一下就行。
实现逻辑是这样的:打开12306登录页,切换到扫码登录标签,找到二维码图片元素,截图保存到本地,然后轮询检查登录状态是否变为已登录。代码框架:
from selenium import webdriver from selenium.webdriver.common.by import By import time driver = webdriver.Chrome() driver.get("https://kyfw.12306.cn/otn/resources/login.html") time.sleep(2) # 切换到扫码登录 scan_tab = driver.find_element(By.XPATH, '//*[@id="toolbar_Div"]/div[2]/ul/li[2]/a') scan_tab.click() time.sleep(1) # 截图二维码 qr_code = driver.find_element(By.XPATH, '//*[@id="qrcode"]') qr_code.screenshot("qr_code.png") print("请扫描qr_code.png中的二维码登录") # 轮询登录状态 while True: if "login" not in driver.current_url: print("登录成功") break time.sleep(2)这段代码的关键点是:截图二维码后你要尽快扫,因为二维码有有效期。轮询判断登录成功的依据是URL变化,登录成功后页面会跳转。
3.2 车次查询:如何高效解析余票信息
登录之后进入车票查询页。你需要填写出发站、到达站、出发日期,然后点击查询。查询结果是一个表格,每行是一个车次,包含车次号、出发到达时间、各席别余票状态。
解析余票信息时要注意:12306的余票显示有几种状态——“有”、“无”、数字(表示剩余张数)、“候补”。脚本需要把这些状态统一处理。我的做法是定义一个函数,把席别状态映射成可比较的优先级:
def parse_seat_status(text): text = text.strip() if text == "有": return 999 # 有票,优先级最高 elif text.isdigit(): return int(text) elif text == "候补": return 1 # 候补优先级低 else: return 0 # 无票然后遍历查询结果表格的每一行,提取车次号和席别状态,存入一个列表。这个列表就是后续判断的依据。
注意:查询频率不要太高。我实测间隔3到5秒比较合理,太快了容易触发风控,页面会要求你重新登录甚至暂时限制访问。
3.3 下单提交:从点击预订到确认订单
当查询到符合预期的车次后,就要执行下单流程。这一步的步骤是:点击该车次的“预订”按钮,进入订单确认页,选择乘客,选择席别,点击“提交订单”。
这里有几个容易出问题的地方。第一,点击“预订”后可能弹出提示框(比如“您还有未完成的订单”),需要处理这些弹窗。第二,乘客选择需要提前在12306里添加好常用联系人,脚本直接勾选就行。第三,席别选择要和查询时看到的余票席别对应,不要选了一个没票的席别。
提交订单后,如果成功会跳转到支付页面。脚本到这里就可以停了,剩下的支付操作建议手动完成,因为涉及资金安全,不建议自动化。
# 点击预订按钮 book_btn = driver.find_element(By.XPATH, f'//tr[td[contains(text(), "{train_no}")]]//a[text()="预订"]') book_btn.click() time.sleep(1) # 选择乘客(假设第一个乘客) passenger_checkbox = driver.find_element(By.XPATH, '//*[@id="normal_passenger_id"]/li[1]/label') passenger_checkbox.click() # 提交订单 submit_btn = driver.find_element(By.XPATH, '//*[@id="submitOrder_id"]') submit_btn.click()3.4 循环监控:让脚本持续工作
单次查询下单不够,因为票是动态放出的。需要把查询和下单逻辑包在一个循环里,持续运行。循环的退出条件是:抢到票(订单提交成功)或者到达设定的截止时间。
import datetime deadline = datetime.datetime.now() + datetime.timedelta(minutes=30) while datetime.datetime.now() < deadline: try: check_and_book(driver) except Exception as e: print(f"出现异常:{e},继续监控") time.sleep(3)这个循环里加了异常捕获,因为页面元素可能因为各种原因找不到,不能让一个异常把整个脚本搞崩。捕获后打印日志继续跑,这是实战中必须的容错设计。
4. 验证码与风控应对:抢票脚本的最大挑战
4.1 验证码的几种类型与处理策略
12306的验证码经历过多次演变。早期是图形验证码(选文字、选图片),后来加了滑块验证,现在登录环节主要靠扫码绕过,但下单环节偶尔还会触发验证。
对于图形验证码,可以用ddddocr尝试自动识别:
import ddddocr ocr = ddddocr.DdddOcr() with open("captcha.png", "rb") as f: img_bytes = f.read() result = ocr.classification(img_bytes) print(f"识别结果:{result}")但说实话,识别率不是特别高,尤其是复杂背景的验证码。更稳妥的策略是:检测到验证码时,脚本暂停并发出提示,让人工介入处理。抢票本身就是一个需要人盯着的活,完全无人值守的方案在验证码这一关就很难走通。
4.2 风控机制:为什么你的脚本会被识别
热词里有人问“bypass分流抢票已经被12306识别了吗”,这说明大家都在担心风控问题。12306的风控主要看几个维度:请求频率、操作行为是否符合人类特征、浏览器指纹、IP地址等。
selenium有一个明显的特征:navigator.webdriver属性为true。网站可以通过JavaScript检测到这个属性,从而判断你用的是自动化工具。应对方法是在启动浏览器时注入一段脚本,把这个属性隐藏掉:
driver = webdriver.Chrome(service=service, options=options) driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", { "source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})" })另外,操作之间加入随机延时,模拟人的反应时间,也能降低被识别的概率。不要用固定的sleep间隔,用random.uniform(1, 3)这种随机值。
4.3 请求频率控制:快不等于好
很多人以为脚本刷新越快越好,其实不然。12306对高频请求有明确的限制,触发后轻则要求重新登录,重则暂时封禁访问。我的经验是查询间隔控制在3到5秒,下单操作间隔控制在1到2秒。这个频率已经比手动快很多了,而且不容易触发风控。
如果你同时监控多个车次,不要每个车次都单独发请求。更好的做法是一次查询拿到所有结果,然后在本地筛选。这样请求次数不变,但覆盖的车次范围大了很多。
提示:抢票高峰期(比如放票整点)服务器压力大,响应会变慢。这时候适当加大间隔,避免请求超时导致脚本异常。
5. 常见问题排查与实战避坑指南
5.1 元素定位失败:最常见的问题
selenium脚本报错最多的就是“NoSuchElementException”,意思是找不到页面元素。原因通常有几个:页面还没加载完、元素在iframe里、XPath写错了、页面结构变了。
解决办法:第一,在查找元素前加显式等待,用WebDriverWait配合expected_conditions,比死等sleep更可靠:
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC element = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.XPATH, '//*[@id="query_ticket"]')) )第二,如果元素在iframe里,需要先driver.switch_to.frame()切换进去。第三,XPath尽量用相对路径和属性定位,不要用绝对路径,因为页面结构一变绝对路径就失效了。
5.2 登录态丢失:怎么保持会话
脚本跑着跑着突然跳回登录页,这是登录态丢失了。原因可能是cookie过期、被风控踢下线、或者页面跳转导致会话中断。应对方法是:检测到当前URL包含login时,重新执行登录流程。另外,可以在登录成功后把cookie保存下来,下次启动时直接加载cookie,省去扫码步骤:
import pickle # 保存cookie pickle.dump(driver.get_cookies(), open("cookies.pkl", "wb")) # 加载cookie cookies = pickle.load(open("cookies.pkl", "rb")) for cookie in cookies: driver.add_cookie(cookie)但cookie也有有效期,过期了还是得重新扫码。
5.3 常见问题速查表
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| session not created | WebDriver版本不匹配 | 用webdriver-manager自动管理 |
| NoSuchElementException | 元素未加载/iframe/XPath错误 | 加显式等待,检查iframe,修正XPath |
| 脚本跑一会就跳登录页 | 登录态丢失/被风控 | 重新登录,降低请求频率 |
| 验证码识别不准 | 验证码复杂度过高 | 人工介入处理 |
| 提交订单提示“未完成订单” | 有未支付订单占用 | 手动取消旧订单 |
| 页面加载超时 | 网络慢/服务器压力大 | 加大等待时间,重试机制 |
5.4 几个实战中踩过的坑
第一个坑:不要用无头模式。无头模式虽然不显示浏览器界面,但更容易被风控识别。我实测有头模式(显示浏览器窗口)的成功率明显更高。
第二个坑:出发站和到达站的输入要用代码补全。12306的站点输入框有自动补全功能,你不能直接send_keys就完事,需要输入拼音首字母后等待下拉列表出现,再点击对应的站点。直接输入中文站名有时候不会触发补全。
第三个坑:日期选择要用日历控件。直接往日期输入框里填文本可能不生效,需要点击日历图标,然后选择对应日期。或者用JavaScript直接修改输入框的值。
第四个坑:订单提交后要确认结果。点击提交按钮不代表一定成功,可能弹出确认对话框,也可能提示“排队中”。需要检查页面反馈,确认订单是否真的提交成功。
6. 脚本优化方向与进阶思路
6.1 多线程与多车次并行监控
单线程脚本一次只能处理一个查询任务。如果想同时监控多个日期、多个车次组合,可以用多线程。但要注意,多线程不等于多请求,不要每个线程都去请求服务器。更好的架构是:一个线程负责查询,把结果放入队列;多个线程从队列取结果,判断是否需要下单。
import threading import queue task_queue = queue.Queue() def query_worker(): while True: # 执行查询,结果放入队列 results = query_tickets() task_queue.put(results) time.sleep(3) def book_worker(): while True: results = task_queue.get() # 判断并下单 check_and_book(results) threading.Thread(target=query_worker, daemon=True).start() threading.Thread(target=book_worker, daemon=True).start()这种生产者-消费者模式,查询和下单解耦,效率更高。
6.2 通知机制:抢到票怎么第一时间知道
脚本抢到票后,你需要立刻知道去支付。可以加一个通知功能,比如播放声音、发送邮件、或者调用一些通知服务的API。最简单的是用Python的winsound库在Windows上播放提示音:
import winsound winsound.Beep(1000, 2000) # 频率1000Hz,持续2秒复杂一点的可以用smtplib发邮件通知。这个看个人需求,核心是别抢到了票却因为没看到而错过支付时间。
6.3 候补策略:没票时的备选方案
现在12306的候补功能很成熟,很多时候候补比直接抢票成功率还高。脚本可以加一个逻辑:如果所有目标车次都无票,就自动提交候补订单。候补的提交入口和普通下单类似,选择候补席别后提交即可。候补的好处是不用一直盯着,系统会自动排队,有票了自动兑现。
6.4 代码结构与可维护性
最后说一点工程上的建议。抢票脚本因为要应对页面变化,维护频率比较高。建议把页面元素定位的XPath统一放在一个配置文件或常量区,页面改版时只需要改一处。另外,把登录、查询、下单、通知拆成独立的函数或类,逻辑清晰,调试也方便。
class TicketGrabber: def __init__(self): self.driver = None def login(self): pass def query(self, from_station, to_station, date): pass def book(self, train_no, seat_type): pass def notify(self, message): pass这种面向对象的写法,比一坨过程式代码好维护得多。后续想加功能,比如支持多乘客、支持指定席别优先级,扩展起来也容易。
说到底,抢票脚本是一个跟页面结构、风控策略持续博弈的项目。没有一劳永逸的代码,只有不断调试和更新的过程。我自己的体会是,把它当成一个学习selenium和自动化测试的练手项目,心态会好很多。真到了抢票的时候,脚本加手动双管齐下,成功率才是最高的。