【问题标题】:PHPExcel runs out of 256, 512 and also 1024MB of RAMPHPExcel 用完了 256、512 和 1024MB 的 RAM
【发布时间】:2011-06-16 14:27:08
【问题描述】:

我不明白。 XSLX 表大约 3MB 大,但即使 1024MB 的 RAM 也不足以让 PHPExcel 将其加载到内存中?

我在这里可能做错了什么:

function ReadXlsxTableIntoArray($theFilePath)
{
    require_once('PHPExcel/Classes/PHPExcel.php');
    $inputFileType = 'Excel2007';
    $objReader = PHPExcel_IOFactory::createReader($inputFileType);
    $objReader->setReadDataOnly(true);
    $objPHPExcel = $objReader->load($theFilePath);
    $rowIterator = $objPHPExcel->getActiveSheet()->getRowIterator();
    $arrayData = $arrayOriginalColumnNames = $arrayColumnNames = array();
    foreach($rowIterator as $row){
        $cellIterator = $row->getCellIterator();
        $cellIterator->setIterateOnlyExistingCells(false); // Loop all cells, even if it is not set
        if(1 == $row->getRowIndex ()) {
            foreach ($cellIterator as $cell) {
                $value = $cell->getCalculatedValue();
                $arrayOriginalColumnNames[] = $value;
                // let's remove the diacritique
                $value = iconv('UTF-8', 'ISO-8859-1//TRANSLIT', $value);
                // and white spaces
                $valueExploded = explode(' ', $value);
                $value = '';
                // capitalize the first letter of each word
                foreach ($valueExploded as $word) {
                    $value .= ucfirst($word);
                }
                $arrayColumnNames[] = $value;
            }
            continue;
        } else {
            $rowIndex = $row->getRowIndex();
            reset($arrayColumnNames);
            foreach ($cellIterator as $cell) {
                $arrayData[$rowIndex][current($arrayColumnNames)] = $cell->getCalculatedValue();
                next($arrayColumnNames);
            }
        }
    }
    return array($arrayOriginalColumnNames, $arrayColumnNames, $arrayData);
}

上面的函数从excel表中读取数据到数组中。

有什么建议吗?

起初,我允许 PHP 使用 256MB 的 RAM。这还不够。然后我将数量翻了一番,然后也尝试了 1024MB。它仍然出现此错误的内存不足:

Fatal error: Allowed memory size of 1073741824 bytes exhausted (tried to allocate 50331648 bytes) in D:\data\o\WebLibThirdParty\src\PHPExcel\Classes\PHPExcel\Reader\Excel2007.php on line 688

Fatal error (shutdown): Allowed memory size of 1073741824 bytes exhausted (tried to allocate 50331648 bytes) in D:\data\o\WebLibThirdParty\src\PHPExcel\Classes\PHPExcel\Reader\Excel2007.php on line 688

