简介本资源是一份基于Java的jsoup分页爬虫实战项目面向Java初学者及网页数据采集入门开发者解决结构化HTML页面多页批量抓取与解析的核心问题。项目完整封装了URL参数化构造、CSS选择器精准定位、分页链接自动提取、多页循环请求与基础数据清洗等关键流程适合作为WebCollectorjsoup混合爬虫学习的过渡实践案例。压缩包共16个文件含7个核心jar包如jsoup-1.9.2.jar、WebCollector-2.52.jar等运行依赖、2个Eclipse工程配置文件.classpath、.project、1个Java源码文件Test.java及日志、缓存、偏好设置等辅助文件整体3.08MB结构体现典型Java桌面爬虫工程组织方式。已有1082人学习下载读者可直接导入Eclipse运行调试获取可复用的分页爬取模板、清晰的目录模块划分、常见异常处理逻辑及jar依赖管理范式。1. jsoup分页爬取网页不是写个 for 循环就能跑通的“简单任务”很多同学拿到「jsoup分页爬取网页」这个毕设/课设题目时第一反应是不就是用for (int i 1; i 100; i) { Document doc Jsoup.connect(url ?page i).get(); }吗——结果跑三页就 403翻五页就反爬拦截到第十二页直接返回空文档控制台连异常都不抛只有一片沉默。这不是代码写错了是没把「分页爬取」当成一个带状态、有节奏、需反馈闭环的工程行为来设计。这套方案专为需要稳定获取多页结构化数据的学生和初级开发者准备它覆盖从真实网站分页特征识别数字页码 / 下一页按钮 / 滚动加载伪装、请求节流与会话复用、HTML 解析容错、异常降级策略到最终数据落库或导出的完整链路。你不需要懂 Selenium也不必上分布式框架——用纯 Java jsoup 就能交出一份答辩老师点头、企业导师愿意放进测试环境跑一周的真实可用方案。2. 分页逻辑建模先读懂网站怎么“分页”再决定你怎么“爬”分页不是数学题是逆向工程。同一句“下一页”在不同网站背后可能是五种实现机制。硬套?page2会翻车因为很多站点早就不暴露页码参数了。我带过三个课程设计组90% 的失败都卡在这一步没做前置分析直接开写。2.1 识别真实分页类型四类典型模式与检测方法必须人工打开目标网站用浏览器开发者工具F12逐页操作并观察网络请求变化。重点看Network → XHR/Fetch标签页而不是 Elements。常见模式如下分页类型典型表现jsoup 应对关键点是否推荐初学者使用URL 参数分页list.html?page3、/api/data?page5size20直接拼接 URL注意编码如中文参数用URLEncoder.encode()✅ 强烈推荐最可控“下一页”链接提取HTML 中存在a href/news/list_4.html下一页/a用doc.select(a:contains(下一页)).attr(abs:href)务必加abs:前缀转绝对路径✅ 推荐但需处理“下一页”文字变体如“»”、“Next”、“下页”AJAX 分页无刷新点击页码时 Network 出现/api/v1/articles类请求Response 是 JSON❌ jsoup 无法直接发 AJAX 请求必须改用HttpClient或OkHttp配合JsonPath若坚持用 jsoup只能模拟其 Referer、User-Agent、Cookie 等头信息伪造请求⚠️ 不推荐初学者易陷入黑匣子调试滚动加载Infinite Scroll滚动到底部自动加载新内容Network 中出现带offset30limit10的请求❌ jsoup 无 DOM 事件驱动能力无法触发滚动必须换方案如 Puppeteer Java 绑定或退化为按固定 offset 构造 URL风险高❌ 明确不推荐提示某高校课程设计中A同学坚持用 jsoup 爬某新闻站的滚动分页折腾两周后发现该站实际是https://xxx.com/api/news?start0count10只是前端隐藏了 URL——他本该在 Network 里多看一眼请求地址栏而不是反复修改doc.select(div.item)的选择器。2.2 构建可扩展的分页控制器PageNavigator 接口与三种实现把分页逻辑从主爬虫解耦是后期维护和更换目标网站的关键。我一般定义一个PageNavigator接口public interface PageNavigator { /** * 获取当前页的 Document 对象 */ Document getCurrentPage() throws IOException; /** * 判断是否还有下一页 */ boolean hasNext(); /** * 跳转到下一页内部更新状态 */ void next() throws IOException; }对应三种实现以某博客列表页为例2.2.1 UrlParamNavigator基于 page 参数的最简实现public class UrlParamNavigator implements PageNavigator { private final String baseUrl; // e.g. https://example.com/posts private int currentPage 1; private final int maxPage; private final Connection connection; public UrlParamNavigator(String baseUrl, int maxPage) { this.baseUrl baseUrl; this.maxPage maxPage; this.connection Jsoup.connect().userAgent(Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36); } Override public Document getCurrentPage() throws IOException { String url baseUrl ?page currentPage; return connection.url(url).timeout(10000).get(); } Override public boolean hasNext() { return currentPage maxPage; } Override public void next() { currentPage; } }设计说明connection复用避免重复创建连接对象减少 TIME_WAIT 占用timeout(10000)是血泪经验——不设超时某次 DNS 故障会让整个爬虫卡死 3 分钟maxPage不建议硬编码应通过首页解析span classpage-total共 86 页/span动态获取见 3.2 节。2.2.2 LinkTextNavigator基于“下一页”文本链接的稳健实现public class LinkTextNavigator implements PageNavigator { private String currentUrl; private final String nextPageText; private final Connection connection; public LinkTextNavigator(String firstUrl, String nextPageText) { this.currentUrl firstUrl; this.nextPageText nextPageText; // 可传入 下一页, Next, » this.connection Jsoup.connect().userAgent(Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36); } Override public Document getCurrentPage() throws IOException { return connection.url(currentUrl).timeout(10000).get(); } Override public boolean hasNext() { try { Document doc getCurrentPage(); // 尝试多种选择器文字匹配、class 匹配、aria-label 匹配 Elements nextLinks doc.select(a:contains( nextPageText ), a.next-page, a[aria-labelnext]) .not(a:disabled, a[disabled]); return !nextLinks.isEmpty() nextLinks.get(0).hasAttr(abs:href); } catch (IOException e) { return false; // 网络失败视为无下一页避免死循环 } } Override public void next() throws IOException { Document doc getCurrentPage(); Elements nextLinks doc.select(a:contains( nextPageText ), a.next-page) .not(a:disabled); if (!nextLinks.isEmpty()) { currentUrl nextLinks.get(0).attr(abs:href); // abs:href 自动补全协议域名 } } }设计说明not(a:disabled)过滤掉已禁用的“下一页”按钮常见于最后一页避免死循环abs:href是关键否则相对路径如list_5.html会导致后续请求 404try-catch包裹hasNext()防止某页网络异常导致整个流程中断——这是生产级爬虫的底线。3. jsoup 核心解析层从“能取到”到“取得稳、取得准”很多人以为 jsoup 就是select(div.title)其实真正难的是当页面结构微调、字段缺失、编码错乱、广告插入时如何让解析逻辑不崩、数据不丢、日志可追溯。这一层决定了你的毕设能不能跑满 100 页不出错。3.1 容错式元素提取ElementExtractor 工具类封装直接调用doc.select(h1).text()在遇到空元素时会返回空字符串但你无法区分“真为空”还是“选择器写错”。我习惯封装一层带上下文记录的提取器public class ElementExtractor { private final Document doc; private final String sourceUrl; public ElementExtractor(Document doc, String sourceUrl) { this.doc doc; this.sourceUrl sourceUrl; } /** * 安全提取文本返回 Optional 避免空指针记录缺失日志 */ public OptionalString text(String selector) { Elements elements doc.select(selector); if (elements.isEmpty()) { System.err.println([WARN] 未找到选择器 selector 来源页 sourceUrl); return Optional.empty(); } String text elements.first().text().trim(); if (text.isEmpty()) { System.err.println([INFO] 选择器 selector 匹配到空文本来源页 sourceUrl); } return Optional.of(text); } /** * 安全提取属性值如 href、src支持默认值 */ public String attr(String selector, String attrName, String defaultValue) { Elements elements doc.select(selector); if (elements.isEmpty()) { return defaultValue; } String value elements.first().attr(attrName); return value.isEmpty() ? defaultValue : value; } /** * 提取多个同类型元素如所有文章标题自动去重 过滤空值 */ public ListString texts(String selector) { return doc.select(selector).stream() .map(Element::text) .map(String::trim) .filter(s - !s.isEmpty()) .distinct() .collect(Collectors.toList()); } }使用示例解析单篇文章页Document articleDoc Jsoup.connect(articleUrl).get(); ElementExtractor ex new ElementExtractor(articleDoc, articleUrl); String title ex.text(h1.post-title).orElse(【缺失标题】); String author ex.text(span.author).orElse(匿名); String publishTime ex.text(time.date).orElseGet(() - ex.text(div.meta span:nth-child(2)).orElse(未知时间) ); ListString tags ex.texts(a.tag-link);设计说明所有.text()调用都包裹在Optional中强制调用方处理空值避免NullPointerExceptionSystem.err.println输出带[WARN]前缀的日志方便 grep 过滤定位问题页texts()方法内置distinct()和空值过滤解决广告模块插入重复标题的常见问题。3.2 编码与重定向鲁棒性绕过乱码与跳转陷阱jsoup 默认使用 UTF-8但很多老网站声明charsetgb2312却返回 UTF-8 内容或中间经历 302 跳转后编码丢失。不处理会导致标题变成æææ°é»。public Document safeConnect(String url) throws IOException { Connection conn Jsoup.connect(url) .userAgent(Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36) .timeout(15000) .followRedirects(true) // 必须开启否则 302 后 document 为空 .ignoreContentType(true); // 关键允许解析非 text/html 类型如 text/plain 乱码页 Connection.Response response conn.execute(); String contentType response.contentType(); String charset extractCharset(contentType); // 从 Content-Type 提取 charset if (charset null) { charset guessCharset(response.body()); // 用字节频次猜测见下方 } // 用正确 charset 重新解析 byte[] bodyBytes response.bodyAsBytes(); String html new String(bodyBytes, charset); return Jsoup.parse(html, url); // 第二个参数是 baseUri确保 abs:href 正确 } private String extractCharset(String contentType) { if (contentType null) return UTF-8; Pattern p Pattern.compile(charset([^;\\s]), Pattern.CASE_INSENSITIVE); Matcher m p.matcher(contentType); return m.find() ? m.group(1) : UTF-8; } private String guessCharset(byte[] bytes) { // 简化版检查 BOM 或高频字节组合生产环境建议用 juniversalchardet if (bytes.length 2 bytes[0] (byte) 0xEF bytes[1] (byte) 0xBB) return UTF-8; if (bytes.length 2 bytes[0] (byte) 0xFF bytes[1] (byte) 0xFE) return UTF-16LE; return GBK; // 中文站 fallback }设计说明ignoreContentType(true)是玄学开关某些反爬站点故意返回Content-Type: image/png但 body 是 HTML不设此参数 jsoup 直接拒绝解析followRedirects(true)必须开启否则https://xxx.com/login?redirect/admin类跳转会失败guessCharset是后悔药当服务器不声明 charset 时靠字节特征兜底比new String(bytes)盲猜强十倍。4. 稳定性加固反爬对抗、节流控制与异常降级没有反爬意识的爬虫就像没关车门就上高速。jsoup 本身不带反爬但你可以用三层防御把它变成“低调的老实人”。4.1 请求节流与随机化让请求像真人而不是扫雷车固定间隔如Thread.sleep(1000)太机械。我采用“基础间隔 随机抖动 页面级延迟”三级策略public class RequestThrottler { private final long baseDelayMs; private final Random random; private final MapString, Long lastRequestTime new ConcurrentHashMap(); public RequestThrottler(long baseDelayMs) { this.baseDelayMs baseDelayMs; this.random new Random(); } /** * 针对特定域名的节流防单域名过载 */ public void throttleForDomain(String url) throws InterruptedException { String domain extractDomain(url); long now System.currentTimeMillis(); Long lastTime lastRequestTime.get(domain); if (lastTime ! null) { long elapsed now - lastTime; if (elapsed baseDelayMs) { long sleepMs baseDelayMs - elapsed random.nextInt(500); // ±500ms 抖动 Thread.sleep(Math.max(100, sleepMs)); // 至少睡 100ms } } lastRequestTime.put(domain, System.currentTimeMillis()); } private String extractDomain(String url) { try { return new URL(url).getHost(); } catch (Exception e) { return unknown; } } }集成到爬取循环中RequestThrottler throttler new RequestThrottler(2000); // 基础 2s while (navigator.hasNext()) { try { throttler.throttleForDomain(navigator.getCurrentUrl()); Document doc navigator.getCurrentPage(); // 解析逻辑... parsePage(doc); } catch (HttpStatusException e) { if (e.getStatusCode() 403 || e.getStatusCode() 429) { System.err.println([BLOCKED] 被封禁暂停 60 秒后重试 e.getUrl()); Thread.sleep(60000); continue; // 重试当前页 } throw e; } navigator.next(); }设计说明ConcurrentHashMap支持多线程安全为未来扩展多域名并发留余地random.nextInt(500)让间隔在2000~2500ms波动打破规律性Thread.sleep(100)最小值防止过度频繁如解析失败重试时。4.2 Cookie 与 Session 复用绕过登录态校验很多网站分页需登录态如后台文章管理页。jsoup 本身不维护 session但可以手动传递 cookie// 登录后获取 cookies Connection.Response loginRes Jsoup.connect(https://xxx.com/login) .data(username, user) .data(password, pass) .method(Connection.Method.POST) .execute(); MapString, String cookies loginRes.cookies(); // 后续所有请求带上 cookies Document doc Jsoup.connect(https://xxx.com/admin/posts?page2) .cookies(cookies) .get();关键细节loginRes.cookies()返回的是MapString, String直接复用即可不要自己拼Cookie: keyvalue; key2value2字符串jsoup 会帮你处理若网站用 JWT 存 cookie需额外提取document.cookie中的 token 字段用正则。4.3 常见问题排查五条让你少熬三夜的血泪经验注意以下问题均来自真实课程设计翻车现场按发生频率排序。4.3.1 现象爬到第 7 页突然返回首页 HTML且doc.title()是 “用户登录”原因网站设置了 session 过期如 10 分钟无操作而你的爬虫间隔太久或未复用 cookies。解决登录后立即获取 cookies并在每次Jsoup.connect()时显式传入.cookies(cookies)添加心跳请求如每 5 分钟访问一次/api/keepalive。4.3.2 现象doc.select(div.list-item)在本地测试正常部署到服务器后返回空原因服务器 IP 被加入黑名单返回的是反爬中间页如 Cloudflare 验证页但状态码仍是 200。解决检查doc.body().text()是否包含 “Checking your browser”、“请开启 JavaScript” 等关键词添加 UA Referer Accept-Language 头模拟真实浏览器。4.3.3 现象中文标题全部乱码但网页源码查看是正常的原因jsoup 解析时未指定 charset且服务器未在 HTTP Header 中声明Content-Type: text/html; charsetutf-8。解决用Connection.execute()获取原始字节再用new String(bytes, UTF-8)手动解析见 3.2 节。4.3.4 现象select(a[href])提取的链接全是相对路径如/article/123保存后打不开原因未使用abs:href属性而是用了href。解决永远用element.attr(abs:href)替代element.attr(href)或在Jsoup.parse(html, baseUrl)中传入正确的 baseUri。4.3.5 现象程序运行半小时后报java.net.SocketException: Too many open files原因Linux 系统文件描述符限制默认 1024每个Jsoup.connect()创建新 socket 未及时释放。解决全局复用Connection对象见 2.2.1 节或在 JVM 启动参数加-XX:MaxFDLimit65536更彻底的是用连接池如 Apache HttpClient。5. 数据落地与验收从“爬下来”到“能交差、能答辩”爬虫的价值不在代码行数而在产出的数据能否被业务方直接使用。学生交毕设老师看三点功能完整、过程可追溯、结果可验证。这一章告诉你怎么做到。5.1 结构化存储JSON 文件 CSV 双输出适配不同验收场景不要只存控制台打印。我要求所有课程设计必须生成两种格式output/data.json标准 JSON 数组含完整字段与元数据来源页、抓取时间output/data.csvExcel 可直接打开字段名首行兼容答辩现场快速演示。public class DataExporter { private final ListArticle articles new ArrayList(); public void add(Article article) { articles.add(article); } public void exportToJson(String filePath) throws IOException { ObjectMapper mapper new ObjectMapper(); mapper.enable(SerializationFeature.INDENT_OUTPUT); mapper.writeValue(new File(filePath), articles); } public void exportToCsv(String filePath) throws IOException { try (PrintWriter pw new PrintWriter(new FileWriter(filePath, StandardCharsets.UTF_8))) { pw.println(title,author,publish_time,url,source_page,fetch_time); for (Article a : articles) { String line String.format( %s,%s,%s,%s,%s,%s, escapeCsv(a.getTitle()), escapeCsv(a.getAuthor()), escapeCsv(a.getPublishTime()), escapeCsv(a.getUrl()), escapeCsv(a.getSourcePage()), new SimpleDateFormat(yyyy-MM-dd HH:mm:ss).format(a.getFetchTime()) ); pw.println(line); } } } private String escapeCsv(String s) { if (s null) return ; return \ s.replace(\, \\) \; } }验收技巧答辩时打开data.csv用 Excel 的「数据透视表」当场统计“各栏目文章数量”、“作者发文频次”老师会觉得你“真干了活”data.json用 VS Code 安装 JSON Tools 插件一键格式化 验证证明数据结构规范。5.2 过程日志与统计报告让答辩老师一眼看到你的工作量光有数据不够要有过程证据。我在每个项目根目录生成report.md内容自动生成# 爬取报告某技术博客2024-06-15 ## 执行概览 - 开始时间2024-06-15 09:23:11 - 结束时间2024-06-15 10:47:33 - 总耗时84 分 22 秒 - 成功页数97 / 100 - 失败页数3详见下方 ## 失败详情 | 页码 | URL | 错误类型 | 时间 | |------|-----|----------|------| | 42 | https://xxx.com/list_42.html | java.net.SocketTimeoutException | 09:35:22 | | 77 | https://xxx.com/list_77.html | org.jsoup.HttpStatusException: HTTP error fetching URL. Status502 | 10:12:05 | | 99 | https://xxx.com/list_99.html | java.io.IOException: Not found | 10:45:11 | ## 数据统计 - 总文章数1247 篇 - 标题平均长度28.3 字 - 最热标签Java217 次、Spring189 次、MySQL152 次生成逻辑伪代码用StopWatch记录总耗时用AtomicInteger successCount/failCount统计页数失败时捕获Exception并记录url e.getClass().getSimpleName()标签统计用MapString, Integer累加ex.texts(a.tag)结果。5.3 答辩现场演示技巧三分钟让老师记住你的项目别一上来就说“我用了 jsoup”。按这个顺序讲先展示结果打开data.csv筛选出“最近 7 天发布”的文章按阅读量排序指出最高一篇标题和阅读数——证明数据真实有效再演示健壮性故意删掉data.json点击“重新爬取”按钮你的 GUI 或命令行入口现场跑 3 页展示日志里SUCCESS: page 1,WARN: no author on page 3—— 说明你处理了异常最后讲设计亮点指着PageNavigator接口说“我把分页逻辑抽象出来如果换一个网站只需要新增一个LinkTextNavigator实现类主流程代码一行不用改。”从那以后我每次带课程设计都强制学生在答辩前用手机拍一段 30 秒视频左屏是data.csv右屏是终端里tail -f crawl.log滚动的日志。这段视频比 PPT 里的架构图管用十倍——它证明你真的跑起来了而且跑得稳。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?