Python 数据分析基础入门:《Excel Python:飞速搞定数据分析与处理》学习笔记系列(第八章 使用读写包操作 Excel 文件 下篇)

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

第三部分 在 Excel 之外读写 Excel 文件

第八章 使用读写包操作 Excel 文件

本章介绍 OpenPyXL、XlsxWriter、pyxlsb、xlrd 和 xlwt:它们都是可以用来读写 Excel 文件的包,在调用pandas 的 read_excel 函数和 to_excel 函数时,这些包就在背后完成相应的工作。我们可以利用这些读写包来创建更加复杂的 Excel 报表,也可以对文件读取过程进行优化。另外,如果你参与的项目中只需要读写 Excel 文件而不需要 pandas 的其他功能,那么安装整个 NumPy/pandas 技术栈完全就是"杀鸡用牛刀"。本章首先会介绍应当如何从诸多读写包中做出选择,其语法又是怎样的。然后再对一些高级主题进行研究,其中包括如何处理大型Excel 文件,以及如何将 pandas 和各种读写包相结合以改进 DataFrame 的样式。最后会回到第 7 章开头的案例研究,通过调整表格的格式并添加图表来改进这份报表。

8.2 读写包的高级主题

如果你要处理的文件比例子中的 Excel 文件更大且更复杂,默认的设置可能就不够好了。因此本节首先研究如何处理大型文件。然后学习如何将 pandas 和这些读写包结合使用:这样你就可以随心所欲地调整 pandas DataFrame 的样式。在本节末尾,我们会运用本章中学到的所有知识来让第 7 章案例研究中的报表看起来更加专业。

8.2.1 处理大型 Excel 文件

处理大型 Excel 文件时可能会遇到两个问题:一是读写的过程可能很慢;二是你的计算机可能会耗尽内存。内存不足通常是一个大问题,因为它会导致程序崩溃。一个文件大不大总是取决于你的系统资源以及你对慢的定义。本节会展示各个包提供的一些优化技巧,这些技巧可以让你尽可能地处理更庞大的 Excel 文件。我会先研究写入库提供的选项,然后再研究读取库提供的选项。在本节末尾,我会向你展示如何并行读取工作簿的工作表以减少处理时间。

1、使用 OpenPyXL 写入文件

在使用 OpenPyXL 写入大型文件时,一定要安装好 lxml 包,因为 lxml 可以让写入过程更迅速。Anaconda 中已经包含了这个包,所以无须再进行额外的操作。然而,最关键的选项是 write_only=True 标志,它可以让内存消耗保持在较低的水平。不过,这个参数会通过 append 方法强制逐行写入,并且不再允许写入单个单元格。

复制代码
In [34]: book = openpyxl.Workbook(write_only=True) 
         # 设置 write_only=True 参数后 book.active 就不可用了 
         sheet = book.create_sheet() 
         # 生成一张包含 1000x200 个单元格的工作表 
         for row in range(1000): 
             sheet.append(list(range(200))) 
         book.save("openpyxl_optimized.xlsx")
2、使用 XlsxWriter 写入文件

XlsxWriter 有一个和 OpenPyXL 类似的选项叫作 constant_memory。它也会强制逐行写入。 你需要像下面这样以字典的形式来传递 options 参数。

复制代码
In [35]: book = xlsxwriter.Workbook("xlsxwriter_optimized.xlsx", 
                                    options={"constant_memory": True}) 
         sheet = book.add_worksheet() 
         # 生成一张包含 1000x200 个单元格的工作表 
         for row in range(1000): 
             sheet.write_row(row , 0, list(range(200))) 
         book.close()

生成的文件和上图是一样的。

3、使用 xlrd 读取文件

在读取旧式的 xls 格式的大型文件时,xlrd 可以按需加载工作表,就像下面这样:

复制代码
In [36]: with xlrd.open_workbook("xl/stores.xls", on_demand=True) as book: 
             sheet = book.sheet_by_index(0)  # 只加载第一张工作表

如果不想像这里的代码这样将工作簿用作上下文管理器,则需要手动调用 book.release_ resources() 来正确关闭工作簿。要搭配 pandas 在上下文管理器模式下使用 xlrd,可以像下面这样编写代码。

