杰瑞科技汇

Java POI如何高效读取Excel文件?

目录

  1. Apache POI 简介
  2. 环境配置
  3. 核心概念
  4. 读取 Excel 文件(基础示例)
    • 读取 .xlsx 格式 (XSSF)
    • 读取 .xls 格式 (HSSF)
  5. 进阶用法
    • 读取不同数据类型(数字、日期、布尔值)
    • 获取单元格样式
    • 处理公式单元格
    • 读取大文件(使用 SAX 模式)
  6. 完整示例代码
  7. 最佳实践与注意事项

Apache POI 简介

Apache POI (Poor Obfuscation Implementation) 是一个开源的 Java 库,由 Apache 软件基金会维护,它提供了 API,允许 Java 程序读取和写入 Microsoft Office 格式的文件,最常用的就是 Excel 文件(.xls.xlsx)。

Java POI如何高效读取Excel文件?-图1
(图片来源网络,侵删)

POI 主要处理两种格式的 Excel:

  • .xls: Excel 97-2003 格式,使用 HSSF (Horrible Spreadsheet Format) API。
  • .xlsx: Excel 2007 及以后版本,基于 Office Open XML 格式,使用 XSSF (XML Spreadsheet Format) API。

环境配置

你需要在你的项目中添加 POI 的依赖,如果你使用 Maven,在 pom.xml 文件中添加以下依赖:

<!-- 如果只需要读取 .xlsx 文件,可以只添加这个 -->
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>5.2.5</version> <!-- 建议使用最新稳定版 -->
</dependency>
<!-- 如果需要读取旧的 .xls 文件,需要额外添加这个 -->
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>5.2.5</version>
</dependency>
<!-- 如果需要处理日期,需要这个 -->
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml-lite</artifactId>
    <version>5.2.5</version>
</dependency>

如果你使用 Gradle,在 build.gradle 文件中添加:

// 如果只需要读取 .xlsx 文件
implementation 'org.apache.poi:poi-ooxml:5.2.5'
// 如果需要读取 .xls 文件
implementation 'org.apache.poi:poi:5.2.5'

核心概念

在开始编写代码前,了解 POI 的核心对象模型非常重要:

Java POI如何高效读取Excel文件?-图2
(图片来源网络,侵删)
  • Workbook: 代表一个 Excel 工作簿,是所有表格的入口。
    • XSSFWorkbook: 对应 .xlsx 文件。
    • HSSFWorkbook: 对应 .xls 文件。
  • Sheet: 代表一个工作表(Excel 底部的标签页,如 Sheet1, Sheet2)。
  • Row: 代表一行,由多个单元格组成。
  • Cell: 代表一个单元格,是数据存储的最小单位。
  • DataFormatter: 一个非常有用的工具类,可以自动将单元格的值格式化为字符串,而无需关心其原始数据类型(数字、日期、布尔值等)。

读取 Excel 文件(基础示例)

下面我们来看如何读取一个 Excel 文件,我们将创建一个通用的方法,根据文件后缀名自动选择使用 HSSF 还是 XSSF

步骤:

  1. 获取 Workbook 对象。
  2. 获取指定的 Sheet
  3. 遍历 Sheet 中的每一行 (Row)。
  4. 遍历每一行中的每一个单元格 (Cell)。
  5. 获取单元格的值。

示例代码片段:

