☰
Java爬虫项目实战源码解析:从架构到二次开发
2026/10/26 21:16:01 网站建设 项目流程

简介:这份Java爬虫项目实战源码面向具备一定Java基础、希望系统掌握网络数据抓取技术的开发者,可作为数据分析与信息自动化的入门实战素材。压缩包共1959个文件,约243.14MB,以png截图、jar依赖包、class与java源码为主,辅以js、css、xml、jsp等前端与配置资源,以及properties、sql、xls等数据与说明文件,完整呈现了一个可运行的爬虫工程结构。项目覆盖HTTP请求发送、Jsoup解析HTML、正则表达式提取、多线程与异步处理、数据落库存储、日志记录及反爬策略等关键环节,并涉及WebMagic类框架与JUnit测试思路,便于读者对照源码理解各模块协作方式。目前已有136人学习,适合想从零搭建爬虫项目或补齐工程实践细节的开发者参考借鉴。

1. 从一份能跑起来的 Java 爬虫源码说起

前阵子接手一个数据采集需求,对方丢过来一个压缩包,说“里面是 Java 爬虫项目实战源码,你看看能不能直接跑”。打开一看,结构完整、分层清晰,不是那种只贴几段 Jsoup 示例的碎片代码,而是一个能独立启动、能配置目标站点、能把抓取结果落库的完整工程。这类源码对两类人特别友好:一类是刚学完 Java 基础、想找一个真实项目练手的开发者,另一类是手头有采集任务、不想从零搭框架的工程同学。它解决的核心问题很实在——把“发请求、解析页面、翻页、去重、存数据”这条链路一次性搭好,你只需要改目标地址和解析规则。下面我就按自己拆项目的顺序,把这个 Java 爬虫源码包的架构、跑法和改造点讲清楚,关键词绕不开 java 爬虫、源代码、软件工程这几件事。

2. 项目定位与技术选型:为什么是这套组合

2.1 这个爬虫项目到底在抓什么

从源码的组织方式看,这不是一个针对单一网站的硬编码脚本,而是一个带配置层的通用采集骨架。它的典型工作流是:读取配置里的起始 URL 和分页规则,用 HTTP 客户端拉取页面,交给 HTML 解析器抽取字段,经过清洗和去重后写入存储层,同时记录抓取日志和失败重试。项目里能看到明显的分层痕迹——请求层、解析层、存储层、调度层各自独立,配置和代码分离。这种结构的好处是换一个目标站点时,大部分改动集中在配置文件和解析规则上,不用动核心流程。对想拿它做二次开发的人来说,这一点比“能抓某个具体网站”更有价值,因为真实需求里目标站点几乎一定会变。

2.2 技术栈拆解:HttpClient + Jsoup + 连接池

这类 Java 爬虫项目实战源码,常见的技术组合是 Apache HttpClient 负责网络请求、Jsoup 负责 HTML 解析、连接池管理复用、JDBC 或轻量 ORM 负责落库。下面这段是我从源码里提炼出的请求层典型写法,保留了关键结构:

// 请求层:基于连接池的 HttpClient 封装 public class HttpFetcher { private final CloseableHttpClient client; public HttpFetcher() { // 连接池配置:并发抓取时避免频繁建连 PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(); cm.setMaxTotal(50); // 总连接数上限 cm.setDefaultMaxPerRoute(10); // 单路由并发上限 this.client = HttpClients.custom() .setConnectionManager(cm) .setDefaultRequestConfig(RequestConfig.custom() .setConnectTimeout(5000) // 建连超时 .setSocketTimeout(10000) // 读超时 .build()) .build(); } public String fetch(String url) throws IOException { HttpGet get = new HttpGet(url); // 伪装常见浏览器 UA,降低被直接拦截的概率 get.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"); try (CloseableHttpResponse resp = client.execute(get)) { return EntityUtils.toString(resp.getEntity(), "UTF-8"); } } }

这段代码里几个参数值得说清楚:setMaxTotal控制整个客户端的连接总数,抓取并发高时调大,但别超过目标站点能承受的范围;setDefaultMaxPerRoute限制对同一域名的并发,这是礼貌抓取的基本约束;两个超时参数是血泪经验,不设的话遇到慢响应线程会一直挂着。User-Agent那行不是玄学,很多站点对默认的 Java UA 直接返回空内容或 403,换成常见浏览器标识能省掉一批莫名其妙的失败。解析层则通常用 Jsoup 的选择器语法,把 CSS 选择器写在配置里,代码只负责执行,这样改规则不用重新编译。