复制代码
In [37]: with xlrd.open_workbook("xl/stores.xls", on_demand=True) as book: 
             with pd.ExcelFile(book, engine="xlrd") as f: 
                 df = pd.read_excel(f, sheet_name=0)
4、使用 OpenPyXL 读取文件

要在使用 OpenPyXL 读取大型 Excel 文件时控制内存 ,应该使用 read_only=True 参数来加载工作簿。由于OpenPyXL 并不支持 with 语句,因此需要确保在工作完成时关闭文件。 如果你的文件包含指向外部工作簿 的链接,那么可能还需要使用 keep_links=False 参数来加速读取过程。keep_links 可以确保对外部工作簿的引用不会丢失 ,因而如果你只对读取某个工作簿的值感兴趣,那么将这个参数设置为 True 可能会造成不必要的性能损失

复制代码
In [38]: book = openpyxl.load_workbook("xl/big.xlsx", 
                                       data_only=True, read_only=True, 
                                       keep_links=False) 
         # 在这里执行所需读取操作 
         book.close()  # 需设置参数 read_only=True
5、并行读取工作表

在使用 pandas 的 read_excel 函数读取大型工作簿的多张工作表 时,你会发现这个过程会花很长时间(马上会看到一个具体的例子)。这是因为 pandas 会逐张读取 工作表。要想让这个过程更快,可以并行读取这些工作表。虽然由于文件的内部结构,并不存在一种简单的方法让工作簿写入过程并行化,但并行读取多张工作表还是非常简单的。不过,由于并行化 是一个高级主题,因此在 Python 入门部分略过了这个主题,将其放在了这里进行详细介绍。

在 Python 中,如果想充分利用现代计算机都具备的多核处理器 ,就需要使用标准库中的多线程包 。多线程包会生成多个并行执行任务的 Python 解释器 (通常一个 CPU 核心一个解释器 )。此时不再是逐张处理工作表,而是一个Python 解释器处理第一张工作表,与此同时另一个 Python 解释器处理第二张工作表,以此类推。然而,每个额外的 Python 解释器都需要一定时间启动且需要占用额外的内存,所以如果你的文件很小,那么并行读取反而可能会更慢。对于包含多个大型工作簿的大型文件的情况,多线程可以显著加快读取过程,不过这是在假定你的系统有足够的内存处理工作负载的情况下。如果像第 2 章所讲的那样在 Binder 中运行 Jupyter 笔记本,那么你就没有足够的内存,因此并行化的版本会运行得更慢。在配套代码库中,你可以找到 parallel_pandas.py,这是使用OpenPyXL 并行读取工作表的一种简单实现方式。这个模块用起来很简单,无须对多线程有任何了解:

复制代码
import parallel_pandas 
parallel_pandas.read_excel(filename, sheet_name=None)

在默认情况下,它会读取所有工作表,不过你可以提供一个想要处理的工作表名称的列表。和 pandas 类似,这个函数会返回如下形式的字典:{"sheetname": df},即以工作表名称为键、DataFrame 为值。

魔法指令 %%time

让我们来了解一下 %%time 单元格魔法。第 5 章通过 Matplotlib 介绍过魔法指令。%%time 是一个可以用来优化性能 的实用单元格魔法,它可以轻松地对比不同代码片段的执行时间 。**真实时间(wall time)**是程序(在这里是单元格)从头到尾经过的时间。如果你使用的是 macOS 或 Linux,则不仅会得到真实时间,还会得到一行 CPU 时间

复制代码
CPU times: user 49.4 s, sys: 108 ms, total: 49.5 s

CPU 时间测量的是程序在 CPU 上花费的时间,这个时间可能比真实时间短(如果程序必须等待 CPU 可用的话),也可能比真实时间长(如果程序在多核处理器上并行执行的话)。为了更准确地测量时间,需要使用 %%timeit 而不是 %%time,%%timeit 会运行单元格多次并取平均时间 。%%time 和 %%timeit 都是单元格魔法,也就是说它们必须出现在单元格的第一行,并且测量的是整个单元格的执行时间。如果你只想测量一行的执行时间,则可以在行首使用 %time 或 %timeit。

