你可能不太需要 Laravel-Excel

项目中大规模的使用了 Laravel Excel 这个库,但运行过程中经常发现非常吃内存以及导入导出速度很慢的问题一直没有太好办法解决。

今天发现了一个新的Excel处理库 Spout,官方的介绍是

Spout is a PHP library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way. Contrary to other file readers or writers, it is capable of processing very large files while keeping the memory usage really low (less than 3MB).

所以我们今天来测试一下这个库是否真的有官方描述的这么牛逼,以及和 Laravel Excel 比较一下性能差距。

准备工作

首先我们来创建一个Laravel项目:

composer create-project --prefer-dist laravel/laravel test-excel-performance

分别安装 Laravel Excel 及我们新发现的 Spout:

composer require maatwebsite/excel

composer require box/spout

除此之外我们需要一个较大的Excel文件,我使用了一个 10000行 * 60列 的文件,没有样式,文件中随机含有空单元格,文件大小为11M左右。

我们将他命名为 test.xlsx,并把它放在 storage/public 目录下。

测试读取性能

我们先来测试两个库分别的读取性能,在框架中创建一个命令:

php artisan make:command ExcelReader

定义一个参数和一个选项

protected $signature = 'excel:reader {path} {--drive=laravel-excel}';

并定义一些需要统计的参数:

/**
 * 测试文件的行数
 * @var int
 */
private $rows = 0;

/**
 * 程序运行消耗的时间(s)
 * @var int
 */
private $timeUsage = 0;

/**
 * 程序运行消耗的内存(byte)
 * @var int
 */
private $memoryUsage = 0;

分别定义两个组件的读取文件逻辑,为了保证公平性在代码中不做任何多余的操作,而仅仅计算一下行数和单元格的数量。

首先是 Spout ,官方给出的快速起步非常简单:

private function useSpoutDrive($path)
{
    ini_set('memory_limit', -1);
    $start = now();
    $reader = ReaderFactory::create(Type::XLSX);
    $reader->setShouldFormatDates(true);
    $reader->open(storage_path('app/'.$path));
    $rows = [];
    foreach ($reader->getSheetIterator() as $sheet) {
        foreach ($sheet->getRowIterator() as $row) {
            $this->rows++;
            $rows[] = $row;
        }
    }
    $this->timeUsage = now()->diffInSeconds($start);
    $this->memoryUsage = xdebug_peak_memory_usage();
}

再来是 Laravel Excel ,3.1版本的 Laravel Excel 对 Api 进行了大量的改造,舍弃了原本层层嵌套闭包的方式,变得更加的面向对象,使用方式非常符合我的胃口。

我们跟着官方文档创建一个通用的导入类:

php artisan make:import TestImport

app/imports 目录下能看到新建了一个 TestImport 类,我们把多余的代码全部删掉,只留一个空的类,代码如下:

<?php

namespace App\Imports;

use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\ToArray;
use Maatwebsite\Excel\Concerns\ToCollection;
use Maatwebsite\Excel\Concerns\WithChunkReading;

class TestImport
{

}

然后我们处理导入逻辑时这样做:

private function useLaravelExcelDrive($path)
{
    $start = now();
    // 这段代码是用来处理导入时报的一个读取XML过大的错误,与本文无关不过多赘述
    Settings::setLibXmlLoaderOptions(LIBXML_COMPACT | LIBXML_PARSEHUGE);
    ini_set('memory_limit', -1);
    $array = Excel::toArray(new TestImport(), $path);
    $this->rows = count($array[0]);
    $this->timeUsage = now()->diffInSeconds($start);
    $this->memoryUsage = xdebug_peak_memory_usage();
}

我们完整的代码是这个样子:


<?php

namespace App\Console\Commands;

use App\Imports\TestImport;
use Box\Spout\Common\Type;
use Box\Spout\Reader\ReaderFactory;
use Illuminate\Console\Command;
use Maatwebsite\Excel\Facades\Excel;
use PhpOffice\PhpSpreadsheet\Settings;

class ExcelReader extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'excel:reader {path} {--drive=laravel-excel}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Command description';

    /**
     * 测试文件的行数
     * @var int
     */
    private $rows = 0;

    /**
     * 程序运行消耗的时间(s)
     * @var int
     */
    private $timeUsage = 0;

    /**
     * 程序运行消耗的内存(byte)
     * @var int
     */
    private $memoryUsage = 0;

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * @throws \Exception
     */
    public function handle()
    {
        $path = $this->argument('path');
        $option = $this->option('drive');

        switch ($option) {
            case 'laravel-excel':
                $this->useLaravelExcelDrive($path);
                break;
            case 'spout':
                $this->useSpoutDrive($path);
                break;
            default:
                throw new \Exception('Invalid option ' . $option);
        }

        $this->info(sprintf('共读取数据:%s 行', $this->rows));
        $this->info(sprintf('共耗时:%s秒', $this->timeUsage));
        $this->info(sprintf('共消耗内存: %sM', $this->memoryUsage / 1024 / 1024));
    }

    private function useSpoutDrive($path)
    {
        ini_set('memory_limit', -1);
        $start = now();
        $reader = ReaderFactory::create(Type::XLSX);
        $reader->setShouldFormatDates(true);
        $reader->open(storage_path('app/'.$path));
        $rows = [];
        foreach ($reader->getSheetIterator() as $sheet) {
            foreach ($sheet->getRowIterator() as $row) {
                $this->rows++;
                $rows[] = $row;
            }
        }
        $this->timeUsage = now()->diffInSeconds($start);
        $this->memoryUsage = xdebug_peak_memory_usage();
    }

    private function useLaravelExcelDrive($path)
    {
        $start = now();
        Settings::setLibXmlLoaderOptions(LIBXML_COMPACT | LIBXML_PARSEHUGE);
        ini_set('memory_limit', -1);
        $array = Excel::toArray(new TestImport(), $path);
        $this->rows = count($array[0]);
        $this->timeUsage = now()->diffInSeconds($start);
        $this->memoryUsage = xdebug_peak_memory_usage();
    }
}

