杰瑞科技汇

Java POI如何高效解析Excel?

  1. 环境准备:如何添加 POI 依赖。
  2. 核心概念Workbook, Sheet, Row, Cell 的关系。
  3. 完整代码示例:一个可运行的 Java 程序,读取 .xlsx 文件内容并打印。
  4. 常见操作:如何获取不同类型的单元格数据(数字、字符串、日期等)。
  5. 处理 .xls 文件:与 .xlsx 的区别。
  6. 注意事项:内存优化、公式处理等。

环境准备 (Maven)

如果你使用 Maven,在 pom.xml 文件中添加以下依赖,POI 的模块化设计使得你可以只引入需要的部分,以减小项目体积。

Java POI如何高效解析Excel?-图1
(图片来源网络,侵删)

对于较新的 .xlsx (OOXML) 格式,你需要 poi-ooxml 依赖:

<dependencies>
    <!-- POI 核心库 -->
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi</artifactId>
        <version>5.2.5</version> <!-- 建议使用较新版本 -->
    </dependency>
    <!-- 用于处理 .xlsx (OOXML) 格式 -->
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml</artifactId>
        <version>5.2.5</version>
    </dependency>
    <!-- POI 依赖的 XML 处理库 -->
    <dependency>
        <groupId>org.apache.xmlbeans</groupId>
        <artifactId>xmlbeans</artifactId>
        <version>5.1.1</version>
    </dependency>
    <!-- 用于处理 OOXML 包中的条目 -->
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-scratchpad</artifactId>
        <version>5.2.5</version>
    </dependency>
    <!-- POI 依赖的 ZIP 压缩库 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-compress</artifactId>
        <version>1.26.1</version>
    </dependency>
</dependencies>

注意poi-ooxml 会自动引入 poi 和其他必需的依赖,所以如果你只处理 .xlsx 文件,理论上只加 poi-ooxml 即可,但为了清晰和兼容性,通常会把核心库也加上。


核心概念

理解 POI 的对象模型是解析 Excel 的关键,它们之间是典型的树形结构关系:

  • Workbook (工作簿):代表整个 Excel 文件,一个 Excel 文件就是一个 Workbook 对象。
    • 对于 .xls 文件,使用 HSSFWorkbook
    • 对于 .xlsx 文件,使用 XSSFWorkbook
  • Sheet (工作表):代表 Workbook 中的一个工作表,"Sheet1", "Sheet2"。
  • Row (行):代表 Sheet 中的一行,行号从 0 开始。
  • Cell (单元格):代表 Row 中的一个单元格,列号也从 0 开始。

关系图Workbook -> Sheet -> Row -> Cell

Java POI如何高效解析Excel?-图2
(图片来源网络,侵删)

完整代码示例 (解析 .xlsx 文件)

假设我们有一个名为 data.xlsx 的文件,内容如下:

姓名 年龄 入职日期 薪资
张三 28 2025-01-15 50
李四 32 2025-05-20 00
王五 25 2025-03-10 00

下面是一个完整的 Java 程序来读取这个文件:

