很久以前,还在用 php5.x 的时候,将数据导出为 Excel 文件需要用到 PHPExcel 扩展包。随着 php 版本更新到 7.x,PHPExcel 已过时,官方推出全新版本 PhpSpreadsheet

但无论是 PHPExcel 还是 PhpSpreadsheet,都有个弊端:内存开销极大!这在操作大文件时及其明显,脚本动不动就内存超标。针对这个问题,PhpSpreadsheet 新增了一个缓存功能,可以在操作表格时,将数据缓冲到 Redis、Memcache 等地方,见官方文档:https://phpspreadsheet.readthedocs.io/en/latest/topics/memory_saving/

近年来,开发项目一直使用 DcatAdmin 后台框架,记得框架里有个导出 Excel 的功能,但却不需要安装 PhpSpreadsheet,而是需要安装 Easy Excel,这是它的介绍:

Easy Excel是一个基于 box/spout 封装的Excel读写工具,可以帮助开发者更快速更轻松地读写Excel文件, 并且无论读取多大的文件只需占用极少的内存。

在翻阅 Esay Excel 的 Issues 列表时,发现有人建议将  box/spout 换成 openspout/openspout,它也是基于 box/spout 开发的,而 box/spout 已在2022年5月日27存档停更,openspout/openspout 一直在迭代更新。

试用 OpenSpout v4.15.0 后,发现这个库不仅读写 Excel 超级快,消耗的内存也很小。如果不是有特殊需求,强烈推荐用 OpenSpout 替代 PhpSpreadsheet。

下面是我简单封装的读写 xlsx 文件的2个函数:

/**
 * 读取XLSX文件
 * @param  string  $filePath  xlsx文件路径
 * @param  int|string  $sheetIndexOrName  工作表下标或名称
 * @return array 返回二维数组,第1维key表示第几行,第2维key表示第几列
 * @throws \OpenSpout\Common\Exception\IOException
 * @throws \OpenSpout\Reader\Exception\ReaderNotOpenedException
 */
function read_xlsx(string $filePath, int|string $sheetIndexOrName = 0): array
{
    $options = new \OpenSpout\Reader\XLSX\Options();
    // 应保留空行,可确保行号正确,因为我们用行号作为返回结果的下标
    $options->SHOULD_PRESERVE_EMPTY_ROWS = true;
    // 日期类型返回字面量而不是日期对象
    $options->SHOULD_FORMAT_DATES = true;

    $reader = new \OpenSpout\Reader\XLSX\Reader($options);
    $reader->open($filePath);

    $data = [];
    $sheetExists = false;

    foreach ($reader->getSheetIterator() as $sheet) {
        if ($sheet->getIndex() === $sheetIndexOrName || $sheet->getName() === $sheetIndexOrName) {
            $sheetExists = true;
            foreach ($sheet->getRowIterator() as $rowIndex => $row) {
                $data[$rowIndex] = [];
                $colIndex = 1;
                foreach ($row->getCells() as $cell) {
                    $data[$rowIndex][$colIndex] = $cell->getValue();
                    $colIndex++;
                }
            }
            break;
        }
    }

    if (!$sheetExists) {
        throw new \Exception("工作表“{$sheetIndexOrName}”不存在");
    }

    $reader->close();

    return $data;
}

/**
 * 保存为XLSX文件
 * @param  array  $data  数据,二维数组
 * @param  string  $filePath  文件保存路径
 * @return void
 * @throws \OpenSpout\Common\Exception\IOException
 * @throws \OpenSpout\Writer\Exception\WriterNotOpenedException
 */
function save_xlsx(array &$data, string $filePath): void
{
    $writer = new \OpenSpout\Writer\XLSX\Writer();

    $writer->openToFile($filePath);

    foreach ($data as $row) {
        $rowFromValues = \OpenSpout\Common\Entity\Row::fromValues($row);
        $writer->addRow($rowFromValues);
    }

    $writer->close();
}