ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java抢票助手源码拆解:自动登录、查询与订单提交实战

Java抢票助手源码拆解:自动登录、查询与订单提交实战

简介:这份资源面向熟悉 Java、希望在春运等购票高峰自动抢购 12306 火车票的程序员,提供一套可运行的自动购票程序及配套说明。程序支持自动登录与重试、自动查询刷新、指定车次与动车/卧铺优先、代理配置及验证码识别,并兼容 Firefox+Scriptish 与 Chrome 环境,帮助无法时刻盯票的用户提升购票成功率。压缩包共 1 个 doc 文件,约 467KB,以图文教程形式记录程序功能、配置项与使用流程,便于对照操作。目前已有 704 人学习浏览。读者可从中了解自动填单、重试机制、config.properties 参数设置与验证码处理思路,也能借鉴其网页解析与自动化实现方法,适合作为 Java 网络爬虫与自动化实践的学习案例。

1. 抢票这件事,程序员有自己的解法

春运放票那天,我盯着屏幕刷了四十分钟,验证码换了二十多张,最后连站票都没了。旁边工位的同事却慢悠悠地喝着茶,说他写了个 Java 小程序,挂着自动刷,抢到票才弹窗提醒他。这就是我拆这个 GOHOME 抢票助手的起因——它不是什么高深的东西,但把「自动登录、自动重试、自动刷新查询、自动提交订单」这条链路串起来了,用 Java 写,双击start.bat就能跑。适合谁?适合想搞明白自动化购票流程怎么落地的人,也适合拿它当 HttpUnit、HTML 解析、验证码识别入门案例的 Java 开发者。下面我按「它是什么 → 怎么跑起来 → 核心代码怎么写的 → 怎么改」的顺序,把这份源码包拆开讲。

2. 项目定位与技术栈:一个 Java 桌面端的网页自动化工具

2.1 它到底在做什么

这个程序本质是一个「带界面的网页机器人」。它不调用任何官方接口,而是模拟浏览器行为:打开登录页、填表单、提交、解析返回的 HTML、判断有没有票、有票就提交订单。整个流程跑在一个 Java 桌面窗口里,用户填好出发站、到达站、日期、车次偏好,点开始,程序就循环执行「查询 → 判断 → 提交」直到成功或用户手动停。

从源码包的结构看,它依赖三块东西:一是 HTTP 客户端,用来发请求、维持会话(Cookie);二是 HTML 解析器,用来从返回页面里抠出车次、余票、验证码图片地址;三是验证码识别模块,把图片转成字符。原文提到作者用htmlparse爬网页、用颜色像素思路识别验证码,这和我们常见的「HttpClient + Jsoup + 简单图像处理」组合是对得上的。

2.2 技术栈拆解

层次选型作用
运行环境JRE 1.6+程序是 Java 编写,start.bat里调java -jar
网络层HttpUnit / 原生 HttpURLConnection发 GET/POST、维持 Cookie 会话
解析层htmlparse(HTML 解析库)从页面提取车次、余票、表单隐藏字段
图像层Java AWT / BufferedImage读取验证码图片像素做识别
配置层config.properties代理、指定车次、优先级开关
界面层Swing输入框、按钮、日志区、弹窗提示

这里有个血泪经验:很多人以为抢票程序是「直接调接口」,其实早期这类工具全是「模拟浏览器点按钮」,因为购票流程里有一堆隐藏字段(比如token、leftTicketStr)必须从上一个页面带过来,漏一个就提交失败。所以它的核心不是算法,是「把浏览器该做的事一件不落地重放一遍」。

2.3 目录与文件构成

解压后大致是这么几块,我按功能归类:

GOHOME/ ├── start.bat # 启动脚本,检查并调用 java -jar ├── config.properties # 代理、指定车次等配置 ├── 使用说明.txt # 原始操作指引 ├── lib/ # 依赖 jar(http 客户端、html 解析等) └── GOHOME.jar # 主程序,含界面与业务逻辑

start.bat的内容很朴素,常见写法是:

@echo off rem 检查 java 是否可用,不可用则提示安装 JRE java -version >nul 2>&1 if errorlevel 1 ( echo 未检测到 Java 运行环境,请先安装 JRE 1.6 以上版本 pause exit /b ) rem 启动主程序 java -jar GOHOME.jar pause

这段脚本干两件事:先用java -version探一下环境,errorlevel 1说明没装或没配 PATH,直接提示;否则java -jar拉起主类。参数上没什么可调的,真正要改的是config.properties。注意pause别删,程序异常退出时窗口不会一闪而过,方便看报错——我第一次跑就是靠这行看到「找不到主类」的。

3. 跑起来:从 JRE 环境到第一张票的完整链路

3.1 环境准备与启动

第一步永远是环境。程序要求 JRE 大于 1.6,原文建议直接装 JRE 6 Update 30。现在机器上多半有更高版本,java -version能出版本号就行。装完在命令行敲一下确认:

java -version # 期望输出类似:java version "1.8.0_xxx"

