简介:这是一份面向PHP开发者的Excel导出入门示例资源,解决在项目中以程序化方式生成Excel文件的需求,覆盖XLS/XLSX等常见格式。压缩包共2个文件,主要为1个PHP演示脚本和1张效果预览图,体积仅30KB,适合快速查阅与直接复用。已有153人浏览学习。demo.php演示了从创建工作簿、添加工作表、填充单元格到保存文件的完整流程,参考了PHPExcel/PhpSpreadsheet常用API,可帮助读者建立生成Excel的基本代码骨架;JPG预览图则直观展示脚本运行后的输出效果,便于对照验证。无论是做后台数据导出、电商订单报表,还是简单统计任务,都能快速迁移其中的写法。资源虽小,但聚焦核心用法,适用于新手入门或需要临时参考的开发者。 在做后台管理系统或者写接口服务的时候,十次需求里至少有八次会碰到“把数据导成Excel”这个动作。客户要的可能是订单明细、财务报表、用户列表,也可能是带格式的统计报表;你写的时候以为很简单,等真的下手,才发现PHP生成Excel文件这件事,坑比想象中多得多。网上关于PHP导出Excel的教程不少,但多数停在“能用就行”,真正涉及选型、内存、性能、兼容性的内容很少。这篇文章就围绕PHP生成Excel文件的完整链路,把方案选型、实操步骤、性能调优、常见坑位一次讲透。适合刚入行的PHP开发,也适合写了几年但没系统整理过这套逻辑的人。
1. 动手之前的方案选型:三种主流实现路线
1.1 原生拼XML:没装扩展时的救命稻草
先说一个很少人提但很实用的冷门方案:直接用PHP拼接XML生成Excel可识别的Spreadsheet文件。Excel从2003开始支持XML表格格式(XML Spreadsheet 2003),本质就是一个带特定命名空间的XML文档。你不需要安装任何扩展,用字符串拼接就能生成,浏览器拿到以后用Excel打开毫无压力。
function exportXmlExcel($filename, $headers, $rows) { header('Content-Type: application/vnd.ms-excel'); header('Content-Disposition: attachment;filename="' . $filename . '.xml"'); echo '<?xml version="1.0" encoding="UTF-8"?>'; echo '<?mso-application progid="Excel.Sheet"?>'; echo '<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">'; echo '<Worksheet ss:Name="Sheet1"><Table>'; foreach ($headers as $h) { echo '<Row><Cell><Data ss:Type="String">' . htmlspecialchars($h) . '</Data></Cell></Row>'; } foreach ($rows as $row) { echo '<Row>'; foreach ($row as $cell) { echo '<Cell><Data ss:Type="String">' . htmlspecialchars($cell) . '</Data></Cell>'; } echo '</Row>'; } echo '</Table></Worksheet></Workbook>'; }这种方式的优点是零依赖、代码量小,适合简单的数据导出;缺点是样式、公式、多工作表这些高级功能实现起来很痛苦,数据量一大字符串拼接也会占内存。我的建议是:只能在服务器环境受限、无法安装任何PHP扩展或Composer依赖的极端场景下用,正常项目不要拿它当主力方案。
1.2 CSV导出:轻量需求的务实之选
很多人忽略CSV,其实业务里大量“导出Excel”的需求,用CSV就能满足。CSV文件本身就是文本,Excel双击可以打开,而且支持UTF-8编码。对用户来说,文件后缀可能是.csv,只要内容正确,大部分人都不会在意格式差异。
function exportCsv($filename, $headers, $rows) { header('Content-Type: text/csv; charset=UTF-8'); header('Content-Disposition: attachment;filename="' . $filename . '.csv"'); $fp = fopen('php://output', 'w'); fwrite($fp, "\xEF\xBB\xBF"); // 写入UTF-8 BOM,防止Excel打开中文乱码 fputcsv($fp, $headers); foreach ($rows as $row) { fputcsv($fp, $row); } fclose($fp); }CSV的优势是内存占用低,配合fputcsv可以一行行写入,几十万行数据也不容易崩。缺点也很明显:不能设置单元格样式、不能合并单元格、不支持公式和图片。如果客户只要“能打开、能筛选、能排序”,用CSV能省下一大堆不必要的复杂度。我一般是这么判断的:需求里没有强调“必须.xlsx后缀”“必须有颜色”这类字眼,就优先考虑CSV。
1.3 PhpSpreadsheet:复杂报表的标配
到了真正需要输出带格式Excel报表的时候,绕不开的就是PhpSpreadsheet。它是老牌PHPExcel的继任者,命名空间从PHPExcel改成了PhpSpreadsheet,底层API大体延续,但性能和代码结构都优化过。它支持xlsx、xls、ods、csv等格式,能写样式、公式、图表、图片、数据透视表,基本覆盖了绝大多数业务需求。
选型时之所以推荐PhpSpreadsheet而不是继续用PHPExcel,有一个很现实的原因:PHPExcel已经停止维护很多年,对PHP 7以上版本的兼容性一直有问题,还会报一些莫名其妙的Deprecated警告。而PhpSpreadsheet支持PHP 7.2以上版本,PHP 8.x也能正常运行。你现在新写代码,没必要给自己埋这种雷。
2. 核心实操:用PhpSpreadsheet从零导出一个带样式的Excel
2.1 安装与基本骨架
安装很简单,项目里执行composer require phpoffice/phpspreadsheet就行。如果你的网络环境比较特殊,可以把composer.json里的repositories配置改成国内镜像源,否则依赖下载会很慢。装完之后,一段最基础的导出代码长这样:
require 'vendor/autoload.php'; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Writer\Xlsx; $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); $sheet->setCellValue('A1', '订单号'); $sheet->setCellValue('B1', '金额'); $writer = new Xlsx($spreadsheet); $writer->save('orders.xlsx');这里要特别提醒一点:很多人习惯从官网复制new Spreadsheet()这段代码,然后直接在控制器里echo输出,结果发现文件下载下来是损坏的。原因是Excel文件本身是二进制,而你在输出之前可能不小心输出了HTML标签、空格或BOM,导致文件头被污染。正确写法是设置响应头之后,用$writer->save('php://output')直接输出到缓冲区,并且确保在save之前没有任何echo或print。
2.2 单元格写入与数据类型
setCellValue最基础,但有个细节容易踩坑:它会自动推断数据类型。比如你写入'001',Excel打开后可能变成1;写入一个很长的数字字符串,可能被转成科学计数法。这种场景下需要显式设置数据类型为字符串。
$sheet->setCellValueExplicit('A2', '001', \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING);另外,日期在Excel里本质是一个数字序列号。如果你直接setCellValue('B2', '2025-06-01'),PhpSpreadsheet会把它当字符串处理,Excel里看是文本,没法做日期筛选和计算。正确做法是先用\PhpOffice\PhpSpreadsheet\Shared\Date::PHPToExcel($timestamp)把Unix时间戳转成Excel日期序列号,再通过样式把数字显示成日期格式。
2.3 样式、合并单元格与冻结窗口
报表好不好看,全靠样式。给标题行加粗、加背景色,给金额列加千分位,这些都是高频操作。PhpSpreadsheet的样式写法比较直观:
$styleArray = [ 'font' => [ 'bold' => true, 'size' => 11, ], 'fill' => [ 'fillType' => \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID, 'startColor' => ['argb' => 'FFCCCCCC'], ], 'alignment' => [ 'horizontal' => \PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER, ], ]; $sheet->getStyle('A1:F1')->applyFromArray($styleArray);合并单元格用$sheet->mergeCells('A1:F1'),适合做表头。冻结首行用$sheet->freezePane('A2'),数据多的时候用户往下拖也能看见标题。这些功能看着琐碎,但恰恰是客户满意度提升最明显的点。一套能用的导出模板,至少要包含标题背景色、字段加粗、金额格式、日期格式这四个基础样式。
2.4 公式、图表与图片
PhpSpreadsheet对公式的支持是一大亮点。写入公式很简单,$sheet->setCellValue('G1', '=SUM(C2:C100)'),文件打开后Excel会自动计算结果。要注意的是,PhpSpreadsheet本身不会马上计算公式,除非你用\PhpOffice\PhpSpreadsheet\Calculation\Calculation::getInstance()->calculate($spreadsheet)强制预计算,否则读文件时拿不到计算后的值。
图表和图片属于进阶功能。图表需要先创建Chart\Chart对象,设置数据源、坐标轴、图例,再通过addChart挂到工作表里。图片更简单,$drawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing(); $drawing->setPath('logo.png'); $sheet->getColumnDimension('A')->setWidth(20); $drawing->setCoordinates('A1'); $drawing->setWorksheet($sheet);。这种需求通常在生成带Logo的销售报表或带趋势图的运营月报时用到,代码量不大,但对坐标和尺寸的计算要仔细,不然图片位置会乱。
3. 批量数据与内存优化:50000行数据导出不崩的调优记录
3.1 内存瓶颈从哪来
用PhpSpreadsheet导出一个几万行的Excel,最让人头疼的不是代码写不出来,而是执行到一半内存爆掉。原因很简单:PhpSpreadsheet默认会把所有单元格对象、样式对象都加载进内存,行数越多内存占用越夸张。一个3列的50万行导出,内存占用可能轻松超过1GB,PHP默认的128M内存限制直接被打爆。
我最早在项目里遇到过这种问题,客户导一个10万行的订单明细,页面直接空白,查日志一看Allowed memory size exhausted。后来做了三件事,才把内存稳稳控制住:分页取数、流式写入、关掉样式缓存。
3.2 分页查询 + 流式写入
分页查询比较好理解,不要一次性SELECT * FROM orders查十万条,而是每次LIMIT 1000,处理完再查下一批。配合PhpSpreadsheet的setCellValue逐行写入,内存增长会小很多。但要注意,默认的Xlsx Writer在写文件时,依然会持有一整张工作表的数据引用,所以光分页还不够,要开启流式写入。
$writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet); $writer->setPreCalculateFormulas(false); $writer->setOffice2003Compatibility(false); $writer->setUseDiskCaching(true, '/tmp/phpspreadsheet_cache');setUseDiskCaching(true, dir)可以把临时数据写到磁盘而不是内存,这是大数据量导出必开的一个选项。setPreCalculateFormulas(false)也很重要,如果你不关心公式的计算结果,只想让Excel打开时自己算,就把它关掉,能省下一大笔计算时间。
3.3 单元格缓存与垃圾回收技巧
PhpSpreadsheet还提供了Cell Cache机制,可以把单元格数据缓存到文件或内存插件里。在new Spreadsheet()之前设置缓存类型,能显著减少内存占用。推荐用Custom缓存类配合Memory或者简单文件缓存,配置起来不复杂:
\PhpOffice\PhpSpreadsheet\Settings::setCache( new \PhpOffice\PhpSpreadsheet\Collection\Memory\SimpleCache3() );另外,写大数据量表格时候,循环里已经用过的PHP变量要及时unset(),甚至手动调用gc_collect_cycles()。PHP的垃圾回收机制在循环里未必会马上回收,自己主动释放能避免不少突发内存上涨。我实测同样的10万行数据,改成分页+流式写入+磁盘缓存之后,峰值内存从800MB降到300MB左右,虽然不算惊艳,但至少不会在真实生产环境里总是被kill。
4. 常见问题与排查技巧实录
4.1 中文乱码怎么定位
中文乱码是PHP生成Excel里出现频率最高的问题,表现形式有两种:一种是用Excel打开下载的xlsx文件时中文全部变成乱码,另一种是用记事本打开CSV文件时中文乱码。xlsx乱码多半是文件本身没写好,要么是用了老旧的第三库,要么是输出过程中夹杂了非UTF-8字符;CSV乱码则是编码声明问题。CSV里面最稳妥的办法就是写BOM头,我在前面代码里已经加了一行\xEF\xBB\xBF,这个小细节可以解决90%的Excel打开CSV中文乱码问题。如果是xlsx乱码,第一步先检查PHP页面本身有没有在文件头输出BOM或其他字符,第二步确认字符串编码统一转成UTF-8后再写入。
4.2 数字变科学计数法、前导零丢失
导出手机号、身份证号这类长数字,Excel默认会转成科学计数法,客户看到肯定不满意。前面提到的setCellValueExplicit加TYPE_STRING是一个解法,还可以通过把单元格格式设置为文本型:$sheet->getStyle('A2')->getNumberFormat()->setFormatCode('@')。不过要注意,这种样式设置对已写入的数字类型数据无效,必须在写入前就用字符串类型写入。我的习惯是:业务字段是身份证、手机号、订单号这类超过12位的,一律先转字符串再写入,再顺手给列格式设为文本。
4.3 文件下载异常
文件下载异常通常是几个原因叠加出来的:用了echo file_get_contents($path)输出但忘记设置响应头,控制器框架里自动加了响应头,重复调用header()导致输出被覆盖,或者使用了框架的视图模板导致HTML内容混入二进制数据。排查时可以先加个最简单的方式验证:把$writer->save('test.xlsx')写到服务器本地,再通过浏览器直接访问这个文件,如果能正常打开,说明生成逻辑没问题,问题出在下载环节;如果本地文件也打不开,说明问题出在Writer或数据上。
4.4 导出超时与内存耗尽
导出超时和内存耗尽本质是执行时间太长、内存占用太高。除了前面说的批量处理优化外,接口层面还可以设置更高的执行时间限制。不过我不建议直接用set_time_limit(0)无限延长,万一代码有慢查询,会把进程拖到很危险的状态。更合理的做法是:给导出接口单独设置一个可接受的时间上限,比如set_time_limit(120),配合前端做“生成中”的loading提示,让用户知道任务在跑。如果数据量实在太大,就考虑走异步队列,生成文件后提供下载链接,不要把压力全压在一次HTTP请求里。
5. 拓展:从导出到导入,以及更完整的工程化
5.1 导入Excel入库时的坑
标题虽然写的是生成Excel,但实际项目里“导出”和“导入”经常是一对出现的。导入时用$reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReaderForFile($filePath); $spreadsheet = $reader->load($filePath);就能读取上传的xlsx文件。这里最容易踩的坑是:Excel单元格里的日期读取后是一个数字序列号,不是字符串。处理方法是判断单元格是否为日期$cell->isFormattedDate(),然后通过\PhpOffice\PhpSpreadsheet\Shared\Date::excelToDateTimeObject($cell->getValue())转成DateTime。另外一个坑是合并单元格读取时只有左上角有值,其他区域是空值,导入数据库前要把继承逻辑自己实现一遍。
5.2 错误处理与日志
导出功能出错最恶心的地方在于,很多错误不是代码抛异常,而是文件默默生成了一半或者返回了不完整数据。强烈建议在导出Service层加一套完整的错误处理逻辑。用try/catch捕获所有Writer和IO异常,记录到日志,然后返回一个可读的错误信息给前端。如果是在CLI里跑队列导出,更要在每个阶段打点日志,方便回溯是哪一行数据导致失败。热词里有人搜“PHP错误处理”,其实就是这个问题——没有错误处理,出问题时你只能在生产环境里用老办法file_put_contents('debug.log', ...)慢慢查。
5.3 图表和数据可视化延伸
很多业务场景已经从“导一张明细表”升级成了“导一张带图表的数据分析报表”。PhpSpreadsheet支持折线图、柱状图、饼图等多种图表,数据源可以直接引用工作表中的区域。如果你想做得再轻量一点,可以在Excel里预置一份模板文件,里面把图表、样式都配置好,然后用PhpSpreadsheet打开模板,只替换数据区域,再另存为新的xlsx。这种做法能极大减少后端代码量,还能保证报表样式永远一致。实际工程里,我会把模板文件放在storage/templates目录,数据变动时程序只改单元格,不改样式逻辑。
我个人的经验是,不管是生成、导入还是报表化,最核心的永远是数据和内存管理。很多时候客户说“导出太慢”,未必是代码写得烂,而是你一次性把整张表塞进内存、还要算几十个公式。先把数据分批、缓存、样式这些基础打好,再考虑花哨的图表和复杂格式。最后分享一个小技巧:上线前拿一份接近线上真实数据量的测试数据,跑一遍导出接口,盯着内存和耗时两个指标调一下,能帮你省掉很多半夜被运维叫醒的麻烦。
本文还有配套的精品资源,点击获取