Java POST模拟请求与Selenium协作:抢购自动化架构解析
2026/9/16 20:16:21 网站建设 项目流程

简介:这份Java脚本面向具备Java基础、想自动参与ibox二级科技抢购的开发者,结合代理IP、模拟提交与Selenium自动化,解决接口加密与商品列表获取等实际问题。包体共10个文件,以4个Java源码为主,配合pom.xml依赖配置、chromedriver.exe驱动、md说明文档及ip.txt代理缓存文件,压缩包仅5.99MB,便于下载部署。已有512人学习,适合用来研究抢购流程和反爬思路。资源中机器人滑动验证部分尚未完成,可留给读者在此基础上继续开发。具体包括通过熊猫代理类服务填充IP并做文件缓存以节省费用,以及利用Selenium调用网站内置JS解密方法,绕过加密接口限制;同时readme.md提供使用指引,适合有经验者快速上手。

1. Java POST 模拟请求与 Selenium 联手做抢购:架构到底卡在哪

做后台抢购、限量商品补货或活动秒杀类的自动化时,很多初接触的人会把全部赌注压在 Selenium 模拟点击上,结果页面一卡、验证码一转、提交超时,整个脚本就崩了。真实情况是,Selenium 擅长处理浏览器的 DOM 交互、下拉框选择、文件上传这类“人看得懂、选择器写得出”的行为,但它不擅长高并发、低延迟、快速重试这些后端性质的流量控制。反过来,Java 侧用 HTTP 客户端模拟 POST 请求,能把提交动作压缩到几十毫秒一轮,但遇到前端 JS 动态生成的签名参数、非原生下拉框、上传组件这类交互时又显得力不从心。

这篇内容把这两种能力拼在一起:Java 负责模拟表单提交、控制请求头与 Cookie,Selenium 负责把需要真实浏览器才能过的页面交互走完,顺便把可信的登录态给 Java 用。面向的读者是已经会写简单 Selenium 脚本、又被真实抢购场景里的“页面元素等不到、提交按钮不可点、Cookie 不一致”折磨过的工程师。下面按“搭请求、走页面、拼并发、补边界”的顺序把整条链路展开。

2. 先把 Java 侧 POST 模拟请求搭起来:HttpClient 选型、Header 伪装与 Cookie 链路

2.1 用 OkHttp 发起一个最小可用的 POST 请求

常见做法是选 OkHttp 或 Apache HttpClient 作为 Java 侧的 HTTP 客户端。我一般会选 OkHttp,原因是接口设计简单、连接池管理成熟、链式调用写出来可读性好,而且对 HTTP/2 的支持也不需要额外配置。先看一个不带任何花哨功能的最小 POST 提交:

OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(3, TimeUnit.SECONDS) .readTimeout(5, TimeUnit.SECONDS) .build(); RequestBody body = new FormBody.Builder() .add("skuId", "10231") .add("quantity", "1") .build(); Request request = new Request.Builder() .url("https://api.example.com/order/create") .post(body) .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)") .header("Referer", "https://www.example.com/detail/10231") .header("Content-Type", "application/x-www-form-urlencoded") .build(); try (Response response = client.newCall(request).execute()) { System.out.println(response.code()); System.out.println(response.body().string()); }

这段代码里最容易被忽略的是FormBody与 JSON 的差异。上面用的是表单编码提交,适合传统 Java Web 后端或者商城类接口;但很多新系统只接收application/json,这时要把FormBody换成RequestBody.create(jsonString, MediaType.parse("application/json; charset=utf-8"))。如果接口文档没写清,先用浏览器的开发者工具看“Payload”标签,它显示的是 Form Data 还是 Request Payload,一眼就能区分。

另外connectTimeoutreadTimeout在抢购场景要刻意调小,默认的 10 秒大概率导致线程堆积。抢购是“宁可失败重试,不可挂住不放”的逻辑,超时设置 3 秒以内更合理。

