通过POI组件读取Excel大文件

本文介绍如何使用Java POI的事件驱动模式高效读取大型Excel文件,包括环境搭建、核心代码实现及测试过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、问题背景

在Java中读取Excel可以通过POI组件。POI提供了跨平台的能力,在linux环境下可以对excel文件做操作;如果是采用COM接口则必须要在window下,而且需要安装office软件。

POI提供UserModel和事件驱动两种方式读取excel。UserModel方式操作简洁,但是内存消耗大,稍微大一点的excel读取就会报内存溢出。

下面讲的是如果通过事件驱动的方式读取excel。代码经测试读取10万行记录的excel在10秒左右。

二、环境准备

需要导入如下Jar包依赖:
这里写图片描述

三、样例代码

1、从DefaultHandler派生事件处理类ExcelAbstract

package com.elon.excel;

import java.io.InputStream;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

import org.apache.poi.xssf.eventusermodel.XSSFReader;
import org.apache.poi.xssf.model.SharedStringsTable;
import org.apache.poi.xssf.usermodel.XSSFRichTextString;
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;

/**
 * POI事件驱动读取Excel文件的抽象类。
 * 
 * @author elon
 * @version 2018年7月7日
 */
public abstract class ExcelAbstract extends DefaultHandler {
    private SharedStringsTable sst;
    private String lastContents;
    private boolean nextIsString;

    private int curRow = 0;
    private String curCellName = "";

    /**
     * 读取当前行的数据。key是单元格名称如A1,value是单元格中的值。如果单元格式空,则没有数据。
     */
    private Map<String, String> rowValueMap = new HashMap<>();

    /**
     * 处理单行数据的回调方法。
     * 
     * @param curRow 当前行号
     * @param rowValueMap 当前行的值
     * @throws SQLException
     */
    public abstract void optRows(int curRow, Map<String, String> rowValueMap);

    /**
     * 读取Excel指定sheet页的数据。
     * 
     * @param filePath 文件路径
     * @param sheetNum sheet页编号.从1开始。
     * @throws Exception
     */
    public void readOneSheet(String filePath, int sheetNum) throws Exception {
        OPCPackage pkg = OPCPackage.open(filePath);
        XSSFReader r = new XSSFReader(pkg);
        SharedStringsTable sst = r.getSharedStringsTable();

        XMLReader parser = getSheetParser(sst);

        // 根据 rId# 或 rSheet# 查找sheet
        InputStream sheet2 = r.getSheet("rId" + sheetNum);
        InputSource sheetSource = new InputSource(sheet2);
        parser.parse(sheetSource);
        sheet2.close();
        pkg.close();
    }

    @Override
    public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException {
        // c => 单元格
        if (name.equals("c")) {
            
            // 如果下一个元素是 SST 的索引,则将nextIsString标记为true
            String cellType = attributes.getValue("t");
            if (cellType != null && cellType.equals("s")) {
                nextIsString = true;
            } else {
                nextIsString = false;
            }
        }
        
        // 置空
        lastContents = "";
        
        /**
         * 记录当前读取单元格的名称
         */
        String cellName = attributes.getValue("r");
        if (cellName != null && !cellName.isEmpty()) {
            curCellName = cellName;
        }
    }

    @Override
    public void endElement(String uri, String localName, String name) throws SAXException {
        // 根据SST的索引值的到单元格的真正要存储的字符串
        // 这时characters()方法可能会被调用多次
        if (nextIsString) {
            try {
                int idx = Integer.parseInt(lastContents);
                lastContents = new XSSFRichTextString(sst.getEntryAt(idx)).toString();
            } catch (Exception e) {

            }
        }

        // v => 单元格的值,如果单元格是字符串则v标签的值为该字符串在SST中的索引
        // 将单元格内容加入rowlist中,在这之前先去掉字符串前后的空白符
        if (name.equals("v")) {
            String value = lastContents.trim();
            value = value.equals("") ? " " : value;
            rowValueMap.put(curCellName, value);
        } else {
            // 如果标签名称为 row ,这说明已到行尾,调用 optRows() 方法
            if (name.equals("row")) {
                optRows(curRow, rowValueMap);
                rowValueMap.clear();
                curRow++;
            }
        }
    }

