[关闭]
@qiao39gs 2025-05-29T06:58:24.000000Z 字数 8453 阅读 2

Okay, 对于“最快”的方法,我们通常会选择 Apache POI,因为它在读取 Excel 元数据(如行数)方面非常高效,特别是当你只需要行数而不需要读取单元格内容时。它通常直接从文件结构中读取这个信息,而不是逐行扫描。

另一个以性能著称的库是 EasyExcel (Alibaba),它在处理大型 Excel 文件时内存占用低且速度快,因为它基于 SAX 解析。但对于仅仅获取行数这个特定任务,Apache POI 的 getLastRowNum() 方法通常已经足够快,因为它不涉及加载整个工作表的数据到内存中。

结论先行:Apache POI 的 Sheet.getLastRowNum() 是获取“有数据或格式的最后一行”的索引的最常用且通常最快的方法。实际行数是 getLastRowNum() + 1(因为它是0基索引)。如果一个工作表完全为空,getLastRowNum() 可能会返回0或-1(取决于POI版本和具体情况),而 getPhysicalNumberOfRows() 会返回0。

下面是使用 Apache POI 的示例:

1. 添加 Maven 依赖 (pom.xml):

  1. <dependencies>
  2. <!-- Apache POI Core -->
  3. <dependency>
  4. <groupId>org.apache.poi</groupId>
  5. <artifactId>poi</artifactId>
  6. <version>5.2.5</version> <!-- 使用最新稳定版 -->
  7. </dependency>
  8. <!-- Apache POI OOXML (for .xlsx files) -->
  9. <dependency>
  10. <groupId>org.apache.poi</groupId>
  11. <artifactId>poi-ooxml</artifactId>
  12. <version>5.2.5</version> <!-- 使用最新稳定版 -->
  13. </dependency>
  14. </dependencies>

