首发于 计算机

以Python控制Word

买的是何华平的《学python,不加班》。这几天抽空看了一下,基本明白它的意思,感觉很不错。学习这个的主要目的,是要在实际工作整理来自不同专业的人员编写的文章内容,各人也没有什么标准,标题分级五花八门。况且很多内容本来是复制的,所以这个问题一直很头痛。另外一般人从来也没有认真学过word软件,都是大概看看百度一下就用它。但是碰到需要严格处理表格、对齐,图片插入等问题时,在有限的时间要求下要解决问题真是急。看上这本书,主要还是发现他正好要谈论深入了解一个python库的问题,正好可以学学如何编写这些库。最好是自己来编写修改这些库。而且该书很精辟地指出面向对象编程中类的方法,就是为了解决太复杂情况下的函数太多的问题的。因此,通过对本书的学习,可以逐渐真正实践使用类的概念来编程了。这对于复杂的系统,是完全不可少的。

python的重要性是不言而喻的。关键的问题是,如何通过上机测试将程序边跑边修正。在这个过程中,不断体会整个程序语言系统架构的合理。我之前不理解为什么要搞个异常处理。后来在企图考虑一切可能情况被折腾得吐血才明白这个道理:允许错误,程序继续运行,不能考虑一切情况。开始也不知道正则表达式之类的用途,后来晓得编程时需要不断转换信息,识别信息,文字处理需要这些东西。总而言之,我一开始的想法就很明确,就是去操作,试错,运行,修正,不断反复,直到成功运行。为了不打击自己的信心,先运行别人编好的程序,有了这些成功的实例,信心就越来越大。到后来就直接边写文字大纲边写,信心十足,出问题也不害怕。

一 基本的认知理解

与word文件相关的,无非是这些东西:

1 打开一个word,是否同时打开多个文件,例如文件1,文件2.关闭它就会关闭多个文件,例如文件1,文件2都将这样关闭。cad就是这样的架构。可能word需要设置特别的参数才会这样,至少我现在操作的时候,这些哪怕从同一个菜单的文件栏打开的不同文件,似乎并不受这个约束。

2 对某一个具体文件,我们关心怎样清晰地辨认节,段,句,词,字,表格,图片等。因为只有清晰地分辨好这些,才能让计算机找到它们,修改它们。

3 对于不规范的分段分节及标题分级,想要完全让计算机来识别并修正,看来是个很复杂的已经与软件的基本学习无关了。我们可以给出一个大概的方案来处理那些不是特别混乱的情形。

a,通过模糊查找来确定某种级别的标题格式,并修正它们。

b,如果将所有的非标题内容都压缩成简单的符号代替,我们将集中观察分析所有的各级标题。这种情况下,这个标题分级并不见得很复杂。哪怕用人工将其修正,再返回都是高效的。关键是这个处理必须还得逆转回去。

c,对作者混乱不堪的标题分级进行分析,识别哪些是一级标题,哪些是二级标题,哪些是三级标题等等。即便如此,在极端情况下,作者仍可能把标题分级搞得很混乱。所以,还是需要一个比较高质量的原始文件而不是任何乱起八糟的文件都可以整合处理。我今天知道原来随便写的标题,word并不认可,有时候又认可。点击查找文件会跳出导航,导航会显示文件的各级标题,如果严格按照要求写标题就会非常清晰条理便于修改。但实际上很多人都是按照自己的认识理解不规范地使用word。

d,在很多时候,同一页面的分两栏排版并不像我们想当然的那样会很好或者很简单地对齐,反而是常常会错位。上边或下边空出不该空出的行数几乎就是常态。在我们设定一个比较完美有序整洁的目标 之后,才发现原来这排版并非那么简单。因为这个起初看上去似乎很简单的软件,在包含了大量的各种不同参数设置后,必定会发生很多的冲突。这需要我们把它的文件构成和底层结构都搞清楚。

e,删除某页,增加某页。并且该页可能突然要用竖向版式,或者一栏形式。完全不同于其它的页面。这是因为在某些局部,需要插入非常大的表格,或图片,按照相同的处理办法,打印出来或显示就会不清楚。此时就要单独增加版式,例如从正常的A3横版变成A2横板或A1横板等。这些处理突然改变原有的版式,往往带来很多的困难。这需要对相关命令作深入的分析学习。

f,如何对齐表格的内部和表格之间,以及合理处理表格与其它文字内容的关系。表格数量很多,内容很复杂,问题就会变得很复杂。如何有效处理各部分的段落,句子的间距控制。如何去掉不必要的空格。在实践中,会出现很多命令失效或特定情况才有效的情况。这些问题都会在严苛要求排版效果时变得尖锐起来。

现在我们就要尝试用python解决这些问题。设计一个个由简单到复杂的程序 W_{i} ,观察如何打开软件建立一些逐渐复杂完善的文件,观察如何读入别人的文件,识别别人文件的相关的节,段,词,表格等等。显然,关键在于我们是如何按照这些系统 S{j} 的构成要素 A_{k} ,来建立并且识别。如果我们能自由确认或者建立文章的正文,标题,明确文章的节,段,句,词,字,我们就能处理所有关乎它们的各个层面上的问题。

就各级标题来讲,我们自己随便写的标题是不被认可的。如果能严格按照软件本身的要求去设置标题,将会使后续的整理非常方便。其它的一些混乱也是因为不规范的操作引起的。从这个角度讲,设法将原来的非标准文本读入,识别清楚其标题分级和段落,句子,词,字,重新将其归零按照严谨的法则去组织成新的文档,是避免产生更多乱七八糟问题的有力举措。也就是重构。

二 构建测试程序

其实网络搜索是可以找到大量有用的知识的。但是用几十块钱去买一些书的方法其实更有价值。这不仅是对作者劳动的尊重,也必然在一个正规的途径上获得真诚的回报。就word的控制,现在我们可以很轻松从原书所带的程序和资料来构建我们自己的测试程序。首先要确保程序运转起来。我们先研究怎样用程序建立自己的word文件。显然,我们很容易迷惑于作者的描述。但实际上,如果以自己的目标为中心,问题会变得简单和清晰。

实际上任何学问都是以实践试验观察为基础的。抽象的理论也同样需要这种扎实的枯燥的劳动。最美妙的结果,是那些只能通过直觉感觉就得到的关键结论,你不可能用逻辑得出这个结论。也许这点会是人工智能的永不可及的地方。你觉得是这样,它就是这样。没有更多的理由。

1 建立最简单的“文件生成、创建内容、关闭文件”模型

# -*- coding: utf-8 -*-
from win32com.client import constants,DispatchEx
wordApp = DispatchEx('Word.Application')
#前台运行
wordApp.Visible=1
#不弹出警告
wordApp.DisplayAlerts=0
with open(r'D:\pythonST\GLXT\WORD自动化\Wordstudy\yswj\第6章\公文格式自动设置.txt') as f:
    text=f.read()
#打开默认的模板    
myDoc=wordApp.Documents.Add()
myDoc.Range(0,0).InsertBefore(text)
#按模板新建的文件保存到指定路径并命名
myDoc.SaveAs(r'D:\pythonST\GLXT\WORD自动化\Wordstudy\yswj\第6章\测试word2.docx')
myDoc.Close()
#新打开默认的模板    
myDoc1=wordApp.Documents.Add()
#再建一个文件但不保存
myDoc1.Range(0,0).InsertBefore(text)
#测试SaveChanges参数的作用
wordApp.Quit(SaveChanges=-2)

上面的测试程序用于建立一个新文件,这个新文件是从txt格式的文件拷贝进来的。否则就会是一个空文件。因为win32是用来对word进行操控,并非直接的编写word文件的。后面再讨论能直接编写word文件的python-docx。

上面程序 运行实际上也解决了前面的一个困惑,使用同一个菜单栏打开的几个word文件好像是不关联的。实际上应该是设置的问题。上面程序运行表明,同时打开建立的多个文件,受控于同一个Application对象,关闭它就会关闭相关的多个文件。

2 获得文件Document属性

上面实际上基本认识了Application对象。现在来看Document对象,先修改前面的测试程序,并打开指定文件,进入shell操作模式。

# -*- coding: utf-8 -*-
from win32com.client import constants,DispatchEx
wordApp = DispatchEx('Word.Application')
#前台运行
wordApp.Visible=1
#不弹出警告
wordApp.DisplayAlerts=0
#打开指定文件   
myDoc=wordApp.Documents.Open(r'D:\pythonST\GLXT\WORD自动化\Wordstudy\yswj\第6章\测试word2.docx')

按书中示范,要获得页数信息。

myDoc.BuiltInDocumentProperties(constants.wdPropertyPages).Value

运行结果是错误的。

Traceback (most recent call last):

File "<pyshell#10>", line 1, in <module>

myDoc.BuiltInDocumentProperties(constants.wdPropertyPages).Value

File "C:\Users\Administrator\AppData\Local\Programs\Python\Python36\lib\site-packages\win32com\client\__init__.py", line 231, in __getattr__

raise AttributeError(a)

AttributeError: wdPropertyPages

搜索“AttributeError: wdPropertyPages”进入相关网页 使用BuiltInDocumentProperties设置或查询Word内置属性 - Wang Ye / 王 晔 Document.BuiltInDocumentProperties 属性 (Word) | Microsoft Learn ,可以看出作者把语句中括号里面的内容写成英文了。应该按该书163页表6-2的索引,wdPropertyPages对应的序号是14,运行

myDoc.BuiltInDocumentProperties(14).Value

结果是8。

分别在不加页眉和加了页眉之后运行如下语句得到的结果

>>> [aStory.StoryType for aStory in myDoc.StoryRanges]
>>> [aStory.StoryType for aStory in myDoc.StoryRanges]
[1, 7, 12, 13, 15, 16]

将StoryType改成Text就得到了相关的内容。

因此,我们现在能获得文档的正文,页眉等相关情况和内容。直接运行myDoc.Content.Text也得到了正文内容。

它是怎么分清“段”的?

>>> myDoc.Paragraphs.Count
9

可以看出该文件段数就是句数。现在改变句子,将几个句子接起来。结果不同。

运行如下命令,让它按段显示文字

>>> for i in range(1,myDoc.Paragraphs.Count+1):
    print('段落'+str(i)+":",myDoc.Paragraphs(i).Range.Text)
段落3: A银行风险管理与内部控制情况的报告一,风险管理
段落4: 核)、条线检查发现的问题,及时制订整改方案,认真分析查找原因,完善相关措施,消除风险隐患。6、内部控制评价结论.本行已按照《企业内部控制基本规范》及内部会计控制具体规范的要求,对于2017年6月30日与财务报表相关的内部控制设计的合理性进行了评价,并对执行的有效性进行了测试。本行确认于2017年6月30日在所有重大方面有效地保持了按照《企业内部控制基本规范》及内部会计控制具体规范标准与财务报表相关的内部控制。(二)会计师对本行内部控制的评价
段落5: 普华永道出具了《A银行股份有限公司截至2017年6月30日止的内部控制审核报告》(普华永道中天特审字(201X)第X号),认为:“贵行于2017年6月30日按照《企业内部控制基本规范》在所有重大方面保持了有效的财务报告内部控制。”

原来前面出现了空行,都被认为是段落。一般情况下,连在一起的句子就构成段落。但如果上一句刚好满行呢?

其它的节,句等句不用测试了。现在我们知道我们能按段落句子确定文件的情况。

