ARTICLE · 1028304
客户的Excel报告无法一比一还原?| PHP 如何直接修改 XLSX:从 ZIP 到 XML
客户的Excel报告无法一比一还原?| PHP 如何直接修改 XLSX:从 ZIP 到 XML平时用 PHP 操作 Excel,通常会想到 PhpSpreadsheet 之类的库。 当客户给了一个复杂的excel模板,使用phpexcel和PhpSpreadsheet 之类的库,操作难以达到模板一模一样的效果 ,这时候怎么办呢?
.xlsx 本身并不是一个特殊的二进制文件。 它本质上可以理解为: 所以,如果不使用 Excel 专用库,也可以通过 PHP 自带的 ZipArchive、DOMDocument、DOMXPath 等能力,对 XLSX 文件进行修改。 核心思路: 核心流程:
把:demo.xlsx 理解成一个 ZIP 文件后,里面大概会有: 其中比较重要的是: 所以,Excel 文件实际上是很多 XML 文件按照约定组织后,再放进 ZIP。
PHP 可以直接使用 ZipArchive: 打开以后,可以读取 ZIP 内部的文件: 这里的 xl/workbook.xml 是 ZIP 内部路径。
workbook.xml 中会记录 Sheet: 但 r:id 并不是最终文件名。 还需要读取: 找到: 因此定位一个 Sheet 的过程是: 简单示例: 实际代码中也是通过 workbook.xml 和关系文件来定位具体工作表。
找到: 之后,可以看到类似结构: 可以简单理解成: 其中: 代表单元格 A1。
PHP 自带 DOMDocument: XML 会被解析成 DOM 树。 然后使用 DOMXPath 查找节点: 可能得到: 这样就可以直接找到 Excel 中的单元格。
例如把 A1 修改成:Hello World 可以找到: 然后清空原来的 XML: 重新写入字符串: 最终对应的 XML 大概是:
数字可以直接写: PHP 示例: 字符串则可以使用: 所以修改 Excel 单元格时,需要根据数据类型生成不同的 XML。
例如 Excel 中:=A1+B1 对应的 XML 可以是: 其中: ``:公式 ``:公式当前缓存结果 PHP 可以直接生成:
Excel 单元格除了内容,还可能包含样式信息: 这里的:s="5" 表示这个单元格使用某个样式。 如果我们只是修改 `` 里面的数据节点,而保留 `` 本身以及 `s` 属性,那么模板原来的样式仍然可以保留。 因此可以把职责拆成: 这也是模板驱动 Excel 的一个核心思路。
XML 修改完成后: 然后放回 XLSX: 最后关闭: 原来的:demo.xlsx 就重新变成一个合法的 XLSX 文件。 完整流程可以浓缩成: 最终可以把整个过程理解成: 所以,PHP 操作 XLSX 的底层逻辑并不复杂: ZipArchive 负责 ZIP,DOMDocument 负责 XML,DOMXPath 负责定位 XML 节点。 理解了这个结构之后,Excel 就不再是一个只能通过专用库操作的黑盒,而变成了一个可以直接解析和修改的文件结构。
ZIP 压缩包 + 一组 XML 文件
Excel 模板↓负责样式、布局、列宽、标题PHP↓负责数据和公式
XLSX↓打开 ZIP↓定位 XML↓解析 XML↓修改 XML 节点↓重新写回 ZIP↓得到 XLSX
1. XLSX 到底是什么?
demo.xlsx├── [Content_Types].xml├── _rels/├── docProps/└── xl/├── workbook.xml├── sharedStrings.xml├── _rels/│ └── workbook.xml.rels└── worksheets/├── sheet1.xml└── sheet2.xml
workbook.xml:工作簿和 Sheet 的关系 workbook.xml.rels:Sheet 对应的 XML 文件 worksheets/sheet1.xml:具体工作表的数据 sharedStrings.xml:共享字符串
2. PHP 打开 XLSX
$zip = new ZipArchive();$result = $zip->open('demo.xlsx');if($result !== true) {throw new RuntimeException('无法打开 XLSX');}
$xml = $zip->getFromName('xl/workbook.xml');3. 从 workbook.xml 找到 Sheet
<sheets><sheetname="Sheet1"r:id="rId1"/><sheetname="Sheet2"r:id="rId2"/></sheets>
xl/_rels/workbook.xml.relsrId1 → worksheets/sheet1.xmlrId2 → worksheets/sheet2.xml
Sheet 名称↓workbook.xml↓r:id↓workbook.xml.rels↓sheet1.xml
$workbookXml = $zip->getFromName('xl/workbook.xml');$relsXml = $zip->getFromName('xl/_rels/workbook.xml.rels');$workbookDom = new DOMDocument();$workbookDom->loadXML($workbookXml);$xpath = new DOMXPath($workbookDom);$xpath->registerNamespace('x','http://schemas.openxmlformats.org/spreadsheetml/2006/main');$xpath->registerNamespace('r','http://schemas.openxmlformats.org/officeDocument/2006/relationships');
4. Sheet 数据是什么样的?
xl/worksheets/sheet1.xml <worksheet><sheetData><rowr="1"><cr="A1"><v>123</v></c><cr="B1"><v>456</v></c></row></sheetData></worksheet>
worksheet└── sheetData└── row├── cell A1└── cell B1
5. 使用 DOMDocument 解析 XML
$dom = new DOMDocument('1.0', 'UTF-8');$dom->loadXML($xml);
$xpath = new DOMXPath($dom);$xpath->registerNamespace('x','http://schemas.openxmlformats.org/spreadsheetml/2006/main');$cells = $xpath->query('//x:sheetData/x:row/x:c');foreach($cells as $cell) {echo $cell->getAttribute('r') . PHP_EOL;}
A1B1C1A2B2C2
6. 修改一个单元格
$cell = $xpath->query('//x:c[@r=”A1”]')->item(0);
while($cell->firstChild !== null) {$cell->removeChild($cell->firstChild);}
$cell->setAttribute('t', 'inlineStr');$is = $dom->createElementNS('http://schemas.openxmlformats.org/spreadsheetml/2006/main','is');$t = $dom->createElementNS('http://schemas.openxmlformats.org/spreadsheetml/2006/main','t','Hello World');$is->appendChild($t);$cell->appendChild($is);
<cr="A1"t="inlineStr"><is><t>Hello World</t></is></c>
7. 数字和字符串的 XML 不一样
<cr="A1"><v>123</v></c>
$value = $dom->createElementNS($namespace,'v','123');$cell->appendChild($value);
<cr="A1"><v>123</v></c>
8. Excel 公式也是 XML
<cr="C1"><f>A1+B1</f><v>300</v></c>
$formula = $dom->createElementNS($namespace,'f','A1+B1');$value = $dom->createElementNS($namespace,'v','300');$cell->appendChild($formula);$cell->appendChild($value);
9. 为什么修改 XML 不会把 Excel 样式弄丢?
<cr="A1"s="5">Excel 模板↓负责样式、布局、列宽、标题PHP↓负责数据和公式
10. 修改完成后重新写回 ZIP
$newXml = $dom->saveXML(); $zip->addFromString('xl/worksheets/sheet1.xml',$newXml);
$zip->close(); $zip = new ZipArchive();$zip->open('demo.xlsx');// 1. 读取 XML$xml = $zip->getFromName('xl/worksheets/sheet1.xml');// 2. 解析 XML$dom = new DOMDocument();$dom->loadXML($xml);// 3. 找到节点并修改$xpath = new DOMXPath($dom);// ...// 4. 保存 XML$newXml = $dom->saveXML();// 5. 写回 ZIP$zip->addFromString('xl/worksheets/sheet1.xml',$newXml);// 6. 生成 XLSX$zip->close();
11. 整个原理
demo.xlsx│▼ZipArchive│▼┌─────────────────┐│ XLSX ZIP 包 ││ ││ workbook.xml ││ sheet1.xml ││ sheet2.xml ││ sharedStrings │└─────────────────┘│▼找到目标 XML│▼DOMDocument│▼DOM 树│▼DOMXPath│▼找到 Cell 节点│▼修改数据│▼saveXML()│▼addFromString()│▼ZipArchive│▼demo.xlsx
打开 ZIP → 找到 XML → 修改 XML → 放回 ZIP。