☰
Python+Selenium抢票脚本实战:环境搭建、登录复用与订单提交全解析
2026/9/25 5:04:28 网站建设 项目流程

1. 抢票脚本的真实定位与核心思路拆解

先把话说在前头:任何声称“100%成功”的抢票脚本,本质上都是在贩卖焦虑。12306的票池是动态的,能不能抢到票,取决于你的网络延迟、提交时机、候补队列位置,以及当天放票的批次策略,脚本能做的只是把“人工刷新-点击-提交”这个循环压缩到毫秒级,减少手速和注意力带来的损耗。我自己用Python配合Selenium写过几版抢票工具,也帮朋友排查过不少跑不起来的脚本,踩过的坑比抢到的票多得多。这篇文章就把整套思路、代码骨架、参数调优和避坑经验完整拆开讲,适合已经装好Python、懂一点基础语法、想自己动手折腾的读者,也适合纯粹想了解自动化测试框架怎么落地到实际场景的人。

1.1 为什么选Selenium而不是纯requests

很多人第一反应是用requests直接怼接口,觉得那样更快。理论上没错,但12306的接口有复杂的加密参数、动态token和风控校验,纯requests方案需要逆向JS加密逻辑,维护成本极高,一旦前端改版就得重新逆向。Selenium走的是浏览器真实渲染路线,你看到什么它就操作什么,稳定性反而更好。代价是速度慢一些,但配合合理的等待策略和元素定位优化,完全够用。

提示:Selenium本质是自动化测试工具,用它做抢票属于“非典型用法”,请控制请求频率,避免对公共资源造成压力。

1.2 整体架构分层

我把脚本分成四层:配置层负责账号、车次、日期、乘客信息;驱动层负责启动浏览器、加载WebDriver;操作层负责登录、查询、提交订单;监控层负责日志记录和异常重试。分层的好处是改一个参数不用动核心逻辑,比如换车次只改配置,换浏览器只改驱动层。

1.3 核心难点预判

登录环节有滑块验证,查询环节有频率限制,提交环节有排队机制。这三个点是脚本能否跑通的关键。我的策略是:登录尽量走扫码或已保存的Cookie,查询用固定间隔加随机抖动,提交阶段用显式等待锁定按钮可点击状态。下面会逐一展开。

2. 环境搭建与WebDriver版本匹配实操

环境问题是新手卡住的第一道坎,尤其是WebDriver和浏览器版本对不上,报错信息还特别模糊。我见过太多人在这里耗掉一整天。

2.1 Python环境与依赖安装

假设你已经装好Python 3.8以上版本,打开终端执行:

pip install selenium pip install requests pip install loguru

loguru是我个人习惯用的日志库,比原生logging省事,输出带时间戳和颜色,排查问题时一眼就能看到哪一步挂了。如果你不想多装依赖,用print也行,但正式跑的时候日志文件更靠谱。

2.2 ChromeDriver版本匹配的坑

热词里有人问“有没有谷歌浏览器版本为138.0.7204.169的webdriver”,这个问题非常典型。ChromeDriver必须和Chrome主版本号一致,138的浏览器就得配138的驱动。查看浏览器版本的方法:地址栏输入chrome://version,第一行就是完整版本号。

下载地址是Chrome for Testing的官方页面,选对应平台的压缩包,解压后把可执行文件放到Python脚本同目录,或者加入系统PATH。我习惯放同目录,避免污染全局环境。

from selenium import webdriver from selenium.webdriver.chrome.service import Service service = Service(executable_path="./chromedriver") options = webdriver.ChromeOptions() driver = webdriver.Chrome(service=service, options=options)

如果你用的是Selenium 4.6以上版本,其实可以省略手动下载驱动,它会自动管理。但自动管理有时会抽风,尤其是网络不好的时候,所以我还是推荐手动指定路径,心里有底。

2.3 浏览器选项调优

默认启动的浏览器窗口小、加载慢,加几个参数能明显改善:

options.add_argument("--disable-blink-features=AutomationControlled") options.add_argument("--start-maximized") options.add_argument("--disable-infobars") options.add_experimental_option("excludeSwitches", ["enable-automation"]) options.add_experimental_option("useAutomationExtension", False)

