多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

python-docx中文字体设置报错NoneType?理解rPr与rFonts并正确使用get_or_add

python-docx中文字体设置报错NoneType?理解rPr与rFonts并正确使用get_or_add 这个问题我太熟了在python-docx里折腾中文字体的人十有八九都会在rPr.rFonts.set(qn(w:eastAsia), 黑体)这一行撞上NoneType object has no attribute set。更气人的是代码明明是从高赞博客或者官方文档抄来的别人能跑你这里却动不动就报错而且报错路径还不稳定有时第一个 run 没事第二个 run 就炸了。其实这个错本身并不复杂一句话就能讲明白rPr 和 rFonts 并不是 python-docx 每次都帮你创建好的对象它们是 Word 文档 XML 里的“可选子元素”文档里没有对应节点时python-docx 就返回 None。但你如果只是知道这个结论下次遇到其他 NoneType 还是不会排。所以我这篇会把报错现场、底层机制、正确写法、标题场景的完整操作都拆开讲一遍最后再教你怎么用一条命令快速定位问题。1. 先还原现场这个报错到底是哪一行触发的先别急着改代码我们把这个报错的真实触发路径完全复原出来。下面是网上最常见的一种写法from docx import Document from docx.oxml.ns import qn doc Document() p doc.add_paragraph() run p.add_run(你好python-docx) rPr run._element.rPr rPr.rFonts.set(qn(w:eastAsia), 黑体)这段代码运行后大概率会在不同位置抛NoneType object has no attribute set。为什么说是“大概率”因为报错位置取决于 run 之前有没有设置过其他格式。1.1 最少代码复现我们直接用最小化代码来复现便于观察from docx import Document from docx.oxml.ns import qn doc Document() p doc.add_paragraph() run p.add_run(你好) print(run._element.xml)新创建的 run它的 XML 结构是这样的w:r xmlns:whttp://schemas.openxmlformats.org/wordprocessingml/2006/main w:t你好/w:t /w:r注意看w:r下面直接就是w:t根本没有w:rPr这个节点。所以当你执行run._element.rPr时得到的是None。接下来访问None.rFonts自然会报NoneType object has no attribute rFonts。如果你的代码写的是rPr.rFonts.set(...)那么在这一步就会看到NoneType object has no attribute set——但严格来说这个报错是因为rPr.rFonts本身就是 None而不是 set 方法的问题。1.2 两种不同的报错路径更常见的情况是你的 run 设置过加粗、字号等属性rPr 节点已经存在了但 rFonts 还不存在。from docx import Document from docx.oxml.ns import qn from docx.shared import Pt doc Document() p doc.add_paragraph() run p.add_run(你好) run.bold True print(run._element.xml)此时 XML 变成w:r xmlns:whttp://schemas.openxmlformats.org/wordprocessingml/2006/main w:rPr w:b/ /w:rPr w:t你好/w:t /w:r现在run._element.rPr是一个有效的对象但w:rPr下面只有w:b/没有w:rFonts/。执行rPr.rFonts返回None接着调用.set(...)就会准确地在rFonts.set这一行抛出NoneType object has no attribute set。我把这两种情况放在一起对比操作历史rPr是否存在rFonts是否存在报错位置新建 run未设置任何格式不存在不存在rPr.rFonts访问时出错设置了 bold / size / color 等格式存在不存在rFonts.set(...)调用时出错调用过run.font.name 黑体存在存在不报错这就是为什么网上代码“时灵时不灵”——不同博客的上下文不一样。有的帖子里作者先调用了run.font.name 黑体顺带把 rPr 和 rFonts 都建出来了后面的 set 就能正常执行有的帖子里作者上来就直接写rPr.rFonts.set(...)下面一堆评论全是 NoneType。2. 根因rPr 和 rFonts 在 python-docx 里都是“可选子元素”要彻底解决这个问题不能只记住“先创建再赋值”这个操作得理解 python-docx 对 Word 文档 XML 的处理方式。2.1 CT_RPr 的可选子元素设计python-docx 底层基于 lxml它在启动时会把 python-docx 自带的一些 OOXML Schema 定义加载成类比如CT_RPr对应文档里的w:rPr节点CT_Fonts对应w:rFonts节点。这些类封装在docx/oxml/text/run.py和docx/oxml/text/font.py这些源码文件里。CT_RPr里面的rFonts子元素被设计成可选的。什么是可选简单类比Word 文档的 XML 遵循“缺省即继承”的原则。一个 run 如果没有特殊字体需求它根本不需要在 XML 里写w:rFonts完全继承样式或主题的字体设置。这样做的好处是让.docx文件体积更紧凑——想象一下每个 run 都写一遍完整的字体属性几百个 run 会撑出多少冗余内容。所以在 python-docx 的封装里rPr.rFonts是一个普通的属性访问只有 XML 中存在w:rFonts子元素时才返回对象否则就返回None。它不会自作主张帮你 new 一个空节点出来。2.2 get_or_add 机制python-docx 处理可选元素的统一思路既然 rFonts 是可选元素python-docx 不可能让开发者每次手动去判断它存不存在。它的解决方案是一组get_or_add_xxx方法。比如CT_RPr类里有get_or_add_rFonts()它的逻辑是检查当前w:rPr下是否有w:rFonts子元素。如果有直接返回现有节点。如果没有就新建一个w:rFonts按照 XML Schema 定义的顺序插到正确位置然后返回。所以你在写代码时不需要关心 rFonts 存不存在只需要调用get_or_add_rFonts()它保证返回一个可用的节点。同理CT_R对应w:r里有get_or_add_rPr()保证返回一个可用的w:rPr。你可能已经发现run.font.name 黑体这个 setter 内部其实就是这么做的。python-docx 的Font.namesetter 大致是这样的逻辑name.setter def name(self, value): rPr self._element.get_or_add_rPr() rFonts rPr.get_or_add_rFonts() rFonts.set(qn(w:ascii), value) rFonts.set(qn(w:hAnsi), value)也就是说只要你给font.name赋值它会自动把一整条链路建好。这也是为什么前面表格里“调用过font.name之后rFonts 就存在了”。2.3 qn(w:eastAsia) 到底干了什么很多人疑惑为什么设置属性的 key 要写成qn(w:eastAsia)不能直接写w:eastAsia吗答案是不行至少不推荐。qn是 qualified name 的缩写它的作用是把w:这个命名空间前缀解析成完整的 XML 命名空间标识。在 lxml 里带命名空间的属性名必须写成 Clark Notation也就是{命名空间URI}属性名的形式。qn(w:eastAsia)实际上会返回类似这样的字符串{http://schemas.openxmlformats.org/wordprocessingml/2006/main}eastAsia如果你直接写rFonts.set(w:eastAsia, 黑体)lxml 会认为这是一个没有命名空间的普通属性写进 XML 后 Word 大概率不认这个属性导致设置无效。所以qn()这个包装不能省它是在告诉 lxml这是 WordprocessingML 命名空间里的eastAsia属性。3. 正确写法三步走和三种方案对比理解了原理之后正确写法就呼之欲出了。核心思想只有一句话在 set 之前确保 rPr 和 rFonts 都已经存在。这里有几种实现方式各有适用场景。3.1 方案A先用 font.name 把节点建出来如果你只是想让当前 run 的中文字体生效最简单的方法是先调用run.font.name赋值再设置eastAsiafrom docx import Document from docx.oxml.ns import qn doc Document() p doc.add_paragraph() run p.add_run(你好python-docx) run.font.name 黑体 run._element.rPr.rFonts.set(qn(w:eastAsia), 黑体)因为run.font.name 黑体的 setter 内部已经调用了get_or_add_rPr()和get_or_add_rFonts()所以当你执行第二行run._element.rPr.rFonts.set(...)时rPr和rFonts都已经真实存在了不会再报 NoneType。这个方案的优点是最贴近原始代码改动最小。缺点也很明显它依赖font.name赋值这个“副作用”。如果哪一天有人把这两行拆开了或者你又加了什么判断条件导致font.name没被执行问题就会重新出现。所以我对它的评价是能用但不够稳。3.2 方案B用 get_or_add 链不依赖任何副作用更推荐的做法是显式调用get_or_addfrom docx import Document from docx.oxml.ns import qn doc Document() p doc.add_paragraph() run p.add_run(你好python-docx) rPr run._element.get_or_add_rPr() rFonts rPr.get_or_add_rFonts() rFonts.set(qn(w:eastAsia), 黑体)这个写法把“创建节点”这件事放在明面上每一步都有明确语义先拿到或创建rPr再拿到或创建rFonts最后给rFonts设置w:eastAsia属性。即使前面没有设置过任何格式它也能正常工作。如果你觉得三行太长可以压缩成一行链式调用run._element.get_or_add_rPr().get_or_add_rFonts().set(qn(w:eastAsia), 黑体)不过从可读性考虑我还是推荐拆成两到三行尤其是你未来可能要在这个基础上继续加其他属性时中间变量很有用。3.3 封装成函数一次解决中英文字体实操中你不会只在一个 run 上设置字体所以最好封装成一个通用函数from docx.oxml.ns import qn def set_run_font(run, chinese_font宋体, western_fontTimes New Roman): 设置一个 run 的字体中英文分别指定。 run.font.name western_font rPr run._element.get_or_add_rPr() rFonts rPr.get_or_add_rFonts() rFonts.set(qn(w:eastAsia), chinese_font)调用方式run doc.add_paragraph().add_run(第一章 实验方法 Experimental Methods) set_run_font(run, chinese_font黑体, western_fontTimes New Roman)这样中文会用黑体英文和数字会用 Times New Roman各管各的互不干扰。顺便说一句u黑体这种写法是 Python 2 时代的遗留Python 3 里字符串默认就是 Unicode直接写黑体即可。如果你是在老项目里维护代码看到u前缀也不必惊讶它不影响运行。三种方案对比方案创建 rPr创建 rFonts是否依赖副作用可读性推荐度A先 font.name 再 set由 setter 创建由 setter 创建依赖中一般Bget_or_add 链显式创建显式创建无好推荐C封装函数内部处理内部处理无最好日常推荐4. 别满足于“不报错”把中文字体和西文字体一次配齐很多初学者看到不报错就以为任务完成了结果打开 Word 一看中文字体变了但英文数字还是原来的 Calibri或者反过来。这是因为 Word 的字体设置本来就不是一个属性能搞定的。4.1 Word 字体面板背后的三个 w: 属性在 Word 的字体对话框里你看上去只有“中文字体”和“西文字体”两个区域但底层 XML 对应了至少三个属性XML 属性控制范围典型场景w:asciiASCII 字符即英文字母、数字、半角标点西文字体w:hAnsiHigh ANSI 字符比如带重音的拉丁字母、某些特殊符号西文字体w:eastAsia东亚字符中文、日文、韩文中文字体python-docx 的font.namesetter 会同时设置w:ascii和w:hAnsi但不会设置w:eastAsia。所以如果你只写这一行run.font.name 黑体最终效果是英文、数字变成黑体中文保持默认。反过来如果你只设置了w:eastAsiarun._element.get_or_add_rPr().get_or_add_rFonts().set(qn(w:eastAsia), 黑体)效果是中文变成黑体英文、数字保持默认通常是 Calibri 或主题字体。只有两个都设置中英文才会统一。4.2 常见误区为什么打开 Word 发现字体没变有一种特别容易让人抓狂的情况代码运行完没有任何报错但打开 Word 一看字体就是没变。我把这类问题归纳成三个原因按出现频率排序第一你只设置了font.name没有设置w:eastAsia。这是最普遍的。因为font.name只影响西文字体中文不动。你在 Word 里选中文字打开字体面板会看到“中文字体”那里还是默认值。第二你设置的是 run 级字体但 run 上面还有一个样式级字体在压制它。Word 的字体渲染优先级是直接格式run 级优先于段落样式段落样式优先于文档默认样式。理论上 run 级设置了就会覆盖样式但如果你是在某个“主题字体”很强的模板上操作某些字符会因为主题映射问题显示出其他字体这种情况多见于从老版本 Word 转过来的文档。第三你设置的是w:eastAsia但中文字符被识别成了西文字符。比如有些全角数字或特殊标点Word 可能把它们归到 hAnsi 而不是 eastAsia 范围。这种情况比较少见但确实存在。我建议每次设置完字体后把 run 的 XML 打印出来看一眼确认w:rFonts节点长这样w:rPr w:rFonts w:ascii黑体 w:hAnsi黑体 w:eastAsia黑体/ /w:rPr三个属性都有值才说明中英文都配到位了。4.3 论文标配中文宋体、西文 Times New Roman这个知识点在实际工作里最刚需的场景就是论文排版。理工科论文一般要求中文用宋体英文和数字用 Times New Roman。我直接给一段可复制的代码from docx import Document from docx.oxml.ns import qn from docx.shared import Pt doc Document() def add_formatted_paragraph(text): p doc.add_paragraph() run p.add_run(text) run.font.name Times New Roman run.font.size Pt(12) rPr run._element.get_or_add_rPr() rPr.get_or_add_rFonts().set(qn(w:eastAsia), 宋体) return p add_formatted_paragraph(摘要本文提出了一种改进方法取得了 99.2% 的准确率。) doc.save(output.docx)这里的run.font.name Times New Roman管住英文和数字set(qn(w:eastAsia), 宋体)管住中文。这样输出文档里中文是宋体英文数字是 Times New Roman完全符合常见论文格式要求。5. 设置标题的中文字体改 run 还是改样式回到标题本身——设置标题中文字体。标题和普通段落不太一样它往往带有样式层级。在 python-docx 里你既可以直接改标题里每个 run 的字体也可以改标题样式本身。这两种方式差别很大。5.1 run 级修改的局限如果你已经创建了一个标题段落可以直接遍历它的 runs 来设置from docx import Document from docx.oxml.ns import qn doc Document() heading doc.add_heading(第一章 引言, level1) for run in heading.runs: run.font.name 黑体 rPr run._element.get_or_add_rPr() rPr.get_or_add_rFonts().set(qn(w:eastAsia), 黑体)这个方法看起来简单但有一个隐藏的坑Word 文档里的文本不一定是一个完整的 run。比如标题“第一章 引言”在底层 XML 里可能被拆成多个 run尤其是带自动编号、交叉引用或者修订痕迹的文档拆得更碎。你必须在for run in heading.runs里循环处理否则会出现“标题里一段字变了另一段没变”的奇怪现象。还有一个更大的局限run 级修改只对当前这个标题段落生效。后面你如果再doc.add_heading(第二章 方法, level1)新标题的字体会被系统样式带偏又回到默认字体。你得重新写一遍那几行设置代码。5.2 样式级修改改一次全文生效我更推荐的做法是修改标题样式。Heading 1、Heading 2 这些在 python-docx 里都是doc.styles集合里的对象你可以直接改样式的字体from docx import Document from docx.oxml.ns import qn doc Document() # 修改 Heading 1 样式中文字体黑体西文字体 Arial style doc.styles[Heading 1] style.font.name Arial style.font.size doc.styles[Normal].font.size # 可选保持字号统一 style.element.get_or_add_rPr().get_or_add_rFonts().set(qn(w:eastAsia), 黑体)这段代码执行后文档里所有应用了 Heading 1 样式的段落都会变成中文字体黑体、西文字体 Arial而且之后新增的 Heading 1 标题也一样生效不需要重复设置。这里的关键点是style.element。python-docx 的样式对象通过element暴露底层的CT_Style节点它同样有get_or_add_rPr()方法。如果你直接访问style.element.rPr有可能因为 rPr 不存在又踩一次 NoneType所以请务必用get_or_add。5.3 新增标题不会继承 run 级设置很多同学在“直接改 run”和“改样式”之间犹豫我用一个具体场景说明差异。假设你用 run 级方式把当前“第一章 引言”改成黑体了然后又执行doc.add_heading(第二章 数据与方法, level1)这个新标题从头到尾没经过你的 run 级设置它的字体由 Heading 1 样式决定。如果 Heading 1 样式的主题字体是 Calibri Light那新标题中文就是 Calibri Light——这显然不是你想要的。你只能再写一遍循环设置字体。如果走样式级修改不管是已有的还是新增的 Heading 1字体都是统一的。所以只要你处理的标题具备明确的样式层级我建议优先改样式。另外补充一点修改样式可能会连带影响文档导航窗格里的样式显示但这本来就是正常行为不用特别担心。如果你只想改某一个标题的字体不考虑全局那用 run 级方式即可二者适用场景不同。6. 排错方法论遇到 NoneType 先看 XML 再改代码最后聊一下排错思路。python-docx 里的 NoneType 报错千奇百怪如果每次都是“报错—试代码—再报错”的循环效率太低了。我有一个习惯遇到这类问题先不猜直接把相关节点的 XML 打出来看。6.1 打印 XML 诊断python-docx 内置了xml属性可以方便地输出节点的 XML 字符串print(run._element.xml)你也可以用 lxml 的tostring获得更漂亮的缩进格式from lxml import etree print(etree.tostring(run._element, pretty_printTrue).decode())对于样式对象print(doc.styles[Heading 1].element.xml)拿到 XML 之后判断思路就清晰了w:r下有没有w:rPr没有就说明 get_or_add_rPr 被跳过或对象类型不对。w:rPr下有没有w:rFonts没有就说明 rPr 存在但 rFonts 是 None。w:rFonts下有没有你预期的三个属性没有就说明 set 时的 key 写错了。这套流程比瞎试代码快得多。我把常见排查顺序做成表格检查步骤看什么可能问题1操作对象的类型操作的是 run 还是 style底层节点类型不同2w:rPr是否存在如果 None要先 get_or_add_rPr()3w:rFonts是否存在如果 None要先 get_or_add_rFonts()4w:ascii/w:hAnsi/w:eastAsia是否有值如果缺少检查 qn 参数是否正确6.2 同类的 NoneType 坑掌握了这个方法python-docx 里很多同类问题都能举一反三。比如有人直接操作段落属性时踩过paragraph._p.pPr是 None 的坑这和rPr是 None 完全同构。解决方式也是一样的paragraph._p.get_or_add_pPr()。还有人踩过访问表格样式时table.style.element里的 rPr 是 None。原理都是可选子元素缺省问题只要记住“读属性之前先确认节点存在设置属性之前先 get_or_add”这个大原则90% 的 NoneType 都能被消灭。我的建议是遇到 NoneType 不要慌先打印 XML再决定是补 get_or_add 还是调整属性名。这套方法论比背具体报错答案通用得多。回到中文字体这个问题本身。我还想分享一个真实经历之前帮一个朋友调毕业论文格式整整一个晚上都在和各种字体设置搏斗。后来发现问题不在于代码而在于他对“Word 字体面板里同时存在中文字体和西文字体”这件事没有概念以为设置一个 font.name 就万事大吉了。如果你也是刚接触 python-docx建议在本地写一个包含中英文混合文本的测试文档把上面几种写法都跑一遍再用我教的方法打印 XML 看看一次搞懂之后这块就再也不会卡你了。
返回列表