回到我们最初的问题,如何处理那些乱起八糟的个人乱写的各级标题呢?我们可以将这些标题控制在不同的段落中。它们不会在同一段落。再者,一级标题通常是作者自定义的汉语或阿拉伯数字后跟简单的一句话,二级标题通常是很多句话甚至有多个段落。但标题总是在段落的前面。现在基本上我们能把标题给拎出来了。怎么分清标题的级别呢?假设我们将某作者的内容插入之前就进行处理的话,那么作者的文件是一个独立的文件。它的第一个标题应该是一级标题。与它相类似的就是一级标题。因此我们基本上就分出一级标题了。在作者不至于极度混乱的情况下,他的文章标题分级应该是有序的,重复的。不应该让不同级别的标题采取同样几乎一样的形式。我们可以先研究作者的标题分级的规则。在这个基础上,我们应该能够分析出一级标题和二级三级等不同级别标题的不同。基于此,我们可以让计算机 \rightarrow 找出作者所有的标题 \rightarrow 判断标题的分级 \rightarrow 将其标题按照标准定义重构,但文字内容不变。

有个别人会在段落里继续写小标题分几部分阐述。这种情况可以针对某些段单独按句子来找到标题。一般的标题分析还是按照段落来确定比较合理。

值得注意的是文件(文档 )的段落,句子,单词等属性,通过类似myDoc.Paragraphs(2).Range.Text的格式才能获得文字,也就是其中引用了Range。括号里的编号是总数量中的某个号,用以指定是哪个具体的对象。另外,我们也关心有多张图片时,是不是也是这样获得其控制?

>>> myDoc.InlineShapes.Count
>>> myDoc.InlineShapes.Count
>>> myDoc.InlineShapes.Count
3

测试表明,插入的图片,如果按嵌入式设置格式,则能找到。否则就不能被认为是图片。这实际上给计算机控制带来了难度。

事实上,类似myDoc.StoryRanges(7).Text这样的加索引号能得到各部分的文字,但是却不能获得第二节的页眉内容。也就是这些操作不能分清不同节的情况。该书给出的文件CorePrinciples.docx是一个相对比较完整的文件,可以用来进行更好的测试。

可以看到,由于表格的原因,我们关于正文所作的段落分析会受到严重影响。也许我们需要先去掉图像和表格才能够通过段落的控制处理混乱的标题。这就需要把它们保存到另一个文件储存,将来再拷贝进来。

从广义的角度看,如果我们能清晰地确定一个文档的段落总数,每个段落索引号,句子总数,句子索引号,非正文的各部分等等,实际上也就基本完成了对象的定位。有了定位,又有各种内容的复制删除修改等控制的话,就基本能实现我们的深入控制。但这些操作未必是方便的,还要继续学习后面的方法。

3 Range对象

这部分内容不是很清晰。不过本着“并不打算成为word专家”的态度,有些东西也不能深究太过。正如上述,我们不能指望myDoc.StoryRanges(7).Text弄出第二节的页眉文字,更不要说页眉里面的文本框里面的文字了。一般来说,关于正文,节和段落,不能直接得到它的文字内容,要加上“.Range”就可以。而句子,词,字符就不需要,而是直接使用。

现在我们考虑页眉页脚等非正文部分的区域控制。对页眉来说,参照以下语句能得到不同节的页眉的文字内容。

>>> myDoc.Sections(1).Headers(1).Range.TexT
'第1节页眉 \r'
>>> myDoc.Sections(2).Headers(1).Range.TexT
'第2节页眉\r'
>>> myDoc.Sections(1).Headers(1).Shapes(1).TextFrame.TextRange.TexT
'学习资料\r'

也能得到页眉里面的文本框里面的文字。如Shapes(1).TextFrame.TextRange.TexT。

上面的Headers(1)括号里面的数字该文件只能取1,2,3,我也不知道什么意思。但显然是不同部分的内容。它应该是列表编号,只是0好像是给C++什么来着专用,似懂非懂。运行

>>> myDoc.Sections(1).Headers.Count
3

知道它包含3块内容,并且数字正好是1,2,3。

原书提供的代码myDoc.Sections(1).Headers(constants.wdHeaderFooterPrimary).Range运行无效。基于经验,括号里的应该是数字,果然是数字,但到底啥意思只能猜了。

本来上一节内容,通过myDoc.StoryRanges(7)这样的句子,我们能索引非正文的每个部分。但实际运行,发现该命令不能区分不同节的变化。该书提供的文件CorePrinciples.docx的第一页和第二页是不同的两节,有不同的页眉页脚,该命令不能得到第二节的页眉页脚。但上面的命令已经解决了这个问题。

由于引入了新变量Headers,不清楚对于其它非正文部分又该各自引入什么变量呢?好在一般情况下,我们用不到如此复杂的非正文设置,也没必要去深究了。倒是通过搜素得到了一个VB参考网页 Document.StoryRanges 属性 (Word) | Microsoft Learn 。开始想找这样的网页学习反而找不到,现在又找到了。

Range作为一个文档区域,它是允许跨越很多的节,段落的,这样,它的里面包含节,段等就不难理解了。它返回的节和段落是否仍需要通过".Range"来获得文字内容可以测试一下即可。

现在基本清楚了确定正文非正文的Range的内容控制。当然,可以尝试myDoc.Sections(1).Headers(1).Shapes(1).Delete()删除页眉里面那个文本框,还真删除了。也可以尝试myDoc.StoryRanges(7).Delete()虽然内容删除了却还是留下一根线。并不是删除页眉的手工效果。

使用myDoc.StoryRanges(5).Text,可以看出是第一个文本框里面的文字。删掉它,再运行,就是第二个文本框的文字。但是myDoc.StoryRanges(7).Delete()不能删除文本框本身,只能删除里面的文字内容。

关于文本框的使用,英文对应的是Shapes,从Document的属性看,它应该跟表格图片是平等的东西。既然内容Content能用myDoc.Content.Text获得文字,Shapes里的文字应该也是使用类似的语句。又参考页眉示例中的文本框文字内容,就得到了正确的语句

>>> myDoc.Shapes(1).TextFrame.TextRange.TexT
'资本的类别和子类的数目应当严格限制。一级资本和二级资本分别被定义为在持续经营下吸收损失的资本和在破产清算时吸收损失的资本。此外,关于监管调整的最低要求也应在国际上达成一致。\r监管资本的各部分都应当充分披露,并与披露财务报告相协调,以保证市场参与者和监管当局能够比较各国银行的资本充足率。\r'
>>> myDoc.Shapes(2).TextFrame.TextRange.TexT
'对于股份公司制的银行,一级资本的主要形式应为普通股和留存收益。监管调整项目将全球统一,并适用于普通股。\r为保证质量和一致性,普通股必须满足一套合格标准才能被计入一级资本的主要形式。这些标准也适用于非股份制公司的银行,比如合作制银行,确定哪些具有相同的质量的金融工具可以作为主要形式计入一级资本\r'

运行

>>> myDoc.Shapes.Count
2

能得到Shapes的数量,从错误信息看,括号里的数字不能为0,它有两个则编号确实就是1,2.猜想myDoc.Shapes(1).Delete()能够删除这个形状,果然如此。书中关于表格的处理使得我们更加熟悉这个软件了。但分类似乎是有点乱,或者自己理解不准确,或者是中文翻译的原因。因为Shapes的内容不计入正文中。但表格却计入正文中。一方面,Shapes跟表格图片Content列在一起,另一方面又能作为非正文的内容被StoryRange(5)索引。然而如前所述,只能索引到第一个Shapes。要索引到第二个就要删除第一个,且是彻底删除它。

关于Header的处理,我们希望能让其它非正文区域也可以类似它这样。基本上应该是没问题的,因为在书的第6章后面我们就看到了Footer等信息。

>>> myDoc.Sections(1).Footers(1).Range.TexT
'\t第1节页脚\t\r'

其实我们希望能将各部分复制下来粘贴到别的位置。也希望能删除某些部分。这样就可以仅仅通过预先手工制作的模板文件完成各种新文件的编辑。但其实实现这些有很多方法可以选择。因此没必要在局部知识点过多测试猜想。

现在来看关于Range的复制和粘贴。要特别注意以下的命令本来是有效的,但如果去复制新代码就会出错。因为复制其实就是复制到了剪贴板而已。要运行正确,此时就不能像平常那样再去复制之前的代码,必须老老实实输入代码的每个字符。

>>> myDoc1=wordApp.Documents.Add()
>>> myDoc.Sections(1).Range.Paragraphs(2).Range.Copy()
>>> myDoc1.Content.Paste()
>>> myDoc.Sections(1).Range.Copy()
>>> myDoc1.Content.Paste()

对Sections(1)的复制好像有点乱没有按照预期。这些都不是问题。

Range的塌陷到起始点再移动,形象是自然的。就像把一个区域收缩到一个点仍然保持它再展开。但是关于它的操作,书中甚至没出现示例。又由于该书的代码总是在括号中引用英文,但在我电脑上证明是不允许的。因此,关于Range的扩充移动等操作必须全部验证一下。

>>> rA = myDoc.Tables(1).Cell(2,1).Range
>>> rA.Text
'剩余期限\r\x07'
>>> rA.Expand(15)
>>> rA.Text
'发行评级AAA 到 AA-/A-1债券的标准的监管折扣系数\x02\r\x07\r\x07剩余期限\r\x07主权\r\x07其他发行人\r\x07证券化暴露\r\x07\r\x07<1年\r\x070.5\r\x071\r\x072\r\x07\r\x07>1年<5年\r\x072\r\x074\r\x078\r\x07\r\x07>5年\r\x074\r\x078\r\x0716\r\x07\r\x07\r\x07\r\x07\r\x07\r\x07\r\x07'

上述测试表明,Range的延申保持使用原名。rB = rA.Expand(15)是错误的语句。

从网页 WdUnits 枚举 (Microsoft.Office.Interop.Word) | Microsoft Learn 我们在Learn菜单搜索wdTable选择 WdUnits 枚举 (Microsoft.Office.Interop.Word) 就找到了16个值的表格,与该书所列表6-7一样。

继续测试Range的塌陷和移动。注意作者没有直接使用Collapse命令,是因为Move命令已经包含了塌陷效果。

>>> rangeA = myDoc.Paragraphs(3).Range
>>> rangeA.Text
'Capital adequacy: Supervisors must set prudent and appropriate minimum capital adequacy requirements for banks that reflect the risks that the bank undertakes, and must define the components of capital, bearing in mind its ability to absorb losses. At least for internationally active banks, these requirements must not be less than those established in the applicable Basel requirement. \r'
>>> rangeB = myDoc.Paragraphs(4).Range
>>> rangeB.Text
'Risk management process: Supervisors must be satisfied that banks and banking groups have in place a comprehensive risk management process (including Board and senior management oversight) to identify, evaluate, monitor and control or mitigate all material risks and to assess their overall capital adequacy in relation to their risk profile. These processes should be commensurate with the size and complexity of the institution.\r'
>>> rangeA.Start
>>> rangeA.End
>>> rangeB.Start
>>> rangeB.End
>>> rangeA.Move(1,1)
>>> rangeA.Start
>>> rangeA.End
>>> rangeA.Expand(4)
>>> rangeA.Start
>>> rangeA.End
>>> 

上面的测试清晰地表明:对第三段Range使用移动命令Move(1,1),将使它塌陷到该段的末尾和第四段的起初。虽然区域变成一个点,还可以展开。所以使用命令Expand(4)之后,它延展的是第四段的全部,而非第三段的全部,这证明塌陷到的那个点,是一个非零收缩点并属于第四段的Range,它的确是第三段Range的塌陷,而是后来按段扩张的起始点。

4 Selection对象和Bookmarks对象

我们可以理解这个Selection是可以跨越连续区域的,因此能够像我们平常在word中作方形选区而已,因此能选择到Range无法选到的东西。只要验证Range对象是怎样变成Selection对象,后者怎样实现复制粘贴即可。

至于Bookmarks对象,可以看出它不影响我们对整个体系的把握。这些知识可以留待将来进阶再补充了。它们可能会给我们带来一些意想不到的好处,可是在前期却没必要耗费太多的精神。

