你可能不太需要 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 协议》,转载必须注明作者和本文链接
推荐文章: