利用DeepSeek辅助WPS电子表格ET格式分析
前面研究了电子表格的xlsx、xlsb、ODS格式,接下来研究WPS电子表格ET格式,这个格式网上的资料很少,WPS官方说 .et 格式是兼容 Xlsx 和 Xls 的。
然而,把它后缀改为xlsx,rust_sheet插件还是无法识别,报错
Binder Error:
Read spreadsheet failed: Invalid xlsx file format: File not found 'xl/_rels/workbook.xml.rels'
而把后缀改为xls,rust_sheet插件就能识别,怎么解释这样神奇的现象?
mv HelloWorld.et HelloWorld.et.xls./duckdb132D load rusty_sheet;
D from read_sheet('HelloWorld.et.xls');
┌─────────────┐
│ Hello World │
│ varchar │
├─────────────┤
│ 0 rows │
└─────────────┘
这要从xls格式的来源说起,以下是DeepSeek的详细说明
Microsoft Excel 97-2003 (XLS) 文件格式详解
Excel 97-2003 使用的 XLS 格式(也称为 BIFF8 格式)是 Microsoft 的二进制文件格式,在 Excel 2007 之前一直是标准格式。以下是该格式的全面介绍:
一、基本结构
XLS 文件是基于 复合文件二进制格式(Compound File Binary Format,简称 CFBF)的结构化存储文件,也称为 OLE2 或 DOCFILE 格式。
1. 存储结构特点
- 类似小型文件系统:包含"存储"(storages)和"流"(streams)
- 扇区分配:数据被分成512字节的扇区
- FAT表:文件分配表记录扇区链接关系
- 目录结构:包含文件内各部分的层次关系
二、核心组成部分
1. 主要存储流
\Workbook // 主工作簿数据
\SummaryInformation // 文档摘要信息
\DocumentSummaryInformation // 文档统计信息
2. Workbook 流结构
Workbook 流由一系列 BIFF记录(Binary Interchange File Format)组成:
记录类型 | 说明 |
---|---|
BOF | 工作簿/工作表开始标记 |
EOF | 结束标记 |
SST | 共享字符串表 |
BoundSheet | 工作表信息 |
DIMENSIONS | 工作表尺寸 |
ROW | 行信息 |
CELL | 单元格数据 |
FORMULA | 公式 |
FONT | 字体定义 |
XF | 单元格格式 |
STYLE | 样式定义 |
三、关键记录详解
1. 单元格数据存储
- 数字:直接以8字节双精度浮点存储
- 字符串:引用SST表中的索引
- 布尔值:0(FALSE)或1(TRUE)
- 错误值:特定错误代码
- 空白单元格:不存储,通过ROW记录推断
2. 共享字符串表(SST)
struct {uint16_t numStrings; // 字符串总数uint16_t numUniqueStrings;// 唯一字符串数XLUnicodeString strings[];// 字符串数组
}
3. 公式存储
struct {uint16_t row;uint16_t col;uint16_t xfIndex;uint64_t value; // 缓存的计算结果byte formula[]; // 公式的RPN表示
}
四、格式限制
项目 | 限制 |
---|---|
行数 | 65,536 (2^16) |
列数 | 256 (2^8) |
工作表数 | 受内存限制 |
单元格样式 | 4,000种左右 |
字符串长度 | 32,767字符 |
五、与新版XLSX对比
特性 | XLS | XLSX |
---|---|---|
格式类型 | 二进制 | XML压缩包 |
文件大小 | 相对较大 | 较小 |
恢复能力 | 差 | 好 |
最大行数 | 65,536 | 1,048,576 |
扩展性 | 有限 | 良好 |
加密支持 | 弱 | 强 |
六、编程处理
1. 使用Python读取
import xlrd# 打开XLS文件
workbook = xlrd.open_workbook("example.xls")# 获取第一个工作表
sheet = workbook.sheet_by_index(0)# 读取单元格数据
print(sheet.cell_value(0, 0)) # 第一行第一列
2. 使用Python写入
import xlwt# 创建工作簿
workbook = xlwt.Workbook()
sheet = workbook.add_sheet("Sheet1")# 写入数据
sheet.write(0, 0, "Hello World")# 保存文件
workbook.save("output.xls")
它的结构可以使用专业工具检查:
from olefile import OleFileIOtry:ole = OleFileIO("test.xls")print("有效的OLE结构:", ole.listdir())
except:print("不是完整的OLE文件")
我们用上述xlwt脚本生成一个HelloWorld.xls,用xlrt和olefile分别去读。
python3 xlrd2.pyHello Worldpython3 ole2.py
有效的OLE结构: [['Workbook']]
然后用WPS打开HelloWorld.xls,再另存为HelloWorld.et,用xlrt和olefile分别去读。
python3 xlrd2.pyHello Worldpython3 ole2.py
有效的OLE结构: [['\x05DocumentSummaryInformation'], ['\x05SummaryInformation'], ['ETExtData'], ['Workbook']]
可见,et格式是有效的OLE结构,但比标准xls格式多了3个额外的段,不影响读取。
那么它怎么又能兼容xlsx格式呢?问题就出在多出来的段上。
我们知道xlsx实际是zip格式,那ET也是zip格式吗,不完全是。
我用unzip工具去列出它包含的文件,结果是这样的。
unzip -l HelloWorld.et
存档文件: HelloWorld.et
warning [HelloWorld.et]: 10223 extra bytes at beginning or within zipfile(attempting to process anyway)Length Date Time Name
--------- ---------- ----- ----540 2012-07-02 09:52 [Content_Types].xml0 2012-07-02 09:52 _rels/310 2012-07-02 09:52 _rels/.rels0 2012-07-02 09:52 theme/0 2012-07-02 09:52 theme/theme/0 2012-07-02 09:52 theme/theme/_rels/283 2012-07-02 09:52 theme/theme/_rels/themeManager.xml.rels6432 2012-07-02 09:52 theme/theme/theme1.xml138 2012-07-02 09:52 theme/theme/themeManager.xml
--------- -------7703 9 files
unzip发现这个文件除了zip格式应该有的部分,还有10223个多余的字节,而且还能继续解压,但多余的字节解压不了,也就取不到真正的数据。
需要特别说明,WPS保存的xls格式并非标准xls格式,仅仅是改个后缀。它利用了OLE结构允许多个段,而读取xls格式的程序只去读’Workbook’段这个特点实现了“兼容”。本质上就是带有xls段和zip信息的混合格式。
再将它与xlsx格式比较,用WPS打开HelloWorld.et,再另存为HelloWorld.xlsx,用unzip工具去列出:
unzip -l HelloWorld.xlsx
存档文件: HelloWorld.xlsxLength Date Time Name
--------- ---------- ----- ----1293 2012-07-02 09:52 [Content_Types].xml0 2012-07-02 09:52 _rels/735 2012-07-02 09:52 _rels/.rels0 2012-07-02 09:52 docProps/564 2012-07-02 09:52 docProps/app.xml573 2012-07-02 09:52 docProps/core.xml526 2012-07-02 09:52 docProps/custom.xml0 2012-07-02 09:52 xl/0 2012-07-02 09:52 xl/_rels/698 2012-07-02 09:52 xl/_rels/workbook.xml.rels187 2012-07-02 09:52 xl/sharedStrings.xml20006 2012-07-02 09:52 xl/styles.xml0 2012-07-02 09:52 xl/theme/6432 2012-07-02 09:52 xl/theme/theme1.xml477 2012-07-02 09:52 xl/workbook.xml0 2012-07-02 09:52 xl/worksheets/1309 2012-07-02 09:52 xl/worksheets/sheet1.xml
--------- -------32800 17 files
可见,这个文件没有多余字节,真正的数据保存在sharedStrings.xml和xl/worksheets/sheet1.xml中,这在上文已经分析过了。
为什么WPS能这么干呢?因为zip格式是允许文件中间有多余部分的,以下是DeepSeek的详细解释。
ZIP 文件格式详解及安全修改指南
ZIP 文件格式基本结构
ZIP 文件是一种常见的归档文件格式,其基本结构如下:
1. 核心组成部分
[Local File Header 1]
[File Data 1]
[Local File Header 2]
[File Data 2]
...
[Central Directory]
[End of Central Directory Record]
2. 关键结构说明
-
Local File Header:每个文件的本地头,包含文件名、压缩方法等元数据
- 签名:0x04034b50 (PK\x03\x04)
- 结构:30字节固定部分 + 文件名 + 额外字段
-
File Data:实际压缩后的文件内容
-
Central Directory:中央目录,包含所有文件的索引信息
- 签名:0x02014b50 (PK\x01\x02)
- 包含所有文件的完整路径、压缩信息、偏移量等
-
End of Central Directory Record:目录结束标记
- 签名:0x06054b50 (PK\x05\x06)
- 包含中央目录的位置和文件总数
在ZIP中添加内容而不影响解压的方法
方法1:利用ZIP注释字段(推荐)
import zipfiledef add_to_zip_comment(zip_path, extra_data, output_path):with zipfile.ZipFile(zip_path, 'a') as zf:# 获取原注释orig_comment = zf.comment# 追加新数据new_comment = orig_comment + extra_data.encode('utf-8')# 创建新ZIP文件with zipfile.ZipFile(output_path, 'w') as new_zip:# 复制所有原文件for item in zf.infolist():with zf.open(item.filename) as f:new_zip.writestr(item, f.read())# 设置新注释new_zip.comment = new_comment# 使用示例
add_to_zip_comment('example.zip', '这是我的额外数据', 'modified.zip')
优点:
- 完全符合ZIP规范
- 所有解压工具都能正常处理
- 不会影响文件校验和
方法2:在Central Directory后添加数据(兼容性较好)
- 找到End of Central Directory Record的起始位置
- 在其前面插入额外数据
- 更新End of Central Directory Record中的偏移量
def append_after_central_dir(original_zip, extra_data, output_zip):with open(original_zip, 'rb') as f:data = f.read()# 查找End of Central Directory Recordeocd_pos = data.rfind(b'PK\x05\x06')if eocd_pos == -1:raise ValueError("无效的ZIP文件")# 在EOCD前插入数据new_data = data[:eocd_pos] + extra_data.encode() + data[eocd_pos:]with open(output_zip, 'wb') as f:f.write(new_data)
方法3:利用Extra Field字段(针对特定文件)
def add_to_extra_field(zip_path, output_path):with zipfile.ZipFile(zip_path, 'a') as zf:# 获取第一个文件的信息file_list = zf.infolist()if not file_list:raise ValueError("ZIP文件为空")# 修改extra字段file_info = file_list[0]file_info.extra = b'MyExtraData\x00\x01' + file_info.extra# 创建新ZIP文件with zipfile.ZipFile(output_path, 'w') as new_zip:for item in file_list:with zf.open(item.filename) as f:content = f.read()new_zip.writestr(item, content)
注意事项
-
签名冲突:确保添加的数据不包含PK\x03\x04、PK\x01\x02等ZIP魔术数字
-
文件校验:修改后建议验证ZIP文件完整性
zipfile.is_zipfile('modified.zip') # 验证ZIP有效性
-
大小限制:
- ZIP注释最大65535字节
- Extra Field字段通常限制为65535字节
-
兼容性测试:建议在目标解压环境中测试修改后的文件
高级技巧:隐藏数据
对于需要隐蔽存储的场景,可以:
- 将数据加密后存入注释
- 使用StegZIP等工具在最低有效位(LSB)隐藏信息
- 利用ZIP64扩展存储大容量额外数据
以上方法都能确保标准解压工具正常解压原始内容,同时携带额外信息。