在日常的办公自动化脚本里python-docx绝对是处理 Word 文档的一把好手。不过越常用的库踩坑的时候越让人头疼。最近有位朋友调试一段设置中文字体的代码卡在报错NoneType object has no attribute set上他贴出来的代码片段是这样的from docx import Document from docx.oxml.ns import qn doc Document() run doc.add_paragraph().add_run(你好世界) run.rPr.rFonts.set(qn(w:eastAsia), 黑体)看着逻辑没毛病先拿段落里的 run 对象再取它的 rPr 属性然后设置 rFonts 的 eastAsia 字体。结果一跑解释器直接甩脸子NoneType object has no attribute set。这到底是哪里出了问题是rPr不存在还是rFonts不存在又或者说是qn()函数的用法不对今天咱们就把这个报错彻底刨开从原理到修复一次性讲明白。1. 先从报错说起NoneType 到底指的是谁见到NoneType object has no attribute set这个报错第一反应应该是有个变量是None然后在这个None身上调用了.set()方法。但问题是代码里连着一串点号run.rPr.rFonts.set(...)到底是哪一个环节返回了None这得挨个拆开看。顺着调用链走一遍run是doc.add_paragraph().add_run(你好世界)的返回值这个对象肯定是真实存在的Run实例没问题。接下来是run.rPr这里就要注意了——rPr是Run对象的一个属性对应 Word 文档底层 XML 里的w:rPr元素。但关键在于python-docx 并不会默认给每个Run创建w:rPr节点。如果你创建一个新的 run它内部的 XML 结构可能只有w:r和w:t根本没有w:rPr。这时候你去访问run.rPr拿到的就是None。拿None继续往下取属性自然就报错了。这跟访问None.rFonts、None.set()是一样的道理。很多人第一眼看到报错以为是rFonts的问题实际上十有八九是rPr这一层就已经是None了。怎么验证这个判断很简单在报错前加一行打印print(run.rPr) # 大概率输出 None只要看到None就说明问题出在rPr这一层。也有人问那我是不是可以直接在add_run之后用run.font.name 黑体这种方式来设置这个确实可以font.name设置的是w:rPr/w:rFonts的w:ascii和w:hAnsi属性但它管不到w:eastAsia。中文环境下的 Word正文默认会走eastAsia这个字体槽位所以光设置font.name中文还是不会变成黑体。好问题定位到了接下来就看怎么让rPr不为None然后顺藤摸瓜把rFonts也建出来。2. 修复方案对比三种路子都能跑通修复NoneType object has no attribute set的思路其实就一条先把rPr和rFonts这两个节点“造”出来再造不出来的时候就用安全手段判断。下面我把三种实测可行的方案都摆出来大家按自己代码洁癖程度选。2.1 最稳妥方案get_or_add 系列方法python-docx 在底层封装了一组get_or_add_*方法专门干这种“有就用没有就建”的活。比如访问一个 run 的rPr不要直接点属性而是调用run._element.get_or_add_rPr()这个方法保证返回一个w:rPr元素绝对不会是None。同理rPr 下面要拿rFonts也有对应的get_or_add_rFonts()方法。完整代码可以这样写from docx import Document from docx.oxml.ns import qn doc Document() run doc.add_paragraph().add_run(你好世界) rPr run._element.get_or_add_rPr() rFonts rPr.get_or_add_rFonts() rFonts.set(qn(w:eastAsia), 黑体) doc.save(test.docx)跑一下不报错。打开生成的 docx选中“你好世界”这几个字看字体设置显示的就是黑体。这就是最根治的写法把None的隐患直接从源头消灭掉。2.2 先判断再操作防御式写法如果你不喜欢直接操作底层元素也可以用 python-docx 公开的 API 来判断。run._element这个私有属性虽然带下划线但社区里普遍都在用稳定性没问题。判断逻辑是如果run.rPr为None就说明w:rPr节点不存在那这时候可以用run._element.get_or_add_rPr()把它建出来如果存在就直接用run.rPr。from docx import Document from docx.oxml.ns import qn doc Document() run doc.add_paragraph().add_run(你好世界) rPr run.rPr if rPr is None: rPr run._element.get_or_add_rPr() rFonts rPr.rFonts if rFonts is None: rFonts rPr.get_or_add_rFonts() rFonts.set(qn(w:eastAsia), 黑体) doc.save(test.docx)这种写法比较直观每一步都做了判空适合刚入门的朋友理解调用链。缺点嘛就是代码啰嗦了一点。我自己的习惯是直接用get_or_add_*简洁且不容易漏判。2.3 结合 font.name 的偷懒方案还有一种更省事的思路既然run.font.name 黑体可以帮我们创建rPr和rFonts节点那不如先设置一次font.name把节点结构“逼”出来然后再手动设置eastAsiafrom docx import Document from docx.oxml.ns import qn doc Document() run doc.add_paragraph().add_run(你好世界) run.font.name 黑体 run._element.rPr.rFonts.set(qn(w:eastAsia), 黑体) doc.save(test.docx)这个方案的核心逻辑是run.font.name 黑体赋值操作内部会触发rPr和rFonts的创建赋值完成后run._element.rPr肯定不为NonerFonts也不再是None。之后你再直接设置eastAsia就不会踩到 NoneType 的坑。这方案我实测过没问题但总感觉有点“曲线救国”的意思。如果你是在循环里批量设置字体每次都要先走一遍font.name再设eastAsia那还不如老老实实用get_or_add_rPr()来得干净。3. 别急着抄代码先搞懂 rPr 和 rFonts 到底是什么很多教程只告诉你怎么改不讲为什么这么改。结果就是换了场景、换了代码一遇到NoneType又懵了。所以这块我多花点篇幅把rPr和rFonts的来龙去脉讲清楚。3.1 Word 文档内部其实就是一堆 XML.docx文件看着是个文档其实是个压缩包里面是一堆 XML 文件。其中word/document.xml存放正文内容。我们这段设置的“你好世界”在 XML 里长这样w:r w:rPr w:rFonts w:ascii黑体 w:eastAsia黑体 w:hAnsi黑体/ /w:rPr w:t你好世界/w:t /w:rw:r是 run 的 XML 表示w:rPr是 run properties 即 run 属性w:rFonts是字体属性。这里面的w:ascii管西文字体w:eastAsia管东亚字体中文、日文、韩文都走这个w:hAnsi管高 ANSI 字符集范围。对应到 python-docx 的对象模型Run.rPr对应w:rPr节点rPr.rFonts对应w:rFonts节点。之所以直接访问run.rPr可能拿到None是因为 python-docx 的这个属性设计得很“诚实”——XML 里没有对应节点就返回None不会帮你自动创建。同样rPr.rFonts在没有w:rFonts节点时也是None。3.2 qn 函数又是干嘛的qn的全称是qualname作用是把w:eastAsia这种带命名空间前缀的名字解析成完整的 XML 限定名。在底层 XML 里w前缀对应http://schemas.openxmlformats.org/wordprocessingml/2006/main这个命名空间。如果不用qn()直接传字符串w:eastAsialxml 大概率会报命名空间相关的错误。所以qn(w:eastAsia)这步是必须的它和NoneType object has no attribute set没有直接关系。但见过不少新手把两者混为一谈以为报错是qn()写错了其实qn()是无辜的。3.3 为什么必须单独设置 eastAsia很多人在 python-docx 里设置中文字体失败就是因为只设置了font.name没设置eastAsia。用 Word 打开一个英文文档给正文设置字体时Word 会自动同步设置英文字体和中文东亚字体。但 python-docx 不会替你操这份心——font.name只管 ascii 和 hAnsieastAsia 是独立的一个槽位必须单独赋值。不设置 eastAsia 的后果是中文还是用 Word 默认主题字体通常是等线或宋体英文字体倒是变了。这也是大家口中常见的“中文字体设置不生效”问题。所以只要涉及中文排版qn(w:eastAsia)就绕不过去。3.4 为什么 get_or_add 系列能避免 Nonepython-docx 底层使用 lxml 操作 XMLget_or_add_*方法的命名就是从 lxml 的get_or_add模式来的。它的语义是如果当前节点没有任何子节点符合条件就创建一个新节点并挂载如果已经存在就直接返回已有的那个节点。这比if None判断更安全也少写几行代码。从源码层面看run._element.get_or_add_rPr()背后走的是CT_R.get_or_add_rPr()它内部会判断w:rPr子元素是否存在不存在就用OxmlElement构造一个新的插进去。插完之后XML 树结构完整了后面再设置什么属性都不会碰到 NoneType。4. 实操演示从空文档到设置中文字体完整流程光说理论容易飘咱们直接跑一个完整脚本演示从零创建一个带中文字体的 Word 文档。这里会刻意加入一些容易踩坑的细节比如标题段落、正文段落、多个 run 的循环处理。4.1 完整示例代码from docx import Document from docx.shared import Pt from docx.oxml.ns import qn from docx.enum.text import WD_ALIGN_PARAGRAPH def set_run_font(run, font_name_cn黑体, font_name_enTimes New Roman, font_size12): 为一个 run 同时设置中英文字体和字号。 run.font.size Pt(font_size) run.font.name font_name_en rPr run._element.get_or_add_rPr() rFonts rPr.get_or_add_rFonts() rFonts.set(qn(w:eastAsia), font_name_cn) doc Document() # 标题 title doc.add_paragraph() title.alignment WD_ALIGN_PARAGRAPH.CENTER run_title title.add_run(Python-docx 字体设置踩坑记录) set_run_font(run_title, font_name_cn黑体, font_name_enArial, font_size16) # 正文段落 content doc.add_paragraph() run_content content.add_run(这是一段用于测试中文字体是否生效的正文包含英文和数字Hello 2025。) set_run_font(run_content, font_name_cn微软雅黑, font_name_enCalibri, font_size12) doc.save(font_test.docx) print(文档生成成功)这段代码里我特意封装了一个set_run_font辅助函数。函数内部先用run.font.name设置英文字体再用get_or_add_rPr()和get_or_add_rFonts()拿到节点最后设置eastAsia。这样每次调用就不用重复写那几行容易出错的代码了。4.2 运行结果与验证脚本跑完同目录下生成font_test.docx。你可以用 Word 或 WPS 打开全选文档查看字体设置面板。正常情况下标题的字体显示为“黑体 Arial”正文字体显示为“微软雅黑 Calibri”。如果文档默认字体还是等线说明设置没生效需要检查代码里的run对象是否对应了真正包含中文文本的那个 run。这里有个需要注意的地方如果一个段落里有多个 run比如add_run(Hello )和add_run(中文)是两个 run你在第一个 run 上设置 eastAsia 字体不会影响第二个 run。Word 中每个 run 的格式可以单独设置python-docx 也是按 run 粒度处理字体。所以批量设置时一定要循环遍历每个 run。4.3 批量设置文档中所有中文字体如果你想把整个文档所有段落、所有 run 的字体统一改掉可以这样遍历from docx import Document from docx.oxml.ns import qn def batch_set_cn_font(doc, font_name宋体): for paragraph in doc.paragraphs: for run in paragraph.runs: rPr run._element.get_or_add_rPr() rFonts rPr.get_or_add_rFonts() rFonts.set(qn(w:eastAsia), font_name) doc Document(existing.docx) batch_set_cn_font(doc, 宋体) doc.save(modified.docx)这个函数拿到文档后遍历所有段落、所有 run统一把 eastAsia 设置为宋体。因为用了get_or_add_rPr()就算某个 run 原来没有字体设置也会自动创建节点不会报 NoneType。你可以在修改前先备份一下原文档万一改完不满意还能回滚。5. 你会遇到的其他几个高频坑代码跑通只是第一步真正生产环境里还藏着各种奇奇怪怪的问题。我把这些年遇到的高频问题整理成了一张速查表附上原因和解决方案大家以后碰到可以直接对号入座。5.1 常见问题速查表现象根本原因解决方法NoneType object has no attribute setrun.rPr或rPr.rFonts为 None用get_or_add_rPr()和get_or_add_rFonts()设置字体后中文不变只设置了run.font.name没设置eastAsia额外设置qn(w:eastAsia)设置字体后英文不变font.name没有覆盖到所有 run确保每个 run 都执行设置或先合并 runqn(w:eastAsia)报 KeyError导错了qn的模块路径必须from docx.oxml.ns import qn字体名不生效显示空白字体名写错或系统未安装检查系统字体使用完整字体名称run._element找不到rPr旧版 python-docx 或自定义 XML 结构异常升级 python-docx检查文档是否损坏5.2 再说说字体名这个暗坑设置rFonts.set(qn(w:eastAsia), 黑体)时第二个参数必须是系统里已安装的字体名。如果你的系统里字体叫“微软雅黑”你写成“微软雅黑 Light”可能就不生效。更麻烦的是同一个字体在 Word 里的显示名和系统字体名可能不一样跨平台时经常出问题。稳妥的做法是先用系统字体列表确认名称再写进代码。我自己习惯先用matplotlib.font_manager或系统命令列出可用中文字体确认名字后再写死到脚本里。比如在 Windows 上黑体、宋体、微软雅黑都是安全的在 macOS 上可以写成“PingFang SC”这种名称。如果你的代码要部署到 Linux 服务器记得先检查服务器上有没有安装中文字体很多精简镜像默认没有中文字体设置了也不生效。5.3 有关 w:eastAsia 斜体加粗等属性顺带提一句rPr下面除了rFonts还有b加粗、i斜体、color颜色、sz字号等一堆子元素。如果你用get_or_add_rPr()拿到了rPr对象也可以继续用类似方式操作这些属性。比如设置加粗from docx.oxml import OxmlElement rPr run._element.get_or_add_rPr() b OxmlElement(w:b) rPr.append(b)或者直接用run.font.bold True公开 API 更省心。不过道理是相通的python-docx 的公开属性有时会返回 None底层get_or_add_*是万能钥匙。5.4 为什么有的 run 访问 rPr 不报 None有一种情况你可能好奇为什么同样是新 run有的run.rPr返回None有的却返回对象这跟 run 的创建方式有关。如果你用doc.add_paragraph(styleHeading 1)然后add_run()标题样式可能自带w:rPr定义但那是样式层面的run 自身的rPr还是可能为 None。如果你复制了一个已有格式的 runrPr就可能存在。总之别赌run.rPr一定存在正确姿势永远是判空或者用get_or_add_*。6. 一个更省心的封装自己的字体工具函数最后把我实际项目里在用的“终极版”设置字体函数分享出来它综合处理了中英文字体、字号、颜色、加粗、斜体同时兼容 run 可能为 None 的边界情况。你们可以直接拿走用。from docx.shared import Pt, RGBColor from docx.oxml.ns import qn def apply_run_style(run, font_cnNone, font_enNone, sizeNone, boldNone, italicNone, colorNone): if run is None: return if font_en: run.font.name font_en if font_cn: rPr run._element.get_or_add_rPr() rFonts rPr.get_or_add_rFonts() rFonts.set(qn(w:eastAsia), font_cn) if size is not None: run.font.size Pt(size) if bold is not None: run.font.bold bold if italic is not None: run.font.italic italic if color is not None: run.font.color.rgb RGBColor.from_string(color)用法示例run doc.add_paragraph().add_run(测试文本) apply_run_style(run, font_cn黑体, font_enTimes New Roman, size14, boldTrue, colorFF0000)这个函数最大的好处是把所有格式设置集中到一处避免每次到处写get_or_add_rPr。团队协作时别人看到apply_run_style就知道是干这个用的不用研究底层 XML。针对中文排版建议把字体名统一配置在一处CN_FONT 黑体 EN_FONT Times New Roman apply_run_style(run_content, font_cnCN_FONT, font_enEN_FONT, size12)后续要整体切换字体只需要改这两个常量不用满代码找。7. 从报错看 python-docx 的设计哲学再往深一层想NoneType object has no attribute set这个报错背后反映的是 python-docx 在设计上的一种“懒加载”哲学所有 XML 节点能省则省。当你新建一个 run 时XML 只包含最核心的w:rw:t内容/w:t/w:r所有格式属性都不存在都是None直到你显式设置才创建对应节点。这么设计的好处是文档体积小不会因为空属性产生冗余 XML。坏处就是新手上手时经常被各种None困扰。理解了这套机制以后再遇到NoneType object has no attribute xxx第一反应就别再是“代码哪里拼错了”而是“是不是 XML 节点不存在”。这个思维转变比任何具体代码都有价值。我在多次踩过这个坑之后已经养成了条件反射凡是访问 python-docx 的嵌套属性先想想这个属性底层对应什么 XML 节点这个节点在当前的场景下是否存在。如果存在不确定性直接上get_or_add_*宁可多造一个空节点也不能让脚本崩在 None 上。希望这篇记录能帮你省下一些排查时间。如果你在 python-docx 里还撞上过其他奇怪的坑欢迎在评论区写出来大家一起填坑。