杰瑞科技汇

Java POI如何高效解析Excel?

Apache POI 是一个开源的 Java 库,专门用于处理 Microsoft Office 格式的文件,包括 Word、Excel 和 PowerPoint,对于 Excel,POI 提供了两种主要的 API:

Java POI如何高效解析Excel?-图1
(图片来源网络,侵删)
  1. HSSF (Horrible SpreadSheet Format): 用于处理 .xls 格式的 Excel 文件(Excel 97-2003)。
  2. XSSF (XML SpreadSheet Format): 用于处理 .xlsx 格式的 Excel 文件(Excel 2007 及以上版本)。
  3. SXSSF (Streaming Usermodel API): XSSF 的一种流式实现,用于处理非常大的 .xlsx 文件,以减少内存消耗。

下面我将分步介绍如何使用这些 API,并提供完整的代码示例。


第一步:添加 Maven 依赖

在你的 pom.xml 文件中添加 POI 的依赖,为了同时支持 .xls.xlsx,以及一些其他功能(如日期处理),我们通常推荐使用 poi-ooxml 这个完整的依赖包。

<dependencies>
    <!-- Apache POI 核心库 -->
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi</artifactId>
        <version>5.2.5</version> <!-- 建议使用较新版本 -->
    </dependency>
    <!-- 用于处理 .xlsx 格式的文件 -->
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml</artifactId>
        <version>5.2.5</version>
    </dependency>
    <!-- 用于处理 .xlsx 格式中的 OOXML 组件(如图片) -->
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml-lite</artifactId>
        <version>5.2.5</version>
    </dependency>
</dependencies>

第二步:准备一个 Excel 文件

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

姓名 年龄 生日 是否在职 薪资
张三 28 1995-05-20 true 50
李四 35 1988-10-01 false 00
王五 22 2001-08-15 true 75

这个文件是 .xlsx 格式,如果是 .xls 格式,代码逻辑基本相同,只需要把 XSSFWorkbook 换成 HSSFWorkbook

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

第三步:编写 Java 代码解析 Excel

我们将分两种主要方式来解析:

  1. 事件模型 (SAX API):适用于读取非常大的文件,内存占用小,它不将整个文件加载到内存,而是逐行触发事件,由开发者处理。
  2. 用户模型 (Usermodel API):最常用、最简单的方式,它会将整个 Excel 文件加载到内存中,形成一个可以操作的树形结构,对于中小型文件非常方便。

用户模型 - 推荐(简单易用)