disable-blink-features这个参数是为了去掉部分自动化特征,让页面行为更接近正常浏览。注意,这不是用来绕过什么限制,只是减少不必要的弹窗干扰。

2.4 验证环境是否就绪

写个最小测试脚本,打开12306首页并打印标题:

driver.get("https://www.12306.cn") print(driver.title) driver.quit()

能正常打印出标题就说明环境通了。如果报SessionNotCreatedException,九成是驱动版本不匹配;如果报连接超时,检查网络和代理设置。

3. 登录环节的自动化处理与Cookie复用

登录是抢票脚本的第一道门槛,也是最容易失败的地方。我的建议是:能扫码就扫码,能复用Cookie就复用,不要硬刚账号密码加滑块。

3.1 扫码登录的自动化思路

12306支持扫码登录,二维码在页面上是一个img元素。脚本可以截取二维码区域保存为图片,然后人工用手机扫。听起来不够“自动”,但实际最稳。因为滑块验证的通过率受网络和风控影响很大,扫码一次能管很久。

driver.get("https://kyfw.12306.cn/otn/resources/login.html") # 等待二维码加载 qr_code = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CLASS_NAME, "qrcode-img")) ) qr_code.screenshot("qr.png") print("请扫描qr.png完成登录")

截图后手动扫码,登录成功后脚本继续执行。这个方案的好处是绕开了密码输入和滑块,稳定性极高。

3.2 Cookie持久化复用

登录成功后,把Cookie保存到本地文件,下次直接加载,省去扫码步骤:

import pickle # 保存 pickle.dump(driver.get_cookies(), open("cookies.pkl", "wb")) # 加载 cookies = pickle.load(open("cookies.pkl", "rb")) for cookie in cookies: driver.add_cookie(cookie) driver.refresh()

注意:Cookie有有效期,一般几天到几周不等。如果加载后跳回登录页,说明失效了,重新扫码即可。

3.3 登录状态检测

加载Cookie后要验证是否真的登录成功,不能盲目往下跑。检测方法很简单:访问个人中心页面,看是否跳转到登录页。

driver.get("https://kyfw.12306.cn/otn/view/index.html") if "login" in driver.current_url: print("Cookie失效,需要重新登录") else: print("登录状态正常")

这个检测步骤一定要加,否则后面查询和提交全是白费功夫。

3.4 常见登录异常处理

有时候页面加载慢,元素还没出来就去找,直接抛NoSuchElementException。解决办法是统一用WebDriverWait加显式等待,超时时间设10到15秒。另外,如果频繁登录失败,可能是触发了风控,建议停一段时间再试,不要死循环重试。

4. 车次查询与提交订单的核心逻辑

登录搞定后,重头戏是查询和提交。这部分逻辑不复杂,但细节决定成败。

4.1 查询参数构造

12306的查询页面URL带参数,直接拼接比在页面上点选更可靠:

query_url = ( "https://kyfw.12306.cn/otn/leftTicket/init" "?linktypeid=dc" f"&fs={from_station}" f"&ts={to_station}" f"&date={travel_date}" "&flag=N,N,Y" ) driver.get(query_url)

车站代码要用三字码,比如北京是BJP,上海是SHH。这个代码表网上能查到,建议提前存成字典。

4.2 车次信息解析

页面加载后,车次列表在table里。用XPath定位每一行,提取车次号、出发时间、余票状态:

rows = driver.find_elements(By.XPATH, "//tr[contains(@id, 'ticket_')]") for row in rows: train_no = row.find_element(By.CLASS_NAME, "number").text if train_no == target_train: # 找到目标车次 book_btn = row.find_element(By.CLASS_NAME, "btn72") if book_btn.is_enabled(): book_btn.click() break

btn72是预订按钮的类名,有票时是可点击状态,无票时是灰色。判断is_enabled()能避免点到无效按钮。

4.3 提交订单的等待策略

点击预订后进入确认页面,需要选择乘客、席别,然后提交。这里的关键是显式等待按钮可点击,而不是固定sleep:

submit_btn = WebDriverWait(driver, 5).until( EC.element_to_be_clickable((By.ID, "submitOrder_id")) ) submit_btn.click()

固定sleep的问题是:设短了元素没出来,设长了浪费时间。显式等待是元素一出现就继续,效率最高。

4.4 循环重试与频率控制