2.3 目录结构:一眼看清模块边界

源码包的目录大致遵循 Maven 标准布局,核心模块可以这样理解:

目录/文件职责
src/main/java/.../fetcherHTTP 请求封装、连接池、重试
src/main/java/.../parserJsoup 解析、字段抽取、清洗
src/main/java/.../pipeline去重、存储、日志落盘
src/main/java/.../scheduler任务调度、分页推进、并发控制
src/main/resources/config目标站点、选择器、线程数等配置
pom.xml依赖声明与打包配置

这种划分的意义在于,每一层都能单独替换。比如你不想用 Jsoup 想换别的解析库,只动 parser 包;想把结果从 MySQL 换成文件或消息队列,只动 pipeline。新手照着这个结构读代码,不会一上来就陷进某个工具类的细节里。

3. 跑起来:从导入到第一次抓取成功

3.1 环境准备与依赖安装

跑这个项目之前,先把环境对齐。需要 JDK 8 或以上(源码里若用了var或新 API 则要 11+,以实际 pom 为准)、Maven 3.6+、以及一个可用的数据库(若存储层是 JDBC)。导入 IDE 后先让 Maven 拉依赖,这一步最容易翻车的地方是仓库网络慢导致依赖下载不全,表现为编译时报找不到符号。稳妥做法是先执行一次依赖解析,看有没有红色报错:

# 在项目根目录执行,先确认依赖能完整拉下来 mvn -q dependency:resolve # 编译并跳过测试,快速验证工程是否完整 mvn -q clean compile -DskipTests

dependency:resolve只做依赖解析,不编译,能最快暴露仓库配置问题;clean compile验证源码本身能否通过编译。两条都过了,说明工程是完整的,再往下走配置。如果卡在某个依赖上,检查pom.xml里的仓库地址和本地settings.xml是否一致,别急着改代码。

3.2 配置目标站点与解析规则

配置层是这个项目能不能直接用的关键。常见做法是把起始 URL、分页参数、字段选择器集中放在一个配置文件里,代码启动时读取。下面是一个典型的配置片段,字段名按项目实际结构理解:

# config/crawler.yml 示例结构 target: startUrl: "https://example-site.com/list?page=1" pageParam: "page" # 分页参数名 maxPage: 20 # 最大翻页数,防止无限抓取 encoding: "UTF-8" selectors: item: ".list-item" # 列表项容器 title: "h3 a" # 标题选择器 link: "h3 a@href" # 详情链接 time: ".pub-time" # 发布时间 storage: type: "mysql" table: "crawl_result" thread: poolSize: 5 # 并发线程数

maxPage是必须设的保险丝,没有它遇到“下一页永远存在”的站点会一直抓下去;item选择器决定一条记录的范围,后面的字段选择器都相对它生效;poolSize别一上来就调大,先小并发跑通再逐步加。改配置时建议一次只改一个字段,抓一条看一条,比一次性改完再排查省时间。

3.3 启动与结果验证

配置就绪后启动主类,观察控制台日志。合格的爬虫项目会在日志里打印每次请求的 URL、状态码、解析出的记录数、写入结果。第一次跑建议把maxPage设成 1,只抓一页,确认字段抽取正确、数据能落库,再放开翻页。验证时别只看“程序没报错”,要去数据库或输出文件里核对字段值:标题是不是空的、链接是不是相对路径没补全、时间格式有没有解析失败。这些细节在单页验证阶段暴露,比抓了几千条再回头修便宜得多。跑通之后,再逐步把并发和页数加上去,观察失败率和响应时间的变化。

4. 核心模块拆解:一条记录从请求到入库的链路

4.1 解析层:Jsoup 选择器与字段清洗

解析层是整个项目里改动最频繁的地方,因为目标站点的页面结构说变就变。源码里通常把“选择器执行”和“字段清洗”分开,前者负责定位节点,后者负责把文本变成可用数据。看一段典型实现:

