JSoup分页爬虫实战:从翻页规律到稳定代码骨架

发布时间:2026/9/10 1:51:08

JSoup分页爬虫实战:从翻页规律到稳定代码骨架 简介面向Java开发者的jsoup分页爬虫入门示例项目以可运行的Java工程代码为主适合正在学习HTML解析、需要处理多页数据抓取的爬虫初学者或相关课程实践者。项目规模不大但结构完整共16个文件压缩包约3.08MB主体包括Java源码、编译后的class文件、Eclipse工程配置与项目描述文件以及jsoup爬虫库、WebCollector框架、JSON处理与日志组件等7个依赖jar包便于直接导入IDE运行调试。内容围绕jsoup分页抓取的核心链路展开涵盖URL分页参数构造、CSS选择器定位、数据提取、循环翻页以及结果存储前的基本清洗同时涉及动态加载内容处理、异常重试、异步优化和Robots协议合规等工程实践要点。随包目录区分src源码与编译输出配置信息完整可用来对照理解爬虫项目的依赖管理方式与工程组织思路。该示例已有1080人学习对入门jsoup及分页抓取具有较高参考价值。 如果你问一个写过爬虫的人抓网页最花时间的环节是什么十个里有八个会说是解析HTML但真正拖垮项目的往往是另一件事——分页没处理好。我曾经拿jsoup抓一个新闻列表单页解析几分钟就写完了结果翻页时各种问题接连冒出来有的页码参数找不到、有的翻着翻着被对方服务器拒绝、还有的页面编码直接乱掉。今天这篇就专门聊jsoup分页爬取网页这件事把我踩过的坑、总结出的规律和可以直接复制的代码骨架都放出来适合刚入门Java爬虫的开发者也适合已经会写单页爬虫但想系统解决分页问题的朋友。1. 为什么是jsoup分页爬虫的技术选型思考1.1 一次数据缺页的翻车现场先讲个真实翻车的场景。之前帮一个做内容调研的朋友抓行业资讯第一版程序跑完终端里刷出来三百多条数据看起来“完成度很高”结果对方拿人工抽样的数据一对比发现我只抓到了第1页——整个列表一共有几十页后面的全漏了。原因很简单我当时只抓了初始URL没有实现翻页循环。后来把jsoup的分页逻辑补齐数据量直接翻了十几倍。这件事给我的教训很直接爬虫的难点从来不在“拿到一页”而在“把每一页都稳定拿到”。这种问题在刚接触爬虫的人身上尤其常见。大家把大量精力花在CSS选择器和数据提取上觉得那才是技术的核心但实际上分页策略才是决定数据完整性的命门。更麻烦的是分页不是一套代码通吃所有网站的不同站点的分页机制差别很大有的藏在URL参数里有的藏在请求头里还有的根本不是GET请求。如果不提前摸清规则写出来的爬虫大概率会在第2页、第3页就翻车。1.2 HttpClient、jsoup、Selenium怎么选一张表说清楚做Java爬虫绕不开三个主流选择Apache HttpClient、jsoup、Selenium。很多人一上来就问哪个厉害其实没有优劣只有合不合适。我把它们的核心差异整理成了一张表。工具请求能力HTML解析执行JS性能上手难度HttpClient强请求头、Cookie完全可控无需配合其他库否高中jsoup够用自带连接方法强CSS选择器否高低Selenium模拟浏览器强是低资源消耗大中高如果是抓一个纯静态的列表页jsoup是综合性价比最高的选择。它把请求和解析做在了一起Jsoup.connect(url).get()返回的就是一个可操作的Document对象后续直接select()就行代码量比HttpClient配合Jsoup.parse()的组合要少很多。HttpClient的优势在于更细粒度的请求控制比如自定义SSL、连接池、复杂的重定向逻辑但分页爬取这个场景通常用不到这些。Selenium虽然能执行JavaScript但代价是启动浏览器实例、占用大量内存抓一个分页列表可能要跑好几分钟完全没必要。我个人的选型习惯是页面源代码里能直接看到列表数据的就是jsoup的主场页面源代码里看不到、需要额外请求接口的先试着找找那个接口找到了还是可以用jsoup发请求去调实在找不到接口、又是重度JavaScript渲染的才考虑Selenium。这个顺序能帮你省下大量无谓的性能开销。1.3 jsoup做不到的事不执行JavaScript的边界jsoup有一个硬边界它不执行JavaScript拿到的是服务端返回的原始HTML。如果目标网页的数据是动态渲染出来的用jsoup抓回来的内容里就不会有这些数据你会得到一堆空的div、空的ul看起来像解析失败实际上是页面本来就没把数据写到HTML里。怎么快速判断一个页面适不适合用jsoup方法很简单在浏览器里右键查看网页源代码或者直接右键另存为然后搜索你想抓的数据内容。如果源代码里能搜到那jsoup完全可以处理如果源代码里搜不到但页面渲染后能看到就得另走他路了——优先去Network面板里找XHR接口很多“动态页面”其实是用异步请求加载的那个接口返回的往往是JSON或一段模板HTML用jsoup照样能打。只有那些接口参数加密、请求链路复杂的页面才需要上Selenium级别的工具。2. 三种分页模式与URL规律分析动手前先做这件事2.1 URL参数型分页最直观也最好处理最常见的分页方式就是把页码作为URL查询参数典型格式长这样https://example.com/news?page1size20 https://example.com/news?page2size20这种类型最好处理写代码时只需要循环拼接页码就行。但有几个细节要注意第一参数名不一定叫page有的叫p、有的叫pn、有的叫pageNum、有的叫pageIndex需要在浏览器的开发者工具里翻几页观察URL变化才能确定第二起始页码不一定是1有些网站从0开始第一页是page0如果你从1开始会漏掉第一页第三每页条数参数有时候是size、有时候是limit或者pageSize这个参数建议固定住不要让它变否则后续解析翻页逻辑容易搞混。举个例子之前爬一个技术博客列表URL看起来是/article/list?page2我按照常识从1开始循环结果一直跑到最后一页数据总数对不上后来才发现第一页其实是page0硬生生把第一页的内容漏了。从那以后我每次写分页爬虫前都会手动翻到第一页和第二页把URL对比一下确认起始页码和参数名再写循环。2.2 路径型分页换个拼接思路另一种常见分页形式是路径型URL长这样https://example.com/news/page/2/ https://example.com/news/2.html这种在WordPress站点和不少CMS系统里很常见。处理方式和参数型类似都是循环拼接URL但拼接方式要注意——你不能简单地在原URL后面加参数得把页码嵌到路径里。用String.format或者字符串模板最方便。String baseUrl https://example.com/news/page/%d/; String pageUrl String.format(baseUrl, page);路径型分页还有一个衍生形态列表页之间有“下一页”链接但没有明显的页码序列翻到最后也没有一个总的页数显示。这时候处理策略要稍微调整不能死板地循环1到N而是跟踪“下一页”链接直到它消失这个我在2.4里详细讲。2.3 POST或JS动态加载抓包定位真实接口有一部分网站的分页不走GET请求点击“下一页”后URL完全不变数据是POST提交参数后返回的。这种情况下直接用浏览器地址栏无法翻页必须在开发者工具里抓包。操作步骤打开目标网站列表页按F12进入开发者工具切到Network面板勾选Fetch/XHR然后手动点击第二页、第三页。重点观察有没有一个请求在每次翻页时都会发起参数里带有page、pageIndex、offset这类关键字。找到之后比对它返回的内容和页面显示的内容是否一致确认了就是数据接口。这种接口通常可以直接用jsoup模拟POST请求来调Document doc Jsoup.connect(url) .data(page, String.valueOf(page)) .data(size, 20) .post();如果接口返回的是JSON而不是HTML也没关系用Connection.Response拿到body文本再用Jackson或Fastjson解析即可。jsoup不只是HTML解析器它同样是一个好用的HTTP客户端。2.4 终止条件怎么判定避免死循环和漏数据分页循环一定要想清楚“什么时候停”。最常见的两种做法一种是从页面上拿到总页数比如页面底部显示“共100条每页20条”那总页数就是5页循环1到5就够了。信息可能在HTML里直接可见也可能藏在某个元素的>dependency groupIdorg.jsoup/groupId artifactIdjsoup/artifactId version1.17.2/version /dependency选版本有个小讲究开源库尽量别用太老的版本。jsoup早期版本对HTTPS证书的兼容性、对HTML5新标签的解析能力都有差距我遇到过用老版本解析某个站点时select结果莫名为空升级版本后正常的情况。依赖这种东西保持适度最新是最省心的策略。3.2 可直接复制的分页爬虫代码下面这份代码是我实际项目里抽出来的精简版抓取一个典型的URL参数分页列表页提取标题、链接和摘要打印到控制台。核心逻辑完整可以直接改改选择器和URL拿去用。import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import java.io.IOException; import java.util.ArrayList; import java.util.List; public class PaginationCrawler { // 以URL参数型分页为例pageIdx从1开始 private static final String BASE_URL https://example.com/news?page%dsize20; private static final int MAX_PAGES 50; private static final long DELAY_MS 1500; // 两次请求间隔单位毫秒 public static void main(String[] args) throws InterruptedException { ListArticle result new ArrayList(); for (int page 1; page MAX_PAGES; page) { String url String.format(BASE_URL, page); Document doc fetchWithRetry(url, 3); if (doc null) { System.out.println(第 page 页获取失败终止爬取); break; } // 假设列表项的容器是 div.news-item根据实际页面调整 Elements items doc.select(div.news-item); if (items.isEmpty()) { System.out.println(第 page 页没有内容提前结束); break; } for (Element item : items) { String title item.selectFirst(h2 a) ! null ? item.selectFirst(h2 a).text() : ; String link item.selectFirst(h2 a) ! null ? item.selectFirst(h2 a).absUrl(href) : ; String summary item.selectFirst(p.summary) ! null ? item.selectFirst(p.summary).text() : ; result.add(new Article(title, link, summary)); } System.out.printf(第 %d 页完成累计 %d 条数据%n, page, result.size()); // 控制请求频率做个人畜无害的爬虫 Thread.sleep(DELAY_MS); } for (Article article : result) { System.out.println(article); } } private static Document fetchWithRetry(String url, int retryTimes) { for (int i 0; i retryTimes; i) { try { return Jsoup.connect(url) .userAgent(Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36) .referrer(https://example.com/) .timeout(10000) .get(); } catch (IOException e) { System.err.println(请求失败: e.getMessage() 第 (i 1) 次重试); if (i retryTimes - 1) { return null; } try { Thread.sleep(2000L * (i 1)); // 退避等待 } catch (InterruptedException ex) { Thread.currentThread().interrupt(); return null; } } } return null; } static class Article { String title; String link; String summary; Article(String title, String link, String summary) { this.title title; this.link link; this.summary summary; } Override public String toString() { return title | link | summary; } } }3.3 代码里的关键设计点这段代码看起来简单但里面有几个点是实战中容易忽略的。先说userAgentjsoup默认发出去的UA是空的或者Java标识很多网站的防火墙会直接拦截这种请求返回403或者跳验证页。提前设置成一个常见浏览器的UA能绕过大量低级拦截。如果你抓的网站还需要登录才能看内容可以在登录后从浏览器复制Cookie通过.cookie(name, value)塞进去。再说absUrl(href)这个方法是把相对路径自动补全成绝对URL。很多网站列表页里的链接写的是/news/detail/123.html而不是完整的https://...如果不处理后面访问详情页时拼接URL就很痛苦。jsoup的absUrl会基于当前页面的地址自动补全省去手工拼接的麻烦。还有一个容易忽略的点列表为空时用break而不是continue。分页数据一般是按顺序排列的如果第N页已经没数据了理论上第N1页也不会有继续请求只会白白增加服务器压力还可能触发反爬。配合MAX_PAGES上限双条件退出基本能杜绝死循环。4. 实际环境避不开的四个坑反爬、编码、频率和中断恢复4.1 被服务器拒绝UA、Referer和Cookie缺一不可拿到代码骨架后最常遇到的第一个问题就是请求被拒。403是服务器明确告诉你“我不给你数据”429是“你请求太快了”。403的常见原因有三个UA异常、缺少Referer、缺少必要的Cookie。UA的问题上面说了Referer也很关键——很多网站的图片列表、分页接口会校验来源如果请求头里没有Referer或者Referer是空的直接拒绝。jsoup里用.referrer(https://example.com/)就能设置。Cookie这块稍微麻烦一点。有些网站首次访问会种一个sessionId后续请求必须带这个Cookie才认你。最简单的处理方式是先用浏览器登录或访问一次目标网站从开发者工具里把Cookie字符串复制出来在代码里手动设置。虽然不够优雅但对个人爬虫项目来说完全够用。如果Cookie会过期再考虑用HttpClient的CookieStore方案那个能自动管理会话复杂度高一些不是第一版该考虑的事。4.2 编码乱码网页不告诉你它是什么编码中文网站里GBK编码还大量存在尤其是一些老牌的资讯站、政府站点、行业门户。jsoup对编码的处理逻辑是先看响应头里的Content-Type再看HTML里的meta标签都找不到就默认UTF-8。一旦网站实际是GBK但没声明就会出现一串乱码。碰到这种情况调整做法先拿到响应字节流手动指定编码解析。Connection.Response resp Jsoup.connect(url) .userAgent(Mozilla/5.0 ...) .execute(); Document doc Jsoup.parse(resp.bodyAsBytes(), GBK, url);用bodyAsBytes()取原始字节再通过Jsoup.parse(byte[], charsetName, baseUri)强制指定编码。这个方法的关键在于确定目标网站到底用的什么编码。判断方法浏览器打开网页右键查看语言编码或者看HTML头部的meta charset...如果是gb2312或gbk就用对应的字符集。别小看这个细节我见过有人因为乱码问题放弃了抓取某个数据源其实一行代码就能解决。4.3 超时重试指数退避比固定重试更靠谱网络请求嘛总有超时、连接重置、DNS解析失败这些幺蛾子。jsoup默认的超时是3秒对国内一些响应慢的网站来说远远不够我一般会设置成10秒左右。重试策略建议用指数退避也就是第一次失败等2秒第二次失败等4秒第三次等8秒而不是每次都等固定时长。原因很简单连续失败大概率是服务器已经盯上你了或者网络出了持续性问题这时候再以固定频率反复打只会加重风险。上面代码里的Thread.sleep(2000L * (i 1))就实现了这个逻辑重试3次之后如果还是不成功就放弃这一页不要一直卡在当前循环里出不来。爬虫最重要的品质不是“永不放弃”而是“知进退”。4.4 数据去重与断点续爬让爬虫具备工程化能力分页爬虫跑久了一定会遇到两个工程化问题重复数据和中断重启。重复数据的来源通常是页面本身的交叉推荐、上一页和下一页内容重叠或者网站有多个入口指向同一篇文章。处理方式很简单——维护一个已访问集合用标题或者URL做维度去重在内存里用HashSet数据量大就落到数据库加唯一索引。断点续爬稍微麻烦一点。假设你爬到第37页时程序崩了或者被服务器断连重新跑一遍又得从第1页开始前面的请求全浪费了还增加了对方服务器的压力。解决思路每完成一页就把当前页码记录到一个本地文件比如progress.txt程序启动时读取这个文件从断点页码继续。代码改造成本很低但带来了两个直接好处一是节省时间二是降低被封风险。我甚至建议在循环体外面包一层try-catch任何异常都先把progress.txt更新成当前页码保证断点信息不丢。5. 爬虫的边界感robots、请求频率与数据使用红线5.1 robots.txt怎么读爬之前花一分钟看一眼写爬虫的人不能只关注技术网站的规则也得看一眼。绝大多数正规站点都在根目录放了robots.txt比如https://example.com/robots.txt里面用Disallow标明哪些路径不允许爬虫抓取。这属于互联网世界的通行约定虽然不是法律强制但尊重它是从业者的基本素养。打开robots.txt后重点看两行User-agent和Disallow。如果User-agent: *下面列出了你的目标路径说明站长明确不想让爬虫访问这个路径那就换个来源或者换个思路。这个检查只需要一分钟但能帮你规避掉大部分不必要的风险。我自己每次开新爬虫项目前都把这个动作当成标准流程。5.2 请求频率的“绅士原则”慢反而是最快的频率控制是爬虫项目能不能长期存活的关键。很多人一上来就开多线程、加代理池短期内抓得很爽但过不了几天IP就被封了整个项目报废。我个人的做法很朴素单线程加1到3秒随机延时每次请求之间休息一下1000条数据爬完也就几分钟的事完全够用。这个原则背后的逻辑是爬虫和网站之间其实是互利关系你拿到数据服务器承受少量压力但如果你把压力放大到影响正常用户访问对方一定会采取措施。控制频率不是示弱而是让你在对方容忍范围内把活干完。封IP这种事一旦发生反而不划算——找代理、换IP的成本远比逗号延时高得多。绅士一点反而走得远。5.3 数据使用的红线能爬不代表能乱用最后说一点很多人不爱听但对大家都好的话能爬到的数据不一定能随便用。公开的、匿名的、聚合类的信息个人学习和研究用途基本没问题但涉及个人隐私、需要登录才能看到的非公开内容、有明确版权声明的作品就不要动心思了。爬下来自己看看是一回事打包转售、做成商业服务又是另一回事这个边界心里要清楚。我见过不少项目因为数据来源不当中途翻车的轻则被对方律师函警告重则整个产品下架。技术本身没有原罪但用技术的人有选择权。做一个能稳定跑很久的爬虫项目窍门恰恰是知道哪里不能碰。最后再分享一点个人体会。jsoup分页爬虫写起来其实不难真正难的是让它稳定、礼貌、可持续地跑下去。我经手过的爬虫项目里代码写得花哨的不少但最后活得久的都是那些频率控制得克制、异常处理得完整、去重逻辑做得干净的朴素实现。如果你刚开始做这件事别急着上并发池和代理池先把分页循环写稳、把UA设置好、把延时加上就已经超过大多数人了。等这套逻辑跑顺了再琢磨更高阶的方案也不迟。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/9/10 1:46:08

