ARTICLE DETAIL

资讯详情

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

用Playwright构建12306余票监控助手:登录态、自动查询与通知实践

用Playwright构建12306余票监控助手:登录态、自动查询与通知实践 1. 整体设计思路先想清楚工具边界1.1 自动化购票辅助工具到底解决什么问题每年一到春运和节假日朋友圈几乎都会变成“车票九宫格”大赛有人在晒抢到的票有人在吐槽验证码、排队提示和“无余票”的灰色按钮。作为一个平时就泡在自动化测试项目里的人我第一反应是想写个脚本把“盯余票”这种纯体力活交出去。折腾了大半年之后我得到的结论是这类工具并不能让你从“必然抢不到”变成“一定抢得到”它真正的价值是帮你从高频手动刷新中解放出来把有限的注意力留给真正需要判断的时刻。举例来说如果你在等一张周三晚上从北京回济南的高铁票通常不会一整天都盯着页面。更合理的做法是让脚本每隔十秒自动查一次一旦某个车次的二等座余票从“无”变成“有”就用钉钉或邮件通知你你再打开手机或者电脑决定是否下单。这中间省下来的是大量重复的点击、等待和焦虑而不是购票规则本身。当然我也不想把这个工具写成“暗黑抢票脚本”它不应该绕过验证码不应该对网站做高频攻击更不应该去破解任何接口。它只是一个帮你“盯着页面”的自动化助手最终下单和决策还是得由人来完成。如果你本身就是自动化测试工程师那这个项目更是一个难得的真实实训场景。它包含了登录态维护、动态元素等待、表格数据解析、验证码出现时的异常处理、失败重试等几乎所有UI自动化测试会遇到的问题。把12306当成测试对象远比拿一个永远不更新的Demo网站练习要有意思得多。1.2 为什么选 Playwright 而不是 Selenium这个项目最核心的自动化框架我最终选择了 Playwright而不是更老牌的 Selenium。说实话Selenium 在很长一段时间里都是Web自动化的默认选项资料多用的人也多但它有几个痛点第一浏览器驱动需要单独下载版本还容易和本机浏览器不匹配第二元素的自动等待需要自己处理写不好就会频繁报“找不到元素”第三登录态的保存和恢复虽然可以操作 Cookie但代码写起来比较啰嗦。Playwright 在这几个方面都舒服得多。安装的时候一条pip install playwright就能搞定再执行playwright install chromium会自动下载匹配的浏览器内核。它的 locator 自带智能等待页面元素没出现时它会自动等而不是一上来就报错。最让我喜欢的是storage_state机制登录完成后把整个浏览器的 Cookie 和本地存储保存到一个 JSON 文件下次直接从这个状态恢复代码只要三行。这对12306这种需要扫码登录的网站来说实在太友好了。下面是一个简单的框架对比可以直观看出区别对比维度SeleniumPlaywright驱动安装需要手动下载兼容驱动一行命令自动下载自动等待需自己写 WebDriverWaitLocator 内置智能等待登录态保存自己处理 Cookie容易漏storage_state 一键保存代码生成工具Selenium IDE 较老Playwright Codegen 很好用多浏览器支持支持但配置繁琐Chromium / Firefox / WebKit 开箱即用调试体验一般自带 Trace Viewer方便截图和录像如果你已经熟悉 Selenium这套思路完全可以平移到 Playwright核心逻辑不变只是 API 换了层皮。但如果你是新手直接上手 Playwright 更划算市面上的教程和社区资料也已经非常多遇到问题基本都能搜到解决方案。1.3 和学习自动化测试的关系很多人看到“12306自动化”会以为这只是一个购票偏门工具但实际上它背后是一整套自动化测试的通用能力。你可以把它理解成一个“真实世界的自动化测试项目”需要登录、需要处理反爬、需要解析动态渲染的页面、需要处理网络异常和弹窗还需要把结果记录下来。这些场景在普通测试 Demo 里很难同时遇到但在12306上全都撞齐了。我在开发这个工具时顺手用 pytest 搭了一套测试用例。比如一个用例专门验证登录态是否有效一个用例验证余票查询结果是否正常返回另一个用例验证收到通知消息后程序状态是否正确。用 pytest 的好处是可以跑出结构化结果还能配合 Allure 生成漂亮的报告。这样一来工具本身能买票代码又可以被包装成自动化测试项目简历上又多了一个拿得出手的实战案例。所以我不太建议一上来就写一个几百行全塞在一起的脚本而是像我下面这样分层组织后面维护起来会轻松很多。2. 核心细节解析登录、查票、下单的关键环节2.1 登录态维护扫码登录与Cookie持久化12306 目前的主流登录方式是扫码登录用户名密码登录也支持但扫码相对安全而且脚本里不需要保存任何敏感信息。这里我们先解决的问题是如何只登录一次后续每次运行脚本都在已登录状态。Playwright 提供了storage_state可以理解成“把浏览器当前状态拍一张快照”保存起来。首次运行时我会打开一个非 headless 的浏览器窗口让它停在12306首页人用手机扫码登录。等你登录完成后脚本把状态保存到state.json。后续再启动浏览器时直接加载这个状态文件就不再需要扫码了。# login.py from playwright.sync_api import sync_playwright def save_login_state(): with sync_playwright() as p: browser p.chromium.launch(headlessFalse) context browser.new_context( user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, localezh-CN ) page context.new_page() page.goto(https://www.12306.cn/index/) print(请在弹出的浏览器中完成扫码登录) input(登录完成后回到这里按回车键继续...) context.storage_state(pathstate.json) print(登录状态已保存到 state.json) browser.close() if __name__ __main__: save_login_state()这里有一点要注意input()会阻塞脚本直到你回车但浏览器窗口是可以操作的所以流程很顺畅。真正执行的时候永远不要使用headlessTrue来保存登录态因为扫码需要看到页面。如果你发现保存后的state.json文件非常小比如只有一两百字节那大概率是登录还没完成就回车了正常登录后的文件至少有几 KB。后续脚本里恢复登录态的代码更是简单context browser.new_context(storage_statestate.json) page context.new_page()我建议在脚本启动后做一个“是否真的已登录”的检查因为 Cookie 可能因为异地登录或过期而失效。比如打开12306首页后如果页面右上角显示“登录”而不是用户名就说明状态过期需要提示用户重新执行登录模块。这个检查可以在后面用 pytest 封装成一个用例。2.2 余票查询接口与数据解析登录态搞定之后接下来要面对的就是查票流程。12306 网页版查询余票有两种方式直接调用它内部的 JSON 接口或者用 Playwright 模拟用户手动点击查询再从渲染出来的表格里读取结果。我强烈推荐后者原因很简单接口的地址、参数和签名都有可能变化而且会被风控盯上而模拟真实页面操作虽然慢一点但行为和真人几乎一样稳定性高很多。页面操作的核心步骤包括输入出发地、输入目的地、选择日期然后点击“查询”。这里的难点是站点输入框有自动补全你不能简单地fill(北京)就完事还要等补全列表出现后选中真正的选项。Playwright 的 Locator 自带等待能力写起来还算顺手def select_station(page, input_selector, station_name): locator page.locator(input_selector) locator.click() locator.fill(station_name) page.wait_for_timeout(1000) # 自动补全列表里通常第一项就是精确匹配 page.locator(ftext{station_name}).first.click() page.wait_for_timeout(300)日期我推荐直接操作页面里的日期输入框用fill写入2025-01-28这种格式这是个老办法但一直能用。写完后点击一次页面空白处让日期控件确认输入。查询结果是一个表格结构大概是车次、出发站、到达站、出发时间、到达时间、各席别余票。用 Playwright 拿到整个表格的 tr 节点然后逐行解析。这里特别容易踩坑的是表头行和无数据的空行所以代码里要做两次过滤一是td数量不够的跳过二是车次列内容为空的跳过。余票列的值可能是“有”“无”或者具体数字我们只需要关注自己关心的席位。def parse_ticket_rows(page): rows page.locator(#ticket_list tr) result [] for i in range(rows.count()): row rows.nth(i) if row.locator(td).count() 10: continue train_no row.locator(td:nth-child(1)).inner_text().strip() if not train_no: continue seat_value row.locator(td:nth-last-child(1)).inner_text().strip() result.append({ train_no: train_no, seat: seat_value, }) return result需要提醒的是12306 的页面结构随时可能微调所以不要迷信这些选择器。更稳妥的办法是先手动打开浏览器的开发者工具确认当前的表格列顺序和元素 class再改脚本。Playwright 自带codegen工具可以录制一段操作后自动生成骨架代码这是前期写脚本的捷径。2.3 提交订单的坑车次、乘车人、席别选择查询到余票只是第一步真正容易出问题的是提交订单环节。在余票列表里找到目标车次后点击该行“预订”按钮页面会跳转到订单确认页。这时候我们需要选择乘车人和席别。先说乘车人。12306 一个账号下会保存多个乘车人每个乘车人右侧通常有一个复选框。自动化脚本可以通过 label 文本定位到对应的复选框例如page.locator(label:has-text(张三)).click()这里要注意乘车人必须已经在12306完成实名核验否则提交订单时会报“乘车人未通过资质核验”。这种错误不是脚本能解决的得提前去车站窗口或自助售票机办理。所以在工具运行前先手动登录12306把乘车人核验状态确认好。再说席别。页面通常是一个下拉框Playwright 用select_option处理page.locator(#seatType_1).select_option(label二等座)提交订单之前我建议脚本停在这里通过钉钉或微信通知用户来人工完成最后一步。因为一旦完全自动化很可能会出现选错了车次、选错了席别甚至误提交才后悔。一个真正负责任的辅助工具应该把人留在决策链上而不是把一切交给代码。2.4 验证码与风控应对人工介入不自动识别12306 的反自动化机制并不算特别激进但验证码是一定会遇到的。尤其是同一 IP 下面的请求频率高到一定阈值系统会强制弹出图片验证码。我从不建议写代码去自动识别验证码原因有三点第一自动识别准确率不高一旦识别错账号可能会被系统标记第二这会破坏购票公平性第三网上那些教你破解验证码的教程大概率会把你带进坑里。更合理的处理方式是在脚本里检测验证码是否出现一旦出现就通过通知工具告诉用户“需要人工操作”。人工处理完脚本继续往下走。if page.locator(#code_more_img).count() 0 or page.locator(.code-msg).count() 0: send_alert(检测到验证码请去浏览器完成验证) page.pause() # 人工完成后手动继续除了验证码还要控制查询频率。我的经验是每轮查询完成之后至少等 8 到 15 秒再进行下一轮并且加入随机延迟不要让请求节奏看起来像机器。下面是常用的随机等待写法import random, time time.sleep(random.uniform(8, 15))把这个节奏放在循环里既不会太频繁又比纯手动快得多。真正的“秒杀”场景里脚本不一定比人工手速有优势但持续监控的场景里它明显更可靠。3. 实操过程从环境搭建到首版脚本跑通3.1 环境准备在开始写代码之前先把运行环境准备好。我用的是 Python 3.10操作系统 Windows 和 Ubuntu 都跑过过程基本一致。主要是这几步python -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate pip install playwright playwright install chromium如果你所在网络下载 Playwright 浏览器内核比较慢可以用环境变量或者镜像配置加速但注意这里指的是正常的开发依赖下载不是什么旁门左道。装好后我们验证一下python -c from playwright.sync_api import sync_playwright; print(ok)能输出ok就说明环境没问题了。目录结构建议这样组织ticket_assistant/ ├── config.json ├── login.py ├── monitor.py ├── state.json └── requirements.txtrequirements.txt里至少包含playwright和requests前者是自动化框架后者用于发送通知。3.2 脚本编写登录模块、监控模块、提醒模块登录模块在上面的代码块里已经写了这里重点说监控模块。监控模块的逻辑可以拆成三步打开浏览器并恢复登录态、循环查询余票、发现目标车次后发送通知。下面是一个简化的监控循环示例目标是“发现某个车次二等座有票后通知用户”# monitor.py import json import random import time import requests from playwright.sync_api import sync_playwright config json.load(open(config.json, encodingutf-8)) def send_alert(message): webhook config.get(dingtalk_webhook) if webhook: requests.post(webhook, json{ msgtype: text, text: {content: message} }) print(message) def monitor(): 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/) page.wait_for_timeout(3000) while True: try: select_station(page, #fromStationText, config[from]) # 目的地、日期、查询按钮的处理同类型 page.click(#query_ticket) page.wait_for_selector(#ticket_list) page.wait_for_timeout(2000) rows page.locator(#ticket_list tr) for i in range(rows.count()): row rows.nth(i) if row.locator(td).count() 10: continue train_no row.locator(td:nth-child(1)).inner_text().strip() seat row.locator(td:nth-last-child(1)).inner_text().strip() if train_no config[target_train] and seat in (有, 1, 2, 3): send_alert(f目标车次 {train_no} 出现余票座位状态{seat}) break time.sleep(random.uniform(8, 15)) except Exception as e: send_alert(f查询出错{e}) time.sleep(30) if __name__ __main__: monitor()这里select_station函数可以单独提取避免两个输入框的处理逻辑重复。另外因为monitor()是无限循环在测试阶段建议先限定只跑5轮减少被风控盯上的可能。只有在确认稳定之后再放开跑长期任务。3.3 参数与配置如何设置出发地、目的地、日期硬编码“北京到上海1月28日”在脚本里并不方便最好把配置抽到一个 JSON 文件。下面是我现在用的config.json{ from: 北京, to: 上海, date: 2025-01-28, target_train: G5, prefer_seat: 二等座, dingtalk_webhook: https://oapi.dingtalk.com/robot/send?access_tokenxxx }from和to是站点名称脚本里通过自动补全选择不需要手动维护站码。target_train是用来匹配特定车次的如果你不关心具体车次只想看“有没有票”可以把这段判断去掉换成解析所有车次中任意一个有票就提示。prefer_seat是席别偏好但目前我示例里只是简单的取最后一列实际页面中二等座、一等座、商务座是多个不同列你可以根据页面结构选取第几列。配置dingtalk_webhook是可选功能如果你不想用钉钉也可以换成邮件通知或者干脆只打印日志。这个根据自己便利来。3.4 运行与日志使用pytest组织测试用例结果输出脚本跑通之后我建议把它工程化用 pytest 管理登录态验证和查询功能测试。这不只是为了显得专业更是为了让脚本后续修改时不容易改坏。一个非常实用的 pytest 用例是验证登录态是否有效# test_login.py import pytest from playwright.sync_api import sync_playwright pytest.fixture(scopemodule) def context(): with sync_playwright() as p: browser p.chromium.launch(headlessTrue) ctx browser.new_context(storage_statestate.json) yield ctx browser.close() def test_login_state(context): page context.new_page() page.goto(https://www.12306.cn/index/) page.wait_for_timeout(2000) # 如果右上角还显示“登录”文本说明登录态失败 assert 登录 not in page.locator(#login_user).inner_text()还可以写一个查询用例用测试日期查询一次断言页面上出现“车次”或“余票”等关键字。跑测试的时候执行pytest -v配合日志模块可以记录每次查询的日期、时间、车次和余票状态输出到logs/ticket.log。日志文件保留一定时间方便回头分析某个时间点是否出现过票。这在节前“蹲票”的场景下很有用能帮你了解放票规律。4. 常见问题与排查技巧实录4.1 登录失效怎么办我遇到最多的一个问题是隔几天再跑脚本时突然查询结果为空或者页面跳回了登录页。原因通常是 Cookie 过期、异地登录或者被系统判定为风险状态。排查时先检查state.json是否存在、大小是否正常然后跑一次登录检测用例如果失败就重新执行登录模块删除旧的state.json再生成新的。还有一个经验12306 会对长期不活跃的会话做强制过期处理所以如果脚本是定时任务最好在每天第一次查询前先访问一次首页再判断登录状态。不要等到需要提交订单时才发现登录失效那就太晚了。4.2 查询太频繁被限流如果你发现脚本跑了十几分钟之后页面上出现的验证码频率越来越高或者直接提示“访问过于频繁”说明你的访问节奏被风控注意到了。这不一定是因为你查询了100次可能只是某个瞬间频率波动太剧烈。解决方法是把查询间隔加大并且使用随机化。比如我常年在 10 到 20 秒之间随机休息高峰期手动跑时可以降到 5 秒但不要长期低于这个值。另一个技巧是设置单次运行时长上限比如在两小时内自动停止避免无人看管时脚本一直在跑。停止之后等一会儿再重启通常风控就会解除。4.3 下单失败原因分析与处理下单环节的问题更多是业务层面的脚本本身反而没那么容易出错。我把常见失败原因整理成一张表失败现象常见原因处理建议提示“该车次已无票”余票信息更新延迟或真无票继续监控不强行提交提示“乘车人未核验”乘车人没完成资质核验提前在12306或车站办理提示“排队人数过多”热门时段热门车次稍等几秒重试或启用官方候补提示“订单提交失败”网络波动或服务异常刷新页面重新选择车次下单选位后没反应席别下拉框没有正确选中检查元素选择器确认选中的文本我在实际使用中最稳妥的策略是脚本只负责把用户带到“提交订单”按钮附近然后停下来通知用户。人工确认车次、日期、乘车人、席别都正确之后再手动点击提交。这样虽然少了“全自动”的爽快感但能避免很多让自己后悔的误操作。4.4 避坑经验不要在高峰期大量请求合规使用官方候补这里想认真说一句市面上那些“抢票加速包”本质上很多都是通过高频请求帮你去刷票成功率并没有宣传那么夸张。与其依赖这些不确定性极强的操作不如直接在12306官方App里把候补订单下好。官方候补的优先级是实际存在的有票先保证候补队列脚本的作用更多是辅助监控和提醒。另外两个细节也要注意第一不要在脚本里明文保存账号密码我们全程使用扫码登录安全得多第二不要同时开几十个浏览器实例去“并发抢票”这不仅没用还会让你的账号被系统重点关注。稳妥、低频、持续这才是自动化辅助工具的正确姿势。5. 扩展把脚本升级成工程化自动化测试项目5.1 用pytestAllure生成报告既然这个项目已经用 pytest 组织了那就顺理成章接入 Allure让测试结果可视化。先在虚拟环境里安装pip install allure-pytest然后在项目根目录执行pytest --alluredir ./allure-results allure serve ./allure-resultsAllure 会把登录态检查、余票查询、通知发送这些用例的结果展示在一份网页报告里图表化展示通过率、失败日志、耗时等。这在小项目里可能显得重但如果你想拿这个项目去面试或者写进技术总结这就是一个很完整的展示案例。5.2 定时任务与监控通知如果不想每天手动启动脚本可以用系统定时任务。Windows 上是“任务计划程序”Linux 上可以用 crontab或者用 Python 的schedule库放到脚本内部。比如每天早上 8 点到晚上 10 点之间每 10 分钟查询一次一旦发现目标车次有票就立刻推送提醒。不过请注意定时任务长期挂在后台跑一定要控制每天的总次数。很多账号被限制不是因为技术问题而是因为脚本违反了平台规则。我的建议是只在你真正需要等票的时间段运行而不是 24 小时全年无休地刷。5.3 借鉴思路这套框架还能用在哪些场景把这套框架抽象一下你会发现它其实是一个通用的“页面监控 提醒”模板。我后来用它做过医院放号监控、博物馆预约提醒都只改了表单填写和结果解析部分核心逻辑没变。如果你再花点时间封装甚至能把登录态、查询循环、通知三个模块拆成可复用的类作为自己的自动化测试工具库。最后再分享一个小技巧吧。我在实际跑这个项目的过程中发现最影响体验的不是代码而是“心态”。当你把脚本调成每 15 秒自动查一次并且在手机上收到一次一次“暂无余票”的提醒时焦虑反而会减轻因为你终于不用再盯着那个灰色按钮。真正出票的那一刻脚本会准时告诉你。对我来说自动化工具的终极意义不是“保证买到”而是“把等待变得可控”。希望这篇教程也能让你的抢票季稍微轻松一点。
返回列表