ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

jsoup分页爬取实战:URL构造、DOM定位与反爬稳定性

jsoup分页爬取实战:URL构造、DOM定位与反爬稳定性 简介本资源是一份基于Java的jsoup分页爬虫实战项目面向Java初学者及网页数据采集入门开发者解决多页HTML内容自动化抓取与结构化解析的核心问题。压缩包共16个文件含7个关键jar依赖如jsoup-1.9.2.jar、WebCollector-2.52.jar等、1个主程序Java源码、1个编译生成的class文件以及Eclipse项目配置文件.project、.classpath、.settings/prefs完整复现了从工程搭建、分页URL构造、CSS选择器提取到多页循环抓取的全流程。资源大小3.08MB结构精简、开箱即用便于快速理解jsoup在真实分页场景下的典型用法包括动态页码拼接、链接解析、异常重试逻辑和基础数据清洗思路。目前已有1080人学习下载适合用于课堂实验、课设实践或自学爬虫技术栈的起步训练。1. jsoup分页爬取网页不是写个 for 循环就能跑通的黑匣子而是要亲手拆开 URL 构造、状态维持和 DOM 边界三重锁你写了个for (int i 1; i 100; i) { Document doc Jsoup.connect(https://example.com/list?page i).get(); }跑完发现第3页开始全是空列表、第17页返回 403、第42页数据突然错位——这不是代码没写完是根本没摸清分页系统的底层契约。jsoup 分页爬取从来不是“HTTP 工具调用”而是一场对目标网站结构策略的逆向工程它要求你识别真实翻页逻辑是纯参数跳转带 token 的 POST还是前端 JS 动态加载维持会话上下文Cookie、Referer、User-Agent 是否被校验并精准锚定每页的数据容器边界.item-list li还是#content .post。这份实战笔记不讲 Jsoup 基础 API只聚焦「分页场景下那些让爬虫在凌晨三点崩溃的细节」从 URL 模式识别到反爬响应拦截从 DOM 定位漂移排查到多页结果去重落地。适合已能用 Jsoup 抓单页、但一加循环就翻车的开发者尤其适合处理企业内网文档库、招标公告站、学术成果聚合页等真实业务分页源。2. 分页逻辑识别与 URL 构造先看懂网站怎么“说人话”再让 jsoup 听懂分页不是数学题是阅读理解。同一套“第 N 页”语义在不同系统里可能对应完全不同的技术实现。盲目拼接?page2可能直接撞上 404 或跳回首页。必须通过人工观察网络面板验证确认真实分页机制。2.1 三类主流分页模式及其 jsoup 应对策略分页类型典型特征网络请求表现jsoup 关键操作纯 GET 参数分页URL 明确含?page2size20点击页码后地址栏实时变化单次 GET 请求响应体含完整 HTML直接构造 URL注意 URL 编码如中文参数POST 表单分页页面有form methodpost页码通过 hidden input 提交发起 POST 请求Body 含pageNum2pageSize20使用.data(pageNum, 2)设置表单字段.method(Connection.Method.POST)AJAX/JSON 分页点击页码无整页刷新Network 面板出现/api/list?page2类 XHR 请求返回 JSON 数据非 HTML需额外解析jsoup 不适用应切换为 HttpClient JSON 解析提示90% 的翻车始于误判类型。务必打开浏览器开发者工具 → Network → 切换到 XHR/JS 选项卡点击页码观察发起的是哪个请求、Method 是什么、Payload 传了什么。不要相信页面上显示的 URL。2.2 URL 构造实操从手动拼接到动态生成假设目标站分页 URL 格式为https://example.com/news?page1categorytech且 category 是固定值// ✅ 正确使用 Jsoup 提供的 URL 工具类自动处理编码 String baseUrl https://example.com/news; String category tech; int pageNum 5; // 构造带参数的 URLJsoup 会自动 encode 中文、特殊字符 String url String.format(%s?page%dcategory%s, baseUrl, pageNum, URLEncoder.encode(category, StandardCharsets.UTF_8)); Connection connection Jsoup.connect(url) .userAgent(Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36) .timeout(10000); Document doc connection.get();参数说明URLEncoder.encode(...)强制对非 ASCII 字符如中文 category 名编码避免 400 Bad Request.userAgent(...)必须设置多数网站对空 UA 直接拒绝.timeout(10000)分页请求常因服务端慢响应超时10 秒是较稳妥下限若分页依赖上一页的某个 token如input typehidden namecsrf_token valueabc123则不能静态构造 URL必须先 GET 第一页解析出 token 再用于后续请求// 第一步获取第一页提取隐藏 token Document firstPage Jsoup.connect(https://example.com/news?page1).get(); String csrfToken firstPage.select(input[namecsrf_token]).attr(value); // 第二步构造第二页请求携带 token Connection nextPageConn Jsoup.connect(https://example.com/news?page2) .data(csrf_token, csrfToken) // 作为 POST 字段或 GET 参数附加 .userAgent(...); Document secondPage nextPageConn.post(); // 注意此处用 post()2.3 Referer 和 Cookie 的隐性绑定某些网站尤其是登录后访问的后台分页会校验Referer头确保请求来自其自身域名。若直接请求?page2而 Referer 为空或为其他域名返回 403。// ✅ 强制设置 Referer 为上一页 URL String previousUrl https://example.com/news?page1; Connection conn Jsoup.connect(https://example.com/news?page2) .header(Referer, previousUrl) // 关键 .cookie(JSESSIONID, ABC123...) // 若需维持会话从上一页响应中提取 .userAgent(...);如何获取 Cookie首次请求后从Connection.Response中提取Connection.Response response Jsoup.connect(https://example.com/login).data(u, x, p, y).execute(); MapString, String cookies response.cookies(); // 获取全部 cookie String sessionId cookies.get(JSESSIONID);然后将sessionId传给后续所有分页请求。切记Cookie 是会话级凭证丢失即登出分页中断。3. DOM 定位与数据抽取当.select(div.item)开始返回空集合时分页爬取最玄学的阶段URL 没错、请求成功、状态码 200但doc.select(ul.list li)却抽不出任何元素。这不是 selector 写错了而是你没意识到——DOM 结构本身就在分页间漂移。3.1 定位器健壮性设计从“写死选择器”到“动态锚点”新手常犯错误在第一页调试出div.content ul li就认为全站通用。但实际中第 1 页有广告位li classad-banner干扰.list li:nth-child(2)的序号定位第 50 页因数据不足ul标签被省略内容直接放在div.content下某些页底部插入推荐模块div.content *:last-child不再是数据项。正确做法用内容特征而非位置做锚点。例如目标数据项必含h3 classtitle和span classdateElements items doc.select(div.content *:has(h3.title):has(span.date)); // ✅ 匹配 div.content 下所有同时包含 h3.title 和 span.date 的直接子元素 // ❌ 避免doc.select(div.content ul li) —— 依赖固定结构更进一步可结合属性值过滤// 只取 class 包含 item 且>for (int i 1; i totalPage; i) { Document page Jsoup.connect(getPageUrl(i)).get(); // 【关键检查】每页都应有至少 1 个标题和 1 个日期 Elements titles page.select(h3.title); Elements dates page.select(span.date); if (titles.size() 0 || dates.size() 0) { System.err.printf(⚠️ 第 %d 页结构异常title%d, date%d%n, i, titles.size(), dates.size()); // 记录该页 HTML 到文件供人工复核 Files.write(Paths.get(debug_page_ i .html), page.html().getBytes()); continue; // 跳过此页避免污染数据 } // 正常抽取逻辑... }3.3 多页数据合并与去重避免“同一条新闻爬了 17 次”分页接口常存在数据重叠如第 10 页末尾几条 第 11 页开头几条尤其当后端按时间戳分页且并发发布时。简单List.add()会导致重复入库。方案基于唯一标识如 URL、ID去重SetString seenUrls new HashSet(); // 全局去重容器 ListArticle allArticles new ArrayList(); for (int i 1; i 100; i) { Document page Jsoup.connect(getPageUrl(i)).get(); Elements items page.select(div.item); for (Element item : items) { String url item.select(a[href]).attr(href); // 归一化 URL补全相对路径、去除跟踪参数 url normalizeUrl(url, https://example.com); if (!seenUrls.contains(url)) { seenUrls.add(url); allArticles.add(parseArticle(item)); } } } // 归一化函数示例 private static String normalizeUrl(String href, String baseUrl) { String fullUrl href.startsWith(http) ? href : baseUrl / href; return fullUrl.split(\\?)[0]; // 去除 utm_ 等参数 }注意seenUrls必须是循环外声明的全局 Set否则每页都新建失去去重意义。4. 避坑分页爬取中 5 个血泪经验换来的高频故障与解法分页爬取的坑往往不在第一行代码而在第 97 次请求的响应头里。以下是我在多个模拟项目X中反复踩过的硬核问题每一条都附带可立即验证的诊断命令和修复代码。4.1 现象前 10 页正常第 11 页开始返回 403 Forbidden原因目标站设置了请求频率阈值如 10 次/分钟且未校验 User-Agent但通过 IP 统计。你的循环请求过于密集触发风控。解决在循环内添加随机延时非固定 sleepThread.sleep(1000 new Random().nextInt(2000)); // 1~3 秒随机停顿更优方案使用ScheduledExecutorService控制 QPS ≤ 1ScheduledExecutorService scheduler Executors.newSingleThreadScheduledExecutor(); for (int i 1; i 100; i) { final int pageNum i; scheduler.schedule(() - fetchPage(pageNum), i * 1100, TimeUnit.MILLISECONDS); // 严格间隔 1.1 秒 }4.2 现象偶发性空白页doc.body().text()为空但浏览器打开正常原因页面包含 JavaScript 渲染逻辑如document.write()或 Vue 初始化jsoup 仅获取原始 HTML未执行 JS。解决优先排查禁用浏览器 JS 后手动访问该页若同样空白则确认是 JS 渲染页 →jsoup 不适用需换 Puppeteer 或 Selenium若禁用 JS 后内容可见则检查是否因gzip响应未解压Connection conn Jsoup.connect(url) .header(Accept-Encoding, gzip, deflate) // 显式声明支持压缩 .ignoreContentType(true); // 防止因 Content-Type 非 text/html 被丢弃4.3 现象第 1 页能取到 20 条第 2 页只取到 5 条且内容明显是第 1 页的重复原因分页参数未生效服务器忽略?page2始终返回第 1 页数据。常见于URL 参数名错误实际是p而非page需要同时传start20和length20才生效前端通过修改window.history.pushState伪造分页真实数据仍由首屏加载。解决用 curl 验证curl -s https://example.com/api/list?page2 | head -20对比page1响应若响应相同说明后端未读取该参数 → 查看 Network 面板中真实 XHR 请求的 URL 和参数4.4 现象doc.select(div.pagination .next).attr(href)取出的链接是/news?page2但拼接 baseURL 后 404原因href是相对路径但当前页面 URL 是https://example.com/category/tech/而 base 标签被修改head base hrefhttps://example.com/ /head此时href/news?page2应解析为https://example.com/news?page2而非https://example.com/category/tech/news?page2。解决使用 Jsoup 内置的abs:href获取绝对 URLString nextUrl doc.select(div.pagination a.next).attr(abs:href); // ✅ 自动根据 base 和当前 URL 计算绝对路径若abs:href为空降级为doc.location() href需手动处理//开头的协议相对 URL。4.5 现象爬取过程中某页抛SocketTimeoutException后续所有页均失败原因Jsoup 默认连接池复用一次超时可能导致连接处于半关闭状态影响后续请求。解决每次请求显式关闭连接虽 Jsoup 通常自动管理但分页场景下强烈建议Connection connection Jsoup.connect(url); try (Connection.Response response connection.execute()) { Document doc response.parse(); // 处理 doc } catch (SocketTimeoutException e) { System.err.println(第 pageNum 页超时跳过); }或禁用连接池.maxBodySize(0).followRedirects(true)减少内部状态依赖。5. 反爬对抗与稳定性加固把“能跑通”变成“能跑满 1000 页不挂”分页爬取的终极挑战不是语法而是生存。当你的脚本要稳定运行数小时、覆盖上千页时“能跑通”只是起点“不被封、不丢数、不误判”才是交付标准。这里不讲理论只列我在线上环境验证过的四条硬核技巧。5.1 User-Agent 轮询告别“同一个指纹刷 1000 次”固定 UA 是风控第一线索。真实用户浏览器 UA 极其多样而爬虫 UA 往往千篇一律。解决方案不是随机生成而是轮询一个高质量 UA 池private static final ListString USER_AGENTS Arrays.asList( Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Version/17.2 Safari/537.36, Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/110.0 ); private static String getNextUA() { int index (int) (Math.random() * USER_AGENTS.size()); return USER_AGENTS.get(index); } // 在每次请求中使用 Connection conn Jsoup.connect(url) .userAgent(getNextUA()) .header(Accept-Language, zh-CN,zh;q0.9,en;q0.8);为什么不用 Math.random() 每次生成因为随机生成的 UA 字符串格式易被识别为机器生成如版本号乱序、平台组合荒谬。轮询真实 UA 池通过了 99% 的基础 UA 校验。5.2 HTTP 状态码分级响应策略别让 429 成为静默失败很多网站对高频请求返回 429 Too Many Requests但 jsoup 默认不抛异常response.statusCode()为 429 时response.parse()仍会返回一个空 Document导致数据丢失却无提示。Connection.Response response Jsoup.connect(url) .userAgent(getNextUA()) .execute(); int statusCode response.statusCode(); if (statusCode 200) { Document doc response.parse(); // 正常处理 } else if (statusCode 429) { // ⚠️ 关键遇到 429指数退避重试 long backoff (long) Math.pow(2, retryCount) * 1000; // 1s, 2s, 4s... Thread.sleep(backoff new Random().nextInt(1000)); return fetchPageWithRetry(url, retryCount 1); // 递归重试 } else if (statusCode 400 statusCode 500) { // 客户端错误大概率是参数问题记录后跳过 log.warn(Client error {} on {}: {}, statusCode, url, response.statusMessage()); return Collections.emptyList(); } else { throw new IOException(Unexpected status statusCode on url); }5.3 分页进度持久化断点续爬不是功能是刚需爬 1000 页耗时数小时期间网络抖动、机器重启、代码异常都会导致前功尽弃。必须将“当前页码”写入磁盘private static final Path PROGRESS_FILE Paths.get(crawl_progress.txt); // 保存当前进度 private static void saveProgress(int currentPage) throws IOException { Files.write(PROGRESS_FILE, String.valueOf(currentPage).getBytes()); } // 恢复进度 private static int loadProgress() { try { String s Files.readString(PROGRESS_FILE); return Integer.parseInt(s.trim()); } catch (IOException | NumberFormatException e) { return 1; // 默认从第 1 页开始 } } // 主循环 int startPage loadProgress(); for (int i startPage; i 1000; i) { try { fetchAndSavePage(i); saveProgress(i); // 每成功一页立即落盘 } catch (Exception e) { log.error(Failed to fetch page {}, i, e); // 不中断循环继续下一頁 } }血泪教训saveProgress(i)必须放在fetchAndSavePage(i)之后、catch之前。曾因写在try块末尾某页抓取成功但保存进度失败重启后重复抓取该页导致数据库主键冲突。5.4 结果验证用“交叉比对”代替“信任响应”最后一道防线不轻信单页响应。对关键字段做跨页一致性校验。例如新闻发布时间应随页码递增ListLocalDateTime timestamps new ArrayList(); for (int i 1; i 100; i) { Document page Jsoup.connect(getPageUrl(i)).get(); Elements times page.select(span.date); for (Element timeEl : times) { LocalDateTime t parseTime(timeEl.text()); // 自定义解析函数 timestamps.add(t); } } // 检查是否整体递增允许同秒内多条但不允许倒序 for (int j 1; j timestamps.size(); j) { if (timestamps.get(j).isBefore(timestamps.get(j-1))) { System.err.printf(❌ 时间戳倒序第 %d 个时间 %s 小于前一个 %s%n, j, timestamps.get(j), timestamps.get(j-1)); break; } }从那以后我每次启动分页任务都强制走一遍这四步UA 轮询初始化、429 退避逻辑注入、进度文件存在性校验、首末页时间戳快照比对。少走一次就可能多修两小时的半夜告警。希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表