
简介这是一份面向Java初学者的网页表格爬取示例运行环境为MyEclipse。示例通过Jsoup等库演示如何连接目标网页、解析HTML结构并提取表格数据适合希望快速上手数据采集或了解静态网页解析的开发者。压缩包共24个文件包含7个Java源码、7个class编译文件、7个jar依赖库如jsoup-1.7.2、httpclient等以及项目配置与MyEclipse工程文件整体约1.15MB结构清晰可直接导入IDE查看运行。资料提供完整的爬取思路从请求网页、选择表格元素到遍历行列数据并包含异常处理与依赖配置等关键细节。通过学习该示例读者可以掌握Java爬虫的基本流程理解Jsoup库的核心API为后续开发动态网页爬虫、数据清洗与存储等更复杂任务打下基础。已有710人学习下载适合入门级Java爬虫学习者参考实践。1. 为什么“Java爬取网页表格”值得做成一个完整方案做课设或毕设时很多人会把“爬网页表格”想得很简单——拿个URL读一读HTML截取标签完事。真正动手才发现网页表格远不止table标签那么简单有的是动态加载有的是嵌套表格有的打开页面是表格、抓下来却是一堆JS脚本。我见过有同学用正则硬抠了一周最后换了个目标网站整个代码直接作废。这套方案的价值在于用Java Jsoup在MyEclipse环境下把“网页表格抓取”做成一个可复用、可讲解、可展示的完整流程从URL采集、HTML解析、数据清洗到JDBC入库一次走通。适合的人群很明确正在做Java课设、毕设或者刚入行需要快速交付一个数据采集模块的开发者。你不需要会Python爬虫不需要懂Scrapy框架只需要有MyEclipse和JDK按这套方案就能跑出结果。方案里的每一步我都会说明“为什么这么设计”和“换个场景怎么改”这样你交作业时不止能运行还能讲清楚设计思路。2. 方案先立住需求分析、技术选型与整体结构2.1 需求拆解这个项目到底要交付什么在做任何代码之前先把需求拆清楚。这个项目的核心目标是定时抓取目标网页中的表格数据清洗后存入MySQL数据库并提供查询接口验证抓取结果。拆开来看有三个子任务第一抓取能力。程序要能从指定的URL列表出发把页面上的表格数据完整提取出来这里说的“完整”包括表头和每一行内容并且要处理分页、相对链接、空单元格等情况。第二存储能力。抓到的数据不能只打印在控制台上要设计一张合理的表结构存进MySQL方便后续查询和统计。第三工程能力。整个采集过程要有日志、有异常处理、能重复执行而不产生重复数据代码结构要分层不能把解析和存储全写在main方法里。这个需求拆解的方式直接决定了后面代码怎么组织。我见过很多同学做类似题目拿到需求就写代码最后main方法里塞了几百行既没法测试也没法答辩。正确做法是先把模块边界划清楚。2.2 技术选型为什么是Jsoup而不是HttpClient 正则这个项目的技术选型我坚持用Jsoup原因很简单MyEclipse自带的Java环境跑Jsoup没有任何额外配置它的核心能力是DOM解析——把HTML当成一棵树来操作用选择器直接定位表格行和单元格这比用正则去匹配标签可靠得多。org.jsoup.Jsoup.connect(URL).get()返回一个Document对象然后doc.select(table)拿到所有表格再遍历tr和td就能提取数据。这套API对做过Web开发的人来说几乎零学习成本。可能有人问为什么不用Selenium因为MyEclipse环境下跑Selenium需要额外下载浏览器驱动而且动态页面渲染会引入大量不确定性对课设和大多数静态网页采集场景来说属于过度设计。也有人在生产环境用Apache HttpClient做请求、用Jsoup做解析但在教学和交付场景里Jsoup自带的连接方法完全够用。下面给出项目的Maven依赖和包结构这是整个方案的骨架dependencies dependency groupIdorg.jsoup/groupId artifactIdjsoup/artifactId version1.15.3/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.49/version /dependency dependency groupIdorg.slf4j/groupId artifactIdslf4j-log4j12/artifactId version1.7.36/version /dependency /dependencies依赖配置说明Jsoup的版本我选1.15.3这是一个相对稳定的版本API变化小网上资料多。MySQL驱动用5.1.49原因是很多学校机房装的MySQL是5.x版本用8.x驱动反而容易出现时区报错。SLF4J是日志门面配合Log4j输出采集日志方便观察抓取过程。如果你用的是Oracle数据库把第二个依赖替换成对应的JDBC驱动即可其余代码不用改。2.3 总体架构三层结构与五个核心类直接上图说话这套方案采用经典的三层架构五个核心类各司其职定时调度层TimerTask负责定时触发采集任务可配置开关 抓取解析层TableSpider核心抓取类负责发送HTTP请求并解析HTML表格 数据加工层DataCleaner清洗数据过滤非法行、处理空值 持久层JdbcUtil数据库连接工具类 TableDao数据入库与去重查询分层的意义在哪里如果你把抓取和存储写在一起哪天数据库密码改了要改动整个爬虫类哪天目标网站的表格结构变了又要改动存储逻辑。分层之后每层只负责一件事答辩的时候你要说“职责单一方便替换和维护”——这句话一出来比你在代码里加一百行注释都管用。持久层采用JDBC 连接池设计让我在答辩和项目展示中既能展示“手写SQL”的扎实功底又兼顾了生产环境的性能需求体现从课设到工程化的思维升级。2.4 数据库设计不做冗余一张表讲清楚我通常的做法是设计两张表一张存储抓取到的业务数据一张记录每次采集的运行日志。以“通用网页表格采集”为例如表所示字段名类型说明idINT 自增主键唯一标识row_keyVARCHAR(64)行指纹用于去重col1 ~ colNVARCHAR(255)表格各列数据source_urlVARCHAR(255)来源URLcreate_timeDATETIME采集时间表结构如图这里以采集某模拟项目X的商品信息表为例CREATE TABLE tb_web_table ( id INT(11) NOT NULL AUTO_INCREMENT COMMENT 自增主键, row_key VARCHAR(255) DEFAULT NULL COMMENT 行数据MD5指纹, col1 VARCHAR(255) DEFAULT NULL COMMENT 第一列数据, col2 VARCHAR(255) DEFAULT NULL COMMENT 第二列数据, col3 VARCHAR(255) DEFAULT NULL COMMENT 第三列数据, col4 VARCHAR(255) DEFAULT NULL COMMENT 第四列数据, source_url VARCHAR(500) DEFAULT NULL COMMENT 来源页面地址, create_time DATETIME DEFAULT NULL COMMENT 抓取时间, PRIMARY KEY (id), KEY idx_row_key (row_key) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT网页采集数据存储表;设计说明row_key字段是整个去重机制的基石通过对一行数据的列值拼接后取MD5得到重复抓取时就能直接跳过。source_url记录数据来源这一步很多人忽略但它在数据溯源和演示时很有说服力。字符集用utf8mb4而不是utf8是为了兼容生僻字和emoji字符。实际的表结构应当根据目标网页的列数量动态创建但核心设计思路是固定的。3. 模块实现从HTTP请求到表格行解析的完整编码3.1 抓取模块连接、超时、重试和解析一气呵成抓取模块是整个方案的心脏。我一般会这样组织TableSpider类外面看是一个solve()方法内部按“连接、解析、提取、封装”四步走。先看核心代码public class TableSpider { private static final Logger logger LoggerFactory.getLogger(TableSpider.class); private static final String USER_AGENT Mozilla/5.0 (Windows NT 10.0; Win64; x64); private static final int TIMEOUT 10000; // 10秒超时防止卡死 private static final int MAX_RETRY 3; // 失败重试次数 /** * 抓取指定URL中的表格数据 * param url 目标网页地址 * return 表格数据列表每个元素是一行的各列数据 */ public ListRowData fetchTableData(String url) throws IOException { ListRowData result new ArrayList(); Document doc fetchDocumentWithRetry(url); // 选择器定位table标签可根据实际情况调整 Elements tables doc.select(table); if (tables.isEmpty()) { logger.warn(页面无table标签可能为动态加载: {}, url); return result; } // 只抓取第一个表格多表格场景可遍历处理 Element table tables.get(0); for (Element row : table.select(tr)) { Elements cells row.select(th, td); if (cells.isEmpty()) continue; RowData rowData new RowData(); for (int i 0; i cells.size(); i) { rowData.addCell(cells.get(i).text().trim()); } rowData.setSourceUrl(url); result.add(rowData); } logger.info(成功解析表格共{}行数据URL: {}, result.size(), url); return result; } private Document fetchDocumentWithRetry(String url) throws IOException { for (int attempt 1; attempt MAX_RETRY; attempt) { try { return Jsoup.connect(url) .userAgent(USER_AGENT) .timeout(TIMEOUT) .get(); } catch (IOException e) { logger.warn(第{}次请求失败: {}, attempt, e.getMessage()); if (attempt MAX_RETRY) throw e; try { Thread.sleep(2000 * attempt); // 简单退避 } catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw new IOException(重试等待被中断, ie); } } } return null; } }代码逻辑拆解fetchDocumentWithRetry封装了网络请求和重试逻辑请求失败时先退避2秒再重试最多3次。为什么要重试因为目标网站偶尔会返回503或者连接被重置直接失败会让整个采集任务中断。USER_AGENT设置成浏览器的标识是为了降低被服务器当作机器人拒绝的概率——这不是绕过什么防抓机制而是像一个正常用户那样去访问公开页面中的数据。解析部分要注意我在选择器上写的是table通用标签实际开发中建议先用浏览器F12看目标网页的表格特征如果页面里有多个表格就用table.with-class这样的带类名选择器精准定位避免抓错、多抓或漏抓。这一判断是纯技术判断与任何平台的防爬协议无关。3.2 清洗模块剥离杂质过滤无效行网络上下来的数据几乎不可能直接用。有的单元格前后带着换行符有的整行是空数据有的列数不齐。清洗模块解决三个问题public class DataCleaner { /** * 清洗一行数据 * param rowData 原始行数据 * param requiredColCount 期望的列数 * return 清洗后的数据不合格返回 null */ public RowData clean(RowData rowData, int requiredColCount) { ListString cells rowData.getCells(); if (cells.isEmpty()) return null; // 列数不一致补空到目标列数 if (cells.size() requiredColCount) { while (cells.size() requiredColCount) { cells.add(); // 用空字符串补齐保持表结构完整 } } // 过滤标题行常见的表头干扰行 if (cells.get(0).contains(序号) cells.get(1).contains(名称)) { return null; } // 全空行直接丢弃 boolean allBlank true; for (String cell : cells) { if (!cell.trim().isEmpty()) { allBlank false; break; } } if (allBlank) return null; rowData.setCells(cells); return rowData; } }清洗模块的设计思路是“宁缺毋滥”——拿不准的行宁可丢掉也不要入库否则后面查数据的时候全是脏数据演示效果很差。列数不足时用空字符串补齐这样入库时不会因为字段缺失导致SQL异常标题行按内容特征过滤不同网站的表头词可以自己扩展。这一段逻辑虽然简单但在答辩时是非常好的切入点——你可以在演示中展示同一批数据清洗前后的对比直观说明这个模块存在的价值。3.3 入库存取MD5去重与批量写入的性能平衡存储层的核心是去重逻辑。如果不做去重定时任务每天跑一次数据库里全是重复数据表格越涨越大查询越来越慢。去重方案用MD5指纹看代码public class TableDao { private JdbcUtil jdbcUtil new JdbcUtil(); /** * 批量插入采集数据 * param rowDataList 清洗后的数据列表 * return 实际插入的行数 */ public int batchInsert(ListRowData rowDataList) { Connection conn null; PreparedStatement ps null; int insertCount 0; try { conn jdbcUtil.getConnection(); // 手动事务要么全部成功要么全部回滚 conn.setAutoCommit(false); String sql INSERT INTO tb_web_table (row_key, col1, col2, col3, col4, source_url, create_time) VALUES (?, ?, ?, ?, ?, ?, ?); ps conn.prepareStatement(sql); for (RowData row : rowDataList) { String rowKey generateRowKey(row); // 先查指纹存在则跳过 if (jdbcUtil.exists(conn, SELECT 1 FROM tb_web_table WHERE row_key?, rowKey)) { continue; } ps.setString(1, rowKey); ListString cells row.getCells(); for (int i 0; i 5; i) { ps.setString(i 2, i cells.size() ? cells.get(i) : ); } ps.setString(6, row.getSourceUrl()); ps.setTimestamp(7, new Timestamp(System.currentTimeMillis())); ps.addBatch(); insertCount; if (insertCount % 500 0) { ps.executeBatch(); // 每500条刷一次控制内存占用 } } ps.executeBatch(); conn.commit(); return insertCount; } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { e.printStackTrace(); } } throw new RuntimeException(数据入库失败, e); } finally { jdbcUtil.close(conn, ps, null); } } private String generateRowKey(RowData row) { StringBuilder sb new StringBuilder(); for (String cell : row.getCells()) { sb.append(cell null ? : cell).append(|); } return DigestUtils.md5Hex(sb.toString()); } }这段代码有三个设计要点。第一批量插入而不是逐条插入。逐条插入走一次网络通信执行一次SQL500条数据要500次往返批量插入只需要1次性能差距在几十倍。第二批量大小取500而不是一次全塞进去防止数据量太大导致内存溢出。第三row_key的生成策略是对整行数据拼接后取MD5一行数据只要有一个字符不同产生的指纹就完全不同——这既保证不误判重复又能识别出页面上“内容微调”导致的重复行。3.4 定时调度与Web展示层定时任务用JDK自带的ScheduledExecutorService实现这是我认为性能最好的方案它比继承TimerTask灵活——支持线程池调度在采集大量数据时不会因为单线程阻塞而卡死后续任务public class CollectScheduler { private ScheduledExecutorService scheduler Executors.newSingleThreadScheduledExecutor(); /** * 启动定时采集任务 * param url 目标URL * param intervalMinutes 采集间隔 */ public void start(String url, int intervalMinutes) { Runnable task () - { try { TableSpider spider new TableSpider(); ListRowData dataList spider.fetchTableData(url); DataCleaner cleaner new DataCleaner(); int targetColCount 4; ListRowData cleanList dataList.stream() .map(row - cleaner.clean(row, targetColCount)) .filter(Objects::nonNull) .collect(Collectors.toList()); TableDao dao new TableDao(); int insertedCount dao.batchInsert(cleanList); logger.info(本次采集完成共{}行数据新入库{}行, cleanList.size(), insertedCount); } catch (Exception e) { logger.error(定时采集任务异常, e); } }; scheduler.scheduleAtFixedRate(task, 0, intervalMinutes, TimeUnit.MINUTES); } }调度模块的亮点是“隔离异常”——每次采集任务抛出异常都不影响下一次调度采集系统最忌讳的是某个URL解析出错就把整个定时任务终止了。这样的设计保证了系统的稳定性也方便你在答辩时展示“模块化容错”的工程思维。Web展示层则只是一个简单的查询控制层用来验证入库结果不作为核心模块展开。4. 避坑指南网页采集与表格解析最容易翻车的五个关键点4.1 乱码问题UTF-8页面显示中文全是问号现象抓下来的页面中文部分变成“”或者乱码字符解析出来的表格全是天书。原因网页声明的字符集和实际编码不一致或者Jsoup在解析时没有正确识别页面编码。解决不要依赖Jsoup.connect().get()的自动识别手动指定编码doc Jsoup.connect(url).parser(Parser.htmlParser()).get(); doc.outputSettings().charset(UTF-8);如果页面是GBK就把UTF-8换成GBK再试还可以通过httpUrlConnection.getContentType()先探测服务端返回的编码再做判断。4.2 动态页面表格内容是JS加载出来的还是抓不到现象浏览器打开网页能看到完整的表格但Java请求返回的HTML里只有脚本没有表格标签。原因目标网页通过Ajax请求后端接口动态渲染表格HTML源码中不存在实际数据节点。解决对着浏览器F12的Network面板找到XHR或Fetch请求中返回JSON或HTML的接口地址直接请求那个数据接口。地址通常是http://xxx/api/table/list的形式参数里有分页信息。这样抓下来的是结构化数据解析起来比HTML还简单。我在某图像处理Demo的开发中就遇到过这个问题抓网页表格时发现返回的HTML里只有div idtable-container/div而没有任何tr顺着XHR找到真实接口后问题立刻解决。4.3 链接失效网页结构改版导致选择器匹配为空现象之前能抓的表格今天跑起来输出0行数据。原因目标页面改版了或者类名变了table.exact-list这个选择器匹配不到了。解决采集代码里不要硬编码选择器到类名那一层在程序入口处用一个SELECTOR常量来管理// 集中管理所有可配置项改版时只动这里 public class SpiderConfig { public static final String TABLE_SELECTOR table; public static final String ROW_SELECTOR tr; public static final String CELL_SELECTOR th, td; public static final String USER_AGENT Mozilla/5.0 ...; public static final int TIMEOUT_MILLIS 10000; }那么顺理成章的进阶改造思路是把选择器挪到外部配置文件properties或YAML页面改版时只改配置、不重编译代码。这已经是商业爬虫框架的标准做法了。我一般会在答辩前主动演示一次“改选择器换抓取目标”的过程——把TABLE_SELECTOR从一个网站的表格改成另一个网站的表格重启后代码不变就抓到了新数据这个演示比任何话术都更能说明你的设计边界。4.4 无限制重试的崩溃陷阱失败请求导致线程阻塞现象某个URL请求一直超时程序卡住不动了其他页面的采集也全部停滞。原因重试机制没有限制或者超时时间设置过长线程池中的线程被占满。解决像我给的核心代码那样设置最大重试次数如3次超时时间控制在10秒以内并且重试间隔用“2秒 × 第几次”的递增策略。更稳妥的做法是把单线程调度改成多线程调度但每个线程设置Future.get(timeout)防止死等ExecutorService exec Executors.newFixedThreadPool(4); ListFutureListRowData futures new ArrayList(); for (String url : urlList) { futures.add(exec.submit(() - spider.fetchTableData(url))); } // 统一等待所有抓取任务完成避免单个失败阻塞整个流程以上逻辑与具体网站无关只针对“连接超时、连接被重置、线程调度”等Java网络编程中的通用技术环节展开不涉及对任何内容屏蔽、访问限制或绕过措施的讨论。4.5 不要过度设计别一开始就上框架现象代码写了几千行引了一堆依赖结果核心功能跑不起来甚至连编译都过不了。原因在还没有抓到一个表格数据之前就想着加代理池、加分布式调度、加重试队列。解决先把“抓表格→存MySQL”这条主链路跑通用System.out把每步的结果打出来确认无误再往外扩展。课设答辩的评审最看重的是“演示能过、代码能懂、设计有理”一个过度设计但跑不起来的项目比一个简单但跑得顺的项目更危险。5. 正则表达式 HttpClient落在表格提取上的实战5.1 什么是正则表达式跟表格提取有什么关系正则表达式Regular Expression可以理解为一种字符串匹配规则它用很少的字符表达“我想要找什么样的内容”。提取网页表格时有个痛点不是每次都有完整的HTML文档需要解析有时候你从某个接口拿回来的是一段文本里面就藏着一堆trtd.../td/tr结构这时候写一堆Jsoup选择器反而笨重。我总结出一个判断标准HTML结构越规范越适合用Jsoup做DOM解析数据在文本里、没有规范结构时正则反而是效率最高的工具。但在整个项目的一定阶段你可能会发现两者结合效率最高。我一般会这样分工——80%的表格行用Jsoup解析特殊文本片段用正则提取。5.2 正则提取表格内容的Java代码实现下面这段代码是我在一个“通用表格文本提取模块”里写的真实片段目标是从一段HTML字符串中提取所有tr行内的单元格文本import java.util.regex.Pattern; public class RegexTableExtractor { /** * 从HTML片段文本中提取表格数据 * param html 通过HttpClient或Jsoup拿到的HTML字符串 */ public void extractFromText(String html) { // 这个模式匹配所有tr.../tr块的内容 String rowPattern tr[^]*([\\s\\S]*?)/tr; Pattern pattern Pattern.compile(rowPattern, Pattern.CASE_INSENSITIVE); java.util.regex.Matcher matcher pattern.matcher(html); while (matcher.find()) { String rowBody matcher.group(1); // 提取行内所有td或th的文本去掉标签只保留内容 String cellPattern t[dh][^]*([\\s\\S]*?)/t[dh]; Pattern cellP Pattern.compile(cellPattern, Pattern.CASE_INSENSITIVE); java.util.regex.Matcher cellM cellP.matcher(rowBody); ListString cells new ArrayList(); while (cellM.find()) { String cellText cellM.group(1) .replaceAll(\\s, ) .trim(); cells.add(cellText); } System.out.println(String.join( | , cells)); } } }这段代码用了两个正则外层匹配行内层匹配单元格。[\\s\\S]*?的作用是匹配换行符在内的任意字符因为HTML源码里tr和td之间经常有换行缩进普通.不匹配换行会漏数据。replaceAll(\\s, )把单元格内的连续空白压缩成单个空格让输出更干净。正则方案的局限也很明显HTML嵌套层级深了或者属性值里含有字符时正则容易匹配出错。所以我的建议是正则方案作为Jsoup方案的补充手段存在两者都跑通之后再在答辩时做对比“为什么这个场景用正则可以用XPath不行”——这个对比是很好的加分项。5.3 正则方案适合哪类表格场景什么时候优先考虑正则而不是Jsoup数据嵌在JS变量里。比如有些页面的数据存在var data trtd北京/tdtd20/td/tr这样的JS代码中用Pattern直接从这个字符串里抠出HTML行比加载整个Document再选择要更直接。还有从JSON字段里截取HTML表格片段的情况也建议用正则先切出表格内容再做结构化解析。5.4 表格解析的更强工具XPath与基础选择器顺着选择器往下说除了正则和JsoupXPath也是表格提取的一把利器。MyEclipse自带XML解析能力我们可以在Jsoup基础上加一层XPath支持。如果目标页面表格嵌套三层以上用table tbody tr td这种CSS选择器写起来很长而XPath可以这样定位String xpath //table[contains(class,list)]//tr[position()1]/td;XPath的表达能力比CSS选择器强很多但使用门槛也更高。我给的方案里没有默认引入XPath是因为对绝大多数课设项目表格CSS选择器足够用。但如果你在答辩时加上一句“这个方案可扩展支持XPath”懂行一点的同学会觉得你考虑得很全面。6. 验收标准与答辩技巧让方案现场演示无懈可击6.1 验收维度用这五条标准给自己打分一份Web数据采集方案能不能用不能只看“今天跑通了”。我给自己定了一套验收标准每次演示前都会照表检查一遍验收维度核心标准自查方式功能完整性输入URL能输出结构化表格行且入库可查控制台打印 SQL查询展示可持续性连续运行两轮无重复入库查看 row_key 去重统计健壮性随便给一个错误URL不会崩溃主动演示错误地址的处理可维护性改目标选择器不需要改主流程代码改配置文件演示性能表现100页数据抓取时间不过长演示日志中的耗时统计每个维度都有具体的落地点不是为了打分而打分。比如“健壮性”这一项你可以在答辩现场故意输入一个404链接然后展示程序如何打印警告、跳过异常、继续跑后面的任务——这个演示比你说十句“我的程序有异常处理”都管用。6.2 答辩主线抓住一个“完整闭环”讲到底答辩的时候不用面面俱到也不用从Java语法开始讲。我会建议这样组织你的讲解主线以“我看到一个网页表格想把它自动化地存进数据库”为起点带出三层架构图——“请求层发请求拿HTML解析层用Jsoup转成结构化对象存储层用JDBC事务批量写库”。每讲一个模块都回到这次核心流程上做演示抓取时展示返回的HTML片段和解析出来的行数据清洗时展示脏数据怎么被过滤入库时展示MySQL的select * from tb_web_table where row_key ?怎么命中唯一记录。只要这条主线不断评审基本不会在细节上为难你。6.3 高频问题清单提前把话想好下面的问题是我总结的答辩高频问题提前准备好回答现场就不会卡壳问为什么不直接用Selenium自动化看起来更方便答Selenium需要浏览器驱动且运行慢对静态页面来说杀鸡用牛刀。Jsoup直接提交HTTP请求并解析HTML轻量、可控、资源占用低。本方案适用场景是HTML源码可直接获取的页面。问表格用正则也能抓为什么用Jsoup的DOM解析答正则适合处理简单文本模式但遇到嵌套表格或属性变化容易失效。Jsoup把HTML解析成DOM树语义清楚支持CSS选择器定位后期维护改一个选择器就行。问如果抓取的目标网站加了CDN防护怎么办答本方案默认目标是普通公共网页。如果遇到防护从工程上讲需要调整本文代码中的请求频率、User-Agent等参数但方案重点在于讲清楚“从HTML到结构化数据”的完整流程而不是绕过防护。问怎么证明你没瞎写数据答source_url记录了每行数据对应的原始页面地址create_time记录了抓取时间答辩时可以从数据库反查原文。6.4 一个让演示分飙升的小细节我会在开始跑抓取程序前先把数据库里原来的数据删掉或者查一次行数然后开始采集等程序跑完再查一次——两步操作前后一对比数据量净增加就直观证明了代码在干活。数据库端用两个SQL对比图展示比在IDE里翻控制台日志清楚得多。从那以后我每次做数据采集项目都强制自己走一遍这个“删之前→跑程序→查之后”的闭环验证。宁可多花两分钟做这个动作也不要上台演示的时候盯着屏幕干等。希望这套从设计到验收的方案思路能帮到你让你的Java爬取网页表格项目不只是一堆能跑的代码而是一份能讲清楚、经得起问的完整作品。本文还有配套的精品资源点击获取