
本文针对使用 selenium + java 爬取网页表格并逐单元格写入 excel 时出现严重性能下降的问题,揭示根本原因(高频文件 i/o),并提供基于内存批量写入的完整优化方案。
本文针对使用 selenium + java 爬取网页表格并逐单元格写入 excel 时出现严重性能下降的问题,揭示根本原因(高频文件 i/o),并提供基于内存批量写入的完整优化方案。
在实际 Web 数据采集项目中,开发者常误将“功能实现”等同于“性能可用”。您提供的 CovidWebTable 示例代码虽能成功抓取 Worldometers 的疫情表格并写入 Excel,但其执行速度随行数增长而急剧恶化——这并非 Selenium 本身慢,而是Excel I/O 策略设计缺陷所致。
? 根本问题:每次写入都触发完整文件读-写-关闭循环
观察 XLUtils.setCellData(...) 方法:
- 每次调用都会 new FileInputStream(xlFile) 重新加载整个 .xls 文件;
- 创建 HSSFWorkbook 解析二进制结构;
- 定位 Sheet/Row/Cell 并写入;
- 最后 wb.write(fo) 全量覆写文件并关闭流。
这意味着:若表格含 231 行 × 15 列 = 3465 个单元格,您的程序将重复打开/解析/写入/关闭 Excel 文件 3465 次。磁盘 I/O 成为绝对瓶颈,且随着写入次数增加,文件系统缓存失效、JVM GC 压力上升,导致“越往后越慢”的典型现象。
✅ 正确解法:内存聚合 + 单次持久化
核心思想:将所有待写数据暂存于内存(如 List>),待全部采集完成后再一次性写入 Excel
。这相当于用一辆大货车(单次 I/O)替代数千次快递小哥(逐单元格 I/O)。
✅ 优化后的主流程(关键修改部分)
// 1. 内存中构建二维数据结构(替代逐单元格写入)
List<List<String>> excelData = new ArrayList<>();
// 写入表头(跳过第0列,按原逻辑)
List<String> headerRow = new ArrayList<>();
for (int col = 1; col < header.size() - 1; col++) {
headerRow.add(header.get(col).getText());
}
excelData.add(headerRow);
// 2. 遍历表格行,逐行提取并添加到内存列表
int xlRow = 1;
for (int r = 1; r < rows.size(); r++) {
WebElement rowElement = rows.get(r);
if (rowElement.getText().trim().isEmpty()) {
System.out.println("Skipped empty row: " + r);
continue;
}
System.out.println("Processing row " + r);
List<String> rowData = new ArrayList<>();
for (int c = 1; c < header.size(); c++) {
try {
// 使用更稳定的 CSS 选择器替代复杂 XPath
String cellText = rowElement.findElement(By.cssSelector("td:nth-of-type(" + (c + 1) + ")")).getText().trim();
rowData.add(cellText);
} catch (Exception e) {
rowData.add(""); // 容错:缺失单元格填空字符串
}
}
excelData.add(rowData);
}
// 3. 【关键】仅调用一次批量写入方法
xl.writeAllData("Covid Data", excelData); // 新增方法,见下方
System.out.println("✅ Export completed. Total rows written: " + excelData.size());✅ 在 XLUtils 中新增高效批量写入方法
// 新增方法:接收完整二维数据,单次 I/O 写入
public static void writeAllData(String xlFile, String xlSheet, List<List<String>> data) throws IOException {
FileInputStream fi = new FileInputStream(xlFile);
HSSFWorkbook wb = new HSSFWorkbook(fi);
HSSFSheet ws = wb.getSheet(xlSheet);
// 清空旧数据(可选,确保干净写入)
ws.removeAllCells();
// 逐行写入内存数据
for (int rowNum = 0; rowNum < data.size(); rowNum++) {
HSSFRow row = ws.createRow(rowNum);
List<String> rowData = data.get(rowNum);
for (int colNum = 0; colNum < rowData.size(); colNum++) {
HSSFCell cell = row.createCell(colNum);
cell.setCellValue(rowData.get(colNum));
}
}
// 【唯一一次】写入磁盘并关闭
FileOutputStream fo = new FileOutputStream(xlFile);
wb.write(fo);
fo.close();
wb.close();
fi.close();
}⚠️ 关键注意事项与增强建议
- 避免隐式等待滥用:implicitlyWait 在查找动态表格时易引发不可预测延迟。推荐改用显式等待(WebDriverWait)+ ExpectedConditions.presenceOfAllElementsLocatedBy 精准等待表格加载完成。
- XPath 性能陷阱:原代码中 rows.get(r).findElement(By.xpath(".//td[...")) 在每行内重复解析 XPath。改用 By.cssSelector("td:nth-of-type(n)") 更轻量且稳定。
- 异常健壮性:网络波动或 DOM 变化可能导致 findElement 抛异常。务必包裹 try-catch 并提供默认值(如空字符串),防止中断整个流程。
- 内存安全提醒:对于超大表格(>10 万行),需评估 JVM 堆内存。此时应考虑分块写入(如每 5000 行 flush 一次)或切换至流式 API(Apache POI SXSSFWorkbook)。
- 现代格式兼容:.xls(HSSF)已属遗留格式。建议升级至 .xlsx(XSSF/SXSSF),支持更大行列数及更好性能。
✅ 性能提升效果预估
| 操作方式 | 文件 I/O 次数 | 预估耗时(231×15 表) | CPU/IO 负载 |
|---|---|---|---|
| 原代码(逐单元格) | ~3465 次 | 8–15 分钟(机械硬盘) | 极高 |
| 优化后(批量写入) | 1 次 | 极低 |
? 总结:Selenium 爬虫的性能瓶颈 rarely 在浏览器自动化层,而常源于下游数据处理的反模式。牢记——I/O 是最昂贵的操作,永远优先聚合、延迟执行、批量处理。将数据从“边爬边存”重构为“先存后写”,是提升采集效率最立竿见影的工程实践。