【问题讨论】:

    标签: php phpexcel


    【解决方案1】:

    在 PHPExcel 论坛上有很多关于 PHPExcel 内存使用的文章;所以阅读之前的一些讨论可能会给你一些想法。 PHPExcel 拥有电子表格的“内存中”表示,并且容易受到 PHP 内存限制的影响。

    文件的物理大小在很大程度上无关紧要...了解它包含多少个单元格(每个工作表上的行*列)更为重要。

    我一直使用的“经验法则”平均约为 1k/cell,因此 5M cell 的工作簿将需要 5GB 的内存。但是,您可以通过多种方式降低该要求。这些可以结合起来,具体取决于您需要在工作簿中访问哪些信息,以及您想用它做什么。

    如果您有多个工作表,但不需要加载所有工作表,则可以使用 setLoadSheetsOnly() 方法限制 Reader 将加载的工作表。 加载单个命名工作表:

    $inputFileType = 'Excel5'; 
    $inputFileName = './sampleData/example1.xls';
    $sheetname = 'Data Sheet #2'; 
    /**  Create a new Reader of the type defined in $inputFileType  **/
    $objReader = PHPExcel_IOFactory::createReader($inputFileType);
    /**  Advise the Reader of which WorkSheets we want to load  **/ 
    $objReader->setLoadSheetsOnly($sheetname); 
    /**  Load $inputFileName to a PHPExcel Object  **/
    $objPHPExcel = $objReader->load($inputFileName);
    

    或者您可以通过传递一组名称来调用 setLoadSheetsOnly() 来指定多个工作表:

    $inputFileType = 'Excel5'; 
    $inputFileName = './sampleData/example1.xls';
    $sheetnames = array('Data Sheet #1','Data Sheet #3'); 
    /** Create a new Reader of the type defined in $inputFileType **/ 
    $objReader = PHPExcel_IOFactory::createReader($inputFileType);
    /** Advise the Reader of which WorkSheets we want to load **/ 
    $objReader->setLoadSheetsOnly($sheetnames); 
    /**  Load $inputFileName to a PHPExcel Object  **/
    $objPHPExcel = $objReader->load($inputFileName);
    

    如果您只需要访问工作表的一部分,那么您可以定义一个读取过滤器来识别您真正想要加载的单元格:

    $inputFileType = 'Excel5'; 
    $inputFileName = './sampleData/example1.xls';
    $sheetname = 'Data Sheet #3'; 
    
    /**  Define a Read Filter class implementing PHPExcel_Reader_IReadFilter  */ 
    class MyReadFilter implements PHPExcel_Reader_IReadFilter {
        public function readCell($column, $row, $worksheetName = '') {
            //  Read rows 1 to 7 and columns A to E only 
            if ($row >= 1 && $row <= 7) {
               if (in_array($column,range('A','E'))) { 
                  return true;
               }
            } 
            return false;
        }
    }
    
    /**  Create an Instance of our Read Filter  **/ 
    $filterSubset = new MyReadFilter(); 
    /** Create a new Reader of the type defined in $inputFileType **/ 
    $objReader = PHPExcel_IOFactory::createReader($inputFileType);
    /**  Advise the Reader of which WorkSheets we want to load 
         It's more efficient to limit sheet loading in this manner rather than coding it into a Read Filter  **/ 
    $objReader->setLoadSheetsOnly($sheetname); 
    echo 'Loading Sheet using filter';
    /**  Tell the Reader that we want to use the Read Filter that we've Instantiated  **/ 
    $objReader->setReadFilter($filterSubset); 
    /**  Load only the rows and columns that match our filter from $inputFileName to a PHPExcel Object  **/
    $objPHPExcel = $objReader->load($inputFileName);
    

    使用读取过滤器,您还可以读取“块”中的工作簿,这样任何时候只有一个块驻留在内存中:

    $inputFileType = 'Excel5'; 
    $inputFileName = './sampleData/example2.xls';
    
    /**  Define a Read Filter class implementing PHPExcel_Reader_IReadFilter  */ 
    class chunkReadFilter implements PHPExcel_Reader_IReadFilter {
        private $_startRow = 0;
        private $_endRow = 0;
    
        /**  Set the list of rows that we want to read  */ 
        public function setRows($startRow, $chunkSize) { 
            $this->_startRow    = $startRow; 
            $this->_endRow      = $startRow + $chunkSize;
        } 
    
        public function readCell($column, $row, $worksheetName = '') {
            //  Only read the heading row, and the rows that are configured in $this->_startRow and $this->_endRow 
            if (($row == 1) || ($row >= $this->_startRow && $row < $this->_endRow)) { 
               return true;
            }
            return false;
        } 
    }
    
    /**  Create a new Reader of the type defined in $inputFileType  **/
    $objReader = PHPExcel_IOFactory::createReader($inputFileType);
    /**  Define how many rows we want to read for each "chunk"  **/ 
    $chunkSize = 20;
    /**  Create a new Instance of our Read Filter  **/ 
    $chunkFilter = new chunkReadFilter(); 
    /**  Tell the Reader that we want to use the Read Filter that we've Instantiated  **/ 
    $objReader->setReadFilter($chunkFilter); 
    
    /**  Loop to read our worksheet in "chunk size" blocks  **/ 
    /**  $startRow is set to 2 initially because we always read the headings in row #1  **/
    for ($startRow = 2; $startRow <= 65536; $startRow += $chunkSize) { 
        /**  Tell the Read Filter, the limits on which rows we want to read this iteration  **/ 
        $chunkFilter->setRows($startRow,$chunkSize); 
        /**  Load only the rows that match our filter from $inputFileName to a PHPExcel Object  **/ 
        $objPHPExcel = $objReader->load($inputFileName); 
        //    Do some processing here 
    
        //    Free up some of the memory 
        $objPHPExcel->disconnectWorksheets(); 
        unset($objPHPExcel); 
    }
    

    如果您不需要加载格式信息,而只需要加载工作表数据,那么 setReadDataOnly() 方法将告诉读者只加载单元格值,而忽略任何单元格格式:

    $inputFileType = 'Excel5';
    $inputFileName = './sampleData/example1.xls';
    /** Create a new Reader of the type defined in $inputFileType **/ 
    $objReader = PHPExcel_IOFactory::createReader($inputFileType);
    /** Advise the Reader that we only want to load cell data, not formatting **/ 
    $objReader->setReadDataOnly(true);
    /**  Load $inputFileName to a PHPExcel Object  **/
    $objPHPExcel = $objReader->load($inputFileName);
    

    使用单元缓存。这是一种减少每个单元所需的 PHP 内存的方法,但会以速度为代价。它通过以压缩格式存储单元对象或在 PHP 内存(例如磁盘、APC、memcache)之外存储单元对象来工作……但是您节省的内存越多,脚本执行的速度就越慢。但是,您可以将每个单元所需的内存减少到大约 300 字节,因此假设的 5M 单元将需要大约 1.4GB 的 PHP 内存。

    开发者文档的第 4.2.1 节描述了单元缓存

    编辑

    查看您的代码,您正在使用效率不高的迭代器,并构建了一个单元格数据数组。您可能想查看已内置在 PHPExcel 中的 toArray() 方法,并为您执行此操作。还请查看 SO 上的 recent discussion,了解新的变体方法 rangeToArray() 以构建行数据的关联数组。

    【讨论】:

    • 谢谢。顺便说一句,toArray() 方法使用迭代器的方式与我相同,因此恕我直言,它不会更有效。
    • @Richard - toArray() 已在最新的 SVN 代码中完全重写......迭代器的缺点之一是如果它们不存在,它们会创建单元格(更慢并添加额外的内存开销)当 setIterateOnlyExistingCells(false) 时。新的 toArray() 方法不仅速度更快,而且它们也不会创建单元格。
    • 很好的答案! ReadFilter 示例对于实现一个读取和处理大块 XLSX 文件以节省内存或转换为磁盘上的平面 CSV 文件的类应该非常有帮助!
    • @MarkBaker:感谢您提供了非常丰富的答案,但是在大型工作表上实现滑动窗口迭代器并不会产生好的结果。这是当窗口大小为1K rows10K rows 时发生的情况;如您所见(第一个“GCed”行),在卸载数据后内存使用量降至“零”。但是,当加载数据时,窗口滑动得越远,就会消耗更多的内存。加载行 9K-10K 比一次加载 0-10K 需要 更多 内存。关于为什么以及如何继续进行的任何见解?
    • @MarkBaker:仅供参考,两个“GCed”行之间的唯一代码是$filter-&gt;setRows(...)$reader-&gt;load(...)
    【解决方案2】:

    我在使用 PHPExcel 以及实际上所有其他库时遇到了同样的内存问题。正如 Mark Ba​​ker 所建议的那样,分块读取数据可以解决问题(缓存也可以),但事实证明内存问题变成了时间问题。读写时间呈指数级增长,因此对于大型电子表格来说,它并不适合。

    PHPExcel 和其他工具不适合处理大文件,所以我创建了一个库来解决这个问题。你可以在这里查看:https://github.com/box/spout

    希望有帮助!

    【讨论】:

    • 如果您有任何问题,请随时在此处或直接在 Github 页面上发布内容(问题/拉取请求)。我自己以及其他开发人员将能够帮助您完成您正在尝试构建的任何内容!
    • 荣誉! @Adrien
    【解决方案3】:

    在使用 PHPExcel 时,您可以采取许多措施来减少内存。我建议您在 Apache 中修改服务器的内存限制之前采取以下措施来优化内存使用。

    /* Use the setReadDataOnly(true);*/
        $objReader->setReadDataOnly(true);
    
    /*Load only Specific Sheets*/
        $objReader->setLoadSheetsOnly( array("1", "6", "6-1", "6-2", "6-3", "6-4", "6-5", "6-6", "6-7", "6-8") );
    
    /*Free memory when you are done with a file*/
    $objPHPExcel->disconnectWorksheets();
       unset($objPHPExcel);
    

    避免使用非常大的 Exel 文件,记住文件大小会导致进程运行缓慢并崩溃。

    避免使用 getCalculatedValue();读取单元格时的函数。

    【讨论】:

      【解决方案4】:

      Ypu 可以试试 PHP Excel http://ilia.ws/archives/237-PHP-Excel-Extension-0.9.1.html 它是 php 的 C 扩展,速度非常快。 (也比 PHP 实现使用更少的内存)

      【讨论】:

      • 并且需要一个商业(199 美元)组件
      • Ilia 的代码是开源的,但它只是商业 libX 库 (libxl.com) 的 PHP 包装器... Ilia 的包装器也仅限于 Linux,还没有 Windows 或 Mac 版本,除非你'准备自己编译
      • +1 用于 libXL,虽然它是商业用途,但在处理非常大的电子表格时它是必不可少的。
      【解决方案5】:

      在我的例子中,phpexcel 总是遍历 19999 行。无论如何,实际上填充了多少行。所以 100 行数据总是以内存错误告终。

      也许您只需要检查当前行中的单元格是否为空,然后“继续”或中断循环,即迭代行。

      【讨论】:

      • 除了检查一行中的单元格是否为空的唯一方法是遍历它们
      【解决方案6】:

      只是从另一个线程重新发布我的帖子。它描述了应考虑的在服务器端生成或编辑 Excel 电子表格的不同方法。对于大量数据,我不推荐 PHPExcel 或 ApachePOI(用于 Java)之类的工具,因为它们需要内存。还有另一种非常方便(尽管可能有点繁琐)的方法可以将数据注入电子表格。可以在服务器端生成或更新 Excel 电子表格,从而进行简单的 XML 编辑。您可以将 XLSX 电子表格放在服务器上,每次从 dB 收集数据时,您都可以使用 php 解压缩它。然后,您访问特定的 XML 文件,这些文件包含需要手动注入和插入数据的工作表内容。之后,您压缩电子表格文件夹,以便将其作为常规 XLSX 文件分发。整个过程非常快速和可靠。显然,与 XLSX/Open XML 文件的内部组织相关的问题和故障很少(例如,Excel 倾向于将所有字符串存储在单独的表中,并在工作表文件中使用对该表的引用)。但是当只注入像数字和字符串这样的数据时,这并不难。如果有人有兴趣,我可以提供一些代码。

      【讨论】:

      • @Ian:你可以在这里查看:link
      【解决方案7】:

      我遇到了这个问题,不幸的是,没有一个建议的解决方案可以帮助我。我需要 PHPExcel 提供的功能(公式、条件样式等),因此不能选择使用不同的库。

      我最终所做的是将每个工作表写入一个单独的(临时)文件,然后将这些单独的文件与我编写的一些特殊软件结合起来。这将我的内存消耗从 > 512 Mb 减少到远低于 100 Mb。如果您有同样的问题,请参阅https://github.com/infostreams/excel-merge

      【讨论】:

        【解决方案8】:

        PHPExcel 已存档,从今天开始的其他选项是:

        • box/spout(或 Laravel 的 FastExcel 包装器)
        • PhpSpreadsheet

        要处理非常大的数据集(5-20​​k 行等),您需要 spout 中没有分块支持。您必须自己实现一些解决方案(例如将自定义限制写入文件,在下一次迭代中打开该文件,创建一个附加新数据的新文件......)

        另外,PhpSpreadsheet 默认支持分块。但是,它比 spout 花费更多的资源。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-02-11
          • 1970-01-01
          • 1970-01-01
          • 2016-11-23
          • 1970-01-01
          相关资源
          最近更新 更多