CANN/ge ATC工具输出类型参数说明

--output_type 【免费下载链接】ge GE(Graph Engine)是面向昇腾的图编译器和执行器,提供了计算图优化、多流并行、内存复用和模型下沉等技术手段,加速模型执行效率,减少模型内存占用。 GE 提供对 PyTorch、TensorFlow …

2026/9/10 1:46:08

GE ATC工具输出参数说明

--output 产品支持情况 全量芯片支持 功能说明 如果是开源框架的网络模型: 存放转换后的离线模型的路径以及文件名,例如:$HOME/module/out/tf_resnet50,转换后的模型文件名以指定的为准,自动以.om后缀结尾&#xff…

2026/9/10 2:41:14

SpringBoot旅游管理系统毕业设计:从技术选型到部署上线

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/10 2:41:14

Unigram频道运营指南:如何高效管理你的Telegram频道

Unigram频道运营指南:如何高效管理你的Telegram频道 想要在Telegram上建立成功的频道?Unigram作为Windows平台上的Telegram客户端,提供了强大的频道管理功能。本指南将带你掌握Unigram频道运营的核心技巧,让你的频道脱颖而出&…

2026/9/10 2:41:14

Unigram数据存储架构:本地数据库与云端同步的实现原理

Unigram数据存储架构:本地数据库与云端同步的实现原理 Unigram作为Windows平台上的Telegram客户端,其数据存储架构采用了本地数据库与云端同步的巧妙设计,确保用户数据既安全又实时可用。在本文中,我们将深入探讨Unigram如何通过S…

