夜雨聆风学习资料网

ARTICLE · 1028304

客户的Excel报告无法一比一还原?| PHP 如何直接修改 XLSX:从 ZIP 到 XML

客户的Excel报告无法一比一还原?| PHP 如何直接修改 XLSX:从 ZIP 到 XML
平时用 PHP 操作 Excel,通常会想到 PhpSpreadsheet 之类的库。
当客户给了一个复杂的excel模板,使用phpexcel和PhpSpreadsheet 之类的库,操作难以达到模板一模一样的效果 ,这时候怎么办呢?

.xlsx 本身并不是一个特殊的二进制文件。
它本质上可以理解为:

ZIP 压缩包 + 一组 XML 文件

所以,如果不使用 Excel 专用库,也可以通过 PHP 自带的 ZipArchive、DOMDocument、DOMXPath 等能力,对 XLSX 文件进行修改。
核心思路:
Excel 模板       ↓负责样式、布局、列宽、标题PHP      ↓负责数据和公式 
核心流程:
XLSX       ↓   打开 ZIP      ↓    定位 XML     ↓   解析 XML      ↓    修改 XML 节点      ↓    重新写回 ZIP     ↓    得到 XLSX  

1. XLSX 到底是什么?

把:demo.xlsx 理解成一个 ZIP 文件后,里面大概会有:
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:共享字符串
所以,Excel 文件实际上是很多 XML 文件按照约定组织后,再放进 ZIP。

2. PHP 打开 XLSX

PHP 可以直接使用 ZipArchive:
$zip new ZipArchive();     $result $zip->open('demo.xlsx');if($result !== true) {    throw new RuntimeException('无法打开 XLSX');}
打开以后,可以读取 ZIP 内部的文件:
$xml = $zip->getFromName('xl/workbook.xml');
这里的 xl/workbook.xml 是 ZIP 内部路径。

3. 从 workbook.xml 找到 Sheet

workbook.xml 中会记录 Sheet:
<sheets>    <sheetname="Sheet1"r:id="rId1"/>    <sheetname="Sheet2"r:id="rId2"/></sheets>
但 r:id 并不是最终文件名。
还需要读取:
   xl/_rels/workbook.xml.rels
找到:
rId1 → worksheets/sheet1.xml      rId2 → worksheets/sheet2.xml
因此定位一个 Sheet 的过程是:
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');
实际代码中也是通过 workbook.xml 和关系文件来定位具体工作表。

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
其中: 代表单元格 A1。

5. 使用 DOMDocument 解析 XML

PHP 自带 DOMDocument:
$dom new DOMDocument('1.0''UTF-8');$dom->loadXML($xml);
XML 会被解析成 DOM 树。
然后使用 DOMXPath 查找节点:
$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;}
可能得到:
A1     B1C1 A2 B2   C2
这样就可以直接找到 Excel 中的单元格。

6. 修改一个单元格

例如把 A1 修改成:Hello World
可以找到:
$cell = $xpath->query(         '//x:c[@r=”A1”]'   )->item(0);
然后清空原来的 XML:
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);
最终对应的 XML 大概是:
<cr="A1"t="inlineStr">    <is>        <t>Hello World</t>    </is></c>      

7. 数字和字符串的 XML 不一样

数字可以直接写:
<cr="A1">    <v>123</v></c>    
PHP 示例:
$value = $dom->createElementNS(         $namespace,        'v',        '123'      );$cell->appendChild($value);      
字符串则可以使用:
<cr="A1">    <v>123</v></c>      
所以修改 Excel 单元格时,需要根据数据类型生成不同的 XML。

8. Excel 公式也是 XML

例如 Excel 中:=A1+B1
对应的 XML 可以是:
<cr="C1">    <f>A1+B1</f>    <v>300</v></c>      
其中:
``:公式
``:公式当前缓存结果
PHP 可以直接生成:
$formula = $dom->createElementNS(    $namespace,    'f',    'A1+B1');$value = $dom->createElementNS(    $namespace,    'v',    '300');$cell->appendChild($formula);$cell->appendChild($value);  

9. 为什么修改 XML 不会把 Excel 样式弄丢?

Excel 单元格除了内容,还可能包含样式信息:
<cr="A1"s="5">
这里的:s="5" 表示这个单元格使用某个样式。
如果我们只是修改 `` 里面的数据节点,而保留 `` 本身以及 `s` 属性,那么模板原来的样式仍然可以保留。
因此可以把职责拆成:
Excel 模板       ↓负责样式、布局、列宽、标题PHP      ↓负责数据和公式  
这也是模板驱动 Excel 的一个核心思路。

10. 修改完成后重新写回 ZIP

XML 修改完成后:
$newXml = $dom->saveXML();  
然后放回 XLSX:
$zip->addFromString(    'xl/worksheets/sheet1.xml',    $newXml);  
最后关闭:
$zip->close();  
原来的:demo.xlsx  就重新变成一个合法的 XLSX 文件。
完整流程可以浓缩成:
$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
所以,PHP 操作 XLSX 的底层逻辑并不复杂:

打开 ZIP → 找到 XML → 修改 XML → 放回 ZIP。

ZipArchive 负责 ZIP,DOMDocument 负责 XML,DOMXPath 负责定位 XML 节点。
理解了这个结构之后,Excel 就不再是一个只能通过专用库操作的黑盒,而变成了一个可以直接解析和修改的文件结构。

相关学习资料

返回首页浏览学习资料