是什么导致 PHPExcel 在使用分块过滤器读取文件时使用如此多的内存?

What causes PHPExcel to use so much memory when reading a file while using a chunked filter?

像许多其他人一样,我在读取文件(将其转换为 MySQL)时一直在为 PHPExcel 内存使用而苦苦挣扎。

当然我已经尝试了各个地方提到的通常的东西并且能够将内存效率提高至少40%。这包括使用自定义分块 reader class、将分块 reader 实例化移出读取循环等。

我的测试服务器上有 16G RAM,并在 PHP 中分配了 2G 最大内存使用量。对于 ~200K 行以下的文件,PHPExcel 将起作用(缓慢但肯定)。一旦超过一定大小,脚本就会失败,只是将 "Killed" 输出到 shell。日志显示内核终止了 PHP 因为它使用了太多内存。在使用 top 命令观察 CPU 和内存使用情况时,我可以看到可用内存和可用交换空间直线下降,而使用的内存和使用的交换空间则猛增。

在阅读了很多关于 PHPExcel 的内容并查看了一些源文件后,我得出的结论是每个单元格都存储了大量数据,而这些数据在处理时并不需要只有文字。使用:

$objReader->setReadDataOnly(true);

有点帮助,但实际上并没有那么多......但是,使用分块 reader 并将块大小设置为较小的值然后使用 unset() 清理大变量理论上应该工作。我知道 PHPExcel 每次都必须读取整个文件,但它不应该存储在内存中,对吗?

这是我目前使用的代码:

<?php

date_default_timezone_set("America/New_York");
set_time_limit(7200);
ini_set('memory_limit', '2048M');

include_once("classes/PHPExcel/PHPExcel/IOFactory.php");

$inputFileName = "/PATH/TO/FILE.xlsx";
$inputFileType = PHPExcel_IOFactory::identify($inputFileName);
$worksheetName = "Sheet1";

class chunkReadFilter implements PHPExcel_Reader_IReadFilter
{
    private $_startRow = 0;
    private $_endRow = 0;

    public function __construct($startRow, $chunkSize)
    {
        $this->_startRow = $startRow;
        $this->_endRow = $startRow + $chunkSize;
    }

    public function setRows($startRow, $chunkSize)
    {
        $this->_startRow = $startRow;
        $this->_endRow   = $startRow + $chunkSize;
    }

    public function readCell($column, $row, $worksheetName = '')
    {
        if (($row == 1) || ($row >= $this->_startRow && $row < $this->_endRow))
        {
            return true;
        }
        return false;
    }
}


$objReader = PHPExcel_IOFactory::createReader($inputFileType);
$objReader->setReadDataOnly(true);

$chunkSize = 1000;

echo "Got here 1\n";

$chunkFilter = new chunkReadFilter(2,$chunkSize);


for ($startRow = 2; $startRow <= 378767; $startRow += $chunkSize)
{
    $chunkFilter->setRows($startRow, $chunkSize);
    $objReader->setReadFilter($chunkFilter);
    echo "Got here 2\n";

    $objPHPExcel = $objReader->load($inputFileName);
    echo "Got here 3\n";

    $sheet = $objPHPExcel->getSheetByName($worksheetName);
    echo "Got here 4\n";

    $highestRow = $sheet->getHighestRow(); 
    $highestColumn = $sheet->getHighestColumn();
    echo "Got here 5\n";

    $sheetData = $sheet->rangeToArray("A".$startRow.":".$highestColumn.$highestRow, NULL, TRUE, FALSE);
    print_r($sheetData);
    echo "\n\n";
}

?>

输出:

[USER@BOX Directory]# php PhpExcelBigFileTest.php
Got here 1
Got here 2
Killed

这引出了一个问题:PHPExcel 是否试图将整个文件加载到内存中而不考虑我的过滤器?如果是,为什么 PHP 不在 2G 内存使用时停止它,而是让它变得如此糟糕以至于内核不得不杀死 PHP?

PHPExcel 目前使用 SimpleXML 读取基于 XML 的格式,例如 OfficeOpenXML (xlsx)、OASIS (.odc)和 Gnumeric 而不是内存效率更高的 XMLReader。这意味着压缩存档中的每个 XML 文件都直接加载到 PHP 内存中进行解析,并构建 PHPExcel 对象。虽然单元分块通过将单元格数量减少到读取过滤器定义的单元格数量来减少 PHPExcel 对象使用的内存,但它仍然需要将整个文件加载到内存中以实现简单XML 来解析它。

开发团队研究了直接从压缩存档到 PHP 的拉式解析器 XMLReader 的流式传输数据,初步实验表明这具有很高的内存效率;但它也是一个主要的代码重写,以重构电子表格阅读器以使用此方法;由于开发资源和可用时间有限,这不是一项可以轻易完成的任务。

除了通过仅将单元格子集加载到 PHPExcel 对象来减少内存之外,您可能还想查看单元格缓存。 documentation 中对此进行了描述,并允许以减少它们占用的内存的方式存储单元格对象。提供了不同的方法以适应不同的系统,节省的内存量将根据 PHP 版本和配置而有所不同,因此您需要确定哪种方法最适合您自己的系统。使用单元格缓存也会以速度为代价。通常,SQLite 是内存效率最高的方法,但也是最慢的方法之一。