PhpSpreadsheet 是一个遵循最新 PSR 标准、完全使用 PHP 编写的用于处理 Excel 表格的第三方库,其前身是 PHPExcel,但 PHPExcel 由于历史技术债的问题被放弃维护。本文基于 PHP 7.4 和 PhpSpreadsheet 1.16.0 的官方文档选取翻译介绍部分使用方法。
数据类型
Excel 的数据类型
在微软的 Microsoft Office Excel 中,支持以下几种形式的数据类型:
string:字符串
number:数字
boolean:布尔值(正确/错误)
null:空
formula:公式
error:错误
Inline string / rich text:富文本,例如超链接、设置背景颜色
PhpSpreadsheet 将 Excel 转换到 PHP 数据类型的一些默认规则
通常情况下,使用 setCellValue()
方法或者 setValue()
方法为表格赋值,PhpSpreadsheet 会自动的将 PHP 数据类型以恰当类型的填入表格,组件预置了一些转换设置,可以通过修改预置的转换设置(value binder)来达到想要的转换效果。以下为部分的预置转换设置:
纯数字类型的字符串会被转换为数字(numbers)
=
开头的字符串会被转换为公式(formula)
分数如 3/4 会被转换为小数 0.75
百分比如 5% 会被转换为小数 0.05
日期会被转换为 Excel 专有的日期时间戳格式(浮点型)
以科学计数法存储的字符串会被转换为数字
\n
为该单元格内的换行符
True 或者 False 会被转换为布尔值
自定义数据类型转换
组件默认使用内置的 DefaultValueBinder
转换类,组件库中有内置了更多处理类型的 AdvancedValueBinder
转换类可供选择,还有用于字符串的 StringValueBinder
转换类。
1 2 3 4 5 6 7 8 9 10 11 \PhpOffice\PhpSpreadsheet\Cell\Cell ::setValueBinder ( new \PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder () );$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet ();$sheet = $spreadsheet ->getActiveSheet ();$sheet ->setCellValue ('A4' , 'Percentage value:' ); $sheet ->setCellValue ('B4' , '10%' ); $sheet ->setCellValue ('A5' , 'Date/time value:' ); $sheet ->setCellValue ('B5' , '21 December 1983' );
当自带的类型转换器无法满足需求时,可以自定义类型转换器来满足特定需求。有两种自定义方法,一种是实现接口 \PhpOffice\PhpSpreadsheet\Cell\IValueBinder
,另外一种是继承重写原有的几个转换类 \PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder 、 \PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder
、\PhpOffice\PhpSpreadsheet\Cell\StringValueBinder
1 2 3 4 5 6 7 8 9 10 11 12 13 class SelfValueBinder implements IValueBinder { public function bindValue (Cell $cell , $value ) { } } class SelfValueBinder extends AdvancedValueBinder { public function bindValue (Cell $cell , $value ) { } }
如果遇到长数字(如身份证号)在 Excel 文档中被记为科学计数法的方式,可以尝试使用下面这个自定义 ValueBinder。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 use PhpOffice \PhpSpreadsheet \Cell \AdvancedValueBinder ;use PhpOffice \PhpSpreadsheet \Cell \DataType ;class CustomValueBinder extends AdvancedValueBinder { public static function dataTypeForValue ($value ): string { if (is_null ($value )) { return DataType ::TYPE_NULL ; } elseif ($value instanceof \PhpOffice\PhpSpreadsheet\RichText\RichText) { return DataType ::TYPE_INLINE ; } elseif (is_string ($value ) && $value [0 ] === '=' && strlen ($value ) > 1 ) { return DataType ::TYPE_FORMULA ; } elseif (is_bool ($value )) { return DataType ::TYPE_BOOL ; } elseif (is_float ($value ) || is_int ($value )) { return DataType ::TYPE_NUMERIC ; } return DataType ::TYPE_STRING ; } }
读取/写入文件
读取文件
相比于专有的读写类。IOFactory 类的优势在于通用方法判断文件类型到底是早期的 Xls 还是 后续的 Xlsx 格式,不需要再自己定义不同格式调用不同的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 $reader = \PhpOffice\PhpSpreadsheet\IOFactory ::createReader ('Xlsx' ); $reader ->setReadDataOnly (TRUE ); $spreadsheet = $reader ->load ("test.xlsx" ); $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory ::load ('test.xlsx' );$reader = \PhpOffice\PhpSpreadsheet\IOFactory ::createReaderForFile ("test.xlsx" ); $reader ->setReadDataOnly (true );$reader ->load ("test.xlsx" );
专用 Reader 类需要自行判断文件是来自早期的 Binary XLS 或者现代的 OpenOfficeXML XLSX。读取文件时还可以设置只读属性,加快读取速度。
1 2 3 4 5 6 7 8 9 10 11 12 13 $reader = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx ();$spreadsheet = $reader ->load ("test.xlsx" );$reader ->setReadDataOnly (true );$spreadsheet = $reader ->load ("test.xlsx" );$reader ->setLoadSheetsOnly (["Sheet 1" , "Sheet 2" ]);$spreadsheet = $reader ->load ("test.xlsx" );
写入文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $spreadsheet = new Spreadsheet ();$writer = \PhpOffice\PhpSpreadsheet\IOFactory ::createWriter ($spreadsheet , "Xlsx" );$writer ->save ("test.xlsx" );$spreadsheet ->disconnectWorksheets ();unset ($spreadsheet );
1 2 3 4 5 6 7 8 9 10 11 12 $spreadsheet = new Spreadsheet ();$writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx ($spreadsheet );header ('Content-Type: application/vnd.ms-excel' ); header ('Content-Disposition: attachment;filename="' .'文件名.xlsx"' ); header ('Cache-Control: max-age=0' ); $writer ->save ('php://output' );$spreadsheet ->disconnectWorksheets ();unset ($spreadsheet );
链式操作 链式操作可以顺畅使用一些接口,使代码简洁利于维护。同时,因为减少了方法的重复调用,还可以一定程度的提高性能,很多的框架中都存在链式操作的使用。假如不使用链式操作,那么定义文档附加信息是这样的:
1 2 3 4 5 6 7 $spreadsheet ->getProperties ()->setCreator ("Maarten Balliauw" ); $spreadsheet ->getProperties ()->setLastModifiedBy ("Maarten Balliauw" ); $spreadsheet ->getProperties ()->setTitle ("Office 2007 XLSX Test Document" ); $spreadsheet ->getProperties ()->setSubject ("Office 2007 XLSX Test Document" ); $spreadsheet ->getProperties ()->setDescription ("Test document" ); $spreadsheet ->getProperties ()->setKeywords ("office 2007 openxml php" ); $spreadsheet ->getProperties ()->setCategory ("Test result file" );
可以看见上面的代码反复调用了 $spreadsheet->getProperties()
方法,而使用链式操作的话,代码就可以简化成这样。
1 2 3 4 5 6 7 8 $spreadsheet ->getProperties () ->setCreator ("Maarten Balliauw" ) ->setLastModifiedBy ("Maarten Balliauw" ) ->setTitle ("Office 2007 XLSX Test Document" ) ->setSubject ("Office 2007 XLSX Test Document" ) ->setDescription ("Test document." ) ->setKeywords ("office 2007 openxml php" ) ->setCategory ("Test result file" );
读取表格数据 读取数据需要使用读取文件方法(IO)载入整个文件,否则读取数据方法无法拿到数据,由于是整个文件载入内存,需要注意服务器的内存占用避免导致内存溢出无法提供服务。同时应注意校验文件的安全性,避免出现安全问题。
读取普通数据
1 2 3 4 5 6 7 8 9 10 11 $spreadsheet = new Spreadsheet (); $spreadsheet ->getActiveSheet () ->getCell ('A1' ) ->getValue (); $spreadsheet ->getActiveSheet () ->getCellByColumnAndRow (1 , 5 ) ->getValue ();
得到公式计算后的数据
1 2 3 4 5 6 7 8 9 10 $spreadsheet = new Spreadsheet (); $spreadsheet ->getActiveSheet () ->getCell ('A4' ) ->getCalculatedValue (); $spreadsheet ->getActiveSheet () ->getCellByColumnAndRow (1 , 4 ) ->getCalculatedValue ();
公式的处理、缓存
可以使用 getCalculatedValue
方法来获得公式计算后的结果。如果想储存一些以 =
开头的数据,则需要将该单元格标记为文本的类型,只要将该单元格标记为文本后即使使用 getCalculatedValue
方法求值也只会返回表达式,而不是求值结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 $spreadsheet = new Spreadsheet (); $spreadsheet ->getActiveSheet () ->setCellValue ( 'A4' , '=IF(A3, CONCATENATE(A1, " ", A2), CONCATENATE(A2, " ", A1))' ); $spreadsheet ->getActiveSheet () ->getCell ('A4' ) ->getStyle () ->setQuotePrefix (true );
因为公式的计算可能会占用大量的硬件资源和时间,在读取表格公式计算值之后如果没有释放资源,公式计算结果是会被缓存的,可以设置在本次读取中禁用缓存或者清除缓存。
1 2 3 4 5 6 7 8 9 10 Calculation ::getInstance ($spreadsheet ) ->disableCalculationCache (); Calculation ::getInstance ($spreadsheet ) ->clearCalculationCache (); $writer = new \PhpOffice\PhpSpreadsheet\Writer\Html ($spreadsheet );$writer ->setPreCalculateFormulas (false );$writer ->save ("test.xlsx" );
返回格式化后的值(日期时间等)
1 2 3 4 $spreadsheet = new Spreadsheet (); $spreadsheet ->getActiveSheet () ->getCell ('A6' ) ->getFormattedValue ();
返回 PHP 数组
1 2 3 4 5 6 7 8 9 10 11 12 13 $spreadsheet = new Spreadsheet (); $spreadsheet ->getActiveSheet () ->rangeToArray ( 'C3:E5' , // 从C3到E5的范围 NULL , // 如果读取到NULL 则该单元格置空 TRUE , // 是否计算公式,TRUE 返回值,FALSE 返回公式本身 TRUE , // 是否对单元格进行统一的格式化 TRUE // 是否按照行、列来索引单元格 );
命名范围(named range)的设置
作用:给某个单元格或者某个范围的单元格命名,方便后续调用
1 2 3 4 $spreadsheet ->addNamedRange ( new \PhpOffice\PhpSpreadsheet\NamedRange ('TAX_RATE' , $worksheet , '=$B$1' ) ); $spreadsheet ->addNamedRange ( new \PhpOffice\PhpSpreadsheet\NamedRange ('PRICE' , $worksheet , 'C3:E5' ) );
根据行列遍历封装的读取数据方法
1 2 3 4 5 6 7 8 9 10 11 12 $sheet = $spreadsheet ->getActiveSheet ();foreach ($sheet ->getRowIterator () as $row ) { $cellIterator = $row ->getCellIterator (); $cellIterator ->setIterateOnlyExistingCells (FALSE ); foreach ($cellIterator as $cell ) { $value = $cell ->getValue (); } }
根据二维坐标遍历封装的读取数据方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 $reader = \PhpOffice\PhpSpreadsheet\IOFactory ::createReader ('Xlsx' ); $reader ->setReadDataOnly (TRUE ); $spreadsheet = $reader ->load ("test.xlsx" ); $sheet = $spreadsheet ->getActiveSheet ();$highestRow = $sheet ->getHighestRow (); $highestColumn = $sheet ->getHighestColumn (); $highestColumnIndex = \PhpOffice\PhpSpreadsheet\Cell\Coordinate ::columnIndexFromString ($highestColumn ); for ($row = 1 ; $row <= $highestRow ; ++$row ) { for ($col = 1 ; $col <= $highestColumnIndex ; ++$col ) { $value = $sheet ->getCellByColumnAndRow ($col , $row ) ->getValue (); } }
基于 Excel 坐标遍历封装的读取数据方法
如果不想进行坐标转换,也可以直接使用 Excel 坐标进行遍历。当列超过 Z 时会重新以 AA 开始计数,而 AA 又会被判断为小于 B ,所以这时候需要修改循环条件,把 <=
改为 !=
作为循环的条件判断。不过这样可能出现初始值超过导致溢出的问题,所以不建议使用 Excel 坐标遍历。可根据实际需求定义循环条件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 $reader = \PhpOffice\PhpSpreadsheet\IOFactory ::createReader ('Xlsx' ); $reader ->setReadDataOnly (TRUE ); $spreadsheet = $reader ->load ("test.xlsx" ); $sheet = $spreadsheet ->getActiveSheet ();$highestRow = $sheet ->getHighestRow ();$highestColumn = $sheet ->getHighestColumn ();$highestColumn ++;for ($row = 1 ; $row <= $highestRow ; ++$row ) { for ($col = 'A' ; $col != $highestColumn ; ++$col ) { $value = $sheet ->getCell ($col . $row ) ->getValue (); } }
写入表格数据 写入数据后需要使用写入文件方法(IO)来保存文件,如果不需要保存在服务器上的话可以将文件输出到 PHP 缓冲区,并且指定 HTTP Header 让浏览器触发下载文件或者储存文件的行为,以此实现直接导出下载的效果。
普通数据
1 2 3 4 5 6 7 8 9 10 11 12 use PhpOffice \PhpSpreadsheet \Spreadsheet ;$spreadsheet = new Spreadsheet (); $sheet = $spreadsheet ->getActiveSheet (); $sheet ->setCellValue ('A1' , 'Hello World !' );$sheet ->getCell ('B8' )->setValue ('Some value' );$sheet ->setCellValueByColumnAndRow (1 , 5 , 'PhpSpreadsheet' );
日期
正常的时间戳为整数,而 Excel 采用了其特有的浮点型时间戳。写入时间戳时需要进行转换。组件内置了 Date::PHPToExcel
方法提供时间戳的转换。
1 2 3 4 5 6 7 8 9 10 11 12 13 $spreadsheet = new Spreadsheet (); $dateTimeNow = time (); $excelDateValue = \PhpOffice\PhpSpreadsheet\Shared\Date ::PHPToExcel ($dateTimeNow ); $spreadsheet ->getActiveSheet () ->setCellValue ('A6' ,$excelDateValue ); $spreadsheet ->getActiveSheet () ->getStyle ('A6' ) ->getNumberFormat () ->setFormatCode (\PhpOffice\PhpSpreadsheet\Style\NumberFormat ::FORMAT_DATE_DATETIME );
储存0开头的数字
有两种方法:一是将单元格格式设置为字符;二是规定单元格的格式及长度,若长度不满则自动做0填充。
1 2 3 4 5 6 7 8 9 10 11 12 13 $spreadsheet = new Spreadsheet (); $spreadsheet ->getActiveSheet () ->setCellValueExplicit ('A8' ,"01513789642" ,\PhpOffice\PhpSpreadsheet\Cell\DataType ::TYPE_STRING ); $spreadsheet ->getActiveSheet () ->getStyle ('A9' ) ->getNumberFormat () ->setFormatCode ('00000000000' ); $spreadsheet ->getActiveSheet () ->setCellValue ('A9' , 1513789642 );
设置单元格格式还可以做到另一个应用:带特殊符号.
1 2 3 4 5 6 7 8 $spreadsheet = new Spreadsheet (); $spreadsheet ->getActiveSheet () ->getStyle ('A10' ) ->getNumberFormat () ->setFormatCode ('0000-000-0000' ); $spreadsheet ->getActiveSheet () ->setCellValue ('A9' , 1513789642 );
将 PHP 数组映射到单元格上
以下代码将向 C3、D3、E3、F3 四个单元格写入数据
1 2 3 4 5 6 7 $spreadsheet = new Spreadsheet (); $rowArray = ['Value1' , 'Value2' , 'Value3' , 'Value4' ]; $spreadsheet ->getActiveSheet () ->fromArray ($rowArray ,NULL ,'C3' );
以下代码将向 C3-C7、D3-D7、E3-E7、F3-F7 区域单元格写入数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 $spreadsheet = new Spreadsheet (); $arrayData = [ [1234 ,2010 ,2011 ,2012 ], ['Q1' ,12 ,15 ,21 ], ['Q2' ,56 ,73 ,86 ], ['Q3' ,52 ,61 ,69 ], ['Q4' ,30 ,32 ,0 ], ]; $spreadsheet ->getActiveSheet () ->fromArray ($arrayData , // PHP 数组 1234 , // 被忽略的值,原本应为该值的位置为空 'C3' // 开始存放的位置,默认为A1 );
杂项设置
工作表
1 2 3 4 5 6 7 $spreadsheet ->getActiveSheet ()->setTitle ('Hello' ); $spreadsheet ->setActiveSheetIndex (0 ); $spreadsheet ->setActiveSheetIndexByName ('工作表表名' );
时间戳转换
1 2 3 4 5 6 7 8 9 $timenow = time ();$spreadsheet ->getActiveSheet () ->setCellValue ('D1' , \PhpOffice\PhpSpreadsheet\Shared\Date ::PHPToExcel ($timenow )); $spreadsheet ->getActiveSheet () ->getStyle ('A1' ) ->getNumberFormat () ->setFormatCode (\PhpOffice\PhpSpreadsheet\Style\NumberFormat ::FORMAT_DATE_YYYYMMDD );
超链接
1 2 3 4 5 6 7 $spreadsheet ->getActiveSheet () ->setCellValue ('E6' ,'百度' ); $spreadsheet ->getActiveSheet () ->getCell ('E6' ) ->getHyperlink () ->setUrl ('https://example.com' );
字体
1 2 3 4 5 6 $spreadsheet ->getActiveSheet () ->getStyle ('A7:B7' ) ->getFont () ->setBold (true ) ->setName ('Arial' ) ->setSize (10 );
文字属性
1 2 3 4 5 $spreadsheet ->getActiveSheet () ->getStyle ('A4' ) ->getFont () ->getColor () ->setARGB (\PhpOffice\PhpSpreadsheet\Style\Color ::COLOR_RED );
1 2 3 4 5 6 7 8 9 10 11 12 13 $styleArray = [ 'alignment' => [ 'horizontal' => \PhpOffice\PhpSpreadsheet\Style\Alignment ::HORIZONTAL_CENTER , ], ]; $spreadsheet ->getStyle ('A1' )->applyFromArray ($styleArray );
列宽
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $spreadsheet ->getActiveSheet () ->getColumnDimension ('A' ) ->setWidth (30 ); $spreadsheet ->getActiveSheet () ->getColumnDimension ('A' ) ->setAutoSize (true ); $spreadsheet ->getActiveSheet () ->getDefaultColumnDimension () ->setWidth (12 );
行高
1 2 3 4 5 6 7 8 $spreadsheet ->getActiveSheet () ->getRowDimension ('10' ) ->setRowHeight (15 ); $spreadsheet ->getActiveSheet () ->getDefaultRowDimension () ->setRowHeight (15 );
合并/拆分单元格
1 2 3 4 5 6 $spreadsheet ->getActiveSheet () ->mergeCells ('A1:B2' ); $spreadsheet ->getActiveSheet () ->unmergeCells ('A1:B2' );
单元格背景颜色
1 2 3 4 5 $spreadsheet ->getStyle ('A1' ) ->getFill () ->setFillType (\PhpOffice\PhpSpreadsheet\Style\Fill ::FILL_SOLID ) ->getStartColor () ->setARGB ('00FF00' );
区域边框及边框颜色
1 2 3 4 5 6 7 8 9 10 11 $styleArray = [ 'borders' => [ 'outline' => [ 'borderStyle' => \PhpOffice\PhpSpreadsheet\Style\Border ::BORDER_THICK , 'color' => ['argb' => 'FFFF0000' ], ], ], ]; $spreadsheet ->getStyle ('B2:G8' ) ->applyFromArray ($styleArray );