
简介本资源是一份基于Java的jsoup分页爬虫实战项目面向Java初学者及网页数据采集入门开发者解决多页HTML内容批量抓取与结构化解析的核心问题。项目完整封装了URL参数化构造、CSS选择器精准定位、分页链接自动识别、循环请求控制及基础异常处理等关键环节覆盖从单页解析到全站分页遍历的典型开发流程。压缩包共16个文件含7个核心jar包如jsoup-1.9.2.jar、WebCollector-2.52.jar等支撑HTTP请求与HTML解析、2个Eclipse工程配置文件.classpath、.project、1个Java源码文件src/com/.../Test.java及日志、缓存、编译配置类文件整体3.08MB结构清晰开箱即用。目前已有1082人学习下载读者可直接运行调试源码理解分页逻辑实现细节复用其URL生成策略、元素提取模板与工程组织方式快速构建自有爬虫脚本。1. 分页爬取不是“加个 for 就完事”jsoup 做对分页关键在识别翻页逻辑与状态隔离很多同学第一次用 jsoup 写爬虫看到列表页有“下一页”按钮就直接for (int i 1; i 10; i) { doc Jsoup.connect(https://xxx/list?page i).get(); }——跑两页就 403第三页返回空数据第五页突然跳登录页。这不是 jsoup 不行是没把「分页」当成一个有状态、有规则、有边界的业务流程来设计。真实项目里分页可能是 URL 参数递增page1→2→3也可能是 POST 提交 offsetlimit还可能是滚动加载触发 Ajax 接口甚至存在反爬校验参数如 sign、timestamp、token随页动态生成。本方案聚焦「静态 HTML 分页」这一最常见但极易翻车的场景用 jsoup 实现可中断、可续爬、防重复、带日志、能收敛的分页抓取闭环。它适合课程设计中需要稳定采集新闻列表、商品目录、论坛帖子等结构化网页的场景也适合作为毕设中数据采集模块的可靠底座——不追求高并发但要求每一页都拿得稳、存得准、断点能接上。2. 总体架构设计三层结构与四个核心模块的职责划分本方案采用清晰的三层结构调度层Controller→ 解析层Parser→ 存储层Writer中间通过统一的数据载体PageResult贯穿。所有模块解耦可独立替换比如把 FileWriter 换成 MySQLWriter 只需改一行注入。整个流程不依赖 Spring 或任何框架纯 JDK jsoup 实现便于嵌入课设系统或毕设主程序。2.1 调度层分页控制器 PageCrawler 的核心逻辑PageCrawler是整个流程的“大脑”它不关心 HTML 怎么解析、数据怎么存只专注三件事判断是否继续翻页、构造下一页请求、控制重试与退出。它基于两个关键策略工作终止判定策略StopPolicy支持MaxPageCountStopPolicy(100)最多爬100页、EmptyContentStopPolicy()某页无目标元素即停、DuplicateUrlStopPolicy()URL 重复即停URL 构造策略UrlBuilder支持QueryParamUrlBuilder(page)?page1、PathSegmentUrlBuilder(page)/page/1/、NextLinkUrlBuilder()自动提取a classnext下一页/a的 href。// 示例使用 NextLinkUrlBuilder 自动提取下一页链接最健壮的通用方案 PageCrawler crawler PageCrawler.builder() .baseUrl(https://example.com/news/) .urlBuilder(new NextLinkUrlBuilder(a.pagination-next, href)) // CSS 选择器定位下一页链接 .stopPolicy(new EmptyContentStopPolicy(div.news-item)) // 若页面中无 div.news-item 元素则停止 .parser(new NewsListParser()) // 注入解析器 .writer(new JsonFileWriter(./output/)) // 注入写入器 .build(); crawler.start(); // 启动爬取提示NextLinkUrlBuilder是我带过三个毕设项目后总结出的首选方案。它不依赖页码数字天然兼容“下一页”文字变化、页码跳转如 1→2→5→10、甚至 AJAX 分页的初始 HTML 快照。比硬编码pagei少踩 80% 的坑。2.2 解析层NewsListParser 的结构化解析与容错处理解析器接收原始Document输出结构化ListNewsItem。关键在于分离关注点selectElements()只负责定位节点extractItem()只负责从单个节点提取字段cleanText()统一做文本清洗。这样每一层都可单独单元测试。public class NewsListParser implements ParserListNewsItem { private static final String ITEM_SELECTOR div.news-item; private static final String TITLE_SELECTOR h2.title a; private static final String TIME_SELECTOR span.time; Override public ListNewsItem parse(Document doc) { Elements items doc.select(ITEM_SELECTOR); ListNewsItem results new ArrayList(); for (Element item : items) { try { NewsItem news extractItem(item); if (news ! null isValid(news)) { // 非空且标题不为广告等无效内容 results.add(news); } } catch (Exception e) { // 单条解析失败不中断整体流程记录警告日志 System.err.println(解析单条新闻失败跳过: item.html().substring(0, Math.min(50, item.html().length()))); } } return results; } private NewsItem extractItem(Element item) { String title cleanText(item.selectFirst(TITLE_SELECTOR)); String url item.selectFirst(TITLE_SELECTOR).attr(href); String timeStr cleanText(item.selectFirst(TIME_SELECTOR)); return new NewsItem(title, url, parseTime(timeStr)); } private String cleanText(Element el) { return el null ? : el.text().trim().replaceAll(\\s, ); } private boolean isValid(NewsItem item) { return item.getTitle() ! null !item.getTitle().contains(广告) !item.getUrl().isEmpty(); } }这段代码体现了三个关键设计点异常隔离单条解析崩溃用try-catch捕获避免整页报废空值防御selectFirst()返回null时cleanText(null)返回空字符串不抛 NPE语义校验isValid()过滤明显无效数据如含“广告”的标题这是学生课设常忽略的“脏数据入口关”。2.3 存储层JsonFileWriter 的原子写入与增量追加存储模块必须解决两个现实问题写入中途崩溃导致文件损坏、重复运行覆盖历史数据。本方案采用“先写临时文件 原子重命名”机制并默认启用时间戳前缀实现增量隔离。public class JsonFileWriter implements WriterListNewsItem { private final Path outputDir; private final DateTimeFormatter formatter DateTimeFormatter.ofPattern(yyyyMMdd_HHmmss); public JsonFileWriter(String outputDirPath) { this.outputDir Paths.get(outputDirPath); try { Files.createDirectories(this.outputDir); } catch (IOException e) { throw new RuntimeException(创建输出目录失败: outputDirPath, e); } } Override public void write(ListNewsItem data, String sourceUrl) { if (data.isEmpty()) return; String timestamp LocalDateTime.now().format(formatter); String filename String.format(news_%s_%s.json, sourceUrl.hashCode() 0xFFFFF, // URL 简易哈希避免长文件名 timestamp); Path tempFile outputDir.resolve(filename .tmp); Path targetFile outputDir.resolve(filename); try (BufferedWriter writer Files.newBufferedWriter(tempFile, StandardCharsets.UTF_8)) { // 使用 Gson 序列化确保中文不乱码 Gson gson new GsonBuilder().setPrettyPrinting().create(); gson.toJson(data, writer); writer.flush(); // 原子重命名Linux/macOS 是原子操作Windows 下尽量保证 Files.move(tempFile, targetFile, StandardCopyOption.REPLACE_EXISTING); System.out.printf(✅ 已保存 %d 条新闻到 %s%n, data.size(), targetFile.getFileName()); } catch (IOException e) { throw new RuntimeException(写入文件失败: targetFile, e); } } }注意sourceUrl.hashCode() 0xFFFFF这个技巧它把任意长度 URL 映射为 6 位十六进制数0–1048575既避免文件名过长Windows 限制 260 字符又保证同源网站的文件名有区分度。这是某高校课程设计答辩时导师当场提问“如何避免文件名冲突”的标准答案。3. 关键实现细节从 URL 构造到反爬绕过的真实落地方案分页爬取的成败往往藏在几个具体参数和一行配置里。这里展开三个最常被忽略、但一错就全盘皆输的实现点。3.1 User-Agent 与 Referer 的组合设置绕过基础服务端拦截很多网站对无 UA 的请求直接返回 403 或空页。但只设 UA 不够——若目标页是通过点击列表页跳转而来服务端可能校验Referer。jsoup 默认不发 Referer需手动添加Connection connection Jsoup.connect(url) .userAgent(Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36) .header(Referer, https://example.com/news/) // 必须是上级页面 URL .timeout(10000) .followRedirects(true);注意Referer必须是上一页的实际 URL不能写死为首页。在分页循环中应将上一页的 URL 作为Referer传给下一页请求。这是某跨平台新闻聚合毕设中前期 70% 请求失败的根源——A同学只设置了 UA却没补 Referer直到用浏览器开发者工具对比请求头才发现差异。3.2 Cookie 管理应对登录态与会话保持当目标网站需要登录才能看全部分页如后台文章管理页或存在 CSRF Token 校验时必须复用 Cookie。jsoup 本身不维护 CookieStore需自行管理// 全局 Cookie 存储线程安全 private static final MapString, String COOKIE_STORE new ConcurrentHashMap(); // 在首次登录成功后提取并保存 Cookie private void saveLoginCookies(Connection.Response loginResponse) { MapString, String cookies loginResponse.cookies(); cookies.forEach((key, value) - COOKIE_STORE.put(key, value)); } // 每次请求前注入 Cookie private Connection addCookies(Connection connection) { COOKIE_STORE.forEach(connection::cookie); return connection; } // 使用示例 Connection conn addCookies(Jsoup.connect(nextUrl)); Document doc conn.get();这个方案轻量且可控。相比CookieManager它避免了全局 Cookie 泄露风险相比每次手动cookie(JSESSIONID, ...)它支持动态更新。某实验室的论文数据采集项目就因未管理 Cookie导致第 3 页开始返回登录页调试两天才发现是会话超时。3.3 动态参数提取从 HTML 中抠出隐藏的 sign/token部分网站分页 URL 包含动态签名如?page2signabc123xyzts1700000000。sign可能由前端 JS 计算生成但其原始参数如page、ts一定存在于 HTML 中常藏在script或meta标签里。此时需用 jsoup 先解析 HTML再用正则提取// 从页面中提取 ts 和 sign示例页面中有 scriptvar ts1700000000, signabc123;/script private MapString, String extractDynamicParams(Document doc) { Elements scripts doc.select(script); for (Element script : scripts) { String text script.html(); Matcher tsMatcher Pattern.compile(ts\\s*\\s*(\\d)).matcher(text); Matcher signMatcher Pattern.compile(sign\\s*\\s*\([^\])\).matcher(text); if (tsMatcher.find() signMatcher.find()) { MapString, String params new HashMap(); params.put(ts, tsMatcher.group(1)); params.put(sign, signMatcher.group(1)); return params; } } return Collections.emptyMap(); } // 在构造下一页 URL 时调用 MapString, String params extractDynamicParams(currentDoc); String nextUrl baseUrl ?page (currentPage 1) ts params.get(ts) sign params.get(sign);这是“玄学”参数处理的标准解法不逆向 JS只信 HTML 源码。只要页面渲染时把参数写进了 HTML就能用 jsoup 拿到。某电商价格监控课设曾因试图用 Selenium 执行 JS 生成 sign导致性能暴跌、内存溢出换成此法后单页耗时从 8s 降至 0.3s。4. 常见问题排查5 个让初学者熬夜到凌晨的典型翻车点分页爬取的调试成本极高往往卡在某个看似微小的细节。以下是我在指导多个项目时学生最高频报错的 5 个点按“现象 → 原因 → 解决”给出可立即执行的排查路径。4.1 现象第 1 页正常第 2 页返回 403 Forbidden原因服务端校验Referer头而第 2 页请求的Referer是空或错误如写死为首页而非第 1 页 URL。解决检查Jsoup.connect(url).header(Referer, ...)中传入的 Referer 是否为上一页的实际 URL用curl -v -H Referer: https://xxx/page1 https://xxx/page2手动验证。4.2 现象爬到第 5 页突然开始重复后续全是第 5 页内容原因URL 构造逻辑错误如pagei但网站实际用offset20导致i5时offset超出范围服务端返回第 1 页缓存。解决打开浏览器开发者工具 → Network 标签 → 手动点“下一页”观察真实请求 URL 和响应内容用NextLinkUrlBuilder替代硬编码页码。4.3 现象解析出的标题全是空字符串但浏览器能看到原因目标元素被 JavaScript 渲染如div.news-item在 DOM 加载后才由 JS 插入而 jsoup 只获取静态 HTML。解决查看网页源代码CtrlU确认div.news-item是否存在于原始 HTML 中若不存在说明是 SPA 页面jsoup 不适用需换 Puppeteer 或提供渲染后 HTML 的接口。4.4 现象程序运行一会儿就卡住CPU 占用 100%原因Jsoup.connect().get()默认无限等待遇到网络抖动或服务端不响应时线程挂起。解决必须设置超时.timeout(10000)单位毫秒并增加重试机制如失败后 sleep 1s 再试 2 次在PageCrawler中加入最大重试计数避免死循环。4.5 现象JSON 文件写入一半程序崩溃再运行时文件损坏无法读取原因直接Files.write(path, content)覆盖写入崩溃时文件被截断。解决严格采用“临时文件 原子重命名”模式见 2.3 节代码生产环境建议额外增加 JSON 格式校验用Gson.fromJson(content, Object.class)预检。提示这 5 条是我给 A同学毕设答辩前划的重点。他当时卡在第 4.1 条反复检查代码却漏看了 Referer最后用 curl 三分钟定位。从此我养成了一个习惯任何 HTTP 状态码异常第一反应是用 curl 复现而不是改 Java 代码。5. 方案验收与答辩准备用三张表建立可信交付证据链毕设或课设答辩时老师最常问“你这个爬虫真的能稳定跑完 100 页吗数据准确率多少怎么证明不是假数据”——这时候光说“我测试过了”毫无说服力。你需要一份可展示、可验证、可回溯的验收材料。我通常要求学生提交三张表它们共同构成一条完整的证据链。5.1 分页过程日志表log_summary.csv这是最核心的交付物记录每一次请求的完整生命周期。用 CSV 格式方便 Excel 打开和筛选。page_nourlstatus_codeitem_countduration_mserror_msgtimestamp1https://example.com/news/?page12002012452024-03-15 10:02:332https://example.com/news/?page2200209872024-03-15 10:02:353https://example.com/news/?page340303210Forbidden2024-03-15 10:02:404https://example.com/news/?page32002011022024-03-15 10:02:42实现要点在PageCrawler的fetchPage()方法中用System.currentTimeMillis()记录起止时间捕获HttpStatusException获取status_code用doc.select(...).size()记录item_count。答辩时打开这张表老师一眼就能看出稳定性、成功率、性能瓶颈。5.2 数据质量抽样表quality_sample.xlsx随机抽取 5–10 页的原始 HTML 片段与解析结果并排对比证明解析逻辑正确。page_nohtml_snippet (first 100 chars)parsed_titleexpected_titlepass1div classnews-itemh2 classtitlea href/n1.html人工智能新突破/a/h2人工智能新突破人工智能新突破✅5div classnews-itemh2 classtitlea href/n5.html【公告】系统维护通知/a/h2【公告】系统维护通知【公告】系统维护通知✅12div classnews-itemh2 classtitlea href/n12.html /a/h2空❌抽样原则覆盖首页、中间页、末页、含特殊字符页如标题带【】、emoji、含空字段页。答辩时只需展示 3 行老师就会相信你做了真实校验。5.3 配置与参数对照表config_reference.md明确列出所有可配置项及其生产环境推荐值体现工程化思维。参数说明课设推荐值毕设推荐值修改位置maxRetry单页请求最大重试次数23PageCrawler.BuildertimeoutMs单次请求超时毫秒1000015000Jsoup.connect().timeout()delayMs页间延迟毫秒10002000Thread.sleep()in loopstopPolicy终止策略MaxPageCountStopPolicy(50)EmptyContentStopPolicy(article)PageCrawler.builder().stopPolicy()这张表的价值在于让老师确信你理解每个参数的意义而不是盲目复制粘贴。某高校答辩中导师指着delayMs问“为什么设 2000 而不是 500”学生答“避免触发 QPS 限流符合 robots.txt 建议”当场加分。从那以后我每次带课设都强制学生在README.md里手写这三张表——不是为了凑字数而是逼他们真正走完“写代码 → 跑数据 → 查日志 → 验结果”的完整闭环。没有日志表的爬虫就像没有仪表盘的飞机没有抽样表的解析就像没有测试用例的算法。希望帮到你。本文还有配套的精品资源点击获取