>>> myDoc2 = wordApp.Documents.Add()
>>> rangeA = myDoc.Paragraphs(3).Range
>>> rangeA.Text
'Capital adequacy: Supervisors must set prudent and appropriate minimum capital adequacy requirements for banks that reflect the risks that the bank undertakes, and must define the components of capital, bearing in mind its ability to absorb losses. At least for internationally active banks, these requirements must not be less than those established in the applicable Basel requirement. \r'
>>> rangeA.Select()
>>> sX = wordApp.Selection
>>> sX.Copy()
>>> myDoc2.Content.Paste()

上面的代码就实现了Selection对象的复制和粘贴。注意,Range对象rangeA通过命令rangeA.Select(),结合sX = wordApp.Selection将选择集赋予了名称sX。而非直接把rangeA.Select()赋给变量sX。这个机制似懂非懂。原书也没有讲清楚。这是自己测试出来的。然后让sA做Copy()操作,粘贴当然跟Range对象的操作一样。

在该节的最后,还是来测试Range在自身文件的插入问题。因为复制的区域粘贴到自身,或者增加新的内容,乃是实际工作最基本的一个需要。

>>> rangeA = myDoc.Paragraphs(3).Range
>>> rangeA.Text
'Capital adequacy: Supervisors must set prudent and appropriate minimum capital adequacy requirements for banks that reflect the risks that the bank undertakes, and must define the components of capital, bearing in mind its ability to absorb losses. At least for internationally active banks, these requirements must not be less than those established in the applicable Basel requirement. \r'
>>> rangeB = myDoc.Paragraphs(4).Range
>>> rangeB.Text
'Risk management process: Supervisors must be satisfied that banks and banking groups have in place a comprehensive risk management process (including Board and senior management oversight) to identify, evaluate, monitor and control or mitigate all material risks and to assess their overall capital adequacy in relation to their risk profile. These processes should be commensurate with the size and complexity of the institution.\r'
>>> rangeA.InsertAfter(rangeB)
>>> rangeA.Text
'Capital adequacy: Supervisors must set prudent and appropriate minimum capital adequacy requirements for banks that reflect the risks that the bank undertakes, and must define the components of capital, bearing in mind its ability to absorb losses. At least for internationally active banks, these requirements must not be less than those established in the applicable Basel requirement. \rRisk management process: Supervisors must be satisfied that banks and banking groups have in place a comprehensive risk management process (including Board and senior management oversight) to identify, evaluate, monitor and control or mitigate all material risks and to assess their overall capital adequacy in relation to their risk profile. These processes should be commensurate with the size and complexity of the institution.\r'
>>> rangeB.Text
'Risk management process: Supervisors must be satisfied that banks and banking groups have in place a comprehensive risk management process (including Board and senior management oversight) to identify, evaluate, monitor and control or mitigate all material risks and to assess their overall capital adequacy in relation to their risk profile. These processes should be commensurate with the size and complexity of the institution.\rRisk management process: Supervisors must be satisfied that banks and banking groups have in place a comprehensive risk management process (including Board and senior management oversight) to identify, evaluate, monitor and control or mitigate all material risks and to assess their overall capital adequacy in relation to their risk profile. These processes should be commensurate with the size and complexity of the institution.\r'
>>> rangeA.InsertAfter('000000000000000000000')
>>> rangeA.Select()
>>> rangeB.Select()

上面的测试表明,我们能将区域A的后面插入B,但这个动作完成之后,A,B两个的定义范围都发生了改变。这个改变使得我们继续想插入A,B就要谨慎。而直接在InsertAfter()的括号里写字符串就可以直接为文档增加内容。这个命令也适用于Content,即也可以运行

>>> myDoc.Content.InsertAfter('000000000000000000000')
>>> myDoc.Content.InsertAfter(RangeA)

这就更加直接地为原来的文本增加内容了。

至此,我们已经理清根本的脉络。后续的设置操作反而变得简单起来。也许有点担心找不到VBA帮助文档。其实在 MsoAutoShapeType 枚举 (Office) | Microsoft Learn 的learn菜单栏搜索MsoAutoShapeType关于各种形状的枚举值,就出来了非常详细的值。例如矩形的值是1,菱形是4。这说明一般情况下我们已经获得了足够的帮助文档用于程序控制word了。

三 一般参数设置

想要清晰地对文件进行定位,例如按”字符“数从1标记到最后一个,到底服从怎样的规律,需要自己来测试观察分析。这种清晰化的工作是为了将来编制复杂的处理程序做准备的。例如我们想整理来自不同人员编写的乱七八糟的标题分级乱七八糟的空格乱七八糟的标点符号不规则地满天飞。测试,不断地互动,这是程序编制的基本出发点。

下面,要以自己想要解决的问题为中心来学习分析各种设置。我们采用新的参考文件,原书自带的《从TXT文件到Word .py》及其原始资料《公文格式自动设置.txt》。这样可以直接复制许多代码,节省输入代码字符的时间。

# -*- coding: utf-8 -*-
from win32com.client import constants,DispatchEx
wordApp = DispatchEx('Word.Application')
#前台运行
wordApp.Visible=1
#不弹出警告
wordApp.DisplayAlerts=0
#打开指定文件   
myDoc=wordApp.Documents.Open(r'D:\pythonST\GLXT\WORD自动化\Wordstudy\yswj\第6章\公文格式自动设置_结果new.docx')


运行上面的程序,自动打开”公文格式自动设置_结果new.docx“文件,进入Shell互动模式。

1 页面设置

1)页面的纵横

>>> myDoc.PageSetup.Orientation= 1
#页面变成横版
>>> myDoc.PageSetup.Orientation=0
#页面变成竖版

2)页面的大小

从PageSetup的PaperSize设置大小。在csdn的 (421条消息) PaperSize.RawKind 属性_weixin_30894583的博客-CSDN博客 可以得到各种大小设置的数字表示。但是直接使用命令修改值无效。

>>> myDoc.PageSetup.PaperSize
>>> myDoc.PageSetup.PaperSize=8
>>> myDoc.PageSetup.PaperSize
7

还是参考《从TXT文件到Word .py》解决这个问题。修改长宽值就能改变纵横,修改Orientation也能再次改变纵横。因为word几乎百分百就是A4,A3,没必要把这个问题弄得很复杂。

>>> CentimetersToPoints=28.35
>>> myDoc.PageSetup.PageWidth=CentimetersToPoints * 21
>>> myDoc.PageSetup.PageHeight=CentimetersToPoints * 29.7
>>> myDoc.PageSetup.PaperSize
>>> myDoc.PageSetup.PageWidth=CentimetersToPoints * 29.7
>>> myDoc.PageSetup.PageHeight=CentimetersToPoints * 42
>>> myDoc.PageSetup.PaperSize
>>> myDoc.PageSetup.PageWidth=CentimetersToPoints * 42
>>> myDoc.PageSetup.PageHeight=CentimetersToPoints * 29.7
>>> myDoc.PageSetup.Orientation
>>> myDoc.PageSetup.Orientation=0
>>> myDoc.PageSetup.Orientation=1

3)页面的特殊化独立处理

想要对某一页采取特殊的页面设置,可能要使用不同的分节试一下。至少上面的设置是针对全部页面的。这个问题此时还不能解决。但后面的分析将会解决这个问题。

因为没有系统学习过word,有些经验和认识是错误的。页眉和页脚,总是随着Section设置。但是分栏,根据现在的测试,其实是随着选择区域的内容变化的。选择某一段文字,标记上红色。让它处于选择状态,下面的程序运行

>>> sX = wordApp.Selection
>>> sX.Copy()

然后在文件末尾"ctr+V"就粘上了这段文字。这表明我们对文档任意部分的选区,已经通过上面的命令被控制。sX.Start和sX.End能够给出具体的位置(字符数位置)。这意味着我们对选择到的部分能够给出精确的字符数字的定位。

选中一段文字,直接操作分栏,在原来一般设置分为两栏的情况下,对这部分文字分为一栏,结果这部分被分为了一栏,在独立的一页是一栏,在后续一页,也是一栏。而此后分为两栏的对象甚至是和它在一页。

假如我们要增加文档的节,可以使用下面的命令。

>>> myDoc.PageSetup.TextColumns.SetCount(2)
>>> myDoc.Sections.Count
>>> myDoc.Sections.Add()
<COMObject Add>
>>> myDoc.Sections.Count
2

此时增加的是空的节,我们看到的增加了一页空白。这样就可以在新的节上进行新的页眉页脚设置。

现在我们重新打开文件《公文格式自动设置_结果new.docx》,运行

>>> myDoc.Sections.Count
>>> myDoc.Range().End
22509
>>> myDoc.Sections.Add()
<COMObject Add>
>>> myDoc.Range().End
22510
>>> myDoc.Sections.Count
>>> myDoc.Sections.Add()
>>> myDoc.Range().End
22511

也就是在原来竖向A4文件的基础上增加了2空白页,分别是第42页,它是一个新的节,第43页也是新的节。手动设置,光标在第42页,设置页面为横版,A3。可以发现,42页变成了A3横版,而其余都没有改变。这就证明我们的确可以随着节来设置页眉页脚的变化,以及随着节来设置页面大小和纵横版式。如果我们要把之前没有分节的内容重新分节,那也只要选择它们重新剪切进入新的节就可以了。这就解决了之前的困惑,即对于特殊的某一页,我们需要独立的纵横版式,页面大小,分栏设置。现在这些问题都解决了。注意局部分栏变化虽然可以使用选择到的区域内容手工改变分栏,但实际上还不知道程序命令怎样实现。倒是后来用Section的操作实现了程序指令按节分栏。下面我们用程序来实现”按节修改其页面大小的设置“

>>> CentimetersToPoints=28.35
>>> myDoc.Sections(2).PageSetup.PageWidth=CentimetersToPoints * 29.7
>>> myDoc.Sections(3).PageSetup.PageWidth=CentimetersToPoints * 29.7
>>> myDoc.Sections(2).PageSetup.PageWidth=CentimetersToPoints * 42
>>> myDoc.Sections(3).PageSetup.PageHeight=CentimetersToPoints * 42

再用程序控制按节实现分栏

>>> myDoc.Sections(2).PageSetup.TextColumns.SetCount(2)
>>> myDoc.Sections(3).PageSetup.TextColumns.SetCount(2)

还要解决一个指定某页删除的问题。上面我们已经用Section节实现了空白页的增加。但按照页来删除是人自然的感觉,可能软件设计者并不这么思考。所以还是通过选择区域来删除吧。因为增加减少内容它会自动增加页数减少页数。

不能直接删除某Section,但可以通过将某节的区域转成选择,然后删除。下面的测试表明,原来的第2节被删除了。但是原来的3节的编号不再存在,此时并非存在Section(1)和Section(3),而只有Section(1)和Section(2)。

>>> myDoc.Sections(2).Range.Select()
>>> Sc = wordApp.Selection
>>> Sc.Delete()
>>> myDoc.Sections.Count
>>> myDoc.Sections(3)
Traceback (most recent call last):
  File "<pyshell#58>", line 1, in <module>
    myDoc.Sections(3)
  File "C:\Users\Administrator\AppData\Local\Programs\Python\Python36\lib\site-packages\win32com\client\dynamic.py", line 226, in __call__
    self._oleobj_.Invoke(*allArgs), self._olerepr_.defaultDispatchName, None
pywintypes.com_error: (-2147352567, '发生意外。', (0, 'Microsoft Word', '集合所要求的成员不存在。', 'wdmain11.chm', 25421, -2146822347), None)
>>> myDoc.Sections(2)
<COMObject <unknown>>

不用去琢磨怎样实现程序将选择到的对象分栏了。现在已经实现了某一页的独立分栏及独立页面纵横和大小。

4)页眉和页脚

下面的代码给出页边距设置和所有节的页眉页脚的位置、文字内容

