news 2026/10/4 4:46:19

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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:val="32"/> </w:rPr> <w:t>Python实现docx中文字体设置</w:t> </w:r>

外层<w:r>是run,<w:rPr>是run properties,也就是run的所有格式属性;<w:rFonts>是字体属性集合,w:ascii、w:hAnsi管西文字体,w:eastAsia管中文字体。python-docx把这个结构映射成了Python对象:run._element对应<w:r>,rPr对应<w:rPr>,rPr.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为None

3.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_size=None, bold=None): """设置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_size=None, bold=None): """设置标题段落的字体样式。""" 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, bold=True) # 再创建一个正文段落 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_print=True) print(xml_bytes.decode())

输出大致长这样:

<w:r xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"> <w:rPr> <w:rFonts w:ascii="黑体" w:hAnsi="黑体" w:eastAsia="黑体"/> <w:b/> <w:sz w:val="36"/> </w:rPr> <w:t>第一章 绪论</w:t> </w:r>

看到这里,w:eastAsia的值确实写进去了,说明中文字体已经生效。注意<w:sz w:val="36">是半磅单位,18磅对应36,这个半磅换算很多人第一次看到会疑惑,实际上python-docx的Pt()已经帮你处理好了。

如果你的目标不是生成新文档,而是处理一份已有的docx文件,只要把打开方式换成Document('已有文件.docx'),再遍历需要修改的段落和run调用set_run_font,同样能用这个思路搞定。

5. 常见问题与排查技巧实录

5.1 为什么同一份文档里,有的run明明有rPr,有的却连rPr都没有

这是我在处理第三方生成的docx时最常遇到的情况。一个Word文档里,有的run可能来自用户手动输入,有的run来自模板自动生成,它们的XML结构差异很大。手动输入后没有做任何格式调整的run,Word通常不会给它写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 range

add_heading('', 1)创建的是一个空文本的标题段落,runs列表是空的,直接取runs[0]必然报错。正确的做法是先加run:

heading = 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')另存为新文件,确认没问题再覆盖原文件。

另外,如果文档里包含文本框、页眉页脚、表格单元格里的run,doc.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属性操作都会在第一步就卡住。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/4 4:46:12

MATLAB2020b识别VS2019编译器失败的原理与修复

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 4:45:21

长沙曾食坊小吃培训的兼职学员:小本试水怎么安排

本篇要点&#xff1a;- 只能做半天或晚间的品类&#xff0c;优先预制和半成品- 以周为单位的试水节奏&#xff0c;控制试错成本- 副业与本职、家庭的精力边界要先划想用兼职做小吃试水的人&#xff0c;时间碎、本钱少&#xff0c;常怕一上来就重投入。本文补的是"小本、短…

作者头像 李华
网站建设 2026/10/4 4:44:07

华东师大计算机保研机试2020题解:字符串、BFS、单调队列通关指南

每年保研季&#xff0c;华东师大计算机学院的机试都会刷掉一批准备不充分的同学。2020年那套题我印象很深&#xff0c;整体难度不算高&#xff0c;但坑点相当密集&#xff1a;有人挂在字符串展开&#xff0c;有人挂在连通块查询的输入读法上&#xff0c;还有人连滑动窗口的暴力…

作者头像 李华
网站建设 2026/10/4 4:43:45

基于SPI接口的MRAM数据存储:PIC18F57Q43读写MR25H40CDF全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 4:43:06

自偏置电流镜设计:从原理到版图匹配的完整实战指南

去年评审一个学生团队的流片项目&#xff0c;看到他们给数据转换器做的电流源阵列&#xff0c;偏置电压还是从主基准那边一路长线拉到各个模块&#xff0c;中间又串了两级buffer。我当时就建议他们换个思路&#xff1a;这个位置其实用自偏置电流镜就够了&#xff0c;既能把偏置…

作者头像 李华
网站建设 2026/10/4 4:41:16

K210+STM32+SD卡实现人脸识别门禁系统开发实战

K210开发板学习笔记写到第三篇&#xff0c;前两篇分别折腾了环境搭建和摄像头基础采集&#xff0c;这次直接上了一个相对完整的组合方案&#xff1a;STM32做逻辑主控&#xff0c;K210负责图像采集和人脸检测识别&#xff0c;SD卡用来存注册人脸照片和比对数据&#xff0c;三者通…

作者头像 李华