2.2 Header 伪装与 Referer 校验

很多后端并不只校验登录态,还会校验来源页。Referer 缺失、请求顺序异常、User-Agent 与浏览器版本不匹配,都可能被风控直接拦截。

我一般会维护一个 Header 常量集合,从浏览器开发者工具里把以下字段抓出来复制进配置:

Header 字段作用抢购场景建议
User-Agent标识客户端环境保持与实际浏览器一致
Referer来源页面必须是商品详情页
Origin跨域来源与站点主域名一致
X-Requested-With标记 AJAX 请求很多服务端会校验该字段
Accept-Language语言偏好zh-CN,zh;q=0.9
Accept-Encoding压缩方式建议去掉 gzip,便于排查响应

这里有一个容易踩的坑:OkHttp 默认会添加Accept-Encoding: gzip,如果你在测试时想直接读原始响应报文,可以用.header("Accept-Encoding", "identity")关掉压缩,否则看到的是乱码,还容易误判接口逻辑。

2.3 Cookie 链路:让 Selenium 登录态与 Java POST 请求共用

这是整套方案里最关键的一步。Selenium 走完登录后,浏览器里维护了一套完整的 Cookie,而 Java 侧发起 POST 时是没有这个状态的。常见做法是用 WebDriver 的manage().getCookies()取出所有 Cookie,再手动装配到 OkHttp 的请求头:

Set<Cookie> seleniumCookies = driver.manage().getCookies(); StringBuilder cookieHeader = new StringBuilder(); for (Cookie cookie : seleniumCookies) { cookieHeader.append(cookie.getName()) .append("=") .append(cookie.getValue()) .append("; "); } Request request = new Request.Builder() .url("https://api.example.com/order/create") .header("Cookie", cookieHeader.toString()) // 其余 Header 省略 .build();

注意Cookie域名的匹配问题。Selenium 拿到的 Cookie 可能包含 Domain 属性,有些 Cookie 是.example.com作用域,有些是www.example.com,直接拼接后发给 API 子域时可能部分失效。一个更稳妥的办法是只保留“不带有 Domain 限制的会话类 Cookie”,比如 JSESSIONID、SESSION、TOKEN 这些,它们在跨子域时通常仍然有效,而购物车、页面埋点这类 Cookie 对 POST 接口毫无意义。

从安全角度看,Cookie 里可能包含 HttpOnly 属性,这并不影响通过getCookies()读取。脏数据干净不掉时,可以在 Selenium 中对 API 域名发起一次预请求,让 WebDriver 自动补全必要 Cookie,再执行抓取。

3. 用 Selenium 补齐页面行为自动化:定位、等待与提交前的最后一公里

3.1 非原生下拉框的处理:div + ul + li 组合

抢购流程里最常见的一个页面交互是“选择商品属性”,比如颜色、尺码、套餐版本。这类控件很多不是原生的<select>,而是前端框架渲染的div + ul + li组合,直接用Select类会抛UnexpectedTagNameException

正确的操作路径是先点击外层 div 触发下拉面板展开,再在ul里按可见文本定位li点击。下面给出一个通用的封装:

public void chooseDivOption(WebDriver driver, String divCss, String optionText) { WebElement dropdown = driver.findElement(By.cssSelector(divCss)); dropdown.click(); // 稍微给点渲染时间,不要用固定 sleep,等待 li 出现即可 WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5)); WebElement targetLi = wait.until( ExpectedConditions.visibilityOfElementLocated( By.xpath("//ul[contains(@class,'dropdown-menu')]//li[contains(text(),'" + optionText + "')]") ) ); targetLi.click(); }

这个方案的问题在于contains(text(), ...)在文本较长且包含空格时极容易匹配多余元素。更稳的做法是在这个li上先找spana标签再取文本,比如:

List<WebElement> allOptions = driver.findElements(By.xpath("//ul[contains(@class,'dropdown-menu')]/li")); for (WebElement item : allOptions) { String text = item.findElement(By.tagName("span")).getText().trim(); if (optionText.equals(text)) { item.click(); return; } }

用文本精确匹配替代模糊匹配,能减少误点相邻项的概率。页面渲染慢时,这个循环会在findElement(By.tagName("span"))上抛StaleElementReferenceException,解决方式是在循环外先整体读取文本列表、退出循环再点击,避免元素引用失效。

3.2 等待策略用显式等待,放弃固定 sleep

固定Thread.sleep(2000)是抢购脚本失效的头号原因。发布会场次不同、服务器负载不同、本地网络延迟不同,固定等待要么提前执行导致找不到元素,要么延后执行拖慢整轮提交。显式等待才是稳定做法:

WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(3)); wait.until(ExpectedConditions.elementToBeClickable( By.id("submit-order-btn") )).click();

elementToBeClickablevisibilityOfElementLocated更严格,它同时校验元素可见且可用。提交按钮在倒计时阶段通常是disabled状态,等到可点击的瞬间再执行点击,正好契合抢购场景。

3.3 Selenium 上传本地文件的边界处理

部分平台在提交订单前要求上传资质图片、身份证照片或收货凭证,input[type=file]元素隐藏起来时,直接sendKeys路径是最可靠的方案。需要注意sendKeys传入的是绝对路径:

WebElement uploadInput = driver.findElement(By.cssSelector("input[type='file']")); uploadInput.sendKeys("C:/Users/me/Desktop/id_card.png");

这要求本机文件路径是稳定存在的,而且不能有中文目录兼容性问题。如果上传组件是自定义的可拖拽区域加隐藏 input,依旧优先定位隐藏 input,而不是模拟拖拽,后者需要计算偏移量,极其不稳定。

3.4 将 Selenium 拿到的新签名参数回传给 Java 请求

现代前端经常在提交前通过 JS 生成一个签名参数,比如_signaturerequestId。Java 侧预先拼好的 POST 体里没有这个参数,直接提交必然失败。我一般先让 Selenium 点击“提交订单”按钮,同时用 Java 侧拦截网络请求,或通过页面 JS 执行结果读取隐藏 input 里的签名值,再交给 Java 发起最终 POST:

// 执行 JS 读取隐藏字段 String signature = (String) ((JavascriptExecutor) driver) .executeScript("return document.getElementById('_signature').value;");

拿到这个值后,Java 侧再组装新的请求体。要注意签名参数通常是一次性的,一旦请求失败不能重放,必须让 Selenium 重新触发一次签名生成,否则会一直报参数无效。

4. 并发与性能:把单个提交变成受控的多线程收口

4.1 为什么不能直接用多线程无脑刷

抢购场景的并发不是“启动 100 个线程无限提交”,而是“在窗口开启的瞬间完成有限次数的有效提交”。无脑多线程会造成请求快速堆积在连接池里,服务端一旦发现单 IP 高频访问,直接丢弃后续请求甚至封禁账号。真正要做的,是让 Java 侧保持请求频率可控、连接复用、失败快速重试。

4.2 用 Semaphore 控制并发提交窗口

提交动作使用固定线程池加信号量双重限制。信号量的作用是把并发提交数压在一个安全阈值内,比如同一时刻最多允许 5 个请求在途:

ExecutorService executor = Executors.newFixedThreadPool(8); Semaphore semaphore = new Semaphore(5); for (int i = 0; i < 20; i++) { executor.submit(() -> { try { semaphore.acquire(); boolean success = submitOrder(buildOrderRequest()); if (!success) { retrySubmit(); // 自定义重试,内部再走一次 semaphore.acquire() } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { semaphore.release(); } }); } executor.shutdown();

Semaphore(5)的 5 不是随便写的,它取决于服务端接口的响应耗时和连接池大小。假设接口平均响应 200ms,本地连接池最大 10,那并发 5 是一个相对安全的起点。压测时看响应码分布,如果出现大量 429 或 5xx,就把信号量调小。

4.3 OkHttp 连接池参数调整

每轮抢购会有大量请求,如果每个请求都新建连接,TCP 握手开销远超请求本身。需要对 OkHttp 的ConnectionPool单独配置:

OkHttpClient client = new OkHttpClient.Builder() .connectionPool(new ConnectionPool(20, 30, TimeUnit.MINUTES)) .retryOnConnectionFailure(true) .build();

maxIdleConnections设置为 20 意味着最多保留 20 条空闲连接,keepAliveDuration设为 30 分钟。这里有个容易出错的地方:retryOnConnectionFailure(true)只对连接层异常生效,不会对 HTTP 500 做重试,业务重试逻辑必须自己写,否则 500 响应会被当成成功处理。

4.4 避免用 Selenium 承载高并发提交

Selenium 是重量级组件,每个 WebDriver 实例都会启动一个完整的浏览器进程。想用 10 个 WebDriver 并发抢购,内存开销轻松超过 4GB,且浏览器进程互相独立,Cookie 同步、资源占用都会成为瓶颈。

所以最终方案必须是:Selenium 只负责登录、取签名、过验证码等“非它不可”的操作,真正提交订单的动作交给 Java OkHttp 线程池。这个架构的稳定性上限远高于“二十个 WebDriver 同时点按钮”的野路子,资源消耗也更低。

5. 提交后的验证、日志与降级策略

5.1 响应码与响应体双重校验

抢购提交后判断成功不能只看 HTTP 200,很多系统即使逻辑失败也返回 200。我一般会把响应体解析成 JSON,按业务码判断:

try (Response response = client.newCall(request).execute()) { String respBody = response.body().string(); if (response.code() == 200 && respBody.contains("\"code\":0")) { // 订单号解析 String orderId = respBody.replaceAll(".*\"orderId\":\"([^\"]+)\".*", "$1"); log.info("submit success, orderId={}", orderId); } else { log.warn("submit failed, code={}, body={}", response.code(), respBody); } }

正则解析订单号只适合快速验证,正式代码还是建议用 Jackson 或 Gson 转成 POJO 后再取字段,避免响应字段顺序变化导致解析崩溃。

5.2 重试退避节奏

抢购提交失败后的重试需要加随机退避,避免所有线程同时发起第二次请求,形成“惊群效应”。比较稳妥的节奏是第一次失败等待 500ms,第二次等待 1s,第三次等待 2s,超过三次就停止当日轮次并告警:

public void retrySubmit(Request request, int maxRetry) { int retry = 0; long delay = 500; while (retry < maxRetry) { try { boolean success = doPost(request); if (success) return; } catch (IOException e) { log.error("retry failed", e); } Thread.sleep(delay + new Random().nextInt(500)); delay *= 2; retry++; } }

随机偏移量是防止多线程同步重试的关键,固定时间间隔在并发场景下基本等于再次同时打爆接口。

5.3 订单结果验证:回到 Selenium 查单页

POST 请求返回成功只能代表服务端受理,最终是否生成订单还要去订单列表页确认。常见做法是提交后让 Selenium 自动刷新订单列表,断言新增订单的关键信息:

WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); wait.until(ExpectedConditions.numberOfElementsToBe( By.cssSelector(".order-item"), expectOrderCount + 1 ));

这个步骤虽然慢,但能有效防止“接口显示成功、实际未扣款”的假订单问题。同时把查单结果写回日志,方便事后分析。

5.4 监控指标与告警埋点

脚本跑完不能只看日志尾部,需要在关键位置打点上送。我在实践中会记录以下三个指标的耗时与成功率:登录耗时、签名生成耗时、POST 提交耗时。其中“签名生成耗时”非常能反映页面加载状态,这个值一旦连续多轮超过 2 秒,大概率是前端资源加载被限流,需要主动降级,而不是继续盲目提交。

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

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

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

立即咨询