>>> CentimetersToPoints=28.35
>>> myDoc.PageSetup.TopMargin=CentimetersToPoints * 3.7
>>> myDoc.PageSetup.BottomMargin=CentimetersToPoints * 3.5
>>> myDoc.PageSetup.LeftMargin=CentimetersToPoints * 2.8
>>> myDoc.PageSetup.RightMargin=CentimetersToPoints * 2.6
>>> myDoc.PageSetup.HeaderDistance=30 #可以把值放大到200看看明显效果
>>> myDoc.PageSetup.FooterDistance=30 #可以把值放大到200看看明显效果
>>> myDoc.Sections(1).Headers(1).Range.Text="某某项目"
>>> myDoc.Sections(1).Footers(1).Range.Text="某某地区"
#字体和大小
>>> myDoc.Sections(1).Headers(1).Range.Text="某某项目"
>>> myDoc.Sections(1).Footers(1).Range.Text="某某地区"
>>> myDoc.Sections(1).Headers(1).Range.Font.NameFarEast="黑体"
>>> myDoc.Sections(1).Headers(1).Range.Font.Size=15.75
>>> myDoc.Sections(1).Footers(1).Range.Font.NameFarEast="仿宋"
>>> myDoc.Sections(1).Footers(1).Range.Font.Size=25.75

下面的代码给页眉添加图片和艺术字

>>> pic=myDoc.Sections(1).Headers(1).Shapes.AddPicture(r'D:\pythonST\GLXT\WORD自动化\Wordstudy\yswj\第6章\logo.png')
>>> pic.LockAspectRatio=True #应该是比例控制
>>> pic.Height=30
>>> pic.RelativeVerticalPosition=0
>>> pic.Left=10-myDoc.PageSetup.LeftMargin
>>> pic.Top=10-myDoc.PageSetup.TopMargin
>>> myDoc.Sections(1).Headers(1).Shapes.AddTextEffect(0,"内部资料,注意保密","宋体",3,False,False,0,0)

页脚的操作也是很麻烦的,好在搞定了以后就可以直接复制代码了。

pns=myDoc.Sections(1).Footers(1).PageNumbers
pns.NumberStyle=57                                    #到VBA帮助里查wdPageNumberStyle的枚举值
pns.HeadingLevelForChapter =0
pns.IncludeChapterNumber=False
pns.RestartNumberingAtSection=True
pns.StartingNumber=0                                  #配合FirstPage=False的取值
pn=pns.Add(PageNumberAlignment=1,FirstPage=False)     #数值0是左对齐,数值1是居中,数值2是右对齐

如果运行

myDoc.Sections(1).Headers(3).Range.Text="xxxxxxxxxxxxxxxxxxxxxxxx"
>>> myDoc.StoryRanges(6).Text
'xxxxxxxxxxxxxxxxxxxxxxxx\r'

的确偶数页页眉的文字内容按照上面的代码分析是这样的,但实际文档的奇数偶数页还是一样的,并没有发生改变。然而将Headers()括号里的数改成2或者3,都会让首页的页眉内容发生改变。改变之前,先模仿新节默认设置连接的取消。

myDoc.Sections(1).Headers(3).LinkToPrevious=False #好像是多余的命令
myDoc.Sections(1).Headers(2).Range.Text="首页会改变"

奇数页和偶数页要区分什么呢?首页的改变是有用的。自然的。

word的优势在文字表格的编排。这些页面和页眉页码等设置,复杂而不稳定明确。实际工作中最后的出图一般都使用pdf合成,不需要在word里搞得很复杂。反而应该是无页眉无页码无页脚等为好。所以大概了解这些就可以了。

2 段落与字体格式

1) 段落的定位

从前面的分析思考我们已经知道,段落的确立是非常重要的事情。一方面,我们是可以按照预定的段落数来自己写作的。另一方面,我们怎么确立一个不规范使用段落的人写的文章的段落呢?我们已经看到,一般人的文章,并不分节即Section,常常就是一节。然而节对于某页的特殊化设置是很关键的。实现这个目标,就要重新建立节。这个问题还好,不算复杂。现在麻烦的就是不清楚一篇不规范的文章,其段落程序是怎样划分的?

创建一篇不规范的文章,我们用它来进行测试。

测试word3.docx
15.1K
·
百度网盘

仅仅知道有多少个段落是远远不够的。我们需要查看段落的内容。

>>> for i in range(1,myDoc1.Paragraphs.Count+1):
	print('段落'+str(i)+":",myDoc1.Paragraphs(i).Range.Text)
段落1: 文章
段落2: 第一章  某某说明
段落4: 一  如何如何
段落5: 业务审查委员会办公室工作;负责同业金融市场业务相关的调查研究,以及制定相关的审查指引或审查标准;负责总行本级同业金融市场业务的放款、核保、档案管理,以及相关的条线管理;负责组织实施同业金融市场业务投后管理风险督查与相关评价工作,并负责总行投后管理委员会办公室工作;负责制定和修订市场风险管理相关制度,改进、完善市场风险管理程序和方法;负责牵头组织实施市场风险的识别、计量、监测与报告;负责对金融市场部交易与投资业务进行风险监控、评估与报告;组织条线业务指导与培训,加强团队建设。
段落6: 金融市场风险控制部是在全行统一风险管理体系的框架内负责同业金融市场业务相关的行业研究、业务审查与管理、核保和放款管理、投后管理督查、市场风险管理等的部门。主要职责有:负责参与全行同业金融市场业务的风险管理体系构建,协同改进和完善同业金融市场业务的风险管理制度、政策、授权、系统、模型等;负责组织实施职责内同业金融市场业务的审查、审议、审批,并负责总行投资与交易
段落8: 二 内容分析
段落9: 资产风险分类审议委员会主要负责审议超过分行认定权限的资产风险分类事项,为资产风险分类有权认定人提供智力支持,并对有权认定人的权力起一定制衡作用。
段落10: 
>>> for i in range(1,myDoc1.Paragraphs.Count+1):
	print('段落'+str(i)+":",myDoc1.Paragraphs(i).Range.Text)
段落1: 文章
段落2: 第一章  某某说明
段落4: 一  如何如何
段落5: 业务审查委员会办公室工作;负责同业金融市场业务相关的调查研究,以及制定相关的审查指引或审查标准;负责总行本级同业金融市场业务的放款、核保、档案管理,以及相关的条线管理;负责组织实施同业金融市场业务投后管理风险督查与相关评价工作,并负责总行投后管理委员会办公室工作;负责制定和修订市场风险管理相关制度,改进、完善市场风险管理程序和方法;负责牵头组织实施市场风险的识别、计量、监测与报告;负责对金融市场部交易与投资业务进行风险监控、评估与报告;组织条线业务指导与培训,加强团队建设。的方法。烦烦烦顶顶顶。
段落6: 金融市场风险控制部是在全行统一风险管理体系的框架内负责同业金融市场业务相关的行业研究、业务审查与管理、核保和放款管理、投后管理督查、市场风险管理等的部门。主要职责有:负责参与全行同业金融市场业务的风险管理体系构建,协同改进和完善同业金融市场业务的风险管理制度、政策、授权、系统、模型等;负责组织实施职责内同业金融市场业务的审查、审议、审批,并负责总行投资与交易
段落8: 二 内容分析
段落9: 资产风险分类审议委员会主要负责审议超过分行认定权限的资产风险分类事项,为资产风险分类有权认定人提供智力支持,并对有权认定人的权力起一定制衡作用。
段落10: 
>>> for i in range(1,myDoc1.Paragraphs.Count+1):
	print('段落'+str(i)+":",myDoc1.Paragraphs(i).Range.Text)
段落1: 文章
段落2: 第一章  某某说明
段落4: 一  如何如何
段落5: 业务审查委员会办公室工作;负责同业金融市场业务相关的调查研究,以及制定相关的审查指引或审查标准;负责总行本级同业金融市场业务的放款、核保、档案管理,以及相关的条线管理;负责组织实施同业金融市场业务投后管理风险督查与相关评价工作,并负责总行投后管理委员会办公室工作;负责制定和修订市场风险管理相关制度,改进、完善市场风险管理程序和方法;负责牵头组织实施市场风险的识别、计量、监测与报告;负责对金融市场部交易与投资业务进行风险监控、评估与报告;组织条线业务指导与培训,加强团队建设。的方法。烦烦烦顶顶顶。   Kankan 。    顶顶顶。    再测试再测测。
段落6: 金融市场风险控制部是在全行统一风险管理体系的框架内负责同业金融市场业务相关的行业研究、业务审查与管理、核保和放款管理、投后管理督查、市场风险管理等的部门。主要职责有:负责参与全行同业金融市场业务的风险管理体系构建,协同改进和完善同业金融市场业务的风险管理制度、政策、授权、系统、模型等;负责组织实施职责内同业金融市场业务的审查、审议、审批,并负责总行投资与交易
段落8: 二 内容分析
段落9: 资产风险分类审议委员会主要负责审议超过分行认定权限的资产风险分类事项,为资产风险分类有权认定人提供智力支持,并对有权认定人的权力起一定制衡作用。
段落10: 

从测试结果看,空行,不连接的句子都是段落。最后剩下的空白也算是一个段落。如果几个句子连在一起,则被认为是一段。连在一起虽然有空格,但没有跨行,仍然是一段。然而这样的结果仍然是不可靠的。百度一下才知道还真有个段落标志符的隐藏和显示的设置。打开word的选项就出来了。这会便于观察,我们能清楚地知道它是怎样给已知文件划分段落的。从作者自定义的不规范的各级标题所在的段落,我们能标定清楚它的位置。不管它怎样计算段落,这些 标题还是会位于不同的段落,拥有唯一的段落编号 。这使我们重构的依据。我们要重构这些段落,但不改变其段落结构和内容。

现在我们将看到一个有点绕的问题。可能是软件设置的问题,原书认为枚举值英文或者数值都是可以的。但我的电脑只能接受数值。如果选择第三段,作为一个Range,我们使用

myDoc1.Paragraphs(3).Range.InsertBefore("00000")

的确就在该段落区域的前面插入了字符"00000",但使用

myDoc1.Paragraphs(3).Range.InsertAfter("00000")

按照理解应该插入在该段最后,实际运行却跑到第四段 的前面去了。如果按照该书的操作,使用如下命令

>>> myDoc1.Paragraphs(3).Range.Select() #选择第三段这个区域,看看word文件,这段话已经加上蓝色覆盖标记
>>> sA = wordApp.Selection              #转为选择
>>> sA.Collapse(1)   #原书括号里用英文constants.wdCollapseStart,我猜想是1,因为一时查不到,但还真是对的
                       # 看看word文件,蓝色覆盖区已经消失,光标移动到了第三段的前面位置
>>>sA.TypeText("这dddd<") #这段文字写在了第三段的最前面,完全正确
>>> myDoc1.Paragraphs(3).Range.Select()#区域已经发生变化,命令重新运行
>>> sA = wordApp.Selection              #转为选择
>>> sA.Collapse(0) #光标的确移动到了第四段的前面去了
>>> sA.MoveLeft(1)#光标的确移动到了第三段的末尾

这些细节将决定我们能否根据段落的定位进行正确的文字插入。也可以先测算第三段区域的开始位置4和最后字符位置14,根据这个差值,在塌陷到最前面的位置后,使用sA.Move(1,9)实现。

应该高度重视word的选项设置。显示空格,显示段落标志符,这些对我们检查程序编写是否正确是很有用的,对于检查成果是否正确也有用。不一定要追求一步到位,可以结合人工观察。倒是觉得第一步应该将所有的内容都简化甚至隐藏,只显示标题,这样就可以迅速发现错误。即我们应该基于整体来控制,把大整体划归为简单的东西,在各个层级上分析。通过层层分析,逐渐得到清晰条理的结果。这样做,也是因为word的东西不可能什么都能学习掌握,我们只能在有限的使用方法中重构。如果出现什么不能处理的问题,宁愿重新写重新编排某个局部,而不是花很多时间钻研软件自身的方法。

