news 2026/10/26 5:43:56

Java爬虫实战源码解析:从请求到入库的完整链路与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java爬虫实战源码解析:从请求到入库的完整链路与避坑指南

简介:这份Java爬虫项目实战源码面向具备一定Java基础、希望系统掌握网络数据抓取技术的开发者,可用于数据分析、信息自动化等场景的入门与进阶练习。压缩包共1959个文件,约243.14MB,以png图片、jar依赖包、class与java源码为主,辅以js、css、xml、jsp等前端与配置资源,以及properties、sql、xls等数据与脚本文件,完整呈现了一个可运行的工程结构。项目覆盖HTTP请求发送、Jsoup解析HTML、正则提取、多线程与异步处理、数据落库存储、日志记录及反反爬策略等关键环节,并涉及WebMagic类框架与JUnit测试思路,便于读者对照源码理解爬虫从请求到存储的完整链路。目前已有136人学习,适合想通过真实工程案例提升Java编程与爬虫实战能力的中级开发者参考。

1. 一份 Java 爬虫实战源码,到底能帮你省掉哪几天的摸索

很多人第一次写 Java 爬虫,卡住的地方往往不是“不会发 HTTP 请求”,而是请求发出去之后的一连串麻烦:页面编码乱码、翻页逻辑写死、抓到的数据重复、跑一晚上内存爆掉、目标站点稍微改个结构就全线崩。你搜“java 爬虫 软件/插件 源代码”,真正想找的其实是一套能跑通完整链路的参考实现,而不是一段孤零零的HttpURLConnection示例。这份 Java 爬虫项目实战源码.zip 的价值就在这里:它把请求发起、页面解析、数据落库、任务调度这几块拼成了一个可运行的工程骨架,适合刚接触爬虫的 Java 开发者照着改,也适合有经验的人拿它当项目脚手架,省掉搭目录、配依赖、写重试这些重复劳动。下面按“它由什么组成、怎么跑起来、参数怎么调、哪里会翻车”的顺序拆。

2. 拆开源码包:模块划分与依赖选型为什么这么定

2.1 工程结构里每个包在干什么

拿到一个爬虫源码包,先别急着找 main 方法,先看目录结构,它基本决定了这套代码的可扩展性。这类实战项目常见的分层是四层:抓取层、解析层、存储层、调度层。抓取层负责发请求、管连接、处理重试和请求头;解析层把 HTML 或 JSON 转成结构化对象;存储层决定数据进内存、进文件还是进数据库;调度层管任务队列、并发数和去重。你打开源码后可以对照下面这张表快速定位:

目录/包典型职责改动频率
crawler/fetchHTTP 客户端封装、请求头、超时、重试低
crawler/parseHTML 选择器、正则、JSON 反序列化高(站点一变就改)
crawler/store数据模型、入库、去重判断中
crawler/schedule线程池、任务分发、限速低
crawler/common工具类、配置加载、日志低

判断一份源码值不值得用,看解析层和抓取层是否解耦。如果解析逻辑直接写在请求回调里,站点一改你就得动抓取代码,这种结构后期维护成本很高。实战项目如果做到了“抓取只管拿字符串、解析只管转对象”,那它就是合格的起点。

2.2 依赖选型:HttpClient 还是 OkHttp,Jsoup 还是正则

Java 爬虫的依赖选择不多,但选错会直接影响后面调参的手感。常见组合是 Apache HttpClient 或 OkHttp 做请求,Jsoup 做 HTML 解析,Jackson 或 Fastjson 做 JSON 处理,连接池用 HikariCP,存储用 JDBC 或 MyBatis。这套源码如果用的是 HttpClient + Jsoup,属于最稳的教科书组合:HttpClient 的连接池和超时控制成熟,Jsoup 的 CSS 选择器写起来接近前端思维,调试成本低。

选型理由要落到具体场景:目标页面是服务端渲染的静态 HTML,Jsoup 足够;如果页面数据靠接口异步加载,那解析层应该走 JSON 反序列化而不是硬啃 DOM。你在读源码时重点看两点——请求是否复用了连接池、解析是否做了空值保护。前者决定跑批量任务时会不会把端口耗光,后者决定遇到残缺页面时是抛异常还是静默跳过。这两点没处理好,后面调参都是白费。

2.3 配置文件里那几个必须改的参数

源码包一般会带一个application.properties或config.yaml,里面藏着真正影响抓取行为的参数。跑之前至少确认这几项:

# 并发线程数,新手先设 2-4,别一上来就 20 crawler.thread.pool.size=4 # 单次请求超时(毫秒),目标站点慢就往上调 crawler.request.timeout=8000 # 两次请求之间的间隔(毫秒),这是礼貌也是保命 crawler.request.interval=1200 # 失败重试次数,超过 3 次基本说明对方在拦你 crawler.retry.times=3 # 单次任务最大抓取页数,防止跑飞 crawler.max.pages=200