// 解析层:按配置的选择器抽取字段 public class ItemParser { public CrawlItem parse(Element item, SelectorConfig cfg) { CrawlItem result = new CrawlItem(); // 标题:取文本并去掉首尾空白 Element titleEl = item.selectFirst(cfg.getTitle()); result.setTitle(titleEl == null ? "" : titleEl.text().trim()); // 链接:取属性值,相对路径补全为绝对路径 Element linkEl = item.selectFirst(cfg.getLink()); String href = linkEl == null ? "" : linkEl.attr("href"); result.setUrl(toAbsolute(href, cfg.getBaseUrl())); // 时间:原始文本可能带“发布于”前缀,做一次清洗 Element timeEl = item.selectFirst(cfg.getTime()); result.setPublishTime(cleanTime(timeEl == null ? "" : timeEl.text())); return result; } }

这里每个字段都做了空值判断,这是实战里必须的——页面缺字段是常态,直接.text()会抛空指针,一条脏数据就能让整批任务中断。链接补全用toAbsolute把相对路径拼成绝对路径,否则入库的链接点不开。时间清洗把“发布于 2024-01-01”这类前缀剥掉,统一成可比较的格式。选择器本身写在配置里,页面改版时只改配置不改代码,这是这套结构最实用的地方。

4.2 调度与去重:分页推进和重复过滤

调度层负责两件事:按规则推进分页,以及在入库前过滤掉已经抓过的记录。去重常见做法是用 URL 或业务主键做唯一约束,配合内存里的 Set 做快速判断。下面是一个简化的调度循环:

// 调度层:分页推进 + 去重 public void run() { Set<String> seen = new HashSet<>(); // 内存去重,单机够用 for (int page = 1; page <= config.getMaxPage(); page++) { String url = buildPageUrl(config.getStartUrl(), page); String html = fetcher.fetch(url); Document doc = Jsoup.parse(html); Elements items = doc.select(config.getItemSelector()); if (items.isEmpty()) break; // 没有数据说明到底了,提前结束 for (Element item : items) { CrawlItem record = parser.parse(item, config); if (record.getUrl().isEmpty() || !seen.add(record.getUrl())) { continue; // 空链接或重复链接直接跳过 } pipeline.save(record); // 交给存储层 } sleepRandom(); // 随机间隔,降低请求频率 } }

items.isEmpty()提前结束是防止空翻页白跑;seen.add返回 false 说明这条 URL 已经处理过,直接跳过;sleepRandom在每次翻页后随机停顿,这是礼貌抓取的基本操作,也能降低被限流的概率。内存去重适合单机小规模,数据量大或要断点续抓时,得把去重集合换成数据库唯一索引或 Redis,这一点在二次开发时要留意。

4.3 存储层:批量写入与失败兜底

存储层最容易被忽视,但它决定了数据能不能用。逐条 insert 在数据量大时性能很差,常见做法是攒一批再批量提交,同时给失败记录留兜底。看一段批量写入的结构:

// 存储层:批量写入,失败记录单独落盘 public class DbPipeline { private final List<CrawlItem> buffer = new ArrayList<>(); private static final int BATCH_SIZE = 100; public void save(CrawlItem item) { buffer.add(item); if (buffer.size() >= BATCH_SIZE) { flush(); } } private void flush() { try { // 批量插入,具体 SQL 按表结构调整 jdbcTemplate.batchUpdate(INSERT_SQL, toArgs(buffer)); buffer.clear(); } catch (Exception e) { // 整批失败时降级为逐条写,定位坏数据 for (CrawlItem item : buffer) { try { jdbcTemplate.update(INSERT_SQL, toArgs(item)); } catch (Exception ex) { log.warn("写入失败: {}", item.getUrl()); } } buffer.clear(); } } }

BATCH_SIZE控制每批提交量,太小性能上不去,太大失败时重试成本高,100 左右是个常见起点。整批失败后降级逐条写,是为了把坏数据隔离出来,避免一条脏数据拖垮整批。日志里记下失败 URL,方便事后补抓。这套兜底逻辑看着朴素,但在真实采集任务里能省掉大量“数据莫名少了一截”的排查时间。

5. 避坑与常见问题排查

5.1 抓回来是空页面或乱码

