ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

用Playwright构建12306购票监控助手:自动化查询与提醒实战

用Playwright构建12306购票监控助手:自动化查询与提醒实战 每年春运抢票那几天我基本是左手手机、右手电脑12306页面刷到怀疑人生。后来把做自动化测试的经验带进来用Playwright搭了一套自己的购票辅助工具查询、监控、提醒这些磨人的环节全交给程序我只负责在收到通知后再去操作。这篇文章就把我用自动化方式处理12306购票的全过程整理出来不推荐任何收费加速包也不碰来路不明的浏览器插件适合有点Python基础、愿意自己掌控完整流程的朋友参考。1. 为什么我把“抢票工具”做成了自动化脚本而不是装个插件1.1 每年开票的真实痛点先聊需求。12306的问题从来不是“网速不够快”而是状态变化完全不可预测。起售时间不是一个不同车站、不同车次各有各的时间点退票回流也不是均匀分布经常半夜冒出来一张票几分钟后又没了还有候补释放、区间放票、临时加开每一个变化都是一次“窗口期”。人在这种场景下有个天然劣势注意力会衰减。盯二十分钟屏幕还能保持敏感盯三个小时就开始麻木真出票了反而反应不过来。我可以熬夜守在第一波开售但没法做到连续十几个小时保持高专注度更扛不住同时盯好几个日期、好几趟车。所以自动化并不是为了“跟黄牛比手速”核心诉求很简单把高频、重复、容易看漏的操作交给程序让我只在需要做决定的时候介入。1.2 现成工具为什么让我不放心市面上各类“抢票助手”和“加速包”本质上只有两种实现路径。第一种是纯前端插件注入脚本模拟点击和刷新。这类工具看起来方便但它运行在一个不受你控制的第三方脚本里页面只要一改版就可能失效还随时可能被网站风控识别把账号搞出异常。第二种是服务器端抢票号称“云端并发请求”。你把12306账号授权给第三方它用高并发代替你去刷票。问题在于你的账号数据在别人服务器上出票逻辑也不透明你根本不知道它在背后发了多少请求、有没有用违规手段。真出了问题你能追的只有一个客服工单。我见过太多人为了抢一张票把账号授权给好几个平台最后票没抢到乘车人信息、常用联系人反而被串得乱七八糟。这些数据风险是实打实的我可不想交给一个“加速成功率”来背书。1.3 自动化测试思路带来的三个优势后来我换了个思路不把这件事当成“抢票”而是当成一个网页自动化测试任务。用做自动化测试的工程化方法去处理能拿到三个明显好处。第一过程可回溯。每一步操作都有日志、截图、视频回放出问题我能看到是选择器失效还是网络超时而不是黑盒一样对着“抢票失败”四个字发呆。第二频率可控。脚本完全跑在本地刷新间隔、并发数、重试策略都由我决定不会出现一觉醒来发现账号被风控的窘况。第三可持续维护。页面改版了我能快速定位并修复今年写的脚本明年还能继续用不像第三方工具那样说停更就停更。说到底抢票辅助的本质就是一个“真实浏览器环境下的UI自动化任务”恰好是我最熟悉的领域。2. 技术选型Playwright为什么比Selenium和纯requests更适合干这活2.1 三种方案的边界选型阶段我对比过三类方案requests接口直连、Selenium、Playwright。对比维度requests接口直连SeleniumPlaywright登录态维护要自己处理Cookie和签名复杂真实浏览器自然真实浏览器且支持storage_state持久化风控识别风险最容易暴露有一定风险相对低但仍需控制频率元素定位稳定性不涉及依赖等待策略常需手动sleep内置自动等待稳得多调试体验弱基本靠抓包一般截图麻烦trace回放、UI模式操作非常好用运行速度最快偏重中等偏快但足够用纯requests方案看起来很“轻”但它绕过了真实浏览器渲染12306页面上的数据大多依赖JavaScript动态加载你得先逆向接口参数、模拟加密逻辑工作量一下子就上来了。更麻烦的是这种非浏览器行为很容易被服务端识别为异常请求账号风险很高。Selenium是老牌工具能跑真实浏览器但配置WebDriver容易踩环境坑对等待条件的处理也不够优雅。我不反对用它只是Playwright同样能实现目标还更顺手。2.2 Playwright比Selenium顺手在哪Playwright有几个点非常契合这类场景。首先是自动等待。它定位元素时会自动等待元素出现、可见、可操作不需要像Selenium那样写一堆显式等待或者到处塞time.sleep。这一点在12306这种页面加载慢、列表异步渲染的站点上特别实用。其次是浏览器上下文隔离。每个context都是独立的会话我可以用一个context跑普通查询另一个context跑登录态保持互不干扰。然后是storage_state机制。登录之后把整个上下文状态保存成JSON文件下次启动直接用省掉了每次扫码登录的麻烦。这个能力在处理12306这种“登录一次能管很久”的站点时价值非常大。最后是trace回放。脚本跑挂了Playwright会记录完整的调用过程、DOM快照、网络请求和页面截图我用playwright show-trace命令打开报告一眼就能看出脚本到底死在哪一步。2.3 环境准备与最小项目结构安装没什么特别的Python 3.9以上然后执行pip install playwright pytest playwright install chromium建议只装Chromium内核够了不需要把所有浏览器都装一遍。项目结构我习惯这样安排ticket_assistant/ ├── config.py # 车次、日期、通知地址等配置 ├── login.py # 扫码登录并保存state.json ├── monitor.py # 核心查询与监控逻辑 ├── notify.py # 消息通知模块 ├── tests/test_monitor.py # pytest用例 └── logs/ # 日志和trace目录这个结构不复杂但能把“登录态”“查询逻辑”“通知通道”拆开后续哪一块出问题都不需要翻整个项目。3. 最小可用闭环登录态保持、余票查询与消息通知3.1 扫码登录后保存storage_state让脚本免扫码脚本的第一步是拿到一个“登录后的浏览器状态”。手动扫码一次把状态保存成本地文件后续所有自动化运行都复用这个状态不用每跑一次就重新扫码。from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch(headlessFalse) context browser.new_context() page context.new_page() page.goto(https://www.12306.cn/index/) input(扫码登录并完成人机验证后按回车...) context.storage_state(pathstate.json) browser.close()这里有一点必须提醒不要用headlessTrue做首次登录。12306的扫码页面和人机校验对无头浏览器的识别非常敏感一旦被判定为自动化流量可能直接弹滑块甚至锁账号。老老实实开一次有头浏览器登录完再关闭。后续脚本启动时直接加载这个状态文件context browser.new_context(storage_statestate.json)3.2 用页面定位器读取车次和余票信息登录态解决之后最关键的是从页面里取出“我想知道的信息”。12306的余票页面是动态渲染的用requests去拿HTML基本拿不到完整数据但Playwright可以等页面渲染完再读取。page.goto(https://www.12306.cn/index/) page.get_by_text(购票).click() page.locator(#fromStationText).fill(北京) page.locator(#toStationText).fill(上海) page.locator(#train_date).fill(2025-02-01) page.keyboard.press(Enter) page.wait_for_load_state(networkidle)这里的选择器是12306当前页面的真实id但我不建议你完全照抄因为页面改版太频繁。更稳妥的做法是打开浏览器F12确认当前版本用的到底是什么id或占位符再填进代码里。读取车次余票信息时用locator把所有行抓下来然后过滤出包含目标车次的那一行rows page.locator(tr.query_model) for row in rows.all(): text row.inner_text() if G1 in text: print(text)我会把解析逻辑封装成一个函数返回结构化结果方便后续做状态对比和监控判断。3.3 写一个不依赖短信网关的提醒通道查询到余票后怎么通知自己最简单的是桌面通知但如果你人不在电脑前最好接一个能推到手机上的通道。我用的最顺手的是钉钉群机器人。创建一个群添加自定义机器人拿到Webhook地址后直接往里面丢文本消息即可。import requests def send_dingtalk(text: str): webhook https://oapi.dingtalk.com/robot/send?access_tokenYOUR_TOKEN payload { msgtype: text, text: {content: text} } requests.post(webhook, jsonpayload)不依赖短信服务、不花钱、配置一次管一年非常适合个人自动化场景。如果用不惯钉钉换Server酱、邮件都可以核心逻辑就是“脚本发现变化 - 发一条消息给我”。3.4 第一个可运行脚本把上面三块整合起来就是最小可用闭环with sync_playwright() as p: browser p.chromium.launch(headlessTrue) context browser.new_context(storage_statestate.json) page context.new_page() page.goto(https://www.12306.cn/index/) # ... 跳转购票页、输入出发地、目的地、日期 ... rows page.locator(tr.query_model).all() found [r.inner_text() for r in rows if G1 in r.inner_text()] if found: send_dingtalk(发现可购车次 | .join(found)) browser.close()这个脚本跑通后你对整个流程的掌控感就完全不一样了。4. 场景扩展候补监控、抢票节奏与多任务巡检4.1 从“查一次”到“盯全程”候补监控循环最小闭环只解决了“主动查一次”实际场景更需要的是“持续盯”。我就设计了一个监控循环每隔一段时间刷新余票页面一旦发现目标车次状态发生变化就立刻通知。import time import random from datetime import datetime, timedelta deadline datetime.now() timedelta(hours2) while datetime.now() deadline: try: page.reload() page.wait_for_load_state(networkidle) rows page.locator(tr.query_model).all() current [r.inner_text() for r in rows if G1 in r.inner_text()] if current ! last_state: send_dingtalk(车次状态变化 | .join(current)) last_state current time.sleep(random.uniform(4, 7)) except Exception as e: # 哪怕报错也不要立刻把进程打死记下日志等下一轮 print(f[ERROR] {e}) time.sleep(10)循环里必须做两件事随机延时和异常兜底。随机延时让请求间隔不固定避免被风控识别成机械行为异常兜底保证某次页面加载失败时不会让整个程序退出。4.2 开售瞬间的抢票节奏控制候补监控解决的是“捡漏”开售首波还得靠主动刷新。我的做法是在开售时间前3分钟启动脚本提前完成页面跳转、输入出发地目的地、选中目标车次。真正的“抢”只发生在开售那一刻——脚本以较高的频率刷新一旦出现余票立即锁定并通知。这里要注意“较高频率”不是无限快。实测下来单账号每3秒刷新一次已经是比较激进的状态再快就容易触发校验页。与其把频率拉满不如让脚本更聪明一次刷新能看到的数据量其实不少重点是判断准确、通知及时。4.3 用pytest参数化同时盯多个车次、多个日期一个人盯几个车次时靠手写循环也可以但一旦要同时监控好几条线路、好几个日期就该上pytest了。用pytest做参数化每条数据都是一条独立的测试用例失败隔离、日志清晰还能用插件做失败重试import pytest from ticket_assistant.monitor import monitor_trip pytest.mark.parametrize(trip, [ (北京南, 上海虹桥, 2025-02-01), (北京南, 南京南, 2025-02-01), (北京南, 杭州东, 2025-02-02), ]) def test_monitor_trip(trip): monitor_trip(*trip)pytest另一重价值是报告所有用例跑完哪些线路状态正常、哪些线路发现过余票自动生成汇总报告不用在控制台里翻日志。4.4 自动填单的边界把“提交”按钮留给人工逻辑上脚本完全能继续往下走自动选择乘车人、自动提交订单。我试过一次后反而把这一层从自动化链路里摘掉了。原因有三。第一订单提交涉及真实资金和出行安排一旦选错人、选错日期纠错成本极高第二提交订单后有支付环节如果自动化程度太高人反而会失去对每一笔操作的感知第三于合规而言自动化购票工具更适合定位为“信息辅助”而不是“全自动代购”。所以我最后的做法是脚本自动完成到“锁定余票、唤起通知、自动勾选乘车人”为止提交订单那一帧永远留给人工确认。这既是安全底线也是给自己留一个冷静的决策点。5. 踩坑实录风控、滑块验证与页面结构变更的排查全流程5.1 登录风控连续扫码触发滑块第一次跑脚本时我为了测试不同登录方式连续在几个context里发起扫码结果没一会儿页面直接弹出了滑块验证之后连续几次登录都要求二次验证。排查下来问题出在“同一IP短时间内频繁请求登录二维码接口”。12306对登录接口的风控阈值并不高连续两三次请求就可能触发高频警告。解决方案很粗暴把重试间隔拉长至少间隔1分钟以上并且尽量使用同一个稳定的网络出口。如果已经触发了滑块别硬刚手动过掉一次让账号恢复正常状态然后再继续。5.2 高频请求触发“访问人数过多”与退避策略监控循环跑久了偶尔会遇到页面返回一个“当前访问人数过多”的提示车次列表完全刷不出来。第一次遇到时我以为是脚本写崩了查了半天发现是请求频率踩线。处理方式是加入指数退避逻辑retry_count 0 while True: page.reload() if 访问人数过多 in page.inner_text(body): retry_count 1 wait_time min(60, 2 ** retry_count random.random()) time.sleep(wait_time) continue # 正常解析逻辑这个模式在很多限流场景下都通用遇到风控提示不重试三次就放弃而是逐步拉长间隔给服务端一个“我在退让”的信号。5.3 页面改版导致选择器失效trace和截图帮我定位12306的改版频率不算低某次运行脚本时车次列表突然解析不到数据。我没有直接去猜选择器而是用Playwright的trace功能记录了完整运行过程context.tracing.start(screenshotsTrue, snapshotsTrue) # 执行查询逻辑 context.tracing.stop(pathtrace.zip)然后通过命令回放playwright show-trace trace.zip在回放界面里能看到每一步操作对应的DOM快照很快发现原来是车次行的class从query_model变成了query_model new列表结构没变但class变了。修正选择器后问题解决。这种排查链路比“盲猜选择器”高效得多也是我把这套流程称为“自动化测试项目”而不是“抢票脚本”的原因——它用工程手段处理不确定性。5.4 登录态过期后的优雅降级storage_state不是永久有效的。12306的登录态可能几个小时就失效也可能撑几天完全取决于服务端策略。最初我没做这个判断脚本从早跑到晚突然某个时刻所有查询都返回登录页面。后来在解析前加了一步if /login in page.url: send_dingtalk(登录态已失效请重新扫码更新state.json) break一旦检测到跳转登录页立即停止脚本并通知我而不是继续徒劳刷新。重新登录一次、覆盖state.json、再启动整个过程两分钟。这个判断虽然简单却避免了很多无效运行时间。6. 合规底线与正确使用姿势6.1 自动化能做什么不能做什么聊到这里必须把边界说清楚。能做的查询余票、监控状态变化、发送提醒、在浏览器内模拟普通用户的正常操作流程。这些不涉及对服务端正常逻辑的破坏本质上是“用自动化工具帮人看得更勤快”。不能碰的绕过或破解人机验证、伪造加密参数直接调接口、用脚本恶意高频请求干扰服务、批量注册账号、任何形式的商业代抢。这些行为不仅违反用户协议也可能触犯相关法律法规风险完全不可控。我始终把这段代码定义成“购票辅助工具”而不是“抢票外挂”就是在划定这个边界。6.2 频率、礼貌与账号安全自动化操作的最起码礼貌是别给服务端制造异常压力。个人使用场景下我建议单账号刷新间隔不低于3秒。同时监控的并发任务控制在个位数。不要自作聪明地开几十个浏览器上下文去并行抢票。避免把登录态文件提交到公开代码仓库文件里包含完整的Cookie信息。优先用自己的主账号做低频率查询高频率测试用不重要的账号去跑。这几点同时也是保护账号安全的手段。被风控锁账号的损失远比“晚了几秒刷票”大得多。6.3 保持“人做决策”的铁律自动化帮我把“盯屏幕”的重复劳动扛了下来这是它最大的价值。但在支付确认、退改签、异常订单处理这些环节我始终保留人工操作。原因很朴素工具能做的是发现机会最终决定权一定要留在有判断能力的人手里。票价不对、日期看错、乘车人选错任何一个失误都可能造成实际损失而这些失误恰恰是高度自动化流程里最难预设的。把自动化当成“另一双眼睛”而不是“另一只手”是我折腾完整套方案后最想强调的一点。最后再分享一个实用技巧这套思路并不局限于12306。只要你面对的是一个网页版业务系统凡是包含重复查询、状态监控、手动刷新这类环节的都可以用同样的方式去改造。我后来把同一个Playwright模板用在了预约挂号、抢公开课名额、库存监控等场景只需要改改选择器和通知逻辑整个框架完全可以复用。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表