2026/9/9 13:11:35

超人会飞不算本事:系统稳定依赖清晰规则与边界设计

开头先不绕弯子。“#斯坦李吐槽dc 所以超人是无缘无故会飞的嘛哈哈哈哈哈哈哈锤哥真是技术人才啊!#雷神 #复联”这类调侃式短标题,第一波冲击力在于它把两个宇宙的角色塞进同一个吐槽箱里,但细想一下就能发现,它真正碰到的根本不是…

2026/9/8 7:15:15

超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论

把“蜘蛛侠 vs 超人”放在 CSDN 上聊,可能很多人第一反应是走错片场了。但如果把这两个角色看成“两个持续运营了 80 多年的文化产品”,你会发现,这场比较本质上是两个不同 IP 策略的长期结果对比:超人赢在定义了整个超级英雄题材…

2026/9/9 16:31:09

基于CNN的调制信号识别:MATLAB实现时频图分类实战

简介:本资源是一套面向通信工程与信号处理方向学习者、研究者的深度学习实践方案,聚焦调制信号自动检测与识别这一典型无线通信任务,解决传统方法依赖人工特征、低信噪比下性能下降等痛点。压缩包共12个文件(10.73MB)&…

2026/9/10 0:00:55

目录对比去重实战:用哈希算法精准清理重复文件

我电脑里现在还有一块换了三次机的“数据墓地”硬盘,里面存着2016年以前所有旧笔记本的完整备份。平时不觉得有什么,直到前阵子想把它整理归档,发现同一个安装包、同一批照片、同一份论文草稿,在几个不同的备份目录里反复出现。更…

2026/9/10 0:00:55

Leaflet离线地图完整Demo合集:内网部署与坐标纠偏实战

简介:这是一份面向Web GIS开发者的LeafLet离线地图示例合集,帮助开发者快速掌握离线地图从搭建到交互的完整流程。压缩包共723个文件,大小14.06MB,以319个js脚本、175个html页面和29个css样式文件为主体,配合png/svg图…

2026/9/10 0:00:55

MATLAB读取Rinex 3.02观测文件:多系统GNSS数据解析实战

简介:基于MATLAB开发的Rinex3.02版观测文件(o文件)读取代码包,面向卫星定位导航方向的学习者与研究人员,用于解决新版观测文件的数据解析、历元提取与时间转换问题。压缩包共4个文件,包含两个m脚本、一个19…

2026/9/7 16:23:03

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/7 22:46:00

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/9 10:21:54

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码