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

第四部分 使用 xlwings 对 Excel 应用程序进行编程
第十一章 Python 包跟踪器
本章会构建一个典型的商业应用程序 ,它可以从互联网上下载数据并存储到数据库 中,然 后再将数据在 Excel 中进行可视化 。在此过程中你会认识到 xlwings 在这样的应用程序开发过程中扮演着怎样的角色,也能看到将 Python 连接至外部系统有多容易。在尝试构建一个十分接近真实情况且简单易懂的项目的过程中,我想到了 Python 包追踪器 。这个 Excel 工具可以显示某个 Python 包每年发布的次数。虽然这只是一个案例研究,但是实际上你可能会发现这个工具可以用来了解一个 Python 包是否处于积极开发的状态。
在对这个应用程序有了一个大致的了解后,为了能够理解它的代码,首先需要研究如下问题:如何才能从互联网上下载数据以及如何与数据库交互。然后再学习 Python 中的异常处理。当我们涉足应用程序开发时,异常处理是一个很重要的概念。学完这些基础知识之 后,我们会研究 Python 包追踪器的各个组件,了解它们是如何相互协作的。本章在最后会研究如何调试 xlwings 代码。和前两章一样,本章也需要在 Windows 或 macOS 中安装 Microsoft Excel。首先来试用一下 Python 包追踪器。
11.3 应用程序架构
本节会通过研究 Python 包跟踪器的底层来理解其工作原理。我们首先会浏览这个应用程序的前端 ,也就是 Excel 文件 ;然后再研究它的后端 ,也就是 Python 代码 ;最后会介绍如何调试 xlwings 项目,对于与包跟踪器具有相同体量和复杂度的应用程序来说,这是很有用的技能。
在配套代码库的 packagetracker目录中可以找到 4 个文件。还记得第 1 章中讲到的关注点分离吗?现在我们可以像表 11-4 这样将这些文件映射到不同的层。

值得一提的是表示层,即这个 Excel 文件并不包含任何单元格公式, 这使得这个工具更加容易审查和控制。
模型--视图--控制器(MVC) :关注点分离有多种实现方式,像表 11-4 这样的划分方法只是其中之一。另一种不久之后你可能就会碰到的流行的设计模式叫作模型--视图--控制器 (model-view-controller, MVC)。 在 MVC 的世界中,应用程序的核心是模型 ,所有的数据和业务逻辑都由模型处理 。而视图对应的是表示层 。控制器 位于模型和视图之间,它会确保模型和视图保持同步。
11.3.1 前端
在构建 Web 应用程序时,会将前端和后端加以区分 。前端是在浏览器中运行的那一部分,而后端是在服务器上运行的代码。可以将同样的术语套用在 xlwings 工具上:前端是 Excel 文件,而后端是可以通过 RunPython 调用的 Python 代码。如果你想从头构建前端,那么可以先在 Anaconda Prompt 中执行如下命令(一定要先通过 cd 命令进入你选择的目录):
(base) J:\Files\PythonLearning>xlwings quickstart packagetracker_1

进入 packagetracker_1 目录,打开packagetracker_1.xlsm 文件。首先添加3 个标签页:Tracker、 Database 和 Dropdown。
细节解释:
按钮: 通过 "插入>形状" 插入一个圆角矩形。如果你想用标准按钮也可以,但是现在先不要为它添加宏。
命名区域: 为了让这个工具维护起来更容易,我们会在 Python 代码中使用命名区域而不是单元格地址。根据表 11-5 添加命名区域。

一种创建命名区域的方式是选择单元格,然后在名称框中输入名称,最后按下回车键确认,如下图所示。

表: 在 Dropdown 工作表中,首先在 A1 单元格输入 "packages",然后在 "插入>表" 中确认已勾选"我的表有标题"。最后在选中表格的情况下,进入功能区的表格设计标签页 (Windows 系统)或表格标签页(macOS系统),将 Table1 重命名为 dropdown_content, 如下图所示。

数据验证: 我们使用数据验证来构造 Tracker 工作表 B5 单元格中的下拉菜单。选中 B5 单元格,进 入 "数据>数据验证",在 "允许" 菜单项中,选择 "列表"。将来源设置为如下公式:
=INDIRECT("dropdown_content[packages]")

然后点击 OK 按钮确认。这只是对表格主体的引用,但是由于 Excel 无法直接接受表格引用,因此必须用 INDIRECT 公式进行包装,它会将表格解析为对应的地址。另外,利用表格可以在添加更多的包时自动调整下拉菜单显示的区域的大小。
条件格式化: 在添加一个包时,我们希望将一些可能发生的错误展示给用户:字段可能为空,包可 能已经在数据库中,可能在 PyPI 中找不到这个包。为了让错误消息以红色显示,而其他消息以黑色显示,这里会用到一种基于条件格式化的小技巧:每当消息包含 "error" 时,就设置成红色的字体。在 Database 工作表中,选择 C5 单元格,我们会在这里输出消息。然后进入 "开始>条件格式>突出显示单元格规则>文本包含",输入 error,在下拉菜单中选择 "红色文本",如下图所示,最后点击 OK 按钮。为 Tracker 工作表中的 C5 单元格应用相同的条件格式。


