xlsx纯标准库只读解析法
适用场景
系统需要读取已有 .xlsx 数据,但环境无 openpyxl(HFMS 两 Python 均无),且要求纯标准库零依赖。典型:把 Excel 数据源(财务账目、健康监测表、资产清单)导入 HFMS 台账前的探查与解析。输出 {sheet名: [ {列号: 单元格值} ... ]},业务层再按列号映射字段。
操作步骤
- 开包与共享字符串:
zipfile.ZipFile(path);若含xl/sharedStrings.xml,先解析为shared[](<si><t>拼接),供t="s"单元格按索引取串。 - sheet 名 + 路径映射:读
xl/workbook.xml拿(name, r:id);读xl/_rels/workbook.xml.rels把rId→Target。Target 可能带前导/(/xl/worksheets/sheetN.xml),必须先lstrip('/')再判startswith('xl/'),否则路径变xl//xl/...报 KeyError。 - 单元格取值:遍历
worksheet的row/c,按t:s→shared[int(v)];inlineStr→<is><t>拼接;无t或t="n"→<v>数值;t="str"→公式串。列字母col_to_idx(A=0 26 进制),ref 拆col+row。 - 返回结构:
{sheet名: [ {colIdx: val}, ... ]}(仅非空行),业务层按列号取字段。 - 日期换算:Excel 日期常为序列号(自 1899-12-30 起的天数),用
date(1899,12,30)+timedelta(days=int(n));时间分数(0.0833=02:00)用*24取 HH:MM。
核心解析器(可直接复用)
import zipfile, re, datetime, xml.etree.ElementTree as ET
NS = "{http://schemas.openxmlformats.org/spreadsheetml/2006/main}"
def col_to_idx(col):
i = 0
for ch in col: i = i * 26 + (ord(ch) - ord('A') + 1)
return i - 1
def split_ref(ref):
m = re.match(r"([A-Z]+)(\d+)", ref); return m.group(1), int(m.group(2))
def read_xlsx(path):
z = zipfile.ZipFile(path); sh = []
if "xl/sharedStrings.xml" in z.namelist():
for si in ET.fromstring(z.read("xl/sharedStrings.xml")).findall(NS + "si"):
sh.append("".join(t.text or "" for t in si.iter(NS + "t")))
wb = ET.fromstring(z.read("xl/workbook.xml")); sheets = []
for s in wb.find(NS + "sheets").findall(NS + "sheet"):
sheets.append((s.get("name"),
s.get("{http://schemas.openxmlformats.org/officeDocument/2006/relationships}id")))
rels = ET.fromstring(z.read("xl/_rels/workbook.xml.rels")); r2t = {}
for r in rels.findall("{http://schemas.openxmlformats.org/package/2006/relationships}Relationship"):
r2t[r.get("Id")] = r.get("Target")
out = {}
for name, rid in sheets:
t = r2t[rid].lstrip("/"); t = t if t.startswith("xl/") else "xl/" + t
root = ET.fromstring(z.read(t)); rows = []
for row in root.iter(NS + "row"):
cells = {}
for c in row.findall(NS + "c"):
col, num = split_ref(c.get("r")); tt = c.get("t")
v = c.find(NS + "v"); is_ = c.find(NS + "is")
if tt == "s" and v is not None: val = sh[int(v.text)]
elif tt == "inlineStr" and is_ is not None:
val = "".join(x.text or "" for x in is_.iter(NS + "t"))
elif v is not None: val = v.text
else: val = ""
cells[col_to_idx(col)] = val
if cells: rows.append(cells)
out[name] = rows
return out
注意边界
- 覆盖最常见布局(共享串 + 内联串 + 数值);合并单元格、公式结果(
<v>已是缓存值)、富文本<r>按现有逻辑够用。 - 单元格
t缺省且<v>是数字=数值;t="s"才查共享串——务必先判 t 再取,否则串被当数字。 - 日期是序列号而非文本时,必须换算;文本日期用
datetime.fromisoformat校验,别用 SQLGLOB '____-__-__'(_是字面量非通配符,会假阳性报"非法日期")。 - 源表常有脏数据:转置宽表、参考列("标准*")、姓名/出生日期元数据行——业务层按列号 + 数值范围过滤,解析器不负责。
反例避雷
- Target 带前导
/不lstrip('/')→xl//xl/worksheets/...KeyError(ZQ 资产踩坑,已修)。 - 用
GLOB '_'判非法日期 → 误报"非法日期149"(CW 财务踩坑);改用datetime.fromisoformat异常捕获。 - 把"标准高压/低压"参考列或"姓名/出生日期"元数据当数据行 → 导入污染;按
col6 非数字 / 数值越界过滤。 - 引 openpyxl → 违反纯标准库零依赖;手写 OOXML 读取即可。
关联笔记
- [[技能|xlsx报表纯标准库离线导出法]](互补:导出用 inlineStr 写,本技能用 zipfile 读)
- [[技能|台账体系SPECS驱动扩展法]]
实战战绩
2026-10-03 实战:CW 财务(房租水电/保险/工资,11 sheet 结构化表)、HL 健康(血压身高体重转置宽表 + 脏模板、陈逐时体温)、ZQ 资产清单查重(291 条只读镜像比对),全部用此解析器,零第三方依赖。
版本记录
| 版本 | 日期 | 变更 |
|---|---|---|
| v1.0.0 | 2026-10-03 | 据本会话 4 次 xlsx 读取实战固化,含绝对路径 Target lstrip('/') 修复与序列号日期换算 |