ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用python-docx高效提取Word高亮文本的实战方法

用python-docx高效提取Word高亮文本的实战方法 上个月帮一个朋友整理项目资料对方发来一份几十页的 Word 文档里面用黄色、绿色、粉色高亮标了一堆重点。整理要求也很简单把文档里所有高亮内容单独拎出来做一份新的重点清单。我一开始觉得这是个体力活打开文档翻了三四页就意识到不对——页数太多、高亮太碎、纯靠手翻不仅慢还容易漏。后来我直接把活儿交给了 python-docx用脚本把每个高亮 run 提取出来几分钟就把事情干完了。这里要特别说明一点python-docx 处理的是 .docx 格式它本身提供了 run 级别的高亮读取能力。这篇实操笔记适合不想在 Word 里手动翻高亮、需要批量提取或周期性处理文档的读者不管你是做文档整理、知识库预处理还是从审阅稿里收集意见都可以直接参考。1. 先说清楚python-docx 和 Word 高亮之间的“最后一公里”1.1 为什么这个需求适合用代码解决很多人第一次听说 python-docx 能提取高亮第一反应是“这东西也能从 Word 里读出来”实际上能而且很稳。Word 里的文字高亮在文件底层是一个很明确的标记python-docx 直接暴露了读取入口。用脚本处理的最大好处有三点第一批量能力是手翻没法比的。一份文档手动扫一遍可能只要十分钟十份文档就是两个小时起步而且翻到后面眼睛容易花很容易漏掉某一段高亮。脚本处理十份文档和一份文档的时间差距几乎可以忽略。第二提取结果可以二次加工。手动整理出来的高亮只能复制粘贴到新的文档里而脚本可以把每条高亮连同颜色、所在段落、所在表格位置一起导出来做成 Excel 或 Markdown方便继续做分类、统计、喂给知识库。第三可重复执行。如果这份文档是周期性更新的比如每周都会收到一份新版本写好的脚本可以直接复用换一个文件路径跑一遍就出结果不用每次重新整理。1.2 拆开 docx 看一眼你就明白原理了在写代码之前我建议先搞清楚 Word 文档的底层结构。.docx 本质上是一个 zip 压缩包里面最主要的内容文件是word/document.xml你看到的正文、表格、样式全都在这个 XML 文件里。文档里的每一段文字在 XML 里会拆成多个w:r节点run一段连续格式的文字就是一个 run。高亮信息就挂在 run 自己的格式描述节点w:rPr里面长这样w:r w:rPr w:highlight w:valyellow/ /w:rPr w:t这段文字是黄色高亮/w:t /w:rruby-docx 做的事情本质上就是解析这个 XML 结构。python-docx把w:r封装成了Run对象把w:rPr里的高亮信息封装成了run.font.highlight_color属性。理解了这个层关系后面不管是直接用现成属性还是回到 XML 层自己解析心里都有底。2. 提取前的准备和“高亮”的定义2.1 安装 python-docx注意版本第一步没有坑用 pip 直接装pip install python-docx但有两个细节我想提醒一下。python-docx 的版本迭代中读取高亮颜色的能力是在 0.8.8 版本左右开始提供完整支持的。如果你之前装过比较老的版本建议先升级一下pip install -U python-docx装好后验证一下版本确保能正常 importpython -c import docx; print(docx.__version__)我本地测试用的是 1.1.0读高亮、读表格、处理嵌套结构都没有问题。如果你在比较老的生产环境里跑建议先用print(docx.__version__)确认一下太低的话优先升级。2.2 先分清楚高亮、底纹、字体颜色不是一回事这块是从实际需求里最容易踩坑的地方。用户在 Word 里看到的“背景有颜色”实际可能是三种完全不同的底层实现视觉效果Word 里的入口底层 XML 标记python-docx 能否直接读取文本高亮“开始”菜单里的“文本突出显示颜色”w:highlight w:valyellow/能用font.highlight_color字符/段落底纹“开始”菜单里的“底纹”或“边框和底纹”w:shd w:fillFFFF00/不能直接用属性需要解析 XML字体颜色字体颜色按钮给文字本身改颜色w:color w:valFF0000/能用font.color.rgb也就是说如果你的目标是把“用高亮笔划过的内容”提取出来那么需要w:highlight标记如果对方说的是“有颜色背景的文字”那可能是底纹需要走另一条解析路径。我拿到的很多文档肉眼看着是一样的黄色背景但 XML 里有的走 highlight有的走 shd。所以在写脚本之前最好先确认你要处理的文档到底属于哪一种。2.3 doc 老格式的转换提醒python-docx 只能读取 .docx 格式对 Word 97-2003 时代的 .doc 二进制格式完全不支持。如果你拿到的文件后缀是 .doc直接用 python-docx 打开会报错。解决方案也很简单先用 Word、WPS 或 LibreOffice 打开文件另存为 .docx 格式然后再跑脚本。如果文件特别多可以用 LibreOffice 的命令行模式批量转换这个后续可以单独写一篇。重点是转换这一步绕不开别在 .doc 文件上浪费时间。3. 核心实操把文档里的高亮内容完整地抠出来3.1 第一种写法直接用 highlight_color 属性如果 Python 版本和 python-docx 版本都不算老用属性读取是最直接的方案。from docx import Document def extract_highlights_from_paragraph(paragraph): result [] for run in paragraph.runs: color run.font.highlight_color if color is not None: result.append((run.text, color)) return result doc Document(example.docx) for para in doc.paragraphs: for text, color in extract_highlights_from_paragraph(para): print(f[{color}] {text})这里有一个判断细节run.font.highlight_color在没有高亮的情况下返回None所以只需要判断is not None就行。没必要去对比WD_COLOR_INDEX.NONE因为正常保存的 docx 里没高亮时压根就不会写这个属性。color返回的是一个枚举值比如WD_COLOR_INDEX.YELLOW。如果你直接打印会看到类似WD_COLOR_INDEX.YELLOW (3)的输出能看懂是黄色但想要更友好的中文名称可以自己做一层映射后面我会给一个颜色对照表。3.2 第二种写法解析 XML更通用更可控属性写法虽然简单但如果遇到老版本 python-docx、特殊模板、或者只想快速验证某个 run 的原始 XML 信息直接解析 XML 也是一种很稳定的方式。from docx import Document from docx.oxml.ns import qn def extract_highlights_from_paragraph_xml(paragraph): result [] for run in paragraph.runs: rPr run._element.rPr if rPr is None: continue highlight rPr.find(qn(w:highlight)) if highlight is not None: color highlight.get(qn(w:val)) result.append((run.text, color)) return result这段代码的核心是run._element它把 python-docx 的Run对象还原成底层的 XML 元素然后用rPr.find找到w:highlight节点再通过get拿到颜色值。这里的颜色值是字符串比如yellow、green更直观。两种方式该怎么选我的建议是新项目直接用font.highlight_color代码简洁但对版本环境不放心时用 XML 解析兜底两种方式结果是一致的。我在实际项目里经常同时保留两个函数调试阶段用 XML 解析打印原始信息确认无误后再切回属性方式。3.3 把相邻高亮文本合并避免碎片化Word 在保存文档时同一句话里的文字因为格式判断不同经常被拆成多个 run。比如一整句黄色高亮的话可能被拆成两三个相邻的 run如果我们只是逐 run 提取导出的结果里就会出现一句完整的话被切成几段的情况。我写了一个简单的合并逻辑遍历段落里的所有 run把相邻且颜色相同的高亮内容拼起来遇到颜色变化或没有高亮的部分就截断。from docx import Document from docx.enum.text import WD_COLOR_INDEX def extract_merged_highlights(paragraph): groups [] current_text current_color None for run in paragraph.runs: color run.font.highlight_color if color is not None: if current_color color: current_text run.text else: if current_color is not None: groups.append((current_text, current_color)) current_text run.text current_color color else: if current_color is not None: groups.append((current_text, current_color)) current_text current_color None if current_color is not None: groups.append((current_text, current_color)) return groups这个函数会返回一个列表每个元素是(文本, 颜色)的元组。实测下来对大部分文档的提取结果看起来舒服很多不再是一堆碎片。3.4 别漏掉表格、页眉页脚和嵌套结构很多人第一次写完提取脚本跑完正文段落发现结果不完整很大概率是漏了表格里的内容。Word 文档里表格单元格中的段落不在doc.paragraphs里需要通过表格对象逐层进去。def unique_cells(cells): seen set() for cell in cells: if id(cell._tc) not in seen: seen.add(id(cell._tc)) yield cell def extract_highlights_from_table(table): results [] for row in table.rows: for cell in unique_cells(row.cells): for para in cell.paragraphs: results.extend(extract_merged_highlights(para)) for nested_table in cell.tables: results.extend(extract_highlights_from_table(nested_table)) return results里面那个unique_cells是处理合并单元格的。python-docx 在遍历行单元格时合并过的单元格会出现多次如果不加去重同一个高亮内容会被重复提取。按id(cell._tc)去重是实测比较稳的方案_tc是单元格的底层 XML 节点合并单元格的底层节点是同一个。页眉页脚里的文字也可能有高亮虽然场景不多但有备无患doc Document(example.docx) for section in doc.sections: for para in section.header.paragraphs: for text, color in extract_merged_highlights(para): print(f[页眉][{color}] {text}) for para in section.footer.paragraphs: for text, color in extract_merged_highlights(para): print(f[页脚][{color}] {text})把正文段落、表格、页眉页脚都封装到一个总入口里就是一个比较完整的提取脚本了。4. 进阶玩法提取之后的数据能怎么用4.1 按颜色分类输出还原文档批注逻辑很多人在文档里用不同颜色表达不同含义比如黄色代表重点内容绿色代表待办事项粉色代表问题风险。如果只是把所有高亮汇成一列等于丢掉了这批信息的结构化属性。我在提取之后先按颜色分组再汇总输出效果会好很多。常见的颜色对照关系可以整理成一张表写进代码里WD_COLOR_INDEX枚举XML 里的 val中文名HTML 近似颜色YELLOWyellow黄色FFFF00GREENgreen绿色00FF00BRIGHT_GREENbrightGreen亮绿00B050PINKpink粉色FF00FFREDred红色FF0000BLUEblue蓝色0000FFDARK_BLUEdarkBlue深蓝000080TURQUOISEturquoise青绿色00FFFFDARK_YELLOWdarkYellow深黄808000GRAY_25gray25浅灰C0C0C0我在实际项目中用的方案是所有提取结果先放进一个列表每个元素不只存文本和颜色还存段落序号、是否来自表格这样后续可以按颜色分组也可以按段落顺序排序还原文档原始结构。4.2 导出成 Excel、Markdown 或者 JSON提取出来之后直接打印在控制台只能用于验证。真正要交给别人或者做后续处理我一般导出成三类格式。导成 Markdown 最简单适合直接放进文档或知识库def export_to_markdown(highlights, output_pathhighlights.md): with open(output_path, w, encodingutf-8) as f: for text, color in highlights: f.write(f- [{color}] {text}\n)导成 JSON 适合程序继续处理比如接入自动化流程或者知识库解析import json data [ {text: text, color: str(color)} for text, color in highlights ] with open(highlights.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2)导成 Excel 更符合一般办公场景可以用 pandaspip install pandas openpyxlimport pandas as pd df pd.DataFrame(highlights, columns[文本, 颜色]) df.to_excel(highlights.xlsx, indexFalse)导出的 Excel 可以直接交给同事或领导汇总查看字段简单清晰。4.3 批量处理整个文件夹的 Word 文档如果你手头不是一份文档而是几十份甚至上百份批量处理的能力才是真正的刚需。用pathlib遍历目录把所有.docx文件里的高亮内容汇总到一起from pathlib import Path import pandas as pd def collect_highlights_from_docx(path): doc Document(str(path)) highlights [] for para in doc.paragraphs: highlights.extend(extract_merged_highlights(para)) for table in doc.tables: highlights.extend(extract_highlights_from_table(table)) return highlights all_rows [] for file_path in Path(docs).glob(*.docx): rows collect_highlights_from_docx(file_path) for text, color in rows: all_rows.append({文件名: file_path.name, 文本: text, 颜色: str(color)}) df pd.DataFrame(all_rows) df.to_excel(全部高亮汇总.xlsx, indexFalse)这样跑完以后你得到的就是一份包含所有源文件、高亮文本和颜色的总表后续筛选、排序、统计都非常方便。我实际处理过一个大目录大概六十多份 docx跑完不到三十秒比人工翻快太多了。5. 踩坑记录和排查思路5.1 提取出来是空的但文档里明明有高亮这是被人问得最多的问题。首先要确认高亮是不是用“文本突出显示颜色”做的而不是“底纹”或“字符底纹”。前面说过底纹走的是w:shd不是w:highlight用font.highlight_color是读不出来的。判断方法很直接在文档里选中那些有颜色的文字看“开始”菜单里“文本突出显示颜色”的按钮是否处于选中状态。如果按钮没有高亮显示说明它是底纹或者字体颜色需要换解析方案。另一种情况是文档里的“高亮感”其实是通过页面背景色或文本框底色实现的这种就更不是 run 级别的高亮了需要单独分析。5.2 文档看着有高亮但代码取不到这种情况多半是文档经过了多次编辑高亮标记被写在了样式级别比如某个自定义样式统一带高亮而不是 run 级别。python-docx 读取的是 run 上的直接格式样式层级的高亮不会体现在run.font.highlight_color里。我的排查思路是先解压 docx直接看 XML。把文件后缀改成 .zip解压后用文本编辑器打开word/document.xml搜索highlight关键词看看标记到底出现在哪个层级。如果是样式里的高亮规则会更复杂需要你自己决定是统一展开样式还是只提取 run 级别的高亮。5.3 用了别人的模板高亮颜色看着正常但读不出标准颜色Word 高亮的标准颜色是有限的几种用户在模板里看到的一些“高亮色”可能是通过自定义颜色值写到别的地方的。比如w:highlight w:valyellow/这类颜色是标准枚举而自定义颜色往往会落到w:shd w:fillXXXXXX/里。如果你拿到的文档是用 WPS 或某些插件生成的这类兼容性问题更常见。遇到这种情况不要想着用 python-docx 一个库通吃先解压看 XML 结构再决定解析策略。5.4 文档太大提取很慢怎么办python-docx 会把整个文档加载到内存然后再遍历。如果遇到几十 MB、上百 MB 的超大文档速度确实会慢内存占用也高。我的建议是我自己的使用习惯先用 python-docx 完成常规场景只有遇到明显卡顿再考虑底层优化。底层优化思路是直接用标准库zipfile读取word/document.xml然后用lxml的迭代解析方式逐节点处理不把整份文档载入内存。但日常处理普通文档python-docx 足够不需要为此提前引入复杂度。5.5 关于版本兼容和扩展名最后提醒一个很多人忽略的点python-docx 只能处理 .docx文件如果实际是 .doc 或者伪装成 .docx 的其他格式打开时会直接报错。遇到PackageNotFoundError或者BadZipFile这类异常先检查文件本身是不是真正的 .docx 格式。另外如果用的是旧版 python-docx有些新 API 可能不可用比如font.highlight_color在非常老的版本里可能是未定义属性运行时会报AttributeError。这种情况下用 XML 解析方案绕过去就行了。最后的一点个人经验折腾完这整套提取流程后我最大的体会是处理 Office 文档任何现成库都不可能覆盖所有边界情况但大部分常规需求其实都能靠 python-docx 解决真正难处理的是那些“看起来像高亮”的格式变体。遇到问题先别急着换库把 docx 解压出来看一眼document.xml80% 的疑问都能在那里找到答案。我自己的习惯是写脚本时永远保留两个函数一个用现成属性一个解析 XML调试的时候对着看往往能很快定位问题。希望这套流程对你有用哪怕只帮你节约一次手动翻文档的时间也值得了。
返回列表