ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

PHP导出Excel实战:从PhpSpreadsheet到性能优化避坑指南

PHP导出Excel实战:从PhpSpreadsheet到性能优化避坑指南 简介面向需要快速实现Excel导出功能的PHP开发者这份源码工具聚焦“用PHP生成Excel文件”这一常见需求可应用于后台管理报表导出、电商商品列表下载、CRM数据输出等场景适合初中级开发者阅读或作为项目模板复用。压缩包整体仅30KB共2个文件一个PHP示例文件演示创建Workbook、写入单元格、设置样式、保存文件等关键步骤一张JPG预览图便于对照查看输出效果。示例围绕PHPExcel及其升级版PhpSpreadsheet展开通过代码展示最常用的创建对象、填充数据、保存文件流程并涉及内存管理与性能优化可作为实际业务中接入Excel生成功能的参考直接改造后用于导出大量数据的报表模块。该资源已有153人学习浏览小巧轻量、内容直接对刚接触PHP表格导出的开发者具有较好的参考价值。1. 为什么还在纠结PHP导出Excel这件事做后端开发这几年导出Excel这个需求几乎是个项目就会遇到。后台管理要导订单、导用户、导财务报表运营天天追着你加导出按钮。PHP做导出Excel的方案五花八门老项目还在用PHPExcel新项目有人推荐PhpSpreadsheet还有人直接告诉你“拼CSV不就完了吗”。但真到上线那天你会发现事情没那么简单——客户说Excel打开乱码运营说十万行数据导出超时财务说数字变成科学计数法了。我最早接手导出功能时还真被这些问题一个个坑过来。这篇就按我实际操作的经验把PHP生成Excel文件的完整套路讲一遍从方案选型到性能优化再到常见的坑和排查技巧全是我自己跑过流程之后的总结直接可以拿去做参考。适合什么人看刚接触PHP、第一次做导出功能的初级开发者或者项目里Excel导出老是出问题、想找一套稳定方案的兄弟们。我会尽量少说废话代码直接给原因也尽量讲透。2. 核心方案怎么选PhpSpreadsheet是当前最优解2.1 为什么不能用老牌PHPExcel很多人一查资料发现网上到处都是PHPExcel的教程于是顺手就装。但PHPExcel在2017年前后已经停止维护了GitHub仓库官方直接把用户往PhpSpreadsheet引。这意味着什么老项目继续用PHPExcel碰到PHP8.0以上版本各种deprecated报错满天飞连个修bug的人都没有。我之前接手过一个老系统服务器升到PHP7.4之后PHPExcel直接报“Method mb_convert_encoding is not available”这类问题是小后面发现它连PHP8都不兼容反复踩坑才彻底换掉。所以新项目别犹豫直接用PhpSpreadsheet。它是PHPExcel的官方继承者命名空间换了API设计更规范算是对现代PHP版本适配最好的一套库。2.2 PhpSpreadsheet的好处完整支持xlsx、xls、csv、ods等多种格式的读写提供丰富的单元格样式字体、边框、填充、合并、列宽、冻结窗格支持公式、图表、图片插入、数据验证甚至表格筛选底层性能比PHPExcel提升明显配合内存优化可以批量处理几万行数据有活跃社区和维护团队安全漏洞及时修复这里多说一句网上很多教程一上来就让用Composer安装PhpSpreadsheet有些人不知道自己服务器环境有没有Composer。如果你有服务器shell权限建议直接全局安装Composer后面所有依赖管理都方便。若实在只能用面板操作宝塔这类面板一般也有Composer功能在“终端”里执行命令即可。2.3 实际上也可以用CSV方案如果你的需求只是把数据导出给用户看没有特殊格式要求如合并单元格、多样式、图表而且数据量很大CSV反而是更合适的方案。CSV本质是纯文本PHP用fputcsv就能写内存占用极小导出几万行也不慌。但有两个致命问题CSV用记事本打开中文会乱码因为默认ANSI编码需要转成GBK或者加上BOM头运营同事非要用Excel打开那CSV也能用Excel打开但保存后再打开格式可能会错乱所以我把决策逻辑理成了这张表需求场景推荐方案理由少量数据、样式复杂、有合并单元格PhpSpreadsheet功能全、格式可控大量数据、纯导出、无格式要求CSV直接输出内存占用低、速度快大量数据且必须用xlsx格式PhpSpreadsheet分页/内存优化平衡性能与格式需求要支持用户在线预览和编辑PhpSpreadsheet生成xlsx后配合前端处理保留Excel原生能力3. 最小实现用PhpSpreadsheet写一个能跑的导出3.1 安装依赖既然是PHP8.x时代强烈建议用Composer安装最新版PhpSpreadsheet。在项目根目录执行composer require phpoffice/phpspreadsheet如果安装过程因为网络问题失败可以切换Composer镜像源国内环境中常见操作再重新执行安装。3.2 第一个导出实例创建export.php文件引入自动加载文件写个最基础的导出逻辑?php require vendor/autoload.php; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Writer\Xlsx; $spreadsheet new Spreadsheet(); $sheet $spreadsheet-getActiveSheet(); // 准备表头和数据 $headers [ID, 用户名, 手机号, 注册时间]; $rows [ [1, 张三, 13800138000, 2024-01-01 10:00:00], [2, 李四, 13900139000, 2024-01-02 11:30:00], [3, 王五, 13700137000, 2024-01-03 09:15:00], ]; // 写表头 foreach ($headers as $colIndex $header) { $column \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($colIndex 1); $sheet-setCellValue($column . 1, $header); } // 写数据 $rowIndex 2; foreach ($rows as $row) { foreach ($row as $colIndex $value) { $column \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($colIndex 1); $sheet-setCellValue($column . $rowIndex, $value); } $rowIndex; } // 设置列宽 $sheet-getColumnDimension(A)-setWidth(10); $sheet-getColumnDimension(B)-setWidth(20); $sheet-getColumnDimension(C)-setWidth(20); $sheet-getColumnDimension(D)-setWidth(22); // 导出到浏览器 $filename 用户列表_ . date(YmdHis) . .xlsx; header(Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); header(Content-Disposition: attachment; filename . $filename . ); header(Cache-Control: max-age0); $writer new Xlsx($spreadsheet); $writer-save(php://output); exit;这里有几个细节值得展开说。第一stringFromColumnIndex($index)这个方法是把数字列索引1、2、3转成Excel列字母A、B、C。我第一次写的时候没注意直接用数字拼进setCellValue结果写成了“11”单元格Excel里根本不存在这个位置。超过26列数据时这个转换方法会自动处理成AA、AB不用自己写循环能省不少事。第二导出到浏览器时使用php://output流直接把文件写在输出缓冲里而不是先存到磁盘再读取。这样速度更快也不会产生临时垃圾文件。第三header头里的Content-Type不同格式不一样。xlsx是application/vnd.openxmlformats-officedocument.spreadsheetml.sheet老版xls是application/vnd.ms-excelcsv是text/csv。写错了浏览器可能直接弹下载失败或乱码。3.3 处理中文文件名和内容乱码PhpSpreadsheet生成的xlsx内部使用UTF-8所以中文字段内容一般不会出问题。常见乱码出现在CSV方案或者某些老旧Excel版本打开xlsx时字体渲染异常。我通常的做法是文件下载名做一下URL编码处理防止中文文件名在不同浏览器中显示异常$filename rawurlencode(用户列表_ . date(YmdHis) . .xlsx); header(Content-Disposition: attachment; filename . $filename . );单元格内容强制设置成文本格式避免手机号、身份证号这类长数字被Excel转成科学计数法$sheet-getStyle(C2:C1000)-getNumberFormat()-setFormatCode();或者写入时单独指定$sheet-setCellValueExplicit(C2, 13800138000, \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING);这里尤其要留意手机号你导出时如果直接setCellValue(C2, 13800138000)Excel大概率显示成1.38001E11客户拿到手直接炸毛。上面这两个方法选一个用就行。4. 让导出的Excel更好看样式与公式4.1 表头样式优化业务方拿到Excel第一眼就是看表头空荡荡的白底黑字虽然能用但总觉得不够专业。给表头加点底色、加粗和居中效果立刻不一样use PhpOffice\PhpSpreadsheet\Style\Color; use PhpOffice\PhpSpreadsheet\Style\Fill; $headerStyle [ font [ bold true, color [argb FFFFFFFF], size 11, ], fill [ fillType Fill::FILL_SOLID, startColor [argb FF4472C4], ], alignment [ horizontal \PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER, vertical \PhpOffice\PhpSpreadsheet\Style\Alignment::VERTICAL_CENTER, ], ]; $sheet-getStyle(A1:D1)-applyFromArray($headerStyle); // 表头行高 $sheet-getRowDimension(1)-setRowHeight(24);这里argb颜色格式是AARRGGBBFF表示完全不透明后面六位是十六进制RGB颜色。我在网上找色值的时候经常被坑很多设计工具给的是RRGGBB六位实际用的时候必须前面补上FF。4.2 动态渲染数据区域样式如果数据行数不固定不能写死A2:D10这种范围需要根据实际行数拼接范围$lastRow $rowIndex - 1; $dataRange A2:D . $lastRow; // 数据区域加边框 $sheet-getStyle($dataRange)-applyFromArray([ borders [ allBorders [ borderStyle \PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN, color [argb FFD9D9D9], ], ], alignment [ vertical \PhpOffice\PhpSpreadsheet\Style\Alignment::VERTICAL_CENTER, ], ]); // 设置自动筛选 $sheet-setAutoFilter(A1:D . $lastRow); // 冻结首行 $sheet-freezePane(A2);自动筛选和冻结首行是运营同事特别喜欢的两个功能尤其是数据量大的时候。加自动筛选之后用户可以自己按条件过滤不用再求开发加筛选功能。冻结首行则是把表头固定住往下滚动数据时表头始终可见。4.3 写入公式PhpSpreadsheet支持写Excel公式比如合计金额、平均值之类。常见做法是把公式字符串直接传给setCellValue$sheet-setCellValue(E1, 合计); $sheet-setCellValue(E2, SUM(B2:B100));注意公式里的单元格地址需要根据实际数据变化千万别写死。另一种方式是让PhpSpreadsheet计算并缓存结果不过用公式的好处是用户拿到Excel后可以修改数据合计会自动更新。如果你只是展示统计数据建议直接把计算好的值写进去性能和兼容性都更好。5. 大数据量导出性能优化与内存控制5.1 问题根因在哪PhpSpreadsheet把整个表格对象保存在内存里。导出一万行问题不大但到了十万行、二十万行以上内存占用飙升到几百MB甚至上GB轻则脚本超时重则服务器OOM。所以做大数据量导出时不能一个劲儿往PhpSpreadsheet的“全量模型”里塞。5.2 批量查询加流式写入数据来源通常是MySQL我们先用游标查询或者分批查询拿数据再逐批写入Excel。判断逻辑是$perPage 5000; $page 1; while (true) { $rows getDataFromDb($page, $perPage); // 分页查数据 if (empty($rows)) { break; } foreach ($rows as $row) { // 循环写入sheet注意维护好当前行号 } $page; unset($rows); // 释放变量帮助GC回收内存 }这里其实有一个更优的方案——用Generator生成器配合yield让数据一边从数据库取一边写入Excel而不是攒一大包数组再处理function getRowsFromDb() { $perPage 2000; $page 1; while (true) { $rows getDataFromDb($page, $perPage); if (empty($rows)) { break; } foreach ($rows as $row) { yield $row; } $page; unset($rows); } } foreach (getRowsFromDb() as $rowIndex $row) { $excelRow $rowIndex 2; // 写入 }不过要注意如果数据量实在太大超过几十万行xlsx格式本身也有行数上限是1048576行超过这个数字Excel是打不开的。业务方如果非要百万行导出要么分文件压缩包要么用CSV。5.3 内存优化设置PhpSpreadsheet提供了setCellValueExplicit和设置单元格缓存的方法。常见优化手段// 使用单元格缓存降低内存占用 \PhpOffice\PhpSpreadsheet\Settings::setCache( new \PhpOffice\PhpSpreadsheet\CachedObjectStorage\MemorySerialized() );还有关闭内部样式计算$spreadsheet-getActiveSheet()-setAutoFilter(false); // 不需要筛选就关闭 $spreadsheet-setActiveSheetIndex(0); $spreadsheet-getProperties()-setCreator(); // 去掉文档属性减少元数据此外如果是导出纯数据列尽量少用合并单元格。合并单元格在PhpSpreadsheet内部会额外保存很多结构信息数据量一大内存压力明显增加。5.4 用CSV应对超大文件超过二十万行且格式要求不高的场景我一般直接转CSV$filename big_data_ . date(YmdHis) . .csv; header(Content-Type: text/csv; charsetUTF-8); header(Content-Disposition: attachment; filename . $filename . ); $output fopen(php://output, w); // 加BOM解决Excel打开UTF-8 CSV乱码 fwrite($output, \xEF\xBB\xBF); // 表头 fputcsv($output, [ID, 用户名, 手机号, 注册时间]); // 逐行写数据 foreach ($rows as $row) { fputcsv($output, $row); } fclose($output); exit;这里添加BOM头\xEF\xBB\xBF是解决中文CSV用Excel打开乱码的关键。但CSV的局限也很明显不能设置列宽样式不能合并单元格单元格内换行容易出问题。所以还是那句话看需求场景选方案。6. 常见问题与排查实录6.1 导出文件提示“文件已损坏”或无法打开这个是最常见的问题几乎每周都有同事来问我。出现原因往往不是代码逻辑本身而是输出的时候混入了非文件内容。比如PHP文件开头有空格或BOM、结束标签后面有换行、调试用的echo/print_r没有删除。排查方法先不直接下载改成保存到服务器本地看能否正常打开$writer-save(/tmp/test.xlsx);本地文件正常说明问题出在输出环节检查文件开头是否有BOM字符检查有没有意外输出——可以用ob_clean()清空输出缓冲区ob_clean(); header(Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); header(Content-Disposition: attachment; filename . $filename . ); $writer-save(php://output); exit;6.2 数字导出后变成科学计数法或精度丢失手机号、身份证、订单号这类超过15位的数字是重灾区。原因就是Excel自身的精度限制。解决办法在3.3部分已经讲过了一个核心原则凡是长数字一律以文本形式写入不要让它变成数字类型。还有一种情况是导出金额的时候希望显示两位小数同时不要四舍五入出精度问题。方案是使用number_format先转换再写入或者在Excel里设置单元格数字格式$sheet-getStyle(F2:F100)-getNumberFormat()-setFormatCode(0.00);6.3 内存溢出Allowed memory size of X bytes exhausted这行报错估计每个PHP开发都不陌生。解决办法优先级从高到低提高内存限制ini_set(memory_limit, 512M);但这不是长久之计治标不治本。数据分批处理参考第5节的分批查询方案使用CSV输出方案绕开PhpSpreadsheet的内存模型检查代码里是否有重复引用数据、大数组未释放的情况我现在做系统的时候导出接口一般把内存上限设到512M同时用分批查询能扛住绝大多数业务场景。6.4 导出时间过长造成脚本超时PHP默认max_execution_time是30秒五万行数据即使内存够用写入也很耗时。处理方式set_time_limit(0);或者限定一个合理时间set_time_limit(300);还可以用ignore_user_abort(true)让浏览器关闭了后台还能继续执行不过这种方式对服务器的防护策略要处理好防止恶意请求不断触发大导出任务。6.5 数字前导零丢失工号、编号这类数据经常会这样比如“001234”Excel打开后变成“1234”。写入时同样要强制处理成文本或设置单元格格式为“”。如果已经导出成数字了Excel里的“单元格格式 - 自定义 - 000000”可以补救但最好的方式还是源头规避。6.6 电子表格被误判成病毒或下载被拦截这个是因为xlsx本质是zip压缩包某些杀毒软件会对文件头做误判。特别是动态生成的文件名、文件里含宏或者自定义XML时更容易触发。常规解法是调整杀毒软件白名单或者把文件打包成zip再下载。如果业务确实经常遇到可以考虑加一个二次下载的验证页避免所有下载请求都被扫描拦截。7. 实操心得小结导出Excel这个功能看起来简单真正做好却有一堆细节。上面这些坑每一个我都在真实项目中踩过打不开、乱码、科学计数法、内存溢出、超时、数字精度丢失。如果你正在做导出功能我建议先花五分钟把需求问清楚——数据量多大需要什么格式要不要样式有没有强制列宽和合并需求把这些确认清楚了再选方案。最后再分享一个小技巧开发阶段调试导出文件时千万别一直输出到浏览器反复下载太浪费时间。先存到服务器本地用代码或者命令行打开检查确认无误再走下载逻辑。另外如果项目里有定时任务需要导出Excel并发送邮件建议用Xlsx写出到临时目录再用PHPMailer或Symfony Mailer发送附件最后记得unlink删临时文件避免磁盘被占满。数据导出的核心不是“能导出来”而是“稳定、高效、格式友好”。把这三点把握住业务方对你的满意度不会低。本文还有配套的精品资源点击获取
返回列表