然后双击start.bat。界面初始化完,你会看到出发站、到达站、日期、车次、是否自动识别验证码、动车优先、卧铺优先这些输入项。填完点开始,程序进入循环。这里有个细节:原文说「除登录信息外,其他信息均可及时修改并即时生效」,意思是循环每一轮都会重新读界面上的值,所以你中途改车次不用重启。

3.2 配置文件怎么改

config.properties是纯文本,记事本打开就能编辑。代理和指定车次是两块重点:

# 代理服务器配置,需要走代理时把 userproxy 改为 true userproxy=false ip=127.0.0.1 port=8080 # 指定车次,如 traincode=D520 # 只是优先预定,该车次无票时程序会自动选其他车次 traincode= # 是否优先动车、优先硬卧 preferD=true preferSleeper=true

参数说明:userproxy是总开关,改成true后ip和port才生效;traincode留空表示不指定,填了就是「优先」而非「只买」——这点原文特意强调过,因为很多人以为填了车次就只抢这一趟,结果没票干等。preferD、preferSleeper对应界面上的动车优先、卧铺优先,逻辑是排序时把符合条件的车次排前面,不是过滤掉其他车次。

3.3 一次完整抢票的时序

把流程按时间线捋一遍,方便你对照日志排查:

  1. 启动后读取config.properties,初始化 HTTP 客户端和 Cookie 容器。
  2. 打开登录页,若开启「记住密码」,从本地存储读出用户名密码自动填入,光标定位到验证码框。
  3. 验证码环节:自动识别模式下调图像模块识别,失败则重试;手动模式弹框等你输入。
  4. 提交登录,若返回「人数过多/网络繁忙」,按重试策略等待后重发,直到成功。
  5. 登录成功后进入查询循环:按出发站、到达站、日期拼查询请求,解析返回的车次列表。
  6. 对每个车次判断余票,命中偏好(指定车次/动车/卧铺)则进入提交订单流程。
  7. 提交订单需要带上页面里的隐藏字段,成功后弹窗+声音提示,剩下的支付要人工去订单页完成。

提示:原文说支付窗口只有 45 分钟,程序抢到票只是「占位」,真正付钱还得你手动登录网页操作,所以抢到后别关程序,赶紧去支付。

4. 核心模块拆解:登录、查询、提交三段代码

4.1 会话维持与自动登录

抢票程序最怕的是「登录成功但下一个请求掉线」,根因是 Cookie 没带全。常见做法是全局维护一个 Cookie 容器,每次请求前把已有 Cookie 塞进请求头,响应里的Set-Cookie再回写。伪代码结构如下:

// 全局会话容器,保存登录后的 Cookie private Map<String, String> cookieStore = new HashMap<>(); // 发请求前注入 Cookie private void attachCookies(HttpRequest request) { StringBuilder sb = new StringBuilder(); for (Map.Entry<String, String> e : cookieStore.entrySet()) { sb.append(e.getKey()).append("=").append(e.getValue()).append("; "); } if (sb.length() > 0) { request.setHeader("Cookie", sb.toString()); } } // 响应回来后更新 Cookie private void saveCookies(HttpResponse response) { List<String> setCookies = response.getHeaders("Set-Cookie"); for (String c : setCookies) { String pair = c.split(";")[0]; // 只取 key=value 部分 String[] kv = pair.split("=", 2); cookieStore.put(kv[0].trim(), kv[1].trim()); } }

逻辑说明:attachCookies在每个请求发出前把容器里的键值拼成Cookie头;saveCookies从响应的Set-Cookie里剥出key=value,忽略Path、Expires这些属性(对维持会话够用)。参数上要注意split("=", 2)的2,因为 Cookie 值里可能含=,不限次数会切坏。自动登录就是在登录页表单里预填账号密码,再把焦点设到验证码输入框,这部分是 Swing 的setText和requestFocus,没什么玄学。

4.2 查询与余票解析

查询请求发出去后,返回的是一大段 HTML,里面藏着车次表格。解析的关键是定位到表格行,再按列取字段。用 htmlparse 的思路大致是:

// 解析查询结果页,提取车次与余票 public List<Train> parseTrains(String html) { List<Train> list = new ArrayList<>(); // 定位车次表格的每一行 List<HtmlElement> rows = HtmlParser.parse(html).findAll("tr[id^=ticket_]"); for (HtmlElement row : rows) { Train t = new Train(); t.code = row.getText("td.train-num"); // 车次号 t.from = row.getText("td.from-station"); // 出发站 t.to = row.getText("td.to-station"); // 到达站 t.left = row.getText("td.left-ticket"); // 余票描述 t.secret = row.getAttribute("data-secret"); // 提交订单要用的隐藏串 list.add(t); } return list; }

逻辑说明:tr[id^=ticket_]是按行 id 前缀筛车次行,getText取单元格文本,getAttribute取隐藏字段。这里最容易翻车的是secret这类隐藏串——它每次查询都会变,提交订单时必须用「最近一次查询」拿到的值,缓存旧值会直接报「订单提交失败」。参数上,余票字段可能是「有」「无」「12」这种混合文本,判断时要先做空值和「无」的过滤,别直接Integer.parseInt。

