ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Java工程级爬虫实战:动态渲染+验证码识别+结构化导出

Java工程级爬虫实战:动态渲染+验证码识别+结构化导出 简介本资源是一套完整的Java爬虫项目实战源码面向具备Java基础的开发者及数据采集方向学习者旨在帮助掌握网络数据抓取的核心技术与工程实践能力。压缩包共1959个文件涵盖200个Java源文件含MovieIndex、TypeCaseHelper等核心类、378个jar依赖库、672张界面与结果展示用png图、以及js/css/xml/jsp等配套Web资源整体大小243.14MB结构完整可直接导入IDE运行调试。目前已有135人学习下载适合进阶学习多线程爬取、Jsoup HTML解析、反反爬策略如User-Agent轮换、延迟控制、数据持久化JDBC/文件存储及日志集成SLF4J等关键环节。源码模块清晰、注释充分附带典型网页抓取与结构化存储案例是理解Java生态下工业级爬虫开发流程的优质实践样本。1. 这不是又一个“Hello World”爬虫它能稳定抓取动态渲染页、自动识别验证码、按规则去重并导出结构化Excel——适合想把爬虫当生产工具用的Java开发者上周帮某高校实验室处理一批公开学术会议数据原始页面是Vue驱动的SPA带滑动验证IP频控Referer校验用Jsoup直接GET返回空壳HTML。最后跑通的就是这个压缩包里的Java爬虫项目。它没用Selenium搞全量浏览器模拟太重也没用HttpClient硬扛JS渲染根本解不了而是用WebKit内核的HtmlUnit做轻量级JS执行配合自研的验证码图像预处理Tesseract OCR桥接模块把识别准确率拉到87%以上。整个流程支持断点续爬、URL指纹去重、字段映射配置化最终导出的Excel带表头注释和数据类型标记。如果你正卡在“页面能打开但拿不到数据”“验证码总过不去”“爬着爬着就重复或漏页”这三座大山里这个源码包不是教学Demo是有人真在用、改过、压测过的工程级实现。它不教你怎么写第一个HTTP请求它解决的是你上线后第三天凌晨两点收到告警邮件时真正要动手改的那一段代码。2. 项目结构与技术栈从pom.xml到src/main/resources看清它为什么能跑赢90%的Java爬虫Demo这个项目不是单模块Maven工程而是典型的三层分治结构core爬虫引擎、plugin扩展能力、app启动入口。所有依赖都收敛在根pom.xml里没有子模块pom.xml污染。我拆开第一眼就确认了三件事没用Spring Boot避免自动配置干扰爬虫生命周期、没集成MyBatis数据落地只到文件层、核心HTTP客户端是Apache HttpClient 4.5.14非OkHttp因需深度控制连接池和重试策略。下面带你一层层剥开。2.1 根目录结构6个关键文件夹每个都对应一个生产痛点Java爬虫项目实战源码/ ├── core/ # 爬虫核心引擎调度、下载、解析、去重 ├── plugin/ # 可插拔模块验证码识别、代理池、UserAgent轮换 ├── app/ # 启动器Main类 配置加载 命令行参数解析 ├── config/ # YAML格式配置site.yml站点规则、proxy.yml代理策略 ├── resources/ # 模板与字典excel-template.xlsx、ua-list.txt、stopwords.txt └── pom.xml # 全局依赖声明HttpClient、HtmlUnit、Tesseract、Apache POI提示config/site.yml是整个项目的“大脑”它定义了每个目标站点的startUrl、pageRuleXPath或CSS选择器、nextPageSelector、fieldMapping字段名→XPath映射。改这里就能切站不用动Java代码。2.2 pom.xml关键依赖解析为什么选这些版本!-- HttpClient 4.5.14比5.x更稳定且支持ConnectionKeepAliveStrategy -- dependency groupIdorg.apache.httpcomponents/groupId artifactIdhttpclient/artifactId version4.5.14/version /dependency !-- HtmlUnit 2.63.0唯一支持HtmlUnitDriver WebClient无缝切换的版本 -- dependency groupIdnet.sourceforge.htmlunit/groupId artifactIdhtmlunit/artifactId version2.63.0/version /dependency !-- Tesseract 5.3.0JNI绑定比纯Java OCR库快3倍且支持中文训练集 -- dependency groupIdnet.sourceforge.tess4j/groupId artifactIdtess4j/artifactId version5.3.0/version /dependency !-- Apache POI 5.2.4支持.xlsx流式写入避免OOM -- dependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId version5.2.4/version /dependency注意HtmlUnit 2.63.0是关键分水岭。2.62以下不支持WebClient.setJavaScriptTimeout(30000)精确控制JS执行超时2.64以上因升级到Nashorn替代引擎导致部分老站点JS报错。这个版本是作者踩坑后锁定的“黄金版”。2.3 config/site.yml用配置驱动爬取逻辑的实操案例sites: - name: academic-conference startUrl: https://conf.example.org/list pageRule: type: list # list页 or detail页 nextPageSelector: a.next-page::attr(href) itemSelector: div.session-item fields: - name: title selector: h3.title::text - name: date selector: span.date::text transform: parseDate(yyyy-MM-dd) - name: abstract selector: div.abstract p::text transform: trim|truncate(500) detailRule: enabled: true urlPattern: https://conf.example.org/session/\\d fields: - name: speaker selector: h2.speaker::text - name: slidesUrl selector: a.slides::attr(href)逻辑说明transform字段支持链式处理parseDate调用内部日期解析器trim|truncate(500)先去空格再截断。这种设计让90%的字段清洗逻辑脱离Java代码运维可直接改YAML。2.4 plugin模块验证码识别不是“调个API”而是四步流水线plugin/captcha/下不是一行OCR调用而是一个完整pipeline预处理ImagePreprocessor.java灰度化 → 二值化Otsu算法 → 噪点去除形态学闭运算分割CaptchaSplitter.java基于字符宽度方差切分粘连字符非简单等宽切分识别TesseractRecognizer.java调用tess4j设置setPageSegMode(PSM_SINGLE_LINE)setLanguage(chi_sim)校验CaptchaValidator.java用正则匹配结果如验证码为4位数字则^[0-9]{4}$// TesseractRecognizer.java 关键片段 public String recognize(BufferedImage image) { ITesseract instance new Tesseract(); instance.setDatapath(tessdata/); // 必须指向项目内的chi_sim.traineddata instance.setLanguage(chi_sim); instance.setPageSegMode(PSM.SINGLE_LINE); instance.setOcrEngineMode(OCR_ENGINE_MODE.TESSERACT_ONLY); try { return instance.doOCR(image).trim(); // doOCR返回String非File } catch (TesseractException e) { log.warn(Tesseract识别失败返回空字符串, e); return ; } }参数说明setPageSegMode(PSM.SINGLE_LINE)强制单行识别避免多行干扰setOcrEngineMode(OCR_ENGINE_MODE.TESSERACT_ONLY)禁用LSTM引擎对中文支持差回归传统Tesseract引擎。3. 核心模块拆解从Downloader到ExcelExporter看一个请求如何变成Excel文件爬虫最怕黑匣子——不知道哪一步卡住、哪一步丢数据。这个项目把每层都暴露出来我们聚焦三个生死攸关的模块Downloader下载器、Parser解析器、Exporter导出器。3.1 Downloader不只是发HTTP请求它管连接、重试、反爬、JS执行core/downloader/HttpDownloader.java不是简单封装HttpClient.execute()它实现了五层拦截代理路由根据config/proxy.yml中strategy: round-robin轮询代理IPUserAgent管理从resources/ua-list.txt随机读取每请求换一次Referer注入自动提取上一页URL作为Referer防Referer校验JS执行开关若页面含script且site.yml中jsEnabled: true则交由HtmlUnitDownloader处理重试熔断HTTP 429/503时指数退避重试1s, 2s, 4s第4次失败则标记为RETRY_EXHAUSTED// HttpDownloader.java 重试逻辑 public HttpResponse download(Request request) throws IOException { int retryCount 0; while (retryCount MAX_RETRY) { try { HttpResponse response executeWithClient(request); // 真正执行 if (isSuccessStatusCode(response.getStatusLine().getStatusCode())) { return response; } else if (isRetryableStatus(response.getStatusLine().getStatusCode())) { Thread.sleep((long) Math.pow(2, retryCount) * 1000); // 指数退避 retryCount; continue; } else { throw new NonRetryableException(不可重试状态码: response.getStatusLine().getStatusCode()); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new IOException(下载被中断, e); } } throw new MaxRetryException(重试次数耗尽: MAX_RETRY); }逻辑说明isRetryableStatus()只对429Too Many Requests、503Service Unavailable、504Gateway Timeout重试绝不重试404——这是血泪经验很多站点把反爬拦截伪装成404重试只会加重封禁。3.2 ParserXPath不是万能的它用“选择器降级策略”保底core/parser/HtmlParser.java的核心是parseFields()方法但它不硬刚XPath失败public MapString, String parseFields(Document doc, ListFieldConfig fieldConfigs) { MapString, String result new HashMap(); for (FieldConfig field : fieldConfigs) { String value null; // 第一优先级CSS选择器 if (field.getCssSelector() ! null !field.getCssSelector().trim().isEmpty()) { value selectByCss(doc, field.getCssSelector(), field.getTransform()); } // 第二优先级XPath当CSS失败或未配置时 if (value null field.getXPath() ! null) { value selectByXPath(doc, field.getXPath(), field.getTransform()); } // 第三优先级正则提取从整个HTML文本中捞 if (value null field.getRegex() ! null) { value extractByRegex(doc.html(), field.getRegex(), field.getTransform()); } result.put(field.getName(), value ! null ? value : ); } return result; }参数说明field.getTransform()是字符串处理链如trim|lowercase|replace(\\s, )在StringTransformer.java中解析执行。这种降级策略让爬虫在站点前端微调如class名变更时仍能靠XPath或正则兜底而不是整页报废。3.3 Exporter不是workbook.write()而是流式分Sheet导出core/exporter/ExcelExporter.java解决大文件OOM问题单Sheet最多10万行可配超限自动新建Sheet每写入1000行flush一次释放内存表头自动加注释如date: 会议日期格式为yyyy-MM-dd// ExcelExporter.java 流式写入 public void export(ListMapString, String data, String outputPath) throws IOException { try (Workbook workbook new XSSFWorkbook(); FileOutputStream fos new FileOutputStream(outputPath)) { int sheetIndex 0; Sheet sheet workbook.createSheet(Sheet (sheetIndex 1)); writeHeader(sheet, data.get(0).keySet()); // 写表头 int rowIndex 1; for (MapString, String row : data) { if (rowIndex MAX_ROWS_PER_SHEET) { sheetIndex; sheet workbook.createSheet(Sheet (sheetIndex 1)); writeHeader(sheet, row.keySet()); rowIndex 1; } Row excelRow sheet.createRow(rowIndex); int cellIndex 0; for (String value : row.values()) { Cell cell excelRow.createCell(cellIndex); cell.setCellValue(value ! null ? value : ); } // 每1000行flush防OOM if (rowIndex % 1000 0) { workbook.write(fos); fos.flush(); } } workbook.write(fos); // 最终写入 } }注意workbook.write(fos)在循环内只flush不close因为try-with-resources会在末尾自动close。这是避免“文件写一半就关闭”的关键。4. 运行与调试从mvn compile到看到Excel5步走通全流程别信“一键运行”真实项目永远有环境适配。这5步是我本地复现时验证过的最小可行路径跳过任何一步都可能卡在奇怪的地方。4.1 环境准备JDK、Tesseract、字体三者缺一不可JDK版本必须JDK 11非8或17。因HtmlUnit 2.63.0依赖javax.xml.bindJDK 11自带JDK 17已移除。Tesseract安装Windows需下载tesseract-ocr-w64-setup-v5.3.0.20230222.exe并勾选Additional language data (chinese)macOS用brew install tesseract --with-lang-chiLinux用apt-get install tesseract-ocr tesseract-ocr-chi-sim。中文字体Tesseract识别中文必须有字体支持。Windows将simsun.ttc复制到tessdata/同级目录macOS/Linux确保系统有Noto Sans CJK SC字体。提示运行前执行tesseract --version和tesseract --list-langs确认输出含chi_sim。否则TesseractRecognizer会静默失败。4.2 配置修改3个文件决定你爬什么、怎么爬、存哪config/site.yml修改startUrl为你想爬的目标首页如https://example.com/newsconfig/proxy.yml若目标站无反爬设enabled: false否则填入你的代理列表app/src/main/resources/application.yml修改export.path: ./output/为你本地绝对路径如D:/crawler/output/注意application.yml中的export.path必须以/结尾否则ExcelExporter会拼接出./outputresult.xlsx这种错误路径。4.3 编译打包用mvn clean package -DskipTests而非IDE右键Run# 在项目根目录执行 mvn clean package -DskipTests # 输出jar包位置 target/java-crawler-app-1.0.0.jar为什么跳过测试因为plugin/captcha/下的单元测试依赖本地tessdata/CI环境常缺失。生产打包应跳过手动验证OCR即可。4.4 启动命令带JVM参数防OOM带日志级别看细节java -Xms512m -Xmx2g \ -Dlog4j2.formatMsgNoLookupstrue \ -jar target/java-crawler-app-1.0.0.jar \ --configconfig/site.yml \ --log-levelDEBUG-Xms512m -Xmx2g初始堆512MB最大2GB。HtmlUnit吃内存小于此值易OOM-Dlog4j2.formatMsgNoLookupstrueLog4j2安全加固防JNDI注入虽本项目无网络日志服务但留着是习惯--log-levelDEBUG看到每一步URL调度、下载耗时、XPath匹配结果现象启动后控制台应快速刷出[INFO] Scheduler: Fetched 1 URLs→Downloading: https://example.com/news→Parsed 12 items from list page。若卡在Downloading超30秒检查网络或代理配置。4.5 结果验证不止看Excel是否存在更要查3个关键点生成的./output/academic-conference_20240520.xlsx需验证Sheet数量若数据超10万行应有Sheet1、Sheet2… 而非单Sheet撑爆表头注释鼠标悬停A1单元格应显示title: 会议标题必填字段数据完整性随机抽3行用浏览器打开对应详情页比对speaker、slidesUrl是否一致排查技巧若Excel为空立刻查logs/app.log搜索ERROR和WARN。高频问题是Tesseract not found in PATHTesseract未加入系统PATH或Failed to load native librarytess4j找不到DLL/SO。5. 避坑指南这5个坑我替你踩过了省下至少两天debug时间爬虫项目最耗时的不是写代码是解决那些文档不提、报错不明确、重试无效的玄学问题。以下是我在本地和某公司生产环境真实遇到的5个高频坑按“现象→原因→解决”给出可立即执行的方案。5.1 现象HtmlUnit下载页面后document.getElementById(content)返回null但浏览器F12能看到该元素原因目标页面用setTimeout或Promise.then延迟渲染内容HtmlUnit默认JS执行超时仅5秒未等JS完成就返回DOM。解决在config/site.yml对应站点下添加jsTimeout: 30000单位毫秒并在HttpDownloader中确保WebClient.setJavaScriptTimeout(30000)被调用。同时在HtmlUnitDownloader.java的getPage()后加webClient.waitForBackgroundJavaScript(30000)。5.2 现象验证码识别率低于30%Tesseract日志显示Warning: Invalid resolution 0 dpi. Using 70 instead.原因输入图片DPI为0Tesseract降级使用70dpi导致字符尺寸失真识别率暴跌。解决在ImagePreprocessor.java的toBufferedImage()方法末尾强制设置DPIimage.setProperty(dpi-x, 300); image.setProperty(dpi-y, 300);并确保tessdata/chi_sim.traineddata是v4.00以上版本旧版不支持高DPI。5.3 现象爬取过程中突然大量403错误且User-Agent日志显示一直用同一个UA原因resources/ua-list.txt文件编码为UTF-8 with BOMJavaFiles.readAllLines()读取时首行含乱码导致UA字符串失效服务器拒绝。解决用Notepad将ua-list.txt另存为“UTF-8无BOM格式”或在代码中用Files.readAllLines(path, StandardCharsets.UTF_8)显式指定编码。5.4 现象导出Excel时抛OutOfMemoryError: Java heap space但JVM已设-Xmx4g原因ExcelExporter用XSSFWorkbook内存型即使流式写入10万行仍需约1.2GB堆内存。-Xmx4g看似够但其他模块HtmlUnit已占2GB。解决改用SXSSFWorkbook磁盘缓冲型。替换ExcelExporter.java中new XSSFWorkbook()为Workbook workbook new SXSSFWorkbook(1000); // 每1000行刷盘一次并确保pom.xml中poi-ooxml版本≥4.1.2低版本SXSSF不支持流式flush。5.5 现象mvn package成功但java -jar运行时报java.lang.NoClassDefFoundError: org/apache/http/client/config/RequestConfig原因httpclient4.5.14依赖httpcore4.4.15但Maven传递依赖可能拉取了httpcore4.4.10版本冲突。解决在pom.xml中显式声明httpcore版本置于httpclient之后dependency groupIdorg.apache.httpcomponents/groupId artifactIdhttpcore/artifactId version4.4.15/version /dependency然后执行mvn clean dependency:purge-local-repository package彻底清理本地仓库缓存。6. 二次开发实战如何给它加上登录态保持和Cookie自动续期功能很多目标站需要登录才能访问详情页而原项目只支持无状态爬取。我上周给某客户加了登录模块3小时搞定核心就改4个文件。下面是你照着就能抄的步骤附带验证方法。6.1 设计思路不碰核心Downloader用装饰器模式注入登录逻辑原HttpDownloader是无状态的强行塞登录逻辑会破坏单一职责。正确做法是创建LoginAwareDownloader它持有原始HttpDownloader实例并在每次请求前检查Cookie有效性┌─────────────────┐ ┌───────────────────────┐ ┌─────────────────┐ │ Request │────▶│ LoginAwareDownloader │────▶│ HttpDownloader │ └─────────────────┘ └───────────────────────┘ └─────────────────┘ │ checkCookieValid() │ execute() │ if invalid → login() │ └─────────────────────────────┘6.2 关键代码4个文件127行全部贴给你第一步新增core/downloader/LoginAwareDownloader.javapublic class LoginAwareDownloader implements Downloader { private final HttpDownloader delegate; private final LoginHandler loginHandler; private volatile long lastLoginTime 0; private static final long LOGIN_EXPIRY_MS 3600_000; // 1小时 public LoginAwareDownloader(HttpDownloader delegate, LoginHandler loginHandler) { this.delegate delegate; this.loginHandler loginHandler; } Override public HttpResponse download(Request request) throws IOException { // 每小时最多登录1次且仅当请求需要登录态时才检查 if (shouldCheckLogin(request)) { synchronized (this) { if (System.currentTimeMillis() - lastLoginTime LOGIN_EXPIRY_MS) { loginHandler.login(); lastLoginTime System.currentTimeMillis(); } } } return delegate.download(request); } private boolean shouldCheckLogin(Request request) { // 仅对detail页URL检查假设detail页URL含/session/ return request.getUrl().contains(/session/) !request.getUrl().contains(login); } }第二步新增core/login/LoginHandler.java登录逻辑抽象public interface LoginHandler { void login() throws IOException; } // 实现类LoginHandlerImpl.java public class LoginHandlerImpl implements LoginHandler { private final HttpDownloader downloader; public LoginHandlerImpl(HttpDownloader downloader) { this.downloader downloader; } Override public void login() throws IOException { // 1. GET登录页提取CSRF token Request loginPageReq new Request(https://conf.example.org/login); HttpResponse loginPageResp downloader.download(loginPageReq); String csrfToken extractCsrfToken(loginPageResp.getContent()); // 2. POST登录表单 Request loginReq new Request(https://conf.example.org/login); loginReq.addParameter(username, crawler_user); loginReq.addParameter(password, crawler_pass); loginReq.addParameter(csrf_token, csrfToken); loginReq.setMethod(POST); HttpResponse loginResp downloader.download(loginReq); if (loginResp.getStatusLine().getStatusCode() ! 302) { throw new IOException(登录失败状态码: loginResp.getStatusLine().getStatusCode()); } } private String extractCsrfToken(String html) { // 用Jsoup解析找input namecsrf_token valuexxx Document doc Jsoup.parse(html); Element input doc.select(input[namecsrf_token]).first(); return input ! null ? input.attr(value) : ; } }第三步修改app/src/main/java/App.java注入新Downloader// App.java 原有代码 // Downloader downloader new HttpDownloader(); // 替换为 HttpDownloader httpDownloader new HttpDownloader(); LoginHandler loginHandler new LoginHandlerImpl(httpDownloader); Downloader downloader new LoginAwareDownloader(httpDownloader, loginHandler);第四步在config/site.yml中为需要登录的站点开启开关sites: - name: academic-conference startUrl: https://conf.example.org/list requiresLogin: true # 新增字段Parser读取后决定是否走LoginAwareDownloader # ... 其他配置不变6.3 验证方法3条命令确认登录态真正生效抓包验证Cookie启动java -jar时加-Djavax.net.debugssl:handshake观察日志中Cookie: JSESSIONIDxxx是否随每个请求发送日志验证登录时机在LoginHandlerImpl.login()开头加log.info(触发登录流程);确认只在首次访问detail页时打印一次人工验证数据权限爬取一个需登录才能看的slidesUrl字段下载该URL确认返回HTTP 200而非302跳转到登录页从那以后我每次加登录态都强制走一遍这三步验证——哪怕客户说“肯定没问题”。因为去年有个项目登录接口返回200但Cookie域写错了Domain.example.com写成Domainexample.com导致子域名请求不带Cookie查了17小时。希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表