PHP原生生成Excel:不装Composer轻松导出xlsx 简介在PHP后端开发中Excel文件生成是数据导出、报表下载等场景的高频需求。这套源码示例围绕“PHP生成Excel文件”提供完整入门演示适合需要快速实现表格导出功能的初中级开发者参考使用。压缩包共2个文件RAR格式整体仅30KB左右包含1个PHP脚本与1张效果展示图脚本串联了创建工作簿、写入单元格数据、设置基础格式、保存文件的关键环节效果图可直观对照最终输出。示例覆盖了PHPExcel与PhpSpreadsheet两类常用库的调用思路前者兼容旧版XLS表单后者采用命名空间写法、支持新版XLSX可对比学习API差异与迁移方法。相关内容能直接迁移到电商订单下载、CRM客户清单导出、后台报表生成等实际业务中同时注意点提示了大数据量下的内存占用与库版本兼容性问题对做技术选型或首次接触导出的开发者都有直接帮助。压缩包结构紧凑适合以代码片段方式直接复用也方便二次修改适配。目前已有155人学习浏览配合官方文档边读边验证效果更佳。1. PHP生成Excel文件的实用价值不装Composer也能导出xlsx接手过一个内部报表导出需求数据量不大但服务器是台老机器既没装Composer也没有可用的外网。试了一圈常见的Excel组件要么依赖太多要么环境不兼容最后发现用原生PHP写一个精简的xlsx导出类反而比引入大型库更省心。这套源码就是干这个事的给一段二维数组直接生成一个能双击打开的Excel文件支持多工作表、基础样式和列宽控制整个过程不依赖任何第三方包连composer install都省了。它的核心思路是手动拼出xlsx的OpenXML结构再调ZipArchive把多个XML打包成一个.xlsx。因为不经过中间层转换内存占用比读入整个Excel对象再导出的方案低不少对中小数据量来说生成速度肉眼可见的快。适合哪类人后端程序员尤其是做后台管理系统、报表中心的需要把查询结果快速吐给前端下载又不想为了一个导出功能引入几百个依赖文件的情况。新手照着示例改改数组就能跑通老手则可以直接改源码按需扩展样式和数据类型。2. 项目结构与技术选型一个单文件类如何撑起多Sheet导出2.1 目录结构与文件职责这套工具不是那种多模块框架更像一个提升开发效率的代码包。整个工程目录非常精简核心就两个源码文件外加一个示例脚本和一个说明文档。我第一次拆包的时候一眼就能看完所有逻辑没有多余的黑匣子。xlsx-export/ ├── src/ │ ├── XlsxWriter.php # 导出主类负责数据接收、单元格处理、ZIP打包 │ ├── XmlHelper.php # XML构建辅助类处理标签转义、节点封装 ├── examples/ │ ├── export.php # 命令行示例演示单Sheet和多Sheet导出 │ └── download.php # Web场景示例输出文件流给浏览器 ├── README.md # 使用说明和参数表 └── composer.json # 仅声明PHP版本要求无任何第三方依赖服务端的核心入口是XlsxWriter.php它对外暴露addSheet和export两个方法。XmlHelper.php本来可以合并进主类单独拆出来是为了让XML相关操作能复用比如后面想加样式、加图片都从这里扩展。示例脚本则把两种最常见的调用姿势写清楚了命令行生成本地文件以及HTTP响应直接下载。2.2 技术栈与依赖说明严格来说这套东西算不上一个正式框架而是典型的“手写工具类”。技术选型上只有三个硬要求PHP 7.4以上因为用到了typed properties和arrow functions如果还在维护老项目也可以把代码里的类型声明去掉PHP 5.6也能运行必须开启zip扩展ZipArchive没有替代方案它是打包xlsx文件的基础如果要用Web下载场景还需要sapi是流行的PHP-FPM或Apache模块这样才能正确发送下载头。依赖少的好处很明显部署时不需要执行composer install直接把整个目录传到服务器的任意路径include对应的类文件就能用。很多生产环境不允许服务器访问外网下载依赖本身就是个坑这个方案刚好绕开了。不过也要说句公道话没有依赖意味着所有功能都得自己造轮子比如数字类型自动转换、日期格式化、单元格样式表这些在PhpSpreadsheet里都是现成的在这里就得自己实现。项目里其实预留了这些扩展点后面章节会逐个讲解。2.3 数据流与架构设计整体数据流是一个典型的“数组进、文件出”管道。调用方把业务数据组织成[sheet名称, [ [列名A, 列名B], [值1, 值2] ]]这样的结构传给XlsxWriter。主类先校验数据格式把二维数组里的每个单元格按类型拆分成“字符串”和“数字”两类。然后生成三个XML文件workbook.xml描述工作表结构worksheetN.xml存放单元格内容sharedStrings.xml统一管理所有出现的字符串。最后用ZipArchive把这几个XML按固定目录结构压成一个[Content_Types].xml、_rels/、xl/组成的符合OpenXML规范的压缩包。这个过程有点类似手工打包一个网页静态XML是网页源码ZIP压缩是最终部署产物。Excel打开时其实就是在解压这个包再渲染。架构上最大的优势是生成每一步都可以拆开测试比如单独输出一个worksheet.xml看看结构比调一个完整体更直观。3. 核心模块拆解从数据数组到ZIP压缩的完整链路3.1 数据准备与类型识别导出前最重要的准备工作是把用户数据转换成Excel认识的类型。PHP数组里可能混着字符串、整数、浮点数、布尔值、null如果直接写入XMLExcel大概率会把“00123”这种字符串当成数字导致前面的零丢失。所以XlsxWriter在接收数据时会做一个类型归一化处理。protected function normalizeValue($value): array { if (is_numeric($value) !is_bool($value)) { return [type n, value (string)$value]; } if (is_bool($value)) { return [type b, value $value ? 1 : 0]; } if ($value instanceof DateTimeInterface) { return [type s, value $value-format(Y-m-d H:i:s)]; } if ($value null) { return [type s, value ]; } return [type s, value (string)$value]; }这里每个判断都有目的is_numeric会把数字转成XML里的n类型让Excel直接按数值处理避免存储为文本后无法参与求和布尔值在OpenXML里没有专属类型统一用0和1表示DateTimeInterface对象转成固定格式字符串方便展示。null则规范成空字符串而不是PHP里自动转成的空字符串避免不同类型的单元格在Excel里出现奇怪的对齐方式。处理完后每个单元格都变成一个包含type和value的标准化结构。后续所有XML生成逻辑只认这个结构不会再去碰原始数据。这个设计让数据的源头可以多样化不管从数据库还是接口拿到的数据最后都落到同一套中间格式。3.2 生成工作表和共享字符串表xlsx的字符串单元格不是直接写在工作表里的而是统一存到一个叫sharedStrings.xml的表里表格里只放字符串索引。Excel这么做是为了让重复字符串只存一份减小文件体积。我们生成时也需要遵循这个规范。XmlHelper里维护了一个字符串池和两个索引数组。public function collectString(string $value): int { if (!isset($this-stringMap[$value])) { $this-stringMap[$value] count($this-strings); $this-strings[] $value; } return $this-stringMap[$value]; } public function buildSharedStringsXml(): string { $xml ?xml version1.0 encodingUTF-8 standaloneyes?; $xml . sst xmlnshttp://schemas.openxmlformats.org/spreadsheetml/2006/main count . count($this-strings) . uniqueCount . count($this-strings) . ; foreach ($this-strings as $str) { $xml . sit . htmlspecialchars($str, ENT_XML1, UTF-8) . /t/si; } $xml . /sst; return $xml; }collectString是幂等的同一个字符串只入池一次返回的是它在strings数组里的下标。buildSharedStringsXml遍历字符串池生成标准格式。这里有个容易踩的坑字符串里包含、、时必须用htmlspecialchars转义而且ENT_XML1这个flag很关键它能把单双引号正常转成XML实体而不是HTML实体。如果用默认的ENT_QUOTES生成的文件在Excel里也能打开但部分严格校验工具会报错。工作表XML的结构稍微复杂一点但本质就是列、行、单元格三层嵌套。每写一个单元格如果是字符串类型先调用collectString拿到索引然后写入ts的c节点。3.3 文件打包与下载响应所有XML准备完毕后最后一步就是打包成.xlsx。这里直接用PHP内置的ZipArchive按OpenXML规范把文件放进对应目录。目录结构错误是导致Excel提示“文件损坏”的头号原因常见错误包括少[Content_Types].xml、把workbook.xml放到xl外面等。下面的代码展示了正确的组装顺序。public function save(string $filename): bool { $zip new ZipArchive(); $zip-open($filename, ZipArchive::OVERWRITE | ZipArchive::CREATE); $zip-addFromString([Content_Types].xml, $this-buildContentTypesXml()); $zip-addFromString(_rels/.rels, $this-buildRelsXml()); $zip-addFromString(xl/workbook.xml, $this-buildWorkbookXml()); $zip-addFromString(xl/_rels/workbook.xml.rels, $this-buildWorkbookRelsXml()); $zip-addFromString(xl/styles.xml, $this-buildStylesXml()); foreach ($this-sheets as $index $sheet) { $zip-addFromString(xl/worksheets/sheet{$index}.xml, $sheet[xml]); } $zip-addFromString(xl/sharedStrings.xml, $this-sharedStringsXml); $zip-close(); return file_exists($filename); }ZipArchive::OVERWRITE确保重复调用时不会遗留旧内容CREATE表示文件不存在则创建。注意参数顺序先写根关系文件再写workbook.xml然后写每个工作表最后补sharedStrings.xml。虽然ZIP内部的文件顺序不影响Excel解析但按照这个顺序写排查问题时层次分明。如果open失败多半是目标目录没有写权限或者路径不存在。Web场景下的下载响应更简单把生成好的文件读出来加上Content-Type和Content-Disposition头即可。这里有个细节必须用readfile而不是file_get_contents再echo前者是零拷贝方式对内存友好。文件名有中文时用rawurlencode处理否则浏览器会截断文件名。4. 运行与部署从命令行脚本到Web接口的快速落地4.1 环境要求与安装配置这个项目对环境非常宽容但有几个点必须在动手前确认。第一PHP版本不要低于7.4虽然代码里只有少量新语法但用过之后你会发现属性类型声明真的能减少很多“隐式类型转错”的问题。第二php_zip扩展必须启用可以用php -m | grep zip验证如果没装在Linux上执行yum install php-zip或apt install php-zip装完重启PHP-FPM即可。第三如果要在浏览器里下载注意output_buffering不要开太大否则文件流会有延迟。部署方式有两种如果线上环境不允许跑命令行可以直接把src目录丢进Web根目录然后写个PHP脚本实例化XlsxWriter如果是要做定时报表就写一个CLI脚本用crontab定期执行。这个项目没有vendor目录所以不存在路径对接的麻烦只要确保src/XlsxWriter.php能被require到就行。4.2 快速体验生成一个最简单的单Sheet文件拿到源码后第一件事建议先跑一下示例脚本确认环境没问题。这里写一个最简单的调用模拟从数据库读出一组用户数据然后导出。?php require_once __DIR__ . /../src/XlsxWriter.php; require_once __DIR__ . /../src/XmlHelper.php; use Export\XlsxWriter; $headers [用户名, 注册时间, 积分]; $rows [ [alice, 2023-05-01 10:00:00, 120], [bob, 2023-05-02 11:30:00, 80], [carol, 2023-05-03 09:15:00, 245], ]; $sheetData array_merge([$headers], $rows); $writer new XlsxWriter(); $writer-addSheet(用户列表, $sheetData); $writer-save(__DIR__ . /output.xlsx); echo 生成成功: . realpath(__DIR__ . /output.xlsx) . PHP_EOL;把这段代码存成example.php和src目录平级执行php example.php控制台会输出生成文件的绝对路径。用Excel打开output.xlsx第一行是表头下面三行数据。如果这一步都跑不起来多半是ZipArchive类不存在执行php -m检查模块。这个脚本在命令行下运行不涉及SAPI限制是最保险的验证方式。加第二个Sheet也简单再次调用addSheet就行。需要注意addSheet的第一个参数是工作表名称名称里不能包含[]:*?/\这几个字符Excel规范不允许。如果传入了非法名称工具会抛异常。4.3 Web下载场景的参数设置把导出能力暴露给前端通常是通过一个export.php接口。常见的做法是用GET参数指定导出类型用POST接收筛选条件。这里展示一个最简版本的下载接口重点是响应头设置。public function download(string $filename) { header(Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); header(Content-Disposition: attachment; filename . rawurlencode($filename) . ); header(Cache-Control: max-age0); $this-save($filename); readfile($filename); unlink($filename); exit; }Content-Type必须是.xlsx对应的MIME类型不能写application/octet-stream虽然也能下载但Windows下双击时可能被当成未知程序。Cache-Control: max-age0告诉浏览器不要缓存否则同一个接口第二次导出的还是旧文件。生成完临时文件后立即readfile然后unlink删掉避免临时垃圾堆积。如果业务量很大建议把临时文件写到sys_get_temp_dir()而不是当前目录。5. 常见问题与避坑记录文件损坏、乱码、内存超时的真实处理5.1 Excel提示“文件格式与扩展名不匹配”或直接打不开现象生成的文件用WPS能打开但是用微软Excel打开会弹黄色警告条提示文件格式有问题甚至直接拒绝打开。原因最常见的是sharedStrings.xml或worksheet.xml里拼错了命名空间比如把http://schemas.openxmlformats.org/spreadsheetml/2006/main写漏了main或者忘了加standaloneyes。其次是用文本编辑器手动改了XML导致编码不一致。解决把生成的.xlsx文件后缀改成.zip解压后用对比工具逐一查看workbook.xml、sheet1.xml里的根节点。我在本地调试时习惯写一个debugDump()方法把$zip里每个XML的内容直接打印出来对照规范检查。另外注意[Content_Types].xml里Override PartName必须和实际文件路径完全一致大小写也不能错。5.2 中文内容导出来全是乱码现象Excel里看到的既有中文又有乱码或者所有中文都变成_xFFFF_这种形式。原因xlsx内部强制使用UTF-8编码而项目中如果用了iconv转GBK再写入就会导致双重编码。也可能是XML声明里写了encodingGBK但实际存的是UTF-8Excel就会按声明的编码去解码自然乱码。解决确保所有字符串进入XmlHelper之前都是UTF-8。如果数据源是GBK编码的数据库在拿到结果后统一执行mb_convert_encoding($value, UTF-8, GBK)。XML声明里固定写encodingUTF-8不要为了图省事改成其他编码。顺便检查一下PHP源文件本身是否带有BOMBOM会导致第一个单元格出现不可见字符。5.3 导出超过两万行时内存溢出或超时现象数据量一大内存直接飙到128M以上甚至PHP报Allowed memory size exhausted前端请求超时。原因绝大多数是因为先把所有数据收集进数组再一次性生成字符串写入XML中间产生了大量拷贝。另一个原因是HtmlHelper里的字符串池没有做去重十万行数据里如果有大量重复词内存直接被撑爆。解决第一个方案是增加内存ini_set(memory_limit, 256M)但这只是临时缓解。更合理的是改成流式写入每生成1000行就把worksheet.xml的片段写入临时文件而不是存在内存变量里。字符串池方面可以设定一个上限比如超过一万条就直接把字符串写进sharedStrings.xml的临时缓冲不能用简单的数组累加。时间超时的话CLI脚本不受max_execution_time限制Web接口则需要做分批导出比如让前端传一个page参数后台每次只导出一段然后前端合并多个文件。5.4 数字开头的字符串丢失了前导零现象在数据库里存的00123导出到Excel后变成123。原因这是Excel的“人性化”设计看到纯数字字符串就自动转成数值。我们在normalizeValue里用is_numeric会把00123识别为数字于是写成了n类型。解决项目在normalizeValue里预留了一个可选参数forceText当设置为true时所有字符串都按文本处理单元格会写tstr而不是ts。另外还有一个取巧的办法在字符串前加一个制表符\t但这样导出的数据会带隐藏字符不推荐。正确做法是在调用addSheet时传入一个列类型定义数组比如[string, datetime, int]工具会根据这个定义决定类型而不是靠is_numeric猜。6. 二次开发与改造思路给导出器加上样式与流式接口6.1 加表头背景色和加粗字体现有的工具生成的是“素颜”Excel因为styles.xml里只有一个默认单元格格式。要加样式得修改buildStylesXml往cellXfs节点里追加自定义格式然后在worksheet.xml的单元格上通过s属性索引引用。这个思路和写CSS很像样式表是全局定义的单元格只管引用。常见做法是在XlsxWriter里增加一个setHeaderStyle()方法专门给第一行设置加粗和背景色。样式索引的映射关系要提前算好比如默认格式是0然后依次追加。注意每个工作表里的单元格都要引用到正确的s值。做了这个扩展后表格的可读性会立刻提升一个档次特别是列头很宽的数据集。如果还要合并单元格worksheet.xml里需要增加mergeCells节点比如mergeCellsmergeCell refA1:C1//mergeCells这个逻辑也不复杂。6.2 改造为流式导出支撑十万行数据我实际开发中遇到最大的瓶颈就是大数据量导出。原始的save方法在调用前已经把所有数据都存在内存里了所以改造会伤筋动骨。建议这样做在addSheet时支持传入一个生成器Generator每产出一行就立刻拼一行XML写入临时文件等所有行都处理完再把这些临时文件合并成一个完整的sheetXML。这样主内存只保留1000行左右的缓冲数据量对内存占用几乎无影响。具体实现上worksheet.xml最好拆成三段头部、数据部分、尾部。数据部分每满一定行数就刷到临时文件头部和尾部最后再拼接。这个改造对现有调用方是透明的因为对外接口不变只是内部实现从全量数组改成了迭代器。我当时改造完把一段六万行的数据从内存崩溃降到稳定跑完耗时也只多了几秒。从那以后我每次设计导出功能都先确认数据是“一次性取回”还是“可以分批读取”然后强制自己写流式接口避免后来再返工。希望帮到你。本文还有配套的精品资源点击获取