简介面向数码照片发烧友与注重打印质量的家庭用户这份docx规格说明文档围绕惠普Photo smart 7660和爱普生Stylus Photo R210两款家用照片打印机展开对比帮助读者快速把握选购要点。包体为单个Word文档约239KB结构简洁便于直接查阅。目前已有109人学习浏览。文档从适合人群、特色功能、打印速度、打印精度、墨滴技术、墨盒系统与参考价格等维度逐一分析例如两者均支持内置读卡器、液晶屏与无边距打印爱普生还支持光盘盘面打印速度方面黑白12页/分钟、彩色11页/分钟分辨率分别达到4800dpi和5760dpi。同时解读了热泡与微压电技术对输出细节的影响以及六色分体墨盒与恒彩照片墨盒在成本控制上的差异让用户能依据自身对质量、功能与长期使用的偏好做出更理性的选择。1. 产品规格资料.docx 为什么不能当普通文档处理产品规格资料.docx 这类文件在 Word 里打开通常只有十几页但用程序提取内容时它可能是二十多个段落、六七个表格和十几处合并单元格的组合。下游要的不是排版而是「字段名 → 值」的结构化数据型号、材料、公差、版本号、审批状态缺一个都不能入库。产品规格资料.docx 的解析难点不在 Word 格式本身而在于它的正文和表格混排、单元格合并规则、以及一段文字被拆成多个 XML 节点这三种情况。下面从文件结构讲起给出一套能直接在命令行跑的解析脚本、表格修正方案和入库前的校验规则最终把一份 docx 落成 JSON 或 CSV喂给物料系统和 CI 流程。2. 拆开产品规格资料.docxZIP、XML 与 WordprocessingML2.1 先把后缀改成 zip看看里面装了什么先解决「解析对象的本质」问题。产品规格资料.docx 不是一种纯文本格式而是一个 ZIP 压缩包。把副本后缀改成 .zip或者直接用 unzip 解压就能在不打开 Office 的情况下看到内部文件mkdir spec_unzip unzip -o 产品规格资料.docx -d spec_unzip/ find spec_unzip -type f | sort输出结构大致如下spec_unzip/ ├── [Content_Types].xml ├── _rels/.rels ├── docProps/ │ ├── app.xml │ └── core.xml └── word/ ├── document.xml ├── styles.xml ├── media/ │ └── image1.png └── tables.xml所有可见文本基本存在word/document.xml这一个文件里docProps/core.xml保存版本、创建人、修改人等元数据图片放在word/media/下面。如果在 Linux 或 CI 容器里解压遇到中文文件名乱码通常是压缩包用了 Unicode 编码而本地 locale 不对先执行export LANGzh_CN.UTF-8再解压。2.2 正文和表格在 XML 里的容器打开 document.xml 会看到文本都在w:p和w:tbl里分别代表段落和表格。w:tbl中包含多行w:tr每行再分若干w:tcw:tc里放文本段落。把这种结构与开发者的习惯对应起来逻辑对象XML 标签python-docx 对应类注意点段落w:pParagraph一段文字可能拆在多个 w:r 里表格w:tblTable只有顶层表会出现在 doc.tables单元格w:tc_Cell合并过的单元格会被重复返回属性docProps/core.xmlCoreProperties保存时间由 Office 覆盖不能当人工填写值这张表是后续解析脚本的索引。遇到提取结果对不上号时先回到这一层判断是「结构问题」还是「解析代码问题」。比如 product spec 表格里常见的「材料」「表面处理」两列在 XML 里可能被拆成两个表格而非一个直接按列名索引就会拿空值。2.3 为什么不能直接 grep document.xml用字符串匹配去 document.xml 里搜「产品规格」四个字运气好能命中运气不好命中不了。Word 保存文档时会把「产品规格」拆成多个w:r运行单元比如「产品」一个w:t、「规格」另一个w:t中间可能还夹着样式切换标签。grep 连续文本会直接拿不到结果。要验证这种拆段xmllint --format spec_unzip/word/document.xml | grep -n w:t | head -20输出里能看到大量短w:t片段每个只有几个字。正则匹配失败后很多工程师会去抓w:p的 textContent但自己拼逐段文本太容易漏掉空段落和制表符。真正省事的方案是用现成的 python-docx它把w:r的文本拼接逻辑封装在paragraph.text里不会漏字解析速度也足够。下一章直接进入可运行的脚本。3. 用 python-docx 批量解析产品规格资料.docx 的骨架代码3.1 搭起最小解析环境解析产品规格资料.docx 最常见的方法是使用 python-docx这是一个专门读写 WordprocessingML 的库。安装并打开文档pip install python-docxfrom docx import Document doc Document(产品规格资料.docx) print(顶层段落数:, len(doc.paragraphs)) print(顶层表格数:, len(doc.tables))Document()负责解压和解析整个包doc.paragraphs是正文里所有顶层段落的列表doc.tables是所有顶层表格。注意「顶层」这个词——嵌在表格单元格里的小表格不会出现在doc.tables里这个在下一章专门处理。如果这一步打印出的段落数是 0先检查文件是否损坏用办公软件另存为 .docx 再重试如果命令行里报 UnicodeEncodeError先执行export LANGzh_CN.UTF-8。3.2 按文档顺序提取段落和表格产品规格资料.docx 通常是「段落 → 表格 → 段落 → 表格」的交错结构只打印段落再打印表格顺序会乱。要按文档原始顺序遍历 body 的子节点from docx import Document from docx.table import Table from docx.text.paragraph import Paragraph from docx.oxml.ns import qn def iter_block_items(doc): 按文档顺序产出 Paragraph 或 Table parent doc.element.body for child in parent.iterchildren(): if child.tag qn(w:p): yield Paragraph(child, doc) elif child.tag qn(w:tbl): yield Table(child, doc) doc Document(产品规格资料.docx) for idx, block in enumerate(iter_block_items(doc)): if isinstance(block, Paragraph): text block.text.strip() if text: print(f{idx:3} [段落] {text}) else: print(f{idx:3} [表格] {len(block.rows)}行 x {len(block.columns)}列)doc.element.body拿到的是正文的 XML 元素iterchildren()按 XML 中的实际书写顺序遍历每个子标签qn(w:p)把命名空间前缀转成完整标签名。当isinstance判断到表格时len(block.rows)是行数len(block.columns)是列数这两个值来自表格网格定义合并单元格不会影响行列总数。段落为空时直接continue能过滤掉大量排版产生的空段落。打印结果建议先输出到文件再进行分析避免控制台刷屏。还需要注意paragraph.style.name可以读到该段落应用的样式名如果文档里的标题用了 Word 内置的 Heading 1样式名可以作为分节依据但模板自定义样式名时样式匹配只能作参考不能当作字段判断的唯一标准。3.3 读取版本号、创建人和修改人产品规格资料.docx 最容易被忽略的信息源是文档属性。不确定版本信息写在哪时先看元数据doc Document(产品规格资料.docx) cp doc.core_properties print(标题:, cp.title) print(作者:, cp.author) print(最近修改人:, cp.last_modified_by) print(创建时间:, cp.created.isoformat() if cp.created else 无) print(修改时间:, cp.modified.isoformat() if cp.modified else 无)core_properties对应压缩包里的docProps/core.xmllast_modified_by在 Office 每次保存时由客户端自动更新created对应当前文件首次保存时间。如果created是 None说明文件由 WPS 或第三方工具生成没有写入标准属性域。此时版本号不要依赖元数据改从正文段落中按「版本: 1.2」这种模式用正则提取。元数据只能作辅助信号文件被批量另存过之后作者和创建时间都可能失真。4. 产品规格资料.docx 表格解析合并单元格与嵌套表格的现实处理4.1 合并单元格为什么会让行错位产品规格资料.docx 的规格表多数带合并单元格。列合并时一个单元格横跨多列行合并时一个单元格纵向占多行。XML 里列合并用w:gridSpan val2表示行合并用w:vMerge表示。python-docx 为了视图兼容把合并后的单元格对象在相关行里重复返回这时候按行列号取值结果必然偏移行合并的续行返回同一个单元格对象值被复制一遍列合并的单元格被拆成多个假列导致下游 CSV 错位。所以处理合并单元格的第一步是不再依赖row.cells的视角回到原始 XML 层取数。4.2 从 tc 层直接读取绕开重复单元格处理合并单元格最直接的办法是不用row.cells而是访问行元素里的原始w:tc列表from docx import Document from docx.oxml.ns import qn doc Document(产品规格资料.docx) def cell_text(tc): runs tc.iter(qn(w:t)) return .join(node.text or for node in runs).strip() def is_vmerge_continue(tc): tcPr tc.tcPr if tcPr is None: return False vmerge tcPr.find(qn(w:vMerge)) if vmerge is None: return False return vmerge.get(qn(w:val)) in (None, continue) for ti, table in enumerate(doc.tables): print(f 表格 {ti} ) for row in table.rows: line [] for tc in row._tr.tc_lst: if is_vmerge_continue(tc): line.append(续) else: line.append(cell_text(tc)) print(row._tr.tr_idx, | .join(line))row._tr拿到的是底层w:tr元素tc_lst是该行原始单元格节点列表。遍历它时行合并的续行是一个独立的w:tc节点判断w:vMerge的属性val为空或continue表示它是延续格只放续占位val为restart时才是合并起点的单元格。列合并的单元格通过w:gridSpan判断不在本段代码的处理范围。这一段的输出不会再出现重复内容。row._tr.tr_idx返回行索引cell_text()用tc.iter(qn(w:t))遍历该单元格内所有文本节点能覆盖单元格里有多个段落的情况比分别取段落再拼接更彻底。如果下游要还原表形把续转成上一行的实际值即可。4.3 嵌套表格的递归提取产品规格资料.docx 里还可能出现「表格套表格」比如某个单元格里嵌着一张子规格表。顶层table.rows遍历时子表内容混在单元格文本里难以拆分。处理思路是递归找到单元格内的w:tbl元素把它包装成 Table 对象继续展开。from docx import Document from docx.oxml.ns import qn def extract_table(table): 把表格输出为嵌套列表遇到嵌套表格递归展开 result [] for row in table.rows: row_data [] for tc in row._tr.tc_lst: nested_tbls tc.findall(qn(w:tbl)) if nested_tbls: row_data.append([ extract_table(type(table)(tbl_el, table._parent)) for tbl_el in nested_tbls ]) else: texts .join(t.text or for t in tc.iter(qn(w:t))).strip() row_data.append(texts) result.append(row_data) return result doc Document(产品规格资料.docx) for i, table in enumerate(doc.tables): data extract_table(table) print(f表格{i} 解析完成行数 {len(data)})tc.findall(qn(w:tbl))返回该单元格内部的表格元素列表有返回值就说明存在嵌套。type(table)(tbl_el, table._parent)是把 XML 元素包回 Table 类实例的写法_parent传 doc 对象这样递归后的表格仍能正常访问rows。注意如果单元格既有文本又有嵌套表本代码优先展开子表外层文本会被丢弃要保留时可以把texts拼到第一个列表元素里。嵌套层级超过三层时输出结构建议直接用 JSON 序列化方便下游处理import json json.dump(data, open(spec.json, w, encodingutf-8), ensure_asciiFalse, indent2)ensure_asciiFalse保留中文indent2只用于人工排查正式入库时可以去掉减小体积。4.4 用 Pandas 做人工复核如果最终要进数据库或和 Excel 对比可以先把解析结果转成 DataFrameimport pandas as pd def table_to_df(table): rows [] for row in table.rows: cells [] for tc in row._tr.tc_lst: if is_vmerge_continue(tc): cells.append(None) else: cells.append(cell_text(tc)) rows.append(cells) return pd.DataFrame(rows) doc Document(产品规格资料.docx) df table_to_df(doc.tables[0]) print(df.to_markdown())这里复用了 4.2 里的is_vmerge_continue和cell_text续行位置用 None 表示DataFrame 里显示为 NaN。to_markdown()输出 Markdown 表格适合贴到 issue 或群里让同事核对。如果列数不齐DataFrame 会用 NaN 填充这正是要暴露的问题——解析后列数不一致说明文件里还有未识别的合并形式回 4.1 看原始 XML不要臆测规则。数值列后续用pd.to_numeric显式转换字符串里混着单位时先用正则拆开。5. 给产品规格资料.docx 解析结果加三道校验再交接给下游5.1 必填字段校验要把产品规格资料.docx 的解析结果入库先做字段完整性检查。规格文档至少要有「产品名称、型号、版本号、材质、尺寸公差」这几个字段。写一个最小的校验REQUIRED_FIELDS [产品名称, 型号, 版本号, 材质, 尺寸公差] def check_required(parsed: dict) - list: missing [k for k in REQUIRED_FIELDS if not parsed.get(k)] if missing: return f缺少字段: {, .join(missing)} return Noneparsed是前面解析脚本产出的字段字典KEY 按产品规格资料.docx 里的实际字段名统一定义。字段缺失时返回可读报错信息方便对文档编号定位。5.2 数值和单位成对检查规格表里常见的错误是「尺寸12.5」而没有单位。检查逻辑是字段名包含尺寸、重量、电压、功率时值里必须同时出现数字和单位后缀import re UNIT_FIELDS [尺寸, 重量, 电压, 功率, 公差] UNIT_PATTERN r(mm|cm|kg|V|W|Hz|℃)$ def check_units(parsed: dict) - list: warnings [] for field in UNIT_FIELDS: value str(parsed.get(field, )) if re.search(r\d, value) and not re.search(UNIT_PATTERN, value.strip()): warnings.append(f{field} 缺少单位: {value}) return warningsUNIT_PATTERN结尾用$锚定避免把「12mm直径」这种带中文后缀的写法放过。公差字段比较特殊值可能是「±0.05」这个模式下没有单位也会告警让核对人补上而不是静默入库。5.3 接入批任务用退出码挡住不完整文档校验函数写完后把整个解析脚本做成命令行入口python parse_spec.py 产品规格资料.docx --strict脚本内部校验失败时用raise SystemExit(1)errors check_required(parsed) or check_units(parsed) if errors: for e in errors: print([ERROR], e) raise SystemExit(1) print([OK], parsed[产品名称], parsed[版本号])批处理里遇到失败直接中断for f in 产品规格资料_*.docx; do python parse_spec.py $f --strict || exit 1 done把--strict开关加上CI 就能在物料导入前拦住那份只有半张表的规格文档。版本对比也走同一套脚本新旧两个文件分别解析成parsed_old、parsed_new再用{k: v for k, v in parsed_new.items() if parsed_old.get(k) ! v}输出变更项一次跑完就能看到这版规格改了什么参数、删了什么条目。本文还有配套的精品资源点击获取