简介这是一个基于Java与Jsoup库抓取网页表格数据的入门实例适用于在MyEclipse环境中学习网络爬虫原理的开发者目标是掌握从HTML页面提取结构化表格信息的完整方法。资源包为zip格式共24个文件其中包含7个Java源码、7个class编译文件以及Jsoup、httpclient、cpdetector等7个依赖jar包同时附带MyEclipse项目配置整体大小仅1.15MB下载后可直接导入IDE运行调试。目前已有710人学习浏览对于刚接触爬虫或需要快速提取网页表格数据的场景具有参考价值。示例覆盖了从发送网页请求到解析表格的完整链路先通过Jsoup.connect()获取页面Document对象再使用select()定位table元素遍历tr与td节点提取单元格文本并可存入集合或自定义对象中。代码还包含了try-catch异常处理和基础数据存储思路借助httpclient、cpdetector等依赖可应对不同编码与请求场景读者可基于这份源码扩展出更通用的定向爬取工具适合作为进一步开发数据采集功能的基础。1. 用 Java 抓网页表格这个例子能省掉你一整天的正则调试搜「java 爬取网页表格 例子」的读者大概率是课设做到一半或者临时接到一个「把这个页面里的表格数据扒下来」的需求。网页表格看着规整真动手写代码却容易翻车标签套标签、属性带引号、单元格里有链接正则一匹配就乱套。这篇文章给出的是一套在 MyEclipse 里能直接跑通的完整例子核心是用 Jsoup 的选择器替代正则把 HTML 表格按 DOM 结构一行行拆出来再落成 CSV 文件。适合 Java 基础没问题、但没写过爬虫的开发者也适合想快速验证「某个页面能不能抓」的熟手。2. 环境与依赖为什么选 Jsoup而不是 HttpClient 加正则2.1 选型理由正则拆 HTML 表格属于吃力不讨好很多第一次写爬虫的开发者会先想到用 HttpClient 把网页源码下载成字符串再用正则提取tr和td。这个思路在页面结构完全不变、标签特别规整时勉强能用但网页表格几乎不可能一直规整单元格里混着a链接、行内样式、注释节点甚至整个表格是嵌套在另一个表格里的。正则面对这种树形嵌套结构写出来的匹配模式会越改越复杂最后变成只有自己能看懂的「玄学表达式」。Jsoup 的做法完全不同。它把 HTML 解析成一颗 DOM 树然后提供类似 jQuery 的选择器语法。你要抓表格只需要用table tr选中所有行再迭代每个行里的td节点取文本。这套写法的优点是把「怎么在 HTML 里找东西」变成了「怎么写选择器」代码量少一个量级而且页面小结构调整时通常只改一行选择器就行。另一个现实原因是这个例子的运行环境是 MyEclipse工程大多是普通 Java 项目不是 Maven 项目。用 HttpClient 加正则至少引入两个依赖而 Jsoup 一个 jar 包就同时覆盖了「连接网页」和「解析 HTML」两个环节依赖管理更简单非常适合没有 Maven 的课设环境。2.2 依赖引入一个 Jsoup jar 包的成本与版本选择在某公司的一次内部工具开发里我见到 A 同学直接把jsoup-1.14.3.jar丢进 MyEclipse 的 lib 目录点击 Build Path 后编译直接报UnsupportedClassVersionError。原因是 MyEclipse 自带的 JDK 版本是 1.7而新版本 Jsoup 要求 Java 8。这个错误几乎每个在 MyEclipse 里跑 Jsoup 的新手都会遇到一次。正规的做法是先确认 JDK 版本。运行环境是 MyEclipse 的话通过 Windows - Preferences - Java - Compiler 查看编译级别或者直接在命令行跑java -version。JDK 1.8 及以上选jsoup-1.14.3到1.15.x都可以JDK 1.6 或 1.7 环境老老实实用jsoup-1.11.2这个版本兼容性好静态表格解析的核心功能全都有。这个例子按 JDK 1.8 环境来写因为绝大多数 MyEclipse 使用者当前配置的是 1.8。项目结构保持最简单的形式WebTableCrawler/ src/ com.demo.crawler/ TableCrawler.java CsvWriter.java lib/ jsoup-1.14.3.jar table_demo.html // 本地测试页面避免网络波动干扰2.3 跑通第一步连接网页并输出标题拿到任意一个网页先别急着写表格解析。第一步是验证「能不能连上、编码对不对、Jsoup 是否正常工作」。我一般会先写一个三行的骨架打印页面的标题。package com.demo.crawler; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; public class TableCrawler { public static void main(String[] args) throws Exception { // 连接目标页面超时设 10 秒避免默认 3 秒不够用 Document doc Jsoup.connect(file:///D:/demo/table_demo.html) .timeout(10000) .userAgent(Mozilla/5.0 (Windows NT 10.0; Win64; x64)) .get(); // 打印页面标题确认解析成功 System.out.println(页面标题: doc.title()); } }这段代码的逻辑是Jsoup.connect()返回一个Connection对象链式调用.timeout()和.userAgent()配置请求行为.get()发起请求并拿到解析好的Document。Document就是整棵 DOM 树后续所有选择器操作都从它开始。参数说明有几个值得注意的点。timeout(10000)是连接和读取的总超时单位是毫秒。Jsoup 默认值是 3000 毫秒很多课程设计页面响应慢不调大就会在get()阶段超时抛异常。userAgent()设置的是请求头里的 User-Agent 字段部分服务器会拒绝无 UA 或默认 UA 的请求显式设置可以少踩一个坑。如果这里用的是file:///本地路径则完全绕开网络请求连接参数不生效正好用来隔离「代码问题」和「网络问题」。3. 定位与解析表格从 HTML 结构到 Java 对象3.1 先看目标表格的 DOM 结构再写选择器写解析代码之前第一步永远是打开浏览器按 F12找到目标表格对应的 HTML 片段。这个习惯能避免你对着错误的结构白写几十行代码。静态表格的典型结构是这样的table idcourseTable thead tr th课程名称/th th授课教师/th th学分/th /tr /thead tbody tr td操作系统/td td某导师/td td4.0/td /tr tr td计算机网络/td td某开发者/td td3.5/td /tr /tbody /table这是一份某高校课程表的简化片段也是这个例子要抓的目标数据。可以看到表格分为thead和tbody两个区域thead里是表头tbody里是数据行。用 Jsoup 选择器抓表很直接#courseTable tr表示「选择 id 为 courseTable 的元素内部的所有 tr 行」然后对每一行用td选择器取该行所有单元格。有个常见的认知误区是把thead里的th也算作td去处理。虽然th在视觉上也是单元格但 Jsoup 的td选择器不会匹配到它。如果表头是必需的要单独用th选择器取一次如果只要数据直接跳过thead行的判断即可。3.2 核心解析代码选择器取值与空值防御下面是这个例子的主解析逻辑目标是把#courseTable里的每一行数据读出来按单元格组成字符串数组。package com.demo.crawler; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; public class TableCrawler { public static void main(String[] args) throws Exception { Document doc Jsoup.connect(file:///D:/demo/table_demo.html) .timeout(10000) .get(); // 定位 id 为 courseTable 的表格然后选取其中的所有 tr 行 Elements rows doc.select(#courseTable tr); for (Element row : rows) { // 当前行下所有 td 单元格注意 th 不会被选中 Elements cells row.select(td); // 跳过空行没有 td 说明是表头行或其他结构 if (cells.size() 0) { continue; } // 遍历单元格取文本并去除首尾空白 for (Element cell : cells) { String text cell.text().trim(); System.out.print(text | ); } System.out.println(); } } }逻辑拆解一下doc.select(#courseTable tr)返回一个Elements集合它底层是一个ArrayList按文档顺序排列。外层 for 循环遍历每一行内层row.select(td)是相对选择器只查找当前行内部的td节点。cell.text()得到的是该单元格去掉内部 HTML 标签后的纯文本比如4.0这个值如果它被strong包裹text()依然能正确提取。参数上trim()是必须的。很多网页表格的文本节点带换行和缩进不清理会出现 4.0 这样的脏数据。cells.size() 0的判断用于跳过空行和表头行thead里是th不是td所以这里size为 0直接 continue。3.3 定位策略id、class 与层级组合的选择器写法现实中的网页表格没有那么规整。同一个页面可能有多个表格或者表格外层还套着div。选择器要从「唯一定位目标表格」的角度出发而不是上来就写doc.select(tr)。常见做法是按优先级尝试第一优先是 id比如#courseTable一定是唯一的第二是 class 加标签组合比如table.grid-table第三是层级关系比如#contentWrapper table.data-table表示「先找 id 为 contentWrapper 的容器再找它内部 class 为>// 假设目标表格在一个 id 为 content 的 div 内部 Elements tables doc.select(#content table); if (tables.size() 0) { System.out.println(未找到目标表格请检查页面结构); return; } // 取第一个表格后续解析只针对它进行 Element targetTable tables.get(0); Elements rows targetTable.select(tr);tables.size() 0的检查很关键它能在页面结构调整后第一时间告诉你「定位失败了」而不是让程序带着空集合往下跑然后报一个莫名其妙的空指针。这算是血泪经验不确定页面结构时候先做存在性判断再进入解析流程。4. 数据落地把解析结果写入 CSV而不是只打印到控制台4.1 为什么用 CSV零依赖、Excel 直接打开、编码可控很多人做完第 3 章的代码就停了觉得控制台能看到数据就行。但课程设计或实际需求里数据最终要交到别人手上控制台输出没法交付。把数据写入文件这一步值得单独封装成一个类。选 CSV 而不是 Excel 的原因很实际写 Excel 需要引入 POI 依赖POI 的体积和技术细节对这个小项目来说过于沉重而 CSV 本质是纯文本文件不需要任何额外依赖用 JDK 自带的FileWriter或BufferedWriter就能写。Excel 和 WPS 都能直接打开 CSV双击就能查看结果对验收方最友好。CSV 的编码是个容易忽略的细节。Windows 上 Excel 打开 CSV 默认按 GBK 解码如果 Java 代码用 UTF-8 写文件生成的 CSV 用 Excel 打开中文全乱码。处理方式两种要么写文件时直接用 GBK 编码要么写完用记事本手动转码。前者更省事代码里把StandardCharsets.UTF_8换成Charset.forName(GBK)即可。4.2 封装 CsvWriter处理逗号、引号与换行表格数据里如果包含逗号、双引号或换行直接拼接会破坏 CSV 的列结构。比如某个课程名称是计算机, 基础不处理的话这一个字段会被 Excel 拆成两列。规范的做法是对每个字段做转义字段里包含逗号、引号或换行时用双引号包裹整个字段并把字段内部的引号替换成两个连续引号。package com.demo.crawler; import java.io.BufferedWriter; import java.io.FileOutputStream; import java.io.OutputStreamWriter; import java.nio.charset.Charset; import java.util.List; public class CsvWriter { /** * 将表格数据写出为 CSV 文件 * * param filePath 输出文件路径 * param rows 每行是一个字段列表 */ public static void write(String filePath, ListString[] rows) throws Exception { try (BufferedWriter writer new BufferedWriter(new OutputStreamWriter( new FileOutputStream(filePath), Charset.forName(GBK)))) { for (String[] row : rows) { StringBuilder line new StringBuilder(); for (int i 0; i row.length; i) { if (i 0) { line.append(,); } line.append(escapeField(row[i])); } writer.write(line.toString()); writer.newLine(); } } } /** * 转义单个字段包含逗号、引号或换行时加双引号 */ private static String escapeField(String field) { if (field null) { return ; } // 判断是否包含 CSV 特殊字符 if (field.indexOf(,) 0 || field.indexOf() 0 || field.indexOf(\n) 0 || field.indexOf(\r) 0) { // 内部双引号换成两个引号整体再用引号包裹 return \ field.replace(\, \\) \; } return field; } }这段代码里的try-with-resources写法是 Java 7 及以上才有的语法MyEclipse 编译级别如果是 1.6 需要改成传统 try-finally。OutputStreamWriter包FileOutputStream是为了指定 GBK 编码这是保证 Excel 打开不乱码的关键。escapeField方法返回值在特殊字符场景下会多出首尾引号这正是 CSV 规范要求的写法。调用方在主程序里收集行数据最后一次性写出ListString[] allRows new ArrayList(); // 解析表格把每行数据存入 allRows Elements rows doc.select(#courseTable tbody tr); for (Element row : rows) { Elements cells row.select(td); if (cells.size() 0) { continue; } String[] rowData new String[cells.size()]; for (int i 0; i cells.size(); i) { rowData[i] cells.get(i).text().trim(); } allRows.add(rowData); } // 写出 CSVGBK 编码便于 Excel 直接打开 CsvWriter.write(D:/demo/course_table.csv, allRows); System.out.println(抓取完成共写入 allRows.size() 行数据);4.3 结果验证先查行数再抽查字段写完数据后如何确认抓取是正确的我一般按三步验证第一步看控制台输出的行数和目标页面tbody里的行数对比第二步打开 CSV随机抽查两三行数据和网页原文核对第三步检查有没有整行缺失特别是表格中间有合并单元格的页面。这三步做完基本可以确认解析逻辑没有大问题。一个容易忽略的细节是 url 里的数据行数可能在打开页面时正常但抓取时被服务端做了权限判断。验证时如果行数为 0先把doc.select(#courseTable tbody tr)改成doc.select(#courseTable tr)打印所有行内容看看是不是连表头都没抓到这能快速区分是选择器问题还是连接被拦截。5. 避坑手册编码、动态表格、HTTPS 与版本兼容的四次实战排错5.1 中文乱码控制台正常写出的 CSV 全乱码现象控制台打印的表格中文正常但写出的 CSV 文件用 Excel 打开全是乱码。原因控制台走的是 MyEclipse 的默认编码通常是 UTF-8而 Excel 打开无 BOM 的 CSV 时按系统本地编码中文系统是 GBK解码。代码里如果用new FileWriter()写文件它使用 JVM 默认编码MyEclipse 里配了 UTF-8 就会写 UTF-8两边不一致就乱码。解决写文件时显式指定编码new OutputStreamWriter(new FileOutputStream(path), Charset.forName(GBK))。注意这个坑最容易在代码评审时被忽略因为控制台输出永远看不出来问题只有把文件发给别人用 Excel 打开时才暴露。5.2 动态表格页面有数据程序抓到的 table 里却是空的现象浏览器打开页面能看到完整的表格数据程序跑完rows.size()返回 0或只抓到表头。原因表格是 JavaScript 动态渲染的。Jsoup.connect().get()只执行 HTTP 请求拿到的是服务端返回的原始 HTML它不会运行 JS。如果页面里的数据是 ajax 请求后台接口后动态拼进表格的原始 HTML 里只有table空壳。解决先确认页面是静态还是动态。在浏览器 F12 里看table节点如果 HTML 里有完整的td内容/td就是静态直接抓没问题如果表格是 JS 创建或填充的先找网络请求里的 XHR 接口很多表格的数据来源于某个返回 JSON 的接口直接抓这个接口更容易。找不到对应接口时常见做法是用 Selenium 等带浏览器的自动化工具渲染后再取表格但那个方案依赖浏览器环境重量级得多作为最后手段。5.3 HTTPS 证书报错本地能访问的 https 页面程序连不上现象Jsoup.connect(url).get()抛出javax.net.ssl.SSLHandshakeException对方证书验证不通过。原因目标站点用的是自签名证书或者内网部署的测试环境证书链不完整JVM 默认信任库不信任这个证书。解决证书问题有专门的免信任连接写法。可以通过自定义TrustManager绕过证书校验。注意这个写法只用于本地开发验证不要放在线上生产环境因为等于放弃了所有安全性。import javax.net.ssl.SSLContext; import javax.net.ssl.X509TrustManager; import java.security.cert.X509Certificate; // 构建不校验证书的 SSLContext仅用于测试环境 SSLContext sslContext SSLContext.getInstance(TLS); sslContext.init(null, new X509TrustManager[] { new X509TrustManager() { public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } public void checkClientTrusted(X509Certificate[] c, String a) { } public void checkServerTrusted(X509Certificate[] c, String a) { } } }, null); // 通过 Jsoup 的连接设置套接字工厂 Connection connection Jsoup.connect(url) .sslSocketFactory(sslContext.getSocketFactory()) .timeout(10000); Document doc connection.get();这段代码的问题是SSLContext.init()没有指定 SecureRandom 参数某些高版本 JDK 对它没要求但最好显式传new java.security.SecureRandom()。整体逻辑就是让 JVM 跳过服务端证书链的校验从而完成 TLS 握手。5.4 版本冲突jsoup 新版本在 MyEclipse 里编译报错现象项目编译时提示UnsupportedClassVersionError或java.lang.NoClassDefFoundError。原因新版 Jsoup 的 class 文件版本高于 MyEclipse 使用的 JDK 编译级别。比如 jsoup 1.14.x 是 Java 8 编译出来的MyEclipse 默认用 JDK 1.7 跑就加载不了。解决调整两处配置。第一处是 Window - Preferences - Java - Compiler 里的 Compiler compliance level 改到 1.8第二处是确认项目 Build Path 里 JRE System Library 是 JDK 1.8而不是 1.7。如果 MyEclipse 里根本没有 JDK 1.8直接换 jsoup 1.11.2这是最后一个支持 Java 7 的版本线。5.5 合并单元格导致列错位rowspan 和 colspan 让数据对不上列现象抓到的每一行字段数量不一致有的一行 5 个值有的一行 3 个值数据整体错位。原因表格里用了rowspan跨行合并或colspan跨列合并。比如课程表里体育课跨了两行HTML 里第二行的td数量就少一个按行逐个取td时自然错位。解决处理合并单元格需要维护一个「被上一行占用的列位」数组。遇到 colspan 时把当前字段在后续行对应的列位标记为「已被占用」遇到 rowspan 时在后续 N 行里跳过该列。这个逻辑有点繁琐实操里如果目标表格有合并单元格先观测合并的规律很多报表类表格只有表头合并数据行不合并只需要单独处理表头数据行按列数最少的行对齐即可。判断标准数一下最大列数和最小列数的差值差值稳定则可以按规律处理差值随机则说明解析思路有问题回到 5.2 检查页面结构。6. 进阶技巧翻页抓取与行去重的完整循环多数课程设计只要抓一页但实际使用里数据往往分布在多页。翻页抓取的通用做法是找 URL 里的页码参数。比如目标页面的分页规则是list_1.html、list_2.html那就在循环里拼接页码。核心是控制抓取节奏别一口气把所有页面全抓完容易被服务端限流也可能给对方服务器带来压力。下面是一个翻页循环加上行内容去重的例子其中去重用的是LinkedHashSet它既能保证顺序又能自动去重。package com.demo.crawler; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; public class PagedCrawler { public static void main(String[] args) throws Exception { // 用 LinkedHashSet 保存行数据保证顺序的同时自动去重 SetString uniqueRows new LinkedHashSet(); ListString[] allRows new ArrayList(); for (int page 1; page 5; page) { // 拼接分页 URL构造示例list_1.html, list_2.html String url file:///D:/demo/list_ page .html; Document doc Jsoup.connect(url) .timeout(10000) .userAgent(Mozilla/5.0 (Windows NT 10.0; Win64; x64)) .get(); Elements rows doc.select(#dataTable tbody tr); for (Element row : rows) { Elements cells row.select(td); if (cells.size() 0) { continue; } // 把当前行的所有单元格拼接成字符串作为去重的 key StringBuilder keyBuilder new StringBuilder(); for (Element cell : cells) { keyBuilder.append(cell.text().trim()).append(|); } String rowKey keyBuilder.toString(); // 如果这个 key 没出现过才保留本行数据 if (uniqueRows.add(rowKey)) { String[] rowData new String[cells.size()]; for (int i 0; i cells.size(); i) { rowData[i] cells.get(i).text().trim(); } allRows.add(rowData); } } System.out.println(第 page 页抓取完成当前累计 allRows.size() 行); // 每页抓完后休息 1.5 秒降低对目标站点的压力 Thread.sleep(1500); } CsvWriter.write(D:/demo/paged_result.csv, allRows); System.out.println(全部完成最终写入 allRows.size() 行); } }这个循环有几个值得留意的细节。SetString判断去重时用整行拼接做 key比用某一列做 key 更可靠因为不同行可能在某一列上相同但整行完全相同的概率极低。Thread.sleep(1500)是翻页抓取的节奏控制单位是毫秒按页面响应速度调整通常 1 到 2 秒够用。page 5是写死结束条件实际使用时要根据总页数调整可以改成从页面底部读「共 N 页」的文本再解析出 N。到这里整套例子就完整了。我跟很多开发者说过抓表格这事最花时间的从来不是写解析代码而是排查页面结构。从那以后我每次拿到新页面都强制先做一遍流程浏览器 F12 看table结构确认是静态还是动态确认有没有合并单元格最后才写选择器和循环。这套顺序走完解析代码基本一次过。希望帮到你。本文还有配套的精品资源点击获取