抢票本质是循环查询,直到有票为止。但循环不能太猛,否则会被限流。我的做法是每次查询间隔1.5到2.5秒随机,模拟人工操作节奏:

import random, time while True: # 查询逻辑 if ticket_found: break time.sleep(random.uniform(1.5, 2.5))

提示:间隔时间不要低于1秒,否则容易触发风控,反而得不偿失。

4.5 候补与直达的取舍

如果直达票一直抢不到,可以同步挂候补。候补是官方机制,成功率取决于排队位置,脚本能做的是尽早提交候补订单。我的经验是:放票瞬间先抢直达,抢不到立刻转候补,两条线并行。

5. 常见报错与排查速查表

跑脚本的过程中会遇到各种报错,这里整理一份速查表,方便对照排查。

报错信息可能原因解决办法
SessionNotCreatedException驱动版本不匹配下载对应版本ChromeDriver
NoSuchElementException元素未加载或定位错误加显式等待,检查XPath
ElementClickInterceptedException元素被遮挡滚动到元素位置再点击
TimeoutException等待超时增加超时时间或检查网络
StaleElementReferenceException页面刷新导致元素失效重新定位元素
登录后跳回登录页Cookie失效重新扫码登录

5.1 元素定位失效的应对

12306前端偶尔改版,类名或ID会变。应对方法是把定位器集中写在配置里,改版时只改一处。另外,优先用相对稳定的属性,比如id比class稳定,name比xpath稳定。

5.2 点击无效的排查

有时候按钮明明找到了,click却没反应。常见原因是元素不在可视区域,或者被弹窗遮挡。解决办法是先scrollIntoView,再检查有没有弹窗需要关闭。

driver.execute_script("arguments[0].scrollIntoView();", button) time.sleep(0.5) button.click()

5.3 脚本跑着跑着卡死

多半是某个等待没有超时限制,或者页面弹出了未处理的对话框。给所有等待加timeout参数,并定期检查driver.window_handles,处理意外弹出的窗口。

6. 实操心得与避坑经验

这部分是我自己踩坑攒下来的,文档里不会写,但实际跑的时候特别有用。

6.1 不要迷信“100%成功”

任何脚本都受制于票池和网络,放票瞬间几万人同时提交,脚本的优势只是快那么几十毫秒。心态要放平,脚本是辅助,不是保证。

6.2 提前测试完整流程

不要等到放票当天才第一次跑脚本。提前用非高峰时段的车次走一遍完整流程,确认登录、查询、提交、支付每个环节都通。我见过太多人放票时才发现某个按钮定位错了,白白错过。

6.3 日志要详细

每个关键步骤都打日志,包括时间戳、当前URL、操作结果。出问题时看日志能快速定位是哪一步挂了。loguru的logger.info和logger.error配合使用,排查效率翻倍。

6.4 支付环节别自动化

提交订单后,支付环节建议手动完成。一方面支付涉及资金安全,另一方面支付页面有额外的验证,自动化容易出问题。脚本跑到“订单提交成功”就可以停了。

6.5 尊重公共资源

脚本查询频率要克制,不要开几十个线程同时怼。12306是公共服务,合理使用是底线。我一般单线程跑,间隔2秒左右,既不影响别人,自己也能抢到。

6.6 备选方案要准备

脚本不是万能的,直达抢不到就候补,候补不行就换中转,中转不行就换日期。多准备几套方案,比死磕一个车次强。

7. 后续可扩展的方向

这套脚本骨架跑通后,可以往几个方向扩展。一是加GUI界面,用tkinter或PyQt做个简单的配置窗口,不用改代码就能换车次。二是加通知功能,抢到票后发邮件或推送提醒。三是把配置抽成JSON文件,方便管理多个车次和乘客。四是研究官方的候补机制,把候补提交也纳入自动化流程。这些扩展都不难,核心逻辑不变,只是外围包装。

我个人在实际操作中的体会是:脚本的价值不在于“抢到票”这个结果,而在于把重复劳动交给机器,让自己从刷新页面的焦虑中解放出来。把精力放在方案规划和备选准备上,比盯着屏幕刷票有用得多。最后再分享一个小技巧:放票前5分钟启动脚本,提前登录好,放票瞬间脚本的响应速度比手动快一个数量级,这个时间差往往就是成败的关键。

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

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

立即咨询