资讯动态

用Jsoup抓取网页表格:Java工程实战,从解析到CSV导出

发布时间:2026/10/15 17:37:18 来源:尧图企业网站定制
简介用Java爬取网页表格的示例代码包运行环境为MyEclipse主要依托Jsoup库实现对网页HTML的请求、解析与表格数据抽取。示例面向Java网络爬虫初学者和需要从网页中提取结构化表格数据的开发者完整演示了创建项目、导入依赖、建立网页连接、通过选择器定位表格元素、逐行逐列读取单元格内容以及将结果存入集合或自定义对象的过程并包含网络异常处理机制。资源包共24个文件包括7个Java源文件、7个class编译文件、7个依赖jar包以及prefs、project、classpath等工程配置文件压缩后总大小1.15MB便于导入MyEclipse直接查看与运行。目前已有710人下载学习。通过阅读运行源码读者可直观理解Jsoup的API调用方式掌握网页表格数据的抓取套路为后续数据清洗、分析或构建更复杂的爬虫工具打下基础。1. 抓网页表格一个 Jsoup 就能跑通的 Java 工程一个常见误解是抓网页表格必须上浏览器自动化。实际只要目标是“那张表的内容”多数静态业务页用一个 Jsoup 库就够了代码量不超过一百行。这个工程就是把最短路径拆给你看MyEclipse 里建一个普通 Java Projectlib 里放个 jsoup 的 jar请求页面后用 CSS 选择器定位 table遍历 tr/td 把数据装进 List再导出成 CSV。全程不依赖 Maven、不碰浏览器适合刚学完 Java SE 想拿真实网页练手的初级开发者也适合业务上需要定时同步公开表格数据的脚本场景。拆解开始前先给结论这个例子里最难的往往不是解析代码本身而是你确定“页面上该出现的那张表到底是不是服务端渲染出来的”。2. 工程结构速览MyEclipse 下一个爬虫工程怎么摆2.1 目录结构与类职责带着 Java 爬虫需求动手最容易翻车的写法是把所有逻辑塞进一个 main 方法。这个例子按“请求、解析、清洗、导出”切了四个类职责一眼能看清后面想加功能也不用重构。解压后的工程目录大概是这样的TableCrawler/ ├── src/ │ ├── com/example/crawler/ │ │ ├── Main.java // 程序入口编排整条链路 │ │ ├── HttpRequester.java // 发起 HTTP 请求返回 Document │ │ ├── TableParser.java // 定位并解析 HTML 表格 │ │ ├── TableData.java // 表格数据模型封装表头和行数据 │ │ └── DataExporter.java // 导出 CSV / 可扩展写数据库 ├── lib/ │ └── jsoup-x.x.jar // 唯一的外部依赖 └── output/ └── table_result.csv // 运行后生成的导出文件每个类的职责划分遵循一条原则一个类只做一件事。HttpRequester 只负责拿到 HTML 并转成 Jsoup 的 Document 对象不关心页面里有哪些表格TableParser 只做定位和解析输入是 Document 和选择器输出是 TableDataDataExporter 只消费 TableData把数据写成 CSV。这样拆的好处是哪天你想把 HttpRequester 换成 HttpClient 实现其他三个类完全不用动。2.2 技术选型Jsoup 为什么比 HttpClient 正则稳很多初学者第一次写爬虫选的是 HttpClient 拿 HTML然后手写正则去匹配tr、td。先说结论表格只有五六行时正则还能扛表格超过二十行、带 colspan 或者出现嵌套表格时正则匹配的结果就开始不可控debug 时候心态容易崩。对比一下两种路线的差异维度HttpClient 正则Jsoup解析起点先拿到整个 HTML 字符串先构建 DOM 树定位元素自己写 Pattern 匹配CSS 选择器 select(table#id tr td)嵌套表格正则容易穿层级DOM 结构天然区分父子关系残损 HTML标签不闭合就匹配失败内置解析器自动修正闭合编码处理手动判断很麻烦可从响应头自动识别字符集Jsoup 本质是个 HTML 解析器不是浏览器所以它不执行 JavaScript、不做页面渲染。这个特性既是优点也是边界静态页面抓取是它的主场动态渲染页面它就无能为力这件事后面避坑章节我会单独展开。2.3 MyEclipse 下把工程跑起来的三步MyEclipse 里运行这个项目不需要额外插件按下面顺序操作即可第一步File → New → Java ProjectProject name 填 TableCrawlerJRE 选择 JDK 1.8 或更高版本Finish 后得到空工程。第二步把上面目录里的 src 和 lib 复制进工程目录回到 MyEclipse 按 F5 刷新然后在 Package Explorer 里右键 lib 下的 jar 文件 → Build Path → Add to Build Path。第三步打开 Main.java把目标 URL 改成一个你确定可以访问的静态表格页面右键 → Run As → Java Application。跑起来后如果 Console 里出现“抓取完成共 N 行数据”之类的输出说明链路已经通了。这里有个小细节MyEclipse 的 Java Compiler 默认 compliance level 可能低于 JDK 实际版本如果你的代码里用了 Java 8 的语法却报编译错误去 Project Properties → Java Compiler 把级别改成 1.8 即可这个坑后面也踩到了。3. 核心抓取逻辑拆解从 URL 到表格数据的完整链路3.1 第一步带请求头的 HTTP 请求抓取的第一步不是解析是“要得到页面”。很多页面服务器会校验请求方身份如果你的程序裸奔访问服务器直接甩一个 403 回来。所以 HttpRequester 里给 Jsoup 的 connect 方法补上了浏览器标识import org.jsoup.Connection; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; public class HttpRequester { public Document fetch(String url) throws IOException { Connection conn Jsoup.connect(url) .userAgent(Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36) .referrer(url) .timeout(10000); Connection.Response response conn.execute(); Document doc response.parse(); return doc; } }这段代码里有三个值得留意的参数。userAgent模拟了 Chrome 浏览器的标识目的不是骗服务器而是让服务器把请求当成普通访问很多反爬策略第一步就是拦截空 UA 的请求。referrer告诉服务器“我从哪个页面跳过来的”部分站点会校验这个头。timeout单位是毫秒10 秒对多数页面够用但如果目标页面是动态生成的大型表格建议改成 20 秒以上。execute()加parse()的组合比直接用get()多一步拿到 Response 对象的机会——Response 里能读到服务器返回的实际 Content-Type 和 charset这为乱码排查留了后路。3.2 第二步用 CSS 选择器定位表格页面拿到手后接下来要回答一个问题页面里可能有好几个表格你到底要哪张TableParser 的做法是把选择器作为参数传进来调用方决定抓哪张表import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; public class TableParser { public TableData parse(Document doc, String cssSelector) { // selectFirst 返回第一个匹配的元素没有匹配时返回 null Element table doc.selectFirst(cssSelector); if (table null) { throw new IllegalArgumentException( 页面中找不到选择器对应的表格 cssSelector); } // 拿到 table 元素后解析行数据 return parseRows(table); } }CSS 选择器的写法决定命中率。如果目标表格长这样table idtradeTable classdata-table那么table#tradeTable精确按 id 定位table.data-table按 class 定位。只有 class 没有 id 的页面用后者更稳。如果页面结构简陋表格没有 id 也没有 class可以退而求其次用doc.select(table).first()取第一个表格这招只在页面结构非常稳定时管用表格顺序一变就会抓错对象。3.3 第三步逐行逐列遍历单元格定位到 table 元素之后核心工作是遍历它的行和列。这个步骤的代码是整个项目里最容易被选错做法的位置常见错误是用 element.html() 再把 HTML 字符串拿出来第二次正则解析。正确姿势是继续让 Jsoup 的结构化查询替你做import org.jsoup.nodes.Element; import org.jsoup.select.Elements; public ListListString parseRows(Element table) { ListListString rows new ArrayListListString(); // 选取表格内所有行 tr Elements trList table.select(tr); for (Element tr : trList) { // 行内单元格既可能是 td 也可能是 th用逗号进行或匹配 Elements cells tr.select(th, td); if (cells.isEmpty()) { continue; // 跳过空行避免导入数据时出现整行 null } ListString row new ArrayListString(); for (Element cell : cells) { // cell.text() 只取可见文本自动忽略内部子标签 // \u00a0 是网页常用的不间断空格先替换再 trim String text cell.text().replace(\u00a0, ).trim(); row.add(text); } rows.add(row); } return rows; }tr.select(th, td)里的逗号是“或”的含义这样表头行里的 th 单元格也能一起被捕获。cell.text()方法会自动拼接单元格内所有文本如果某个单元格里嵌套了span或者atext() 会把它们内部的文字一并取出来不用手动剥标签。replace(\u00a0, ).trim()处理的是网页排版常用的不间断空格这种空格在解析后看起来像空格但编码不同不过滤的话写进 CSV 会变成肉眼可见的脏数据。3.4 第四步封装成表格模型并编排入口解析出来的行数据需要一个容器承接TableData 这个模型把表头和表体分开存放方便导出时按不同逻辑处理import java.util.ArrayList; import java.util.List; public class TableData { private ListString headers new ArrayListString(); private ListListString rows new ArrayListListString(); public void setHeaders(ListString headers) { this.headers headers; } public void addRow(ListString row) { rows.add(row); } public ListString getHeaders() { return headers; } public ListListString getRows() { return rows; } public int getRowCount() { return rows.size(); } }模型保持“纯字符串列表”这个朴素形态不做任何业务字段映射。原因是表格列是动态的不同页面列数和语义都不同一旦写死成具体的业务对象这个工具就只能服务一个页面了。保持字符串列表下游做数据清洗时可以自由按列处理。入口类 Main 的任务是把上面这几步串成一条完整的链路public class Main { public static void main(String[] args) throws Exception { String targetUrl http://example.com/public/table.jsp; String selector table#tradeTable; String outputPath output/table_result.csv; HttpRequester requester new HttpRequester(); Document doc requester.fetch(targetUrl); TableParser parser new TableParser(); TableData table parser.parse(doc, selector); DataExporter exporter new DataExporter(); exporter.exportCsv(table, outputPath); System.out.println(抓取完成共 table.getRowCount() 行数据); } }这里 targetUrl 写的是占位符实际运行前必须替换成你自己的目标页面。整个流程一句话描述就是发请求拿 Document → 用选择器取出 table → 遍历 tr/td 生成 List → 封装成 TableData → 交给导出器落盘。4. 数据清洗与落盘让抓下来的表是干净的表4.1 清洗规则解决空格、千分位和空行很多新手认为表格解析完就能直接导出结果打开 CSV 一看行行都有隐藏空格数字列带着千分位逗号用 Excel 求和时直接报“不是数字”。这个项目把清洗逻辑单独放了一层跑完解析后再统一处理。清洗的核心规则有三个。第一个是处理空白除了前面代码里已经做的\u00a0替换每行数据还要过滤掉全部单元格都是空字符串的行。第二个是数字清洗网页表格里的数字常写成“1,234.56”这种带千分位格式转成 double 前必须把逗号去掉。第三个是表头识别如果页面第一行是th把第一行单独抽出来存到 headers 里不混进 rows。public static String cleanNumeric(String raw) { if (raw null || raw.isEmpty()) { return raw; } // 去掉千分位逗号注意中文逗号也要处理 return raw.replace(,, ).replace(, ); } public static boolean isEmptyRow(ListString row) { for (String cell : row) { if (cell ! null !cell.isEmpty()) { return false; } } return true; }这里有一个容易忽略的边界raw 为 null 时直接调 replace 会抛空指针所以先用 null 判断挡一道。清洗逻辑设计成静态方法是因为它不持有状态纯粹输入输出工具方法这样写最省心。实际项目中我一般会在清洗完成后打印一次行数和解析前的原始行数对比如果数量不一致说明有空行被过滤这属于预期行为。4.2 导出 CSV带 BOM 解决 Excel 乱码CSV 是最通用的表格交付格式任何数据分析工具都能读。但直接用FileWriter写中文 CSV用 Excel 打开大概率乱码原因是 Excel 默认用 ANSI 编码去猜文件而 Java 里的字符串默认 UTF-8。实战中的做法是写入时加上 UTF-8 BOM 头让 Excel 正确识别编码import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; public void exportCsv(TableData data, String outputPath) throws IOException { BufferedWriter writer Files.newBufferedWriter( Paths.get(outputPath), StandardCharsets.UTF_8); // 写入 UTF-8 BOM避免 Excel 打开 CSV 中文乱码 writer.write(\uFEFF); for (ListString row : data.getRows()) { ListString escaped new ArrayListString(); for (String cell : row) { if (cell.contains(,) || cell.contains(\) || cell.contains(\n)) { // 含逗号/引号/换行的单元格必须用双引号包围并转义内部引号 escaped.add(\ cell.replace(\, \\) \); } else { escaped.add(cell); } } writer.write(String.join(,, escaped)); writer.newLine(); } writer.flush(); writer.close(); }这段代码有两个关键点。\uFEFF是零宽不换行空格放在文件头部就是 Excel 识别 UTF-8 的标记不带这个标记WPS 打开没问题但 Excel 会乱。String.join(,, escaped)是 Java 8 的字符串拼接方法如果 MyEclipse 里编译报错说明编译级别太低去 Project Properties 改成 1.8 及以上即可。单元格转义这步是 CSV 规范里的硬性要求——单元格内含逗号却不用双引号包住导出的文件会被 Excel 错误拆成多列数据错位后特别难看。4.3 可选扩展从 CSV 到写入数据库CSV 对一次性交付够用但如果这个爬虫要每天定时跑并把数据累积起来CSV 管理起来就麻烦了。到时把 DataExporter 里加一个 JDBC 实现就行。常见做法是把连接信息放到配置文件不硬编码在类里jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/crawler_db?useUnicodetruecharacterEncodingutf8useSSLfalse jdbc.usernameyour_user jdbc.passwordyour_password写入时用 PreparedStatement 批量插入比 executeStatement 拼 SQL 安全得多既能防止 SQL 注入又能提升批处理效率。注意 MySQL 8.x 和 5.x 的驱动类名不一样前者是 com.mysql.cj.jdbc.Driver后者是 com.mysql.jdbc.Driver用错类名会报 ClassNotFoundException。5. 避坑指南四个最容易翻车的点5.1 抓下来的中文全是乱码现象控制台打印数据时中文显示成问号或乱码写进 CSV 打开也是乱码。原因目标页面是 GBK 编码而 Jsoup 默认按 UTF-8 去解析文档。HTML 文档里如果没有明确声明 charset或者声明了但服务器响应头覆盖了它解析就会错位。解决在 HttpRequester 里拿到 Response 后先输出response.charset()看看服务器返回的编码。如果目标是 GBK解析前执行doc.charset(Charset.forName(GBK))。另外注意 MyEclipse 控制台本身的编码Window → Preferences → General → Workspace 把 Text File Encoding 改成 UTF-8同时跑程序时加 JVM 参数-Dfile.encodingUTF-8双保险。5.2 select 返回 null表格像被空气抓走现象页面用浏览器打开能看到表格但程序里selectFirst(selector)返回 null解析逻辑直接抛异常。原因表格是前端 JavaScript 动态渲染出来的Jsoup 拿到的是 JS 执行前的 HTML 骨架原本放表格的地方是空的 div 或 script 占位。解决第一时间打开浏览器开发者工具的 Network 面板刷新页面后筛选 XHR 请求很多动态表格的数据来自一个 JSON 或 XML 接口网址和表格数据一一对应。找到接口后用 HttpRequester 直接请求它再在内存里把 JSON 转成表格数据比解析渲染后的 HTML 更稳。如果数据接口也需要复杂签名那时才考虑上无头浏览器但对多数公开页面来说“找接口”才是正路。5.3 请求返回 403 Forbidden现象第一次抓取正常多跑几次后开始返回 403或者第一次就没成功。原因服务器检测到短时间高频请求触发了反爬限制或者请求头缺少 User-Agent服务器直接拒绝陌生客户端。解决基础手段是把 User-Agent 设成常见浏览器的标识Referer 指向目标页面的来源地址。进阶手段是在两次请求之间加随机延时比如Thread.sleep(1000 new Random().nextInt(2000))把访问频率降到人类浏览的水平。注意访问频率过快还可能把服务器拖垮采集公开数据也要有节制请求节奏设得克制一些对双方都好。5.4 MyEclipse 编译报错方法引用或 Lambda 不合法现象代码里用了String.join、Lambda 这类 Java 8 语法但 MyEclipse 报语法错误或者编译通过后运行直接抛 UnsupportedClassVersionError。原因MyEclipse 的 Java Compiler compliance level 默认可能配置成 1.5 或 1.6编译器按老语法规则检查代码遇到新语法就报错。解决右键工程 → Properties → Java Compiler → Compiler compliance level 改为 1.8然后在 Project Facets 里确认 Java 版本也是 1.8。改完记得 Project → Clean 重新编译一次。这类问题属于“环境比代码更坑”的典型案例代码本身没问题但工具链配置不对就是跑不起来。6. 从例子到顺手工具两处改造就够6.1 把 URL 和选择器挪到配置文件里硬编码 URL 的方式第一次跑通没问题第二周目标网页改地址就得改代码重新编译。动手改造的第一步是把 targetUrl、selector、outputPath 这些变量抽到一个 config.properties 文件里target.urlhttp://example.com/public/table.jsp table.selectortable#tradeTable output.pathoutput/table_result.csv request.timeout10000Main 里用 java.util.Properties 加载这个文件读取值时加一个默认值兜底避免配置文件漏写导致启动失败。改造完的效果是日常使用只需要编辑配置文件不需要碰代码这个工具才算从“调试 Demo”变成了“能用的脚本”。6.2 给请求加一个带退避的重试机制网络请求是最不可控的一环服务器偶尔超时或者返回 500不能因为一次失败就让整个抓取中断。在 HttpRequester 里包一层循环重试每次失败后等待间隔递增int maxRetry 3; for (int attempt 0; attempt maxRetry; attempt) { try { doc requester.fetch(url); break; } catch (IOException e) { // 最后一次失败时不再等待直接抛出 if (attempt maxRetry - 1) { throw e; } Thread.sleep(500 * (attempt 1)); } }重试等待采用线性退避第一次失败等 500 毫秒第二次等 1 秒给服务器留出恢复时间。配合前面避坑章节提到的随机延时请求节奏既稳定又礼貌这个工具放到定时任务里连跑一周基本不会断。改造完成的验证方法很简单跑一次全流程对比 CSV 文件行数和目标页面提示的记录总数再关掉网络跑第二次确认重试提示出现且程序不退出。自己写爬虫这几年最大的教训是每个请求参数都不能想当然——UA 没设置会 403编码没匹配会乱码超时太短在慢网下会连环失败。从那以后我每次写抓取脚本都会先确认两件事页面是不是静态渲染、响应头声明了什么编码然后把 URL、超时、重试次数全部抽到配置里再动手写解析代码。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