import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.FileInputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
public class ExcelReader {
    public static void main(String[] args) {
        // 1. 定义要读取的文件路径
        String excelFilePath = "data.xlsx";
        try (// 2. 使用 try-with-resources 自动关闭流
             FileInputStream fis = new FileInputStream(excelFilePath);
             // 3. 根据 .xlsx 文件创建 XSSFWorkbook 对象
             Workbook workbook = new XSSFWorkbook(fis)
        ) {
            // 4. 获取第一个工作表 (Sheet)
            Sheet sheet = workbook.getSheetAt(0);
            // 5. 遍历工作表中的每一行 (从第一行开始,跳过表头)
            // sheet.iterator() 会获取所有行,包括空行
            // sheet.lastRowNum() 获取最后一行的索引
            for (int rowNum = 1; rowNum <= sheet.getLastRowNum(); rowNum++) {
                Row row = sheet.getRow(rowNum);
                if (row == null) {
                    continue; // 如果某一行是空的,则跳过
                }
                // 6. 获取单元格数据
                // getCell(0) 获取第一列,注意列号从0开始
                Cell nameCell = row.getCell(0);
                Cell ageCell = row.getCell(1);
                Cell dateCell = row.getCell(2);
                Cell salaryCell = row.getCell(3);
                // 7. 处理不同类型的单元格数据 (非常重要!)
                String name = getCellValueAsString(nameCell);
                int age = (int) getCellValueAsNumeric(ageCell);
                String joinDate = getCellValueAsString(dateCell);
                double salary = getCellValueAsNumeric(salaryCell);
                // 8. 打印读取到的数据
                System.out.println("姓名: " + name + ", 年龄: " + age + ", 入职日期: " + joinDate + ", 薪资: " + salary);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /**
     * 将单元格内容转换为字符串
     * @param cell 单元格对象
     * @return 单元格内容的字符串表示
     */
    private static String getCellValueAsString(Cell cell) {
        if (cell == null) {
            return "";
        }
        switch (cell.getCellType()) {
            case STRING:
                return cell.getStringCellValue().trim();
            case NUMERIC:
                // 处理日期类型的数字
                if (DateUtil.isCellDateFormatted(cell)) {
                    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
                    return sdf.format(cell.getDateCellValue());
                } else {
                    // 普通数字,防止出现科学计数法
                    return String.valueOf(cell.getNumericCellValue());
                }
            case BOOLEAN:
                return String.valueOf(cell.getBooleanCellValue());
            case FORMULA:
                // 如果单元格包含公式,则计算公式的值
                return cell.getCellFormula();
            case BLANK:
                return "";
            default:
                return "";
        }
    }
    /**
     * 将单元格内容转换为数字
     * @param cell 单元格对象
     * @return 单元格内容的 double 值
     */
    private static double getCellValueAsNumeric(Cell cell) {
        if (cell == null) {
            return 0.0;
        }
        switch (cell.getCellType()) {
            case NUMERIC:
                return cell.getNumericCellValue();
            case STRING:
                try {
                    // 尝试将字符串形式的数字转为 double
                    return Double.parseDouble(cell.getStringCellValue().trim());
                } catch (NumberFormatException e) {
                    return 0.0;
                }
            case BOOLEAN:
                return cell.getBooleanCellValue() ? 1.0 : 0.0;
            case FORMULA:
                return cell.getNumericCellValue();
            case BLANK:
                return 0.0;
            default:
                return 0.0;
        }
    }
}

代码解析

  1. try-with-resourcesFileInputStreamWorkbook 都实现了 AutoCloseable 接口,使用 try-with-resources 可以确保它们在使用后被自动关闭,避免资源泄漏。
  2. new XSSFWorkbook(fis):通过文件输入流创建一个 XSSFWorkbook 对象,这个对象代表了整个 data.xlsx 文件。
  3. workbook.getSheetAt(0):获取文件中的第一个工作表,索引从 0 开始。
  4. sheet.getLastRowNum():获取最后一行的索引,我们使用这个来确定循环的次数。
  5. row.getCell(0):获取指定行、指定列的单元格,列索引从 0 开始。
  6. getCellValueAsString() 方法:这是最关键的部分,直接使用 cell.getStringCellValue()cell.getNumericCellValue() 是不安全的,因为单元格的类型可能和你预期的不一样。
    • cell.getCellType():返回单元格的类型,如 CellType.STRING, CellType.NUMERIC, CellType.BOOLEAN 等。
    • DateUtil.isCellDateFormatted(cell):一个非常有用的工具方法,用于判断一个数字类型的单元格是否实际上是日期格式。
    • case FORMULA:如果单元格包含公式(如 =SUM(A1:A3)),cell.getCellType() 会返回 FORMULA,你可以调用 cell.getCellFormula() 获取公式字符串,或者调用 cell.getNumericCellValue() / cell.getStringCellValue() 来获取公式计算后的结果。

处理 .xls 文件

处理旧版的 .xls 文件与处理 .xlsx 文件几乎完全相同,唯一区别在于创建 Workbook 的方式。

Java POI如何高效解析Excel?-图3
(图片来源网络,侵删)

将上面代码中的:

Workbook workbook = new XSSFWorkbook(fis);

替换为:

Workbook workbook = new HSSFWorkbook(fis);

其他所有逻辑(遍历 Sheet, Row, Cell 以及获取单元格内容)都完全通用。


注意事项与最佳实践

a. 内存优化 (SAX 模式)

对于非常大的 Excel 文件(超过 100MB),使用 XSSFWorkbookHSSFWorkbook 会将整个文件加载到内存中,可能导致 OutOfMemoryError

POI 提供了 SAX (Event Model) 模式来解决这个问题,它不会一次性加载整个文件,而是像解析 XML 一样,事件驱动地逐行读取。

使用 XSSFSAX 模式 (op sax)

import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.xssf.eventusermodel.XSSFReader;
import org.apache.poi.xssf.eventusermodel.XSSFReader.SheetIterator;
import org.apache.poi.xssf.model.SharedStringsTable;
import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;
// ... 其他 import
public class LargeExcelReader {
    public static void main(String[] args) throws Exception {
        String excelFilePath = "very_large_file.xlsx";
        try (OPCPackage pkg = OPCPackage.open(excelFilePath)) {
            XSSFReader reader = new XSSFReader(pkg);
            SharedStringsTable sst = reader.getSharedStringsTable();
            XMLReader parser = XMLReaderFactory.createXMLReader();
            ContentHandler handler = new SheetHandler(sst); // 需要自定义一个 ContentHandler
            parser.setContentHandler(handler);
            SheetIterator sheets = (SheetIterator) reader.getSheetsData();
            while (sheets.hasNext()) {
                System.out.println("Processing sheet: " + sheets.getSheetName());
                InputSource source = new InputSource(sheets.next());
                parser.parse(source);
            }
        }
    }
}
// 你需要自己实现一个继承自 DefaultHandler 的 ContentHandler
// 来处理具体的 XML 事件 (startElement, characters, endElement 等)
// 这个过程比较复杂,但对于大文件是必须的。
class SheetHandler extends org.xml.sax.helpers.DefaultHandler {
    // ... 实现细节略 ...
}

SAX 模式更复杂,但内存占用非常低,如果你的文件不大,建议还是使用简单易用的 XSSFWorkbook

b. 处理公式单元格

如前所述,当 cell.getCellType() 返回 CellType.FORMULA 时,单元格的值是其公式的计算结果,你可以直接调用 getNumericCellValue()getStringCellValue() 来获取结果,POI 会帮你计算。

c. 空单元格和合并单元格

  • 空单元格sheet.getRow(i).getCell(j) 可能会返回 null,表示该单元格不存在或为空,在访问前务必检查。
  • 合并单元格:POI 会将合并区域左上角的单元格作为“主单元格”,其他被合并的单元格在读取时内容为空或为 null,如果你需要知道哪些单元格被合并了,可以使用 sheet.getMergedRegions() 方法获取所有合并区域。
// 获取所有合并区域
List<CellRangeAddress> mergedRegions = sheet.getMergedRegions();
for (CellRangeAddress region : mergedRegions) {
    System.out.println("合并区域: " + region.formatAsString());
    // region.getFirstRow(), region.getLastRow() 等
}
任务 .xls (HSSF) .xlsx (XSSF)
创建 Workbook new HSSFWorkbook(fis) new XSSFWorkbook(fis)
获取单元格类型 cell.getCellType() cell.getCellType()
判断是否为日期 HSSFDateUtil.isCellDateFormatted(cell) DateUtil.isCellDateFormatted(cell)
处理大文件 HSSF.eventusermodel XSSF.eventusermodel (SAX)

掌握 POI 解析 Excel 的核心在于理解 Workbook-Sheet-Row-Cell 的结构,并熟练使用 getCellType()DateUtil 来安全地获取不同类型的单元格数据,对于绝大多数应用场景,标准的事件驱动模型已经足够强大和高效。

分享:
扫描分享到社交APP
上一篇
下一篇