从新建立一个按word各级标题方法编写的文件,它的段落情况与上述一致。通常,一个标题就是一段,段之间的空格也是段落。一般自然的理解,段落间的空格不是段落。但word已经定义为段落,这反而便于分析了。

2)段落的样式

A 初步测试

同样,我们需要认真做一个试验文件,一个段落的比较完整的文件,对段落进行分析。不一定能将软件所有的功能都研习个遍,我们只关心实际工作中最常见的需要。

此时,再来研读原书所带程序《从TXT文件到Word .py》和文件《公文格式自动设置.txt》就是正当其时了。其原来的程序运行结果文件,各级标题是加了粗的。但我电脑的结果没有加粗,可是Bold那里却是True。我们来分析测试这个程序。

# -*- coding: utf-8 -*-
Created on Tue Aug  4 20:55:52 2020
@author: Administrator
from win32com.client import constants,DispatchEx
wordApp = DispatchEx('Word.Application')
wordApp.Visible=1
wordApp.DisplayAlerts=0
with open(r'D:\pythonST\GLXT\WORD自动化\Wordstudy\yswj\第6章\公文格式自动设置.txt') as f:
    text=f.read()
myDoc=wordApp.Documents.Add()
myDoc.Range(0,0).InsertBefore(text)
CentimetersToPoints=28.35
myDoc.PageSetup.PageWidth=CentimetersToPoints * 21
myDoc.PageSetup.PageHeight=CentimetersToPoints * 29.7
myDoc.PageSetup.TopMargin=CentimetersToPoints * 3.7
myDoc.PageSetup.BottomMargin=CentimetersToPoints * 3.5
myDoc.PageSetup.LeftMargin=CentimetersToPoints * 2.8
myDoc.PageSetup.RightMargin=CentimetersToPoints * 2.6
myDoc.PageSetup.LinesPage=22
myDoc.Content.Font.NameFarEast="仿宋_GB2312"
myDoc.Content.Font.Size=15.75
Start=myDoc.Paragraphs(2).Range.Start
End=myDoc.Paragraphs(myDoc.Paragraphs.Count).Range.End
myRange=myDoc.Range(Start,End)
myRange.ParagraphFormat.CharacterUnitFirstLineIndent=2
myDoc.Paragraphs(1).Range.Font.NameFarEast="方正小标宋简体"
myDoc.Paragraphs(1).Range.Font.Size=21
myDoc.Paragraphs(1).Range.Font.Bold=True
myDoc.Paragraphs(1).Range.ParagraphFormat.Alignment=1
FindA=myDoc.Content.Find
FindA.ClearFormatting
FindA.MatchWildcards=True
FindA.Replacement.ClearFormatting
FindA.Replacement.Font.NameFarEast="黑体"
FindA.Replacement.Font.Bold=True
FindText="[一二三四五六七八九十]@、*^13"
FindA.Execute(FindText,ReplaceWith="",Format=True,Replace=2)
FindA.Replacement.Font.NameFarEast="楷体_GB2312"
FindA.Replacement.Font.Bold=True
FindText="([一二三四五六七八九十]@)*^13"
FindA.Execute(FindText,ReplaceWith="",Format=True,Replace=2)
FindA.Replacement.Font.NameFarEast="仿宋_GB2312"
FindA.Replacement.Font.Bold=True
FindA.Execute(FindText="[0-9]@、*^13",ReplaceWith="",Format=True,Replace=2)
FindA.Execute(FindText="([0-9]@)*^13",ReplaceWith="",Format=True,Replace=2)
FindText="[一二三四五六七八九十]@是"
FindA.Execute(FindText,ReplaceWith="",Format=True,Replace=2)
myDoc.Sections(1).Footers(1).PageNumbers.Add(PageNumberAlignment=4)
myDoc.Sections(1).Footers(1).Range.Select()
pageNum=wordApp.Selection
pageNum.MoveLeft(1,2)
pageNum.TypeText("—")
pageNum.MoveRight(1,1)
pageNum.TypeText("—")
pageNum.WholeStory()
pageNum.Font.Name="宋体"
pageNum.Font.Size=14
myDoc.Sections(1).Headers(1).Range.ParagraphFormat.Borders(-3).LineStyle=0
myDoc.SaveAs(r'D:\pythonST\GLXT\WORD自动化\Wordstudy\yswj\第6章\公文格式自动设置_结果1026.docx')
##myDoc.Close()
##wordApp.Quit()


a-1 从word里点击“查找”就会出现导航,导航显示,两个文件都是不规范的无word认可的标题组织的文件。

a-2 下面的程序得到段落的缩进

>>> Start=myDoc.Paragraphs(2).Range.Start
>>> End=myDoc.Paragraphs(myDoc.Paragraphs.Count).Range.End
>>> myRange=myDoc.Range(Start,End)
>>> myRange.ParagraphFormat.CharacterUnitFirstLineIndent=2 #搞得那么复杂,也不知道怎么查来的,不就是一个段落缩进吗

a-3 段落1的加粗

#段落1加粗及变字体及大小就是文章总标题
Start=myDoc.Paragraphs(2).Range.Start
End=myDoc.Paragraphs(myDoc.Paragraphs.Count).Range.End
myRange=myDoc.Range(Start,End)
myRange.ParagraphFormat.CharacterUnitFirstLineIndent=2
myDoc.Paragraphs(1).Range.Font.NameFarEast="方正小标宋简体"
myDoc.Paragraphs(1).Range.Font.Size=21
myDoc.Paragraphs(1).Range.Font.Bold=True
myDoc.Paragraphs(1).Range.ParagraphFormat.Alignment=1
#测试段落编号
>>> for i in range(1,myDoc.Paragraphs.Count+1):
	print('段落'+str(i)+":",myDoc.Paragraphs(i).Range.Text)
段落1: A银行风险管理与内部控制情况的报告
段落2: 一、风险管理情况
段落3: (一)概述
......

a-4 通过查找替换将原来的标题进行处理

FindA=myDoc.Content.Find
FindA.ClearFormatting
FindA.MatchWildcards=True    #使用通配符
FindA.Replacement.ClearFormatting
FindA.Replacement.Font.NameFarEast="黑体"
FindA.Replacement.Font.Bold=True
FindText="[一二三四五六七八九十]@、*^13"
FindA.Execute(FindText,ReplaceWith="",Format=True,Replace=2)    #一级标题的处理
FindA.Replacement.Font.NameFarEast="楷体_GB2312"
FindA.Replacement.Font.Bold=True
FindText="([一二三四五六七八九十]@)*^13"
FindA.Execute(FindText,ReplaceWith="",Format=True,Replace=2)    #二级标题的处理
FindA.Replacement.Font.NameFarEast="仿宋_GB2312"
FindA.Replacement.Font.Bold=True
FindA.Execute(FindText="[0-9]@、*^13",ReplaceWith="",Format=True,Replace=2)
FindA.Execute(FindText="([0-9]@)*^13",ReplaceWith="",Format=True,Replace=2)#重复了 #三级标题的处理
FindText="[一二三四五六七八九十]@是"
FindA.Execute(FindText,ReplaceWith="",Format=True,Replace=2)      #一般文字的处理

上面关于各级标题的处理在我电脑上根本没有成功。这还需要对查找进行细致研习一下。

后面关于页眉页脚页码的处理,倒是发现奇数偶数页的设置产生效果了。原来奇数页偶数页的划分主要是为了双面打印的需要。

B 几个疑难问题

B.1 现在我们集中精力解决几个疑点。177页的wdLineSpaceAtleast作为一个参数选择,还有哪几个并行的选择?需要变成数字吗?先测算文件的段落编号。确定段落7是一个多语句段落。


constants.wdStyleHeading3又不能运行,这显然就是标题枚举值。直接在前述VBA帮助网页搜素“wdStyleHeading3”得到了很长的一张枚举值表。为方便,最后一起打包附在本文后面。现在我们知道这个“标题3”枚举值是-4,而“标题1”对应-2,“标题9”对应-10。

最后发现constants.wdLineSpaceAtLeast应该是作者笔误造的长词。通过搜索LineSpacingRule并在整个文档搜索,才找到LineSpacingRule枚举值表,AtLeast对应的值是4。这个表包含了1倍1.5倍2倍等行距的设置。还有个PbLineSpacingRule枚举,表示指定段落的控制。LineSpacingRule应该是通用设置。

>>> prg_7=myDoc.Paragraphs(7)
>>> prg_7.Style = -4
>>> prg_7.LineSpacingRule = 4
>>> prg_7.LineSpacing = 25 #可以把25改成75看剧烈变化

可以看出,别的段落没有变化,段落7变成了标题3的标题,在导航面板也能看到。那么PbLineSpacingRule又有何意义呢?

对应word手工操作的效果,我们可以看出不同的值0,1,2,3,4,5会导致不同的变化。选择第7段(程序编的号)即“董事会是本行风险管理的最高决策机构,负责确立本行整体风险偏好及风险承受水平,审批本行风险管理的战略、政策和程序,督促高级管理层采取必要的风险应对措施,监控和评价风险管理的全面性和有效性。董事会下设风险与关联交易控制委员会及审计委员会。”,右键 \rightarrow 段落,可以看出5对应的是多倍行距,4对应的是固定值,3对应的是最小值,2对应的是2倍行距,1对应的是1.5倍行距,0对应的是单倍行距,5对应的是多倍行距。在连续的测试中,好像5没有正确反应。但先输入prg_7.LineSpacingRule = 4,LineSpacing = 25,再输入prg_7.LineSpacingRule = 5就变成了多倍,此时设置栏自动变成了2.08,一般情况下是由LineSpacing的值决定。与手工输入不同,程序不能将LineSpacing输入为4就是4倍,输入4的话显示的是0.33,我们还得找出函数关系。然而这几乎根本用不到,没必要深究了。

Edge(这个浏览器好像更容易找国外的资料,直接在其上翻译网页和查找了)了一下,基本搞清这些概念了。选择单倍,1.5倍,双倍行距的话,就是按系统自定的值来定。右边那个设置将关闭。选择多倍间距,则设置栏的数据单位不是磅,而是倍,3就是3倍,不是3磅。选择固定值,就可以在设置里自由设置若30磅。选择最小值,也可以在设置栏自由设置。把字体大小改一下,按最小值设置的段落间距将不会改变行之间的间距。但固定值会随着字体的变大行距越来越小。

这些困难都是因为信息的沟通交流产生的。我们不清楚软件公司对一些概念的定义到底如何,也有可能网页翻译的混乱,也有可能是不同的软件环境。谁去将这些乱七八糟的东西完全统一呢?但我们仍然可以尽量去搞清楚它们,使用不同的搜索策略,过难的时候要改变思路,甚至完全放弃,没有必要深究。解决这些问题,落实代码的运行,学习才能顺利实现目标。其实用下棋取乐沉溺其中是很愚蠢的。想要打败对手,就要下功夫研究各种变化规律。无非是多比别人做了准备,以多打少。然后你也坐下来钻研这个,他也钻研这个,就为了杠上挣个输赢。好像自己很厉害的。这实在没有什么真正的价值。把精力用到学习上来,这些问题,这些困难的解决同样给我们带来快乐。而这种快乐将使我们自己变得越来越强大。越学习就越能学习。马太效应是确确实实的。