import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import java.io.FileInputStream;
import java.io.IOException;
public class ExcelReader {
    public static void main(String[] args) {
        // Excel文件路径
        String excelFilePath = "C:\\path\\to\\your\\file.xlsx"; // 或者 file.xls
        try (FileInputStream fis = new FileInputStream(excelFilePath);
             Workbook workbook = WorkbookFactory.create(fis)) { // 使用WorkbookFactory可以自动识别.xls或.xlsx
            // 获取第一个工作表
            Sheet sheet = workbook.getSheetAt(0);
            // 遍历每一行
            for (Row row : sheet) {
                // 遍历每一个单元格
                for (Cell cell : row) {
                    // 打印单元格的值
                    System.out.print(getCellValueAsString(cell) + "\t");
                }
                System.out.println(); // 换行
            }
        } 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();
            case NUMERIC:
                // 检查是数字还是日期
                if (DateUtil.isCellDateFormatted(cell)) {
                    return cell.getDateCellValue().toString();
                } else {
                    return String.valueOf(cell.getNumericCellValue());
                }
            case BOOLEAN:
                return String.valueOf(cell.getBooleanCellValue());
            case FORMULA:
                // 对于公式单元格,获取其计算后的值
                return cell.getCellFormula();
            case BLANK:
                return "";
            default:
                return "";
        }
    }
}

代码解释:

  • try-with-resources: 这是 Java 7+ 的推荐写法,可以自动关闭 FileInputStreamWorkbook,避免资源泄漏。
  • WorkbookFactory.create(fis): 这是一个非常方便的工厂方法,它会根据文件内容自动创建 XSSFWorkbookHSSFWorkbook,你无需手动判断文件后缀。
  • sheet.getSheetAt(0): 获取工作簿中第一个工作表(索引从0开始)。
  • for (Row row : sheet)for (Cell cell : row): 使用增强 for 循环遍历行和单元格。
  • getCellValueAsString(): 这是一个辅助方法,用于处理不同类型的单元格数据,使其统一输出为字符串。DateUtil.isCellDateFormatted(cell) 是判断数字单元格是否为日期格式的关键。

进阶用法