参数说明:线程池大小和请求间隔要一起看,4 个线程配 1.2 秒间隔,实际 QPS 大约 3,这个量级对大多数中小站点是安全的。超时设太短会把慢响应误判为失败,设太长会让线程池被拖死。重试次数不是越多越好,连续失败往往意味着触发了对方的频率限制,继续重试只会让封禁来得更快。这些值没有标准答案,但有一个原则:先用保守值跑通,再根据日志里的失败率逐步放宽。

3. 从零跑通一次抓取:请求、解析、入库三段拆解

3.1 发起请求:连接复用与请求头伪装

抓取层的第一件事不是写 URL,而是把 HttpClient 实例管好。连接池要复用,否则每次 new 一个客户端,跑几百个请求就开始报连接拒绝。典型写法是单例持有连接池,配合请求拦截器统一加请求头:

// 构建带连接池的 HttpClient,全局复用一个实例 PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(); cm.setMaxTotal(50); // 总连接上限 cm.setDefaultMaxPerRoute(10); // 单域名并发上限,防止把目标站打挂 RequestConfig config = RequestConfig.custom() .setConnectTimeout(8000) // 建连超时 .setSocketTimeout(8000) // 读数据超时 .setConnectionRequestTimeout(3000) .build(); HttpClient client = HttpClients.custom() .setConnectionManager(cm) .setDefaultRequestConfig(config) .setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64)") // 别用默认 UA .build();

逻辑说明:setDefaultMaxPerRoute是很多人忽略的参数,它限制的是对同一个域名的并发连接数,设太大容易触发对方的风控,设太小又跑不满带宽。请求头里 User-Agent 必须改,Java 默认 UA 一眼就能被识别成脚本。如果目标站点校验 Referer 或 Cookie,还要在拦截器里统一补上,别在每个请求里手写。这一段跑通的标准是:连续发 100 个请求,日志里没有连接超时和 403。

3.2 解析页面:选择器写法与空值兜底

解析层最容易翻车的地方是“假设页面结构永远不变”。合格的写法是每个字段单独取值、单独判空,取不到就记日志跳过,而不是让整个任务抛异常中断:

// 用 Jsoup 解析列表页,每个字段独立兜底 Document doc = Jsoup.parse(html, "UTF-8"); Elements items = doc.select("div.list-item"); // 选择器按实际页面改 for (Element item : items) { String title = item.select("h3.title").text(); // 取不到返回空串 String link = item.select("a.detail").attr("href"); String time = item.select("span.time").text(); if (title.isEmpty() || link.isEmpty()) { log.warn("字段缺失,跳过该条: {}", item.html()); continue; // 单条失败不影响整页 } // 相对路径补全成绝对路径 if (link.startsWith("/")) { link = baseUrl + link; } resultList.add(new Item(title, link, time)); }

逻辑说明:select返回空集合时.text()给空串而不是抛异常,这是 Jsoup 的默认行为,但.attr()取不到属性同样返回空串,所以判空要放在业务层做。相对路径补全是高频坑,很多源码示例直接存相对路径,入库后才发现链接点不开。编码问题也要在这一层解决,Jsoup.parse的第二个参数显式指定字符集,别依赖自动探测,中文站点自动探测翻车的概率不低。

3.3 数据入库与去重:唯一键比内存 Set 靠谱

存储层要解决两个问题:写进去、不重复。小数据量用内存HashSet去重没问题,但任务一重启就全丢,重复数据照样入库。稳妥做法是在数据库层建唯一索引,插入时用INSERT IGNORE或捕获唯一键冲突:

-- 以链接的哈希值作为唯一键,天然去重 CREATE TABLE crawl_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, url_hash CHAR(32) NOT NULL, title VARCHAR(255), link VARCHAR(512), publish_time VARCHAR(64), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_url_hash (url_hash) );
// 入库前先算哈希,冲突则跳过 String hash = DigestUtils.md5Hex(link); try { jdbcTemplate.update( "INSERT IGNORE INTO crawl_item(url_hash, title, link, publish_time) VALUES (?,?,?,?)", hash, title, link, time); } catch (DuplicateKeyException e) { log.debug("重复数据,跳过: {}", link); }

逻辑说明:用链接的 MD5 做唯一键,比用自增 ID 或标题去重更可靠,因为同一篇文章标题可能重复,链接基本唯一。INSERT IGNORE在 MySQL 里遇到唯一键冲突会静默跳过,配合批量提交能把入库速度拉起来。注意批量插入时要把rewriteBatchedStatements=true加到 JDBC URL 里,否则逐条提交的性能差距能到十倍以上。