有很多问题,需要我们自己去解决的问题,不是靠别人清清楚楚地给你标记得那么明白。因为他可能会出错,他的软件环境知识经验与你不同,等等。想要冲出困难的包围,从根本上讲,只有自己努力测试,分析,设定正确的目标,以自己的“学习研究运动”为中心才能从别人那里得到的模糊信息中提出有价值的线索。但是我们还是提倡把成果写得清楚明白,尽量用典型的实例将使用方法展示出来。这样做,就不需要花费一周两周来学这个word控制,恐怕有个两三天就够了。直接复制代码就可以了。

B.2 表6-9列举了大量的段落属性,怎样实现,怎样找到枚举值?

以Alignment为例,直接百度“Paragraphs.Alignment枚举”就找到了枚举值列表。分别对应0,1,2,3,4产生不同的结果。其实一般的这种值我们猜想也就是0,1,2,3,4等,取7不对它会提示你错误。

看首行缩进FirstLineIndent。直接输入prg_7.FirstLineIndent=4没有反应,程序运行。换成段落9,还是没反应。

prg_9.CharacterUnitLeftIndent=4,运行结果是该段的整个左侧整齐地移动了这么长变成了空白。

prg_9.CharacterUnitFirstLineIndent=12,该段首行最左边的字符剧烈往右移动12个字符长度。这个才是该段首行缩进的控制。那么FirstLineIndent又是何方神圣呢?查了一下,这两个的差别,好像就是单位的不同。这个FirstLineIndent是以磅为单位的,所以2,8之类的并非字符数,将会只有很小的效果。试试50?100?还是没反应。

看看PageBreakBefore,prg_9.PageBreakBefore = True,段前插入分页符。在word选项里把显示那部分的勾全点上,将在文件中看到段落标志符分页符。顺带还看到了手动空白页的增加方法,删除方法。之前还真没注意到。

总的来说,都能实现。除了那个FirstLineIndent莫名其妙。看到了KeepWithNext,与下段同页,等等、这些可能解决曾经遇到的烦恼,就是在一个A3横板的分栏,其最后一行两行因为行距设置或什么原因,空了很多,手工输入几个字,它又能接受。

C 字体设置

Font可以通过Range和Selection设置。它并非别的什么原因性,本来就是这样使用的,我们甚至能对任意一句话里的一个字进行字体的独立设置,因此当然就是通过Range和Selection来设置字体了。我们测试下划线和加粗及字体和大小。

>>> rA = myDoc.Paragraphs(9).Range
>>> rA.Font.Size = 50
>>> rA.Font.Size = 25
>>> rA.Font.Size = 12
>>> rA.Font.Name = "仿宋"
>>> rA.Font.Bold = True
>>> rA.Font.Bold = True
>>> rA.Font.UnderLine=0
>>> rA.Font.UnderLine=1
>>> rA.Font.UnderLine=2
>>> rA.Font.UnderLine=3
>>> rA.Font.UnderLine=4
>>> rA.Font.UnderLine=5 #非法设置
Traceback (most recent call last):
  File "<pyshell#89>", line 1, in <module>
    rA.Font.UnderLine=5
  File "C:\Users\Administrator\AppData\Local\Programs\Python\Python36\lib\site-packages\win32com\client\dynamic.py", line 708, in __setattr__
    "Property '%s.%s' can not be set." % (self._username_, attr)
AttributeError: Property '<unknown>.UnderLine' can not be set.
>>> rA.Font.UnderLine=6

没什么问题。唯独搜素Font.UnderLine枚举值,找到的是wdUnderLine枚举值,确实5等数字不能设。直接从0开始测试,发现下面的下划线作不同变化。其实这些枚举值无非就是0到几,在难以找到的情况下自己来测试即可。

D 样式

同样需要查阅wdStyleType枚举值,wdStyleTypeCharacter正文字符样式的值是2,wdStyleTypeParagraph段落样式的值是1,wdStyleTypeTable表格样式的值是3,wdStyleTypeList列表样式的值是4,另外两个5,6称为什么仅供内部使用。

>>> rA.Font.UnderLine=6
>>> Style_1 = myDoc.Styles.Add(Name="Bold",Type=2)
>>> Style_1 .Font.Bold=True
>>> Style_1 .Font.Size=10
>>> Style_2 = myDoc.Styles.Add(Name="Center",Type=1)
>>> Style_2.ParagraphFormat.Alignment=1
>>> rangeA=myDoc.Range(myDoc.Paragraphs(7).Range.Start,myDoc.Paragraphs(9).Range.End)
>>> rangeA.Style=Style_1
>>> rangeA.Style=Style_1
>>> rangeA.Style=Style_2
>>> rangeA.Style=Style_2
>>> prg_26=myDoc.Paragraphs(26)
>>> prg_29=myDoc.Paragraphs(29)
>>> rangeB=myDoc.Range(myDoc.Paragraphs(26).Range.Start,myDoc.Paragraphs(29).Range.End)
>>> rangeB.Style=Style_2
>>> rangeB.Style=Style_1

可以看到,样式中并未定义下划线,却包含了下划线作用了。但如下重新定义,就没有了下划线。这说明定义和应用样式时要谨慎这种关联效果。

>>> Style_1 = myDoc.Styles.Add(Name="Boldcc",Type=2)
>>> Style_1 .Font.Bold=True
>>> Style_1.Font.UnderLine=0
>>> Style_1 .Font.Size=10
>>> Style_2 = myDoc.Styles.Add(Name="Centerpp",Type=1)
>>> Style_2.ParagraphFormat.Alignment=1
>>> rangeB.Style=Style_2
>>> rangeB.Style=Style_1

上面的测试也表明Name="Bold"里面的名字仅仅是个名字。

3 查找

A 查找的三种类型和重要作用

表格和图片,是相对次要的内容,放到后面研习。查找,特别是段落中文字的查找,是个非常重要的功能。

首先要注意,查找不仅仅是字符的查找。查找包括 字符查找,格式查找,特殊格式字符查找 。特殊字符就是指分页符,分节符,分栏符,段落标记,制表位等这些标志符。如果要手工操作,在某一页作特殊页面处理,就是通过在“页面布局”菜单中找到“分隔符”点击三角按钮不动展开就看到了。通过添加分节符,添加空白页,并在空白页上继续添加分节符,就能产生一页设置完全不同的页面了。所以添加空白页也是很重要的。添加空白页要在“插入”菜单栏进行,那里还有分页操作。在“页面布局”中还有“分栏”操作。它们与添加分节符分栏符分页符的结果差不多,但是有细节的差别。

前面已经解决了节的添加和删除的程序方法。那么空白页呢?其实增加删除节就同时增加删除了空白页。因为word会自动根据文字多少增加需要的页和删除不需要的页,所以一般情况是不需要去加空白页的。要加的空白页往往就是有特殊需要的页,那么将它同时作为不同的节就是非常合理安全的了。

原书提示了^p表示段落标志符,^t表示制表符,没有给出所有特殊标识符的表。循着这个线索,在百度中找到了CSDN上的网友珍贵的文章, (432条消息) Word中形如^p等27种分节符的奇妙用法!_kandari的博客-CSDN博客 ,这些重要的资料我都将其用浏览器网页打印成pdf集成起来,附到本文的最后,便于直接查阅。

这个^m分页符是非常重要的。我们使用PageBreakBefore虽然能在段前插入分页符,但这个分页符能够成功分页,却不能查找到。幸好该书详细给出了范例,如何将放在一个文档的8首诗添加分页符并借着分页符将文件分成8个文件存起来。我们可以套用类似的方法,将所有按照文章的各级大标题设定的段落。加上分页符,然后将它们拆开成大量零散的文件,每个零散文件就是一个标题段。这样做的目的是为了简化每个文件。这个简化的意义是什么呢?就是帮助我们清理垃圾,发现错误,排除可能的难以处理的混乱和错误,将它的各种设置归零,然后重构整个系统。这个拆分用手工操作将会增加混乱和工作量,但使用计算机是不会的。

好像我们使用别的特定的文字内容或符号也可以作这种标记。但显然,分页更让人感到安全舒适自然。而且像字符数标记段落标记都会随着编辑的进行不断变化,它们只能在某一刻可以用来定位,却不能一直用来明确地定位。这些特殊字符的控制,能够使我们对文档的控制更加深入。包括空格,空白区域,表格,图片,它们的定位,内部的修正,外部的对齐,使用上特殊标识无疑就更加便利了。以禁止出现断行的要求为例,仅仅使用眼睛来观察寻找可能存在的断行真是太麻烦了。使用段落标识符查找一下就轻松了。文字间的空格的清除也是如此。通过这次python控制word的研习,手工操作word也搞清了很多问题。实际上,手动和程序还是要结合起来。为了解除一两百页大量内容的压力,拆分,查找,重构,都要用起来。程序,手工,都要用起来。通过这种大开大合的操作和彻底通透的分析编辑,就必然能高效得到高质量排版的成果了。

B 一般字符串查找替换

>>> FindA.ClearFormatting()
>>> FindA.Text = "银行"
>>> FindA.Replacement.ClearFormatting()
>>> FindA.Replacement.Text="金融"
>>> FindA.Execute(Replace=2,Forward=True,wrap=1)
>>>FindA.Execute(Replace=2,Forward=True,Wrap=1)
True

上述程序能够运行,但是没有效果。

>>> FindA.Execute(FindText = "银行",ReplaceWith="金融",Replace=2,Forward=True,Wrap=1)
True

换成另一种格式,能够运行,但还是没有效果。可能是因为word本身的原因了。

花了好几个小时解决这个问题。不可思议的是,我本来想套用作者的原程序《 长文档自动分拆1-4.py 》这几个程序,看看是不是Find命令能否运行,结果一运行这几个程序中的一个,所使用的word原文就被锁死。搞不清是word本身,还是病毒,还是作者的诡异文件,搞了半天,按照网友分享重新注销,还好并非是什么病毒,原因就是那几个程序就是个坑。作者不会故意这样搞的。但这个文件就是有这么怪。还好,查了知乎网友的文章 Python操作Word(Win32com) - 知乎 (zhihu.com) ,意识到把区域Range换成选择或许可行。试了一下,小有成功,实现了查找,但每次只能查到一个,继续执行同样的命令,总是只能往下查到一个。换其它的命令,还是这样。那既然如此,说明是软件开发者的程序问题,参数搞得那么复杂。其实有现在这个功能,自己想要查找到全部,这个代码不是很容易吗?因此,使用如下代码,实际上已经完成查找替换功能。只是特殊标识符就未必能行了。

>>> myDoc.Range().Select()
>>> sx=wordApp.Selection
>>> sx.Find.Execute('银行')
>>> sx.Find.Execute("银行")
>>> sy=wordApp.Selection
>>> sy.Text
>>> sy.Start
>>> sy.End
>>> sx.Find.Execute("银行")
>>> sy=wordApp.Selection
>>> sy.Text
>>> sy.Start
>>> sy.End
>>> myDoc.Range(103,105).Select()

因为查找到了以后,查看word文件,它被蓝色覆盖。这就是个选择区域,所以上面的代码sy=wordApp.Selection就是一个随时的选择变量。有了这个选择,就可以重新写内容,实现代替。因为测试表明,无法实现替代功能。但能有这个结果,自己编个代码,简单多了。软件开发的人搞了那么多参数,因为软件环境等都方面原因,到我这里就没法实行。

将全部文章按段按句分解,即使使用Python自带的字符串查找工具都能解决一般字符的查找替换功能。所以这个问题实际上已经解决。

C 格式查找

下面的测试,表明我们可以自己编写代码实现查找一般字符串的格式。

>>> myDoc.Paragraphs(4).Range.Text
'本行风险管理的目标为建立在风险偏好指导下的全面风险管理体系并实现风险回报优化,全面管理信用风险、市场风险、流动性风险、操作风险、国别风险、银行账户利率风险、声誉风险、战略风险、信息科技风险及合规风险等。\r'
>>> zf=myDoc.Paragraphs(4).Range.Text
>>> type(zf)
<class 'str'>
>>> styA = myDoc.Paragraphs(4).Range.Style
>>> styA.Font.Size
>>> styA.Font.Name
'Calibri'
>>> myDoc.Paragraphs(4).Range.Select()
>>> sc=wordApp.Selection
>>> sc.Font.Size
>>> sc.Style.Font.Size
>>> sc.Font.Name
'仿宋_GB2312'
>>> sc.Font.Bold
>>> sc.Font.UnderLine
>>> 

