Java爬虫实战:用Jsoup抓取网页表格并导出CSV 简介这是一份面向Java初学者的网页表格爬取示例基于MyEclipse环境演示如何通过Jsoup解析HTML表格并提取数据适合需要快速上手网络爬虫技术的数据采集、数据分析与自动化处理场景。资源提供了完整的可运行项目压缩包共24个文件主要包含7个Java源代码、对应编译后的class文件以及7个依赖库jar包如Jsoup、HttpClient、编码检测组件等另附MyEclipse的项目配置文件导入IDE后即可直接运行查看效果。该示例以表格数据抓取为核心从连接目标网页、按选择器定位表格元素到遍历行与单元格提取文本内容均有清晰完整的实现并加入异常处理以应对网络波动与解析失败。资源已有710人学习下载代码注释与结构便于初学者逐段对照理解。通过阅读和调试源码可以掌握Jsoup核心API的典型用法理解Java实现静态网页数据抓取的基本流程并为进一步扩展动态页面爬取、数据持久化等功能打下基础。1. 一个Java爬取网页表格的例子从HTML到行列数据的完整链路设想一个场景某个业务系统的页面里有一张几十行的产品参数表你急着把这些数据落到CSV或者数据库里手动复制会让你怀疑人生写个Python脚本又和现有Java工程割裂。这个项目就是一个Java爬取网页表格的完整例子运行环境锁定在MyEclipse从发起HTTP请求、拿到HTML字符串到用解析库取出表格里每一个单元格最后输出成结构化数据。它适合正在学Java、想接触爬虫但公司技术栈又不在Python上的开发者也适合那些需要把网页表格抓取逻辑嵌进老系统里的工程师。整个工程对外不依赖复杂框架适合作为第一次动手跑通的爬虫模板——它的示范价值大于业务价值你把它跑通后再往上加逻辑比从零写要快得多。2. 技术选型与工程结构Java爬表格为什么首选Jsoup而不是正则2.1 技术选型Jsoup让“抠表格”从玄学变成确定性操作先回答为什么不用正则。很多人看到HTML第一个念头是匹配tr(.*?)/tr但HTML不是规范文本属性顺序、标签大小写、嵌套层级一变表达式就废了。像td style...和td 都能让正则匹配纠结更别说表格里再套一个表格。用正则抠表格属于典型的“这次能成、下次翻车”我见过太多同事在这上面栽跟头最后一个字一个字去核对HTML源文件。Jsoup的思路完全不同——它先把整个HTML解析成DOM树然后告诉它“我要table下面的所有tr里的所有td”它走过的路径结构是确定的所以解析结果稳定得多。Java侧的网络请求这个例子用的是JDK自带的HttpURLConnection。为什么不直接上第三方请求库因为这些框架虽然功能强但引入它们意味着多几个jar包、多一堆配置对入门示例不友好。在MyEclipse里跑一个网页表格爬虫JDK自带类能覆盖绝大多数静态页面请求场景。核心配置就三样请求方法、超时时间、请求头User-Agent。真要遇到需要连接池、重试策略的生产场景再平滑替换也不迟。方案解析表格能力依赖适合场景正则表达式弱嵌套与属性变化即失效无一次性临时提取Jsoup强DOM选择器稳定一个jar包多数Java爬虫场景无头浏览器最强能渲染JS重异步动态页面2.2 工程结构抓取、解析、输出三条链路四个Java类解压工程包后目录是这样TableCrawler/ ├── src/ │ └── com/demo/crawler/ │ ├── HttpUtils.java // 负责把URL变成HTML字符串 │ ├── TableParser.java // 负责从HTML中切出表格行列 │ ├── DataExporter.java // 负责把列表数据写出 │ └── Main.java // 入口组装前三者的调用顺序 ├── lib/ │ └── jsoup.jar // 唯一外部依赖 └── .project / .classpath // MyEclipse工程标识文件四个类各自只做一件事。HttpUtils的对外方法接收URL和编码返回StringTableParser接收String返回ListListString——外层List是一行内层List是这一行的单元格DataExporter接收这个二维列表输出到控制台或CSVMain就是把url、charset、输出路径这些参数喂给它们。这种拆法不是刻意做设计而是为了让调试时能单独验证一环。比如你觉得解析有问题可以直接在TableParser里传一段写死的HTML字符串去测而不用每次都发网络请求。依赖文件的细节lib目录里的jsoup jar导入工程后必须加入Build PathMyEclipse才会把它写进classpath。很多人导入工程后跑起来报NoClassDefFoundError不是代码问题是这一步漏了。具体操作在第4章会展开。2.3 改动影响面替换任何一环都不动其他代码这个工程小但模块边界画得清楚。看下面的调用链就能明白Main - HttpUtils.fetch(url, charset) // 返回HTML - TableParser.parse(html) // 返回ListListString - DataExporter.toConsole(rows) // 控制台 - DataExporter.toCsv(rows, path) // CSV文件Main不关心HttpUtils底层是用URLConnection还是HttpClient也不关心DataExporter是写CSV还是写数据库。它只依赖三个对外方法。这意味着你想把网络请求换成OkHttp只需要改HttpUtils内部实现返回值类型保持String不变另外三个类一行都不用动。反向推导也一样目标页面表格结构变了只改TableParser里的选择器输出端要接数据库只改DataExporter。这种“改哪块只动哪块”的拆法是这个小工程里最值得保留的设计习惯。3. 核心模块拆解请求、解析、输出三段代码的协作链路3.1 抓取模块把URL变成干净的HTML字符串import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; public class HttpUtils { public static String fetch(String urlStr, String charset) { HttpURLConnection conn null; StringBuilder sb new StringBuilder(); try { URL url new URL(urlStr); conn (HttpURLConnection) url.openConnection(); conn.setConnectTimeout(5000); // 连接超时5秒超时直接抛异常 conn.setReadTimeout(5000); // 读取超时5秒防止页面卡住 conn.setRequestProperty(User-Agent, Mozilla/5.0 (Windows NT 10.0; Win64; x64)); conn.setRequestMethod(GET); BufferedReader reader new BufferedReader( new InputStreamReader(conn.getInputStream(), charset)); String line; while ((line reader.readLine()) ! null) { sb.append(line).append(\n); } reader.close(); } catch (Exception e) { e.printStackTrace(); } finally { if (conn ! null) { conn.disconnect(); // 断开连接避免网络资源泄漏 } } return sb.toString(); } }逻辑说明这段代码做的事情很简单——打开URL连接设置两个超时时间伪装成浏览器的User-Agent然后按指定字符集逐行读取响应体拼成字符串。setConnectTimeout管的是TCP连接建立阶段setReadTimeout管的是连接建立后读不到数据的等待时间。两个都设成5秒是为了避免目标站点响应慢时程序无限期挂住。参数说明urlStr是目标页面地址必须是完整URL带http://或https://前缀charset是页面编码常见的有UTF-8和GBK传错的结果是后面解析出来的中文全是乱码。注意这段代码把异常直接打印并返回空串生产代码应该抛出业务异常示例这样写是为了让新手看到异常堆栈就能定位。需要带Cookie或Referer时在请求头上继续setRequestProperty即可。3.2 表格解析模块用Jsoup选择器定位行与列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.List; public class TableParser { public static ListListString parse(String html) { ListListString result new ArrayListListString(); Document doc Jsoup.parse(html); // 选取页面里第一个table如果目标是带id的表格改成 table#dataId Element table doc.select(table).first(); if (table null) { return result; // 页面里没有表格返回空集合 } Elements rows table.select(tr); for (Element row : rows) { ListString rowData new ArrayListString(); // 同时取td和th表头和数据行都能覆盖 Elements cells row.select(td, th); for (Element cell : cells) { rowData.add(cell.text().trim()); // 取纯文本并去掉首尾空白 } result.add(rowData); } return result; } }逻辑说明两段式选择器。doc.select(table).first()先定位到表格节点row.select(td, th)横向取出这一行的所有单元格。cell.text()是Jsoup最常用的方法它拿到的是单元格内的纯文本会自动丢弃内部的HTML标签。比如tda href...链接文本/a/td拿到的只是“链接文本”而不是整个a标签。trim()把文本首尾的换行和空格清掉避免CSV里出现肉眼看不见的杂质。参数说明select(table)匹配的是第一个表格。如果页面里有多个表格想精确抓某一个可以改成doc.select(table#dataTable).first()或doc.select(table.list)前者按id选后者按class选。这种选择器写法和jQuery几乎一样熟悉前端的人上手很快。要注意如果表格里嵌了子表格row.select(td)不会横跨到子表格里去Jsoup的层级选择天然规避了嵌套表格串行的问题。3.3 输出模块控制台、CSV文件两种落点切换import java.io.FileWriter; import java.util.List; public class DataExporter { public static void toConsole(ListListString rows) { for (ListString row : rows) { System.out.println(String.join( | , row)); } } public static void toCsv(ListListString rows, String filePath) { try (FileWriter writer new FileWriter(filePath)) { for (ListString row : rows) { writer.write(String.join(,, row) \n); } } catch (Exception e) { e.printStackTrace(); } } }逻辑说明把一行数据用分隔符拼成字符串控制台输出用竖线|CSV文件用逗号,。CSV写入使用了try-with-resources语法文件流用完自动关闭不用手动finally去关JDK 1.7以上都支持。toCsv跑完会在项目根目录生成文件用Excel打开前要注意编码。这段代码有个隐藏的坑如果单元格内容本身包含逗号或引号直接String.join(,, row)会产生一个非法CSV。常见做法是给每个字段加双引号包裹内部原有引号再翻倍转义。示例为了保持简单没有处理但你在二次开发时大概率会撞上——尤其是抓“地址”“备注”这类长文本字段时。3.4 入口Main把三段拼成一条管道import java.util.List; public class Main { public static void main(String[] args) { // 改成你要抓的页面URL String url http://example.com/data.html; // 页面的字符编码拿不准就打开浏览器开发者工具看meta标签 String charset UTF-8; long start System.currentTimeMillis(); String html HttpUtils.fetch(url, charset); System.out.println(HTML抓取耗时(ms): (System.currentTimeMillis() - start)); ListListString rows TableParser.parse(html); System.out.println(解析到 rows.size() 行数据); DataExporter.toCsv(rows, output.csv); DataExporter.toConsole(rows); } }逻辑说明先抓HTML再解析表格最后导出。三步之间没有中间变量纠缠任何一步出问题都能通过控制台打印判断出来。System.currentTimeMillis()算出来的耗时用来确认网络请求是否正常rows.size()打印出来的行数是判断解析是否成功的第一指标。参数说明url是唯一需要频繁改的地方charset一旦传错后续解析和输出都基于是乱码的字符串output.csv是输出路径示例写的是相对路径生成在工程根目录下。这个Main结构极简没有配置文件和命令行参数解析所有参数硬编码在代码里——这正是第6章要改成配置文件的动机。4. MyEclipse运行全流程导入工程到验证结果4.1 环境准备与工程导入运行环境是题目点名的MyEclipse我的建议是JDK 1.8及以上版本兼容性最好。拿到工程包后先解压然后在MyEclipse里走导入流程File - Import - General - Existing Projects into Workspace选中解压目录看到TableCrawler工程被勾选后Finish。导入后不要急着跑先做一步关键操作右键项目 -Build Path - Configure Build Path - Libraries - Add JARs选中lib目录下的jsoup jar。这步漏掉的话运行到Jsoup.parse(html)那一行就会报NoClassDefFoundError——这是这个项目里最常见的起步翻车点代码一行没错纯粹是依赖没挂上。如果你拿到的工程包解压后没有.project或.classpath文件导入向导里根本看不到工程列表。原因是工程元信息文件在传输时被过滤掉了。碰到这种情况不用慌新建一个Java Project把src目录和lib目录复制进去再重新Add JARs一样能跑。区别只是工程名可能和你本地预期不一样。4.2 第一次运行与参数调整第一次跑建议保持示例URL不变先验证整条链路是通的。直接右键Main.java - Run As - Java Application控制台会打印耗时、行数和具体表格内容。这一步通过后再替换成你自己的目标页面。替换目标页面时按三个步骤检查。第一打开浏览器右键查看页面源代码确认数据是直接写在HTML里而不是由JavaScript异步加载——如果源码里搜索不到table说明这个页面抓不到得另找数据接口第二查看页面meta charset...标签把Main里的charset改成对应值第三如果页面里不止一个表格修改TableParser里的select(table)为更精确的选择器。有一个调试技巧能少走弯路在Main里临时加一行System.out.println(html.substring(0, 500))打印HTML前500个字符。如果这500个字符里能看到table说明抓取环节正常看不到说明URL、编码或者反爬拦截出问题了。排错先抓到HTML再谈解析这是爬虫调式的固定顺序。4.3 结果核对行数对比与CSV编码检查运行结束后控制台会打印“解析到 xx 行数据”。此时打开浏览器数一下目标表格的行数两个数字一致基本就是成功了。CSV文件默认用UTF-8编码写入用记事本打开看到正常中文就说明内容没问题。如果直接用Excel打开CSV中文可能是一堆乱码。这不是程序的问题是Windows版Excel默认用本地编码读CSV。解决办法有两个要么把CSV另存为带BOM的UTF-8格式要么在Excel里用数据导入功能手动指定UTF-8编码。很多初学者在这里误以为爬虫写坏了实际上文件内容是对的只是打开方式不对。示例的运行结果大致会长这样HTML抓取耗时(ms): 234 解析到 48 行数据 序号 | 名称 | 数量 | 备注 1 | 件A | 12 | 正常 2 | 件B | 7 | 缺货5. 避坑记录高频运行问题与排查手法5.1 现象运行到Jsoup.parse报NoClassDefFoundError这是出现频率最高的问题。原因只有一个jsoup jar没有加入项目的Build Path。很多人以为把jar放进lib目录就等于引入依赖但MyEclipse只认classpath里的条目磁盘上有文件不等于编译期可见。解决方法是右键项目 -Build Path - Configure Build Path - Libraries - Add JARs选中lib下的jsoup jar后点确定重新运行即可。注意Add JARs和Add External JARs的区别前者走的是项目内路径换机器后工程不丢依赖后者指向的是外部绝对路径换机器就失效。5.2 现象控制台输出的中文全是问号或乱码分两种情况。如果解析出来的数据本身就是乱的说明HttpUtils.fetch里的charset与页面实际编码不符。解决办法是查看页面源码里的meta charset...把Main里的charset改成对应值最常见的就是把UTF-8改成GBK。另一种情况是CSV文件打开正常、控制台打印乱码那问题出在IDE的控制台编码在Run配置里的VM参数加上-Dfile.encodingutf-8或者在Window - Preferences里把工作区编码改成UTF-8。5.3 现象浏览器能打开页面程序返回403或302这说明目标站点识别到你是一个非浏览器的客户端。代码里已经设置了User-Agent但部分站点还会校验Referer和Cookie。解决办法是在HttpUtils的请求头上补信息conn.setRequestProperty(Referer, https://目标站首页)如果页面需要登录后才有数据先从浏览器开发者工具里复制Cookie字符串再用conn.setRequestProperty(Cookie, 复制的值)带上。这里要提醒一句抓取他人站点前先确认对方是否有robots协议约定和数据使用授权工具无罪使用场景要合规。5.4 现象解析结果第一行是表头写入数据后表头混在数据里很多表格用thead包表头、tbody包数据行但示例代码遍历了所有tr所以表头也被当成普通行处理。解决办法是在TableParser里加一层判断如果当前row下的th数量大于0就认定为表头行跳过不加入结果集或者更规范一点用table.select(thead tr)和table.select(tbody tr)分别抓表头和数据区。日常爬虫中表头是否保留取决于你的需求——如果是做数据归档表头反而应该单独保留如果只是做清洗直接丢弃更干净。5.5 排查链路一个方法定位问题出在抓取还是解析上面四个问题经常同时出现新手容易一锅粥地乱改。我习惯用分段排查法先在Main里打印html.length()和html.substring(0, 500)如果长度异常大或出现html说明抓取环节正常往解析看如果长度是0或报堆栈往请求看。解析环节看rows.size()行数和目标表格对得上就往输出看对不上打印第一行rowData.toString()看内容结构。这样一层层剥大多数问题5分钟内能定位不会把时间耗在瞎改选择器上。6. 二次开发把它改成能配URL和分页的通用抓取工具这份例子下载下来之后我建议你别急着改业务逻辑先把“改URL要重新编译”这个别扭点解决掉。最简单的方式是引入一个配置文件config.properties把url、charset、表格选择器、输出路径全放进去用java.util.Properties读取Properties props new Properties(); props.load(new FileInputStream(config.properties)); String url props.getProperty(crawler.url); String charset props.getProperty(crawler.charset);这样换目标站点就把配置改一改代码不用重新编译。很多生产环境的爬虫任务都是这个套路因为运维同学改起来也顺手。分页是下一个马上会遇到的刚需。多数列表页的翻页是通过URL参数控制的比如?page1、?page2。在Main里套一个for循环每抓一页把解析结果累加到同一个List里页与页之间Thread.sleep(500)给目标站点留点缓冲也防自己被封for (int page 1; page 3; page) { String pageUrl baseUrl ?page page; String html HttpUtils.fetch(pageUrl, charset); rows.addAll(TableParser.parse(html)); Thread.sleep(500); }输出端要接数据库也很顺不用碰抓取和解析在DataExporter里加一个insertIntoDb(ListListString rows, Connection conn)方法内部用PreparedStatement循环插入即可。整体思路就是第2章说的那件事——边界拆得清楚改造就是加代码而不是改结构。我最早写抓取脚本时图省事请求、解析、输出全塞在一个类里后来被一个带嵌套表格的页面折磨到凌晨才真正明白把三条链路分开才是爬虫代码的“后悔药”。从那以后我每接到一个抓取任务都先把URL和选择器写进配置文件再跑一遍最小链路确认HTML到手了才继续往下做。希望这个例子能帮你把第一个网页表格顺利抓下来。本文还有配套的精品资源点击获取