ARTICLE DETAIL

资讯详情

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

Selenium、代理IP与原生POST融合:抢购脚本的工程化架构

Selenium、代理IP与原生POST融合:抢购脚本的工程化架构 简介一套面向有Java基础开发者的抢购辅助脚本聚焦ibox二级科技场景通过代理IP与Selenium自动化解决接口数据加密、手动提交耗时等问题。源码包共10个文件含4个Java核心代码、2个XML配置、1个readme文档、1个IP池文本、1个manifest清单及chromedriver.exe驱动整体仅5.99MB小巧易部署。已有512人学习下载。脚本实现getip文件缓存以节省代理费用借助Selenium调用网站内js解密方法获取商品列表并预留机器人滑动验证模块当前未完成。对研究Java模拟请求、逆向解密逻辑或自动化抢购的开发者这份代码提供了可直接运行的骨架和排错思路便于继续补全验证流程并接入实际业务。1. 抢购 ibox 二级科技脚本为什么把 Selenium、代理 IP 和原生 POST 拼在一起在 ibox 二级科技的抢购场景里最大的坑不是手速而是前端 JS 把请求加密了。直接从控制台复制 cURL 会发现请求体是密文改了商品 ID 却带不过去因为加密令牌绑定了时间戳和页面会话。用 Selenium 一路模拟点击虽然能过加密但每个动作都走浏览器渲染延迟高、IP 容易暴露多账号操作马上触发风控。这个项目拆了一个更务实的架构用 Selenium 只做“解密窗口”和“登录态”用 Java 原生 POST 做商品列表轮询和下单提交再配合代理 IP 池轮换出口 IP。对有一定 Java 基础的人这种“浏览器解耦”模式比纯 Selenium 自动化更容易控制节奏也给未完成的滑动验证留了人工兜底的位置。2. 用 Selenium 执行 window.de让浏览器替你解密2.1 为什么必须依赖浏览器而不是直接调 JS拿到 ibox 前端的加密入口后很多人的第一反应是把 JS 复制到本地用 Java 的 GraalJS 或 Node 去调。两周后你会发现页面改版时混淆逻辑变了解密函数内部还会读取当前页面上下文里的 cookie、localStorage甚至某些 DOM 元素脱离浏览器后要么直接报错要么解密出来是空字符串。所以我更建议把“解密”这件事留在页面里用 Selenium 打开真实页面等待 window.de 被注入然后通过 JavascriptExecutor 传参调用。这样解密函数的运行环境与线上一致Token 和时间戳都是从页面上下文自动获取的脚本这边只关心传什么密文、拿什么明文。2.2 解密入口探测与最小 Java 实现先用typeof window.de确认入口是否存在再调用它。注入 JavaScript 时尽量避免把密文字符串直接拼进脚本字符串因为密文里常有引号、反斜杠拼进去很容易破坏 JS 语法。用arguments[0]传参就不会有这个问题。// decryption_demo.java 片段 JavascriptExecutor js (JavascriptExecutor) driver; // 第一步确认 window.de 是否已经挂载 String deType (String) js.executeScript(return typeof window.de;); if (!function.equals(deType) !object.equals(deType)) { throw new IllegalStateException(window.de 未注入检查页面是否加载完成); } // 第二步根据不同类型调用解密入口 String plain (String) js.executeScript( var fn window.de; if (typeof fn function) { return fn(arguments[0]); } if (fn typeof fn.decrypt function) { return fn.decrypt(arguments[0]); } return ;, encryptedPayload );这段代码做了三件事先探测入口类型再按照“函数”或“对象方法”两种形式调用最后在调用失败时返回空字符串而不是抛异常。如果你第一次跑就返回 null优先检查页面是不是被 iframe 包裹window.de 是否挂载在 iframe 而不是主页面。若是 iframe先driver.switchTo().frame(frame_id)再执行 JS。2.3 解密结果的传递长 JSON 别直接 return解密结果通常是一个 JSON 对象直接return会被 Selenium 自动序列化成 JSON但它一旦包含 undefined、循环引用或大量转义符驱动端就可能拿到失败结果。常见做法是先在页面里JSON.stringify再以字符串方式返回Java 侧再解析。解密方式动态参数获取维护成本延迟适用阶段浏览器控制台手工复制手动补参数低人工接口调试Selenium 动态执行页面环境自动补充中高完整链路Java 反混淆重写算法需维护各种密钥和算法迭代高低长期高频请求这三种方式里Selenium 动态执行是成本和成功率最平衡的。手工复制适合一开始分析字段Java 重写适合后续优化速度但项目开发初期不建议碰。// 对象转字符串返回避免序列化边界问题 String json (String) js.executeScript( var data window.de(arguments[0]); if (data null || data undefined) return ; return typeof data string ? data : JSON.stringify(data);, encryptedPayload );注意这里encryptedPayload是外部传入的密文通过arguments[0]映射到页面内的arguments[0]。不像拼字符串那样容易造成引号冲突。拿到json后后期会结合商品列表和下单参数一起使用这部分在第 4 章展开。3. 代理 IP 池与 ip.txt 文件缓存把取号成本压下来3.1 从代理接口取号并写入 ip.txt项目里getip的用途很直接从代理商拿一批 IP 写进ip.txtSelenium 和 HttpClient 每次请求都从这份文件里取。之所以做文件缓存是因为代理商按次计费高频轮询商品列表时如果每次都重新取号几十个请求就会把一个月的额度刷完。常见代理商会返回类似下面这种结构的 JSON不过字段名可能有差异以你的代理商文档为准{ code: 200, data: { proxy_list: [1.2.3.4:8080, 5.6.7.8:9090], expire_time: 1710000000 } }把取号结果写入本地缓存的逻辑可以做成这样private void refreshIpCache() throws IOException { String api http://你的代理商接口/getip?num20formatjson; String jsonBody doHttpGet(api); JSONObject obj JSON.parseObject(jsonBody); JSONArray list obj.getJSONObject(data).getJSONArray(proxy_list); // 在每行末尾追加过期时间避免下次读的时候猜不清是否失效 ListString lines new ArrayList(); for (int i 0; i list.size(); i) { lines.add(list.getString(i) # obj.getJSONObject(data).getLongValue(expire_time)); } Files.write(Paths.get(ip.txt), lines, StandardCharsets.UTF_8); }doHttpGet可以用 Apache HttpClient 实现也可以用URL.openConnection()关键是别自己拼 socket。上面这段把expire_time跟着 IP 一起写进文件之后读取时就能直接判断是否过期。如果你用的是多个代理组合expire_time 不同可以把取号接口返回的每个代理单独带上过期时间。3.2 带过期时间读取缓存与轮换队列ip.txt的每一行格式我建议定为ip:port#unix_timestamp字段说明如下字段示例说明IP 地址和端口1.2.3.4:8080代理请求入口过期时间1710000000Unix 秒超过即失效读取并构建可用队列的代码// ProxyPool.java QueueString availableProxies new ConcurrentLinkedQueue(); public void loadProxies() throws IOException { long now System.currentTimeMillis() / 1000L; ListString lines Files.readAllLines(Paths.get(ip.txt), StandardCharsets.UTF_8); for (String line : lines) { String[] parts line.split(#); if (parts.length 2) continue; long expire Long.parseLong(parts[1]); if (expire now) { availableProxies.offer(parts[0]); } } }这里用了ConcurrentLinkedQueue因为抢购脚本通常有多个线程并发取号。队列的poll()取头一个代理offer()放回可用队列。如果某个代理连续失败三次就不要放回而是从队列中移除避免一个坏 IP 反复重试。3.3 把可用代理接进 Selenium 和 HttpClient代理池不只是给 HttpClient 用的Selenium 的浏览器访问也需要切换 IP。Selenium 启动时可以通过ChromeOptions设置代理ChromeOptions options new ChromeOptions(); options.addArguments(--proxy-serverhttp:// proxyHost); WebDriver driver new ChromeDriver(options);HttpClient 侧设置代理更灵活因为可以每次请求前从队列拿一个 IPHttpHost proxy new HttpHost(ip, Integer.parseInt(port)); RequestConfig config RequestConfig.custom() .setProxy(proxy) .setConnectTimeout(1500) .setSocketTimeout(3000) .build();连接超时设置短一点是有意的。代理 IP 不可用时1500 毫秒内直接失败然后立刻换下一个代理。如果超时设成 10 秒抢购窗口早过去了。Socket 超时 3 秒是防止某个代理响应缓慢把请求阻塞住。这块直接决定脚本的稳定程度建议在正式跑之前先用一个小的地址探测脚本验证代理连通率。4. 商品列表解析与 POST 下单混合会话链路4.1 解密后的商品列表怎么映射成 Java 对象第 2 章拿到的json字符串就是商品列表明文字段一般是goodsId、skuId、price、stock之类。用 fastjson 或 Jackson 解析成 Java 对象即可。这里直接操作 JSONObject避免为核心数据写一个完整 POJO// goods_list_demo.java JSONArray goodsList JSON.parseArray(plainJson); for (int i 0; i goodsList.size(); i) { JSONObject item goodsList.getJSONObject(i); String goodsId item.getString(goodsId); int stock item.getIntValue(stock); if (stock 0) continue; // 只把有库存的商品 ID 放入待抢队列 pendingGoods.offer(goodsId); }pendingGoods是一个阻塞队列后续下单线程从这个队列取商品 ID不需要在列表解析线程里直接发起请求。这样能实现“一边轮询列表一边提交订单”响应延迟能降低不少。实际解析时字段名可能不是驼峰注意看解密后的原始 key。4.2 为什么提交必须走 POSTget 和 post 习惯的差别很多人在拿到抢购接口后习惯性把参数拼到 URL 后面这是 GET 思维。GET 适合查询参数会出现在 URL 和访问日志里POST 适合提交订单参数放在 Request Body 中并且需要Content-Type与请求体格式匹配。抢购 ibox 二级科技这类操作会改变库存状态只能走 POST。项GETPOST参数位置URL Query StringRequest Body幂等性是多个请求一般不会改变状态否会创建或更新订单调试难度浏览器直接访问即可需要检查 Body 和响应状态抢购场景拉取列表可以用下单必须用用 Apache HttpClient 发送 POST 请求的代码// submit_order_demo.java HttpPost post new HttpPost(ORDER_URL); post.setConfig(config); post.setHeader(Content-Type, application/json;charsetutf-8); post.setHeader(Referer, https://目标页面); JSONObject body new JSONObject(); body.put(goodsId, goodsId); body.put(count, 1); body.put(sign, encryptedSign); post.setEntity(new StringEntity(body.toJSONString(), StandardCharsets.UTF_8)); try (CloseableHttpResponse resp httpClient.execute(post)) { int status resp.getStatusLine().getStatusCode(); String result EntityUtils.toString(resp.getEntity(), StandardCharsets.UTF_8); // 返回状态非 200 时打印响应便于分析是否被风控 }encryptedSign不能直接写明文它需要在页面上下文里通过 window.de 生成。一种做法是下单线程拿到goodsId后调用js.executeScript把goodsId和时间戳交给window.de拿到返回值再拼接订单 body。这样就能保证签名算法每次都能命中。4.3 会话打通把 Selenium 的 Cookie 迁移到 HttpClient实际跑单时Selenium 负责登录和解密HttpClient 负责高频 POST。两者必须共享同一个登录会话。最容易出错的地方就是把 Selenium 里的 Cookie 直接塞给 HttpClient但忘了设置 domain 和 path导致请求出去只带着空 cookie。正确做法是从 Selenium 读取 Cookie再逐个复制到 Apache HttpClient 的 CookieStore// cookie_sync_demo.java CookieStore cookieStore new BasicCookieStore(); for (Cookie ck : driver.manage().getCookies()) { BasicClientCookie clientCookie new BasicClientCookie(ck.getName(), ck.getValue()); clientCookie.setDomain(ck.getDomain()); clientCookie.setPath(ck.getPath()); clientCookie.setExpiryDate(ck.getExpiry()); cookieStore.addCookie(clientCookie); } CloseableHttpClient client HttpClients.custom() .setDefaultCookieStore(cookieStore) .setDefaultRequestConfig(config) .build();cookieStore和config都是之前构建好的对象。迁移以后HttpClient 发出的请求就能带上 Selenium 页面里生成的登录态。要注意cookie 里的 HttpOnly 标记不会影响这里因为BasicClientCookie不关心原始标记只有 domain 匹配才会被使用。如果你发现请求返回未登录第一件事就是确认 domain 是不是丢了点前缀比如backend.xxx.com和.xxx.com的匹配范围完全不同。5. 滑动验证未完成时的工程化兜底人工滑块与 Cookie 回传5.1 把“未完成”变成“人工介入”项目里写着“通过机器人滑动验证(未完成)”我认为这个结论是对的。滑动验证的核心不只是拖一条轨迹后端还会校验浏览器指纹、缺口识别结果和时间曲线。与其在未完成的算法上死磕不如把它当成暂停信号。我一般这么设计HttpClient 下单时如果返回某个错误码表示滑块风控触发就把当前订单请求挂起同时调起 Selenium 窗口让浏览器停留在需要滑块的页面然后人工拖一下。Java 端轮询滑块元素是否消失消失后再继续后续请求。5.2 检测滑块出现并等待人工完成的代码// captcha_wait_demo.java JavascriptExecutor js (JavascriptExecutor) driver; boolean appeared false; try { WebDriverWait wait new WebDriverWait(driver, Duration.ofSeconds(5)); WebElement captcha wait.until( ExpectedConditions.visibilityOfElementLocated( By.cssSelector(.captcha, .slide-verify, .geetest_slider) ) ); appeared true; } catch (TimeoutException e) { // 当前没有滑块继续提交即可 } if (appeared) { // 提示人工拖拽阻塞等待验证码消失 System.out.println(出现滑动验证请手动处理); WebDriverWait wait new WebDriverWait(driver, Duration.ofSeconds(60)); wait.until(ExpectedConditions.stalenessOf( driver.findElement(By.cssSelector(.captcha, .slide-verify, .geetest_slider)) )); }这个方案不追求机器模拟而是保证抢购流程不中断。检测到滑块后把浏览器窗口保持在前台人工处理完代码检测到元素被移除或重新渲染后再从 Selenium 读取最新的 Cookie 回填到 HttpClient。滑块通过后验证服务一般会生成新的 Token 并写入 Cookie不回填的话下一次 POST 还是会失败。回填代码就是把第 4.3 节那段再执行一次。要注意验证出现时不能把旧 Cookie 清掉否则人工拖完滑块后浏览器已经要重新登录反而更慢。实际场景里等滑块消失后立刻执行driver.manage().getCookies()然后重建 CookieStore重发刚才失败的 POST。这样整套脚本就从“未完成滑块机器人”变成了“人工兜底 半自动抢购”抢购窗口来临时至少不会卡死在验证码这步。本文还有配套的精品资源点击获取
返回列表