一种基于Python的Excel复杂处理方法:在Python中调用VBA
Zack_ZHU_
2024年03月25日 11:49

某些情况下,我们希望利用Python在Excel中批量且自动地调整格式。这种时候,比起查阅复杂的win32api,直接在Python中向Excel的VBA宏发起调用显然是一个更加简单且直观的方法。

下文会简单介绍一下我常用的一种方法,我对于它的使用多半基于经验,而非严谨的理论,因此它可能并不是最好的一种方法,甚至都算不上一种好方法,并且在我没有试验过的硬件环境下存在出错的可能。请务必以批判的眼光阅读,如有问题可随时与我探讨。


环境

  • Windows (因为需要用到win32api)

  • Python3 (我用的是3.12,但是版本应当并不重要,只要能够安装下面的依赖)

  • Microsoft Office (在Python中调用VBA需要创建一个Excel进程,因此你的机器上一定要安装Office)

准备工作

安装pywin32库

pip install pypiwin32

确保在Excel中启用并信任宏

打开Excel,在文件-选项-信任中心-信任中心设置-宏设置中,勾选信任对 VBA 工程对象模型的访问

录制并编辑你的宏模板

如果在此之前你有VBA和宏的使用经验,那么这一部分对你来说应当小菜一碟。

首先要明白,录制宏是为了帮助我们简化宏和VBA的编写,让你省去大量查询VBA对象文档的时间,它不一定是万能的,但相当有效。

录制宏的时机也很有讲究,比如,

我需要利用程序向Sheet中填入n行,而对于这n行中的任意行N,我都希望把第N行A列的单元格格式设置为加粗居中,同时把第1行从B到E列的所有单元格的格式全部刷到第N行从B到E列的单元格上。

那么我们需要做的便是,

在Excel顶上的视图-宏的下拉框中选择录制宏,宏名随意,快捷键不设,然后点击确定开始录制。回到Sheet,选中A2单元格(或者是A3、A4、A2000,这个不重要),在开始选项卡中依次点击加粗居中;然后选中B1到E1的单元格,在开始选项卡中点击格式刷,然后点击B2单元格将格式刷上去。最后回到视图-宏,在下拉框中停止录制

发现了吗?在录制宏的整个流程中,我们所扮演的是“指导者”的角色,我们并不需要告诉机器每一步应当具体怎么做(实际上我们也做不到),我们只需要给机器演示其中一步,之后再通过编程的手段让机器“举一反三”即可。

好,到此为止,我们已经录制了我们的第一个宏。让我们在视图-宏的下拉框中找到查看宏,选中我们的宏名,点击编辑。在这里,我们可以看到刚刚录制的宏的VB代码:

Sub 宏1() ' ' 宏1 宏 ' ' Range("A2").Select Selection.Font.Bold = True With Selection .HorizontalAlignment = xlCenter .VerticalAlignment = xlCenter .WrapText = False .Orientation = 0 .AddIndent = False .IndentLevel = 0 .ShrinkToFit = False .ReadingOrder = xlContext .MergeCells = False End With Range("B1:E1").Select Selection.Copy Range("B2").Select Selection.PasteSpecial Paste:=xlPasteFormats, Operation:=xlNone, _ SkipBlanks:=False, Transpose:=False Application.CutCopyMode = False End Sub

对于这段代码,我们大可不必弄懂它每一步说的是啥,干了什么。我们唯一需要做的,就是把那些可变的量标记出来。

还记得我们刚刚做了什么吗?我们希望为任意的AN单元格设置加粗和居中,但在录制的时候我们仅仅为A2一个单元格设置了加粗和居中。因此,我们要把A2替换成变量AN,并在之后的编程中进一步替换。

Range("AN").Select Selection.Font.Bold = True With Selection .HorizontalAlignment = xlCenter .VerticalAlignment = xlCenter .WrapText = False .Orientation = 0 .AddIndent = False .IndentLevel = 0 .ShrinkToFit = False .ReadingOrder = xlContext .MergeCells = False End With

以此类推,我们希望把B1:E1的单元格刷到任意BN:EN单元格中,那么我们应当把代码中的B2:E2标记为变量……吗?你可能已经发现了,代码中并没有出现B2:E2的字眼,只有B2出现过一次。不过不用担心,这是格式刷的特性,在这里我们也作特殊处理,只要把B2替换为变量BN即可。