同样,不能直接针对整个文档进行,可以将文档拆成段落,句子,然后在列表中循环查找即得。这是因为格式无非就是那几个常用性质。

D 特殊字符串的查找替换

以下的测试表明任何特殊字符都可以查找到。不用记特殊字符,打开word的查找和替换窗口,找到高级格式,里面包含了全部的特殊字符,点击后自动显示在查找字符里。

>>> myDoc.Range().Select()
>>> sx=wordApp.Selection
>>> sx.Find.Execute("^p")
>>> sx.Find.Execute("^p")
>>> sx.Find.Execute("^p")
>>> 

查找特殊格式字符是没问题了。如何写入呢?在Find失效的情况下,只能使用间接办法了。要在某个位置写入段落标志符,只要插入一个空段落。

>>> myDoc.Range(5,6).Select()
>>> myDoc.Paragraphs.Add(myDoc.Range(5,5))#插入空段落就是一个段落标志符
<COMObject Add>
>>> myDoc.Sections.Add(myDoc.Range(15,15))#插入空段落就是一个段落标志符
<COMObject Add>
>>> myDoc.Sections.Add(myDoc.Range(15,15))#插入空节就是一个分节符
<COMObject Add>

分页可以使用Paragraphs(i).PageBreakBefore = True,分栏可以用前面所示的myDoc.Sections(3).PageSetup.TextColumns.SetCount(2)来设置。特殊字符包括格式字符和特用的字符标识,不仅仅指格式。涉及到整体的节,栏,页,段,实际上我们已经解决了。

4 表格

如下的程序在某个位置插入一个表格,并设置表格的全部线为单线。所用到的枚举值,后面再一起附上。

>>> Sa= myDoc.Paragraphs(4).Range.Start
>>> tapble=myDoc.Tables.Add(myDoc.Range(Sa,Sa),2,3)
>>> tapble.Borders(-1).LineStyle=1
>>> tapble.Borders(-3).LineStyle=1
>>> tapble.Borders(-2).LineStyle=1
>>> tapble.Borders(-4).LineStyle=1
>>> tapble.Borders(-5).LineStyle=1
>>> tapble.Borders(-6).LineStyle=1
>>> tapble.Borders(-7).LineStyle=1 #-7,-8画表格对角线,但这里把所有表格都画上了,不对
>>> tapble.AllowAutoFit=False
>>> tapble.AllowAutoFit=True#还不清楚功能
>>> tapble.Cell(1,1).Range.InsertAfter("单元格")表格位置(1,1)中写文字内容

得到了细线表格,并写入了文字。文字在表格内的状况还要设置。表格的大小怎么控制?表格怎么删除呢?

四 使用宏方法

1 缩进增量的简单测试

对我来说,实际工作中大量使用的正是表格和文字的混合。图片用得不多。但是关于表格的操作,在买到的这本书上,在网上,能查到的资料都非常有限了。乱测试也是没有用的。其实我需要控制表格的编辑,对齐它们,控制确切的位置。控制表格内外的文字。控制表格与文字内容的关系。这些正是造成整个排版的混乱的根源。所以,到这里,要么放弃在这个软件里继续探索换新思路,要么在这个软件的使用上开启新的自由高度。

进入 ParagraphFormat.IndentCharWidth 方法 (Word) | Microsoft Learn 这个VBA帮助网站,我决定围绕它再转转。如果找不到突破口,可能就得放弃,至少暂时没必要深入了。在它的首页左下角,有详细的包含Exel,Word的详细列表。我仔细看了关于Word的列表。看到了宏方法。刚好昨天找到的知乎网页也谈到了宏方法,那时我觉得没必要学太深也没怎么注意。

通过录制宏生成代码 | Microsoft Learn ,但现在,我可以试试了。没想到如此简单清晰。按照其示例,即 一个选区增加缩进量 ,如何用宏得到VBA代码。按照其操作,我们得到如下VBA代码

Sub 宏1()
' 宏1 宏
    Selection.ParagraphFormat.IndentCharWidth 1
End Sub

现在我们进一步就是要从VBA代码得到Python代码。前面的Section我们已经掌握,后面的IndentCharWidth虽然不是很清楚,但无非就是函数或者枚举值。所以这时候当然可以猜想和测试了。在Word文件里手工选择一段话,显示为蓝色。这段选择正是刚才做宏试验的时候光标所在位置的一段话。运行如下代码

>>> sXianzai=wordApp.Selection
>>> sXianzai.Start
>>> sXianzai.End
>>> sXianzai.ParagraphFormat.IndentCharWidth(1)

程序不仅能够运行而且结果与手工操作一致。

2 表格的对齐控制

1) 原理分析

在包含大量表格的文本内,表格的对齐是一个非常重大的问题。因此我们先来解决表格的对齐。由于表格内的文字有可能继续添加,并造成必然的延申,因此,一般来说,表格的稳定的相对固定的尺寸是在横向上的设置。这决定了横向维度和竖向维度具有完全的不同作用。所以对齐,其实主要就是针对横向上的关系。

习惯于坐标思维的人,总是希望给定一个坐标点来控制对象的位置。但实际上软件设计者没有这样去考虑,而是采用了相对固定的关系来控制这种对齐。通过使用宏的方法,结合在word里手工操作的测试观察,同时在Shell上的测试,这个问题基本完美地解决了。

首先,我们应该注意到,整个文本的基本布局是从页面边距建立的。也许我们会设想控制表格与这个边距的关系来定位。这个想法基本正确,但实施的方法可能与我们自己的想象不同。通过手工测试和网页搜索,参考网友们的经验分享,我们可以看出,任意制作一个表格例如2*3行列这样的表格,里面放些文字,我们就不难发现,软件设计者设置的按窗口调整表格和按文字内容调整表格这两个功能是非常关键的。选择我们设计的简单表格,右键,即可看到自动调整 \rightarrow 根据窗口调整表格的操作。由于这些字面意思未必能正确地帮助我们知道真实的意义,但反复测试还是明白了。其实这个翻译是不准确的,所谓的按窗口调整,正是按照页面边距设置之后的整体布局调整。手工测试就会发现,一个乱七八糟的表格,我们将首先通过这个自动调整获得与整体的根本协调对齐。通过按窗口自动调整之后,它两侧的边线就与文字的边缘对齐了。由于我经常采用A3横向双栏布局形式,因此直接在双分栏的A3页面上测试。这个命令能够让一个任意的表格两侧拉长与文字边距对齐。

OK,这是一个非常关键的结论。我们也理解了为什么还有按文字内容自动调整。那是因为文字内容可能比较少,表格里多余的空白是不应该占地方的,所以它会收缩到一个合理的大小。自动调整完后,它可能会居于一个我们仍然不清楚的位置,没有坐标控制。那这时,显然,我们考虑的是这个表格的左对齐,中对齐,右对齐整个文字区域。我们选择表格,右键, \rightarrow 表格属性 \rightarrow 对齐方式即可。

看到表格属性的窗口后,我们也不难理解需要控制一下左右的缩进。在美观或特殊情况下,我们需要在对齐的前提下缩进,控制这个量,它们仍然能对齐。

进一步,考虑表格总长度宽度的设置的统一。这个统一也将有益于整体的对齐。

显然也应该考虑单元格的长宽设置,但实际测试有点不稳定。有时候会出现参数无效有时候又会有效,这是因为参数太多,冲突了。

不管怎样,完成这种控制,整个表格的布局排版就再也不会乱了。除此以外,当然还有表格里面的设计。但这不是我们考虑的主要问题。一般来说,这些大量的表格都是已经制作好了的,局部的协调性一般是没有问题的,但还是会有问题,这个后面再分析。我们考虑另外一个重要的措施,就是将本来逻辑上一致的表格应该统一起来。这需要考虑样式的复制传递或者表格的重构,相当于我们把两张本质一样的表格,选一个做标准。另一个表格,将它的文字内容取出来,重构新的表格,样式就按标准来。这样也必定把表格关系给整理得更整齐了。不难发现,只要我们能确切地从表格读取数据,再将另一张表格复制过来填上内容,就实现了重构。不一定要去找软件本身的命令,我们自己编个代码反而简单省心。

这样看来,基本已经解决了所有的表格问题了。后续我们一步步落实即可。现在我们要做的就是使用宏方法获得VBA程序,从VBA程序获得python程序。我们注意两种模式,一种是等于模式,用等号连接起来的语句。还有一种是没有等式的直接书写的模式。而这些长串的变量,无非两种基本的关系,属性或者是属性的几种情况,它要转成枚举数值。

2)基本的对齐方法

如下的代码是通过测试获得的结论。

#(一)表格的选择和删除
>>> mDoc.Tables.Count#做个有代表性的2*3表格用来测试,表格里放些文字,为研究对齐,多复制几个
>>> myDoc.Tables(1).Select()
>>> sxb= wordApp.Selection
>>> sxb.Delete()
>>> #只是删除了内容
>>> myDoc.Tables(1).Delete()
>>> #上面的命令删除全部的表格和内容
#实际上还是通过myDoc.Tables(index)来确定哪个表格比较好。可以在word直接做个选取,然后使用sy=wordApp.Selection命令
获得这个选区,查看它的Start,End数据。反过来又由这个数据可以构造Range和选区,让它显示在word里。互相观察分析
#(二)表格的自动按窗口调整和按文字内容调整
#>>> myDoc.Tables(1).AllowAutoFit=True
#提示要自动调整表格必须确保这个参数是True状态
>>>  myDoc.Tables(1).AutoFitBehavior(2)#括号的数字是WdAUtoFitBehavior枚举值,2是指根据窗口宽度调整表格大小,其实就是
根据页面文字区域的宽度调整到与文字边界对齐。1就是按照表格里的文字内容压缩到合适大小的表格,但表格竖向不变,变的是横向宽度
0就是所谓的表格固定宽度,就是不会自动调整
>>> myDoc.Tables(1).AutoFitBehavior(1)
>>> #根据文字调整,就是根据文字内容的字符数压缩不必要的空隙
#(三)表格的居中,靠左,居右
>>> myDoc.Tables(1).Rows.Alignment=1
>>> myDoc.Tables(1).Rows.Alignment=0
>>> myDoc.Tables(1).Rows.Alignment=2
>>> #分别是中左右,左缩进一般基于居左设置,在双栏设置的A3页面当然分左右缩进和对齐了
#(四)表格的总宽度控制
>>> myDoc.Tables(1).PreferredWidthType=2 #数字2是PreferredWidthType枚举值,2表示按百分比控制长度试试100就知道了。1表示按
系统当时的设置,3表示磅数。具体其实没关系,选择表格,在Shell上输入myDoc.Tables(1).PreferredWidth它会告诉你在word看到的表格
宽度其长度数值是多少
>>> myDoc.Tables(1).PreferredWidth = 100#先看看100%是多长
>>> myDoc.Tables(1).PreferredWidth = 70 #选择的是70%
#(五)表格的缩进控制
>>> myDoc.Tables(1).Rows.LeftIndent=10
>>> myDoc.Tables(1).Rows.LeftIndent=30
>>> #以上控制表格的左缩进,因为我的测试表格在A3分栏的左侧。如果在右侧,当然就要考虑右缩进了
>>> #都是按百分比
#(六)表格单元格的宽度
以下的测试表明,单元格的宽度控制不稳定。有时候不能正确反映出真实表格单元格的宽度。
>>> myDoc.Tables(1).PreferredWidthType=1
>>> myDoc.Tables(1).Cell(1,1).PreferredWidth
144.9499969482422
>>> myDoc.Tables(1).Cell(1,1).PreferredWidth=145
>>> myDoc.Tables(1).Cell(1,2).PreferredWidth=145
>>> myDoc.Tables(1).Cell(1,3).PreferredWidth=145
>>> myDoc.Tables(1).Cell(1,3).PreferredWidth
145.0
>>> myDoc.Tables(1).Cell(1,1).PreferredWidth
145.0
>>> myDoc.Tables(1).Cell(1,2).PreferredWidth
145.0
>>> myDoc.Tables(1).Cell(1,3).PreferredWidth
145.0
>>> myDoc.Tables(1).Cell(1,3).PreferredWidth

