Python-docx设置中文字体NoneType报错解析与解决方案

发布时间:2026/10/4 4:46:21
Python-docx设置中文字体NoneType报错解析与解决方案
写Python操作Word文档时设置中文字体几乎是一道必踩的坎。网上搜“python docx 设置中文字体”会出来一堆代码片段其中出现频率最高的几行就有rPr.rFonts.set(qn(w:eastAsia), u黑体)这种操作。但把它粘到自己脚本里一运行就给你一句AttributeError: NoneType object has no attribute set。这个报错说直白点你访问的对象是空值还硬要调用人家的方法。今天这篇就把这个报错拆到底讲清楚为什么会出现以及怎么一次性解决。1. 问题现场一行代码引发的NoneType连环坑1.1 报错复现典型的失败代码长什么样先别急着看理论我们把报错现场还原出来。下面这段代码是我在实际开发中见过的高频写法也是标题里那个报错最典型的触发方式from docx import Document from docx.oxml.ns import qn from docx.shared import Pt doc Document() heading doc.add_heading(, 1) run heading.add_run(Python实现docx中文字体设置) # 先设置字号这一步会在run里创建rPr元素 run.font.size Pt(16) # 然后直接访问rPr.rFonts想设置中文字体 rPr run._element.rPr rPr.rFonts.set(qn(w:eastAsia), 黑体) # AttributeError: NoneType object has no attribute set如果你是在已有docx文件基础上做批量处理代码更常见也更容易踩坑doc Document(existing.docx) for para in doc.paragraphs: for run in para.runs: rPr run._element.rPr rPr.rFonts.set(qn(w:eastAsia), 黑体)这两种场景本质是同一个问题但往往有细微差别第一种场景里rPr是有值的因为设置字号时python-docx会创建rPr但rFonts还没有被创建所以rPr.rFonts返回None再调用.set()就直接炸了。第二种场景里如果文档段落中某个run只是普通文本没显式设置过任何格式那么连rPr都可能不存在这时候run._element.rPr就已经是None了。1.2 定位问题先分清是rPr为None还是rFonts为None报错信息只有一句“NoneType object has no attribute set”但真正为None的对象可能是两个排查思路完全不同。先用一段最简单的诊断代码区分rPr run._element.rPr print(rPr) # 如果是None说明run连基本属性都没有 if rPr is not None: print(rPr.rFonts) # 如果是None说明rPr存在但字体元素没创建判断逻辑其实很直接如果run._element.rPr本身就是None说明这个run是“纯净文本”没有任何显式格式。这种情况遍布在从外部导入的docx里。如果run._element.rPr不是None但rPr.rFonts是None说明这个run设置过字号、颜色、加粗之类的属性但从未触碰过字体名。标题里的报错大多数属于这一种。这两种情况都不能直接链式调用.set()。只有搞清楚对象在哪一层为空才能选对下面的修复方案。2. 底层原因python-docx的“按需创建”元素模型2.1 rPr和rFonts在Word XML里的真实位置要彻底理解这个报错得先看看Word文档在XML层面长什么样。一个run的结构大致是这样w:r w:rPr w:rFonts w:ascii黑体 w:hAnsi黑体 w:eastAsia黑体/ w:sz w:val32/ /w:rPr w:tPython实现docx中文字体设置/w:t /w:r外层w:r是runw:rPr是run properties也就是run的所有格式属性w:rFonts是字体属性集合w:ascii、w:hAnsi管西文字体w:eastAsia管中文字体。python-docx把这个结构映射成了Python对象run._element对应w:rrPr对应w:rPrrPr.rFonts对应w:rFonts。技术点在于w:rPr和w:rFonts在XML规范里都是可选元素python-docx不会在创建每个run时就自动把它们填满。你打开一个docx里面可能有几百个run但绝大多数run的XML里根本没有rPr格式全靠样式继承。这在Word里完全合法但对编程操作来说就是个坑。2.2 ZeroOrOne的懒加载机制python-docx底层用lxml解析XML然后通过oxml描述符把XML元素映射成Python对象的属性。这里面最关键的一个描述符叫ZeroOrOne含义是“这个子元素在XML中最多出现一次且不是必须存在”。在python-docx源码里CT_R.rPr的定义就是rPr ZeroOrOne(w:rPr, successors(w:t, w:br, ...))CT_RPr.rFonts的定义同样是rFonts ZeroOrOne(w:rFonts, successors(w:b, w:i, ...))ZeroOrOne的行为是如果XML里有对应的子元素就把这个子元素包装成对象返回如果XML里没有就直接返回None。这个设计本意是节省内存、避免频繁填充冗余XML但对使用者来说它意味着“访问属性不一定有值”。所以当你写rPr.rFonts.set(...)时实际被解析成两步先获取rPr.rFonts如果它返回None下一步.set()自然就不存在。本质上不是rFonts被删了而是它本来就没被创建。2.3 为什么run.font.name能触发元素创建理解了解释None的逻辑接下来要理解解决方案的逻辑。为什么设置run.font.name之后再访问rPr.rFonts就不会为空因为python-docx在Font.name的setter里做了“找不到就创建”的操作。run.font.name 黑体这行代码内部大概是这样执行的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)get_or_add_xxx是python-docx为元素自动生成的方法它会先去XML里寻找对应子元素找不到就按XML规范在正确的位置创建出来然后返回这个元素。所以这行代码执行完rPr存在了rFonts也一定存在了后面再访问run._element.rPr.rFonts自然就有值。这也是为什么网上绝大多数解决方案都让你“先设置run.font.name再设置eastAsia”因为前者是个“触发器”会让后者的前置条件自动满足。3. 解决方案四种方式彻底告别rFonts为None3.1 方案一先调用run.font.name强制创建元素最省事的办法就是按上面说的原理先通过run.font.name把rPr和rFonts都“逼”出来然后再设置中文字体from docx import Document from docx.oxml.ns import qn from docx.shared import Pt doc Document() heading doc.add_heading(, 1) run heading.add_run(Python实现docx中文字体设置) # 先触发rFonts元素创建 run.font.name 黑体 # 这个setter已经创建了rPr和rFonts后面就可以放心访问了 run._element.rPr.rFonts.set(qn(w:eastAsia), 黑体) run.font.size Pt(16) doc.save(output.docx)这里有个细节需要注意run.font.name 黑体同时会设置w:ascii和w:hAnsi也就是西文字体也变成黑体。如果你的文档里包含英文或数字这样设置对视觉呈现来说通常是合理的因为黑体覆盖中英文会显得统一。但如果只想改中文字体、保留西文字体不变就要用下一个方案。3.2 方案二用get_or_add系列方法完全不依赖触发顺序更稳、更专业的做法是直接调用显式的创建方法不让代码逻辑依赖“先做哪一步”from docx import Document from docx.oxml.ns import qn from docx.shared import Pt doc Document() heading doc.add_heading(, 1) run heading.add_run(Python实现docx中文字体设置) rPr run._element.get_or_add_rPr() rFonts rPr.get_or_add_rFonts() rFonts.set(qn(w:ascii), 黑体) rFonts.set(qn(w:hAnsi), 黑体) rFonts.set(qn(w:eastAsia), 黑体) run.font.size Pt(16) doc.save(output.docx)这样无论run之前有没有rPr、有没有rFonts都能保证元素被创建后直接设置属性。这段代码在遍历已有文档的run时尤其管用不会因为某几个run比较“干净”就直接崩溃。我建议在封装通用函数时优先采用这个方案因为它不依赖任何先置条件行为最确定。后面4.2节会用这个方案写一个完整示范。3.3 方案三手动构建rFonts元素插入XML如果你对python-docx的get_or_add机制心存疑虑想完全靠自己控制XML还有一种更底层的方式直接创建一个w:rFonts元素清掉旧的再插入到rPr里。from docx.oxml import OxmlElement from docx.oxml.ns import qn rPr run._element.get_or_add_rPr() # 如果有旧rFonts先移除避免出现两个重复元素 old rPr.find(qn(w:rFonts)) if old is not None: rPr.remove(old) rFonts OxmlElement(w:rFonts) rFonts.set(qn(w:ascii), 黑体) rFonts.set(qn(w:hAnsi), 黑体) rFonts.set(qn(w:eastAsia), 黑体) rPr.append(rFonts)这种方式需要你自己保证元素插入的位置正确python-docx在get_or_add方法里会自动处理元素顺序但手动append可能把rFonts放到rPr里不太规范的位置。虽然Word对顺序不是绝对敏感但为了不给自己挖坑一般还是推荐方案二。方案三适合你想深度定制XML、或者某些特殊场景下要同时操作多个属性时使用。3.4 方案四在样式层面统一设置标题字体如果你的目标是“整个文档所有一级标题都用黑体”完全不必逐个run去设置直接在样式层面改一次就够了。doc Document() style doc.styles[Heading 1] # 同样要用get_or_add因为样式里的rPr也可能不存在 rPr style.element.get_or_add_rPr() rFonts rPr.get_or_add_rFonts() rFonts.set(qn(w:ascii), 黑体) rFonts.set(qn(w:hAnsi), 黑体) rFonts.set(qn(w:eastAsia), 黑体)这样设置后文档中所有使用Heading 1样式的文本只要没有手动覆盖字体都会统一显示为黑体。这个方案的优越性在于不用遍历每个run代码简洁后续如果要换字体只改一处就全局生效。缺点也很明显如果文档里很多run显式设置了字体会覆盖样式层面的设置这种情况还是要回到run级别处理。实际项目里我通常是“样式run”双管齐下先设置样式保证整体统一再对特殊run单独覆盖这样能把维护成本压到最低。4. 实操过程从零到一完整实现标题中文字体设置4.1 环境准备与版本选择开始实操之前先确认python-docx已经安装。常规操作pip install python-docx如果你用国内源可以加上镜像参数pip install python-docx -i https://pypi.tuna.tsinghua.edu.cn/simple版本上python-docx目前主流版本是0.8.11和1.x系列。本文中的代码在这两个版本下都测试过核心API没有变化。安装完成后可以打印一下版本确认环境import docx print(docx.__version__)4.2 完整代码创建文档并设置标题中文字体下面这份完整代码就是我在项目里常用的封装逻辑上是方案二的加强版增加了对字号、加粗等属性的统一处理from docx import Document from docx.shared import Pt from docx.oxml.ns import qn def set_run_font(run, font_name, font_sizeNone, boldNone): 设置run的字体兼容中英文场景。 :param run: python-docx的Run对象 :param font_name: 字体名如黑体、宋体、微软雅黑 :param font_size: 字号整数单位磅 :param bold: 是否加粗 rPr run._element.get_or_add_rPr() rFonts rPr.get_or_add_rFonts() rFonts.set(qn(w:ascii), font_name) rFonts.set(qn(w:hAnsi), font_name) rFonts.set(qn(w:eastAsia), font_name) if font_size is not None: run.font.size Pt(font_size) if bold is not None: run.font.bold bold def set_heading_font(heading, font_name, font_sizeNone, boldNone): 设置标题段落的字体样式。 for run in heading.runs: set_run_font(run, font_name, font_size, bold) # 使用示例 doc Document() # 创建一级标题 heading doc.add_heading(, 1) run heading.add_run(第一章 绪论) set_run_font(run, 黑体, 18, boldTrue) # 再创建一个正文段落 para doc.add_paragraph() run2 para.add_run(这是正文内容用来验证字体设置没有串位。) set_run_font(run2, 宋体, 12) doc.save(标题字体设置示例.docx) print(生成成功标题字体设置示例.docx)这个封装里我把字体设置拆成了三个set调用与方案一对比好处是不会意外覆盖别的字体属性get_or_add_rPr()保证了即使run没有任何格式也能顺利创建rPr。这是我在遍历旧文档时最喜欢用的写法因为它对文档里那些“素面朝天”的run也能安全处理。4.3 验证结果直接查看生成的XML代码执行完后如果心里没底可以用lxml把生成后的run XML打印出来确认rFonts各个属性是否都写进去了from lxml import etree doc Document(标题字体设置示例.docx) heading doc.paragraphs[0] run heading.runs[0] xml_bytes etree.tostring(run._element, pretty_printTrue) print(xml_bytes.decode())输出大致长这样w:r xmlns:whttp://schemas.openxmlformats.org/wordprocessingml/2006/main w:rPr w:rFonts w:ascii黑体 w:hAnsi黑体 w:eastAsia黑体/ w:b/ w:sz w:val36/ /w:rPr w:t第一章 绪论/w:t /w:r看到这里w:eastAsia的值确实写进去了说明中文字体已经生效。注意w:sz w:val36是半磅单位18磅对应36这个半磅换算很多人第一次看到会疑惑实际上python-docx的Pt()已经帮你处理好了。如果你的目标不是生成新文档而是处理一份已有的docx文件只要把打开方式换成Document(已有文件.docx)再遍历需要修改的段落和run调用set_run_font同样能用这个思路搞定。5. 常见问题与排查技巧实录5.1 为什么同一份文档里有的run明明有rPr有的却连rPr都没有这是我在处理第三方生成的docx时最常遇到的情况。一个Word文档里有的run可能来自用户手动输入有的run来自模板自动生成它们的XML结构差异很大。手动输入后没有做任何格式调整的runWord通常不会给它写rPr因为所有格式都继承自段落样式或默认样式。举个例子一个“Hello World”文本如果能直接从样式继承字体、字号、颜色就没必要在XML里写一大堆冗余属性。python-docx忠实反映这个现实所以访问run._element.rPr时得到None再正常不过。应对思路就是不要假设run一定有rPr统一用get_or_add_rPr()。这个方法的语义本来就是“没有就创建”在存在性不确定的场景里是最稳妥的选择。5.2 标题run为空导致runs[0]索引报错添加标题时还有一种衍生坑不是rFonts报错而是索引报错heading doc.add_heading(, 1) # 空的标题段落 run heading.runs[0] # IndexError: list index out of rangeadd_heading(, 1)创建的是一个空文本的标题段落runs列表是空的直接取runs[0]必然报错。正确的做法是先加runheading doc.add_heading(, 1) run heading.add_run(第一章 绪论)或者直接传入标题文本heading doc.add_heading(第一章 绪论, 1)后面这种写法runs[0]就能正常拿到了。但在处理外部文档时如果遍历到某个标题段落本身没有run空标题还需要加个判断if not heading.runs: heading.add_run()否则后续对run的字体设置操作仍会失败。5.3 设置了eastAsia之后还是显示宋体问题出在哪这是很多人在完成代码修改后遇到的最后一道坎代码不报错了eastAsia也设置成功了但用Word或WPS打开文档标题还是默认的宋体/等线。排查方向主要有三个第一确认设置作用到了正确的位置。如果你设置的是样式但run级别存在显式的rPr字体设置样式就会被覆盖。Word的优先级是直接格式高于样式所以哪怕样式里写了黑体run自己的rPr.rFonts里如果没有eastAsia中文字体依然按它自己的逻辑来。遇到这种情况只能回到run级别再设置一遍。第二确认w:ascii和w:hAnsi也设置了。有些场景下中文内容的字体实际由w:ascii或w:hAnsi控制而不是w:eastAsia因为Word对不同字符集的字体匹配规则比较复杂。稳妥的做法是三个属性一起设置就像我在4.2节写的函数那样。第三确认Word没有启用“忽略样式中的字体”之类的兼容选项或者文档是基于某个模板生成且模板本身带兜底格式。这类问题绕不开最简单的方法就是把run级别的字体全部显式设置一遍包括中英文属性。5.4 对已有docx反复修改时的几个隐藏坑处理旧文档时我习惯在执行任何字体操作前先备份一份原文件。python-docx直接对原路径保存时如果中途报错很容易产生半个导出文件而且多次保存同一份文件还会出现样式残留、重复属性等奇怪问题。稳妥的做法是先Document(old.docx)读取处理完doc.save(new.docx)另存为新文件确认没问题再覆盖原文件。另外如果文档里包含文本框、页眉页脚、表格单元格里的rundoc.paragraphs是遍历不到的。这些位置的run也需要单独处理。表格里的run可以通过table.rows[i].cells[j].paragraphs拿到页眉页脚则是section.header.paragraphs。我在之前的一个项目里就因为这个漏改了页眉里的标题字体最后在验收时才被发现相当尴尬。还有一个容易忽略的点如果run.font.name已经被设置成某个西文字体名再设置eastAsia时最好把ascii和hAnsi也统一设一次否则可能出现英文和中文各用一套字体的割裂效果。最后说一个我自己踩过几次的细节python-docx里qn(w:eastAsia)中的w:前缀是必需的它代表WordprocessingML命名空间。看到“Namespace prefix not defined”之类的报错时先检查是不是把qn导成了别的东西或者拼错了命名空间。别小看这个导入from docx.oxml.ns import qn要是漏了后续所有XML属性操作都会在第一步就卡住。