让我们先来看一下 big.xlsx 这个文件的大小如何:

下面来见识一下并行读取 big.xlsx(你可以在配套代码库的 xl 文件夹中找到)的代码有多快:

复制代码
In [39]: %%time 
         data = pd.read_excel("xl/big.xlsx", 
                              sheet_name=None, engine="openpyxl") 
Wall time: 49.5 s 		# 真实时间
In [40]: %%time 
         import parallel_pandas 
         data = parallel_pandas.read_excel("xl/big.xlsx", sheet_name=None) 
Wall time: 12.1 s

从上图的两段代码执行结果的对比不难看出:在引入 parallel_pandas 这个模块处理 big.xlsx 文件的时间大幅减少了将近 75% 左右。

要获得代表 Sheet1 的 DataFrame,在两种情况下都是使用 data["Sheet1"]。如果还想更快,可以直接将OpenPyXL 并行化:在配套代码库中可以找到对应的实现(parallel_openpyxl.py),同时还有一个用于 xlrd 并行读取旧式 xls 格式文件的实现(parallel_xlrd.py)。通过底层的包而不是 panda 让你能够跳过 DataFrame 的转换过程, 也可以应用所需的清理过程。这很可能可以帮助你提升代码的运行速度------如果这对你很重要的话。

使用 Modin 并行读取工作表

如果只需读取单张大型工作表,那么值得了解一下 Modin。Modin 项目可以作为 pandas 的替代品。它会并行处理单张工作表的读取过程,并实现显著的速度提升。由于 Modin 需要指定版本的 pandas,因此你在安装 Modin时可能会导致 Anaconda 附带的 pandas 被降级。如果想尝试一下,建议你为此创建一个单独的 Conda 环境, 确保你的 base 环境不会被弄乱。参见附录 A 详细了解如何创建 Conda 环境:

复制代码
(base)> conda create --name modin python=3.8 -y  
(base)> conda activate modin 
(modin)> conda install -c conda-forge modin -y

笔者在实际操作后,出现一点小问题,记录在此。

首先是在 Anaconda Prompt 窗口下,在 base 环境下输入 "conda create --name modin python=3.8 -y" 出现报错的情况。

在网上查阅相关文档后,实现了解决:

复制代码
(base) C:\Users\SHY>conda config --show-sources		# 查看是否存在.condarc文件

找到上面图片输出的.condarc的文件位置,将.condarc删除。

然后重新执行 conda 新建环境的命令 "conda create --name modin python=3.8 -y"。

接下来,博主激活 modin 环境,执行 "conda install -c conda-forge modin -y" 命令报错:

经过一番刨根问底后,博主发现是系统代理的问题(博主的主机挂了梯子),即:系统代理不支持HTTPS。但是使用命令 "conda install -c conda-forge modin -y" 依旧报错,于是博主改用 pip 命令进行安装(先别急执行此命令,继续往下看):

复制代码
pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple "modin[all]"

问题又出现了,安装进行到一半又出现了下图中的报错:

继续查阅相关文档,发现是由于 setuptools 版本问题引起的报错。根据上面报错中显示的要求 Python 版本至少是 3.9 以上,但是博主构建的 modin 环境是基于 python 3.8 版本的。因此需要安装与之对应的 setuptools 版本。先来查看当前 modin 环境下的 setuptools 的版本:

复制代码
pip show setuptools

我们来到 setuptools 的官网查找对应的 setuptools 版本,找到 setuptools 70.0.0 正好对应 python >= 3.8。

于是,回到命令行窗口,先将 setuptools 的当前版本卸载,重新安装对应的 70.0.0 版本。

复制代码
pip install setuptools==70.0.0

ok,重新执行命令 pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple "modin[all]",你会发现还是出现了相同的报错,博主哭了。但是困难就是这样,不断出现,才有挑战。于是,博主将这段报错 copy 下来,丢给 Claude Sonnet 帮我分析原因,给出了如下答案:

那么,博主就先来看看 puccinialin 是个什么。百度后得到这样的答案:

puccinialin是一个Python包,主要用于Rust环境的安装和构建依赖管理,在某些Python包安装过程中可能被误认为必需依赖

好啊,原来是 Rust 环境的包。因为博主安装的是 modin[all],所以导致报错。那么我们来安装 modin[dask] 试一试。输入命令(放心执行吧!):

复制代码
pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple "modin[dask]"	# 改成 modin[dask]

可以看到,安装没有问题,继续下一步。首先安装 conda 包,移步附录 A 按照步骤进行安装。然后,输入以下命令,激活内核:

复制代码
pip install ipykernel -i https://pypi.tuna.tsinghua.edu.cn/simple
python -m ipykernel install --user --name=modin

激活成功后,在 modin 环境下输入 "jupyter notebook",启动网页:

可以看到,我们成功地切换到 modin 环境下运行。输入以下命令:

复制代码
import modin.pandas
import os
os.environ["MODIN_ENGINE"] = "dask"

%%time
data = modin.pandas.read_excel("xl/big.xlsx",
                               sheet_name=0, engine="openpyxl")

至此,经过一番折腾,我们成功地证明了在读取单张大型工作表时,Modin 确实能在读取速度上带来显著提升。

8.2.2 调整DataFrame在Excel中的格式

要想按照需求调整 DataFrame 在 Excel 中的格式,可以将 pandas 与 OpenPyXL 或 XlsxWriter 结合使用。首先用它们来为导出的 DataFrame 添加一个标题。然后对 DataFrame 的标题和索引进行格式化。最后再格式化 DataFrame 的数据部分。在读取文件的过程中将 pandas 和 OpenPyXL 搭配使用在有些时候相当强大,所以从这里开始:

复制代码
In [41]: with pd.ExcelFile("xl/stores.xlsx", engine="openpyxl") as xlfile: 
             # 读取DataFrame 
             df = pd.read_excel(xlfile, sheet_name="2020") 
             # 获取OpenPyXL工作簿对象 
             book = xlfile.book 
             # OpenPyXL代码从这里开始 
             sheet = book["2019"] 
             value = sheet["B3"].value  # 读取单个值

在写入工作簿时,其工作方式是类似的,我们可以方便地为 DataFrame 报表添加一个标题:

复制代码
In [42]: with pd.ExcelWriter("pandas_and_openpyxl.xlsx", 
                             engine="openpyxl") as writer: 
             df = pd.DataFrame({"col1": [1, 2, 3, 4], "col2": [5, 6, 7, 8]}) 
             # 写入DataFrame 
             df.to_excel(writer, "Sheet1", startrow=4, startcol=2)
             # 获取OpenPyXL工作簿和工作表对象 
             book = writer.book 
             sheet = writer.sheets["Sheet1"] 
             # OpenPyXL的代码从这里开始 
             sheet["A1"].value = "This is a Title"  # 写入单个单元格的值

这些示例中使用了 OpenPyXL,但是对于其他包来说在概念上也是一样的。接下来了解一下应该如何调整 DataFrame 的索引和标题。

1、调整 DataFrame 索引和标题的格式

要想完全控制索引和列标题的格式,最简单的方式是亲自编写相应的代码。下面的示例分别展示了如何利用OpenPyXL 和 XlsxWriter 达到这一目的。先从创建 DataFrame 开始:

复制代码
In [43]: df = pd.DataFrame({"col1": [1, -2], "col2": [-3, 4]}, 
                            index=["row1", "row2"]) 
         df.index.name = "ix" 
         df 
Out[43]:       col1  col2 
         ix 
         row1     1    -3 
         row2    -2     4

用 OpenPyXL 格式化索引和标题如下:

复制代码
In [44]: from openpyxl.styles import PatternFill 
In [45]: with pd.ExcelWriter("formatting_openpyxl.xlsx", 
                             engine="openpyxl") as writer: 
             # 将整个df以默认格式从A1处写入 
             df.to_excel(writer, startrow=0, startcol=0) 
             # 将整个df以默认自定义索引/标题格式从A6处写入 
             startrow, startcol = 0, 5 
             # 1.写入DataFrame的数据部分 
             df.to_excel(writer, header=False, index=False, 
                         startrow=startrow + 1, startcol=startcol + 1) 
             # 获取工作表对象并创建样式对象 
             sheet = writer.sheets["Sheet1"] 
             style = PatternFill(fgColor="D9D9D9", fill_type="solid") 
             # 2.写入带样式的列标题 
             for i, col in enumerate(df.columns): 
                 sheet.cell(row=startrow + 1, column=i + startcol + 2, 
                            value=col).fill = style 
             # 3.写入带样式的索引 
             index = [df.index.name if df.index.name else None] + list(df.index) 
             for i, row in enumerate(index): 
                 sheet.cell(row=i + startrow + 1, column=startcol + 1, 
                            value=row).fill = style

如果想使用 XlsxWriter 对索引和标题进行格式化,那么需要对代码进行一些微调:

复制代码
In [46]: # 使用XlsxWriter对索引/标题进行格式化 
         with pd.ExcelWriter("formatting_xlsxwriter.xlsx", 
                             engine="xlsxwriter") as writer: 
             # 将整个df以默认格式从A1处写入 
             df.to_excel(writer, startrow=0, startcol=0) 
             # 将整个df以默认自定义索引/标题格式从A6处写入 
             startrow, startcol = 0, 5 
             # 1.写入DataFrame的数据部分 
             df.to_excel(writer, header=False, index=False, 
                         startrow=startrow + 1, startcol=startcol + 1) 
             # 获取工作簿对象和工作表对象并创建样式对象 
             book = writer.book 
             sheet = writer.sheets["Sheet1"] 
             style = book.add_format({"bg_color": "#D9D9D9"}) 
             # 2.写入带样式的标题 
             for i, col in enumerate(df.columns): 
                 sheet.write(startrow, startcol + i + 1, col, style) 
             # 3.写入带样式的索引 
             index = [df.index.name if df.index.name else None] + list(df.index) 
             for i, row in enumerate(index): 
                 sheet.write(startrow + i, startcol, row, style)

执行后生成的文件如下:

表格中为带有默认格式的DataFrame(左)和带有自定义格式的DataFrame(右)。

2、格式化 DataFrame 的数据部分

数据部分的格式化能达到何种程度取决于你所使用的是哪种包:如果你使用的是 pandas 的 to_excel 方法,那么OpenPyXL 可以为每一个单元格应用一种格式,而 XlsxWriter 只能对行或列应用格式。如果要设置单元格的数字格式为 3 位小数并居中内容(见下图), 那 么可以使用 OpenPyXL 编写如下代码。

复制代码
In [47]: from openpyxl.styles import Alignment 
In [48]: with pd.ExcelWriter("data_format_openpyxl.xlsx", 
                             engine="openpyxl") as writer: 
             # 写入DataFrame 
             df.to_excel(writer) 
             # 获取工作簿对象和工作表对象
             book = writer.book 
             sheet = writer.sheets["Sheet1"] 
             
			 # 格式化每一个单元格
             nrows, ncols = df.shape 
             for row in range(nrows): 
                 for col in range(ncols): 
                     # 考虑到标题/索引,这里加1 
                     # 因为 OpenPyXL 的索引是从 1 开始的,所以还要加 1 
                     cell = sheet.cell(row=row + 2, 
                                       column=col + 2) 
                     cell.number_format = "0.000" 
                     cell.alignment = Alignment(horizontal="center")

对于 XlsxWriter,将代码进行如下调整:

复制代码
In [49]: with pd.ExcelWriter("data_format_xlsxwriter.xlsx", 
                             engine="xlsxwriter") as writer: 
             # 写入DataFrame 
             df.to_excel(writer) 
             # 获取工作簿对象和工作表对象 
             book = writer.book 
             sheet = writer.sheets["Sheet1"] 
             # 格式化各列(无法格式化单个单元格) 
             number_format = book.add_format({"num_format": "0.000", 
                                              "align": "center"}) 
             sheet.set_column(first_col=1, last_col=2, 
                              cell_format=number_format)