代码写好后我们先来运行通过Laravel Excel读取:

php artisan excel:read public/test.xlsx

得到的结果是:

然后我们运行通过 Spout 读取的方式:

php artisan excel:read public/test.xlsx --drive=spout

两者导入的速度是差不多的但消耗的内存要相差10倍。

测试写入性能

在写入性能的测试里流程与上文类似,我们通过生成10W条用户数据来测试。

首先生成假数据的逻辑:

if (! function_exists('generate_test_data')) {
    function generate_test_data()
    {
        $faker = \Faker\Factory::create('zh_CN');
        $result = [];
        for ($i = 0; $i < 100000; $i++) {
            $arr = [
                'name' => $faker->name,
                'age' =>$faker->randomNumber(),
                'email' => $faker->email,
                'address' => $faker->address,
                'company' => $faker->company,
                'country' => $faker->country,
                'birthday' => $faker->date(),
                'city' => $faker->city,
                'creditCardNumber' => $faker->creditCardNumber,
                'street' => $faker->streetName,
                'postCode' => $faker->postcode,
            ];
            $result[] = $arr;
        }
        return $result;
    }
}

通过此函数生成出来的Excel文件大约为 9.5 MB。

通过Laravel Excel导出:

// 生成的Export类

namespace App\Exports;

use Faker\Factory;
use Maatwebsite\Excel\Concerns\FromArray;
use Maatwebsite\Excel\Concerns\FromCollection;

class TestExport implements FromArray
{
    /**
     * @return array
     */
    public function array(): array
    {
        return generate_test_data();
    }
}

// 导出逻辑
private function useLaravelExcelDrive()
{
    ini_set('memory_limit', -1);
    $start = now();
    Excel::store(new TestExport(), 'testWriter.xlsx');
    $this->timeUsage = now()->diffInSeconds($start);
    $this->memoryUsage = xdebug_peak_memory_usage();
}

通过Spout导出:

private function useSpoutDrive()
{
    ini_set('memory_limit', -1);
    $start = now();
    $writer = WriterFactory::create(Type::XLSX);
    $writer->openToFile(storage_path('app/testWriter.xlsx'));
    $writer->addRows(generate_test_data());
    $writer->close();
    $this->timeUsage = now()->diffInSeconds($start);
    $this->memoryUsage = xdebug_peak_memory_usage();
}

最终的测试结果如下:

结论

以上测试由于两个包的差异性,或许测试的环境不是完全一致,但能一定程度上的说明问题。

综上所述,Laravel Excel 因为封装了过多的流程和操作,在性能上处于绝对的下风,如果你的项目里有大量的数据导入导出需求,且对性能有一定要求的情况下不妨尝试一下替换掉Laravel Excel。

如果你对性能没有太高的要求或项目中的数据没有到达一定量级,Laravel Excel 仍然是最好的选择 (新版本的写法实在是非常符合我的胃口)。

最后祝大家 good good coding, day day up。

over~

注:可以在https://github.com/lybc/excel-process-test找到以上测试的代码。

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 5年前 自动加精
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 41

读取表格,数据计29068行,7列

$ php artisan excel:reader public/test.xlsx --drive=spout
共读取数据:29068 行
共耗时:63秒
共消耗内存: 33.287582397461M
$ php artisan excel:reader public/test.xlsx --drive=laravel-excel
共读取数据:29068 行
共耗时:47秒
共消耗内存: 146.36081695557M

执行了几次,耗时如下:

  • Laravel-Excel耗时:47-50秒
  • Spout耗时:63-65秒

内存使用基本不变:

  • Laravel-Excel使用内存146M
  • Spout使用内存33M

导出测试

  • 使用mk-j/PHP_XLSXWriter

    #第一次
    $ php artisan excel:writer --drive=PHP_XLSXWriter
    共导出数据:100000 行
    共耗时:156秒
    共消耗内存: 150.8012008667M
    # 第二次
    $ php artisan excel:writer --drive=PHP_XLSXWriter
    共导出数据:100000 行
    共耗时:257秒
    共消耗内存: 150.82746887207M
  • 使用box/spout

    # 第一次
    $ php artisan excel:writer --drive=spout
    共导出数据:100000 行
    共耗时:318秒
    共消耗内存: 151.07627105713M
    # 第二次
    $ php artisan excel:writer --drive=spout
    共导出数据:100000 行
    共耗时:316秒
    共消耗内存: 151.08995056152M
  • 使用maatwebsite/excel

    $ php artisan excel:writer --drive=laravel-excel
    共导出数据:100000 行
    共耗时:982秒
    共消耗内存: 1017.810295105M
5年前 评论

多一个选择,很好的

5年前 评论
阿麦

支持支持

5年前 评论

读取表格,数据计29068行,7列

$ php artisan excel:reader public/test.xlsx --drive=spout
共读取数据:29068 行
共耗时:63秒
共消耗内存: 33.287582397461M
$ php artisan excel:reader public/test.xlsx --drive=laravel-excel
共读取数据:29068 行
共耗时:47秒
共消耗内存: 146.36081695557M

执行了几次,耗时如下:

  • Laravel-Excel耗时:47-50秒
  • Spout耗时:63-65秒

内存使用基本不变:

  • Laravel-Excel使用内存146M
  • Spout使用内存33M

导出测试

  • 使用mk-j/PHP_XLSXWriter

    #第一次
    $ php artisan excel:writer --drive=PHP_XLSXWriter
    共导出数据:100000 行
    共耗时:156秒
    共消耗内存: 150.8012008667M
    # 第二次
    $ php artisan excel:writer --drive=PHP_XLSXWriter
    共导出数据:100000 行
    共耗时:257秒
    共消耗内存: 150.82746887207M
  • 使用box/spout

    # 第一次
    $ php artisan excel:writer --drive=spout
    共导出数据:100000 行
    共耗时:318秒
    共消耗内存: 151.07627105713M
    # 第二次
    $ php artisan excel:writer --drive=spout
    共导出数据:100000 行
    共耗时:316秒
    共消耗内存: 151.08995056152M
  • 使用maatwebsite/excel

    $ php artisan excel:writer --drive=laravel-excel
    共导出数据:100000 行
    共耗时:982秒
    共消耗内存: 1017.810295105M
5年前 评论

阅罢此文,去laravel-excel官网看了一下文档,果然,新版本的laravel-excel已经不是以前的laravel-excel了,没有了各种回调,变得特别的laravel,味道很好的样子

5年前 评论

@ColderWinter 我特别喜欢新版本的使用方式

5年前 评论
xingchen

版本更新了,也更加耗费资源了,
2.1版本的6W行数据 cpu占用30%
3.x版本的6w行数据 cpu占用100%

5年前 评论

laravel-excel 3.1中文文档可否有大神翻译下

5年前 评论
mengdodo

file

我在一个项目里头用的是这个
rap2hpoutre/fast-excel

个人感觉还好

5年前 评论

@mengdodo 这个也是基于 spout 的一个封装 😄

5年前 评论

@Outlaws 博成兄?

5年前 评论

@Outlaws 幸会幸会 :bowtie:

5年前 评论
guanhui07

支持

5年前 评论

@Outlaws 想问下,如何导出到指定模板的 Excel 呢?

5年前 评论

:+1: :+1: :+1:

4年前 评论

php artisan make:import TestImport
Command "make:import" is not defined.
请问 这是啥问题?

4年前 评论
Outlaws (楼主) 4年前

请教下,当title为中文时,出现了在本地运行没问题,但是在服务器端没有文件名的问题。源码如下:

$writer = WriterFactory::create(Type::XLSX);
            $writer->openToBrowser($title);
            $writer->addRows($content);
            $writer->close();
4年前 评论
Outlaws (楼主) 4年前

我使用laravel excel3.1版本导出1.5w条数据,大概是1.5M,速度10s,感觉很慢啊,,之前使用2.1版本才花费3秒左右??升级版性能确实很有问题啊,,怎么优化呢?

4年前 评论
jasonjiang123 4年前
jasonjiang123 4年前

php artisan make:import 这个命令哪里来的

4年前 评论
未进化的类人猿

大文件的话还是生成器好一些。

4年前 评论

要想速度快,还是要看 https://github.com/viest/php-ext-xlswriter

4年前 评论

感觉 对比的方式不对,一个是一行行读取计算,一个是直接整个excel导入为一个大数组,不用想是消耗内存的

3年前 评论
未进化的类人猿 3年前

Laravel Excel 名字取得好,沾了 laravel 的光

3年前 评论

今天在项目中试用了下 spout,因为 laravel-excel 会超出内存,所以想看看这个能不能用

  'driver' => 'spout',
  'memory' => 79247368,
  'time' => 29.124168872833252,

  // 
  'driver' => 'laravel-excel',
  'memory' => 136286144,
  'time' => 36.768759965896606,

2万多行,7列。 虽然 spout 胜出,但是,spout 不支持单元格合并,格式化值,列宽设置 等,所以它的胜出只是适合简单导入导出,CSV最适合不过。

最后还是哀叹的调大点内存限制。

spout 在使用上还是很顺手,简单明了,后续有适合的业务,可能会选择。

3年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!