这是最直观的方式,类似于操作一个二维数组。

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;
import java.util.Date;
public class ExcelReaderUserModel {
    public static void main(String[] args) {
        // 1. 定义文件路径
        String filePath = "path/to/your/data.xlsx"; // 请替换为你的文件路径
        try (FileInputStream fis = new FileInputStream(filePath);
             // 2. 根据文件格式创建 Workbook 对象
             // 对于 .xls 文件,使用 new HSSFWorkbook(fis);
             Workbook workbook = new XSSFWorkbook(fis)) {
            // 3. 获取第一个工作表
            Sheet sheet = workbook.getSheetAt(0);
            // 4. 遍历每一行(从0开始,0通常是标题行)
            for (Row row : sheet) {
                // 跳过标题行(可选)
                if (row.getRowNum() == 0) {
                    continue;
                }
                // 5. 遍历每一列
                // 假设列顺序是:姓名, 年龄, 生日, 是否在职, 薪资
                String name = getCellValueAsString(row.getCell(0));
                int age = (int) row.getCell(1).getNumericCellValue();
                Date birthday = row.getCell(2).getDateCellValue();
                boolean isActive = row.getCell(3).getBooleanCellValue();
                double salary = row.getCell(4).getNumericCellValue();
                // 格式化日期
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
                String formattedBirthday = sdf.format(birthday);
                // 6. 打印数据
                System.out.printf("姓名: %s, 年龄: %d, 生日: %s, 是否在职: %b, 薪资: %.2f%n",
                        name, age, formattedBirthday, isActive, salary);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /**
     * 将 Cell 的值安全地转换为字符串
     * @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)) {
                    return new SimpleDateFormat("yyyy-MM-dd").format(cell.getDateCellValue());
                } else {
                    // 处理普通数字,避免科学计数法
                    return String.valueOf((long) cell.getNumericCellValue());
                }
            case BOOLEAN:
                return String.valueOf(cell.getBooleanCellValue());
            case FORMULA:
                // 获取公式的计算结果
                return String.valueOf(cell.getNumericCellValue());
            case BLANK:
                return "";
            default:
                return "";
        }
    }
}

代码解析:

  1. FileInputStream: 用于读取 Excel 文件。
  2. Workbook: 代表整个 Excel 文档。.xlsxXSSFWorkbook.xlsHSSFWorkbook,使用 try-with-resources 确保 WorkbookFileInputStream 在使用后被自动关闭。
  3. Sheet: 代表 Excel 中的一个工作表(Sheet)。getSheetAt(0) 获取第一个工作表。
  4. Row: 代表工作表中的一行,我们使用增强的 for 循环遍历每一行。
  5. Cell: 代表行中的一个单元格,同样使用 for 循环遍历每一列。
  6. cell.getCellType(): 非常重要!Excel 单元格有多种类型(字符串、数字、布尔值、公式等),必须根据类型调用不同的 getXXXCellValue() 方法。
    • CellType.STRING
    • CellType.NUMERIC
    • CellType.BOOLEAN
    • CellType.FORMULA
    • CellType.BLANK
  7. DateUtil.isCellDateFormatted(cell): 一个工具方法,用于判断数字格式的单元格是否代表日期。
  8. getCellValueAsString 辅助方法: 这是一个很好的实践,可以统一处理各种单元格类型,避免在业务逻辑中写大量的 switch 语句,并处理了 null 值。

事件模型 - 适用于大文件

事件模型更复杂一些,但它不将整个文件加载到内存,因此可以处理非常大的文件而不会导致 OutOfMemoryError

import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.eventusermodel.XSSFReader;
import org.apache.poi.xssf.model.SharedStringsTable;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
public class ExcelReaderEventModel {
    public static void main(String[] args) {
        String filePath = "path/to/your/data.xlsx"; // 请替换为你的文件路径
        try {
            processOneSheet(filePath);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static void processOneSheet(String filePath) throws IOException, InvalidFormatException, SAXException {
        try (InputStream is = new FileInputStream(filePath)) {
            // 1. 创建 XSSFReader
            XSSFReader reader = new XSSFReader(is);
            // 2. 获取共享字符串表
            SharedStringsTable sst = reader.getSharedStringsTable();
            // 3. 创建 XMLReader
            XMLReader parser = XMLReaderFactory.createXMLReader();
            // 4. 设置自定义的 ContentHandler
            // MyHandler 是我们自己实现的处理器
            ContentHandler handler = new MyHandler(sst);
            parser.setContentHandler(handler);
            // 5. 获取第一个工作表的 InputStream 并解析
            InputStream sheetStream = reader.getSheet("rId1"); // "rId1" 通常是第一个sheet
            InputSource sheetSource = new InputSource(sheetStream);
            parser.parse(sheetSource);
            sheetStream.close();
            // 从处理器中获取解析结果
            List<String[]> dataList = ((MyHandler) handler).getDataList();
            for (String[] row : dataList) {
                for (String cell : row) {
                    System.out.print(cell + "\t");
                }
                System.out.println();
            }
        }
    }
}
/**
 * 自定义的 SAX ContentHandler,用于处理 Excel 的 XML 数据
 */
class MyHandler extends DefaultHandler {
    private SharedStringsTable sst;
    private String[] currentRowData;
    private int currentCellIndex = 0;
    private StringBuilder currentCellValue;
    private List<String[]> dataList = new ArrayList<>();
    private boolean isCellValue = false;
    public MyHandler(SharedStringsTable sst) {
        this.sst = sst;
    }
    public List<String[]> getDataList() {
        return dataList;
    }
    @Override
    public void startElement(String uri, String localName, String qName, org.xml.sax.Attributes attributes) throws SAXException {
        if ("row".equals(qName)) {
            // 开始新的一行,初始化行数据数组
            // 假设每行最多20列,可以根据实际情况调整
            currentRowData = new String[20]; 
            currentCellIndex = 0;
        } else if ("c".equals(qName)) {
            // 遇到单元格,重置当前单元格值
            currentCellValue = new StringBuilder();
            // 可以在这里根据单元格类型做不同处理,这里简化处理
        } else if ("v".equals(qName)) {
            // 遇到单元格的值
            isCellValue = true;
        }
    }
    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        if (isCellValue) {
            currentCellValue.append(ch, start, length);
        }
    }
    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        if ("v".equals(qName)) {
            isCellValue = false;
            String value = currentCellValue.toString();
            // 处理共享字符串
            if (value != null && !value.isEmpty()) {
                try {
                    int idx = Integer.parseInt(value);
                    value = sst.getItemAt(idx).getString();
                } catch (NumberFormatException e) {
                    // 如果不是共享字符串索引,直接使用原始值(如数字、布尔值)
                    // 这里简化处理,实际应用中需要更复杂的逻辑
                }
            }
            // 将值存入当前行数组
            if (currentCellIndex < currentRowData.length) {
                currentRowData[currentCellIndex++] = value;
            }
        } else if ("row".equals(qName)) {
            // 一行结束,将当前行数据添加到总列表中
            // 截取有效数据长度,避免末尾的null
            String[] trimmedRow = new String[currentCellIndex];
            System.arraycopy(currentRowData, 0, trimmedRow, 0, currentCellIndex);
            dataList.add(trimmedRow);
        }
    }
}

代码解析:

  1. XSSFReader: 用于读取 .xlsx 文件的底层 XML 结构。
  2. SharedStringsTable (SST): Excel 为了节省空间,会把所有字符串(如列名、文本内容)都存放在一个公共的字符串池里,单元格中存的是这个池中的索引,我们需要通过 SST 来获取真正的字符串值。
  3. XMLReader 和 ContentHandler: 这是标准的 SAX 解析模型。MyHandler 继承 DefaultHandler,我们在这里重写 startElement, characters, endElement 方法来解析 XML 流。
    • startElement: 当遇到 XML 标签开始时触发,我们在这里识别 <row>(行开始)和 <c>(单元格开始)。
    • characters: 当遇到标签内的文本内容时触发,这里就是单元格的值。
    • endElement: 当遇到 XML 标签结束时触发,我们在这里处理 <v>(单元格值结束),将值从 SST 中取出,并添加到当前行的数组中,当遇到 </row> 时,将整行数据存入总列表。
  4. reader.getSheet("rId1"): 获取特定工作表的流。"rId1" 是一个内部引用 ID,通常对应第一个工作表,要获取所有工作表,需要遍历 Workbook 的相关部分。

总结与建议

特性 用户模型 事件模型
易用性 ,API 直观,像操作对象 ,需要理解 SAX 和 XML 结构
内存占用 ,将整个文件加载到内存 ,逐行解析,内存占用恒定
性能 中等 对于大文件,性能更好
适用场景 中小型 Excel 文件,需要灵活访问任意单元格 大型或超大型 Excel 文件(几百MB以上)

给你的建议:

  • 对于 99% 的应用场景,直接使用“用户模型”,它简单、快速、不易出错。
  • 只有当你的 Excel 文件特别大(比如超过 100MB),并且你遇到了内存溢出(OutOfMemoryError)问题时,才考虑切换到“事件模型”,虽然复杂,但它是解决大文件问题的标准方案。

希望这个详细的教程能帮助你掌握 Java POI 解析 Excel!

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