资讯详情 jsoup分页爬取实战:轻量稳定HTML数据采集方案
📅 2026/10/17 13:40:15
简介本资源是一份基于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 做分页爬取而不是直接上 Selenium 或 Scrapy很多同学一上来就卡在选型上看到“爬网页”本能想到 Selenium 模拟点击、Scrapy 构建管道、甚至有人想用 Playwright 写个黑匣子脚本——结果毕设答辩前一周发现环境配不齐、渲染超时、反爬报错堆成山最后硬着头皮重写。而这个「jsoup 分页爬取网页」方案恰恰是为轻量、稳定、可调试、易交付的场景量身定做的它不依赖浏览器内核不启动 WebDriver不维护异步事件循环只要目标网站结构清晰比如新闻列表页有a href/news/page/2下一页/a、内容以 HTML 标签形式静态呈现非 JS 渲染就能用几行 Java 代码把几十页数据稳稳抓下来。我带过的三个课程设计里A同学用它三天搭完毕业论文数据采集模块B同学靠它给某高校实验室的文献摘要库做月度更新C同学甚至把这套逻辑封装进 Spring Boot 接口供前端分页展示调用。它不是万能锤但对「结构化 HTML 明确分页链接 无强动态渲染」这类真实课设/毕设高频场景是少有能一次写对、二次复用、三次答辩不翻车的落地选择。2. 总体架构设计三层结构与四个核心模块的职责划分整个方案采用清晰的三层结构数据获取层 → 数据解析层 → 业务处理层。不搞微服务拆分不加消息队列所有逻辑压缩在一个 Maven 工程内便于学生打包提交、导师本地一键运行。项目结构如下src/main/java下├── crawler/ │ ├── PageCrawler.java // 主爬虫调度器控制分页循环、异常重试、终止条件 │ ├── HtmlParser.java // 解析器提取标题/正文/时间等字段返回统一 POJO │ └── UrlGenerator.java // 分页 URL 构造器支持数字递增、参数替换、锚点跳转三类模式 ├── model/ │ └── Article.java // 数据实体含 title, content, publishDate, url 等字段 ├── storage/ │ └── CsvWriter.java // 存储适配器支持 CSV / JSON / SQLite 三种输出格式切换 └── Main.java // 入口类配置起始页、最大页数、请求头启动流程这种结构不是为了炫技而是为答辩时能说清楚每一行代码“谁在干啥、为啥这么干”。比如PageCrawler不负责解析只管“翻页”HtmlParser不关心 URL 怎么来只管“从 HTML 里抠字段”CsvWriter不知道爬了多少页只管“把 Article 对象存成文件”。模块边界清晰改一个功能不影响其他查 Bug 时能快速定位到具体类。2.1 分页策略设计三种常见分页模式的识别与适配实际项目中分页链接形态千差万别。我们不写死for (int i 1; i 100; i)这种玄学循环而是按真实网页结构抽象出三类主流模式并在UrlGenerator中提供对应构造方法数字递增型最常见https://example.com/list?page1→?page2适用场景后台分页参数明确URL 规则固定实现要点generateNextUrl(String baseUrl, int currentPage)方法中用正则替换page\d部分避免字符串拼接出错路径递增型https://example.com/news/1.html→/news/2.html适用场景静态生成页面URL 路径含页码实现要点需先解析当前 URL 路径提取末尾数字再构造新路径注意处理/news/index.html这类无数字情况锚点/按钮型页面内a classnext-page href/list/2下一页/a适用场景前端通过a标签跳转无显式参数实现要点PageCrawler在每次请求后用 jsoup 的doc.select(a.next-page).attr(href)提取下一页地址再拼接 base URI提示UrlGenerator类中所有方法均声明为static且接受String currentUrl作为输入确保无状态、可单元测试。不要在类里缓存baseUrl避免多线程下 URL 混乱。2.2 请求控制与反爬基础防护User-Agent、连接超时与重试机制jsoup 默认请求头极简User-Agent: Java/1.8.0_291极易被服务器拦截。我们在PageCrawler初始化时强制注入合理配置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) .timeout(10000) // 连接读取总超时 10 秒 .followRedirects(true) // 自动处理 301/302 重定向 .ignoreContentType(true); // 防止因 Content-Type 不匹配如 text/plain导致解析失败关键参数说明timeout(10000)必须设否则遇到慢响应页面会无限阻塞线程课设演示时当场卡死ignoreContentType(true)某些网站返回text/plain但内容实为 HTML不加此参数 jsoup 会抛UnsupportedContentTypeExceptionfollowRedirects(true)处理登录跳转、地域重定向等常见场景避免拿到 302 响应体却误判为“空页面”。重试机制采用指数退避Exponential Backoff首次失败等待 1 秒第二次 2 秒第三次 4 秒最多重试 3 次。代码逻辑嵌入PageCrawler.fetchPage()方法中不依赖外部库for (int attempt 0; attempt MAX_RETRY; attempt) { try { return Jsoup.connect(url).execute(); // 执行请求 } catch (IOException e) { if (attempt MAX_RETRY - 1) throw e; // 最后一次失败抛出原始异常 Thread.sleep((long) Math.pow(2, attempt) * 1000); // 指数等待 } }这样设计的好处是既避免频繁请求触发风控又保证网络抖动时任务不中断且所有重试逻辑集中在一处调试时加日志一目了然。2.3 数据解析层用 CSS 选择器精准定位拒绝 XPath 玄学调试HtmlParser是整个方案的“眼睛”。它不写正则去硬匹配 HTML 字符串那是后悔药剂量最大的操作而是完全基于 jsoup 的 CSS 选择器语法实现高可读、易维护的字段提取。典型解析逻辑如下以新闻标题、发布时间、正文为例public Article parse(String html, String sourceUrl) { Document doc Jsoup.parse(html); Element titleEl doc.selectFirst(h1.title, .article-title, h2); // 多 selector 回退机制 Element timeEl doc.selectFirst(time, .publish-time, [class*date]); Element contentEl doc.selectFirst(.article-content, #content, .post-body); Article article new Article(); article.setTitle(titleEl ! null ? titleEl.text().trim() : ); article.setPublishDate(parseDate(timeEl ! null ? timeEl.text() : )); article.setContent(contentEl ! null ? contentEl.text().trim() : ); article.setUrl(sourceUrl); return article; }设计说明使用selectFirst()而非select()避免取到多个元素后手动取[0]减少 NPE 风险多 selector 回退如h1.title, .article-title, h2兼容不同网站模板一个 selector 失效自动降级parseDate()方法内部用DateTimeFormatter尝试多种常见日期格式yyyy-MM-dd,yyyy年MM月dd日,MMM dd, yyyy失败则返回null不中断主流程所有.text().trim()强制调用清除换行、空格、零宽字符避免 CSV 导出时字段错位。注意不要在parse()方法里做网络请求如加载图片 URL、不要调用耗时 IO如写文件。它只做一件事从 HTML 字符串到 Java 对象的映射。3. 关键模块实现PageCrawler 分页调度器的完整代码与逻辑拆解PageCrawler是整个流程的“心脏”它决定爬多少页、何时停止、如何应对异常。下面给出其核心方法crawlPages()的完整实现已去除日志和异常包装保留主干逻辑public ListArticle crawlPages(String startUrl, int maxPages) { ListArticle allArticles new ArrayList(); String currentUrl startUrl; int crawledPages 0; while (crawledPages maxPages currentUrl ! null) { try { // 1. 发起请求获取 HTML Response response fetchPage(currentUrl); String html response.body(); // 2. 解析当前页文章列表注意不是单篇文章是列表页 ListString articleUrls extractArticleUrls(html, currentUrl); // 3. 遍历每篇文章 URL抓取详情页并解析 for (String articleUrl : articleUrls) { String articleHtml fetchPage(articleUrl).body(); Article article new HtmlParser().parse(articleHtml, articleUrl); allArticles.add(article); Thread.sleep(300); // 友好延时避免请求过密 } // 4. 提取下一页 URL currentUrl extractNextPageUrl(html, currentUrl); crawledPages; System.out.printf(✅ 已爬取第 %d 页共获取 %d 篇文章\n, crawledPages, allArticles.size()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } catch (Exception e) { System.err.printf(❌ 第 %d 页爬取失败%s\n, crawledPages 1, e.getMessage()); // 失败时不中断循环继续尝试下一页防止某页 HTML 结构突变导致全盘崩溃 currentUrl extractNextPageUrlFallback(currentUrl); // 启用备用策略 } } return allArticles; }关键逻辑说明循环终止条件双保险crawledPages maxPages控制总量currentUrl ! null控制自然结束如最后一页无“下一页”链接列表页与详情页分离extractArticleUrls()从列表页提取所有a href...文章链接再逐个请求详情页——这是结构化数据采集的黄金准则比在列表页硬抠摘要更可靠Thread.sleep(300)放在内层循环即每抓一篇详情页后延时而非每页后延时更符合真实用户行为降低被限速概率异常处理不 throw 出去捕获所有Exception仅打印错误并尝试降级extractNextPageUrlFallback确保单页失败不影响整体进度extractNextPageUrlFallback()方法实现当 CSS 选择器找不到下一页链接时尝试currentUrl.replace(page\\d, page (currentPage 1))数字递增作为保底策略。该类还包含两个重要辅助方法fetchPage(String url)封装了 2.2 节所述的带重试、超时、UA 的请求逻辑extractArticleUrls(String html, String baseUrl)用doc.select(div.list-item a[href])提取所有文章链接并通过baseUri补全相对路径避免href/article/123解析成http://example.com/article/123。4. 常见问题排查五条血泪经验总结的翻车点与解法做这个方案时我见过太多同学在最后一步导出 CSV 时发现字段全是空的或者爬到第 5 页突然停止——问题往往不出在算法而在几个极易忽略的细节。以下是真实踩坑记录按发生频率排序4.1 现象爬取内容为空字符串title/content 全是 原因HtmlParser中 CSS 选择器写错或目标网站用了>// Main.java 中 DataStorage storage new CsvWriter(output/articles.csv); // 切换为 CSV // DataStorage storage new JsonWriter(output/articles.json); // 切换为 JSON // DataStorage storage new SqliteWriter(db/articles.db); // 切换为 SQLite5.1 CSV 输出兼顾 Excel 兼容性与字段完整性CsvWriter使用 OpenCSV关键配置如下CSVWriter writer new CSVWriter( new FileWriter(filePath), CSVWriter.DEFAULT_SEPARATOR, CSVWriter.NO_QUOTE_CHARACTER, CSVWriter.DEFAULT_ESCAPE_CHARACTER, CSVWriter.RFC4180_LINE_END ); // RFC4180_LINE_END 确保换行符为 \r\nExcel 打开不乱码字段顺序严格按Article类属性定义url,title,content,publishDate。特别处理publishDate若为null写入N/A而非空字符串避免 Excel 日期列识别失败。5.2 JSON 输出结构化嵌套方便后续 API 对接JsonWriter使用 Jackson输出格式为标准 JSON Array[ { url: https://example.com/article/1, title: 第一篇文章, content: 这里是正文内容..., publishDate: 2024-03-15 } ]关键点ObjectMapper设置WRITE_DATES_AS_TIMESTAMPS false确保LocalDateTime字段序列化为2024-03-15T09:30:00格式而非毫秒数。5.3 SQLite 输出支持增量爬取与去重SqliteWriter创建表时带唯一索引防止重复插入CREATE TABLE IF NOT EXISTS articles ( id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT UNIQUE NOT NULL, title TEXT, content TEXT, publish_date TEXT, crawled_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );增量爬取逻辑在PageCrawler中实现每次解析完一篇文章先SELECT COUNT(*) FROM articles WHERE url ?仅当不存在时才INSERT。这样即使程序中断重启也不会重复抓取已存数据。注意SQLite 驱动需添加org.xerial:sqlite-jdbc:3.42.0.0依赖且 Windows 下注意路径中的反斜杠转义C:\\data\\db.db。6. 验收与答辩三步验证法 一份让导师眼前一亮的演示文档答辩不是讲原理是证明“这东西真能用”。我带过的项目里凡是顺利通过的都做了这三步验证缺一不可6.1 功能验收用真实网站跑通最小闭环选一个结构简单、无反爬的公开网站如某高校新闻网、开源项目博客执行以下命令mvn compile exec:java -Dexec.mainClassMain \ -Dexec.argshttps://example.com/news/ 5预期输出控制台打印✅ 已爬取第 1 页共获取 12 篇文章…✅ 已爬取第 5 页共获取 60 篇文章output/articles.csv文件存在用 Excel 打开可见 60 行数据每行titlecontent字段非空任取 CSV 中一条url浏览器访问确认内容与 CSV 中titlecontent完全一致。提示不要用知乎、微博等强反爬站做演示导师现场联网测试失败会直接质疑方案可行性。选自己搭建的测试页如http://localhost:8080/test-list.html最稳妥。6.2 代码验收重点检查三处“答辩必问点”导师常问的代码细节其实就藏在这三个地方提前准备好答案检查位置问题示例应答要点UrlGenerator.extractNextPageUrl()“如果下一页链接是 JavaScriptonclick触发的你怎么处理”“jsoup 不执行 JS所以此方案默认不支持 onclick 分页但我会在文档中明确标注适用边界并提供 fallback 方案当 CSS 选择器失效时自动启用数字递增模式如 page1→page2。”HtmlParser.parse()“怎么保证不同网站的标题选择器都能命中”“采用多 selector 回退机制h1.title, .article-title, h2优先匹配高置信度选择器同时在 README.md 中提供‘选择器调试指南’教用户用浏览器开发者工具快速定位。”PageCrawler.crawlPages()“如果爬到一半网络断了怎么续爬”“通过 SQLite 的url UNIQUE约束实现天然去重下次运行时已存 URL 自动跳过。若需精确断点续传可扩展crawlPages(String startUrl, int fromPage, int toPage)方法。”6.3 演示文档一份 2 页 PDF 胜过 20 分钟口头解释答辩前务必准备一份精简 PDF命名jsoup-crawler-demo.pdf包含三页核心内容第 1 页架构图 运行截图手绘风格架构图三层结构 箭头标注数据流向右侧贴一张终端运行截图含✅ 已爬取第 5 页和output/目录ls -l结果第 2 页关键配置表 常见问题速查表格列出maxPagestimeoutUser-Agent等 5 个核心参数每行含“推荐值”“修改影响”“调试建议”三列下方用小字号列出 4.1–4.5 节的排查口诀如“内容为空→ 查源码 → 换选择器”第 3 页扩展性说明可选用一句话说明“如何接入 Spring Boot”Service注入PageCrawler暴露POST /crawl接口和“如何加代理”Jsoup.connect(url).proxy(127.0.0.1, 8080)展现工程化思维。这份文档不追求美观但必须打印出来放在答辩桌上——导师翻两页就能抓住重点比你紧张地背稿子强十倍。从那以后我每次带课设都强制学生在README.md里写清“本方案适用的 3 个网站特征”和“不适用的 2 个网站特征”并附上自己跑通的测试页截图。不是为了显得严谨而是让答辩时任何问题都能指向具体代码行而不是在“理论上应该可以”里打转。希望帮到你。本文还有配套的精品资源点击获取