Python实现word文档自动插入交叉引用
0 需求介绍
Word文档(doc,docx)支持的 交叉引用(Cross-reference) 功能在长文档写作中尤为重要.
手动插入交叉引用易错、低效. 使用Word VBA能够部分实现需求, 但基于Python的实现更易与其他脚本相结合.
本文将使用Python的win32com库, 在Word文档中自动插入交叉引用.
另外, 由于Microsoft Learn官方的介绍并不完善, 本文通过实验确定了具体的使用方法和代码行为.
1 环境准备
需要安装Python, 并安装win32com库. 可以使用pip来安装:
pip install pypiwin32
2 win32com基本用法介绍
新建文档
from win32com.client import Dispatch
app = Dispatch('Word.Application')
doc = app.Documents.Add()
app.Visible = True
打开现有文档
import win32com.client
word = win32com.client.Dispatch('Word.Application')
doc = word.Documents.Open(r"C:\PATH\xxx.docx")
根据上面的代码, Dispatch('Word.Application')
打开的是Word这个App, 而App内支持同时打开多个Document.
3 获取所有交叉引用项
allCrossRef = doc.GetCrossReferenceItems(0)
此处, GetCrossReferenceItems()
是Word VBA所支持的函数, 可以参考下面的链接.
https://learn.microsoft.com/en-us/office/vba/api/word.document.getcrossreferenceitems
GetCrossReferenceItems()
的用法为: expression.GetCrossReferenceItems(ReferenceType)
其中,expression
为Document对象, 即Word App内的某个Document.
而该函数的参数为 ReferenceType , 为必需项, 其值为WdReferenceType枚举的常量, 用于表示该函数将要获取的交叉引用项的类型.
WdReferenceType枚举
可参考 https://learn.microsoft.com/en-us/office/vba/api/word.wdreferencetype
Name | Value | Description | 插入方式 |
---|---|---|---|
wdRefTypeEndnote | 4 | Endnote. 尾注, 显示在文档末尾 | 引用(顶部功能区)->脚注(组)->插入尾注 |
wdRefTypeFootnote | 3 | Footnote. 脚注, 显示在页面底部 | 引用(顶部功能区)->脚注(组)->插入脚注 |
wdRefTypeBookmark | 2 | Bookmark. 书签 | 插入(顶部功能区)->链接(组)->书签 |
wdRefTypeHeading | 1 | Heading. 标题 | 开始(顶部功能区)->样式(组)->标题 |
wdRefTypeNumberedItem | 0 | Numbered item. 编号项目 | 开始(顶部功能区)->段落(组)->编号/多级列表 |
通过向GetCrossReferenceItems()
传入对应的参数, 它会以 tuple(元组) 数据类型, 返回document中的交叉引用项.
4 定义和实现
在此,我们将通过GetCrossReferenceItems()
获取的交叉引用项,视作cross_reference_target(CR_tgt).
则接下来,我们需要在文档中确定cross_reference_source(CR_src). 在1个Document中,1个CR_tgt可以被多个CR_src引用.
为了确定CR_src在文档中的位置,我们可以使用如 ‘#’ , ‘^’ 等文档中不常用的符号作为占位符号.
5 Word中Range的概念
在开始编写代码之前,我们还需要了解Word VBA常用的Range概念.
关于Word文档中Range的概念,请参考链接:
https://learn.microsoft.com/zh-cn/office/vba/api/word.range
关于如何处理Word文档中的Range对象,请参考链接:
https://learn.microsoft.com/zh-cn/office/vba/word/concepts/working-with-word/working-with-range-objects
Range对象表示文档中的一个连续区域, 每个Range对象由一个起始字符位置和一个终止字符位置定义,
即Range.Start和Range.End,
这2个属性均为Long数据类型. 它们的位置值从文章开头的字符开始计算, 第一个位置值为0.
注意, Range将计算所有字符, 包括非打印字符, 隐藏字符即便未显示也计算在内.
Range属性 适用于多种对象,如Paragraph,Bookmark,Cell,Document等
start_position = ActiveDocument.Paragraphs(1).Range.Start # 获取Range的起始字符位置
end_position = ActiveDocument.Paragraphs(2).Range.End # 获取Range的终止字符位置
paragraph_string = ActiveDocument.Paragraphs(3).Range.Text # 获取Range字符串
#使用Range方法新建Range对象
my_range = ActiveDocument.Range(0,10) # 返回ActiveDocument对象的前10个字符所对应的range对象
#使用SetRange方法重新设置已经声明的range对象的范围
my_range = ActiveDocument.Paragraphs(4).Range
myrange.SetRange(ActiveDocument.Paragraphs(5).Range.Start, ActiveDocument.Paragraphs(6).Range.Start)
6 确定CR_src的位置
在确定使用占位符号标记CR_src后, 我们可以通过遍历文档并查找占位字符, 来确定CR_src的位置.
for para in doc.paragraphs: #遍历文档中所有paragraph对象,一般地,一个换行符对应一个paragraph
parastr = para.Range.Text #获取paragraph的文本
#将'&','#'作为占位符号,并获取它们在文档(doc)中的位置
CR_src_head_position = [(i+para.Range.Start) for i,c in enumerate(parastr) if c == '&']
CR_src_tail_position = [(i+para.Range.Start) for i,c in enumerate(parastr) if c == '#']
实际应用场景下, CR_src的顺序会不同于GetCrossReferenceItems()
所返回数据的顺序; 返回值也未必一一对应.
因此, 还需要获取CR_src的文本来与GetCrossReferenceItems()
的返回值进行比较或文本匹配.
7 插入交叉引用链接
要在Word中插入交叉引用链接,可以参考下面的链接:
https://learn.microsoft.com/zh-cn/office/vba/api/word.range.insertcrossreference
Range.InsertCrossReference()
可以插入对标题、书签、脚注、尾注或定义了题注标签的项(如公式、图表或表格)的交叉引用.
其语法为:
expression.InsertCrossReference(
_ReferenceType_ , _ReferenceKind_ , _ReferenceItem_ , _InsertAsHyperlink_ ,
_IncludePosition_ , _SeparateNumbers_ , _SeparatorString_ )
例如CR_src_range.InsertCrossReference(0, -1, 1, True)
参数名称 | 是否为可选参数 | 数据类型 | 说明 |
---|---|---|---|
ReferenceType | 必需 | Variant | 为要插入交叉引用的项的类型.可以是任何 WdReferenceType 或 WdCaptionLabelID 常量或用户定义的标题标签 |
ReferenceKind | 必需 | WdReferenceKind | 要包含在交叉引用中的信息类型 |
ReferenceItem | 必需 | Variant | 如果 ReferenceType==wdRefTypeBookmark,该参数指定的书签名称;对于所有其他 ReferenceType 值,此参数在 交叉引用对话框中的 引用类型选项中指定物料编号或名称. 使用 GetCrossReferenceItems 方法返回可与此参数一起使用的项名称的列表 |
InsertAsHyperlink | 可选 | Variant | True 则将交叉引用作为超链接来插入 |
IncludePosition | 可选 | Variant | True 则根据引用项相对于交叉引用的位置插入"见上方"或"见下方," |
SeperateNumbers | 可选 | Variant | 若( ReferenceType==wdRefTypeNumberedItem 且 ReferenceKind==wdNumberFullContext)才可将该参数设置为True, 否则会报错; 若True 则用 SeperateString 以从associated text中分离numbers |
SeperateString | 可选 | Variant | 若 SeperateNumbers==True, 则用该参数作为分隔符分离数字, 否则会报错 |
参数说明较为抽象,接下来详细说明:
ReferenceType 使用的WdReferenceType枚举, 如前文所示.
ReferenceKind 使用WdReferenceKind枚举, 如下所示:
Name | Value | 说明 |
---|---|---|
wdContentText | -1 | 插入指定项的文本值. 例如,插入指定标题的文本 |
wdEndnoteNumber | 6 | 插入尾注引用标记. |
wdEndnoteNumberFormatted | 17 | 插入带格式的尾注引用标记. |
wdEntireCaption | 2 | 插入指定的公式、图表或表格的标签、编号和任何其他题注. |
wdFootnoteNumber | 5 | 插入脚注引用标记. |
wdFootnoteNumberFormatted | 16 | 插入带格式的脚注引用标记. |
wdNumberFullContext | -4 | 插入完整的标题或段落编号. |
wdNumberNoContext | -3 | 插入标题或段落,但不插入它在多级符号列表中的相对位置. |
wdNumberRelativeContext | -2 | 插入标题或段落,并且插入它在多级符号列表中的相对位置,该相对位置应足以标识该项目. |
wdOnlyCaptionText | 4 | 只插入指定公式、图表或表格的题注文字. |
wdOnlyLabelAndNumber | 3 | 只插入指定公式、图表或表格的标签和编号. |
wdPageNumber | 7 | 插入指定项的页码. |
wdPosition | 15 | 根据需要插入单词"Above"或"Below". |
ReferenceItem : 即GetCrossReferenceItem()
返回值所对应的项目.
Eg: 对于编号项, GetCrossReferenceItem(0)
的返回值: ('1.1 第一个编号','1.2 第二个编号')
.
则当InsertCrossReference(_ReferenceItem_ =1)时
, 插入指向('1.1 第一个编号')
的交叉引用.
InsertAsHyperLink : 所插入的交叉引用是否形成超链接.
该值为True, 则插入交叉引用超链接(右键该交叉引用,点击切换域代码,可查看域代码;按住ctrl点击该交叉引用,跳转到对应位置)
该值为False,则插入交叉引用(右键该交叉引用,点击切换域代码,可查看域代码;按住ctrl点击该交叉引用,不跳转)
IncludePosition : 该值为True, 则根据插入交叉引用的位置, 插入"见上方"或"见下方".
一些例子
Eg1: 对于编号项, GetCrossReferenceItem(0)
的返回值: (‘1.1 第一个编号’,‘1.2 第二个编号’).
运行代码: my_range.InsertCrossReference(0,RefKind,1,True)
RefKind = 1
, 在 my_range 位置插入交叉引用超链接,显示的文本为"第一个编号"
RefKind in [-2,-3,-4]
, 在 my_range 位置插入交叉引用超链接, 显示的文本为"1.1"
RefKind = 7
, 在 my_range 位置插入交叉引用超链接, 显示的文本为交叉引用所在页码
RefKind = 15
, 在 my_range 位置插入交叉引用超链接, 显示的文本为"见上方"或"见下方"(根据交叉引用所在位置决定)
RefKind in [2,3,4,5,6,16,17]
, 由于 ReferenceType 和 ReferenceKind 不匹配,会报错如下:
File “<COMObject Range>”, line 3, in InsertCrossReference
pywintypes.com_error: (-2147352567, ‘发生意外.’, (0, ‘Microsoft Word’, ‘命令失败’, ‘wdmain11.chm’, 36966, -2146824090), None)
Eg2: 文档内容如下:
1. 第1个编号(\r\n)
a) 第1a个编号(\r\n)
i. 第1ai个编号(\r\n)
.....(some text here)
GetCrossReferenceItem(0)
的返回值: ('1. 第一个编号', ' a) 第1a个编号', ' i. 第1ai个编号')
.
运行代码: my_range.InsertCrossReference(0, -4, 3, True, False , True,"-" )
.
则会在 my_range
位置插入指向 i. 第1ai个编号
的交叉引用链接, 插入的链接的文本为"1.-a)-i".
Eg3: 文档内容如下:
1. 第1个编号(\r\n)
a) 第1a个编号(\r\n)
i. 第1ai个编号 ##(注意, 此处没有后续内容)
GetCrossReferenceItem(0)
的返回值: ('1. 第一个编号', ' a) 第1a个编号', ' i. 第1ai个编号')
.
运行代码: my_range.InsertCrossReference(0, -4, 3, True, False , True,"-" )
.
则由于 i. 第ai个编号
所处的已经是doc的最后一个paragraph, 导致InsertCrossReference()
的 ReferenceItem=3时, 会出现溢出, 导致报错.
File “”, line 3, in InsertCrossReference
pywintypes.com_error: (-2147352567, ‘发生意外。’, (0, ‘Microsoft Word’, ‘命令失败’, ‘wdmain11.chm’, 36966, -2146824090), None)
这种GetCrossReferenceItem()
和InsertCrossReference()
行为不一致的现象, 需要特别关注.
8 总结
通过阅读本文, 可以实现需求中预期实现的功能, 即通过Python win32com库在Word文档中自动地插入交叉引用. 希望能提高工作效率.