1 读取不同数据类型(推荐使用 DataFormatter

手动判断单元格类型比较繁琐,POI 提供了 DataFormatter 工具类,可以大大简化代码。

import org.apache.poi.ss.usermodel.DataFormatter;
import org.apache.poi.ss.usermodel.Row;
// ...
DataFormatter dataFormatter = new DataFormatter();
for (Row row : sheet) {
    for (Cell cell : row) {
        // getCellValueAsString 会自动处理数字、日期、布尔值等,并返回格式化后的字符串
        String cellValue = dataFormatter.formatCellValue(cell);
        System.out.print(cellValue + "\t");
    }
    System.out.println();
}

优点:

Java POI如何高效读取Excel文件?-图3
(图片来源网络,侵删)
  • 代码更简洁。
  • 能正确应用单元格的自定义格式(如 "¥#,##0.00")。
  • 对于公式单元格,它返回的是公式的字符串,而不是计算结果,如果需要计算结果,需要额外处理。

2 获取单元格样式

你可以获取单元格的字体、颜色、对齐方式、边框等样式信息。

CellStyle cellStyle = cell.getCellStyle();
// 获取字体
Font font = workbook.getFontAt(cellStyle.getFontIndex());
System.out.println("字体名称: " + font.getFontName());
System.out.println("字体大小: " + font.getFontHeightInPoints());
System.out.println("是否加粗: " + font.getBold());
// 获取背景颜色
// 注意:XSSF 和 HSSF 获取颜色的方式不同
// XSSF
if (workbook instanceof XSSFWorkbook) {
    XSSFCellStyle xssfCellStyle = (XSSFCellStyle) cellStyle;
    XSSFColor xssfColor = xssfCellStyle.getFillForegroundColorColor();
    if (xssfColor != null) {
        byte[] rgb = xssfColor.getRGB();
        // ...处理rgb颜色
    }
}
// HSSF
if (workbook instanceof HSSFWorkbook) {
    HSSFCellStyle hssfCellStyle = (HSSFCellStyle) cellStyle;
    short index = hssfCellStyle.getFillForegroundColorIndex();
    HSSFColor hssfColor = ((HSSFWorkbook) workbook).getCustomPalette().getColor(index);
    if (hssfColor != null) {
        short[] rgb = hssfColor.getRGB();
        // ...处理rgb颜色
    }
}

3 处理公式单元格

默认情况下,cell.getCellType() 返回 FORMULA,你需要调用 cell.getCachedFormulaResultType() 来获取公式计算结果的类型,然后再获取值。

if (cell.getCellType() == CellType.FORMULA) {
    CellType resultType = cell.getCachedFormulaResultType();
    switch (resultType) {
        case NUMERIC:
            if (DateUtil.isCellDateFormatted(cell)) {
                System.out.println("公式计算结果(日期): " + cell.getDateCellValue());
            } else {
                System.out.println("公式计算结果(数字): " + cell.getNumericCellValue());
            }
            break;
        case STRING:
            System.out.println("公式计算结果(字符串): " + cell.getStringCellValue());
            break;
        case BOOLEAN:
            System.out.println("公式计算结果(布尔值): " + cell.getBooleanCellValue());
            break;
        case ERROR:
            System.out.println("公式计算结果(错误): " + cell.getErrorCellValue());
            break;
    }
}

4 读取大文件(SAX 模式)

对于非常大的 Excel 文件(如几百MB),使用 XSSFWorkbook 会将整个文件加载到内存中,容易导致 OutOfMemoryError,这时,应该使用 SAX 模式(Event API)进行流式读取,它逐行解析,内存占用非常小。

import org.apache.poi.xssf.eventusermodel.XSSFReader;
import org.apache.poi.xssf.model.SharedStringsTable;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.xml.sax.Attributes;
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.InputStream;
public class LargeExcelReader {
    public static void main(String[] args) throws Exception {
        String excelFilePath = "C:\\path\\to\\large\\file.xlsx";
        try (OPCPackage pkg = OPCPackage.open(excelFilePath)) {
            XSSFReader reader = new XSSFReader(pkg);
            SharedStringsTable sst = reader.getSharedStringsTable();
            XMLReader parser = XMLReaderFactory.createXMLReader();
            parser.setContentHandler(new SheetHandler(sst));
            InputStream sheet = reader.getSheet("rId1"); // 获取第一个工作表
            InputSource source = new InputSource(sheet);
            parser.parse(source);
        }
    }
    // 自定义的SAX事件处理器
    private static class SheetHandler extends DefaultHandler {
        private SharedStringsTable sst;
        private String lastContents;
        private boolean nextIsString;
        public SheetHandler(SharedStringsTable sst) {
            this.sst = sst;
        }
        @Override
        public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
            // c => cell
            if (qName.equals("c")) {
                // 图例: B=布尔值, S=字符串, n=数字
                String cellType = attributes.getValue("t");
                if (cellType != null && cellType.equals("s")) {
                    nextIsString = true;
                } else {
                    nextIsString = false;
                }
            }
            // 清空内容
            lastContents = "";
        }
        @Override
        public void endElement(String uri, String localName, String qName) throws SAXException {
            if (nextIsString) {
                int idx = Integer.parseInt(lastContents);
                lastContents = sst.getItemAt(idx).getString();
                nextIsString = false;
            }
            // v => 单元格的值
            if (qName.equals("v")) {
                System.out.print(lastContents + "\t");
            }
            // 每行结束,打印换行符
            if (qName.equals("row")) {
                System.out.println();
            }
        }
        @Override
        public void characters(char[] ch, int start, int length) throws SAXException {
            lastContents += new String(ch, start, length);
        }
    }
}

SAX 模式要点:

  • 它不直接提供 RowCell 对象,而是通过解析 XML 事件来获取数据。
  • 需要自己处理 XML 解析逻辑,相对复杂。
  • 对于共享的字符串(如 Excel 中的 "姓名", "年龄"),会先存入一个共享字符串表,单元格中只存索引,需要通过索引去表中查找。

完整示例代码

这是一个完整的、可运行的示例,读取一个包含标题的 Excel 文件,并将数据封装到 Student 对象列表中。

Student.java (POJO)

public class Student {
    private String name;
    private int age;
    private double score;
    private boolean isMale;
    // Getters and Setters
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
    public double getScore() { return score; }
    public void setScore(double score) { this.score = score; }
    public boolean isMale() { return isMale; }
    public void setMale(boolean male) { isMale = male; }
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", score=" + score +
                ", isMale=" + isMale +
                '}';
    }
}