网格线: 在 Tracker 工作表和 Database 工作表中,通过取消勾选 "页面布局>网格线" 中的 "视图" 选项来隐藏网格线。
现在需要在 VBA 编辑器中添加 RunPython 调用,并将其连接到按钮。按下快捷键 Alt+F11(Windows系统)或Option-F11(macOS 系统)打开 VBA 编辑器,然后在 packagetracker_1.xlsm 的 VBA 项目下的模块窗口中双击模块 1 打开该模块。删除既存的 SamplCall 代码,替换成下面的宏:
Sub AddPackage()
RunPython "import packagetracker; packagetracker.add_package()"
End Sub
Sub ShowHistory()
RunPython "import packagetracker; packagetracker.show_history()"
End Sub
Sub UpdateDatabase()
RunPython "import packagetracker; packagetracker.update_database()"
End Sub

接下来,在各个按钮上单击右键,选择 "指定宏" 并选择和按钮对应的宏。下图展示的是 Show History 按钮,但是对于 Add Package 和 Update Database 按钮来说也是一样的。

现在前端就算完成了,接下来开始编写 Python 后端。
11.3.2 后端
在 VS Code 中打开配套代码库的 packagetracker.py 和 database.py 这两个文件。本节会引用一些代码片段来解释几个关键概念。先来看看当你点击 Database 工作表中的 Add Package 按钮时会发生什么。这个按钮被指定了下面的 VBA 宏:
Sub AddPackage()
RunPython "import packagetracker; packagetracker.add_package()"
End Sub
如你所见,RunPython 函数调用了例 11-1 展示的 pakcgaetracker 模块中的 add_package 这一 Python 函数。
例 11-1 packagetracker.py 中的 add_package 函数(略去注释)
def add_package():
db_sheet = xw.Book.caller().sheets["Database"]
package_name = db_sheet["new_package"].value
feedback_cell = db_sheet["new_package"].offset(column_offset=1) 】
feedback_cell.clear_contents()
if not package_name:
feedback_cell.value = "Error: Please provide a name!" ➊
return
if requests.get(f"{BASE_URL}/{package_name}/json",
timeout=6).status_code != 200: ➋
feedback_cell.value = "Error: Package not found!"
return
error = database.store_package(package_name) ➌
db_sheet["new_package"].clear_contents()
if error:
feedback_cell.value = f"Error: {error}"
else:
feedback_cell.value = f"Added {package_name} successfully."
update_database() ➍
refresh_dropdown() ➎
➊ 反馈信息中的 "error" 会通过条件格式化触发 Excel 中的红色字体。

