Excel Python:飞速搞定数据分析与处理

第四部分 使用 xlwings 对 Excel 应用程序进行编程
第九章 Excel 自动化
作为第四部分的第一章,本章不再通过读写包操作 Excel 文件,而是开始利用 xlwings 自动化 Excel 应用程序。
xlwings 的主要用途是构建以 Excel 工作表为用户界面的交互式应用程序,你可以通过点击按钮调用 Python 代码或用户定义函数,而这类功能是读写包无法提供的。不过这并不是说 xlwings 无法用来读写文件------只要你在 macOS 或者 Windows 中安装了 Excel 就行。 xlwings 的一个优势是它能够真正地编辑各种格式的 Excel 文件,且不会修改或者丢失任何现有的内容或者格式;另一个优势是你可以从 Excel 工作簿中读取单元格的值而无须先保存这个工作簿。
在本章的开头,会向你介绍 Excel 对象模型和 xlwings:首先学习一些基础知识,比如连接工作簿或者读写单元格的值,然后深入学习如何利用转换器和各种选项来处理 pandas DataFrame 和 NumPy 数组。你也会看到如何与图表、图片和自定义名称进行互动。最后, 会解释 xlwings的工作原理,有了这些知识之后,你就知道如何才能让脚本更加好用以及如何处理一些缺少的功能。
9.1 开始使用 xlwings
xlwings 的目标之一是成为 VBA 的替代品,它让你能够在 Windows 或 macOS 中通过 Python 与 Excel 进行互动。由于 Excel 的网格是显示 Python 数据结构(比如嵌套列表、 NumPy 数组和 pandas DataFrame)的绝佳布局,因此 xlwings 的核心功能就是让读写 Excel 文件尽可能简单。本节首先会介绍如何将 Excel 用作数据查看器------当你在 Jupyter 笔记本中与 DataFrame 进行互动时,这是非常实用的功能。然后会介绍 Excel 对象模型,之后我们会利用 xlwings 边做边学。本节在最后会向你展示如何调用那些可能仍然存在于旧式工作簿中的 VBA 代码。由于 xlwings 是 Anaconda 的一部分,因此无须在这里手动安装。
9.1.1 将 Excel 用作数据查看器
在默认情况下,Jupyter 笔记本会将大型 DataFrame 的大部分数据隐藏,只显示前几行(列)和最后几行(列)。要获得对数据的直观感受,一种方法是绘制图像,因为图像可以让你发现异常的数据。然而在某些时候,真正有用的是直接浏览数据表。在读完第 7 章之后,你已经知道如何在 DataFrame 中使用 to_excel 方法。 虽然本章也可以这样做,但还是有点儿麻烦:需要给 Excel 文件取一个名字,在文件系统中找到它,打开它,在对DataFrame 进行修改后,需要关闭 Excel 文件然后再重新执行这个过程。一种更好的方法是执行 df.to_clipboard(),其可以将 DataFrame df 复制到剪贴板,这样你就可以把它粘贴到 Excel。不过还有一种更简单的方法,即使用 xlwings 中的 view 函数:
In [1]: # 首先,导入本章会用到的包
import datetime as dt
import xlwings as xw
import pandas as pd
import numpy as np
In [2]: # 创建一个基于伪随机数的DataFrame,
# 它有足够多的行使得只有首尾几行会被显示
df = pd.DataFrame(data=np.random.randn(100, 5),
columns=[f"Trial {i}" for i in range(1, 6)])
df
Out[2]: Trial 1 Trial 2 Trial 3 Trial 4 Trial 5
0 -1.313877 1.164258 -1.306419 -0.529533 -0.524978
1 -0.854415 0.022859 -0.246443 -0.229146 -0.005493
2 -0.327510 -0.492201 -1.353566 -1.229236 0.024385
3 -0.728083 -0.080525 0.628288 -0.382586 -0.590157
4 -1.227684 0.498541 -0.266466 0.297261 -1.297985
.. ... ... ... ... ...
95 -0.903446 1.103650 0.033915 0.336871 0.345999
96 -1.354898 -1.290954 -0.738396 -1.102659 0.115076
97 -0.070092 -0.416991 -0.203445 -0.686915 -1.163205
98 -1.201963 0.471854 -0.458501 -0.357171 1.954585
99 1.863610 0.214047 -1.426806 0.751906 -2.338352
[100 rows x 5 columns]
In [3]: # 在Excel中查看DataFrame
xw.view(df)