Range("B1:E1").Select Selection.Copy Range("BN").Select Selection.PasteSpecial Paste:=xlPasteFormats, Operation:=xlNone, _ SkipBlanks:=False, Transpose:=False Application.CutCopyMode = False

最后,让我们把宏的名字从宏1改成我的宏,然后删除多余的注释行,一个VBA模板的雏形就此诞生!

Sub 我的宏() Range("AN").Select Selection.Font.Bold = True With Selection .HorizontalAlignment = xlCenter .VerticalAlignment = xlCenter .WrapText = False .Orientation = 0 .AddIndent = False .IndentLevel = 0 .ShrinkToFit = False .ReadingOrder = xlContext .MergeCells = False End With Range("B1:E1").Select Selection.Copy Range("BN").Select Selection.PasteSpecial Paste:=xlPasteFormats, Operation:=xlNone, _ SkipBlanks:=False, Transpose:=False Application.CutCopyMode = False End Sub

Python代码部分

首先引入pywin32:

from win32com.client import DispatchEx

还记得我们在刚刚录制宏的最后产生了一个VBA代码模板吗?将它复制过来,同时利用Python的字符串Formatter功能实现变量替换。

让我们创建一个根据模板生成VBA代码的函数:

def gen_script(script_id: int, row_index: int) -> str: return f''' Sub 我的宏{str(script_id)}() Range("A{str(row_index)}").Select Selection.Font.Bold = True With Selection .HorizontalAlignment = xlCenter .VerticalAlignment = xlCenter .WrapText = False .Orientation = 0 .AddIndent = False .IndentLevel = 0 .ShrinkToFit = False .ReadingOrder = xlContext .MergeCells = False End With Range("B1:E1").Select Selection.Copy Range("B{str(row_index)}").Select Selection.PasteSpecial Paste:=xlPasteFormats, Operation:=xlNone, _ SkipBlanks:=False, Transpose:=False Application.CutCopyMode = False End Sub '''

我们这个函数读取了一个行索引row_index作为参数,并用其替换我们刚刚的变量ANBN,最后以字符串的形式返回生成的VBA代码。

我想你可能注意到了,我们还有一个形参script_id,这个参数改变了我们的宏名,如果script_id取1,那么生成的宏名则为我的宏1;如果取2000,那么宏名则为我的宏2000……

理由很简单,这么做是为了保证在一个Excel文件中执行多个宏时不会出现冲突。就像同一命名空间下Python的函数名不能重复(类里面对于成员方法的重写除外)一样,同一文件下的VBA宏名也不能重复。

好了,最复杂的部分搞定了,让我们“八股文”式地编写Python代码来调用这个宏吧!

首先创建Excel进程,将其设置为“无头”模式:

client = DispatchEx('Excel.Application') client.Visible = False client.DisplayAlerts = False

初始化script_id,我们准备让它从1开始迭代,每次的增量为1:

script_id = 1

然后打开工作表、打开Sheet、打开……

workbook = client.Workbooks.Open(os.path.abspath('MyWorkbook.xlsx'), ReadOnly=False) worksheet = workbook.Worksheets('Sheet1')

注意:在Open时请务必使用绝对路径,使用相对路径一定会报错!

接下来我用一个简单的for循环,对Sheet的3到23行生成并执行宏:

for row_index in range(3, 24): script_module = workbook.VBProject.VBComponents.Add(1) script_module.CodeModule.AddFromString(gen_script(script_id, row_index)) client.Application.Run(f'我的宏{script_id}') script_id += 1

最后,让我们保存并退出: workbook.SaveAs(os.path.abspath('MyWorkbookNEW.xlsx')) workbook.Close() client.Quit()

同样,SaveAs也要使用绝对路径


内容到此告一段落。

我在GitHub上有一个应用实例,包含了本文所述的基于Python的VBA调用方法,代码可随时查看,不过使用的数据,由于涉及隐私不便公开。

https://github.com/kressety/ZehuaMethod : 一个基于 pandas 和 pywin32 的 Excel 处理实例

如遇到了什么问题,或有什么内容想与我进一步探讨,请在评论区留言或随时与我私信,我会第一时间回复。