ExcelReaderExample.java

import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class ExcelReaderExample {
    public static void main(String[] args) {
        String filePath = "students.xlsx"; // 确保文件和程序在同一目录,或提供完整路径
        List<Student> students = readExcel(filePath);
        // 打印读取到的数据
        for (Student student : students) {
            System.out.println(student);
        }
    }
    public static List<Student> readExcel(String filePath) {
        List<Student> studentList = new ArrayList<>();
        DataFormatter dataFormatter = new DataFormatter();
        try (FileInputStream fis = new FileInputStream(filePath);
             Workbook workbook = WorkbookFactory.create(fis)) {
            Sheet sheet = workbook.getSheetAt(0);
            // 假设第一行是标题,从第二行开始读取数据
            for (int i = 1; i <= sheet.getLastRowNum(); i++) {
                Row row = sheet.getRow(i);
                if (row == null) {
                    continue; // 跳过空行
                }
                Student student = new Student();
                // 读取姓名 (String)
                Cell nameCell = row.getCell(0);
                student.setName(dataFormatter.formatCellValue(nameCell));
                // 读取年龄 (Numeric)
                Cell ageCell = row.getCell(1);
                if (ageCell.getCellType() == CellType.NUMERIC) {
                    student.setAge((int) ageCell.getNumericCellValue());
                }
                // 读取分数 (Numeric)
                Cell scoreCell = row.getCell(2);
                if (scoreCell.getCellType() == CellType.NUMERIC) {
                    student.setScore(scoreCell.getNumericCellValue());
                }
                // 读取性别 (Boolean)
                Cell maleCell = row.getCell(3);
                if (maleCell.getCellType() == CellType.BOOLEAN) {
                    student.setMale(maleCell.getBooleanCellValue());
                }
                // 读取出生日期 (Date)
                Cell birthDateCell = row.getCell(4);
                if (birthDateCell.getCellType() == CellType.NUMERIC && DateUtil.isCellDateFormatted(birthDateCell)) {
                    Date birthDate = birthDateCell.getDateCellValue();
                    // 这里可以处理日期,例如格式化或存储为字符串
                    System.out.println("读取到日期: " + birthDate);
                }
                studentList.add(student);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return studentList;
    }
}

students.xlsx 文件内容示例: | 姓名 | 年龄 | 分数 | 是否男性 | 出生日期 | | :--- | :--- | :--- | :--- | :--- | | 张三 | 20 | 89.5 | TRUE | 2003-05-15 | | 李四 | 21 | 92.0 | FALSE | 2002-11-20 | | 王五 | 19 | 78.5 | TRUE | 2004-01-30 |


最佳实践与注意事项

  1. 总是使用 try-with-resources: 确保 WorkbookFileInputStream 等资源被正确关闭,防止内存泄漏。
  2. 优先使用 DataFormatter: 对于大多数场景,使用 DataFormatter.formatCellValue() 是最简单、最不容易出错的方式来获取单元格的显示值。
  3. 处理空单元格和空行: 在遍历时,始终检查 row == nullcell == null,避免 NullPointerException
  4. 为大数据量使用 SAX 模型: 当处理超过几十兆的 Excel 文件时,请务必使用 Event API (SAX 模式) 来避免内存溢出。
  5. 日期处理: Excel 中日期通常存储为数字,使用 DateUtil.isCellDateFormatted(cell) 来判断,并用 cell.getDateCellValue() 来获取 Date 对象。
  6. 性能考虑: 反复创建 Workbook 对象会很慢,如果你的应用需要频繁读取同一个模板文件,可以考虑缓存 Workbook 对象(但要小心并发问题)。
  7. 版本兼容性: XSSF (.xlsx) 是目前的主流格式,功能更全,性能也更好,除非必须兼容旧版 Office 2003,否则建议优先使用 .xlsx 格式。

希望这份详细的指南能帮助你熟练地使用 Java POI 读取 Excel 文件!

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