view 函数可以接受所有常见的 Python 对象,包括数字、字符串、列表、字典、元组、 NumPy 数组和 pandas DataFrame。在默认情况下,它会打开一个新的工作簿,然后将对象粘贴到第一张工作表的 A1 单元格。它甚至会通过 Excel 的自动适应功能来调整列宽。不必每次都打开一个新的工作簿,你也可以通过为 view 函数提供一个xlwings sheet 对象作为第二个参数来重复利用同一个工作簿文件:xw.view(df, mysheet)。

9.1.2 Excel 对象模型
当利用程序控制 Excel 时,你会和它的各种组件(比如工作簿或工作表)进行交互。这些组件以 Excel 对象模型的形式进行组织。Excel 对象模型是一种表示 Excel 图形用户界面 (参见图 9-1)的层次结构。微软在其官方支持的所有编程语言中大量使用了这种对象模型。无论是 VBA、Office Scripts(Web 中用于 Excel 的 JavaScript 接口)和 C#,它们使用的都是同样的对象模型。与第 8 章中的读写包相比,xlwings 相当严格地遵循了 Excel 的对象模型,但也有一点不同,比如,xlwings 使用 app 而不使用 application,使用 book 而不使用 workbook。

- app 包含 book 的集合。
- book 包含 sheet 的集合。
- 通过 sheet 可以访问 range 对象和 charts 等集合。
- range 包含一个或多个连续的单元格作为其元素。
虚线框表示集合 ,其中包含一个或多个相同类型的对象。app 对应着一个 Excel 实例,即作为独立进程运行的 Excel 应用程序。高级用户有时候会并行使用多个 Excel 实例并打开同一个工作簿两次,比如要并行计算具有不同输入的工作簿的值时就需要这样做。一个 Excel 实例就是一个沙箱化的环境,也就是说一个实例无法同另一个实例进行通信。sheet 对象让你可以访问各种集合,比如图表、图片和自定义名称,9.2 节会对这些主题进行深入研究。
为了体会 Excel 对象模型,最好的方法依然是以交互方式实际操作一下。先从 Book 类开始:它让你可以新建工作簿并连接至现有的工作簿,参见表 9-1 的总结。

下面来看看如何从 book 对象开始沿着 Excel 对象模型的层次结构一路向下到 range 对象:
In [4]: # 创建一个新的空工作簿并打印其名称
# 我们会用这个工作簿运行本章中的所有代码示例
book = xw.Book()
book.name
Out[4]: 'Book2'
In [5]: # 访问工作表集合
book.sheets
Out[5]: Sheets([<Sheet [Book2]Sheet1>])
In [6]: # 通过索引或名称获取工作表对象
# 如果你的工作表不叫"Sheet1",则需要做出一些调整
sheet1 = book.sheets[0]
sheet1 = book.sheets["Sheet1"]
In [7]: sheet1.range("A1")
Out[7]: <Range [Book2]Sheet1!$A$1>

有了 range 对象之后,就到达了 Excel 对象模型的最底层(即锁定到单元格)。在尖括号之间打印的字符串向你提供了该对象的一些有用的信息,但要真正利用它们完成一些工作,通常需要访问它们的属性,就像下面的示例这样:
In [8]: # 最常见的任务:写入值......
sheet1.range("A1").value = [[1, 2],
[3, 4]]
sheet1.range("A4").value = "Hello!"
In [9]: # ......和读取值
sheet1.range("A1:B2").value
Out[9]: [[1.0, 2.0], [3.0, 4.0]]
In [10]: sheet1.range("A4").value
Out[10]: 'Hello!'

