逆向解析,基于Java与Selenium自动化获取全国公共资源交易平台招投标详情数据 📅 2026/7/5 11:06:41 1. 项目背景与需求分析全国公共资源交易平台作为招投标信息的官方发布渠道每天产生大量有价值的工程、采购、产权交易数据。这些数据对企业市场分析、竞争对手监测具有重要参考价值。但平台采用动态加载技术传统爬虫难以直接获取完整信息。我曾为某建筑集团搭建数据采集系统时发现直接解析接口返回的JSON数据缺失关键字段而详情页的招标文件、资质要求等核心内容需要通过模拟点击才能获取完整HTML。2. 技术选型与环境准备2.1 核心工具链组合经过多次对比测试最终确定的技术方案是Selenium WebDriver处理动态页面交互实测ChromeDriver兼容性最佳WebMagic作为爬虫框架管理请求队列和管道HtmlUnit辅助快速检测页面变更但遇到验证码时仍需切换回Selenium// Maven依赖配置示例 dependency groupIdorg.seleniumhq.selenium/groupId artifactIdselenium-java/artifactId version4.1.4/version /dependency dependency groupIdus.codecraft/groupId artifactIdwebmagic-core/artifactId version0.7.3/version /dependency2.2 反爬应对策略平台常见的防护措施包括IP频次限制每个请求间隔3秒以上UserAgent验证需模拟主流浏览器标识动态参数签名观察到的TIMEBEGIN_SHOW等时间戳参数需按规则生成// 请求头伪装示例 chromeOptions.addArguments(user-agentMozilla/5.0 (Windows NT 10.0; Win64 x64)); chromeOptions.addArguments(--disable-blink-featuresAutomationControlled);3. 页面结构逆向分析3.1 省级行政区划解析通过分析页面DOM树发现省份数据直接硬编码在select idprovinceId中。但市级数据通过AJAX动态加载需要提取省级编码作为请求参数// 省份数据提取示例 ListWebElement options driver.findElements(By.cssSelector(#provinceId option)); MapString, String provinceMap options.stream() .filter(e - !e.getAttribute(value).equals(0)) .collect(Collectors.toMap( e - e.getText(), e - e.getAttribute(value) ));3.2 动态标签定位技巧招标公告通常隐藏在li idt_0101标签内需先触发点击事件才能加载内容。这里有个坑必须等待iframe完全加载后再操作否则会抛出NoSuchElementExceptionnew WebDriverWait(driver, Duration.ofSeconds(10)) .until(ExpectedConditions.frameToBeAvailableAndSwitchToIt( By.id(iframe0101) ));4. 核心采集流程实现4.1 列表页数据获取通过抓包分析找到真实数据接口dealList_find.jsp其关键参数包括DEAL_PROVINCE省级编码如山东为370000TIMEBEGIN/TIMEEND时间范围通常限制10天内PAGENUMBER分页参数// POST请求构造示例 HttpPost post new HttpPost(http://deal.ggzy.gov.cn/ds/deal/dealList_find.jsp); ListNameValuePair params new ArrayList(); params.add(new BasicNameValuePair(DEAL_PROVINCE, 370000)); params.add(new BasicNameValuePair(TIMEBEGIN, 2026-07-01)); post.setEntity(new UrlEncodedFormEntity(params, UTF-8));4.2 详情页深度提取获取到详情页URL后需要处理两个技术难点多标签页切换使用windowHandles管理浏览器标签内容iframe嵌套先定位外层div再切换到内部iframe// 多标签页处理示例 String mainWindow driver.getWindowHandle(); for (String handle : driver.getWindowHandles()) { if (!handle.equals(mainWindow)) { driver.switchTo().window(handle); break; } }5. 数据存储与优化5.1 本地化存储方案采用HTML元数据分离存储模式原始HTML保存为[项目ID].html结构化数据存入MySQL包含字段CREATE TABLE tender_data ( id VARCHAR(32) PRIMARY KEY, title TEXT, province VARCHAR(20), publish_date DATE, html_path VARCHAR(255), url_hash CHAR(64) );5.2 性能优化实践请求间隔随机化在3-5秒间随机休眠失败重试机制对502/504状态码自动重试代理IP池使用Luminati等商业服务需企业级预算// 指数退避重试示例 int retries 0; while (retries 3) { try { return httpClient.execute(post); } catch (SocketTimeoutException e) { Thread.sleep((long) Math.pow(2, retries) * 1000); retries; } }6. 完整代码结构项目采用模块化设计src/ ├── main/ │ ├── java/ │ │ ├── model/ # 数据模型 │ │ ├── parser/ # 页面解析器 │ │ ├── pipeline/ # 存储管道 │ │ └── scheduler/ # 任务调度 │ └── resources/ │ ├── proxy.list # 代理IP列表 │ └── regions.json # 行政区划配置核心处理器示例public class GGZYProcessor implements PageProcessor { private Site site Site.me() .setRetryTimes(3) .setSleepTime(3000 new Random().nextInt(2000)); Override public void process(Page page) { if (page.getUrl().regex(dealList_find).match()) { // 列表页解析逻辑 } else { // 详情页处理逻辑 page.putField(html, page.getHtml().xpath(//div[idmycontent])); } } Override public Site getSite() { return site; } }7. 常见问题解决方案动态元素加载失败采用显式等待结合JS重试机制public WebElement safeFind(By locator) { try { return new WebDriverWait(driver, 10) .until(d - d.findElement(locator)); } catch (TimeoutException e) { ((JavascriptExecutor)driver).executeScript( arguments[0].scrollIntoView(), driver.findElement(locator) ); return driver.findElement(locator); } }验证码触发通过控制浏览器窗口大小避免触发实测1920x1080最稳定driver.manage().window().setSize(new Dimension(1920, 1080));8. 法律合规要点Robots协议遵守检查/robots.txt禁止爬取的目录数据使用限制不得用于商业倒卖等违规用途访问频率控制单IP请求量不超过行业公认合理范围建议在代码中加入合规性检查public boolean checkCompliance() { try { Document doc Jsoup.connect(https://www.ggzy.gov.cn/robots.txt) .timeout(5000) .get(); return !doc.text().contains(Disallow: /ds/); } catch (IOException e) { return false; } }