4. 并发、限速与断点续爬:让任务能跑一整晚不崩

4.1 线程池参数怎么配才不把内存跑爆

并发不是越大越好。每个线程持有一个响应对象和解析后的 DOM,页面大的时候单线程内存占用能到几 MB,20 个线程同时跑,堆内存很快见底。实战项目里线程池一般用固定大小,配合有界队列:

// 固定线程池 + 有界队列,队列满了触发拒绝策略而不是无限堆积 ThreadPoolExecutor executor = new ThreadPoolExecutor( 4, // 核心线程数 4, // 最大线程数,固定住 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(200), // 有界队列,防止 OOM new ThreadPoolExecutor.CallerRunsPolicy() // 满了让提交线程自己跑,天然限速 );

逻辑说明:CallerRunsPolicy是个很实用的拒绝策略,队列满时由提交任务的线程自己执行,相当于自动降速,比直接丢弃任务或抛异常温和得多。核心线程数和最大线程数设成一样,避免线程频繁创建销毁。队列容量别设太大,200 左右足够,设成无界队列等于把内存风险藏起来,跑久了必然出问题。

4.2 限速的两种做法:固定间隔与令牌桶

固定间隔最简单,每次请求前Thread.sleep一下,但多线程下这个 sleep 是各睡各的,实际 QPS 会翻倍。要精确控制全局速率,用令牌桶更靠谱:

// 用 Guava RateLimiter 做全局限速,每秒放行 3 个请求 RateLimiter limiter = RateLimiter.create(3.0); public String fetch(String url) { limiter.acquire(); // 拿不到令牌就阻塞,天然限速 return doHttpGet(url); }

逻辑说明:RateLimiter.create(3.0)表示每秒 3 个许可,多线程共享同一个实例时,所有线程一起排队拿令牌,全局速率就被压住了。这个方案比在每个线程里 sleep 精确得多。如果目标站点对突发流量敏感,还可以用SmoothBursty之外的平滑模式,让请求间隔更均匀。限速值从 2 到 3 起步,观察日志里的响应码,出现 429 或 403 就往下调。

4.3 断点续爬:把进度落到文件或数据库

跑一整晚的任务最怕中途挂掉,重启后从头再来。断点续爬的核心是把“已完成的页码或 ID”持久化,启动时先读进度:

// 每抓完一页就把进度写进文件,重启时从断点继续 int startPage = readProgress("progress.txt"); // 读不到默认 1 for (int page = startPage; page <= maxPages; page++) { List<Item> items = crawlPage(page); saveItems(items); writeProgress("progress.txt", page + 1); // 先写进度再进下一页 log.info("第 {} 页完成,共 {} 条", page, items.size()); }

逻辑说明:进度写入要放在数据入库之后、进入下一页之前,顺序反了会出现“进度前进了但数据没存”的丢数据情况。进度文件用覆盖写而不是追加写,避免文件越滚越大。如果任务规模大,把进度存到数据库的一张crawl_progress表里更稳,还能支持多任务并行。

5. 避坑清单:这几处翻车我见过太多次

5.1 现象:跑几十个请求后开始大量连接超时

原因:HttpClient 实例在循环里反复 new,连接池没复用,端口被耗尽,或者连接用完没关闭。解决:把 HttpClient 提成单例,用try-with-resources或EntityUtils.consume确保响应体被消费,连接才能归还池子。检查方式是看netstat里 TIME_WAIT 数量是否暴涨。

5.2 现象:中文全部变成乱码或问号

原因:响应头里的字符集和页面 meta 声明不一致,代码里又写死了UTF-8。解决:优先读响应头的Content-Type,取不到再解析页面 meta 的 charset,两者都没有才回退 UTF-8。Jsoup 解析时把探测到的字符集显式传进去,别让它自己猜。

5.3 现象:数据入库后大量重复

原因:只靠内存 Set 去重,任务重启后集合清空,或者翻页逻辑有重叠页。解决:数据库建唯一索引兜底,翻页时检查相邻两页的首条记录是否相同,相同说明分页参数算错了。这个坑在页码从 0 开始还是从 1 开始的站点之间切换时特别常见。

5.4 现象:跑一段时间后对方返回 403 或验证页

原因:请求频率过高、UA 固定不变、缺少必要的请求头。解决:降并发、加间隔、轮换 UA,检查是否漏了 Referer。注意不要试图用更激进的方式绕过,频率降下来通常就能恢复,硬顶只会让封禁时间变长。

5.5 现象:解析突然全部返回空