2. Java 代码:

  1. import org.apache.poi.ss.usermodel.*;
  2. import org.apache.poi.xssf.usermodel.XSSFWorkbook; // For .xlsx
  3. import org.apache.poi.hssf.usermodel.HSSFWorkbook; // For .xls
  4. import java.io.File;
  5. import java.io.FileInputStream;
  6. import java.io.IOException;
  7. import java.io.InputStream;
  8. public class ExcelRowCountReader {
  9. /**
  10. * 获取指定 Excel 文件中指定 sheet 页的行数。
  11. * 这是最常用的方法,返回的是“最后有效行”的行号(0基)+ 1。
  12. *
  13. * @param filePath Excel 文件路径
  14. * @param sheetName Sheet 页名称
  15. * @return 行数;如果 sheet 不存在或文件有问题,则返回 -1
  16. */
  17. public static int getRowCountBySheetName(String filePath, String sheetName) {
  18. try (InputStream inp = new FileInputStream(filePath);
  19. Workbook workbook = WorkbookFactory.create(inp)) { // WorkbookFactory 能自动识别 .xls 和 .xlsx
  20. Sheet sheet = workbook.getSheet(sheetName);
  21. if (sheet == null) {
  22. System.err.println("Sheet '" + sheetName + "' not found in " + filePath);
  23. return -1; // Sheet 不存在
  24. }
  25. // getLastRowNum() 返回最后一行的索引 (0-based)。
  26. // 如果工作表为空,或者只有第一行(索引为0)有数据,它可能会返回0。
  27. // 如果工作表完全为空且从未有过数据,HSSFSheet 可能返回 -1,XSSFSheet 返回 0。
  28. // getPhysicalNumberOfRows() 返回物理上存在的行数(即XML中定义的<row>标签数量),
  29. // 即使这些行是空的。如果一个sheet完全是空的(从未有过任何操作),它返回0。
  30. int lastRowNum = sheet.getLastRowNum();
  31. int physicalRows = sheet.getPhysicalNumberOfRows();
  32. // System.out.println("Sheet: " + sheetName + ", LastRowNum: " + lastRowNum + ", PhysicalNumberOfRows: " + physicalRows);
  33. if (physicalRows == 0) { // 如果物理行数为0,说明表是完全空的
  34. return 0;
  35. }
  36. // 否则,使用getLastRowNum() + 1 作为行数。
  37. // 对于只有一个空行或者第一行有数据的情况,lastRowNum是0,所以行数是1。
  38. return lastRowNum + 1;
  39. } catch (IOException e) {
  40. System.err.println("Error reading Excel file: " + e.getMessage());
  41. e.printStackTrace();
  42. return -1;
  43. } catch (Exception e) { // WorkbookFactory.create 可能会抛出其他异常,如 EncryptedDocumentException
  44. System.err.println("Error processing Excel file: " + e.getMessage());
  45. e.printStackTrace();
  46. return -1;
  47. }
  48. }
  49. /**
  50. * 获取指定 Excel 文件中指定索引的 sheet 页的行数。
  51. *
  52. * @param filePath Excel 文件路径
  53. * @param sheetIndex Sheet 页索引 (0-based)
  54. * @return 行数;如果 sheet 不存在或文件有问题,则返回 -1
  55. */
  56. public static int getRowCountBySheetIndex(String filePath, int sheetIndex) {
  57. try (InputStream inp = new FileInputStream(filePath);
  58. Workbook workbook = WorkbookFactory.create(inp)) {
  59. if (sheetIndex < 0 || sheetIndex >= workbook.getNumberOfSheets()) {
  60. System.err.println("Sheet index " + sheetIndex + " is out of bounds.");
  61. return -1;
  62. }
  63. Sheet sheet = workbook.getSheetAt(sheetIndex);
  64. if (sheet == null) { // 理论上,如果索引有效,这里不应该为 null
  65. System.err.println("Sheet at index " + sheetIndex + " not found (should not happen if index is valid).");
  66. return -1;
  67. }
  68. int lastRowNum = sheet.getLastRowNum();
  69. int physicalRows = sheet.getPhysicalNumberOfRows();
  70. // System.out.println("Sheet Index: " + sheetIndex + ", LastRowNum: " + lastRowNum + ", PhysicalNumberOfRows: " + physicalRows);
  71. if (physicalRows == 0) {
  72. return 0;
  73. }
  74. return lastRowNum + 1;
  75. } catch (IOException e) {
  76. System.err.println("Error reading Excel file: " + e.getMessage());
  77. e.printStackTrace();
  78. return -1;
  79. } catch (Exception e) {
  80. System.err.println("Error processing Excel file: " + e.getMessage());
  81. e.printStackTrace();
  82. return -1;
  83. }
  84. }
  85. public static void main(String[] args) {
  86. // 创建一个临时的测试Excel文件 (你可以替换成你自己的文件路径)
  87. String testFilePathXlsx = "test_workbook.xlsx";
  88. String testFilePathXls = "test_workbook.xls";
  89. // 创建 XLSX 文件
  90. try (Workbook workbook = new XSSFWorkbook()) {
  91. Sheet sheet1 = workbook.createSheet("DataSheet1");
  92. sheet1.createRow(0).createCell(0).setCellValue("Header1");
  93. sheet1.createRow(1).createCell(0).setCellValue("Data1");
  94. sheet1.createRow(4).createCell(0).setCellValue("Data_in_row_5"); // 第5行 (索引4)
  95. Sheet sheet2 = workbook.createSheet("EmptySheet"); // 完全空的
  96. Sheet sheet3 = workbook.createSheet("SingleRowSheet");
  97. sheet3.createRow(0).createCell(0).setCellValue("OnlyRow");
  98. try (java.io.FileOutputStream fos = new java.io.FileOutputStream(testFilePathXlsx)) {
  99. workbook.write(fos);
  100. }
  101. } catch (IOException e) {
  102. e.printStackTrace();
  103. }
  104. // 创建 XLS 文件 (HSSFWorkbook)
  105. try (Workbook workbook = new HSSFWorkbook()) {
  106. Sheet sheet1 = workbook.createSheet("DataSheet1_xls");
  107. sheet1.createRow(0).createCell(0).setCellValue("Header1_xls");
  108. sheet1.createRow(1).createCell(0).setCellValue("Data1_xls");
  109. sheet1.createRow(9).createCell(0).setCellValue("Data_in_row_10_xls"); // 第10行 (索引9)
  110. try (java.io.FileOutputStream fos = new java.io.FileOutputStream(testFilePathXls)) {
  111. workbook.write(fos);
  112. }
  113. } catch (IOException e) {
  114. e.printStackTrace();
  115. }
  116. System.out.println("--- Testing XLSX (" + testFilePathXlsx + ") ---");
  117. int rowCount1 = getRowCountBySheetName(testFilePathXlsx, "DataSheet1");
  118. System.out.println("Rows in DataSheet1 (by name): " + rowCount1); // 应该输出 5
  119. int rowCount2 = getRowCountBySheetName(testFilePathXlsx, "EmptySheet");
  120. System.out.println("Rows in EmptySheet (by name): " + rowCount2); // 应该输出 0
  121. int rowCount3 = getRowCountBySheetName(testFilePathXlsx, "NonExistentSheet");
  122. System.out.println("Rows in NonExistentSheet (by name): " + rowCount3); // 应该输出 -1 (并打印错误)
  123. int rowCount4 = getRowCountBySheetIndex(testFilePathXlsx, 0);
  124. System.out.println("Rows in Sheet at index 0 (by index): " + rowCount4); // 应该输出 5 (DataSheet1)
  125. int rowCount5 = getRowCountBySheetIndex(testFilePathXlsx, 2);
  126. System.out.println("Rows in Sheet at index 2 (by index): " + rowCount5); // 应该输出 1 (SingleRowSheet)
  127. System.out.println("\n--- Testing XLS (" + testFilePathXls + ") ---");
  128. int rowCountXls = getRowCountBySheetName(testFilePathXls, "DataSheet1_xls");
  129. System.out.println("Rows in DataSheet1_xls (by name): " + rowCountXls); // 应该输出 10
  130. // 清理测试文件
  131. // new File(testFilePathXlsx).delete();
  132. // new File(testFilePathXls).delete();
  133. }
  134. }