3)样式的整合

考虑如何将两个构造一样的表格按照第一张为标准格式重构第二张表格。我们不去费力研习原软件关于表格样式的设计。因为编个简单代码能解决的问题效率更高,运行更可靠。

在 word文件里制作几个不同的2行3列的表,里面塞些不同的文字。这个研习的目的是将来建立一个函数或者标准处理程序,使结构内容基本一致的表格采用同样的表格设计,从而使得整个排版更加整齐有序。

第1步,在word中根据人的判断,手工操作标记需要按统一样式处理的表格。方法就是将这些表格文字一律刷成红色。根据表格文字确定需要处理的表格编号。


#测试表明,能根据手工标记颜色区分要操作的表格
>>> myDoc.Tables.Count
>>> myDoc.Tables(1).Range.Font.Color
-16777216
>>> myDoc.Tables(2).Range.Font.Color
>>> myDoc.Tables(3).Range.Font.Color
255

第2步,复制第某张表格,将该表格内的文字清除,得到标准表格。将它作为新表格。放入指定位置暂存。

#使用复制粘贴进行文字表格等操作的时候,请注意Shell上的代码不要复制,要一个个字符亲自键入
>>> myDoc.Tables.Count
>>> wordApp.Selection.Start#将光标放到文件最后一个段落标识符并往右拖动鼠标选择一个空格,有蓝色翻盖它
22697
>>> myDoc.Range().End
22698
>>> myDoc.Tables(1).Select()
>>> biaoS=wordApp.Selection
>>> biaoS.Copy()
>>> myDoc.Range(myDoc.Range().Start,myDoc.Range().Start).Paste()#可以复制表格到文件开头,但好像表格异常了
>>> myDoc.Range(myDoc.Range().End-1,myDoc.Range().End-1).Paste()#测试表明,如果不减1直接用End则运行错误
>>> myDoc.Tables.Count
>>> myDoc.Tables(4).Select()#运行这个命令,到word文件里查看蓝色选中的区域是什么

第3步,读取要操作表格的文字数据,写入新表格。

#读取表格2的全部数据
>>> myDoc.Tables(2).Cell(1,1).Range.Text
'二日\r\x07'
>>> myDoc.Tables(2).Cell(1,2).Range.Text
'及解决方法\r\x07'
>>> myDoc.Tables(2).Cell(1,3).Range.Text
'发嘎嘎嘎\r\x07'
>>> myDoc.Tables(2).Cell(2,1).Range.Text
'嘀嘀咕咕\r\x07'
>>> myDoc.Tables(2).Cell(2,2).Range.Text
'发个广告\r\x07'
>>> myDoc.Tables(2).Cell(2,3).Range.Text
'缓解肌肤\r\x07'

第4步,读取标准表格单元格的文字样式biaoStyle_1,将它应用于新表格。

测试表明,表格里的文字布局,并不是单纯的文字的对齐属性,也不是单纯的表格的对齐属性。因此上面的说法是不太正确的。选择word表格4的2行3列,右键,一共有9种选择。交替取9种状态之一并查看文字和表格的属性值,测试程序如下

>>> myDoc.Tables(4).Cell(2,3).Range.Select()
>>> sb=wordApp.Selection
>>> sb.ParagraphFormat.Alignment #这是文字属性,这时候文字靠左
>>> sb.ParagraphFormat.Alignment #这是文字属性,这时候文字居中
>>> sb.ParagraphFormat.Alignment  #这是文字属性,这时候文字靠右
>>> sb.ParagraphFormat.Alignment=3
#改变值就会改变文字的靠左或居中或靠右对齐
>>> myDoc.Tables(4).Cell(2,3).VerticalAlignment   #这是表格属性,这时候文字在表格中整体靠上
>>> myDoc.Tables(4).Cell(2,3).VerticalAlignment
>>> myDoc.Tables(4).Cell(2,3).VerticalAlignment
>>> myDoc.Tables(4).Cell(2,3).VerticalAlignment   #这是表格属性,这时候文字在表格中整体居中
>>> myDoc.Tables(4).Cell(2,3).VerticalAlignment
>>> myDoc.Tables(4).Cell(2,3).VerticalAlignment
>>> myDoc.Tables(4).Cell(2,3).VerticalAlignment   #这是表格属性,这时候文字在表格中整体靠下
>>> myDoc.Tables(4).Cell(2,3).VerticalAlignment
>>> myDoc.Tables(4).Cell(2,3).VerticalAlignment=0 
#改变值就会改变Word里该表格的文字对齐,改为0时文字往上靠近表格分布
>>> myDoc.Tables(4).Cell(2,3).VerticalAlignment=2
Traceback (most recent call last):
  File "<pyshell#89>", line 1, in <module>
    myDoc.Tables(4).Cell(2,3).VerticalAlignment=2
  File "C:\Users\Administrator\AppData\Local\Programs\Python\Python36\lib\site-packages\win32com\client\dynamic.py", line 686, in __setattr__
    self._oleobj_.Invoke(entry.dispid, 0, invoke_type, 0, value)
pywintypes.com_error: (-2147352567, '发生意外。', (0, None, None, None, 0, -2147024809), None)
#取值为2就发生错误

查到WdCellVerticalAlignment枚举值果然是0,1,3没有2。还是比较奇葩的。至于ParagraphFormat.Alignment枚举,现在也搜不出来。但是测出来的就是1,2,3,而且其作用已经很清楚。这也算是很实用的方法,通过实际观测,找到对应的数值。避开极其复杂的枚举值搜索。

将这两个属性值记录下来,即可运用于新表格。

第5步,删除要操作的表格。复制新表格,粘贴于要操作表格的位置。

#删除表格4的内容
>>> myDoc.Tables(4).Range.Select()
>>> tx=wordApp.Selection
>>> tx.Delete()
#将表格2的内容复制到相应位置
>>> myDoc.Tables(4).Cell(1,1).Range.Text=myDoc.Tables(2).Cell(1,1).Range.Text
>>> myDoc.Tables(4).Cell(1,2).Range.Text=myDoc.Tables(2).Cell(1,2).Range.Text
>>> myDoc.Tables(4).Cell(1,3).Range.Text=myDoc.Tables(2).Cell(1,3).Range.Text
>>> myDoc.Tables(4).Cell(2,1).Range.Text=myDoc.Tables(2).Cell(2,1).Range.Text
>>> myDoc.Tables(4).Cell(2,2).Range.Text=myDoc.Tables(2).Cell(2,2).Range.Text
>>> myDoc.Tables(4).Cell(2,3).Range.Text=myDoc.Tables(2).Cell(2,3).Range.Text

第6步,同样宽度的表格,其对齐方式和缩进相同当然就对齐了。但为了对齐各种宽度的表格,应该能够获得任意表格的总宽度,并且这个宽度是按同一度量方式来标记。

>>> myDoc.Tables(1).PreferredWidthType=2 #按百分比计量
>>> myDoc.Tables(1).PreferredWidth
73.04000091552734
>>> myDoc.Tables(1).PreferredWidth=100 #修改数据会变动表格
#100就是与整个文字区域宽度一致
>>> myDoc.Tables(1).PreferredWidth=73
>>> myDoc.Tables(4).PreferredWidthType=2
>>> myDoc.Tables(4).PreferredWidth
47.400001525878906
>>> myDoc.Tables(4).PreferredWidth=myDoc.Tables(1).PreferredWidth

总宽度的确控制一致了。要防止单元格长度不一致,还是应能查找到单元格的数据才放心。

>>> myDoc.Tables(1).PreferredWidthType=2
>>> myDoc.Tables(1).PreferredWidth
73.04000091552734
>>> myDoc.Tables(1).PreferredWidth=100
>>> myDoc.Tables(1).PreferredWidth=73
>>> myDoc.Tables(2).PreferredWidthType=2
>>> myDoc.Tables(4).PreferredWidthType=2
>>> myDoc.Tables(4).PreferredWidth
47.400001525878906
>>> myDoc.Tables(4).PreferredWidth=myDoc.Tables(1).PreferredWidth
>>> myDoc.Tables(4).Cell(2,3).PreferredWidth
>>> myDoc.Tables(4).Cell(2,3).PreferredWidthType=2
>>> myDoc.Tables(4).Cell(2,3).PreferredWidth
31.1200008392334
>>> myDoc.Tables(4).Cell(2,2).PreferredWidth
37.7599983215332
>>> myDoc.Tables(4).Cell(2,1).PreferredWidth
31.1200008392334
>>> 31.1200008392334+37.7599983215332+31.1200008392334
100.0
>>> myDoc.Tables(4).PreferredWidthType=2
>>> myDoc.Tables(4).PreferredWidth
9999999.0
>>> myDoc.Tables(1).PreferredWidth
>>> myDoc.Tables(4).PreferredWidth
9999999.0
>>> myDoc.Tables(4).PreferredWidthType=2
>>> myDoc.Tables(4).PreferredWidth
9999999.0
>>> #由于单位表格宽度的测算,总宽度测算发生了混乱
>>> myDoc.Tables(1).PreferredWidth
>>> myDoc.Tables(4).Cell(2,3).PreferredWidthType=2
>>> myDoc.Tables(4).Cell(2,3).PreferredWidth
31.1200008392334
>>> myDoc.Tables(4).Cell(2,2).PreferredWidthType=2
>>> myDoc.Tables(4).Cell(2,2).PreferredWidth
37.7599983215332
>>> myDoc.Tables(4).Cell(2,1).PreferredWidthType=2
>>> myDoc.Tables(4).Cell(2,1).PreferredWidth
31.1200008392334
>>> myDoc.Tables(4).PreferredWidthType=2
>>> myDoc.Tables(4).PreferredWidth
9999999.0
>>> myDoc.Tables(4).PreferredWidth=myDoc.Tables(1).PreferredWidth
>>> myDoc.Tables(4).PreferredWidth
9999999.0
>>> myDoc.Tables(4).PreferredWidthType=1
>>> myDoc.Tables(4).PreferredWidth
9999999.0
>>> myDoc.Tables(4).PreferredWidthType=0
Traceback (most recent call last):
  File "<pyshell#151>", line 1, in <module>
    myDoc.Tables(4).PreferredWidthType=0
  File "C:\Users\Administrator\AppData\Local\Programs\Python\Python36\lib\site-packages\win32com\client\dynamic.py", line 686, in __setattr__
    self._oleobj_.Invoke(entry.dispid, 0, invoke_type, 0, value)
pywintypes.com_error: (-2147352567, '发生意外。', (0, 'Microsoft Word', '数值超出范围', 'wdmain11.chm', 37376, -2146823680), None)
>>> myDoc.Tables(4).PreferredWidthType=3
>>> myDoc.Tables(4).PreferredWidth
9999999.0
>>> myDoc.Tables(4).PreferredWidthType=3
>>> myDoc.Tables(1).PreferredWidthType=3
>>> myDoc.Tables(1).PreferredWidth
360.20001220703125
>>> myDoc.Tables(1).PreferredWidth=400
>>> myDoc.Tables(1).PreferredWidth