ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Selenium的12306抢票脚本实战:从环境搭建到风控应对

基于Selenium的12306抢票脚本实战:从环境搭建到风控应对 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里按CtrlShiftP输入“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(serviceservice)这样每次运行都会自动检查并下载匹配的驱动省去了手动对版本的麻烦。实测下来很稳强烈推荐。2.3 依赖库清单与安装除了selenium和webdriver-manager还需要几个辅助库库名用途安装命令selenium浏览器自动化核心pip install seleniumwebdriver-manager自动管理WebDriverpip install webdriver-managerrequests辅助HTTP请求pip install requestspillow处理验证码图片pip install pillowddddocr验证码识别可选pip install ddddocrddddocr是一个开源的验证码识别库对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, //*[idtoolbar_Div]/div[2]/ul/li[2]/a) scan_tab.click() time.sleep(1) # 截图二维码 qr_code driver.find_element(By.XPATH, //*[idqrcode]) 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, //*[idnormal_passenger_id]/li[1]/label) passenger_checkbox.click() # 提交订单 submit_btn driver.find_element(By.XPATH, //*[idsubmitOrder_id]) submit_btn.click()3.4 循环监控让脚本持续工作单次查询下单不够因为票是动态放出的。需要把查询和下单逻辑包在一个循环里持续运行。循环的退出条件是抢到票订单提交成功或者到达设定的截止时间。import datetime deadline datetime.datetime.now() datetime.timedelta(minutes30) 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(serviceservice, optionsoptions) 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, //*[idquery_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 createdWebDriver版本不匹配用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(targetquery_worker, daemonTrue).start() threading.Thread(targetbook_worker, daemonTrue).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和自动化测试的练手项目心态会好很多。真到了抢票的时候脚本加手动双管齐下成功率才是最高的。
返回列表