现象是请求返回 200,但解析出来一条记录都没有,或者中文全是问号。原因通常有两个:一是站点对默认 UA 或缺少 Referer 的请求返回了精简页,二是响应编码和代码里写死的 UTF-8 不一致。解决方式是先用浏览器开发者工具对比请求头,把必要的头补上;编码不要写死,从响应头的Content-Type或页面 meta 里读,Jsoup 的parse支持传入输入流自动探测编码。改完先抓一页验证,别直接跑全量。

5.2 翻页翻到一半数据重复或断档

现象是入库数据里同一批记录出现多次,或者中间某几页完全缺失。重复多半是去重键选得不对,比如用标题去重但标题会重复,应该用详情页 URL 或业务 ID;断档常见于分页参数从 0 开始还是从 1 开始没对齐,或者某页请求超时被跳过却没重试。解决方式是给请求层加有限次重试,并把分页起始值做成配置项,跑之前先用两三个页码手工验证 URL 拼得对不对。

5.3 并发一开就大量超时

现象是单线程跑得好好的,把poolSize调大后失败率飙升。原因一般是对同一域名并发过高触发限流,或者连接池上限和线程数不匹配导致线程排队等连接。解决方式是先确认setDefaultMaxPerRoute不小于并发线程数,再把并发逐步加,观察失败率曲线;对同一站点保持随机间隔,别让请求像节拍器一样整齐。并发不是越大越好,稳定抓完比抓得快更重要。

5.4 页面改版后解析全空

现象是昨天还正常,今天所有字段都取不到。原因基本是目标站点调整了 DOM 结构,选择器失效。解决方式是把选择器全部外置到配置,改版时只改配置;同时在解析层对每个字段做空值兜底,避免一条解析失败中断整批。平时可以在日志里统计每页解析出的记录数,数量突然掉到 0 就是改版的信号,比等到入库为空才发现要早。

5.5 程序跑久了内存持续上涨

现象是长时间运行后 GC 频繁甚至 OOM。原因常见于去重集合无限增长、响应实体没关闭、或者缓冲区只加不清。解决方式是给内存去重设上限或换外部存储,确保CloseableHttpResponse用 try-with-resources 关闭,批量缓冲区在 flush 后清空。长时间任务还要考虑断点续抓,把已完成的页码或 URL 持久化,避免重启后从头再来。

6. 二次开发:把通用骨架改成你的采集任务

拿到这份源码,真正的工作量在改造,而不是跑通。我的习惯是先定“改哪里”,再动代码。第一步换目标:把配置文件里的起始 URL、分页参数、选择器全部替换成新站点,用单页模式验证字段,这一步不改任何 Java 代码。第二步调存储:如果结果不需要入库,把 pipeline 换成一个写 CSV 或 JSON 的实现,接口保持不变,调度层不用动。第三步加约束:给请求层补上重试次数、随机间隔范围、单域名并发上限,这些参数都做成配置项,方便不同任务复用。

验证改造是否成功,我一般用一个小对照法:手工用浏览器打开目标页,记下前三条记录的标题和链接,再跑程序抓第一页,逐条比对。字段对得上,再放开页数和并发。这个习惯来自一次教训——当时图快直接跑全量,结果选择器写错了一层,抓了几千条全是空标题,只能清库重来。从那以后我每次改完解析规则,都强制走一遍“单页三条比对”,确认无误才放量。

几个改造方向可以按需选:

改造点做法注意
换解析库替换 parser 包实现,保持接口选择器语法要跟着换
加代理池在 fetcher 层轮换出口控制频率,别把压力转嫁
断点续抓持久化已完成页码/URL去重集合要同步落盘
增量采集按发布时间或 ID 过滤边界值要单独验证

如果要做成长期运行的服务,还得考虑任务调度和监控:把单次运行改成定时触发,日志里加上每轮抓取量、失败率、耗时,异常时能告警。这些不属于源码自带的部分,但按这个分层结构加进去并不难,fetcher、parser、pipeline 各自都能独立测试。改造时尽量别把业务逻辑塞进调度循环,保持每层职责单一,后面换需求时才不用推倒重来。希望这份拆解能帮你把这个 Java 爬虫源码包真正用起来,而不是跑个 demo 就放下。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询