尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java Selenium爬虫实战:Chromedriver 118版本匹配与反爬工程化

Java Selenium爬虫实战:Chromedriver 118版本匹配与反爬工程化 简介这是一套面向Java爬虫初学者与自动化测试入门者的Selenium实战资源围绕Chrome 118.0.5958.0测试版环境搭建与Java爬虫开发展开帮助读者解决浏览器与驱动版本不匹配、环境配置繁琐等常见问题。资源包共56个文件约706.55MB包含9个java源码与9个class文件构成的核心爬虫工程、12个js与2个html/css组成的页面资源、2个properties配置及pom.xml构建文件另附Selenium学习笔记、演示视频和覆盖Windows、Linux、Mac多平台的Chrome与Chromedriver压缩包。已有121人学习下载。读者可获得可直接运行的Java爬虫实战代码、分模块的学习笔记、完整操作录屏以及免去版本适配烦恼的浏览器与驱动组合适合边看边练、快速上手Selenium自动化与爬虫开发。1. 版本锁死这件事为什么在 Selenium 爬虫里是生死线很多人第一次写 Selenium 爬虫代码跑不起来报的错五花八门session not created、This version of ChromeDriver only supports Chrome version XX、浏览器一闪就退。折腾半天最后发现根本不是代码问题而是谷歌浏览器和 Chromedriver 版本对不上。这个标题里特意把版本号 118.0.5958.0 写出来就是在提醒一件事Selenium 爬虫的稳定性一半靠代码一半靠环境对齐。这篇讲的是用 Java 写 Selenium 爬虫的完整落地路径从环境搭建、驱动匹配、页面元素定位到反爬对抗和工程化封装。适合两类人一是 Java 后端想快速搞一套可控的采集工具二是从 Python 爬虫转过来、发现 Java 生态在多线程和工程管理上更顺手的同学。核心不是教你 Selenium 的 API 怎么调而是把版本匹配、驱动管理、元素定位、异常处理这几件真正会翻车的事讲透。2. 环境搭建JDK、Maven 与 Chromedriver 118 的版本对齐2.1 为什么 Chromedriver 版本必须和 Chrome 严格对应Chromedriver 本质是一个实现了 WebDriver 协议的服务端它通过 DevTools 协议和 Chrome 浏览器通信。每次 Chrome 大版本更新DevTools 协议可能有破坏性变更所以 Chromedriver 的版本号前三位必须和 Chrome 完全一致。118.0.5958.0 这个版本意味着你的 Chrome 主版本必须是 118Chromedriver 也必须是 118 系列。常见做法是去 Chromedriver 的版本索引页找到对应大版本下的最新小版本。注意Chromedriver 只保证大版本匹配小版本号不需要完全一致但为了减少玄学问题我一般会尽量对齐到标题里这个具体版本。提示Chrome 浏览器设置里「关于 Chrome」可以看到完整版本号。如果显示 118.0.5958.0那就下 118 系列的 Chromedriver不要用 119 或 117。2.2 Maven 依赖与驱动路径配置Java 项目用 Maven 管理依赖是最省事的。Selenium 的 Java 包在 Maven 中央仓库有直接写 pom 就行。dependencies !-- Selenium Java 核心包4.x 版本内置了 Selenium Manager但国内网络环境下建议手动管理驱动 -- dependency groupIdorg.seleniumhq.selenium/groupId artifactIdselenium-java/artifactId version4.15.0/version /dependency !-- 如果要做页面解析jsoup 比直接用 Selenium 的 getText 更灵活 -- dependency groupIdorg.jsoup/groupId artifactIdjsoup/artifactId version1.16.1/version /dependency /dependenciesSelenium 4.x 引入了 Selenium Manager理论上可以自动下载匹配的驱动。但实际用下来国内网络环境下自动下载经常超时而且它下载的版本不一定是你想要的 118。所以我一般会手动指定驱动路径。import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.chrome.ChromeOptions; public class DriverFactory { public static WebDriver createDriver() { // 手动指定 Chromedriver 可执行文件的绝对路径 System.setProperty(webdriver.chrome.driver, /opt/drivers/chromedriver-118.0.5958.0/chromedriver); ChromeOptions options new ChromeOptions(); // 无头模式服务器上跑必须加 options.addArguments(--headlessnew); // 禁用 GPU容器环境里不加会报错 options.addArguments(--disable-gpu); // 禁用沙箱Docker 里跑必须加 options.addArguments(--no-sandbox); // 设置窗口大小避免响应式布局导致元素定位失败 options.addArguments(--window-size1920,1080); // 禁用自动化特征降低被检测概率 options.addArguments(--disable-blink-featuresAutomationControlled); return new ChromeDriver(options); } }这段代码里几个参数值得展开说。--headlessnew是 Chrome 112 之后的新无头模式比老的--headless更接近真实浏览器行为很多老教程还在用旧写法会导致部分页面渲染异常。--disable-blink-featuresAutomationControlled是去掉navigator.webdriver这个自动化标志的关键参数不加的话很多网站直接识别你是机器人。--no-sandbox在 Docker 里不加会直接启动失败报DevToolsActivePort file doesnt exist这个坑我踩过不止一次。2.3 验证环境是否跑通的最小命令写完工厂类先别急着写业务逻辑跑一个最小验证。public class SmokeTest { public static void main(String[] args) { WebDriver driver DriverFactory.createDriver(); try { driver.get(https://www.example.com); // 打印页面标题确认浏览器真的加载了页面 System.out.println(Page title: driver.getTitle()); // 打印当前 URL确认没有跳转到错误页 System.out.println(Current URL: driver.getCurrentUrl()); } finally { // 必须关闭否则进程残留下次启动端口被占用 driver.quit(); } } }如果这一步能打印出标题和 URL说明 JDK、Maven、Chromedriver、Chrome 四者版本对齐了。如果报session not created九成是版本不匹配如果报connection refused检查 Chromedriver 路径是否写对如果浏览器启动后立刻退出加--no-sandbox和--disable-dev-shm-usage。3. 页面元素定位从 By 选择器到显式等待的工程化写法3.1 八种定位方式的适用场景与优先级Selenium 提供了八种By定位方式但实际项目里常用的就四种By.id、By.cssSelector、By.xpath、By.className。优先级我一般是id cssSelector xpath className。id 最稳但现代前端框架生成的 id 往往是动态的比如idapp-3f2a1b这种就不能用。cssSelector 性能最好语法也够用div.content ul.list li.item这种层级选择很直观。xpath 功能最强支持文本匹配和轴定位但性能差一些而且页面结构一变就容易断。className 最不稳定因为一个元素经常有多个 class而且 class 名可能被压缩混淆。// id 定位最优先 WebElement searchBox driver.findElement(By.id(search-input)); // cssSelector 定位次优先支持层级和属性 WebElement submitBtn driver.findElement( By.cssSelector(form#search-form button[typesubmit])); // xpath 文本定位适合按钮文字固定的场景 WebElement loginLink driver.findElement( By.xpath(//a[contains(text(),登录)])); // xpath 轴定位适合表格里找特定行 WebElement targetCell driver.findElement( By.xpath(//tr[td[text()张三]]/td[3]));xpath 的contains(text(),登录)比text()登录更实用因为按钮文字前后可能有空格或换行。轴定位//tr[td[text()张三]]/td[3]这种写法在抓表格数据时特别有用意思是「找到包含张三这个单元格的行然后取该行第三个单元格」。3.2 显式等待为什么 sleep 是万恶之源新手最爱用Thread.sleep(3000)觉得等三秒页面肯定加载完了。实际项目中这个三秒要么不够导致元素找不到要么太长导致整体速度慢得无法接受。Selenium 提供了WebDriverWait做显式等待条件满足就立刻继续不满足才等到超时。import org.openqa.selenium.support.ui.WebDriverWait; import org.openqa.selenium.support.ui.ExpectedConditions; import java.time.Duration; // 创建等待对象最长等 10 秒每 500 毫秒轮询一次 WebDriverWait wait new WebDriverWait(driver, Duration.ofSeconds(10)); wait.pollingEvery(Duration.ofMillis(500)); // 忽略找不到元素的异常继续轮询 wait.ignoring(NoSuchElementException.class); // 等待元素可点击适合按钮 WebElement btn wait.until( ExpectedConditions.elementToBeClickable(By.id(submit))); // 等待元素可见适合文本内容 WebElement content wait.until( ExpectedConditions.visibilityOfElementLocated( By.cssSelector(div.result-list))); // 等待 URL 包含特定字符串适合页面跳转 wait.until(ExpectedConditions.urlContains(/search?q));elementToBeClickable比visibilityOfElementLocated更严格它要求元素既可见又可交互。如果按钮被遮罩层挡住前者会继续等后者可能直接返回但点击失败。pollingEvery默认是 500 毫秒一般不用改改太小会增加浏览器压力改太大响应变慢。注意显式等待和隐式等待不要混用。隐式等待是全局的显式等待是局部的两者叠加会导致实际等待时间不可预测出现「明明设了 10 秒却等了 30 秒」的玄学问题。3.3 用 Page Object 模式封装页面操作当爬虫要操作的页面超过三个代码里到处是findElement会变得难以维护。Page Object 模式把每个页面封装成一个类元素定位和操作都放在类里业务代码只调方法。public class SearchPage { private final WebDriver driver; private final WebDriverWait wait; // 元素定位用 By 对象存起来页面结构变了只改这里 private final By searchInput By.id(search-input); private final By searchButton By.cssSelector(button.search-btn); private final By resultItems By.cssSelector(div.result-item); public SearchPage(WebDriver driver) { this.driver driver; this.wait new WebDriverWait(driver, Duration.ofSeconds(10)); } public void search(String keyword) { WebElement input wait.until( ExpectedConditions.elementToBeClickable(searchInput)); // clear 防止输入框有默认值 input.clear(); input.sendKeys(keyword); driver.findElement(searchButton).click(); } public ListString getResults() { // 等待结果列表出现 wait.until(ExpectedConditions.visibilityOfElementLocated(resultItems)); return driver.findElements(resultItems) .stream() .map(WebElement::getText) .collect(Collectors.toList()); } }这样业务代码就变成new SearchPage(driver).search(关键词)页面改版时只需要改 SearchPage 里的 By 对象。clear()这一步很多人会漏如果输入框有 placeholder 或者上次搜索的残留值不 clear 会导致搜索词拼接错误。4. 反爬对抗从 webdriver 检测到请求头指纹的排查清单4.1 现象页面能打开但数据是空的这是最典型的反爬表现。浏览器正常加载了页面但关键数据区域是空白或者返回一个验证码页面。原因通常是网站检测到了navigator.webdriver true直接返回了阉割版页面。解决分三步。第一步加--disable-blink-featuresAutomationControlled启动参数。第二步用 CDP 注入脚本覆盖 webdriver 属性。import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.devtools.DevTools; import org.openqa.selenium.devtools.v120.page.Page; ChromeDriver driver new ChromeDriver(options); DevTools devTools driver.getDevTools(); devTools.createSession(); // 在页面加载前注入脚本覆盖 navigator.webdriver devTools.send(Page.addScriptToEvaluateOnNewDocument( Object.defineProperty(navigator, webdriver, {get: () undefined}) ));第三步检查 User-Agent。无头模式的 UA 里会带HeadlessChrome必须手动覆盖。options.addArguments(--user-agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36);4.2 现象请求频率一高就被封 IPSelenium 启动的浏览器每次都是新会话但出口 IP 是同一个。如果目标网站有频率限制连续请求几十次后就会返回 403 或验证码。常见做法是加随机延迟模拟人类操作节奏。// 每次操作之间随机等待 1 到 3 秒 private void randomDelay() { try { long delay 1000 (long)(Math.random() * 2000); Thread.sleep(delay); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }但随机延迟只能缓解不能根治。如果目标网站风控严格需要考虑用代理池。代理池的搭建涉及 IP 轮换和健康检查这里不展开核心思路是每次创建 Driver 时从池里取一个可用代理通过--proxy-server参数传入。4.3 现象Chromedriver 进程残留导致内存暴涨长时间运行的爬虫如果driver.quit()没有在 finally 里调用Chromedriver 和 Chrome 进程会一直残留。跑几个小时下来服务器内存被吃满。WebDriver driver null; try { driver DriverFactory.createDriver(); // 业务逻辑 } catch (Exception e) { // 记录日志不要吞异常 logger.error(Crawl failed, e); } finally { if (driver ! null) { try { driver.quit(); } catch (Exception e) { logger.warn(Failed to quit driver, e); } } }quit()和close()的区别close()只关闭当前窗口quit()关闭整个浏览器并结束 Chromedriver 进程。爬虫里必须用quit()。另外在 Linux 上可以用pkill -f chromedriver做兜底清理但这是最后手段正常流程还是靠 finally 保证。5. 工程化收尾多线程采集与异常重试的落地参数5.1 用 ThreadPoolExecutor 控制并发数单线程 Selenium 爬虫速度受限于页面加载时间一个页面平均 2 到 3 秒一天跑不了多少数据。多线程是必须的但线程数不是越多越好。每个 Chrome 实例占 200 到 400 MB 内存开太多会 OOM。// 根据服务器内存定线程数8G 内存一般开 4 到 6 个 int threadCount 5; ExecutorService executor new ThreadPoolExecutor( threadCount, threadCount, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(100), // 线程工厂给线程起名字方便排查 new ThreadFactoryBuilder().setNameFormat(crawler-%d).build(), // 队列满了由调用线程执行不丢任务 new ThreadPoolExecutor.CallerRunsPolicy() );每个线程独立创建 Driver不要共享。Selenium 的 WebDriver 不是线程安全的共享会导致命令错乱。CallerRunsPolicy是当队列满时让提交任务的线程自己执行这样不会丢任务但会阻塞提交相当于自动降速。5.2 失败重试哪些异常该重试哪些不该不是所有异常都值得重试。NoSuchElementException可能是页面还没加载完重试有意义。SessionNotFoundException说明浏览器已经崩了重试也没用应该重建 Driver。TimeoutException看情况如果是网络慢可以重试如果是元素真的不存在就是浪费。public T T retry(CallableT task, int maxRetries) { int attempt 0; while (true) { try { return task.call(); } catch (NoSuchElementException | TimeoutException e) { attempt; if (attempt maxRetries) { throw new RuntimeException(Retry exhausted, e); } // 指数退避第一次等 1 秒第二次 2 秒第三次 4 秒 long backoff (long) Math.pow(2, attempt - 1) * 1000; try { Thread.sleep(backoff); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw new RuntimeException(Interrupted, ie); } } catch (Exception e) { // 其他异常直接抛出不重试 throw new RuntimeException(e); } } }指数退避比固定间隔更合理因为如果是服务端限流连续快速重试只会加重封禁。第一次等 1 秒第二次 2 秒第三次 4 秒给服务端足够的恢复时间。5.3 数据落库前的去重与字段校验采集到的数据在入库前必须做两件事去重和字段校验。去重可以用 URL 的 MD5 做唯一索引字段校验检查必填项是否为空。// 用 URL 的 MD5 作为唯一键避免重复插入 String urlHash DigestUtils.md5Hex(url); // 字段校验空值直接跳过 if (StringUtils.isBlank(title) || StringUtils.isBlank(content)) { logger.warn(Skip empty record, url{}, url); return; }数据库层面加唯一索引UNIQUE KEY uk_url_hash (url_hash)插入时用INSERT IGNORE或ON DUPLICATE KEY UPDATE。这样即使多线程同时插入同一条数据也不会产生重复记录。6. 版本升级时怎么不翻车Chromedriver 118 的兼容性验证清单Chrome 自动更新是爬虫最大的敌人。某天早上起来Chrome 悄悄升到了 119Chromedriver 还是 118所有爬虫全部报session not created。这种事我经历过三次后来总结了一套升级验证流程。第一步关闭 Chrome 自动更新。Windows 上改注册表Linux 上锁定版本。但这不是长久之计安全补丁还是要打。第二步建立版本映射表把 Chrome 版本、Chromedriver 版本、Selenium 版本、验证状态记录在案。Chrome 版本Chromedriver 版本Selenium 版本验证状态备注118.0.5958.0118.0.5958.04.15.0通过当前生产版本119.0.6045.0119.0.6045.04.15.0待验证测试环境117.0.5938.0117.0.5938.04.14.0通过旧版本备份第三步升级前跑回归测试。不要只测一个页面要把所有目标网站的采集脚本都跑一遍。重点看三类页面需要登录的、有懒加载的、有验证码的。这三类最容易受版本影响。第四步准备回滚方案。Chromedriver 是绿色文件直接替换就行。Chrome 降级麻烦一些Linux 上用apt install chromium版本号可以指定版本Windows 上需要卸载后装离线包。所以升级前一定要把旧版本的安装包留着这就是后悔药。提示如果用的是 Docker把 Chrome 和 Chromedriver 打包进镜像版本就锁死了。每次升级重新构建镜像回滚就是换回旧镜像比在宿主机上折腾省心得多。我现在的习惯是每次 Chrome 大版本更新后先在一台测试机上跑一周确认所有目标网站都能正常采集再更新生产环境。升级当天不跑重要任务留出回滚窗口。这套流程看起来麻烦但比半夜被报警叫起来修爬虫强得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表