4.3 自动重试与订单提交

重试机制是这个程序的灵魂。原文说「遇购票人数过多或网络繁忙会自动重试直到成功」,实现上就是一个带退避的循环:

// 带最大次数和间隔的重试封装 public <T> T retry(Callable<T> action, int maxTimes, long intervalMs) throws Exception { Exception last = null; for (int i = 0; i < maxTimes; i++) { try { T result = action.call(); if (result != null) return result; // 成功即返回 } catch (Exception e) { last = e; // 记录最后一次异常 } Thread.sleep(intervalMs); // 固定间隔,避免请求过密 } throw last != null ? last : new RuntimeException("重试次数耗尽"); }

逻辑说明:action是任意可重试的操作(登录、查询、提交),maxTimes控制上限防止死循环,intervalMs是每次失败后的等待。参数怎么调:登录阶段可以maxTimes=10、间隔 2 秒;提交订单阶段建议间隔更短但次数别太多,因为余票窗口很短,等太久票就没了。提交订单时要把secret、乘客信息、车次一起 POST 出去,返回里判断是否含「订单成功」关键字,成功就触发弹窗和声音。

注意:原文明确说密码是明文存在 Cookie 里的,公共电脑上别勾「记住密码」,这是设计上的取舍,不是 bug。

5. 避坑与排查:那些跑不通的真实原因

5.1 现象:双击 start.bat 窗口一闪而过

原因:java不在 PATH 里,或者 JRE 版本低于 1.6,java -jar直接失败退出。解决:命令行手动执行java -jar GOHOME.jar看完整报错;确认java -version有输出;start.bat末尾保留pause让窗口停住。

5.2 现象:登录一直失败,日志刷「繁忙」

原因:验证码识别率低(原文说十次对一次),或者请求间隔太短被限流。解决:关掉自动识别改手动输入;把重试间隔从 1 秒调到 2~3 秒;检查 Cookie 是否在登录后被正确回写,掉 Cookie 会表现为「登录成功但查询要求重新登录」。

5.3 现象:查到票了但提交订单总报错

原因:提交时用的隐藏字段(secret、token)是旧的,或者漏带了某个表单字段。解决:每次提交前重新走一遍查询拿最新隐藏串;用浏览器开发者工具对照真实提交的 Form Data,逐字段核对,少一个都不行。

5.4 现象:指定了车次却抢到别的车

原因:traincode是「优先」不是「只买」,指定车次无票时程序按设计自动选其他车次。解决:这是预期行为,原文写得很清楚;如果只想买某一趟,得改代码把过滤逻辑从「排序优先」改成「硬过滤」,但代价是无票时一直空转。

5.5 现象:抢到票但没付上款

原因:程序只负责下单占位,支付要人工登录网页在 45 分钟内完成。解决:抢到后立刻去「我的订单」支付;别关程序别断网;提前把支付方式准备好,减少操作时间。

6. 二次开发:把「能跑」改成「好用」的几个方向

拿到这份源码,最值得动的不是界面,是三个地方。第一是验证码识别,原文用颜色像素思路,准确率有限,可以换成「先二值化再按字符切分」的流程:读图、灰度、按阈值转黑白、按列投影找字符间隙、逐字符比对模板。核心是把BufferedImage的像素矩阵玩熟:

// 简单二值化:灰度大于阈值置白,否则置黑 public BufferedImage binarize(BufferedImage src, int threshold) { int w = src.getWidth(), h = src.getHeight(); BufferedImage out = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_BINARY); for (int x = 0; x < w; x++) { for (int y = 0; y < h; y++) { int rgb = src.getRGB(x, y); int gray = (rgb >> 16 & 0xff) * 30 + (rgb >> 8 & 0xff) * 59 + (rgb & 0xff) * 11; out.setRGB(x, y, gray / 100 > threshold ? 0xFFFFFF : 0x000000); } } return out; }

阈值threshold一般取 100~140 之间试,不同批次的验证码底色不一样,得拿几十张样本调。第二是重试策略,把固定间隔改成「指数退避 + 随机抖动」,避免所有请求卡在同一秒。第三是配置热加载,现在改config.properties要重启,可以加个WatchService监听文件变更,改完自动重读。

验证改造是否有效,我的习惯是先用「回放」的方式:把一次真实查询的 HTML 存成本地文件,让解析模块直接读文件跑,不依赖网络,这样调解析逻辑时不会因为网络抖动误判。等解析稳定了再接真实请求。

说个我自己的教训。早些年我改这类工具,总想一步到位把识别率拉到很高,结果在图像处理上耗了两周,票没抢到几张。后来才明白,抢票的瓶颈往往不在识别,而在「请求节奏」和「隐藏字段的时效性」——识别错一次重试就行,字段带错一次直接出局。从那以后我每次动这类自动化流程,都强制先把「一次成功提交需要哪些字段、每个字段从哪个页面来」列成表,再写代码。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进