➋ 在默认情况下,Requests 会一直等待响应,当 PyPI 出现问题响应缓慢时,这可能会导致应用程序 "挂起"。这就是为什么在生产代码中,你总是应该显式地提供一个 timeout 参数。
➌ 如果操作成功,那么 store_package 函数会返回 None;否则会返回带有错误信息的字符串。
➍ 为了保持例子简单,在这里整个数据库都被更新了。在生产环境中,你只会添加新包的记录。
➎ 该函数会使用 packages 表的内容更新 Dropdown 工作表中的表格。同时还会更新已经在 Excel 中配置好的数据验证部分,这可以确保所有的包都会出现在 Tracker 工作表的下拉菜单中。如果允许在 Excel 文件外部为数据库填充数据,那么就需要为用户提供一种直接调用该函数的方法。如果你有多个用户在不同的文件中使用同一个数据库,那么就需要这样做。
在注释的帮助下你应该能够理解 packagetracker.py 文件中的其他函数。现在将注意力转向 database.py 文件。例 11-2 展示了该文件中的前几行。
例11-2 database.py(带有相关import 语句的片段)
from pathlib import Path
import sqlalchemy
import pandas as pd
...
# 我们想要数据库文件和该文件在同一目录中
# 在这里,将路径转换为绝对路径
this_dir = Path(__file__).resolve().parent ➊
db_path = this_dir / "packagetracker.db"
# 数据库引擎
engine = sqlalchemy.create_engine(f"sqlite:///{db_path}")
虽然这段代码负责的是拼接出数据库文件的路径,但是它也展示了在处理各种文件(无论是图片、CSV 文件,还是这里谈到的数据库文件)时如何避免常见的错误。在编写一些简单的 Python 脚本时,你可以像在大部分 Jupyter 笔记本示例中所做的那样,只使用相对路径:
engine = sqlalchemy.create_engine("sqlite:///packagetracker.db")
只要你的文件在工作目录中,这就没有问题。不过当你在 Excel 中通过 RunPython 执行这段代码时,工作目录可能就不一样了,也就是会造成 Python 在错误的文件夹中查找这个文件,即你会得到 File not found 错误。为了解决这样的问题,你可以提供一个绝对路径, 或者像例 11-2 那样创建一个 Path 对象。这样一来,即使在 Excel 中通过 RunPython 执行代 码,也可以确保 Python 在源文件所在的目录查找文件。
如果想从头构建 Python 包跟踪器,则需要手动创建数据库:以脚本形式运行 database.py 文件,比如在 VS Code 中点击运行文件按钮。这个脚本会创建包含那两张表的数据库文件 packagetracker.db。你可以在 database.py 的底部找到创建数据库的代码:
if __name__ == "__main__":
create_db()
最后一行调用了 create_db 函数,而前面的 if 会在下面的"提示"中解释。
提示:if name == "main"
你会在很多 Python 文件底部看到这种 if 语句。它可以确保只有在该文件以脚本形式(比如,在 Anaconda Prompt 中执行 python database.py,或者点击 VS Code 中的运行文件按钮)运行时才会执行这段代码。而在文件被当作模块导入(比如 import database)时,这段代码不会被触发。之所以能达到这样的效果,是因为当你直接以脚本形式执行文件时,Python 会将名称 __main__ 赋予该文件,而通过 import 语句导入时,Python 会使用模块名 (database)进行调用。由于 Python 会使用一个叫作 __name__ 的变量来跟踪文件名,因此只有在以脚本形式执行文件时,if 语句才会得到 True 的结果, 而从 packagetracker.py 中导入时则不会被触发。
database 模块的其他代码会分别通过 SQLAlchemy 和 pandas 的 to_sql 方法及 read_sql 方法执行 SQL 语句,你可以体会一下这两种方法。
在构建 Python 包管理器这种复杂度的工具的过程中你可能会遇到一些问题,比如,你可能在 Excel 中重命名了一个命名区域,但是忘了在 Python 代码中也进行相应的更改。是时候了解一下如何进行调试了。
11.3.3 调试
要想方便地调试 xlwings 脚本,可以直接在 VS Code 中执行你的函数,而不用在 Excel 中点击相应按钮。 packagetracker.py 文件底部的如下几行代码可以帮助你调试 add_package 函数(在 quickstart 项目中也能找到这几行代码):
if __name__ == "__main__": ➊
xw.Book("packagetracker.xlsm").set_mock_caller() ➋
add_package()
➋ 由于只有当该文件以脚本形式被 Python 直接执行时,这段代码才会被执行,因此这里的 set_mock_caller() 命令只是用于调试目的:当你在 VS Code 或者 Anaconda Prompt 中运行文件时,它会将 xw.Book.caller() 设置为 xw.Book("packagetracker.xlsm")。这 样做的唯一目的是可以从 Python 和 Excel 任意一方运行这段脚本,而不需要在 add_ package 函数中来回切换 xw.Book("packagetracker.xlsm")(从 VS Code 调用时使用) 和 xw.Book.caller()(从 Excel 调用时使用)。
在 VS Code 中打开 packagetracker.py,点击 add_package 函数的任何一行代码行号左边的空白处设置一个断点。然后按下 F5 键,在对话框中选择 "Python文件" 以启动调试器,让代码在断点处暂停。一定要按 F5 键而不是使用运行文件按钮,因为运行文件按钮会忽略断点。
如果对 VS Code 的调试器并不熟悉,可以看一下附录 B,其中对相关的功能和按钮进行了解释。第 12 章还会再提到这个话题。如果你想调试其他的函数,则可以先停止当前的调试会话,然后再修改文件底部的函数名。例如,要调试 show_history 函数,可以将 packagetracker.py 的最后一行改成下面这样,然后再按 F5 键:
if __name__ == "__main__":
xw.Book("packagetracker.xlsm").set_mock_caller()
show_history()
在 Windows 中也可以在 xlwings 插件中勾选 Show Console(显示控制台)选项,激活该选项后在 RunPython 调用执行时会同时显示一个命令提示符窗口 2。这样就可以打印额外的信息来帮助你调试问题。例如,你可以打印变量的值,然后在命令提示符中进行检查。不过在代码执行完毕后,命令提示符就会立即关闭。如果需要让它多停留一会儿,这里有一个小技巧:将 input() 添加为函数中的最后一行。这样 Python 就会等待用户输入而不是立即 关闭命令提示符。检查完输出之后,在命令提示符中按回车键关闭窗口。但是在取消勾选 Show Console 选项之前一定要记得移除 input() 这一行。
11.4 小结
本章揭示了一个道理:即使不费那么大的劲也可以构建出相对复杂的应用程序。利用强大的 Python 包(比如 Requests 和 SQLAlchemy),你的开发工作将大为改观。相比之下, 用 VBA 和外部系统沟通则要困难得多。如果有相似的用例,强烈建议你深入研究一下 Requests 和 SQLAlchemy------利用它们能够高效地和外部数据源进行沟通,这样就可以和复制/粘贴这样的操作说再见了。 比起按按钮,有些用户更喜欢通过单元格公式来创建 Excel 工具。第 12 章会展示如何利用 xlwings 在 Python 中编写用户定义函数,这样你就可以再次用到前面已经学习过的大部分 xlwings 概念。