十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Java POST模拟请求与Selenium协作:抢购自动化架构解析

Java POST模拟请求与Selenium协作:抢购自动化架构解析 简介这份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 负责模拟表单提交、控制请求头与 CookieSelenium 负责把需要真实浏览器才能过的页面交互走完顺便把可信的登录态给 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; charsetutf-8))。如果接口文档没写清先用浏览器的开发者工具看“Payload”标签它显示的是 Form Data 还是 Request Payload一眼就能区分。另外connectTimeout和readTimeout在抢购场景要刻意调小默认的 10 秒大概率导致线程堆积。抢购是“宁可失败重试不可挂住不放”的逻辑超时设置 3 秒以内更合理。2.2 Header 伪装与 Referer 校验很多后端并不只校验登录态还会校验来源页。Referer 缺失、请求顺序异常、User-Agent 与浏览器版本不匹配都可能被风控直接拦截。我一般会维护一个 Header 常量集合从浏览器开发者工具里把以下字段抓出来复制进配置Header 字段作用抢购场景建议User-Agent标识客户端环境保持与实际浏览器一致Referer来源页面必须是商品详情页Origin跨域来源与站点主域名一致X-Requested-With标记 AJAX 请求很多服务端会校验该字段Accept-Language语言偏好zh-CN,zh;q0.9Accept-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 的请求头SetCookie 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上先找span或a标签再取文本比如ListWebElement 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();elementToBeClickable比visibilityOfElementLocated更严格它同时校验元素可见且可用。提交按钮在倒计时阶段通常是disabled状态等到可点击的瞬间再执行点击正好契合抢购场景。3.3 Selenium 上传本地文件的边界处理部分平台在提交订单前要求上传资质图片、身份证照片或收货凭证input[typefile]元素隐藏起来时直接sendKeys路径是最可靠的方案。需要注意sendKeys传入的是绝对路径WebElement uploadInput driver.findElement(By.cssSelector(input[typefile])); uploadInput.sendKeys(C:/Users/me/Desktop/id_card.png);这要求本机文件路径是稳定存在的而且不能有中文目录兼容性问题。如果上传组件是自定义的可拖拽区域加隐藏 input依旧优先定位隐藏 input而不是模拟拖拽后者需要计算偏移量极其不稳定。3.4 将 Selenium 拿到的新签名参数回传给 Java 请求现代前端经常在提交前通过 JS 生成一个签名参数比如_signature或requestId。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 秒大概率是前端资源加载被限流需要主动降级而不是继续盲目提交。本文还有配套的精品资源点击获取
返回列表