我们在工作中经常会遇到 Excel 导出的问题,比如:
表格内容的填充
下载数据过大执行超时、内存溢出
异步导出
那么,这些问题应该怎样解决呢?
1. 表格内容的填充 我们知道,在 PHP 中,ord()
可将字符转换为 ASCII 码,chr()
则可以将 ASCII 码转换为对应字符,二者为互补函数。
通过查看 Excel,我们知道一个 sheet 中,列序号是 A1, B1, C1 ...
递增的,而行序号是A2, A3, A4...
递增的。下面,就让我们用代码来生成一下Excel 中的列。
方案一 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 $rawData = [ [ 'name' => '张三' , 'gender' => ' 男' , 'age' => 19 , 'birthday' => '2000-01-01' ], [ 'name' => '李四' , 'gender' => '男' , 'age' => 29 , 'birthday' => '1990-01-01' ] ]; $sheetData = [ '姓名' => array_column ($rawData , 'name' ), '性别' => array_column ($rawData , 'gender' ), '年龄' => array_column ($rawData , 'age' ), '生日' => array_column ($rawData , 'birthday' ), ]; $excelObj = new \PHPExcel ();$sheet = $excelObj ->getActiveSheet ();$i = 65 ;foreach ($sheetData as $title => $datum ) { $row = chr ($i ++); $sheet ->setCellValue ($row . '1' , $title ); $j = 2 ; foreach ($datum as $item ) { $sheet ->setCellValueExplicit ($row . $j ++, $item ); } }
优点:可随导出内容自适应,扩展性强
缺点:表头只能兼容到A1~Z1
,超过Z1
则会触发 bug 而无法导出
方案二 针对方案一的缺点,我们来扩展表头的容量,利用PHP中的字符串递增来生成表头。
1 2 3 4 <?php for ($i = 'A' ; $i <= 'Z' ; $i ++) { echo $i . '1' , PHP_EOL; }
优点:简单快速生成表头
缺点:只能兼容到YZ1
在 Microsoft Office 365 2019 中,最大列为XFD
,所以,如果要满足 Excel 的导出,我们可以这样完全兼容:
1 2 3 4 5 <?php $alphabet = [];for ($i = 'A' ; $i < 'ZZZ' ; $i ++) { $alphabet [] = $i . '1' ; }
好了,现在我们已经能够完全满足 Excel 的列需求了,但这里还有一点小瑕疵:我们生成的所有数据都存储在数组中,这显然是对内存的极大浪费,应该按需存取。我们再来尝试另一种新的方式:
1 2 3 4 5 6 7 8 9 10 <?php function getAlphabet ( ) { for ($i = 'A' ; $i < 'ZZZ' ; $i ++) { yield $i ; } } foreach (getAlphabet () as $letter ) { echo $letter . '1' ; }
现在优雅了许多。但这种方式和上一种有什么区别呢? 请注意这段代码中的yield
关键字,这是迭代生成器。我们可以理解为每当 CPU 执行到yield
关键字时,就会暂存当前的上下文,并将控制权交回foreach
,而不像普通函数一次调用结束后销毁上下文(所以我们需要将函数所产生的变量进行返回),这让我们得以在每次需要到这段函数时,都可以从上次停下的位置继续,而无需暂存整个函数的处理结果,从而节省了内存空间。通过memory_get_usage()
函数可以看到,上一种实现方式需要1.9MB
,而yield
只需要287.6KB
,二者的空间复杂度分别为O(n)
和O(1)
。
方案三 虽然上述两种方案已经能够满足正常的需求,而且实现也足够简单。但我们还是要探索一下是否能够解决上述缺陷的方式,这样,这个问题就演变成了二十六进制的字符计数器。
思路:我们常见的有二进制、八进制、十进制、十六进制,所以我们就可以照猫画虎,写一个二十六进制。通过观察,这些进制均是末位递增,当末位为最大值时,向上一位进位,并将低位复位。那就让我们把想法转换为代码吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 <?php function getAlphabet ($data ) { $letter = []; for ($i = 65 , $count = 0 ; $i < 95 ; $i ++) { if ($i > 64 && $i < 91 ) { $lastPlace = chr ($i ); } if (90 === $i ) { $letter [] = substr (end ($letter ), 0 , -1 ) . chr ($i ); if (substr_count (end ($letter ), 'Z' ) === strlen (end ($letter ))) { $letter [] = implode ('' , ['A' , str_repeat ('A' , strlen (end ($letter )))]); } else { $letter [] = chr (ord (substr (end ($letter ), 0 , 1 )) + 1 ) . 'A' ; } $i = 65 ; } else { $letter [] = substr_replace (end ($letter ), $lastPlace , -1 , 1 ); } if (empty ($letter )) { $letter [] = 'A' ; } if ($count > count ($data )) { break ; } else { $count ++; } } return $letter ; } echo json_encode (getAlphabet (range (1 ,100 )));
emmmm,看起来离我们的目标又近了一步:可以连续生成一段字母,并且可以根据我们的数据长度自适应,但是!多位进位如何处理,如何保障多进位处理的性能问题?关于这个问题,目前的想法是参照计算机的进位实现思想 ,先挖个坑,等搞清楚来填 :)
2. 导出内容过大 很多项目初期的数据量很小,Excel 导出非常快,但随着数据量的增加,导出越来越慢,直到超时、内存溢出。怎样才能一劳永逸呢? 我们知道,PHPExcel(新版为 PhpSpreadsheet)支持向 Excel 中追加内容,所以,我们依然可以使用yield
来保证内存:比如内存可以承载 1 万条数据的容量,我们就可以使用 yield 先处理 1 万条暂存于内存中,然后再将这 1 万条追加入磁盘的 Excel 文件,这样既保证了内存的使用,有保证了处理效率。
3. 异步导出 众所周知,PHP 是阻塞式语言,从而导致一些交互后产生的长时间执行的任务难以处理。如果对于一些小型的异步任务通过 crontab 执行,则可能导致后期的定时任务越来越多而难以管理,而 PHP 中的fastcgi_finish_request()
可以使客户端结束连接后,需要大量时间运行的任务能够继续运行,正好满足我们轻量的异步执行需求。但该方式也会有一些问题,如该方式无错误重试机制。如果有大量需要异步处理的任务需要执行,建议使用 swoole 或者支持异步执行的语言。
参考资料
注意PHP对字符串的递增运算
生成器 - PHP手册
yield关键字 - PHP手册
四位计算机的原理及实现 - 阮一峰
PHP如何读取大文件