通过 Apache POI 读取大型 Excel 文件 (xlsx) 时出错

Posted

技术标签:

【中文标题】通过 Apache POI 读取大型 Excel 文件 (xlsx) 时出错【英文标题】:Error While Reading Large Excel Files (xlsx) Via Apache POI 【发布时间】:2012-10-22 22:10:00 【问题描述】:

我正在尝试通过 Apache POI 读取大型 excel 文件 xlsx,例如 40-50 MB。我的内存不足异常。当前堆内存为 3GB。

我可以毫无问题地读取较小的 Excel 文件。我需要一种方法来读取大型 excel 文件,然后通过 Spring excel 视图将它们作为响应返回。

public class FetchExcel extends AbstractView 


    @Override
    protected void renderMergedOutputModel(
            Map model, HttpServletRequest request, HttpServletResponse response) 
    throws Exception 

    String fileName = "SomeExcel.xlsx";

    response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");

    OPCPackage pkg = OPCPackage.open("/someDir/SomeExcel.xlsx");

    XSSFWorkbook workbook = new XSSFWorkbook(pkg);

    ServletOutputStream respOut = response.getOutputStream();

    pkg.close();
    workbook.write(respOut);
    respOut.flush();

    workbook = null;                    

    response.setHeader("Content-disposition", "attachment;filename=\"" +fileName+ "\"");


        


我一开始是使用XSSFWorkbook workbook = new XSSFWorkbook(FileInputStream in); 但这对于每个 Apache POI API 来说都是昂贵的,所以我改用 OPC 封装方式,但效果还是一样。我不需要解析或处理文件,只需读取并返回即可。

【问题讨论】:

试试 SXSSF poi.apache.org/spreadsheet/index.html 我需要一个例子。我在网上搜索,但找不到通过 SXSSF 阅读大表格的示例,否则一开始就不会问这个问题。 @jamesT 你运行这个选项了吗? -Xms1024M -Xmx2048M 没关系,SXSSF 仅用于写入大量数据。给 JVM heap 更多的内存,避免这种粗鲁的 cmets。 您尝试过 ODBC 连接吗?也许会是更好的方法 【参考方案1】:

这是一个使用 sax 解析器读取大型 xls 文件的示例。

public void parseExcel(File file) throws IOException 

        OPCPackage container;
        try 
            container = OPCPackage.open(file.getAbsolutePath());
            ReadOnlySharedStringsTable strings = new ReadOnlySharedStringsTable(container);
            XSSFReader xssfReader = new XSSFReader(container);
            StylesTable styles = xssfReader.getStylesTable();
            XSSFReader.SheetIterator iter = (XSSFReader.SheetIterator) xssfReader.getSheetsData();
            while (iter.hasNext()) 
                InputStream stream = iter.next();

                processSheet(styles, strings, stream);
                stream.close();
            
         catch (InvalidFormatException e) 
            e.printStackTrace();
         catch (SAXException e) 
            e.printStackTrace();
         catch (OpenXML4JException e) 
            e.printStackTrace();
        



protected void processSheet(StylesTable styles, ReadOnlySharedStringsTable strings, InputStream sheetInputStream) throws IOException, SAXException 

        InputSource sheetSource = new InputSource(sheetInputStream);
        SAXParserFactory saxFactory = SAXParserFactory.newInstance();
        try 
            SAXParser saxParser = saxFactory.newSAXParser();
            XMLReader sheetParser = saxParser.getXMLReader();
            ContentHandler handler = new XSSFSheetXMLHandler(styles, strings, new SheetContentsHandler() 

            @Override
                public void startRow(int rowNum) 
                
                @Override
                public void endRow() 
                
                @Override
                public void cell(String cellReference, String formattedValue) 
                
                @Override
                public void headerFooter(String text, boolean isHeader, String tagName) 

                

            , 
            false//means result instead of formula
            );
            sheetParser.setContentHandler(handler);
            sheetParser.parse(sheetSource);
         catch (ParserConfigurationException e) 
            throw new RuntimeException("SAX parser appears to be broken - " + e.getMessage());