解释和为什么这通常是“最快”的:

  1. WorkbookFactory.create(inp): 这是一个方便的方法,它可以自动检测文件是 .xls (HSSF) 还是 .xlsx (XSSF) 格式,并创建相应的 Workbook 对象。
  2. workbook.getSheet(sheetName)workbook.getSheetAt(sheetIndex): 获取对特定工作表的引用。这个操作本身很快。
  3. sheet.getLastRowNum():
    • 这是关键。此方法返回工作表中最后一行的索引(0基)。例如,如果最后一行是第5行,它将返回4。
    • 性能: Apache POI 在实现此方法时,通常会读取工作表元数据或文件结构中关于维度(dimension)的信息,而不是实际迭代每一行或加载所有单元格数据。
      • 对于 .xlsx (XSSF),它通常解析 xl/worksheets/sheetN.xml 文件中的 <dimension ref="A1:C10"/> 这样的标签,直接获取范围。
      • 对于 .xls (HSSF),它会查找内部记录来确定最后一行的位置。
    • 这些操作远快于加载并迭代所有行和单元格。
  4. sheet.getPhysicalNumberOfRows():
    • 这个方法返回在工作表文件中实际定义的 <row> 元素的数量。一个行可以被定义但完全是空的。
    • 如果一个工作表被创建但从未添加任何行(或者所有行都被删除了且文件中没有留下空的<row>标签),这个方法会返回0。
  5. 行数的计算:
    • 如果 getPhysicalNumberOfRows() 返回 0,那么工作表肯定是空的,行数为 0
    • 否则,行数通常是 getLastRowNum() + 1
      • 如果 getLastRowNum() 返回 0(意味着最后有数据的行是第一行,索引为0),那么行数是 0 + 1 = 1
      • 如果一个工作表在 HSSF 格式下完全空,getLastRowNum() 可能返回 -1,此时 -1 + 1 = 0,这也是正确的。较新版本的 POI 对于 XSSF 空表返回 0
      • 所以,if (physicalRows == 0) return 0; else return lastRowNum + 1; 是一个比较稳妥的逻辑。

