前言
一说到php excel
数据读取,一般都会想到phpspreadsheet
这个扩展。但从它的前身phpexcel
以来读取大文件时的性能一直都是问题。
一次偶然的情况下发现了.xlsx
后缀的Excel文件原来也是一个压缩包(可能是众所周知),观察了解压出来的文件,发现存储数据的均为xml
格式的文件。
前阵子项目中遇到大文件导入phpspreadsheet
频繁导致内存超出报错,在百度谷歌没有找到替代品的情况家,遂决定自己手撸。
实现思路
结构
解压缩出来的文件结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| _rels └ .rels docProps └ app.xml └ core.xml xl └ _rels └ workbook.xml.rels └ worksheets └ sheet1.xml └ sheet2.xml ... └ sharedStrings.xml └ styles.xml └ workbook [Content_Types].xml
|
其中比较重要的有:
worksheets
文件夹下的sheet.xml
用于存储每个sheet
内的结构及数据
sharedStrings.xml
用于存储sheet
内的字符串格式的数据,相当于是一张字典表
解包
既然文件是压缩包,那么首要的问题就是解包,先解出来相关的xml
文件。php
原生扩展zip
的类ZipArchive
即可满足,相关语法:
1 2 3 4 5 6 7 8 9 10
| $zip = new \ZipArchive;
$zip->open($filename);
$zip->extractTo($destination, $entries);
|
解析XML
接下来就需要解析XML的数据了。使用的依旧是原生扩展SimpleXML
的simplexml_load_file()
方法,将XML文件解析为SimpleXMLElement
对象。由于SimpleXMLElement
实现了RecursiveIterator
接口,所以我们可以遍历解析结果读取数据。
1 2 3 4 5 6 7 8 9 10 11
|
$xmlData = simplexml_load_file($filename);
|
存储解析数据
如果将全部的数据存储到数组或者对象里,势必还是会导致内存飙升。由于使用的框架是laravel
,所以使用惰性集合LazyCollection
来存储解析数据,降低内存使用率。这里可以参考laravel
的官方文档惰性集合,大致的使用方式如下:
1 2 3 4 5 6 7 8 9 10 11
| use Illuminate\Support\LazyCollection;
$xmlData = simplexml_load_file($filename);
LazyCollection::make(function () use ($xmlData) { foreach ($xmlData as $node) { .... yield $data; } });
|
如果非laravel框架或较低版本的laravel框架,在没有LazyCollection
的情况下,可以考虑使用yield
自己封装
封装
基于以上思路及知识,接下来就可以简单封装一个解析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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
| <?php
namespace App\Services;
use Illuminate\Support\LazyCollection;
class ExcelReader {
const SUBDAYS = 25569; const UZIPPAH = 'uzip'; const MAPNAME = 'xl/sharedStrings.xml'; const SHEETNAME = 'xl/worksheets/sheet%d.xml';
public static function read($filename, $sheetNumber = 1, $withTitle = true) { $pathinfo = pathinfo($filename);
$uzipPath = storage_path(static::UZIPPAH . '/' . $pathinfo['filename']); $sheetName = sprintf(static::SHEETNAME, $sheetNumber);
$zip = new \ZipArchive; try { $zip->open($filename); $zip->extractTo($uzipPath, [ static::MAPNAME, $sheetName ]); $zip->close(); } catch (\Throwable $th) { Log::error('文件解压缩失败', [$filename]); throw $th; }
try { $mapOrigin = simplexml_load_file($uzipPath . '/' . static::MAPNAME); $maps = array(); foreach ($mapOrigin->si as $s) { if (isset($s->r)) { $si = ''; foreach ($s->r as $sr) { $si .= $sr->t; } } else { $si = $s->t . ''; } $maps[] = $si; } } catch (\Throwable $th) { Log::error('字典解析失败', [ $filename, $uzipPath . '/' . static::MAPNAME, $th->getMessage() ]); throw $th; }
try { $sheetXmls = simplexml_load_file($uzipPath . '/' . $sheetName); $sheetDatas = LazyCollection::make(function () use ($withTitle, $sheetXmls, $maps) { $titles = array(); foreach ($sheetXmls->sheetData->row as $row) { $data = array(); foreach ($row->c as $col) { $column = trim($col['r'], $row['r']); $withTitle && $titles AND $column = $titles[$column] ?? $column; switch ($col['t'] . '') { case 's': $data[$column] = $maps[intval($col->v)] ?? ''; break; case 'str': $data[$column] = false; case 'inlineStr': $data[$column] = $col->is->t . ''; break; default: $data[$column] = $col->v . ''; break; } } if ($withTitle && $row['r'] == 1) { $titles = $data; } else { yield $data; } } }); } catch (\Throwable $th) { Log::error('数据解析失败', [ $filename, $uzipPath . '/' . $sheetName, $th->getMessage() ]); throw $th; }
@unlink($uzipPath . '/' . static::MAPNAME); @unlink($uzipPath . '/' . $sheetName);
return $sheetDatas; } }
|
使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
$xmlData = ExcelReader::read('./demo1.xlsx', 1); foreach ($xmlData as $row) { dd($row['姓名']); }
$xmlData = ExcelReader::read('./demo2.xlsx', 1, false); foreach ($xmlData as $row) { dd($row['A'], $row['AB']); }
dd($xmlData->count());
|
遗留问题
- Excel的日期格式存储的值是一个数字,值为1900年至今的天数,而程序里的时间戳起始时间是1970年,所以如果自己转换日期需要用这个值减去25569天(即常量
ExcelReader::SUBDAYS
)。
- 由于使用了
yield
数据并没有完整加载到内存中,所以Excel的公式没有办法计算(跨行)
参考文档
https://www.php.net/manual/zh/book.zip.php
https://www.php.net/manual/zh/book.simplexml.php
https://www.php.net/manual/zh/language.generators.syntax.php
https://learnku.com/docs/laravel/9.x/collections/12225#lazy-collections