You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何校验两个Excel文件内容一致性?(Apache POI测试场景)

问题描述

我用Apache POI生成Excel文件,想写测试用例验证生成的文件和另一个Excel内容是否一致。一开始试了对比文件校验和,但发现同一流程生成的文件校验和却不一样,内容明明是一致的。

把xlsx文件(本质是ZIP)解压后对比目录内容,发现docProps/core.xml里的创建日期会随测试运行变化,导致文件差异,终端操作如下:

Excel文件file1.xlsx和file2.xlsx由同一流程生成,内容完全一致。

$ unzip file1.xlsx -d /tmp/file1
Archive:  file1.xlsx
  inflating: file1/[Content_Types].xml  
  inflating: file1/_rels/.rels       
  inflating: file1/docProps/app.xml  
  inflating: file1/docProps/core.xml  
  inflating: file1/xl/sharedStrings.xml  
  inflating: file1/xl/styles.xml     
  inflating: file1/xl/workbook.xml   
  inflating: file1/xl/_rels/workbook.xml.rels  
  inflating: file1/xl/worksheets/sheet1.xml  

$ unzip file2.xlsx -d /tmp/file2
Archive:  file2.xlsx
  inflating: file2/[Content_Types].xml  
  inflating: file2/_rels/.rels       
  inflating: file2/docProps/app.xml  
  inflating: file2/docProps/core.xml  
  inflating: file2/xl/sharedStrings.xml  
  inflating: file2/xl/styles.xml     
  inflating: file2/xl/workbook.xml   
  inflating: file2/xl/_rels/workbook.xml.rels  
  inflating: file2/xl/worksheets/sheet1.xml  

# File containing Excel creation date
$ diff --brief --recursive /tmp/file1 /tmp/file2
Files /tmp/file1/docProps/core.xml and /tmp/file2/docProps/core.xml differ

$ diff <(xmllint --format /tmp/file1/docProps/core.xml) <(xmllint --format /tmp/file2/docProps/core.xml)
3c3
<   <dcterms:created xsi:type="dcterms:W3CDTF">2024-12-06T12:57:08Z</dcterms:created>
---
>   <dcterms:created xsi:type="dcterms:W3CDTF">2024-12-06T12:57:28Z</dcterms:created>

我原本考虑解压后忽略创建日期文件再对比,但这种方法不够健壮,而且xls文件不是ZIP格式没法用,想问有没有更可靠的校验方案?


可靠的校验方案

1. 使用Apache POI API直接读取并对比内容

这是最通用的方案,同时支持xls和xlsx格式,直接跳过元数据差异,只校验业务相关内容:

  • 分别用POI打开两个Excel文件,获取Workbook对象
  • 对比工作表数量、名称是否一致
  • 遍历每个工作表的行和单元格,校验单元格的类型、值(包括公式计算结果)、格式(按需校验)
  • 可选校验工作簿样式、共享字符串表等核心配置

示例伪代码:

// 打开两个工作簿
Workbook wb1 = WorkbookFactory.create(new File("file1.xlsx"));
Workbook wb2 = WorkbookFactory.create(new File("file2.xlsx"));

// 校验工作表数量
if (wb1.getNumberOfSheets() != wb2.getNumberOfSheets()) {
    return false;
}