原因:目标页面改版,选择器失效,但代码没做空值告警,任务“正常”跑完却一条数据没存。解决:在解析层加一条统计,单页解析结果为零时打 error 日志并暂停任务,别让它静默跑完。这个保护机制比任何重试都重要。

6. 二次开发前先做的一件事:用固定样本回归验证

拿到这份源码,改完选择器和参数之后,别直接上全量任务。我的习惯是先截一份页面样本存到本地,写一个只读本地文件的解析测试,把字段提取结果和预期值逐条比对。这样做的好处是:解析逻辑的改动可以脱离网络单独验证,站点改版时也能快速定位是选择器问题还是请求问题。

具体做法是建一个resources/samples/目录,放三五份不同结构的页面 HTML,然后写一个参数化测试:

@Test public void testParseSample() throws Exception { String html = Files.readString(Path.of("src/test/resources/samples/list.html")); List<Item> items = parser.parseList(html); assertEquals(20, items.size()); // 条数对不对 assertEquals("示例标题", items.get(0).getTitle()); // 字段对不对 assertTrue(items.get(0).getLink().startsWith("http")); // 链接补全没有 }

这段测试跑通,说明解析层是干净的,后面抓取失败就只可能是网络或风控问题,排查范围直接缩小一半。参数上注意样本文件要用真实抓下来的 HTML,别手写简化版,真实页面里的空白字符、嵌套标签、缺失属性才是坑的来源。

还有一个容易被忽略的点:日志。源码包自带的日志配置往往只打 INFO,跑批量任务时你需要知道每个请求的耗时、状态码、重试次数。我一般会在抓取层加一个拦截器,把这三项打成一行结构化日志,出问题时按状态码一筛就知道是限速还是解析。从那以后我每次改完爬虫,都强制先跑样本回归、再看一小时日志,确认没有静默失败才放开并发。希望这份拆解能帮你在拿到源码包时少走几步弯路。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 5:43:39

YOLOv8改进实战:BGF-YOLO注意力与多尺度融合提升脑肿瘤检测精度

1. 为什么要在 YOLOv8 上动刀&#xff1a;脑肿瘤检测的真实痛点脑肿瘤检测这件事&#xff0c;外行看是"拿模型跑个图"&#xff0c;内行看是"在极低对比度、极高类间相似度的灰度图里找异常"。我最早接触这类任务时用的是标准 YOLOv8&#xff0c;训练集上 m…

作者头像 李华
网站建设 2026/10/26 5:43:33

AI工程实践周报:Agent搭建、模型部署与落地避坑指南

这周的AI圈子&#xff0c;热词密度比往常高不少。翻了一遍本周搜索趋势&#xff0c;有个很直观的感受&#xff1a;大家讨论的重心正在从"哪个模型又涨了几个点"转向"AI Agent 怎么搭""模型部署怎么搞""工程实践怎么落地"。换句话说&am…

作者头像 李华
网站建设 2026/10/26 5:42:54

OpenClaw跨平台安装报错解决方案大全

1. OpenClaw安装报错速查手册&#xff1a;全平台兼容指南&#xff08;2026版&#xff09;OpenClaw作为当前最热门的跨平台开发工具链&#xff0c;在金融分析、AI模型部署、微信生态接入等场景展现出了惊人的灵活性。但每次版本更新后&#xff0c;总有一批开发者会在安装阶段遭遇…

作者头像 李华
网站建设 2026/10/26 5:41:38

SQL注入原理与防御实战指南

1. 从零开始理解SQL注入的本质SQL注入&#xff08;SQL Injection&#xff09;是Web安全领域最古老也最危险的漏洞之一。简单来说&#xff0c;就是攻击者通过构造特殊的输入&#xff0c;改变后端数据库查询语句的原始逻辑。我第一次接触这个概念是在2013年参加某次网络安全演练时…

作者头像 李华
网站建设 2026/10/26 5:41:37

从77元到年入200万:AI云服务“卖铲人”模式实战拆解

00后&#xff0c;第一单77元&#xff0c;7年做到年入200万。这几个数字放在一起&#xff0c;很多人第一反应是"割韭菜的又来了"。但如果你真去拆这个案例的底层逻辑&#xff0c;会发现它其实是AI淘金热里一个特别典型的"卖铲人"生意——不自己挖金矿&#…

作者头像 李华
网站建设 2026/10/26 5:41:35

调用量登顶的太空兔:OpenAI兼容网关与动态路由实测

1. 太空兔到底是什么&#xff1a;从一次“调用量登顶”事件说起1.1 一次反常的榜单变化上周四晚上&#xff0c;我在一个开发者群里看到有人丢出链接&#xff1a;SpaceRabbit&#xff0c;中文圈里都叫它“太空兔”。消息附带了一张截图&#xff0c;某个第三方API观测平台上&…

作者头像 李华