    public void characters(char[] ch, int start, int length) throws SAXException {
        // 得到单元格内容的值
        lastContents += new String(ch, start, length);
    }
    
    /**
     * 获取单个sheet页的xml解析器。
     * @param sst
     * @return
     * @throws SAXException
     */
    private XMLReader getSheetParser(SharedStringsTable sst) throws SAXException {
        XMLReader parser = XMLReaderFactory.createXMLReader("org.apache.xerces.parsers.SAXParser");
        this.sst = sst;
        parser.setContentHandler(this);
        return parser;
    }
}


2、从ExcelAbstract派生ExcelReaderUtil处理每一行数据

package com.elon.excel;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.poi.hssf.usermodel.HSSFDateUtil;

/**
 * Excel读取公共类。
 * @author elon
 * @version 2018年7月7日
 */
public class ExcelReaderUtil extends ExcelAbstract {
    
    /**
     * 提取列名称的正则表达式
     */
    private static final String DISTILL_COLUMN_REG = "^([A-Z]{1,})";
    
    /**
     * 读取excel的每一行记录。map的key是列号(A、B、C...), value是单元格的值。如果单元格是空,则没有值。
     */
    private List<Map<String, String>> dataList = new ArrayList<>();
    
    @Override
    public void optRows(int curRow, Map<String, String> rowValueMap) {
        
        Map<String, String> dataMap = new HashMap<>();
        rowValueMap.forEach((k,v)->dataMap.put(removeNum(k), v));
        dataList.add(dataMap);
    }
    
    /**
     * 日期数字转换为字符串。
     * 
     * @param dateNum excel中存储日期的数字
     * @return 格式化后的字符串形式
     */
    public static String dateNum2Str(String dateNum) {
        Date date = HSSFDateUtil.getJavaDate(Double.parseDouble(dateNum));
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
        return formatter.format(date);
    }
    
    /**
     * 删除单元格名称中的数字,只保留列号。
     * @param cellName 单元格名称。如:A1
     * @return 列号。如:A
     */
    private String removeNum(String cellName) {
        Pattern pattern = Pattern.compile(DISTILL_COLUMN_REG);
        Matcher m = pattern.matcher(cellName);
        if (m.find()) {
            return m.group(1);
        }
        
        return "";
    }

    public List<Map<String, String>> getDataList() {
        return dataList;
    }
}

上面的dateNum2Str方法将excel中读取的日期数字转换为可读的字符串形式。之所不在ExcelAbstract中根据 attributes.getValue(“s”)的返回值自动判断日期类型,是由于不同的excel版本这个值是不同的。但是客户端是清楚哪一列是存储日期的,最好由调用者判断。

四、测试代码

package com.elon.excel;