如你所见,在默认情况下,xlwings 的 range 对象在接受单个单元格作为参数时,其 value 属性返回的是一个标量;在接受二维区域作为参数时,返回的是一个嵌套列表。到目前为止,我们用到的所有功能的代码几乎和 VBA代码一模一样:假设 book 是一个 VBA 或 xlwings 的工作簿对象,下面分别是 VBA 和 xlwings 访问 A1 到 B2 的单元格的值的代码。
book.Sheets(1).Range("A1:B2").Value # VBA
book.sheets[0].range("A1:B2").value # xlwings
两行代码有以下区别。
属性:Python 使用的是小写字母,还可能包含第 3 章讲过的 Python 编程风格指南,即 PEP 8 建议的下划线。
索引:Python 使用方括号和从 0 开始的索引来访问 sheets 集合中的元素。
表 9-2 是对 xlwings 的 range 可接受的字符串格式总结。


也可以对 xlwings 的 range 对象进行索引和切片,通过观察尖括号之间的地址(打印出来的字符串表示)来确认你最终会得到怎样的单元格区域:
In [11]: # 索引
sheet1.range("A1:B2")[0, 0]
Out[11]: <Range [Book2]Sheet1!$A$1>
In [12]: # 切片
sheet1.range("A1:B2")[:, 1]
Out[12]: <Range [Book2]Sheet1!$B$1:$B$2>
索引对应着 VBA 中的 Cells 属性:
book.Sheets(1).Range("A1:B2").Cells(1, 1) # VBA
book.sheets[0].range("A1:B2")[0, 0] # xlwings
除了显式地使用 sheet 对象的 range 属性,也可以通过对 sheet 对象进行索引和切片来获 得一个 range 对象。利用 A1 表示法可以让你少敲些字,而使用整数切片可以让 Excel 工作表看起来像 NumPy 数组:
In [13]: # 单个单元格:A1表示法
sheet1["A1"]
Out[13]: <Range [Book2]Sheet1!$A$1>
In [14]: # 多个单元格:A1表示法
sheet1["A1:B2"]
Out[14]: <Range [Book2]Sheet1!$A$1:$B$2>
In [15]: # 单个单元格:索引
sheet1[0, 0]
Out[15]: <Range [Book2]Sheet1!$A$1>
In [16]: # 多个单元格:切片
sheet1[:2, :2]
Out[16]: <Range [Book2]Sheet1!$A$1:$B$2>
不过,有时候通过引用区域左上角和右下角的元素来定义一个区域可能更直观。下面的示例分别引用的是 D10 和D10:F11,以使你能够理解对 sheet 对象进行索引/切片和使用 range 对象之间的区别:
In [17]: # 通过sheet索引访问D10
sheet1[9, 3]
Out[17]: <Range [Book2]Sheet1!$D$10>
In [18]: # 通过range对象访问D10
sheet1.range((10, 4))
Out[18]: <Range [Book2]Sheet1!$D$10>
In [19]: # 通过sheet切片访问D10:F11
sheet1[9:11, 3:6]
Out[19]: <Range [Book2]Sheet1!$D$10:$F$11>
In [20]: # 通过range对象访问D10:F11
sheet1.range((10, 4), (11, 6))
Out[20]: <Range [Book2]Sheet1!$D$10:$F$11>

通过元组定义 range 对象与 VBA 中 Cells 属性的工作原理类似,下面的对比体现了这一 点。这里依然假定 book是 VBA 工作簿对象或 xlwings 的 book 对象。先来看 VBA 版本:
With book.Sheets(1)
myrange = .Range(.Cells(10, 4), .Cells(11, 6))
End With
这和下面的 xlwings 表达式是等价的。
myrange = book.sheets[0].range((10, 4), (11, 6))
从0开始的索引和从1开始的索引 :作为一个 Python 包,只要你通过 Python 的索引或切片语法(通过方括号) 访问元素,xlwings 就始终使用从 0开始的索引。不过 xlwings 的 range 对象使用的是和 Excel 一样从 1 开始的行列索引。和 Excel 的用户接口采用同样的行列索引在某些时候是有利的。如果你更喜欢使用 Python 的从 0 开始的索引,那么可以直接使用sheet[row_selection, column_selection] 语法。
下面的示例展示了如何从一个 range 对象(sheet["A1"])自底向上得到 app 对象。要记住 app 对象代表的是Excel 实例(尖括号之间的输出代表的是 Excel 的进程 ID,因此在你的计算机中这串数字可能会不一样):
In [21]: sheet1["A1"].sheet.book.app
Out[21]: <Excel App 9092>