【讨论】:

感谢 O.C 正是我正在寻找的处理超过 250k 行的内容。完美的作品。 非常感谢上面的代码 sn-p。 Apache POI 应该在他们的文档中发布一个与上述示例一样的示例,以便更容易地宣传这些 API。 @O.C 非常感谢!!你能告诉如何使用上面的代码在excel中考虑空白单元格吗? 有没有办法使用基于迭代器/基于行的方法?我想用 hasNext() 和 next() 方法围绕它包装一个迭代器,以便调用者有更大的影响力。在这种基于事件的方法中,我无法控制进度,因为我必须获取所有事件,直到没有事件为止。 但这是一个 xlsx 解析器而不是 xls 解析器 :(【参考方案2】:

你没有提到是否需要修改电子表格。

这可能很明显,但是如果您不需要修改电子表格,那么您不需要解析它并将其写回,您可以简单地从文件中读取字节,并写出字节,如你会用,比如图像,或任何其他二进制格式。

如果您确实需要在将电子表格发送给用户之前对其进行修改,那么据我所知,您可能需要采取不同的方法。

我所知道的用于在 Java 中读取 Excel 文件的每个库都会将整个电子表格读取到内存中,因此您必须为每个可能同时处理的电子表格提供 50MB 的可用内存。正如其他人指出的那样,这涉及调整 VM 可用的堆。

如果您需要同时处理大量电子表格,并且无法分配足够的内存,请考虑使用可以流式传输的格式,而不是一次性将所有电子表格读入内存。 CSV格式可以用Excel打开,过去我通过将content-type设置为application/vnd.ms-excel,将附件文件名设置为以“.xls”结尾的东西,但实际上返回的是CSV内容。我已经有几年没有尝试过了,所以 YMMV。

【讨论】:

【参考方案3】:

在 bellwo 示例中,我将添加一个完整的代码,如何将完整的 excel 文件(对我来说是 60Mo)解析为对象列表,而不会出现“内存不足”的任何问题并且工作正常:

import java.util.ArrayList;
import java.util.List;


class DistinctByProperty 

    private static OPCPackage xlsxPackage = null;
    private static PrintStream output= System.out;
    private static List<MassUpdateMonitoringRow> resultMapping = new ArrayList<>();


    public static void main(String[] args) throws IOException 

        File file = new File("C:\\Users\\aberguig032018\\Downloads\\your_excel.xlsx");

        double bytes = file.length();
        double kilobytes = (bytes / 1024);
        double megabytes = (kilobytes / 1024);
        System.out.println("Size "+megabytes);

        parseExcel(file);
    

    public static void parseExcel(File file) throws IOException 

        try 
            xlsxPackage = OPCPackage.open(file.getAbsolutePath(), PackageAccess.READ);
            ReadOnlySharedStringsTable strings = new ReadOnlySharedStringsTable(xlsxPackage);
            XSSFReader xssfReader = new XSSFReader(xlsxPackage);
            StylesTable styles = xssfReader.getStylesTable();
            XSSFReader.SheetIterator iter = (XSSFReader.SheetIterator) xssfReader.getSheetsData();
            int index = 0;
            while (iter.hasNext()) 
                try (InputStream stream = iter.next()) 
                    String sheetName = iter.getSheetName();
                    output.println();
                    output.println(sheetName + " [index=" + index + "]:");
                    processSheet(styles, strings, new MappingFromXml(resultMapping), stream);
                
                ++index;
            

         catch (InvalidFormatException e) 
            e.printStackTrace();
         catch (OpenXML4JException e) 
            e.printStackTrace();
         catch (SAXException e) 
            e.printStackTrace();
        
    

    private static void processSheet(StylesTable styles, ReadOnlySharedStringsTable strings, MappingFromXml mappingFromXml, InputStream sheetInputStream) throws IOException, SAXException 
        DataFormatter formatter = new DataFormatter();
        InputSource sheetSource = new InputSource(sheetInputStream);
        try 
            XMLReader sheetParser = SAXHelper.newXMLReader();
            ContentHandler handler = new XSSFSheetXMLHandler(
                    styles, null, strings, mappingFromXml, formatter, false);

            sheetParser.setContentHandler(handler);
            sheetParser.parse(sheetSource);
            System.out.println("Size of Array "+resultMapping.size());
         catch(ParserConfigurationException e) 
            throw new RuntimeException("SAX parser appears to be broken - " + e.getMessage());
        
    

你必须添加一个实现的类

SheetContentsHandler

import com.sun.org.apache.xpath.internal.operations.Bool;
import org.apache.poi.ss.util.CellAddress;
import org.apache.poi.ss.util.CellReference;
import org.apache.poi.xssf.eventusermodel.XSSFSheetXMLHandler.SheetContentsHandler;

import org.apache.poi.xssf.usermodel.XSSFComment;

import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;

public class MappingFromXml implements SheetContentsHandler 

    private List<myObject> result = new ArrayList<>();
    private myObject myObject = null;
    private int lineNumber = 0;
    /**
     * Number of columns to read starting with leftmost
     */
    private int minColumns = 25;
    /**
     * Destination for data
     */
    private PrintStream output = System.out;

    public MappingFromXml(List<myObject> list) 
        this.result = list;
    

    @Override
    public void startRow(int i) 
        output.println("iii " + i);
        lineNumber = i;
        myObject = new myObject();
    

    @Override
    public void endRow(int i) 
        output.println("jjj " + i);
        result.add(myObject);
        myObject = null;
    

    @Override
    public void cell(String cellReference, String formattedValue, XSSFComment comment) 
        int columnIndex = (new CellReference(cellReference)).getCol();

        if(lineNumber > 0)
            switch (columnIndex) 
                case 0: //Tech id
                    if (formattedValue != null && !formattedValue.isEmpty())
                        myObject.setId(Integer.parseInt(formattedValue));
                
                break;
                //TODO add other cell
            
        
    

    @Override
    public void headerFooter(String s, boolean b, String s1) 

    

欲了解更多信息,请访问link

【讨论】:

【参考方案4】:

我在解析xlsx文件时也遇到了同样的OOM问题……经过两天的努力,我终于发现下面的代码真的很完美;

此代码基于 sjxlsx。它读取 xlsx 并存储在 HSSF 表中。

           [code=java] 
            // read the xlsx file
       SimpleXLSXWorkbook = new SimpleXLSXWorkbook(new File("C:/test.xlsx"));

        HSSFWorkbook hsfWorkbook = new HSSFWorkbook();

        org.apache.poi.ss.usermodel.Sheet hsfSheet = hsfWorkbook.createSheet();

        Sheet sheetToRead = workbook.getSheet(0, false);

        SheetRowReader reader = sheetToRead.newReader();
        Cell[] row;
        int rowPos = 0;
        while ((row = reader.readRow()) != null) 
            org.apache.poi.ss.usermodel.Row hfsRow = hsfSheet.createRow(rowPos);
            int cellPos = 0;
            for (Cell cell : row) 
                if(cell != null)
                    org.apache.poi.ss.usermodel.Cell hfsCell = hfsRow.createCell(cellPos);
                    hfsCell.setCellType(org.apache.poi.ss.usermodel.Cell.CELL_TYPE_STRING);
                    hfsCell.setCellValue(cell.getValue());
                
                cellPos++;
            
            rowPos++;
        
        return hsfSheet;[/code]

【讨论】:

这个例子展示了如何写入一个excel文件,问题是我们如何在poi中写入一个excel文件。

以上是关于通过 Apache POI 读取大型 Excel 文件 (xlsx) 时出错的主要内容,如果未能解决你的问题,请参考以下文章

java通过apache poi框架读取2007版Excel文件

JAVA:通过poi读取excel

Java通过POI读取Excel

POI读取Excel

Apache POI将txt文件读取为excel文件

Java通过poi读取excel中文件