关于EasyExcel:
EasyExcel 在读取大量数据时非常快且内存友好,因为它使用 SAX 解析器逐步读取。但如果你仅仅需要行数,它也需要解析XML结构来找到行。

  1. // 仅作概念演示,获取行数通常不这么用EasyExcel
  2. import com.alibaba.excel.EasyExcel;
  3. import com.alibaba.excel.context.AnalysisContext;
  4. import com.alibaba.excel.read.listener.ReadListener;
  5. import com.alibaba.excel.metadata.CellData; // 如果需要用CellData
  6. import com.alibaba.excel.metadata.data.ReadCellData; // EasyExcel 3.x
  7. import java.util.Map;
  8. import java.util.concurrent.atomic.AtomicInteger;
  9. // ... (假设你已添加 EasyExcel 依赖)
  10. public class EasyExcelRowCount {
  11. public static int getRowCountWithEasyExcel(String filePath, String sheetName) {
  12. AtomicInteger maxRowIndex = new AtomicInteger(-1); // 用 AtomicInteger 确保线程安全(虽然这里单线程)
  13. try {
  14. EasyExcel.read(filePath, new ReadListener<Map<Integer, String>>() {
  15. @Override
  16. public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
  17. // EasyExcel 3.x 使用 ReadCellData
  18. // 如果有表头,当前行索引是0
  19. if (!headMap.isEmpty()) {
  20. maxRowIndex.set(Math.max(maxRowIndex.get(), context.readRowHolder().getRowIndex()));
  21. }
  22. }
  23. @Override
  24. public void invoke(Map<Integer, String> data, AnalysisContext context) {
  25. // 任何数据行都会调用这里
  26. maxRowIndex.set(Math.max(maxRowIndex.get(), context.readRowHolder().getRowIndex()));
  27. }
  28. @Override
  29. public void doAfterAllAnalysed(AnalysisContext context) {
  30. // 所有行读取完毕
  31. }
  32. }).sheet(sheetName).doRead(); // 或者 .sheet(sheetIndex)
  33. // maxRowIndex 现在是0基的最大行索引
  34. if (maxRowIndex.get() == -1) { // 如果没有调用过 invoke 或 invokeHead
  35. // 尝试获取近似总行数 (但这可能不精确,或在某些情况下不可用)
  36. // Integer approximateTotalRowNumber = context.readSheetHolder().getApproximateTotalRowNumber();
  37. // 对于精确计数,还是依赖 invoke/invokeHead 更好
  38. return 0;
  39. }
  40. return maxRowIndex.get() + 1;
  41. } catch (Exception e) {
  42. e.printStackTrace();
  43. return -1;
  44. }
  45. }
  46. // main 方法中调用 EasyExcelRowCount.getRowCountWithEasyExcel(...)
  47. }

对于仅仅获取行数,EasyExcel 的方式会显得有点“重”,因为它设计的目标是流式处理数据。AnalysisContext 中的 getApproximateTotalRowNumber() 可能有用,但它不总是精确的。所以,为了精确行数,你还是得让它至少把行结构读一遍。

总结:
对于“获取指定sheet页的行数”这个特定且简单的任务,Apache POI 的 sheet.getLastRowNum() 加上适当的逻辑判断,通常是最直接且性能足够高的方法。它不需要完整的数据解析,而是利用了文件结构中的元信息。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注