你可能不太需要 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 协议》,转载必须注明作者和本文链接
高认可度评论:
读取表格,数据计29068行,7列
执行了几次,耗时如下:
内存使用基本不变:
导出测试
使用mk-j/PHP_XLSXWriter
使用box/spout
使用maatwebsite/excel
多一个选择,很好的
支持支持
读取表格,数据计29068行,7列
执行了几次,耗时如下:
内存使用基本不变:
导出测试
使用mk-j/PHP_XLSXWriter
使用box/spout
使用maatwebsite/excel
阅罢此文,去laravel-excel官网看了一下文档,果然,新版本的laravel-excel已经不是以前的laravel-excel了,没有了各种回调,变得特别的laravel,味道很好的样子
@wangchao 严谨 :+1:
@ColderWinter 我特别喜欢新版本的使用方式
版本更新了,也更加耗费资源了,
2.1版本的6W行数据 cpu占用30%
3.x版本的6w行数据 cpu占用100%
@xingchen 您指的是Laravel Excel ?
菜鸟观摩
laravel-excel 3.1中文文档可否有大神翻译下
我在一个项目里头用的是这个
rap2hpoutre/fast-excel
个人感觉还好
@mengdodo 这个也是基于 spout 的一个封装 😄
@Outlaws 博成兄?
@王成涛 😁
@Outlaws 幸会幸会 :bowtie:
支持
@Outlaws 想问下,如何导出到指定模板的
Excel
呢?:+1: :+1: :+1:
php artisan make:import TestImport
Command "make:import" is not defined.
请问 这是啥问题?
请教下,当title为中文时,出现了在本地运行没问题,但是在服务器端没有文件名的问题。源码如下:
我使用laravel excel3.1版本导出1.5w条数据,大概是1.5M,速度10s,感觉很慢啊,,之前使用2.1版本才花费3秒左右??升级版性能确实很有问题啊,,怎么优化呢?
php artisan make:import 这个命令哪里来的
大文件的话还是生成器好一些。
要想速度快,还是要看
https://github.com/viest/php-ext-xlswriter
感觉 对比的方式不对,一个是一行行读取计算,一个是直接整个excel导入为一个大数组,不用想是消耗内存的
Laravel Excel 名字取得好,沾了 laravel 的光
今天在项目中试用了下 spout,因为 laravel-excel 会超出内存,所以想看看这个能不能用
2万多行,7列。 虽然 spout 胜出,但是,spout 不支持单元格合并,格式化值,列宽设置 等,所以它的胜出只是适合简单导入导出,CSV最适合不过。
最后还是哀叹的调大点内存限制。
spout 在使用上还是很顺手,简单明了,后续有适合的业务,可能会选择。
spout好久没更新了