你可能不太需要 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找到以上测试的代码。

本帖由系统于 3个月前 自动加精
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
讨论数量: 17

读取表格,数据计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
3个月前
BradStevens

多一个选择,很好的

3个月前
GhostCoder

支持支持

3个月前

读取表格,数据计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
3个月前

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

3个月前
Outlaws

@wangchao 严谨 :+1:

3个月前
Outlaws

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

3个月前
xingchen

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

3个月前

@xingchen 您指的是Laravel Excel ?

3个月前
Zerin

菜鸟观摩

3个月前

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

3个月前

file

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

个人感觉还好

3个月前
Outlaws

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

3个月前

@Outlaws 博成兄?

3个月前
Outlaws

@王成涛 😁

3个月前

@Outlaws 幸会幸会 :bowtie:

3个月前
guanhui07

支持

1个月前
Promisehp

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

1天前

请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!