如何用Python-Docx创建兼容Google Docs的多级数字列表?
解决方案
原代码的核心问题是每个层级单独创建abstractNum定义,不符合docx的标准嵌套列表规范。正确的做法是:一个嵌套列表对应一个包含所有层级(ilvl 0、1、2...)的abstractNum,每个层级单独定义编号格式、缩进和计数规则,且Google Docs严格遵循这一规范。
修正后的代码如下:
from docx import Document from docx.oxml import OxmlElement from docx.oxml.ns import qn # 预定义嵌套列表的层级配置:(编号格式, 显示文本, 缩进英寸, 悬挂缩进英寸) LEVEL_CONFIGS = [ ("decimal", "%1.", 0, 0.25), ("decimal", "%1.%2.", 0.5, 0.25), ("decimal", "%1.%2.%3.", 1.0, 0.25) ] # 全局变量:存储已创建的数字列表numId,避免重复创建 numbered_list_numId = None def get_next_abstractNumId(numbering): abstract_nums = numbering.findall(qn('w:abstractNum')) existing_ids = [int(num.get(qn('w:abstractNumId'))) for num in abstract_nums] return max(existing_ids) + 1 if existing_ids else 0 def get_next_numId(numbering): nums = numbering.findall(qn('w:num')) existing_ids = [int(num.get(qn('w:numId'))) for num in nums] return max(existing_ids) + 1 if existing_ids else 0 def create_nested_numbering_def(doc): """创建包含所有层级的嵌套数字列表定义""" numbering = doc.part.numbering_part.numbering_definitions._numbering abstract_num_id = get_next_abstractNumId(numbering) abstract_num = OxmlElement('w:abstractNum') abstract_num.set(qn('w:abstractNumId'), str(abstract_num_id)) # 为每个层级创建lvl元素 for ilvl, (num_fmt, lvl_text, left_indent, hanging_indent) in enumerate(LEVEL_CONFIGS): lvl = OxmlElement('w:lvl') lvl.set(qn('w:ilvl'), str(ilvl)) lvl.set(qn('w:start'), '1') # 编号格式 numFmt = OxmlElement('w:numFmt') numFmt.set(qn('w:val'), num_fmt) lvl.append(numFmt) # 显示文本 lvlText = OxmlElement('w:lvlText') lvlText.set(qn('w:val'), lvl_text) lvl.append(lvlText) # 对齐方式 lvlJc = OxmlElement('w:lvlJc') lvlJc.set(qn('w:val'), 'left') lvl.append(lvlJc) # 缩进设置(Google Docs依赖此识别层级) ind = OxmlElement('w:ind') ind.set(qn('w:left'), str(int(left_indent * 1440))) # 1英寸=1440缇 ind.set(qn('w:hanging'), str(int(hanging_indent * 1440))) lvl.append(ind) # 段落样式关联 pStyle = OxmlElement('w:pStyle') pStyle.set(qn('w:val'), f'List Number {ilvl+1}') lvl.append(pStyle) abstract_num.append(lvl) numbering.append(abstract_num) # 创建num实例关联abstractNum num_id = get_next_numId(numbering) num = OxmlElement('w:num') num.set(qn('w:numId'), str(num_id)) abstract_num_ref = OxmlElement('w:abstractNumId') abstract_num_ref.set(qn('w:val'), str(abstract_num_id)) num.append(abstract_num_ref) numbering.append(num) return num_id def apply_numbering(paragraph, num_id, level): """为段落应用指定numId和层级的编号""" pPr = paragraph._p.get_or_add_pPr() numPr = pPr.get_or_add_numPr() # 设置numId numId = numPr.get_or_add_numId() numId.val = str(num_id) # 设置层级 ilvl = numPr.get_or_add_ilvl() ilvl.val = str(level) def add_new_list_item(doc, text, level, list_style): global numbered_list_numId paragraph = doc.add_paragraph(text, style=list_style) paragraph.paragraph_format.line_spacing = 1 if list_style == 'List Number': # 首次使用时创建嵌套编号定义 if numbered_list_numId is None: numbered_list_numId = create_nested_numbering_def(doc) apply_numbering(paragraph, numbered_list_numId, level) # 生成文档 document = Document() document.add_heading('First Header') add_new_list_item(doc=document, text='First Item', level=0, list_style='List Number') add_new_list_item(doc=document, text='Second Item', level=0, list_style='List Number') add_new_list_item(doc=document, text='Third Item', level=0, list_style='List Number') document.add_heading('Second Header') add_new_list_item(doc=document, text='First Item', level=0, list_style='List Number') add_new_list_item(doc=document, text='First SubItem', level=1, list_style='List Number') add_new_list_item(doc=document, text='Second SubItem', level=1, list_style='List Number') add_new_list_item(doc=document, text='Second Item', level=0, list_style='List Number') add_new_list_item(doc=document, text='Third Item', level=0, list_style='List Number') add_new_list_item(doc=document, text='Third SubItem', level=1, list_style='List Bullet') add_new_list_item(doc=document, text='Fourth Item', level=0, list_style='List Number') document.save('fixed_lists_test.docx')
关键改动说明:
- 统一嵌套列表定义:创建一个包含所有层级的abstractNum,每个层级设置对应的编号格式(如
%1.%2.表示二级编号)、缩进参数,符合docx标准结构,Google Docs能正确识别。 - 标准化缩进设置:使用docx原生的
w:ind元素(以缇为单位)设置缩进,替代手动设置left_indent,确保层级信息被Google Docs正确解析。 - 复用numId:同一个嵌套列表复用同一个numId,避免重复创建numbering实例,保证计数连续性。
- 移除不当全局变量重置:删除
prev_li = None的重置操作,确保列表上下文连续,计数逻辑正确。 - 层级配置可视化:用
LEVEL_CONFIGS统一管理各层级的格式,便于扩展更多层级。
此代码生成的docx文件在本地显示正常,上传到Google Docs后嵌套列表的层级和编号也能完整保留,和MS Word创建的文档效果一致。
内容的提问来源于stack exchange,提问作者dfop02
相关产品推荐
相关产品推荐

