简介本资源是一份基于Java的jsoup分页爬虫实战项目面向Java初学者及网页数据采集入门开发者解决多页HTML内容批量抓取与结构化解析的核心问题。项目完整实现URL参数化构造、CSS选择器精准定位、分页循环遍历、异常重试及基础数据清洗等关键流程适用于新闻列表、商品页、论坛帖子等典型分页场景。压缩包共16个文件含7个核心jar包如jsoup-1.9.2.jar、WebCollector-2.52.jar等运行依赖、1个主程序Java源码、1个编译生成的class文件、2个Eclipse工程配置文件.project、.classpath及日志与缓存相关辅助文件整体3.08MB结构清晰开箱即用。目前已有1080人学习下载读者可直接导入Eclipse运行调试掌握分页逻辑实现细节、依赖管理方式及常见反爬应对思路是理解jsoup工程化应用的优质参考范例。1. jsoup分页爬取网页不是写个for循环就完事而是要稳住连接、绕过反爬、对齐页码逻辑的完整链路很多同学拿到“jsoup分页爬取网页”这个毕设/课设题目第一反应是不就是用for (int i 1; i 100; i) { String url https://xxx/list?page i; Document doc Jsoup.connect(url).get(); ... }跑两页就403翻三页就空响应到第5页直接超时——这不是代码问题是根本没立住「分页爬取」这个动作本身的工程约束。这套方案要解决的是真实场景下页面结构不统一、URL参数不规范、服务端校验页码合法性、响应体混杂HTML/JSON、以及网络抖动导致中间页丢失却无法重试的连锁问题。它适合需要交付可运行、可演示、可答辩的课程设计或毕业设计的同学也适合某公司前端团队临时要批量抓取公开产品文档做知识库初始化的轻量级需求。方案不依赖Selenium这类重量级工具全程基于jsoup 1.17原生能力但通过请求头精细化控制、DOM路径容错匹配、页码状态双校验URL参数页面实际页码文本、断点续爬标记机制把“分页”从一个循环变量变成一条有状态、可追溯、能自愈的数据采集流水线。2. 总体架构设计三层结构与四个核心模块的职责划分分页爬取不是单点技术活而是一条数据流从起始页发现总页数 → 按序发起请求 → 解析每页内容并提取目标字段 → 汇总写入本地。我们把它拆成调度层、采集层、解析层、存储层四块每块边界清晰、职责单一方便调试和替换。调度层只管“该爬哪一页”不碰HTML采集层只管“怎么连上、怎么拿回响应”不解析解析层只管“从Document里抠出标题、发布时间、正文段落”不关心页码存储层只管“按顺序存进CSV还是数据库”不参与网络逻辑。这种切分让A同学改分页策略、B同学换解析规则、C同学对接MySQL互不干扰。整个流程不引入Spring Boot或Web框架纯Java SE环境即可编译运行降低部署门槛——毕竟毕设答辩现场你不可能现场配Tomcat。2.1 调度层动态页码发现与安全步进机制分页的核心陷阱在于你以为URL里的?page5就真对应第5页但服务端可能返回第3页的内容比如数据被删了也可能跳转到第1页比如页码越界。所以不能信URL得信页面自己说的。我们采用「双源页码校验」先从URL提取初始页码值再从响应HTML中用jsoup定位页码元素如span classcurrent-page5/span或li classactive5/li两者必须一致才认为本次采集有效。若不一致则记录warn日志并跳过该页避免脏数据污染后续分析。// PageScheduler.java 核心逻辑 public class PageScheduler { private final String baseUrl; // 如 https://example.com/news private final int maxPage; // 安全上限防无限爬 private final SetInteger completedPages ConcurrentHashMap.newKeySet(); public PageScheduler(String baseUrl, int maxPage) { this.baseUrl baseUrl; this.maxPage maxPage; } // 返回待爬页码列表支持断点续爬 public ListInteger getPendingPages() { ListInteger pending new ArrayList(); for (int i 1; i maxPage; i) { if (!completedPages.contains(i)) { pending.add(i); } } return pending; } // 标记某页已成功完成在存储层写入后调用 public void markCompleted(int page) { completedPages.add(page); } }提示completedPages用ConcurrentHashMap.newKeySet()而非HashSet是因为后续可能扩展为多线程采集虽本方案默认单线程但留出扩展口。maxPage不是拍脑袋定的而是首次请求首页时从分页导航栏DOM中解析出最大页码值如a href?page123末页/a再加10%余量作为安全上限。2.2 采集层带会话保持与智能重试的Jsoup封装jsoup默认每次connect()都是新TCP连接无Cookie复用、无User-Agent轮换、无超时分级。我们封装SafeJsoupClient内置三点关键增强连接复用通过Connection.Response缓存cookies()下次请求自动携带请求头模拟固定User-Agent避免被识别为爬虫添加Accept、Accept-Language等浏览器常见头三级超时指数退避重试连接超时3s、读取超时8s、总超时15s失败后按1s→2s→4s延迟重试最多3次。// SafeJsoupClient.java public class SafeJsoupClient { private static final String USER_AGENT Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36; private final MapString, String cookies new HashMap(); public Document fetch(String url, int pageNum) throws IOException { Connection connection Jsoup.connect(url) .userAgent(USER_AGENT) .header(Accept, text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8) .header(Accept-Language, zh-CN,zh;q0.9,en;q0.8) .cookie(JSESSIONID, cookies.getOrDefault(JSESSIONID, )) .timeout(3000) // 连接超时3秒 .followRedirects(true); // 指数退避重试 for (int attempt 0; attempt 3; attempt) { try { Connection.Response response connection.execute(); cookies.putAll(response.cookies()); // 更新cookies return response.parse(); } catch (SocketTimeoutException e) { if (attempt 2) throw e; Thread.sleep((long) Math.pow(2, attempt) * 1000); // 1s, 2s, 4s } } return null; } }注意followRedirects(true)必须开启否则遇到302跳转如未登录跳转到login页会直接返回跳转HTML而不是目标页。cookie(JSESSIONID, ...)只是示例实际需根据目标站是否需要Session保持来决定是否透传——多数静态新闻页无需但带登录态的后台管理页必须。2.3 解析层DOM路径容错与字段强校验提取解析不是doc.select(div.title).text()就完事。真实网页DOM结构常变动今天class叫article-title明天改成post-h1今天标题在h1里明天包进div classcontent再嵌一层p。我们定义FieldExtractor接口每个字段实现自己的「多路径尝试」逻辑// FieldExtractor.java FunctionalInterface public interface FieldExtractor { String extract(Document doc); } // TitleExtractor.java 具体实现 public class TitleExtractor implements FieldExtractor { private static final ListString TITLE_SELECTORS Arrays.asList( h1.article-title, // 主路径 h1.post-title, // 备用路径1 div.content h1, // 备用路径2 meta[propertyog:title] // 终极兜底取Open Graph标签 ); Override public String extract(Document doc) { for (String selector : TITLE_SELECTORS) { Elements els doc.select(selector); if (!els.isEmpty()) { String text els.first().text().trim(); if (text.length() 5 !text.toLowerCase().contains(404)) { return text; } } } return [未提取到标题]; // 明确标记缺失不返回空串 } }关键设计点多路径尝试按优先级列selector列表逐个试不因某条路径失效就中断内容校验长度过滤防提取到首页、返回等导航文字、关键词过滤防404/500提示兜底策略meta propertyog:title是SEO通用标签比DOM结构更稳定明确缺省值返回[未提取到标题]而非null或空串后续统计时可一眼识别异常页。2.4 存储层结构化写入与断点续爬文件标记存储必须支持两种模式快速验证用的CSV方便Excel打开查数据和正式交付用的SQLite单文件、免服务、可SQL查询。关键设计是「断点续爬标记」——每次成功解析一页并写入后将页码写入checkpoint.txt格式为纯数字每行一个。下次启动时调度层读此文件自动跳过已存页码。// CsvStorage.java 片段 public class CsvStorage { private final Path csvPath; private final Path checkpointPath; public CsvStorage(String outputPath) { this.csvPath Paths.get(outputPath, articles.csv); this.checkpointPath Paths.get(outputPath, checkpoint.txt); initCsvHeader(); } public void saveArticle(int pageNum, String title, String publishDate, String content) throws IOException { try (BufferedWriter writer Files.newBufferedWriter(csvPath, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.APPEND)) { CSVFormat.EXCEL.print(writer) // 使用Apache Commons CSV避免逗号乱码 .println(new String[]{String.valueOf(pageNum), title, publishDate, content}); } // 写入检查点 Files.write(checkpointPath, (pageNum \n).getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.APPEND); } }注意CSVFormat.EXCEL来自Apache Commons CSV库org.apache.commons:commons-csv:1.10.0它自动处理字段含逗号、换行符的情况比手动拼接\title\ ,可靠得多。checkpoint.txt用追加模式写入确保即使程序崩溃最后成功的页码也已落盘。3. 分页策略实现四种典型分页模式的代码适配与判断逻辑分页URL构造绝非千篇一律。我们归纳出四类高频模式每种对应不同解析逻辑和URL生成方式。方案内置PaginationStrategy枚举运行时根据首页HTML自动识别类型避免硬编码。3.1 Query参数型?page1size20最常见特征URL中存在page参数且分页导航栏a href?page22/a的href是相对路径。识别逻辑解析首页DOM查找所有a标签正则匹配href.*page\\d若匹配到则判定为此类。// QueryParamStrategy.java public class QueryParamStrategy implements PaginationStrategy { private final String baseUrl; // 如 https://news.example.com/list public QueryParamStrategy(String baseUrl) { this.baseUrl baseUrl; } Override public String buildUrl(int pageNum) { // 保留baseUrl原有参数仅更新page值 if (baseUrl.contains(?)) { return baseUrl.replaceFirst(page\\d, page pageNum); } else { return baseUrl ?page pageNum; } } Override public int detectMaxPage(Document firstPage) { // 从分页栏找末页链接或最大数字 Elements lastPageLinks firstPage.select(a:contains(末页), a:contains(尾页), a:contains(Last)); if (!lastPageLinks.isEmpty()) { String href lastPageLinks.first().attr(href); Matcher m Pattern.compile(page(\\d)).matcher(href); if (m.find()) return Integer.parseInt(m.group(1)); } // 备用找所有页码链接中的最大值 Elements pageLinks firstPage.select(a[href*page]); return pageLinks.stream() .map(link - link.attr(href)) .map(h - { Matcher mm Pattern.compile(page(\\d)).matcher(h); return mm.find() ? Integer.parseInt(mm.group(1)) : 0; }) .mapToInt(Integer::intValue) .max().orElse(1); } }血泪经验baseUrl.replaceFirst(page\\d, ...)比baseUrl.replaceAll(page\\d, ...)安全避免误替换URL中其他位置的page123如/api/page/123/detail。3.2 路径型/news/2024/05/15/page/2特征页码是URL路径一部分如/page/2。识别逻辑首页link relnext或分页栏a href/page/2的href以/page/开头。// PathStrategy.java public class PathStrategy implements PaginationStrategy { private final String baseUrl; // 如 https://blog.example.com/archive Override public String buildUrl(int pageNum) { // 将 baseUrl 末尾 / 替换为 /page/{pageNum}/ String cleanBase baseUrl.endsWith(/) ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl; return cleanBase /page/ pageNum /; } Override public int detectMaxPage(Document firstPage) { // 查找 a href/page/123123/a Elements pageLinks firstPage.select(a[href^/page/]); return pageLinks.stream() .map(el - el.attr(href)) .map(h - { Matcher m Pattern.compile(/page/(\\d)/).matcher(h); return m.find() ? Integer.parseInt(m.group(1)) : 0; }) .mapToInt(Integer::intValue) .max().orElse(1); } }玄学注意某些站点a href/page/2实际指向第2页但a href/page/123可能404数据不足。因此detectMaxPage返回值需结合buildUrl测试——方案中会在启动时用detectMaxPage()1尝试请求若返回404则自动下调上限。3.3 JSON API型POST /api/articles?page2需伪造Referer特征页面内容由AJAX加载Network面板看到/api/articles?page2的XHR请求返回JSON。识别逻辑首页HTML中存在script内含fetch(/api/articles或$.get(/api/articles。// JsonApiStrategy.java public class JsonApiStrategy implements PaginationStrategy { private final String apiUrl; // 如 https://api.example.com/v1/articles Override public String buildUrl(int pageNum) { return apiUrl ?page pageNum size20; } Override public int detectMaxPage(Document firstPage) { // 从首页script中提取总条数再算页数 Elements scripts firstPage.select(script); for (Element script : scripts) { String data script.data(); Matcher m Pattern.compile(total:\\s*(\\d)).matcher(data); if (m.find()) { int total Integer.parseInt(m.group(1)); return (int) Math.ceil(total / 20.0); // size20 } } return 10; // 保守默认值 } }排查重点此类请求常需Referer头防盗链和X-Requested-With头标识AJAX。SafeJsoupClient中需额外添加.header(Referer, https://example.com/news) .header(X-Requested-With, XMLHttpRequest)3.4 下拉加载型无传统分页靠滚动触发本方案不支持但需识别并报错特征页面无分页栏滚动到底部自动加载更多。识别逻辑首页DOM中无a含page或/page/且存在div idload-more或window.addEventListener(scroll, ...)。此时方案主动抛出UnsupportedPaginationException(检测到无限滚动分页当前版本不支持请切换为Selenium方案)避免用户误以为能跑通。后悔药设计所有PaginationStrategy实现都要求提供isSupported(Document firstPage)静态方法主流程在初始化时调用不支持则立刻终止并打印明确错误不进入死循环。4. 避坑指南分页爬取中5个最高频翻车点与根治方案分页爬取的坑不在代码语法而在服务端策略与网络环境的组合效应。以下是我在某高校课程设计指导中学生提交的137份作业里出现频率最高的5个问题每条都附真实现象、底层原因和可立即落地的修复代码。4.1 现象前10页正常第11页开始全部返回首页HTML原因服务端做了页码阈值拦截如if page 10 return redirect to /或CDN缓存了首页响应并错误地复用给?page11请求。解决在SafeJsoupClient.fetch()中增加响应内容指纹校验。请求后对比response.parse().title()与预期页码标题是否匹配如第11页应含第11页或page11字样不匹配则视为被重定向抛出PageRedirectedException并跳过。// 在 SafeJsoupClient.fetch() 的 try 块内添加 Document doc response.parse(); if (pageNum 1 doc.title().contains(首页)) { throw new PageRedirectedException(第 pageNum 页被重定向到首页疑似服务端拦截); }4.2 现象解析出的标题全是加载中...或空字符串原因目标页面使用Vue/React渲染jsoup获取的是原始HTML骨架真实内容由JS动态注入。解决方案强制要求在FieldExtractor中加入「等待JS渲染」的兜底判断。若首条selector提取为空休眠1秒后重新Jsoup.connect(url).get()此时服务端可能已返回渲染后HTML最多重试2次。// TitleExtractor.extract() 中追加逻辑 String text els.first().text().trim(); if (text.isEmpty() || text.contains(加载中)) { try { Thread.sleep(1000); Document freshDoc Jsoup.connect(url).get(); // 重新请求 text freshDoc.select(selector).first().text().trim(); } catch (Exception ignored) {} }4.3 现象CSV文件中文乱码Excel打开显示涓枃原因Windows记事本默认ANSI编码而JavaFiles.write()用UTF-8导致Excel误读。解决存储层强制生成BOM头。CsvStorage.saveArticle()中首次写入时先写\uFEFFUTF-8 BOMif (!Files.exists(csvPath)) { Files.write(csvPath, \uFEFF.getBytes(StandardCharsets.UTF_8)); }4.4 现象程序运行到第37页突然卡死CPU 100%原因doc.select(div.content p)返回巨量p元素如长文章含500段els.each()遍历耗时过长且未设超时。解决所有select()后立即加.limit(100)限制元素数量并在FieldExtractor中增加执行时间监控long start System.currentTimeMillis(); String result extractor.extract(doc); long cost System.currentTimeMillis() - start; if (cost 5000) { // 超5秒告警 logger.warn(字段提取超时{}ms页码{}, cost, pageNum); result [提取超时]; }4.5 现象checkpoint.txt里写了页码但重启后仍重复爬第1页原因Files.write()未指定StandardOpenOption.CREATE当文件不存在时抛NoSuchFileException但被外层try-catch吞掉。解决CsvStorage构造函数中强制创建checkpointPath父目录及空文件public CsvStorage(String outputPath) { this.csvPath Paths.get(outputPath, articles.csv); this.checkpointPath Paths.get(outputPath, checkpoint.txt); try { Files.createDirectories(Paths.get(outputPath)); Files.createFile(checkpointPath); // 确保文件存在 } catch (IOException e) { throw new RuntimeException(初始化存储目录失败, e); } initCsvHeader(); }5. 验收与答辩用三张表一个演示脚本让老师当场点头通过毕设/课设验收不是看代码行数而是看能否说清问题、能否展示结果、能否解释异常。我带过的某跨平台系统毕设最终答辩只用了三张表和一个30秒演示脚本导师看完直接说“这个工作量够了细节可以后续优化”。以下是可直接套用的验收组合拳。5.1 验收表1分页策略识别与页码校验报告自动生成程序启动时自动输出report_strategy.md包含三项关键证据检查项实际值是否通过说明首页URLhttps://news.example.com/✅手动确认正确识别分页类型QUERY_PARAM✅DOM中匹配到a href?page2解析最大页码127✅从a href?page127末页/a提取页码双校验一致性127 127✅URL参数与页面显示页码完全一致这张表的价值在于它证明你不是盲目写for(int i1;i100;i)而是真正理解了目标站的分页机制。答辩时把report_strategy.md投影出来导师扫一眼就知道你做了基础调研。5.2 验收表2采集质量统计CSV导出后自动生成程序结束时扫描articles.csv生成report_quality.md聚焦三个硬指标指标数值达标线状态成功采集页数127≥127✅标题非空率100%≥95%✅发布日期可解析率98.4%≥90%✅2页无日期属正常平均单页耗时1.2s≤3s✅计算逻辑用Python一行命令生成答辩前预跑好# 统计标题非空率 awk -F, NR1 {if($2! $2![未提取到标题]) c} END{print c/NR*100 %} articles.csv导师最关心“数据有没有用”这张表用百分比说话比贴100行日志有力得多。5.3 验收表3异常页明细供答辩时解释生成report_errors.csv只记录真正需要人工介入的页页码错误类型错误摘要处理建议45PageRedirectedException被重定向到首页检查服务端是否对page40做拦截88HttpStatusExceptionHTTP 403 Forbidden需添加Referer头或更换User-Agent112ParseTimeoutException提取标题超5秒页面结构异常复杂建议降级为纯文本提取这张表是答辩时的“后悔药”。当导师问“第88页为什么没数据”你直接打开此表指着那行说“这里检测到403我已在代码中预留了Referer配置入口只需在SafeJsoupClient里添加一行header(Referer, ...)即可修复。”——展现的是工程化思维不是背代码。5.4 30秒演示脚本从零启动到结果展示写一个demo.batWindows或demo.shMac/Linux内容极简# demo.sh rm -rf output/ java -jar jsoup-pager.jar --url https://news.example.com/ --max-page 5 head -20 output/articles.csv echo ✅ 已生成5页数据共$(wc -l output/articles.csv)行答辩时当着导师面双击运行30秒内看到命令行输出CSV前20行再打开output/report_strategy.md——整个过程无任何IDE、无任何配置就是一个JAR包一个URL参数。这传递的信息是可交付、可复现、无隐藏依赖。从那以后我每次指导毕设都强制要求学生在README.md里写清这三张表的位置和demo.sh的用法。因为答辩现场没有时间debug只有3分钟证明你真的做完了。希望帮到你。本文还有配套的精品资源点击获取