pandas 在 DataFrame 上提供了实验性的 style 属性作为一种替代品。"实验性"意味着其语法随时可能发生改变。由于样式属性是为了将 DataFrame 格式化为 HTML 而引入的,因此它们使用的是 CSS 语法。CSS 代表 cascading style sheet(层叠样式表),它是用来定义 HTML 元素的样式的。要应用与前面示例中相同的格式(3 位小数和居中对齐),需要通过 applymap 对 Styler 对象的每个元素应用一个函数。通过 df.style 属性可以获得 Styler 对象:

复制代码
In [50]: df.style.applymap(lambda x: "number-format: 0.000;" 
                                     "text-align: center")\ 
                 .to_excel("styled.xlsx")

在不依赖样式属性的情况下,pandas 还提供了对日期和时间的格式化支持:

复制代码
In [51]: df = pd.DataFrame({"Date": [dt.date(2020, 1, 1)], 
                            "Datetime": [dt.datetime(2020, 1, 1, 10)]}) 
         with pd.ExcelWriter("date.xlsx", 
                             date_format="yyyy-mm-dd", 
                             datetime_format="yyyy-mm-dd hh:mm:ss") as writer: 
             df.to_excel(writer)

其他读写包

  • pyexcel 为读写不同的 Excel 包以及其他文件格式(包括 CSV 文件和 OpenOffice 文 件)提供了一种通用的语法。
  • PyExcelerate 的目标是以尽可能快的速度写入 Excel 文件。
  • pylightxl 可以读取 xlsx 文件和 xlsm 文件,可以写入 xlsx 文件。
  • styleframe 将 pandas 和 OpenPyXL 相结合以从格式规整的 DataFrame 生成 Excel 文件。
  • oletools 并非一个经典的读写包,但它可以分析 Microsoft Office 文档(比如分析恶意软件)。它提供了一种便捷的方法来提取 Excel 工作簿中的 VBA 代码。

8.3 小结

本章首先介绍了 pandas 在底层使用的各种读写包。无须安装 pandas,直接使用这些读写包就能够读写 Excel 工作簿。不过,将它们和 pandas 结合起来则可以通过添加标题、图表、 格式等方式来改进 Excel DataFrame 报表。虽然目前的读写包已经非常强大,但还是希望在某一天出现一个 "NumPy 时刻" 将所有开发者的努力都汇聚到同一个项目中(天下大同)。如果不需要查表就能知道什么时候该用什么包,也不需要为不同的 Excel 文件使用不同的语法, 那就再好不过了。因此,一开始可以尽可能地使用 pandas 来解决这类问题,只在 pandas 没有提供你所需要的功能时才用到读写包。

不过,Excel 远非一个简单的数据文件,也不仅仅是一张简单的报表。Excel 应用程序拥有非常直观的用户界面,用户只需输入几个数字就可以让其显示所需信息。不直接读写 Excel 文件,而是对 Excel 应用程序进行自动化可以让我们进入一个全新的领域,这就是本书第四部分将要探索的内容。第 9 章会展示如何用 Python 远程控制Excel,这就是 Excel 自动化之旅的起点站。

相关推荐
tryCbest1 小时前
Flask vs FastAPI 全方位对比与实战
python·flask·fastapi
测试员周周1 小时前
【Appium 系列】第04节-Page Object 模式 — BasePage 基类设计
开发语言·数据库·人工智能·python·语言模型·appium·web app
无限中终1 小时前
如何抓取某音视频的互动数据
爬虫·python
坐吃山猪1 小时前
Python34_装饰器知识
开发语言·python·ubuntu
ZHW_AI课题组1 小时前
调用华为智能云API实现手写图片识别
图像处理·python·机器学习·华为·分类
赵钰老师1 小时前
生态系统NPP及碳源、碳汇模拟(土地利用变化、未来气候变化、空间动态模拟)
数据分析
weixin_660096781 小时前
【无标题】
python·debugpy
小草cys1 小时前
Anaconda 的虚拟环境(envs)从默认的 C 盘迁移到其他磁盘
开发语言·python·anaconda
keineahnung23451 小时前
PyTorch SymNode 的 _is_contiguous 從何而來?──sizes_strides_impl 實作詳解
人工智能·pytorch·python·深度学习