// 遍历每个工作表
for (int i = 0; i < wb1.getNumberOfSheets(); i++) {
    Sheet sheet1 = wb1.getSheetAt(i);
    Sheet sheet2 = wb2.getSheetAt(i);

    // 校验工作表名称
    if (!sheet1.getSheetName().equals(sheet2.getSheetName())) {
        return false;
    }

    // 遍历行(处理空行情况)
    Iterator<Row> rowIter1 = sheet1.iterator();
    Iterator<Row> rowIter2 = sheet2.iterator();
    while (rowIter1.hasNext() || rowIter2.hasNext()) {
        Row row1 = rowIter1.hasNext() ? rowIter1.next() : null;
        Row row2 = rowIter2.hasNext() ? rowIter2.next() : null;

        if ((row1 == null && row2 != null) || (row1 != null && row2 == null)) {
            return false;
        }
        if (row1 != null && row1.getRowNum() != row2.getRowNum()) {
            return false;
        }

        // 遍历单元格
        Iterator<Cell> cellIter1 = row1 != null ? row1.iterator() : Collections.emptyIterator();
        Iterator<Cell> cellIter2 = row2 != null ? row2.iterator() : Collections.emptyIterator();
        while (cellIter1.hasNext() || cellIter2.hasNext()) {
            Cell cell1 = cellIter1.hasNext() ? cellIter1.next() : null;
            Cell cell2 = cellIter2.hasNext() ? cellIter2.next() : null;

            if ((cell1 == null && cell2 != null) || (cell1 != null && cell2 == null)) {
                return false;
            }
            if (cell1 != null) {
                // 校验单元格类型
                if (cell1.getCellType() != cell2.getCellType()) {
                    return false;
                }
                // 根据类型校验值
                switch (cell1.getCellType()) {
                    case STRING:
                        if (!cell1.getStringCellValue().equals(cell2.getStringCellValue())) {
                            return false;
                        }
                        break;
                    case NUMERIC:
                        if (Math.abs(cell1.getNumericCellValue() - cell2.getNumericCellValue()) > 1e-6) {
                            return false;
                        }
                        break;
                    case BOOLEAN:
                        if (cell1.getBooleanCellValue() != cell2.getBooleanCellValue()) {
                            return false;
                        }
                        break;
                    case FORMULA:
                        // 可选校验公式本身或计算结果
                        if (!cell1.getCellFormula().equals(cell2.getCellFormula())) {
                            return false;
                        }
                        // 若需校验计算结果,先触发求值
                        wb1.getCreationHelper().createFormulaEvaluator().evaluate(cell1);
                        wb2.getCreationHelper().createFormulaEvaluator().evaluate(cell2);
                        if (cell1.getCachedFormulaResultType() != cell2.getCachedFormulaResultType()) {
                            return false;
                        }
                        break;
                    // 按需处理其他单元格类型
                }
            }
        }
    }
}

// 关闭资源
wb1.close();
wb2.close();

2. 标准化Excel文件后对比校验和

如果不想编写大量对比逻辑,可以先消除元数据差异再对比校验和:

  • 用POI读取原文件,手动设置固定的创建/修改日期(或清空可变元数据)
  • 将标准化后的文件写入临时文件,计算两个临时文件的校验和并对比

示例伪代码:

// 标准化第一个文件
Workbook wb1 = WorkbookFactory.create(new File("file1.xlsx"));
// 设置固定创建日期,消除差异
wb1.getCreationHelper().createProperties().setCreated(new Date(0)); // 固定为1970-01-01
File tempFile1 = File.createTempFile("standardized", ".xlsx");
try (FileOutputStream fos = new FileOutputStream(tempFile1)) {
    wb1.write(fos);
}
wb1.close();

// 对第二个文件执行同样操作...

// 计算校验和并对比
String checksum1 = calculateChecksum(tempFile1);
String checksum2 = calculateChecksum(tempFile2);
boolean isEqual = checksum1.equals(checksum2);

// 清理临时文件
tempFile1.delete();
tempFile2.delete();

3. 针对xlsx的轻量方案(仅适用于OOXML格式)

如果只需要处理xlsx文件,可以解压时排除可变元数据文件再对比:

  • 解压时跳过docProps/core.xml、docProps/app.xml这类包含日期的文件
  • 递归对比剩余核心内容(xl/目录下的工作表、样式等)

终端命令示例:

# 解压时排除可变元数据
unzip file1.xlsx -d /tmp/file1 -x docProps/core.xml docProps/app.xml
unzip file2.xlsx -d /tmp/file2 -x docProps/core.xml docProps/app.xml

# 对比剩余内容
diff --brief --recursive /tmp/file1 /tmp/file2

注意:此方法不适用于xls格式,优先推荐前两种方案。


内容的提问来源于stack exchange,提问作者ka3ak

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 19:41:06