现在我们到达了 Excel 对象模型的顶层,是时候看看可以如何利用多个 Excel 实例了。如 果你想在多个 Excel 实例中打开同一个工作簿,或是出于性能方面的原因想要将多个工作簿分发给多个实例,那么就需要显式地使用 app 对象。app 对象的另一个常见用例是在隐藏的 Excel 实例中打开工作簿:这样你就可以在后台运行 xlwings 脚本且同时在 Excel 中完成其他工作。
In [22]: # 从打开的工作簿中获取 app 对象,
# 并创建一个额外的隐藏的 app 实例
visible_app = sheet1.book.app
invisible_app = xw.App(visible=False)
In [23]: # 通过列表推导式列出各实例中打开的工作簿名称
[book.name for book in visible_app.books]
Out[23]: ['Book1', 'Book2']
In [24]: [book.name for book in invisible_app.books]
Out[24]: ['Book3']
In [25]: # app 的键代表进程 ID (PID)
xw.apps.keys()
Out[25]: [5996, 9092]
In [26]: # 也可以通过 pid 属性访问
xw.apps.active.pid
Out[26]: 5996
In [27]: # 处理隐藏的 Excel 实例中的工作簿
invisible_book = invisible_app.books[0]
invisible_book.sheets[0]["A1"].value = "Created by an invisible app."
In [28]: # 将这个 Excel工作簿保存在 xl 目录中
invisible_book.save("xl/invisible.xlsx")
In [29]: # 退出隐藏的 Excel 实例
invisible_app.quit()

如果你在两个 Excel 实例中打开了同一个工作簿,或者想要指定某个 Excel 实例打开某个工作簿,就不能再使用 xw.book 了。此时需要使用表 9-3 中列出的 books 集合。注意, myapp 代表一个 xlwings app 对象。如果将 myapp.books 替换成 xw.books,则 xlwings 会使用活动的 app。

在深入了解 xlwings 如何取代 VBA 宏之前,先来看看 xlwings 如何与现有的 VBA 代码交互:如果你有大量的过时代码且没有时间把它们迁移到 Python 的话,这一特性会帮你的忙。
9.1.3 运行 VBA 代码
如果你手上有一些包含大量 VBA 代码的旧式 Excel 项目,那么要将所有东西都迁移到 Python 并非易事。在这种情况下,可以通过 Python 来运行 VBA 宏。下面的示例使用了配套代码库的 xl 文件夹中的 vba.xlsm 文件。在Module1 中有如下代码:
Function MySum(x As Double, y As Double) As Double
MySum = x + y
End Function
Sub ShowMsgBox(msg As String)
MsgBox msg
End Sub
要通过 Python 调用这些函数,首先需要实例化一个随后会被调用的 xlwings macro 对象, 使其用起来就像一个原生的 Python 函数一样。
In [30]: vba_book = xw.Book("xl/vba.xlsm")
In [31]: # 用该 VBA 函数实例化一个 macro 对象
mysum = vba_book.macro("Module1.MySum")
# 调用 VBA 函数
mysum(5, 4)
Out[31]: 9.0
In [32]: # 与 VBA 子程序进行同样的工作
show_msgbox = vba_book.macro("Module1.ShowMsgBox")
show_msgbox("Hello xlwings!")
In [33]: # 关闭 book 对象(一定要先关闭对话框)
vba_book.close()

注意 :不要在 Sheet 模块和 ThisWorkbook 模块中保存 VBA 函数
如果将 VBA 函数 MySum 保存在工作簿的 ThisWorkbook 模块或者工作表模块 (如 Sheet1)中,就必须通过 ThisWorkbook.MySum 或 Sheet1.MySum 来引用这些函数。但是,这样你就不能在 Python 中访问这些函数的返回值了,所以一定要将 VBA 函数保存在标准的 VBA 代码模块中,即在 VBA 编辑器中右键单击模块文件夹来插入 VBA 函数。
现在你已经知道了如何与现有的 VBA 代码交互,我们可以继续 xlwings 探索之旅了。接下 来你会看到如何用 xlwings 来操作 DataFrame,NumPy 数组,以及图表、图片、已定义名称等集合。