public class TestExcel {
    public static void main(String[] args) {
        try {
            ExcelReaderUtil excel = new ExcelReaderUtil();
            excel.readOneSheet("E:/temp/test.xlsx", 1);
            System.out.println(excel.getDataList());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}



# **3、测试打印读取的Excel数据**
![这里写图片描述](https://img-blog.csdn.net/20180707231147811?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3lsZm9yZXZlcg==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)

在ExcelAbstract中增加curCellName记录每一次处理的单元格名称是为了解决空单元格的问题。如果只记录值,空单元格无法区分。通过curCellName可以识别出没有值的单元格。
<think>嗯,用户想了解POI读取Excel,那我得先确认POI是什么。POI应该是Apache的POI库,用来处理Office文档的Java库。对吧?然后,用户可能需要知道如何用POI读取Excel文件的基本步骤。 首先,我应该介绍POI是什么,它的全称是Apache POI,用于读写Microsoft Office格式文件,比如Excel、Word等。然后重点放在Excel上,因为用户问的是读取Excel。 接下来,可能需要分步骤说明。比如,添加依赖,创建工作簿对象,获取工作表,遍历行和单元格,最后关闭资源。要确保步骤清晰,用户能一步步跟着操作。 同时,用户可能想知道不同版本的Excel处理方式,比如xls和xlsx的区别。HSSF和XSSF的区别需要提到。还有,处理日期或数字格式的单元格需要注意什么,比如获取单元格类型,判断是否是数值或日期,可能需要处理不同的数据格式。 另外,异常处理也很重要,比如IOException,还有关闭资源的部分,应该用try-with-resources自动关闭,避免资源泄露。用户可能对异常处理不太熟悉,所以需要提醒。 可能还需要一个代码示例,这样用户可以直接参考。代码示例应该包括读取xlsx文件的例子,使用XSSF,然后遍历行和单元格,提取数据。注意代码的正确性,比如正确导入类,处理不同类型的单元格。 还要注意中文的问题,确保注释或输出是中文,或者在解释的时候用中文说明。比如在代码里用中文注释,或者输出中文提示。 用户可能的背景是Java开发者,需要处理Excel数据导入导出,比如在Web应用中上传Excel文件进行数据处理。所以需要强调实际应用场景,比如数据导入、报表生成等。 可能还需要提到一些常见问题,比如大文件处理时内存溢出,这时候可以提到使用SXSSF来优化内存,或者使用事件驱动的模型来解析,比如XSSF和SAX结合。 另外,性能优化方面,比如避免在循环中创建太多对象,及时关闭资源,使用适当的方法读取单元格值,避免类型错误导致的异常。 总结一下,回答的结构应该是:介绍POI读取Excel的步骤,代码示例,不同版本处理,数据类型处理,异常和资源管理,常见问题及优化建议。这样用户能全面了解如何使用POI读取Excel,并且避免常见错误。</think>POI(Apache POI)是一个开源的Java库,用于读写Microsoft Office格式文件(如Excel、Word、PowerPoint)。以下是使用POI读取Excel文件的详细介绍: --- ### **1. 核心概念** - **HSSF**:处理`.xls`格式(Excel 97-2003) - **XSSF**:处理`.xlsx`格式(Excel 2007+) - **SXSSF**:用于大数据量的`.xlsx`文件(基于流式处理) --- ### **2. 实现步骤** #### **步骤1:添加依赖** Maven项目中需引入依赖: ```xml <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>5.2.3</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>5.2.3</version> </dependency> ``` #### **步骤2:读取Excel文件** ```java import org.apache.poi.ss.usermodel.*; public class ReadExcelDemo { public static void main(String[] args) { try (Workbook workbook = WorkbookFactory.create(new File("data.xlsx"))) { // 获取第一个Sheet Sheet sheet = workbook.getSheetAt(0); // 遍历所有行 for (Row row : sheet) { // 遍历所有单元格 for (Cell cell : row) { // 根据单元格类型获取值 switch (cell.getCellType()) { case STRING: System.out.print(cell.getStringCellValue() + "\t"); break; case NUMERIC: if (DateUtil.isCellDateFormatted(cell)) { System.out.print(cell.getDateCellValue() + "\t"); } else { System.out.print(cell.getNumericCellValue() + "\t"); } break; case BOOLEAN: System.out.print(cell.getBooleanCellValue() + "\t"); break; default: System.out.print("[未知类型]\t"); } } System.out.println(); // 换行 } } catch (IOException e) { e.printStackTrace(); } } } ``` --- ### **3. 关键注意事项** 1. **文件格式兼容性** - `.xls` 使用 `HSSFWorkbook` - `.xlsx` 使用 `XSSFWorkbook` - 通用方法:通过 `WorkbookFactory.create()` 自动识别格式 2. **数据类型处理** - 数值和日期共用`NUMERIC`类型,需用`DateUtil.isCellDateFormatted()`判断 - 公式单元格需使用`cell.getCellFormula()`获取公式,或`cell.getNumericCellValue()`获取计算结果 3. **性能优化** - 大文件使用`SXSSFWorkbook`(流式读取) - 避免频繁操作单元格样式 --- ### **4. 常见问题** - **内存溢出**:读取大文件时,优先选择`SXSSF`或事件驱动模式(如`XSSF SAX Parser`) - **空单元格处理**:使用`CellIterator`跳过空单元格 - **编码问题**:确保文本编码与Excel文件一致(通常为UTF-8) --- ### **5. 应用场景** - 数据导入(如用户批量上传) - 报表自动化生成 - 数据分析与统计 通过Apache POI,开发者可以灵活实现Excel文件的读写操作,适用于大多数企业级数据处理需求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值