ARTICLE DETAIL

资讯详情

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

用python-docx轻松提取Word高亮文字:原理、代码与踩坑记录

用python-docx轻松提取Word高亮文字:原理、代码与踩坑记录 在整理Word文档的过程中我遇到过一件非常磨人的事几十页的合同里对方用黄色高亮标出了十几处需要修改的条款我得一个个滚屏把高亮文字全部找出来逐条复制到邮件里回复。第一次干这活花了快一个小时第二次实在忍不了花了十分钟写了个python-docx脚本几秒钟就把所有高亮内容全部提取出来了。这篇内容就是把当时的完整思路、代码实现和踩过的坑重新梳理一遍。适合谁看经常处理Word文档、需要批量提取标注内容、或者想给文档处理流程提效的人无论你基础深浅照着操作基本都能跑通。1. 提取高亮这件事为什么值得用脚本解决很多人听到“提取Word高亮文字”第一反应是直接打开文档手动复制不就行了吗这个思路在小文档上没毛病但在真实工作场景里高亮通常意味着“重点”“待办”“需修改”往往散落在长文档的各个角落。人工提取最大的问题不是不会做而是不稳定——漏行、看花眼、重复复制甚至高亮颜色不同代表不同语义时还得一边滚屏一边做记号。一次两次可以忍如果每周都要处理这类文档就是在用时间成本为重复劳动买单。1.1 手动方案的上限在哪里我统计过自己手动提取的失误率一个30页的文档约40处高亮漏掉3到5处是非常正常的事。尤其是高亮恰好出现在表格里、页眉页脚、或者某个文本框中的时候肉眼扫过很容易当成普通修饰忽略掉。而高亮文字一旦漏掉后续传达信息就会出错轻则返工重则造成误解。手动方案的上限还体现在“提取后的二次整理”上。如果只是看一遍还好但如果要把高亮内容整理成表格、按颜色分类、或者录入到别的系统里手动复制回来还需要再做一轮格式清洗和归类这部分时间几乎是提取本身的2倍以上。脚本方案一次做好后续每次都是一个命令的事情。1.2 四个备选方案的优劣势对比我试过几种不同的技术路线各有各的适用场景简单对比如下。方案优点缺点适用场景win32com / pywin32直接调用Word程序保真度高能处理所有Word能识别的对象只能跑在WindowsOffice环境启动Word比较慢多次操作后Word进程可能卡顿残留对服务端环境不友好本地单机、需要连带处理修订或批注等复杂对象的场景直接解压docx读XML不依赖第三方库原理透明自己解析XML容易踩嵌套结构的坑正文和表格、文本框的标签结构不一致时写起来很痛苦本质上是重复造轮子只是临时看一下XML结构的好奇场景pandoc等转换工具转格式很快无法精确提取高亮标记转换后高亮样式基本会丢失不适合本需求python-docx跨平台纯Python实现不依赖Word程序API清晰在服务器或离线环境也能跑只能处理.docx格式旧版.doc需要先转换批量处理、自动化管道、团队共享脚本对比完就明白python-docx在“批量、自动化、跨平台”这几个维度上优势非常突出。它是解析文档结构而不是模拟人操作Word所以不会出现“Word打开弹出对话框卡住”这类意外问题也很适合放在定时任务或后台服务里跑。1.3 为什么最终落在python-docx上选择python-docx的核心原因是它把Word的底层XML结构封装成了比较直观的对象模型段落是Paragraph段内的子片段是Run高亮就是Run的一个字体属性。对大多数提取需求来说这意味着代码量可以压缩到很短而且语义清晰。另一个重要原因是纯Python实现意味着不需要在目标机器上安装Office部署成本低很多。后面要处理的文件多了这个优势会越来越明显。2. 高亮在docx文件里到底是怎么存的直接用python-docx之前建议先搞清楚一件事Word里用鼠标划一下给文字加上“黄色荧光笔”效果保存到文件里时到底发生了什么。只有理解了底层存储方式写代码时才能绕开那些莫名其妙的坑。2.1 docx的zip结构和XML基本盘一个看似普通的.docx文件实际上是一个zip压缩包。把后缀改成.zip再解压能看到word/document.xml、word/styles.xml、word/media/等一批文件。正文内容基本都在word/document.xml里而文档中每个段落的排版信息、字符级格式信息都嵌套在XML节点中。一个段落的XML结构大致长这样w:p w:r w:rPr w:highlight w:valyellow/ w:color w:valFF0000/ /w:rPr w:t这段文字被标黄了/w:t /w:r /w:p可以理解为w:p是段落w:r是段落里的一个片段runw:rPr是片段格式w:t是实际的文字内容。高亮样式就挂在w:rPr下面的w:highlight标签上w:val属性就是颜色名比如yellow、green、pink。2.2 w:highlight标签和python-docx的映射关系python-docx把XML结构做了面向对象映射。在代码里一个run就是Run对象它有一个font属性font对象上又有highlight_color这个属性类型是WD_COLOR_INDEX枚举或者None。判断某个run有没有高亮最简单的写法就是看run.font.highlight_color是不是None。比较有意思的一点是如果只是想“提取所有有高亮的文字”根本不用关心具体是什么颜色只要判断“有没有”就行。但如果不同颜色代表不同含义那就需要进一步判断颜色值。枚举类型和XML里的颜色名是对应的比如WD_COLOR_INDEX.YELLOW对应XML里的yellow。2.3 高亮和底纹的区别用Word时还有一个特别容易混淆的概念文本高亮和底纹shading。文本高亮就是“荧光笔”效果用w:highlight表示底纹是用段落或单元格的w:shd标签表示的视觉上很接近但完全是两种机制。python-docx对w:shd没有现成的高层API只能通过底层XML去读。如果你要提取的文档里别人用的是底纹而不是荧光笔高亮简单用highlight_color是提取不出来的。这也是很多人在试用脚本后发现“为什么我这里跑出来是空的”一个重要原因。做需求的时候先确认一下对方的操作习惯或者两者都做兼容会更稳妥。3. 最核心场景遍历段落runs提取高亮文字理解了底层的结构核心代码其实不复杂。它的逻辑就三步打开文档、遍历所有段落、遍历每个段落里的所有run只要run有高亮颜色就把文字收起来。3.1 基础提取代码与逐行解释直接给一个能跑的版本。from docx import Document from docx.enum.text import WD_COLOR_INDEX def extract_highlight_from_paragraphs(docx_path): doc Document(docx_path) results [] for para in doc.paragraphs: highlight_runs [] for run in para.runs: highlight run.font.highlight_color if highlight is not None and run.text.strip(): highlight_runs.append({ text: run.text, color: str(highlight) }) if highlight_runs: # 把同一段落内多个连续的highlight run合并成一段文本 merged_text .join(item[text] for item in highlight_runs) results.append({ paragraph: para.text, highlight_text: merged_text, runs: highlight_runs }) return results注释里提到的“合并”很关键。Word在保存高亮时不一定把一整句高亮放在同一个run里中间可能因为格式切换、拼写检查、中英文切换而被拆分成多个run。如果逐run打印你会看到高亮内容支离破碎明明一句话被拆成三四个片段输出。所以我在收集完段内所有高亮run之后把它们按位置顺序拼接回完整文字这样提取结果才像人话。运行后的效果大概是第2段需要修改的地方请将合同期限修订为三年。 第5段新增条款乙方应于每月5日前提交上月对账单。每个段落一条记录同时保留了高亮后的内容和所在段落原文后面要定位就方便多了。3.2 按颜色分类收集保留颜色信息如果文档里的高亮颜色是有语义的比如黄色表示待确认、绿色表示已通过、粉色表示重点提示那提取时就不能只判断“有没有”要把颜色也提取出来。from docx.enum.text import WD_COLOR_INDEX def extract_highlight_with_color(docx_path): doc Document(docx_path) items [] for para in doc.paragraphs: for run in para.runs: if run.font.highlight_color is not None and run.text.strip(): items.append({ text: run.text, color: run.font.highlight_color, color_name: run.font.highlight_color.name }) return itemshighlight_color.name返回的是枚举名称比如YELLOW、GREEN、PINK。如果是按颜色做统计直接对这个字段做分组就行。这样整理出来的材料不管是用表格还是透视图都方便得很。3.3 大文档和.doc老格式的处理有人问文档很大几十MB这样遍历会不会很慢实测下来python-docx是整体加载然后解析为对象模型对几十MB的docx是能扛住的正常几秒钟内能遍历完。如果文档大得离谱可以考虑只读取不渲染反正我们只要text字段性能上限比首屏渲染高很多。还有一个高频率问题Document()打开.doc文件直接报错。python-docx不支持旧的.doc二进制格式只支持.docx。这种场景下要么让文档源另存为docx要么用LibreOffice的命令行批量转换一下转换后再交给脚本处理。我自己一般在自动化流水线里加一步预转换把.doc先转成.docx再进提取逻辑。4. 表格、文本框、页眉页脚最容易漏的高亮区域第一个版本的代码跑完你以为万事大吉了结果一核对发现文档里有些高亮没被提取出来。这种情况十有八九是那些高亮不在正文段落里而是藏在表格单元格、文本框、页眉页脚这些“段落之外”的区域。4.1 表格的遍历方式与合并单元格去重Word表格里的文字在python-docx里需要按“表格→行→单元格→段落→run”的路径去找。def extract_highlight_from_table(docx_path): doc Document(docx_path) items [] seen_cells set() for table in doc.tables: for row in table.rows: for cell in row.cells: # 合并单元格会被重复引用跳过已处理的cell对象 if id(cell) in seen_cells: continue seen_cells.add(id(cell)) for para in cell.paragraphs: for run in para.runs: if run.font.highlight_color is not None and run.text.strip(): items.append(run.text) return items这里有个小坑Word的合并单元格机制会导致同一个_Cell对象在row.cells里出现多次。如果不去重提取结果里同一个单元格的高亮会重复出现好几遍看起来就像内容无故翻倍了。我的处理方式是维护一个seen_cells集合用id(cell)判断是否处理过虽然粗暴但很有效。4.2 文本框没有现成API怎么用XML兜底文本框比表格更隐蔽。Word里的文本框本质上是一个浮动在正文上的容器它的内容存在XML的w:txbxContent节点里。python-docx的对象模型没有把文本框里的段落暴露给doc.paragraphs直接用上面的遍历代码是碰不到这些文字的。这种时候只能下沉到XML层。思路是找到文档体中所有w:txbxContent节点然后遍历其中每个w:r检查有没有w:highlight子节点。from docx.oxml.ns import qn def extract_highlight_from_textboxes(doc): body doc.element.body items [] for txbx in body.iter(qn(w:txbxContent)): for r in txbx.iter(qn(w:r)): rPr r.find(qn(w:rPr)) if rPr is None: continue highlight rPr.find(qn(w:highlight)) texts r.findall(qn(w:t)) if highlight is not None and texts: text .join(t.text or for t in texts) if text.strip(): items.append({ text: text, val: highlight.get(qn(w:val)) }) return items这段代码的意思是把文档XML里所有文本框容器找出来再在容器内部遍历所有run片段判断这些run是否有高亮标签。它绕过了python-docx的API限制直接和底层XML对话虽然代码看起来绕但正是这种兜底方案能救你于水火。4.3 页眉页脚和嵌套表格的遍历思路页眉页脚提取高亮的方式和正文类似只是入口不同。每个section对象都带有header和footer属性它们的paragraphs列表可以直接遍历。def extract_highlight_from_headers_footers(doc): items [] for section in doc.sections: for paragraph in section.header.paragraphs: for run in paragraph.runs: if run.font.highlight_color is not None and run.text.strip(): items.append((header, run.text)) for paragraph in section.footer.paragraphs: for run in paragraph.runs: if run.font.highlight_color is not None and run.text.strip(): items.append((footer, run.text)) return items嵌套表格稍微麻烦一些。doc.tables只能拿到文档体最外层的表格如果表格里又套了表格内层的表格在doc.tables里是看不到的。解决方法还是用XML遍历找到所有w:tbl节点再逐个包装成Table对象处理。做法不复杂核心就是doc.element.body.iter(qn(w:tbl))。到这里一个相对完整的提取程序应该覆盖了正文、表格、文本框、页眉页脚、嵌套表格这几大区域。把这些函数组合起来最终返回的是一个完整的高亮清单。5. 踩坑实录颜色枚举、空白run和版本差异python-docx跑通基本功能不难难的是面对“奇怪文档”时不翻车。我把实际使用中遇到的几个高频问题集中在这里这些都是平时文档里不会写的东西。5.1 自定义高亮颜色会导致解析异常前面说过标准高亮颜色的枚举名是yellow、green、pink这些。但Word允许用户选“其他颜色”来自定义荧光笔颜色一旦自定义颜色被保存到XML里w:highlight的w:val可能变成一个非标准的值比如十六进制颜色码形式00B0F0。这种时候run.font.highlight_color的枚举解析可能直接异常也可能返回None行为取决于库的具体实现版本。最稳妥的兜底做法是不要完全依赖枚举而是在确认异常时回退到XML取值def get_highlight_val(run): rPr run._element.rPr if rPr is None: return None highlight rPr.find(qn(w:highlight)) if highlight is None: return None return highlight.get(qn(w:val))这个方法返回的是原始字符串无论是不是标准枚举名都拿得到适合做兼容处理和人工核对。5.2 高亮run里全是空白或隐形内容还有一种情况是高亮标记确实存在highlight_color也不是None但run.text得到的是空字符串或者全是空格。这通常发生在用户高亮了段落末尾的换行标记或者高亮区域里包含了一些特殊的控制字符。提取时如果不加过滤输出结果里就会混入一批看不见内容的条目看着像bug其实是文档本身的问题。代码里两个地方做了防护一个是run.text.strip()判断非空另一个是收集后参与合并的run本身必须有实际字符。这样处理下来输出结果基本不会出现“假高亮”条目。5.3 python-docx版本差异与快速排查顺序python-docx版本之间有一些API差异。早期版本里高亮相关的属性名或枚举组织方式和最新版有微调如果代码在某个环境报AttributeError: Font object has no attribute highlight_color排查方向很可能是库版本太老。解决办法就是升级到新版pip install --upgrade python-docx还有一类问题是文档本身是加密的或者启用了宏。加密文档直接读取会报错需要先通过msoffcrypto-tool解密.docm宏文档如果只是提取文本通常能读但宏代码不会被处理。碰到这类文档先单独确认格式再跑脚本省得排查半天最后发现是文件本身不配合。整体排查顺序我的经验是先确认后缀是.docx再确认文档没有加密接着确认库版本够新最后再用上面提到的XML兜底函数去查原始标签。按这个顺序走90%的问题都能定位到。6. 把脚本扩展成批量小工具单个文档能提取了接下来比较自然的动作就是批量处理。把几十个文件拖进一个目录跑一遍脚本全部高亮内容汇总在一个文件里这才是自动化该有的样子。6.1 批量扫描目录下的docx文件用glob或os.walk就能实现目录扫描。import glob from docx import Document def batch_extract(directory): all_items [] for file_path in glob.glob(f{directory}/**/*.docx, recursiveTrue): try: doc Document(file_path) items extract_all(doc) all_items.append({ file: file_path, items: items }) except Exception as e: print(f处理失败: {file_path}, 错误: {e}) return all_items注意这里我给每个文件都加了异常捕获。批量处理场景下一个文件报错不应该让整个任务中断先把能处理的处理完最后再集中看失败清单才是实用的做法。6.2 输出结构化结果TXT、CSV、Markdown提取出来的内容最终要落到文件里。按用途不同可以输出成不同格式。TXT适合快速浏览CSV适合导入Excel做透视和分析Markdown适合直接贴进文档、知识库或协作平台。我一般输出CSV时会保留这几个字段文件名、所在区域正文/表格/页眉等、高亮颜色、高亮文本、所在段落上下文。这样既能看到内容也能知道它在哪个位置、什么来头后续回查特别快。6.3 保留上下文提取高亮的同时带上所在段落文本这点是我实际用了一阵子后才补上的。最初我只提取高亮文字结果汇总出来之后发现有些句子脱离了上下文根本看不懂在说什么。后来我把高亮run所在段落的完整文本也提取出来作为“上下文”字段保存。别看只是多存了一列文本用起来方便太多了——单独看高亮可能不知道是哪份文件里的哪一处但一看所在段落马上就能定位。def extract_all(doc): items [] for para in doc.paragraphs: for run in para.runs: if run.font.highlight_color is not None and run.text.strip(): items.append({ area: body, color: str(run.font.highlight_color), text: run.text, context: para.text }) return items如果是比较重要的文档我还会额外记录一下页码。python-docx不直接提供页码信息需要先通过遍历找到run所在的段落再配合其他手段推算页码这个略复杂但思路是清晰的可做的。批量工具跑一段时间后你会发现自己的生活被简化了一大半。原来可能要一个下午才能整理完的资料现在双击一下脚本几秒钟出结果。更妙的是这个脚本是通用的以后任何人的Word文档需要提取高亮都可以直接拿过来用只不过改一下输入目录而已。我个人在实际使用中还有一个习惯脚本里提取出来的结果文件我会统一命名为高亮提取_日期.csv按日期留档这样哪天要复盘翻历史文件就能看到当时的处理结果不用重新跑一遍。这也是用了几次之后才总结出来的经验希望对你有帮助。
返回列表