179-Python自动化办公实战课

导读｜入门Python的必备知识

你好，我是尹会生。

咱们这个课程是用 Python 解决办公低效问题，但是即使你从来没有用过 Python，甚至没有任何的编程语言基础，也完全可以学会这门课。

为了解决你的语言问题，我特意准备了这节课。我会给你讲解 Python 的五个最基础的语法知识，包括运行环境配置、变量、数据类型、控制语句和使用函数库的方法。

这节课的内容也不需要你马上掌握，其中提到的一些关键知识，我会在后面的课程中详细讲解，包括它们的使用场景和具体用处。学完这节课，你只要能对 Python 有一个初步的了解，可以看懂基本的 Python 代码就行了。

当然，如果你有一定的 Python 语言基础，那么这节课就相当于给你巩固复习了，帮你查漏补缺。

运行环境配置

我们先从 Python 的运行环境配置开始说起。初学者面对的最大难题，就是如何让自己的 Python 程序运行起来。

一般情况下，运行的 Python 代码会被保存到一个以.py 作为扩展名的文件中，也就是 Python 脚本文件。要想让 Python 程序运行，我们需要打开终端应用程序（在 Windows 中开始 - 运行 -cmd.exe）：

python3 /py文件所在的路径/xxx.py
或
cd /py文件所在的路径
python3 xxx.py

这段代码中，xxx.py 就是我们编写好的 Python 脚本文件，Python3 是 Python 脚本文件的解释器，它会把我们编写好的代码翻译给计算机，让计算机去执行。

在运行程序的过程中，有两点需要注意。

第一，Windows 和 macOS 默认是没有安装 Python 解释器的，所以你需要通过Python 官方网站下载不同操作系统下的 Python 解释器。关于 Python 的版本，我建议你使用官方建议的最新稳定版本，下载后直接安装就可以使用了。

第二，Python3 解释器需要在字符终端下运行。

以 macOS 系统为例，你需要打开终端来运行python3 -V命令来验证 Python 解释器是否安装成功，如果显示了 Python 解释器的版本，那说明安装是成功的。如果提示没有找到这个命令，你需要重新安装，并注意安装过程中是否有错误提示信息。

 python3 -V   # 执行的命令
 Python 3.9.1 # 返回的结果

搭建 Python 的开发环境，是运行一切程序的基础。无论你现在手边电脑操作系统是哪一种，我都建议你马上行动，先按照刚刚讲解的步骤，正确地安装和设置 Python 开发环境。这样在之后的课程中，你就能马上跟着我的讲解进行操作了。

变量

在正确配置好 Python 的开发环境之后，咱们继续学习一个重要概念：变量。

变量这个词来源于数学，在编程语言中，变量主要是用来存储计算结果或表示值的抽象概念。

计算机中的变量都有一个名字，我们把它称为变量名。这个名字一般是根据变量的英文名进行人为命名的，一般多用简短且易于记忆的名字。比如，我在课程中会使用 src_path、dst_path 两个变量表示操作源文件路径和目标文件路径。

我们还可以为变量指定一段数据，这段数据称作变量的值。计算机处理的数据很大一部分来源于用户输入的数据和运算的结果数据，如果这部分数据需要跟踪，我们就可以将数据存储在变量中。

对于初学者来说，变量的概念比较抽象，你可能现在有点蒙。别急，我现在给你举一个简单的例子，你一看就明白了。假如我要在 Python 当中计算两个数的和，代码如下：

x=100
y=200
z=x+y
print(z)

在上面的代码当中， 我们分别定义了 x、y、z 三个变量。 x、y、z 称作变量名，定义变量名必须要遵从以下四个规则：

变量名只能是字母、数字或下划线；

变量名的第 1 个字母不能是数字；

变量名也不能是 Python 的关键字；

变量名是区分大小写的。

上述代码中的 = 和 +，称作运算符。

“=”叫做赋值运算符。它的功能是将右边的表达式赋值给左边的变量。

“+”叫做算术运算符。进行算术运算时还可以使用 -、*、/、% 等常用算术运算符号。

代码的最后一行，为了能让你的计算结果在终端上显示，我们需要使用一个叫做 print 的内置函数。

内置函数是 Python 内置的一种功能。比如代码中的 print 就是用来进行输出的。它的最基本用法就是在 print 函数的括号中写入变量，除了输出最终的结果外，也可以将 print 函数放在程序运行的过程中，方便你输出更丰富的调试信息。

通过上面的讲解，我为你介绍了变量如何进行赋值和如何输出。其实，在 Python 中变量用来存储的数据还会区分类，这些类型也有专门的名称，叫做数据类型。

数据类型

Python 支持的数据类型有数字、字符串、元组、列表、集合、字典 6 种，不同的数据类型用于描述不同的类别，比如姓名可以用字符串类型，性别可以用布尔型，身高可以用数字类型。下面的代码，我为你演示一下三种最简单的数据类型：

name = "yin huisheng"
male = True
hight = 180

上面的三个变量，我分别为它赋予了字符串、布尔类型和浮点型这三种数据类型。 Python 的变量不需要事先定义变量类型，跟其他语言相比更加简洁。Python 可以直接对变量进行赋值，这样就可以根据变量值自动识别变量的类型了。

不过在上面的例子当中，你还需要注意两点。

一方面，如果你使用字符串类型，就必须给字符串增加引号。这是字符串的语法规定，如果不加引号会被识别为其他类型，在进行操作时就会得到不同的结果。

比如说，我定义 Python 的变量为“123”，加了引号会被识别为字符串，如果一个变量“123”没有加引号，则会被自动识别为数字。如果在 Python 程序中使用“123+123”会得到两个整数的和“246”，如果是“123”+“123”就会得到两个字符串的连接“123123”。

另一方面，使用布尔类型的时候，True 和 False 首字母必须要大写，它们是 Python 的关键字，如果首字母没有大写会被当作变量处理，解释器也会报错。True 在布尔类型表示真（对），False 表示假（错）。比如算式 10>5 是正确的，在 Python 中就会用 True 来表示结果是真，反之为假。

上面提到的整数、浮点数和布尔型都是属于数字类型。除了这些，Python 还支持很多种数据类型，包括数字、字符串、元组、列表、集合、字典等。它们之间的关系，我用下面的思维导图为你进行展示。

我在课程中会逐一为你剖析它们的用处, 让你能够全部掌握。当你学会这些类型的时候, 可以更精确地描述数据，比如，我想用 Python 的数据类型存储“5 个苹果和 10 个橘子”，我们就需要使用字典和字符串来表示这些数据。你可以看一下下面这个记录水果数量的例子：

fruits1 = {"apple":5, "orange":10}
fruits2 = "apple,5,orange,10"

我分别用字典和字符串记录了水果的名称和数量，可以看到，字典表达的水果和数量之间的映射关系更明确，而且不同的数据类型还能支持它特有的内置方法。例如上面定义了水果的字典数据类型，我们可以通过 fruits["apple"] 直接得到苹果的数量 5 , 有时候，这些便捷的内置方法也是我们选择数据类型的依据。

数据类型是一门编程语言的必要组成部分，学习编程语言主要就是为了掌握它的数据类型。这节课我只要求你能够了解 Python 有哪些基本数据类型就可以了，在之后的课程当中，我会为你重点介绍每一种数据类型的优缺点，以及该怎么使用它们。

流程控制

流程控制，就是我们经常说的程序执行的顺序控制。 从上面的几个例子当中，你会发现程序都是从上向下依次执行的。这也是 Python 语言最简单的结构——顺序结构。在 Python 中，除了顺序结构之外，还有分支和循环两种结构。

分支结构

分支结构用来判定某个条件是否成立，它会根据一条或多条语句的判定结果（是 True 还是 False）来执行对应操作的语句，从而实现分支的效果。实现分支结构的语句叫做 if 语句。除了 if 这个关键词外，我们经常还会用 if...else 和 if...else...if 语句来实现更多的分支和更复杂的逻辑。

我们一起看一个比较数值大小的例子吧。

x = 20
if x > 10:  # 注意结尾的冒号:
    print("if的判断结果为True")
    print("x的值大于10")
else:
    print("if的判断结果为False")
    print("x的值小于10")
print("程序执行结束")

# 执行结果如下
if的判断结果为True
x的值大于10
程序执行结束

在上面这段代码当中，我们实现了判断变量 x 是否大于 10 的功能。在书写分支结构时，要注意书写格式问题，分支结构的书写格式比顺序结构要复杂，例如：

if 判断的书写格式，冒号标志着 if 判断的结束。

if 下方有缩进的代码，是在 True 的条件下才能运行的。在演示代码中，缩进代码由 2 行组成，其实，它也可以由很多行代码组成，多行代码的形式被称作代码块。代码块一直到缩进再次改变的时候，True 条件下的代码才结束。

需要你注意的是，在整个程序当中，缩进方式必须一致，要么使用 Tab、要么使用 4 个空格进行缩进。Python 对于缩进和复合语句结尾的冒号要求非常严格，如果弄错了就会报错。而且，缩进不同会大大影响一个程序的运行结果。例如上面的代码中“print("程序执行结束")” 如果有缩进，你就看不到“程序执行结束”这行执行结果了。

循环结构

在程序运行逻辑中，另一个重要的结构叫做循环结构。循环结构是可以多次执行同一段代码的语句结构。在 Python 当中有两种循环语句，分别是 while 语句和 for 语句。

while 语句在循环开始前先对条件语句进行判断。如果条件语句的结果为 True，则循环一次再次进行判断。如果条件允许的结果为 False，则循环结束。这里所指的条件语句和 if 语句当中的条件是一样的。下面的例子是我使用 while 语句实现的从 1~10 的累加。

start = 1
total = 10
sum = 0
while start <= total: # 条件为True 继续执行循环
    sum += start
    start += 1
print(sum)  #条件为False循环结束,输出结果

另一个循环语句叫做 for 语句，它和 while 的功能有所不同。我们一般把 for 的功能称作遍历功能。一般我们会提供一个列表，通过 for 将列表中的每一个元素读取出来，进行处理。

举个例子，如果我们要依次输出列表中的每一个元素，就可以用下面的代码：

total = list(range(1,11))  # 定义一个1-10 的列表
for i in total:
    print(i)

# 输出结果
1
2
3
4
5
6
7
8
9
10

流程控制语句可以改变 Python 程序的执行顺序，而我们通常会根据实际的业务场景来控制某些语句块进行重复执行的。

使用函数库

Python 语言的强大之处，除了语法简单外，就在于它庞大的函数库了。

Python 的函数库有标准库和第三方库（也叫扩展库）。标准库是 Python 自带的，而扩展库是需要再进行安装的库。你可以从[Python 的官方文档]((https://docs.python.org/zh-cn/3.9/library/index.html) 和PyPI 网站中找到它们，其中，PyPI 网站的库就是扩展库。

PyPI网站上面的项目超过 28 万个，几乎包含了各个行业和各个功能的应用，实现了很多你日常需要手动操作的功能。

这些库如何是用呢？首先看一下官方自带的库。

如果要使用官方自带的库，可以使用 import 关键字导入。例如我计算算数平方根功能，就可以使用如下方法：

import math
math.sqrt(16)

import 后面指定要使用的库的名称，使用库的具体功能，通过官方文档来找到相应的函数。例如，算数平方根使用 sqrt 函数就可以实现你想要的功能了。

标准库的使用非常容易吧，那如何使用扩展库呢？由于扩展库都需要通过网络进行下载，因此我们还需要提前在终端使用 pip3 命令进行下载和安装。例如我希望 Python 可以支持 Word 文件格式, 我就需要在终端执行下面的代码。

pip3 install python-docx

下载和安装之后, 就可以像标准函数库一样使用 import 导入使用了。

总结

通过上面的内容，我为你简单介绍了一下入门 Python 需要掌握的五大部分，包括环境配置、变量、数据类型、控制语句和使用函数库的方法。这五个部分是 Python 最基础也是最必要的语法知识。希望你能通过我的介绍，对 Python 语言有所了解，消除对编程语言的畏惧心理。

最重要的是，即使今天这节课你没有完全懂，也没有关系。

一方面，我会在之后的课程中不断讲解每个部分的用法和具体使用场景。还是我在开篇词中说的学习方法，你需要在听我讲解的同时，动手操作，反复练习，这些语法知识就不会是太大的问题。

另一方面，我也希望你能在学习之后的课程时，如果遇到哪些关键语法知识不太明白，就回头来学习今天这节课。这节课相当于整个课程的基石，需要反复琢磨、反复学习。

如果你能跟着我的课程不断练习实操，那学完后，你都能够熟练地修改课程中涉及到的代码，并且还能根据自己的工作场景，自行编写提升工作效率的程序。

思考题

最后留一道思考题给你，你以前是如何学习编程语言的呢? 有没有好的学习方法分享一下？或者你在学习过程中有遇到什么困难，也可以分享出来。

如果你觉得这节课能帮助你提升办公效率，欢迎点击“请朋友读”，分享给你的朋友或同事。

开篇词 | 重复工作这么多，怎样才能提高工作效率？

你好，我是尹会生。欢迎你来到《Python 自动化办公实战课》，在接下来的 3 个月时间里，我们一起通过办公自动化来提升工作效率。

工作 15 年来，我在游戏公司做过技术总监，也在新浪网做过技术经理，负责公司运维平台建设和相关工具的开发工作，几乎每天都在和 Linux 打交道。

在这门课之前，我已经在极客时间开设了两门视频课程：《零基础学 Python》和《Linux 实战技能 100 讲》，为 Python 语言的初学者和运维工程师、开发工程师提供系统化的课程，目的就是解决他们在学习、实践中遇到的 80% 的问题。

目前，这两门课程已经有 3 万多名同学加入学习了。看到这些内容能够切实地帮助同学们解决他们的问题，我非常开心，也很受鼓舞。

所以，这次我又为你带来了《Python 自动化办公实战课》这门专栏课程。我总结了工作这些年来切身经历的、经常被问到的办公效率低下问题，以及对应的解决方案，我会把它们全部分享给你。

希望你学完这门课，能够告别重复、机械的工作，提高工作效率，同时也能把时间花在刀刃上，去提升自己的核心竞争力。

说到这里，你脑中是不是回忆起了一幕幕的低效工作场景：

需要对 Excel 文件中的内容进行拆分和合并，你要一个个手动拆分再合并；

文件夹里有大量图片需要重命名，你不得不挨个儿点击一张张图片，再重新命名；

网络中有大量新闻和图片需要下载，你就要手动把每个网页另存为文件；

需要把数据快速生成自己想要的图形，一个一个图形参数来设置；

需要让计算机在凌晨执行任务，你只能让闹钟在半夜把自己叫醒，人工执行，观察执行结果；

……

这些工作其实难度都不大，但它们有一个共同的特点，就是重复、机械的手工劳动，非常耗费时间和精力。实际上，我们换一种方法，就可以快速完成这些工作，节省数倍的时间。如你所想，这些都是我要通过这门课帮助你解决的问题。

那具体怎么解决呢？

解决这些低效问题，我的思路是什么？

其实，我们现在的办公环境，已经基本实现自动化了，Word、Excel、邮件、思维导图等各种各样的工具都非常顺手，表格的合并拆分、使用快捷键进行文件的各种操作等各种技巧也是随手一搜就能得到。但相应的问题也就来了，我们怎么用好这些工具、怎么内化这些技巧呢？

静下心来想一下，现在我们掌握的绝大多数办公工具和技巧，都是基于计算机的。那这个问题，就可以用计算机的思维从根儿上去解决了。所以，我们不妨引入《设计模式》的思路：代码封装得越“高级”，解决的问题就越具体；越深入计算机底层，解决的问题就越通用。

如果你暂时不理解这句话的意思也没关系，只需要明白这样一点就可以：要想快速提高办公效率，解决方法不在各种小技巧和小软件，而在于理解底层逻辑，以及加快人和计算机的交互过程，能够高效地解决输入（格式转换）、输出（格式统一）、控制（内容处理）、运算（查找、替换）、存储（文件保存和绘图），自然就能解决大部分的效率问题。

只要对计算机的体系结构稍有了解，你就会听说过冯 · 诺依曼结构：计算机由运算器、控制器、存储器、输入设备和输出设备这五部分组成。所以，我们解决用好自动化办公工具和技巧的方法，和经典的冯 · 诺依曼结构是相吻合的，就是理解计算机的底层逻辑，从而提升我们和计算机交互的效率。

说到这里，我可以给你分享一段我的工作经历。我曾经维护过日活超过 3 亿用户的微博私信平台，你可以看看我是怎么用计算机的思维，来提高自己和团队的工作效率的。

我们在一个业务模块中，需要批量替换 200 台服务器中的软件配置，而且每个服务器都有一个文件，需要将第五行内容, 由原有的接口版本 v1 统一替换成 v2。

面对这样的需求，其实有很多挑战在里面。第一个是替换的实效性，如果手动替换接口版本，由于服务器过多，用户就有可能访问到还没来得及替换的接口上，如后就有可能看到自己的消息是已读状态，一刷新页面，又变成了消息未读。第二个就是服务器数量很多，手动替换还没做完，下一个需求就接着来了。第三，手动替换这么多服务，非常容易出现拼写错误，也就是我们常说的手误，导致你要再花更多的时间来排捉 Bug。

这样很低效对不对？如果使用 Python 的话，我们就可以从 3 个方面来提升效率。

第一，用 Python 程序代替一个个的手动操作，实现文字内容的替换，这样就会解放人力，你的工作压力会减轻很多。

第二，我可以通过 Python 批量控制服务器，让服务器自动完成这些工作。

第三个就是灵活性方面的优化了, 我们可以让这段程序定时运行，又可以让它们能够同时运行，从一个一个执行，到五个五个执行。

这三方面的优化，我在课程中都会为你讲解到。

你可能会问，编程语言有那么多，比如 Java、Go 等等，我为什么要选择 Python 呢？或者说，为什么我会认为，Python 非常适合用来提升我们的工作效率呢？如果你不会 Python，可以学这门课吗？

不要着急，我来和你说下 Python 语言的几个特点，你就能明白了。

为什么选择用 Python 来解决？

首先，Python 的用法非常简洁、灵活，就像汉语、英语这类自然语言一样容易理解和使用。世界著名程序员、软件开源运动旗手埃里克·雷蒙（Eric S. Raymond）在《如何成为一名黑客》中说道：

如果你不懂任何计算机语言，我建议从 Python 入门。它设计整洁，文档良好，对初学者很友好。

Python 用法简单，但是对于初学者，尤其是没有 Python 基础的话，那该怎么来学这门课呢？其实这也是我在设计这门课时重点考虑的一个问题。所以不用担心，我为你准备好了 0 基础的学习路径。

第一，导读是整个课程的基石。导读部分我会为你讲解 Python 最基础且最重要的五大语法部分，让你对 Python 有一个初步的了解，能够看懂基本的 Python 代码。这也是你入门 Python 学习的第一步。

同时，导读在整个课程中会起到一个引领的作用。其中的语法知识我会在之后的课程中讲解，所以在学习时如果有不懂的话，仍然可以回过头来看导读部分。

第二，一定要动手来操作，毕竟看一遍我的操作，跟你自己操作是不一样的。

第三，如果你有时间的话，我还建议你去抄写每一段代码。要知道，看一遍课程和把看的东西写出来的感觉是完全不一样的。这个建议我在之前的《零基础学 Python》这门课中也提过，很多用户反映学习效果确实不错，提升速度很快。

问题都是在不断学习中解决掉的，所以最后我也建议你遇到问题不怕畏惧，跟着课程来慢慢学习就行了。

其次，Python 的扩展库很丰富，可以满足非常多的复杂场景的需求，能够替代非常多的手工操作。这也是 Python 相对于其他编程语言的一大优势。

目前，Python 的扩展库已经覆盖了文件、声音、视频、数据科学、深度学习等众多行业。通过这些扩展库，我们可以用 Python 连接 Excel、Word、邮件等常用办公组件，轻松应对各类工作场景，不用自己手动写很多功能代码了。而连接方法也很简单，以操作 Word 文档为例，你只要掌握下面这行代码就可以了：

import  docx

通过这样一行简单的代码，你的 Python 就能支持 Word 中的文字、字体、段落、样式、表格等各种功能进行读写操作了，当然了，为了实现你的各种定制需求，还需要继续编写代码（这里的优点是直接支持，其他语言是要先像是操作压缩文件一样，对 docx 格式解压缩，再对 xml 文件进行处理，非常复杂）。

最后，Python 还有一个你无法抗拒的优点，那就是它的跨平台性。

也许有一天，你用的电脑的操作系统，从 macOS 变成了 Windows，或者从 Windows 变成了 macOS，那也完全不用担心。Python 的跨平台特性，可以帮你做到不用修改任何一行代码，就可以让已经写好的程序直接在新的平台上运行。

总结来说，Python 的简洁、扩展库丰富和跨平台特性这三点，就保证了你可以轻松学会这门课，用 Python 去实现办公自动化，提升自己的工作效率。

说了这么多，哪些工作可以用 Python 实现自动化，而我又会怎么给你讲这门课呢？

这门课是怎么设计的？

在开头的时候我也说了，要用计算机的思维去解决办公自动化工具和技巧的问题，所以我就把常见的 30 个机械、重复的工作场景，按照任务类型划分成了输入、运算、控制、存储和输出这 5 个模块。

“输入”模块：解决不同文件类型的批量合并和拆分问题

这类任务往往包含了格式相似的大量文件，比如 Word、Excel、Txt 文件，我会带着你用 Python 去进行批量合并和拆分。

“运算”模块：扩展常用的统计、搜索和排序功能

很多软件自带的统计、搜索和排序功能，都很好用，但不支持在多个文件或者跨类型文件中使用。所以，在这个模块我们要学习的就是，怎么通过 Python 进行扩展，让这些好用、常用的功能，可以支持多个文件或不同类型的文件。

“控制”模块：通过插件的方式增强办公软件以及周边软件、硬件的交互能力

办公软件的核心功能，通常是支持文字和表格等内容的相关操作，对控制外部设备相对较薄弱。例如，Word 本身是不支持批量打印 Word 文件的，但批量打印又是一个常见的需求。这个需求，就可以通过脚本化来实现，达到打印自动化的目的。

“存储”模块：和文件相关的很多常用操作部分

在工作中，我们经常会面对这么几种需求：需要对大量文件进行重命名；需要通过网络批量下载视频和图片；需要在海量文件中快速找到自己想要的文件；等等。

这些需求最大的问题，就是我们需要手工重复操作，或者自带工具不好用。那么利用 Python 和文件、网络功能相结合，就完全可以实现目录下的批量改名、文件的批量下载，免去了手工重复操作的问题。

对于系统自带的文件查找工具来说，速度慢而且不够简洁，那我们可以使用 Python 根据自己定义的目录搜索，加快搜索文件的效率。

“输出”模块：智能化输出自己的工作成果

在这一部分，我要教你更直观和更智能地输出自己的工作成果。比如说你交付给同事的数据，可以通过 Python 一键转为图形，也可以根据你的需要将图形采用图片或网页的形式展示给你的同事，提高工作汇报的效率，更直观地展示自己的工作成果。

在讲解这 5 个模块、30 个常见的办公场景的效率提升方法时，我还会带你剖析它们背后的原理，和你展示我分解任务、解决问题的思路。

因为提升办公效率的方法和技巧真的是非常非常多，但如果我们没能把这些方法和技巧做系统的梳理，那它们就是孤立的存在，知道得再多，也没办法根据自己的工作场景灵活运用。

如果到这里，你还是担心自己不会 Python 就学不会这门课、就没办法提升自己工作效率的话，我还可以再给你吃一颗定心丸，这也是我额外送你的一份礼物：在每节课的最后，我都会给你交付一个可运行的小程序。你只需要调整保存路径和几个简单的参数，就可以在自己的电脑上运行，就可以去应对你实际工作中相似场景的重复工作，提升自己的办公效率。

所以，不但能用 Python 让你的工作效率翻无数倍，还可以收获编程思维、解决问题的思路，以及 30 个小程序。

最后，我们来一起立个 Flag 吧：通过三个月的学习，让自己成为 10x 职场人！也欢迎你邀请你的小伙伴们一起来学习，共同成长！

01 | 拆分与合并：如何快速地批量处理内容相似的Excel？

你好，我是尹会生。今天是咱们的第一节课，我先带你学习下，如何用 Python 操作 Excel。

Excel 是我们在工作中用到的最频繁的软件之一，它有着强大的计算能力和便捷的图表功能。如果我们要在同一个 Excel 文件中进行操作，手工进行也很方便，但问题是，如果我们需要同时操作多个 Excel 文件，就是一件非常耗时的事情了。

在工作场景中，需要同时操作多个 Excel 的情况主要有 2 种：批量合并和批量拆分。我来带你看 2 个场景。

批量合并。假设你需要对某些工作内容进行问卷调查，这时你用 Excel 做了调查问卷模版。我想你会这样做：先把 Excel 通过工作群分发给所有员工，再把群里收集到的反馈附件汇总成一个文件。

批量拆分。假设你是公司的财务人员，你需要使用 Excel 对员工工资进行核算，之后再打印出来。但是公司要求员工薪水保密，所以每个员工的工资需要拆分成一个独立的文件，最后还需要打印出来。

无论是合并，还是拆分，我们都面临着一个困境：没有现成的软件可以实现多个 Excel 文件的合并和拆分操作，所以你只好对每一个 Excel 文件都进行“打开 - 复制粘贴 - 保存”的工作。

很多人在面对这样的工作需求时，都忍不住立马去做，却很少停下来分析问题。其实，这三步是很简单的工作，不过也是无意义的重复工作，既浪费了时间，又没有真正产生价值。

幸运的是，这些工作都可以通过 Python 来解决。今天，我就给你介绍一下用 Python 实现重复工作自动化，快速实现 Excel 的合并和拆分的方法。

如何用 Python 手工操作一个 Excel 文件？

刚刚我们说到批量处理，其实也就是逐一处理多个文件。如果我们想要提升这类工作的效率，就可以先借助 Python 把每一次处理都自动化。所以，在讲具体的合并和拆分方法前，我们要解决的第一个问题，就是用 Python 代替你的双手来操作一个 Excel 文件。

如果要用 Python 操作 Excel 文件，首先就要支持读写 Excel 的功能。在 Python 中，要想实现对某一个功能的支持，就需要安装扩展库。

支持 Excel 读取的扩展库叫做 xlrd 库，支持 Excel 写入的扩展库叫做 xlwt 库。我们可以使用下面的命令行进行安装：

pip3 install xlrd
pip3 install xlwt

那么，如何使用 Python 读取 Excel 文件的内容呢？你可以使用这样的代码实现：

import xlrd

file = '/Users/user1/Desktop/a.xlsx'

data = xlrd.open_workbook(file)
table = data.sheets()[0]
value = table.cell_value(rowx=4, colx=4)

虽然代码很简单，但是它对自动化操作 Excel 的意义非常大。

试想一下，如果能够使用 Python 替代全部的手工操作，大批量的文件就可以使用 Python 的循环功能自动化完成对每一个文件的自动处理工作了。

对于编程语言来说，文件合并的步骤可以分解为读取第一个文件，读取第二个文件，将第一个文件的内容追加到第二个文件下方。

所以在我们学会使用 Python 读取 Excel 文件之后，我们还需要掌握如何将读取的内容写入到 Excel 文件。写入文件的代码如下：

import xlwt

dst_file = '/Users/edz/Desktop/文章1/result/结果.xlsx'

workbook = xlwt.Workbook(encoding='utf-8')
xlsheet = workbook.add_sheet("统计结果")

# 写入内容,假设取出的内容是value
xlsheet.write(0, 0, value)

# 保存文件
workbook.save(dst_file)

可以看到，写入文件的时候，我们使用了一个叫做 write 的函数。它的前两个参数代表的写入位置，分别是指定写入的行和列坐标。无需多言，这个写入位置非常重要。如果按照上面的代码方式写入，也就是前两个参数均指定为 0，就会覆盖这个 Excel 文件中的现有内容了。

所以，你如果想完成合并操作的话，就要实现对现有 Excel 内容进行追加写入。通常我们会先获取现有的内容一共有多少行、多少列，然后向后移动一个位置，再进行写入。

这种追加写入的方式，在我们的工作场景中非常常见。接下来，我们就看看怎么用追加写入的方式，实现多个 Excel 文件的合并吧。

怎样实现 Excel 的合并？

我们还是用前面提到的做调查问卷模板的场景，来具体讲一讲怎么实现 Excel 的合并。这里，我们就要用到一个重要功能了：循环功能。

循环功能的核心代码是：

from pathlib import Path, PurePath

# 指定要合并excel的路径
src_path = '/Users/edz/Desktop/文章1/调查问卷'

# 取得该目录下所有的xlsx格式文件
p = Path(src_path)
files = [x for x in p.iterdir() if PurePath(x).match('*.xlsx')]

在这段代码中，我使用了 for 语句，实现了 Python 的循环功能。通过这样的功能，我可以依次获取 src_path 变量指向的路径下所有的文件。同时，为了避免这个目录里的文件类型过多，我使用一个 if 语句用于条件判断，只提取.xlsx 结尾的文件。

现在，用 Excel 实现调查问卷自动化的主要功能已经都实现了。接下来，我们看看怎样实现整个工作过程。我把它们的工作流程定义为三个步骤：

找到整个工作过程当中重复操作的部分；

将重复操作的部分需要哪些手工操作找出来，使用 Python 编写程序代替手工操作的部分；

对重复的部分，使用循环语句进行批量处理。

如果你对把手工操作改成 Python 程序，还没有任何经验的话，我还可以为你提供更直观的办法，叫做画时序图。

我先带你看看时序图是什么呢？如下图表:

简单来讲，时序图就是以时间顺序来排列程序中的事件的图表。通过上图，你应该很容易看出重复操作的这四个事件：

打开文件；

提取用户填写内容；

粘贴到汇总文件；

关闭文件。

下面，我们就用 Python 来替代这四个事件。由于它们是重复的，所以我会使用 for 循环对它们依次进行处理。

回到我们的 Excel 做调查问卷的场景。当我们回收了调查问卷之后，每份问卷的格式是完全相同的，刚好可以利用上面提到的循环功能处理每份问卷。而问卷的选项则是我们需要提取出来用于汇总的，所以我们要使用 Python 实现读取 Excel 调查问卷的功能，最后再写入到一个新的 Excel 中。

好了，我们来看下这一功能的 Python 代码是如何编写的：

import xlrd
import xlwt
from pathlib import Path, PurePath
# 导入excel和文件操作库

# 指定要合并excel的路径
src_path = '/Users/edz/Desktop/文章1/调查问卷'
# 指定合并完成的路径
dst_file = '/Users/edz/Desktop/文章1/result/结果.xlsx'

# 取得该目录下所有的xlsx格式文件
p = Path(src_path)
files = [x for x in p.iterdir() if PurePath(x).match('*.xlsx')]

# 准备一个列表存放读取结果
content = []

# 对每一个文件进行重复处理
for file in files:
    # 用文件名作为每个用户的标识
    username = file.stem
    data = xlrd.open_workbook(file)
    table = data.sheets()[0]
    # 取得每一项的结果
    answer1 = table.cell_value(rowx=4, colx=4)
    answer2 = table.cell_value(rowx=10, colx=4)
    temp = f'{username},{answer1},{answer2}'
    # 合并为一行先存储起来
    content.append(temp.split(','))
    print(temp)
    # 输出
    # 韩梅梅,D,B
    # 李雷,D,C

# 准备写入文件的表头
table_header = ['员工姓名', '第一题', '第二题']

workbook = xlwt.Workbook(encoding='utf-8')
xlsheet = workbook.add_sheet("统计结果")

# 写入表头
row = 0
col = 0
for cell_header in table_header:
    xlsheet.write(row, col, cell_header)
    col += 1

# 向下移动一行
row += 1
# 取出每一行内容
for line in content:
    col = 0
    # 取出每个单元格内容
    for cell in line:
        # 写入内容
        xlsheet.write(row, col, cell)
        # 向右移动一个单元格
        col += 1
    # 向下移动一行
    row += 1
# 保存最终结果
workbook.save(dst_file)

在这段代码中，Excel 的读取和写入操作、for 循环操作都派上了用场，它的整个工作过程就像我画的时序图一样：先打开用来汇总的 Excel 文件，依次对多个调查问卷进行读取，最后逐行写入到新建立的汇总文件中。

合并后的效果如下图：

有一点需要你注意的是，为了让你更好地理解 Python 循环的工作过程，我没有严格遵守编码规范，而是尽可能地让程序按照从上到下的顺序执行。如果你已经不是第一次使用 Python，可以根据你的使用习惯，将程序进一步封装成 Python 里的函数，更加优雅地去实现它。

总的来说，在使用 Python 对 Excel 的合并操作时，需要你掌握的最核心的操作就是读写文件和行列坐标控制。熟练掌握这两个功能，你才能一次得到想要合并成的样子，不用再对 Excel 进行二次的手工操作。

怎样实现 Excel 的拆分？

对于批量操作 Excel，还有一种情况是批量拆分。比如很多公司会用 Excel 记录和统计员工的薪水、记录货物信息、记录客户情况等数据。这些数据越来越多之后，文件会越来越大，打开文件和查找速度就会变得很慢，最后只好按照某些列进行 Excel 的拆分。

接下来，我就为你讲解一下如何进行 Excel 的批量拆分。让我们来看一个工资条的案例。

例如我在一个 Excel 中存放了工资信息，需要把第一行的表头和员工工资拆分成一个以员工名字命名的 Excel 文件。我来带你看下具体该怎么操作：

如果把拆分工作也画成时序图，就会发现，逐行读取可以使用循环功能批量操作，对每一行的内容处理，如果能使用 Python 进行自动化的话，一个 Excel 拆分的工作就全部能使用 Python 自动化实现了。所以，我打算设计一个 for 循环语句用于遍历所有的行，在 for 循环语句当中实现对每一行具体内容的处理。

我把文件拆分的关键代码写了出来，你可以参考一下：

for line in range(1,employee_number):
    content = table.row_values(rowx=line, start_colx=0, end_colx=None)
    # 将表头和员工数量重新组成一个新的文件
    new_content = []
    # 增加表头到要写入的内容中
    new_content.append(salary_header)
    # 增加员工工资到要写入的内容中
    new_content.append(content)
    # 调用自定义函数write_to_file()写入新的文件
    write_to_file(filename = content[1], cnt = new_content)

在这段代码的第一行，我使用了一个 range 函数，它会生成从 1 到员工总数的数字范围。你可能会问，为什么没有直接写出 Excel 中总员工的数量，而是使用 employee_number 这样一个变量呢？

这是因为，如果直接写出员工数量，一旦遇到员工入职或员工离职等情况，你就需要根据 Excel 中的行数重新编写 Python 代码，而我现在使用的方式是每次打开 Excel 文件，会自动统计员工的数量（即行数），这种编写代码的方式能够让你的程序有更好的扩展性，一般这种方式用于处理文件内容经常变动的情况。

文件的批量拆分也是通过循环来实现逐行处理的功能的，但是你需要注意拆分以后的要保存的文件名称不要重复，不然很容易导致 Excel 中只有最后一次循环写入的内容。

小结

今天，我为你讲解了如何使用 Python 代替手工进行 Excel 的批量合并和拆分操作。一般来说，批量合并和拆分主要有几个步骤：

手动或借助时序图找到需要重复操作的部分。

将重复的部分用 Python 实现自动化操作。

对 Python 实现自动化的脚本, 再用循环功能实现批量操作。

Python 有着丰富的扩展库，当你掌握了熟练操作 Excel 的方法之后，对于 WPS 等其他办公软件，也可以通过相应的 API 进行类似的合并和拆分操作。

除了基本操作呢，我还给你介绍了分析问题的方法——时序图。通过时序图，你可以找到那些运行逻辑中重复的部分，利用循环完成自动化操作。

不管是利用 Python 代替手工操作节约时间，还是使用循环代替手工多次执行减少工作难度，这些都是提升工作效率的有效办法。希望你能在处理日常工作时多思考，有意识地把你的日常办公工作自动化。

思考题

欢迎你在课程后留言，告诉我使用 Python 解决了你工作中的哪些重复性问题。

如果你觉得这节课有用，能解决你的办公效率问题，欢迎你点击“请朋友读”，分享给你的朋友或同事。

02｜善用Python扩展库：如何批量合并多个文档？

你好，我是尹会生。

在日常工作中，我们打交道最多的文件就要数 Word 和 Excel 了。我们经常面临这么一种场景：需要将 Excel 的内容合并到 Word 中。你可以想一想，完成这个需求，需要手动进行几个步骤的操作呢？很显然，有 4 步。

首先，要手动打开 Excel、Word 文件；

接着，复制一个单元格的文字到 Word 指定位置；

然后，如果有多个单元格，就需要重复复制多次；

最后，保存 Word 文件，并关闭 Excel 和 Word 文件。

如果只有两个文件，这几步手动操作一定不成问题，不会耗费太多的时间。但是如果文件特别多，哪怕只有十几个，手动操作就相当耗费时间了，而且一不小心还容易出错。幸运的是，现在我们可以通过 Python 来实现批量文件合并功能，你只需要执行一个 Python 程序就能搞定所有文件的合并操作。

所以今天这节课，我们先从比较简单的内容讲起：用 Python 自动合并两个 Word 文件。然后再进阶，学习如何合并 Word 和其他类型的文件。一步一步来，相信你会掌握得既牢固又扎实。

手工操作和用 Python 操作的区别

首先我们要知道，为什么在合并文件的时候用 Python 更高效。我用一个例子来给你讲解手工操作和用 Python 操作的区别。比如下面这一段文字：

简易流程——集团原则在每年 1～5 月、7～11 月生产工作任务较重或考核时间较紧的情况下运用。

在 Word 文件中，不但有文字内容，还有加粗、红色等格式，而且这些特殊的格式和文字内容是混合在一起的。

但如果用 Python 来读取 Word 文件，这段文字会被分为纯文字、段落、字体、字号以及表格等更具体的部分，而且每一个部分都对应着 Python 的变量和函数。

这样一来，你就可以根据自己的需求，只提取某一具体部分的内容。比如说，你看到哪一段文字的字体很好看，就可以直接读取之后套用到新的文字段落就行，非常便捷。

不过，用 Python 读取文件时，你需要记住很多个 Python 变量和函数。当然了，针对这一点，你也不用担心，这些变量和函数在 Python 的 Word 扩展库官方文档可以查看。所以如果你需要某个功能，但是不知道应该用什么变量和函数名称时，可以在官方文档中找到它的名字和描述信息。

总之，用 Python 读取文件的方式是非常有助于提高工作效率的。所以接下来我们用 Python 处理 Word 文件时，就需要通过刚才介绍的变量和函数来替代手动操作。

接下来，我先带你学习怎样用 Python 合并多个 Word 文件，然后再讲怎么把 Word 文件与纯文本、图片和 Excel 进行合并。

如何合并多个 Word 文件，只保留文字内容？

先从合并两个 Word 文件说起。假设你现在有两个 Word 文件，需要进行文件中的文字合并操作。两个文件的内容分别是:

文件一（内容包含字体、字号、颜色等额外信息）：

1. 简易流程——集团原则在每年 1～5 月、7～11 月生产工作任务较重或考核时间较紧的情况下运用。

文件二（内容文字出现在表格中）：

我把两个文件的信息总结如下：

第一个文件中，字体使用了黑体和宋体字，此外还有红色字体和加粗等格式。现在我需要只提取其中的文字内容，不带任何格式。

第二个文件中，文字被放在了一张表格里。现在我需要用 Python 把表格中的文字提取出来，合并成一个新的文件。

我先把核心代码给你贴出来，然后再给你详细讲解具体的操作方法。

首先，我们可以使用一段 Python 代码提取 Word 文件里的内容，然后合并成一个文件。

import docx
def merge_without_format(docx_files: list):
    '''
    只获取内容进行合并
    '''
    # 遍历每个文件
    for docx_file in sorted(docx_files):
        another_doc = Document(docx_file)
        # 获取每个文件的所有“段落”
        paras = another_doc.paragraphs
        # 获取所有段落的文字内容
        # paras_content = [para.text for para in paras]
        for para in paras:
            # 为新的word文件创建一个新段落
            newpar = doc.add_paragraph('')
            # 将提取的内容写入新的文本段落中
            newpar.add_run(para.text)

    # 所有文件合并完成后在指定路径进行保存
    doc.save(Path(word_files_path, 'new.docx'))

# 调用函数
merge_without_format(files)

在这段代码中你可以看到，我使用了一个 Python 的扩展库，它叫做 python-docx，这也是我想重点给你讲解的一个扩展库。

python-docx 是 Python 专门用来编辑 Word 文档的库，我在实现 Word 文档自动化操作的工作中经常会用到它。使用它的好处就是不必自己研究 docx 文件类型的底层实现细节，你可以像操作.txt 文本一样直接打开、修改和保存关闭文件。可以说，python-docx 扩展库降低了用户使用 Python 的复杂度。

我再举个例子展示一下具体的操作过程。例如 python-docx 库支持函数 Document，它实现了 Word 文件的打开功能，底层也做了很多对 Word 格式的处理工作，让你可以直接使用 paragraphs 变量就能读取一整段 Word 文件。

Document 函数格式如下：

Document(docx_file)

还有，函数 save 也是 python-docx 扩展库提供的 word 文件保存函数。同样的，save 函数在底层也做了很多对 docx 格式兼容的操作。像下面的代码一样，你就可以直接给这个函数传递一个文件路径，然后进行保存。是不是降低了编写代码的难度呢?

doc.save(Path(word_files_path, 'new.docx'))

通过我举的例子，你就能更直观地感受到 Python 扩展库的方便之处了。接下来我们再回到刚才那两个文件的合并，合并之后的结果如下：

1.简易流程——集团原则在每年1-5月、7-11月生产工作任务较重或考核时间较紧的情况下运用。
人力资源、生产、品管、财务等部门整理、提供绩效考核数据。
人力资源部门收集各部门提供的考核数据，依据员工绩效考核评分标准对集团所有员工进行绩效考核得分计算。
人力资源部门将核计的员工绩效考核结果提交部门经理确认后报集团主管领导核定。

现在你已经掌握了两个 Word 文件的合并方法了。但如果我想让这段程序适用于三个、四个，甚至更多个 Word 文件的合并，那该怎么操作呢?

一个好消息就是，上面的代码我们不需要做任何修改，就可以合并多个 Word 文件。因为我使用了一个叫做函数的功能。函数有时候也被称作过程、方法，它的作用是将那些需要反复使用的代码组合在一起。

之前我们使用过函数，这些函数是 Python 自带的，或是扩展库提供的。这些函数我们可以直接拿来使用，使用函数在计算机术语中被称作函数调用。通过函数我们可以实现程序的模块化，多次使用可以多次调用，从而减少代码的重复性。但如果你需要自己编写函数怎么办呢?

你可以将重复的代码功能写在自己定义的函数中，在需要使用的地方调用就可以了。这种自己编写的函数就被称作自定义函数。自定义函数和 Python 自带的函数一样，也可以实现减少代码重复性的作用。

关于自定义函数，你需要熟悉它们的相关语法，主要是函数名、函数定义和调用方法。通常编写一个函数要为函数并起一个名字，这个名字叫做函数名。当你需要使用函数的功能时，可以使用函数名加“()”的方式来使用它，而且使用一个函数一般被称作调用函数。

我把函数定义和函数调用的写法单独拿出来给你看下，定义函数的格式是 def 后面跟着函数名称，调用函数是函数名称后面跟着一个"()" ，这是它的语法格式：

# 定义一个函数
def 函数名(参数列表):
    函数体
# 调用一个函数
函数名(参数)

知道了自定义函数的语法，接下来我们就可以在程序中使用自定义函数了。

举个简单的例子。像我在合并 Word 文件的程序中，第 2 行的 merge_without_format 就是我定义的一个函数，第 24 行 merge_without_format(files) 就是对函数进行调用，files 叫做函数的参数。通过函数参数，可以在调用函数的时候为函数指定要操作的对象。

相信你不难发现，使用函数以后，不但可以提高代码的重复利用率，还能提高代码的可读性。

那这段程序是怎样处理多个文件合并的呢? 我在调用函数 merge_without_format 时，使用了 files 变量作为参数，而 files 变量包含了大量的文件。因为是多个文件合并，所以在函数中我使用了一个小技巧，就是你熟悉的 for 循环语句，for 循环语句能够遍历 files 变量的值，这样就可以将 files 指向的全部 Word 文件逐一进行文件内容的提取，进行两两合并，从而实现任意多个 Word 文件的合并操作。

通过对多个 word 合并，我希望你能学会怎么提取 Word 中的文字内容，如果你需要编写大量重复的代码，可以将它们写成自己定义的函数。

怎样合并不同类型的文件？

通过上面的例子，我们实现了 Word 文件之间的合并。在工作中，我们经常需要处理 Word 和 Txt 文件、图片、Excel 这些类型合并的情况，又该如何操作呢？接下来，我就一个一个来讲一讲。

将纯文本和 Word 文件合并

如果是为了支持信息丰富，我们 Word 和 Txt 合并之后保存到新的 Word 中，会出现 Txt 里的字体字号和原有文件不统一的问题，我们可以使用 python-docx 扩展库为 Txt 文件中的文字增加格式。

如果合并前 Word 文件是仿宋字体，而且有下划线和红色字体，我们将 Txt 合并之后如何进行字体、样式和颜色的统一呢？我们可以使用下面这段代码。

def add_content_mode1(content):
    '''
    增加内容
    '''
    para = doc.add_paragraph().add_run(content)
    # 设置字体格式
    para.font.name = '仿宋'
    # 设置下划线
    para.font.underline = True
    # 设置颜色
    para.font.color.rgb = RGBColor(255,128,128)

首先，我定义了一个叫做 add_content_mode1 的函数。考虑到 Word 合并 Txt 是否会有多个 Txt 进行合并操作，所以我使用自定义函数功能。

当你需要对多个 Txt 进行合并，就调用函数依次对它们进行处理，这样你就不用编写重复的代码了，这也是我在编写代码时进行提效的一个小技巧。

接下来，我们将每个新合并的 txt 内容作为一个新的段落合并到原有的文字中，这个功能使用 python-docx 的 add_paragraph 函数就可以增加了一个新的段落。

最后，把这一段所有文字设置成和原有的 Word 统一的字体、下划线和颜色，保证新的段落在格式上的统一。

在具体操作的时候，我还要提醒你，Word 文件支持的格式丰富程度远远高于 Txt 文件，所以当这两种格式丰富程度不一致的文件进行合并时，要么向下兼容，去掉 Txt 不支持的格式；要么向上兼容，对 Txt 进行格式再调整。否则容易出现合并之后仍需要手动调整格式的问题，影响工作效率。

将图片和 Word 文件合并

我们再来看一下第 2 种情况，怎么把图片和 Word 文件进行合并呢?

想一下，我们经常见到的图片格式就有.jpg、.png、.gif 等，由于这些格式应用范围广，格式没有被商业软件加密，所以 python-docx 库的 add_picture 函数就能实现把图片插入 Word 的功能。代码如下：

from docx import Document
from docx import shared

doc = Document()
# 按英寸设置宽度,添加图片
doc.add_picture('test.jpg', width=shared.Inches(1))

那有没有被商业保护、不能直接支持的格式呢？比如 Pohotshop 自带的.ps 格式，我们如果将.ps 格式插入 Word 文档，.ps 格式不能被 add_picture 所支持，就只能以附件的形式添加到 Word 文件中，作为附件添加的文件无法直接展示图片的内容，和 add_picture 相比不够直观。

所以如果不需要进行内容的加密等商业目的的时候，建议使用通用和公开格式，这些格式对编程语言的兼容性更好。

总的来说，python-docx 的功能非常强大，除了将文本和图片合并到 Word 文件中，还可以和第一节课我们学过的 xlrd 扩展库相配合，将 Excel 和 Word 进行合并。

将 Excel 和 Word 文件合并

为了让你更好地理解如何进行 Word 和 Excel 文件的合并，我用一个利用 Excel 和 Word 批量制作邀请函的例子来给你讲解。

我在 Word 中保存了邀请函的标准公文格式，但是其中的被邀请人、性别（先生、女士）以及发出邀请的时间，分别用“< 姓名 >”“< 性别 >”“< 时间 >”替代。邀请函格式如下：

尊敬的 < 姓名 > < 性别 >：

... 邀请函内容 ...

< 今天日期 >

我在 Excel 的每一行中写了被邀请人的姓名、性别信息。格式如下：

现在，我们需要将 Excel 和 Word 进行合并操作，为每个被邀请人自动生成一个 Word 格式的邀请函。

虽然 Word 中自带的邮件功能可以批量制作邀请函，但是在灵活性还是较差的。比如我要在邀请函制作完成的时候自动添加制作时间等功能，就无法通过 Word 自带的邮件功能实现。接下来我就用 Python 来生成邀请函，代码如下：

def generat_invitation():
    '''
    生成邀请函文件
    '''
    doc = Document(invitation)
    # 取出每一段
    for para in doc.paragraphs:
        for key, value in replace_content.items():
            if key in para.text:
                # 逐个关键字进行替换
                para.text = para.text.replace(key, value)

    file_name = PurePath(invitation_path).with_name(replace_content['<姓名>']).with_suffix('.docx')
    doc.save(file_name)

对于这个问题，我是这样思考的。如果手动操作，我需要：

先将 Excel 中的每一行中的姓名、性别填入 Word 文件中；

再将当前日期填入到 Word 文件中；

最后再按照姓名另存为一个文件。

但如果使用 Python 来实现呢？就会非常简单。首先在整个过程中，Word 文档是被反复使用到的，所以对 Word 文档进行修改的这个动作，我会将它写入到循环语句当中。

接着，我需要一个循环语句来处理 Excel 里的每一行循环，因为我们需要把 Excel 的每一行读取出来，然后替换“< 姓名 >”“< 性别 >”。

最后我们要解决的就是替换问题了。python-docx 功能非常强大，它自带了替换函数 --replace 函数，能够将"< 姓名 >""< 性别 >"替换成 Excel 真实的用户和性别。

我再用代码解释一下。对应上面的代码：

第 7 行的 for 循环实现了遍历每个段落功能，para 变量就是表示每个段落的变量。

第 8 行我们将 excel 提前处理为 python 的基础类型 -- 字典（链接）， for 循环实现了姓名、性别的遍历。

第 11 行实现了内容的替换功能。

第 12 行我将姓名作为文件名称，将.docx 作为扩展名指定为新的文件名称，通过第 13 行的 save 函数进行了邀请函的保存。

你看，多次读取 Word 文件的循环、多次按行读取 Excel 文件的循环、替换的函数都有了，那我们就可以实现自动化生成邀请函的功能了。

最终每张邀请函实现的效果如下图：

如你所见，我们在对不同类型文件进行合并时，要考虑不同的问题：

对于支持格式丰富不同的文件时要考虑格式的兼容性；

对于图片、音乐、视频和 Word 合并时要考虑是否是受到 word 支持的通用格式；

对于像 Excel 格式于 Word 合并时能实现更复杂的功能，代码的复杂程度也会随之提高，一般需要先分析功能，再进行代码编写。

小结

通过上面对 Word 文件的批量处理，我为你总结了 Word 和各种类型合并增效的几个通用法则。

首先，尽量选择 Word 兼容的格式, 这些格式往往也是 python-docx 库能直接支持的类型。

第二，善于将手工操作转换为 Python 程序实现。如果无法直接转换为 Python 程序，可以尝试将手工操作继续细化拆分。

第三，反复在程序中出现的代码可以编写为函数功能，函数可以让你的程序更健壮，较短的代码数量也减少了出现 Bug 的机率。

思考题

在最后我也想留一个问题给你思考，如果邀请函的格式从 Word 文件改为图片，你将会如何去解决呢?

如果你觉得这节课有用，能解决你的办公效率问题，欢迎你点击“请朋友读”，分享给你的朋友或同事。

03｜图片转文字：如何提高识别准确率？

你好，我是尹会生。

不知道你有没有遇见过这样的场景：在工作中，你遇见了一个紧急情况，对方给你发了一串儿聊天记录的长截图，当你处理完事情想要复盘的时候，必须要把这些记录处理成文字，然后就发现图片转文字过程中会出现很多问题，还需要自己二次手动校对。

经过不断尝试，就发现用互联网上的 AI 产品可以非常准确地识别出图片中的印刷体文字。而且再通过 Python 还可以实现把识别到的文字进行格式处理、文件保存等自动化操作。

那么今天，我就给你介绍两种能够精准、快速地把图片转成文字的方式：在线识别和离线识别。我会给你讲解具体的操作方法，当你再遇见这样的需求的时候，就可以很轻松应对了。

图片转文字的两种处理方法

我先来对图片转文字的两种处理方法进行介绍。

目前能够达到较高文字识别正确率的一般分为两种识别方式：一种是文字识别工作都需要在网络侧完成的方式，我们称为在线识别；另一种是不需要互联网功能的，我们称作离线识别。

根据不同的工作场景，我会选择不同的方式实现文字识别。那么，接下来我就带你了解下这两种方式各自的特性。

先看第一种，在线识别的方式。

在线识别方式最大的优点就是，它在初次进行文字识别的时候，准确率非常高。比如对聊天截图中的识别准确率就高达 99%。因为在线识别使用了人工智能领域的深度学习算法和文字识别相结合的技术，能够把图片转换成文字后，还能在语义上把相近的字进行二次纠正。

比如说，被识别的内容包含英文单词“Hello”，一旦它的字母“o”被识别成数字“0”，在线识别软件就会根据上下文语境把这类错误纠正回来，而这种二次纠正的功能在离线识别软件中是没有的。

不过在线识别软件也有它的缺点，那就是识别文字的过程需要在公有云的服务器上完成。也就是说需要通过互联网把图片上传到服务器，那么一旦图片过大，或者图片数量比较多，就会导致上传时间过长。我们知道，一张高清图片至少有 3MB 大小，根据个人的网络情况至少要达到秒级上传才行。这就意味着在大批量文字识别的场景中，或对实时性要求很高的场景下，在线识别是不能满足要求的。

另外，图片需要经过互联网传输，识别以后的图片该怎么保存，怎么销毁，是不是会被其他人得到，这些都是安全风险。总之，信息泄露的风险比较大。所以像公司的合同、财务资料等涉密程度比较高的扫描件，很少使用在线识别。

再看第二种，离线识别的方式。

这种方式在识别过程中不需要连接网络，节省了在线传输图片的时间，适合那些对实时性要求比较高或网络信号比较差的场景。

但是离线识别方式的问题就在于，初次识别文字的准确率比较低，识别完之后必须要经过人工二次纠正才行。所以在前期人工校对，花费的时间相对来说会比较长。

我把它们各自的优缺点做了一张表格，如下：

那接下来，我就带你学习一下这两种识别方式怎样具体实现。

怎么进行在线文字识别？

我们先来看怎么进行在线文字识别。

在线文字识别方式，识别的主要功能的需要放在公有云的服务器中才能实现，所以在代码实现中就要考虑用户验证和图片加密传输问题。

用户验证能确保识别的结果交还给你本人，图片加密传输能确保图片上的信息不会被其他人窃取到。这些功能，各个公有云的 AI 产品都考虑得非常周全，一般会提供给用户一个扩展库。你要做的，就是安装这些扩展库。

举个例子，百度云的 AI 产品，你可以在终端下执行这样一个命令来进行安装。

pip install baidu-aip

在这里我使用了百度云提供的在线文字识别产品，它提供了一个 baidu-aip 的安装包，安装之后提供了 AipOcr 函数实现用户验证、client.basicGeneral 函数实现文字识别功能。代码如下：

from aip import AipOcr
""" 你的 APPID AK SK """
# APP_ID = '你的 App ID'
# API_KEY = '你的 Api Key'
# SECRET_KEY = '你的 Secret Key'

client = AipOcr(APP_ID, API_KEY, SECRET_KEY)
""" 读取图片 """
def get_file_content(filePath):
    with open(filePath, 'rb') as fp:
        return fp.read()
image = get_file_content('example.png')
""" 调用通用文字识别, 图片参数为本地图片 """
result = client.basicGeneral(image)
print(result)

在这段代码里，实现了三个功能，分别是用户验证、读取图片和识别图片。为了让你更直观地看到效果，我来对一张电子发票的图片 example.png 进行识别。识别结果如下：

其实识别的准确率是非常高的，基本上没有错误。那接下来我具体给你介绍一下核心代码。

在代码的第一行，我使用了一个 AipOcr 库。AipOcr 是百度云提供给用户的 OCR Python SDK 客户端，能够让你用 Python 语言和百度云进行交互。

一般情况下，我们进行用户认证、图片上传至服务器功能，都需要自己编写很多代码，但是使用 AipOcr 库之后，这些基础功能都被封装好了。你只需要填写三个变量，就能正式进入文字识别的环节了：

第一个变量是 APP_ID，它用来识别应用；

第二个变量是 API_KEY，用于识别用户；

第三个变量是 SECRET_KEY ，用来加密密钥。

当把这三个变量传入 AipOcr 函数，使用 AipOcr 函数通过互联网交互后，就可以用来识别用户是不是被授权使用相应的产品，之后就可以把图片加密发送到 AI 产品的服务器上了。

这些就是所有公有云识别用户的通用做法，我把这个做法整理为三个步骤。

第一步， 安装 SDK。代码是：

pip install baidu-aip

第二步，注册用户。

以百度云为例，你需要登录 https://ai.baidu.com/ 网址，以自己的手机为用户名注册一个新的用户。

第三步，申请应用。

成功登录网站之后，你会进入服务控制台界面，然后选择文字识别功能，再新创建一个文字识别类型的应用。创建应用之后，就可以在服务控制台中的应用列表中查看百度云提供的 APP_ID、API_KEY、SECRET_KEY 三个变量。

在这三个变量中，APP_ID 在百度云的控制台中创建用户之后会自动创建。

变量 API_KEY 与 SECRET_KEY 是在创建完毕应用后分配给用户的。

在你成功通过服务控制台得到这三个变量之后，就可以把这三个变量回添到代码中的第 3-5 行，然后去掉“#”开头的注释，这样就可以通过代码完成用户签名和验证功能。

你在掌握它们之后，也可以使用其他的公有云完成类似的用户签名和验证功能。

完成以上三个步骤之后，我们需要把指定路径的图片上传到百度云，通过第 12 行的 get_file_content 函数，把图片的路径和名称作为参数传入这个函数之后，再交给 client.basicGeneral(image) 函数处理，这样就能够完成图片的上传功能了，图片的识别和返回结果都会由 AipOcr 包自动处理以后放入 result 变量中。

总结来说，我在代码中实现了单个图片的在线识别文字功能，当你掌握了对单个图片实现文字识别的方法之后, 你就可以通过我们在第二讲学过的的 for 循环功能，遍历多个文件夹里的多个图片，你可以实现批量图片的识别功能了。

怎么进行离线文字识别？

我们再来看另一种文字识别形式——离线文字识别。被识别的图片同样是 example.png 这张发票的扫描件，代码如下：

import pytesseract
from PIL import Image

# 打开图片
image = Image.open('example.png')

# 转为灰度图片
imgry = image.convert('L')

# 二值化，采用阈值分割算法，threshold为分割点,根据图片质量调节
threshold = 150
table = []
for j in range(256):
    if j < threshold:
        table.append(0)
    else:
        table.append(1)

temp = imgry.point(table, '1')

# OCR识别：lang指定中文,--psm 6 表示按行识别，有助于提升识别准确率
text = pytesseract.image_to_string(temp, lang="chi_sim+eng", config='--psm 6')

# 打印识别后的文本
print(text)

在进行离线识别的时候，有一个现成的文字识别库，那就是 pytesseract 库，这个库实现了对图片中的文字识别功能。使用 pytesseract 库，可以自动实现文字的切分和识别功能，识别效果如下：

通过识别结果，你可以发现英文和数字被准确识别出来的正确率是比较高的，达到 90% 以上。如果是纯英文的图片，识别之后的文字是可以直接使用的。但是在对中文、字母、数字混合的图片识别结果中，中文会出现较多的错误，正确率不足 50%，这种情况下就需要人工去对每个字进行一遍纠正。

不过，如果我们想让初次识别的正确率就很高，那能不能实现呢？其实是可以的。不过在这个过程中我们需要花大量的时间把自己工作场景中经常出现的字手工进行纠错，形成新的识别模型。实现起来还是有一定难度的，为了让你更好地评估是不是值得自己优化离线识别软件的识别正确率，我先来带你看一下原理。

对图像进行文字识别的一般过程为 5 个步骤：

第一步，图像输入；

第二步，前期处理，比如二值化，图像降噪，倾斜纠正；

第三步，文字检测，比如版面分析，字符分割；

第四步，文本识别，比如字符识别，后期矫正；

第五步，也就是最后一步，输出文本。

在这 5 个步骤中，影响识别准确率最大的就是文本识别部分，这部分会受到对文字的标注和算法模型的大小这两个关键因素的影响。既然我们搞清楚了图像识别的步骤和原理，那能不能在保留本地识别的优势前提下，通过在线识别产品来优化离线识别产品的正确率呢？我们来具体分析一下。

首先，我们可以在文字标注方面优化识别正确率。

像身份证识别、票据识别、聊天截图等大部分文字识别场景，这些待识别图片采用了印刷体，每个字的间隔都是固定的，所以我们要想提升准确率，通常的做法是对每个字都进行人工纠错。

当 Tesseract 再识别到同一个字的时候，就会按照我们纠正过的来识别，那么逐个汉字纠正是离线识别方式提升识别正确率的必要方式，所以已经有高手为你写好了汉字纠正的工具了，它就是 jTessBoxEditorFX 软件。

具体来说，这个软件可以把图片中的每个字切割开，并提示哪个方框对应哪个识别的文字，更正为正确的文字之后，就可以把更正的结果进行保存。这个保存好的结果就被称作新训练的模型。

所以整个过程你可以理解成三个步骤：

第一步，人工观察；

第二步，对比原始图像；

第三步，把错误的文字手工纠正为正确的汉字。

比如截图中的“某”字，多次识别错误，当我们人工把这些文字改为正确的“某”字之后，就可以生成一个训练文件，再把训练文件和文字识别的算法合并成新的识别模型。等下次再识别发票的时候，就可以采用新的模型进行识别。所以再次出现“某”字，就可以正确识别了。

这种文字标注对于单一场景非常有效，比如名片、火车票、飞机票、发票、车牌的识别场景，经过手工标注，不断增加样本数量之后，能够让正确率呈对数级别增加。

接下来我们再看一下在算法的模型上，能不能通过在线识别软件来优化离线识别软件。

在线识别软件的模型很大，通常是上百 GB 的容量，离线识别软件的模型往往只有几十 MB，它们之间存在着巨大的差异，模型越大样本数量就越多，类似离线识别软件手工标注的模型一样，巨大的模型让在线识别软件的准确率在第一次识别就会非常高。

如果把在线模型直接放到你的笔记本当中，因为模型庞大，本地计算能力不足，识别的时间甚至超过在线识别的时间，那本地识别就不具有实时性的优势了。所以直接使用在线识别软件的模型方式不适合离线识别软件。

总的来说，由于手工标注和模型大小有限两个因素的影响，离线识别软件的应用场景大部分在企业内部票据识别上面。虽然离线识别中文的效果比较差，但是如果场景单一，在线识别的实时性达不到你的要求，那还是值得投入一部分时间人工标注汉字的。

识别出文字以后，我们就要提取文字内容并保存了。

识别成功后的文字处理工作

通过刚才案例中的识别结果，我们知道识别结果准确率很高，像单张图片已经可以把识别的结果直接保存使用了。不过一旦需要识别的图片比较多，手工操作起来就非常繁琐了，因为识别过程是使用 Python 编写的程序，后续可以继续利用 Python 对识别结果进行处理和保存。比如说在线识别的结果：

为了能够提取文本内容，去掉记录分段信息的"word"和 “{ }”，需要对这段文字再加工处理。

如果你仔细观察的话，就会发现这段文字是有一定规律的，它的写法就是 Python 的基本数据类型“字典”，字典类型会强调一对或多对数据之间的映射关系。为了将识别的文字结果进行保存，我们还会使用另一个基本数据类型“列表”，一般用来表示多段文字的并列关系，为了提取文本内容，我接下来把字典转换成列表就可以实现内容的提取了。

从字典到列表转换的功能怎么实现呢？首先来看一下两种数据类型的定义。

字典由花扩号中的 KEY 和 VALUE 两部分组成的，它的格式是：{"words":"1234567" } 。

列表由方括号的 VALUE 组成，可以存放一个，也可以存放多个 VALUE，格式是：["1234567",] 或["123", "abc"]。

观察这两个数据类型不难发现，把字典的 VALUE 存放到列表里就能实现内容的提取了，如果字典里有多个值，可以使用 for 循环进行遍历，然后批量提取。对于识别的结果，我们可以采用一段代码进行从字典到列表的转换。代码如下：

info = []
for i in result['words_result']:
    info.append(i['words'])
print(info)

经过数据类型的转换以后，我们可以得到处理好的文字内容：

最后，我们把处理好的结果保存至文件就行了。因为类型转换和保存我们都使用 Python 完成，所以我可以把这部分功能和文字识别功能放在同一个脚本中，避免手工操作。

不过我还要提醒你一下，在实现用户认证、图片上传、文字识别、数据清洗、文件保存的过程中，我们要尽可能实现流程的全自动化，避免中间步骤手工介入，这样可以更高效地提升工作效率。

小结

这节课我给你提供了两种文字识别的方式：在线识别和离线识别。不管哪一种识别方式，它们都需要经过图片输入、图片预处理、文字检测、文字识别和输出这样 5 个部分来实现完整的文字的识别功能。

但因为实现方式不同，两种方式各有优缺点。在线方式准确率更高、离线方式保密性更强。你需要根据自己的工作场景来选择不同的识别方式。不过不管哪种识别，最后我都建议你用 Python 对识别后的结果实现数据清洗和保存工作，毕竟减少手工操作才是实现办公效率提升最有效方式。

现在你可以开始动手将自己电脑里存储多年的图片，自动转成文字了。

思考题

在讲离线文字识别的时候，我只讲了单张图片转换文字的操作方法。如果是多张图片呢？该怎么实现呢？我建议你使用第二节课学过的 for 循环来进行操作。

欢迎把你对今天这节课的思考和想法分享在留言区，我们一起交流讨论。也欢迎你点击“请朋友读”，我们一起提高工作效率。

春节特别放送1｜实体水果店转线上销售的数据统计问题

你好，我是尹会生。今天是大年三十儿，先祝你春节快乐。同时我也要为假期还在坚持学习的你点赞。

春节期间，我为你准备了一个项目作业，帮助你检验第一模块的学习成果。这个作业由 3 个部分组成，今天，我会介绍项目背景，给出你操作题目，2 月 13 号，我会给出参考思路，在 2 月 16 号，我会公布答案。希望你跟着更新节奏，完整地参与进来，巩固你之前的学习成果。

接下来，我先介绍下项目情况。

这个题目来自我身边的一个真实案例。我先给你简单介绍下背景：一家实体水果店上了多多买菜后，店里的销售管理系统都没法直接使用实体店的那一套。而且像多多买菜、美团社群现在都只能下载 Excel 数据表。

题目是：F 水果店由于疫情原因，2020 年由实体店改为了线上销售。老板为了进行年终数据统计，需要从在线管理软件下载 Excel 格式的销售数据，并对销售数据进行处理，分析各个种类的水果受欢迎的程度和库存量，最后录入销售管理系统。不过老板在分析 Excel 的过程中，遇到了这样一个问题。

将线上的数据转换为线下数据的时候，由于数据不兼容，必须进行统计和一定格式的调整。首先需要合并 Excel，统计全年水果销售量。在 Excel 中，每个 sheet 记录了当日的销售数据，每个 Excel 文件记录了当月的销售数据。图表如下：

我给你留两道题目：

统计出全年每种水果的总销售额。

统计出每月销售数量排名 Top 3 的水果。

欢迎你在留言区分享你对这道题的想法和思考，我们一起交流、讨论。在 2 月 13 日，也就是大年初二这一天，我会把解题思路给你分享出来，供你参考。

春节特别放送2｜用自顶至底的思路解决数据统计问题

你好，我是尹会生。今天是大年初二，先祝你春节快乐。

上节课我策划了一个项目作业，你完成得怎么样了呢？如果你对第一模块的内容掌握了 60%，其实这道题你就能完成得差不多了，或者说至少会有一个思路。那么今天这节课，我也来给你提供一些我的解题思路和解题方法。

我们再来回顾下两道题目：

统计出全年每种水果的总销售额。

统计出每月销售数量排名 Top 3 的水果。

因为这两道题目都是需要在统计数据的前提下进行的，一个是统计之后进行求和，一个是统计之后进行排序。所以，我们优先要做的就是统计全部文件所有 sheet 里的单元格数据。

以第一题为例，可以按照自顶至底的思路拆分问题。

什么是自顶至底呢？意思是分解问题的时候，先关注总体，再解决具体问题。我给你举个例子你会更容易理解。这是一个你熟悉的问题：怎样把大象装进冰箱？

你会马上想到，把大象装进冰箱分成三个步骤。你看，你不会关注冰箱是什么品牌，大象是什么品种，冰箱的容积如何，大象的体积有多大等等。这样思考的好处是你可以先对高层次的问题进行定义、设计、编码，再细节的问题，再其中的子任务或下一层次去解决。这样逐层去进行设计和编码能够让你的程序有更好的可读性。

那统计销售总数，我也可以把问题拆分成三个步骤。

第一步，拆解 Excel，通过第二讲的循环功能，取得每日销售数据。

第二步，读取每天里的每次销售金额数据。

第三步，将数据按照自己的算法（这里就是简单的累加）进行汇总。

接下来我带你具体看下每个步骤需要做什么。

首先，我们来取出每日销售数据。

你可以先从总销售额开始入手，思考如何拆解问题,。总销售额可以拆分成按月销售额的累加，每月的销售数据可以存放到单独的文件中。月销售额可以拆分成日销售额的累加，日销售额放在每个 sheet 中。

如果用 python 来实现总销售额的统计，实现思路应该为：通过 pathlib 库和 for 循环遍历文件，读取每个 Excel 文件，再通过 xlrd 库读取每个 Excel 里的每个 sheet，每个 sheet 里记录了每一天的销售数据, 这样可以把每天的销售数据读取出来。

接着，我们来读取每一次销售数据。

由于 Excel 中除了销售金额，还有日期等其他噪声数据，我们只需要读取“水果名称”“销售金额”这两列必须数据，就能实现统计销售总额的功能了。因此通过这两个数据所在的列取出“水果名称”“销售金额”这两列。

用 python 实现的话, 就是：使用 for 循环遍历每一行的水果，再采用“字典”数据类型临时存放到 Python 的一种数据结构中。为什么要使用“字典”呢？在第三讲我给你讲解过,“字典”适合记录有映射关系的一对或多对数据。

最后，将数据汇总在一起。

根据需求，销售的金额只需要记录汇总的金额就可以实现最终要求，也就是统计全年销售额的任务了。所以这一步具体做法就是：把字典中可以对每种水果的每次销售金额累加存放，就可以得到当日每种水果销售总金额了。

第二道题，那如何来实现 Top3 功能呢?

这两道问题的处理思路非常相似，都是需要处理每个月的销售额，但是第一个问题，统计全年销售额，需要将 12 个月的销售数据进行累加。第二个问题，需要对每个月的销售数据进行从大到小的排序，前三位的就是“水果销售 Top3”了。

你的思路和想法是什么呢？可以在留言区分享一下，我们一起交流、讨论。在 2 月 16 日，也就是大年初五这一天，我会把完整答案给你分享出来，供你参考。

春节特别放送3｜揭晓项目作业的答案

前两节课，我给你策划了一个项目作业，也给你提供了解题思路和方法，那么今天，就是揭晓答案的时候了。不管你有没有完成作业，或者完成了多少，我都建议你在学习今天这节课的时候，一边看我的讲解，一边把你的答案与我讲解的答案进行对照。

不过我也要提醒你，因为现在你才学到了第三节课，所以答案对你来说并不是最重要的。重要的是你对前三节课的内容掌握了多少，以及这个项目作业的解题思路和方法。

话不多说，我们现在就开始吧。

第一题

我们先看第一道题中关于订单统计的处理思路。

首先我们要明确要解决的问题。

我们希望对多个 Excel 进行统计，并取得每个 Excel 文件里的 sheet。然后再从每一个 sheet 当中取出水果的名称和它的每一次销售金额，最终要将多个 Excel 里的所有的数据进行合并。而合并之后就是我们想要的全年水果销售金额统计结果。

所以解决这个问题的关键点有两个。

第一个关键知识点是遍历。这个知识点在我们的第 1 讲、第 2 讲中都有提到。通过遍历的方式，我们能依次取到每一个文件里的每一个 sheet，甚至能从每个 sheet 取出每一行的内容。

** 第二个关键的知识点就是多个元素的累加操作。** 主要就是把相同名称的水果销售金额进行计算。不过这里涉及到的计算很简单，就是累加操作，通过循环就可以实现多个元素的依次累加了。由于累加是针对每一行数据的，所以我们要把累加操作写在遍历每一行的循环当中。

在搞清楚了两个关键知识点之后，接下来我来带你去分析一下我实现全年销售统计的代码。

import xlrd
from pathlib import Path, PurePath
from collections import defaultdict

# 订单路径
download_path = '/Users/edz/Desktop/效率专栏/新年特辑/订单'

# 取得该目录下所有的xlsx格式文件
p = Path(download_path)
files = [x for x in p.iterdir() if PurePath(x).match('*.xlsx')]

# 定义字典用于结果统计
total = defaultdict(int)

# 中文做字典的key会有问题,做两个简单的翻译函数
tran_dict =  {
    "dragon_fruit":"火龙果",
    "coconut":"椰子",
    "watermelon":"西瓜"
}
# 中文翻译成英文
def dict_trans_chi2eng(value):
    return [k for k,v in tran_dict.items() if v == value]

# 英文翻译成中文
def dict_name_eng2chi(key):
    return tran_dict[key]

# 遍历文件
for file in files:
    sheet = xlrd.open_workbook(file)

    # 遍历表格
    for table in  sheet.sheets():
        # 从第二行遍历内容
        for line in range(1,table.nrows):
            fruit = table.row_values(rowx=line, start_colx=0, end_colx=None)
            # 第一列水果名称, 倒数第二列销售金额
            # print(f'{fruit[0]},{fruit[-2]}')
            # 火龙果,15.0
            # 椰子,16.0
            # 西瓜,10.5

            # 统计每种水果的销售额
            fruit_name = dict_trans_chi2eng(fruit[0])[0]
            total[fruit_name] = total[fruit_name] + fruit[-2]

for fruit_name in total:
    print(f'水果: {dict_name_eng2chi(fruit_name)} , 总金额: {total[fruit_name]}')

    # 水果: 火龙果 , 总金额: 135.0
    # 水果: 椰子 , 总金额: 144.0
    # 水果: 西瓜 , 总金额: 94.5

我们来看代码的前 3 行。第 1 行是读取 Excel 文件的库，第 2 行是用来去处理路径的库，这两个库在我们课程的第一讲都已经为你重点讲解过。读取Excel 使用的是 xlrd 库, 处理路径需要使用 pathlib。之后的课程你也会多次看到它们，所以没有记住的话一定要再去复习。

我重点讲解一下第 3 行出现的库。第 3 行的库是一个叫做 defaultdict 的特殊字典。它特殊在哪里呢？和字典相比**，defaultdict 可以在字典初始化的时候得到一个默认的值。在我的代码中，就使用 defaultdict 记录了水果数量，默认把水果数量设置为 0，那就不用像默认字典一样，设置了字典之后还要手动把字典的值赋值为 0。

这三行都是程序引入的库，接下来我们来看具体的代码逻辑。

首先我们需要获得 Excel 文件所在的路径。第 9、10 行代码是用来读取目录下所有的 Excel 文件的，获取路径使用的是第 2 讲的 pathlib 库，我在第 2 讲也为你详细剖析过它的逻辑，这里就不再赘述了。

获得路径之后，接着就要去处理每一个文件了。从第 30 行开始就是我们程序的核心逻辑，像咱们的第二讲一样，我在这里使用了三个 for 循环。

第 1 个循环用来遍历所有的 Excel 文件。

第 2 个循环用来遍历所有的 sheet。

第 3 个循环用来读取文件当中的，第 2 行到最后一行。

通过这些循环我就可以按行进行处理。那再接着需要处理的就是如何进行水果销售金额的累加。

如果你仔细观察代码就会发现，我并没有马上去进行累加，而是在这里做了一个把中文转换成英文的额外处理，这个额外的处理使用了第三讲学过的自定义函数功能。

你可能会问，为什么要进行中文转换英文呢？因为在 Python 程序当中，字典这个数据类型对中文支持并不友好，使用中文作为字典的 key 进行操作时会报错。

所以为了避免字典在处理中文的时候出现报错，我在这里做了一个中文英文映射和转换的一个小功能。这个功能被我写在文件当中的第 15 到第 27 行，你会看到这里有一个被定义为 translate_dict 的一个映射关系的字典。

在这个字典当中，Key 就是英文单词，value 就是中文汉字。如果在你编写的程序当中需要支持更多的水果种类，那你就可以按照我的格式在这个字典中继续去扩展中英文的映射关系。

那中英文转换又是怎么实现的呢？我在字典之后又自定义了两个函数，分别被我命名为 Chinese to English 和 English to Chinese。

在有了中英文转换功能，那我在进行水果的累加之前，“Chinese to English”函数就可以把字典的“key”从中文的水果名称转换成英文。转换成英文，就意味着英文的水果名称可以作为统计总金额的字典 total 的 key。那么：

使用 total[fruit_name]方法就可以取得已经统计的水果金额。

使用 fruit[-2]取得通过当前得到的水果金额。

使用 total[fruit_name] = total[fruit_name] + fruit[-2] 方式，就可以实现全年的水果销售总金额统计功能。

最后，在第 49 行把销售金额统计结果进行输出。由于水果名称在处理过程被处理成英文，为了让你查看的结果更加友好，我还需要再做两件事情。

第一个是我在输出前把英文的水果名称使用“English to Chinese”函数转换成了中文。

第二个是使用了字符串连接功能，将输出的内容进行对齐并增加必要的提示信息，这样你就可以看到每种水果今年的销售总额了。

以上就是我在实现全年水果销售统计的问题上，是如何编码来实现的。

第二题

接下来我们再看第二个问题。第二个问题要求你按月进行水果销售额 Top3 的统计信息，并且取出当月销售数量前三名的水果名称。

针对这道题，在进行编码之前我们要思考一下，按月统计数据和全年统计数据有什么区别？怎么才能取出销售数量的前三名呢？

咱们首先来解决按月统计数据和全年统计数据有什么区别这个问题。

回顾一下我们在解决全年统计问题的过程。我们先取得了每次销售数据。之后又使用了 total 字典类型，统计了所有行的结果，通过循环, 将所有行的结果汇总成日数据和月数据, 又继续通过循环将月数据统计为全年销售总额。

如果需要统计水果销售额 Top3, 我们也需要先将数据统计为月销售数据, 之后不能将月数据合并到一起, 而是需要对每个月的数据进行排序, 取出销售最多的前三个水果销售额, 所以统计月销售数据以前的程序逻辑是相同的, 区别是统计月销售数据以后, 一个是进行合并一个是进行排序操作。

那第二个问题，怎样去取前三名呢？我们要想取出前几名，前提是要先进行数据的排序工作。因为排序之后才能从大到小取出销售量最高的前三名。为了实现排序这一功能，我们必须要掌握 Python 的排序和截取前三个元素的程序编写方法。

所以接下来我们要学习的就是具体编码。由于和统计总金额的代码功能非常相似，我们就直接复用统计总金额的获取文件路径，统计月销售额的代码。由于每一个文件代表一个月份的销售数据，所以我在第 48 行用输出文件名的方式告诉你当前在统计的文件是哪一个月份的。

通过上面的逻辑方法，我们实现了按月统计销售额和输出当前月份的功能。接下来就可以进行后续的排序，并且取出字典的 value 排前 3 的数据了。你可以通过比较传统的 sort 函数进行排序，并采用复循环三次取出前 3 水果名称和销售数量。

不过我觉得采用这样的方式，代码其实不够简洁。所以我们还有另外一种简洁的方法。

在程序中，我为了避免自己需要编程实现代码逻辑，就直接引入了 collections 标准库的 Counter 包实现排序并提取前 n 个数的功能。Counter 包支持对列表和字典进行排序、数量统计、取前 n 个数的功能，非常强大。

那这里就用它来实现基于字典的 value 从大到小进行排序，排序之后取前三个元素的功能来实现，从而取得水果销售额 Top3。

在代码的第 53 行，就是我使用了 Counter 包实现对 total 变量进行排序的具体实现代码。

在排序之后，接下来的问题就是如何取出前三个元素了。由于提取前 N 个元素的问题，Counter 包已经内置了该功能，所以在第 58 行，我可以再通过 most_common() 函数，为函数增加数字“3”作为参数，从而实现提取销量 Top3 的功能。

# 使用Counter函数排序和统计数量
sorted_total = Counter(total)

# 清空本月统计数据
total = defaultdict(int)

# 通过most_commnon函数排序取出Top3
print(sorted_total.most_common(3))

在这个程序当中，我们也有一个要注意的事项。因为每个月份统计好的数据都会放到 total 变量当中，因此在进行下一个月统计的时候，total 变量会把本月统计结果累加上去，这样就会导致下一个月统计的数量不准确。

所以我们在统计完每一个月的统计数据之后，需要把 total 变量重新初始化。为了能够让你注意这件事情，我特意把它写在了第 56 行并详细进行了标注。这也是在进行数据统计时极容易犯错误的地方，希望你能注意它的用法。

以上就是我实现所有水果的金额统计和按月统计的实现方法，希望你能通过这两个代码理解如何去处理大批量重复的 Excel 工作。

好了，我们的春节策划到这节课就结束了，你的答案和我的一样吗？希望你能借助这个项目，查漏补缺，多多巩固前面的内容。下节课，我们继续来自动化办公的方法，我们下次见。

04 | 函数与字典:如何实现多次替换

你好，我是尹会生。

“替换”是我们日常办公经常遇到的操作，比较常见是把文件中的英文标点符号（,.""”）替换成中文标点符号（，。“”）。有时候不仅是标点符号，还需要替换好几个词。还有一种情况不太常见，但是一碰到就很棘手，那就是根据数字范围进行替换，比如“0-6 岁”替换成“少年”，“7-17 岁”替换成“青年”，“18-65 岁”替换成“中年”。

如果直接使用替换函数，你需要编写大量的代码逻辑，但是使用逻辑判断和字典就可以用更高效的代码来实现快速替换功能。那么今天这节课，我们就来学习下怎么实现快速替换。

用 Python 实现“替换”功能的原理

为了让你更直观地理解编程语言里的替换，我先来给你讲一讲用 Python 实现替换的原理。我用一个例子来给你讲解。比如我需要把字符串“新年快乐”替换为“恭喜发财”，在 Python 中，我是通过 replace() 函数来实现的：

string1="aaa新年快乐bbb"
string2=string1.replace("新年快乐", "恭喜发财")
print(string2)
# aaa恭喜发财bbb

string3="aaa新年快乐bbb新年快乐ccc"
string4=string3.replace("新年快乐", "恭喜发财", 2)
print(string4)
# aaa恭喜发财bbb恭喜发财ccc

你可以看到，在这段代码中我使用了 replace() 函数来实现文件内容的替换。为什么使用的是字符串的替换函数呢？因为在编程语言中，我们通常会把文件内容读取到内存用变量临时储存，再进行处理。为了便于对文字进行查找替换这类的操作，通常会使用字符串这种数据类型的变量来存储文字内容。

实现字符串替换的 replace() 函数，是 Python 字符串内置的函数。它既可以实现匹配关键字进行一次替换，也能支持对多次出现的字符串进行替换。比如在代码的第 7 行我就增加了参数“2”，这就实现了“新年快乐”两次的替换。

刚刚说的 replace() 函数，实现字符串替换操作的原理，就是从字符串中从前向后, 逐个字符去和 replace() 函数的第一个参数去比较, 如果文字内容相同，就把匹配的内容替换为 replace() 函数第二个参数的字符串内容。如果不匹配, 就继续对比下一个字符, 直到整个字符串比对完成. 为了便于描述，我把这种逐一比对, 找到相同字符进行替换的操作, 称作“一对一替换”。

在实际工作中，你遇到的替换场景会更复杂。比如需要把字符串中出现的所有英文标点符号，全部一对一地替换成中文的标点符号。那么这个问题其实就变成了多个“一对一”的替换操作了。该怎么解决呢?

相信你会想到通过多个 replace() 替换函数来实现多个“一对一”替换操作。为了让你更好地理解这一逻辑，我使用了如下代码，给你演示一下它的基本功能。

string5='aaa,."bbb'
string6=string5.replace(',', '，')
string6=string6.replace('.', '。')
string6=string6.replace('"', '“')
# 需要更多的replace()匹配更多的标点符号
print(string6)
# aaa，。“bbb

在这段代码当中，我使用了三个 replace() 函数实现“,."” 三个符号的替换。不过一旦考虑替换更多的符号时，就要编写更多个 replace() 函数。这一行为虽然不会在运行效率上产生问题，但是会带来代码阅读上的障碍。

比如需要你通过 Python 把全国的省市地县的汉语拼音替换成汉字，像把“GUANGDONG”替换成“广东省”，你至少要编写上百个替换函数。那面对如此大量的“一对一”替换，我们该怎么高效地编写代码呢?

怎样实现批量替换？

我来解决这类问题一般会采用两种方式实现，一种方式是用字典 + 自定义函数替代 replace() 函数，另一种是用逻辑判断 + 自定义函数替代 replace() 函数。我们先来看字典 + 自定义函数的方式是怎么对带有大量 replace() 的程序进行优化的。

用字典 + 自定义函数替代 replace 函数实现批量“一对一”替换

我们还是用把城市名称的拼音替换成汉字的例子来讲解。为了让你更直观地比较字典 + 自定义函数方式和 replace() 函数的区别，我先给你演示一下实现替换功能的代码：

# 保存映射关系的函数,函数的主要功能是通过字典实现的
def replace_city(city_name):
    return {
        "GUANGDONG":"广东省",
        "HEBEI":"河北省",
        "HUNAN":"湖南省",
        "HANGZHOU":"杭州市"
    }[city_name]

# 根据映射关系实现批量循环
def replace_multi(my_citys, replaced_string):
    for pinyin_city in my_citys:
        replaced_string = replaced_string.replace(
            pinyin_city,replace_city(pinyin_city))
    return replaced_string

# 哪些城市要替换
citys = ("GUANGDONG", "HUNAN")

# 需要替换的字符串
string1 = """
GUANGDONG，简称“粤”，中华人民共和国省级行政区，省会广州。
因古地名广信之东，故名“GUANGDONG”。位于南岭以南，南海之滨，
与香港、澳门、广西、HUNAN、江西及福建接壤，与海南隔海相望。"""

string2 = replace_multi(citys, string1)
print(string2)
# 广东省，简称“粤”，中华人民共和国省级行政区，省会广州。
# 因古地名广信之东，故名“广东省”。位于南岭以南，南海之滨，
# 与香港、澳门、广西、湖南省、江西及福建接壤，与海南隔海相望。

我在代码里是通过两个核心函数来实现替换的，它们分别是 replace_city() 和 replace_multi() 函数。

我们先来分析一下 replace_city() 函数。它实现的是城市拼音和城市中文名称的全部对应关系，其中有两个技术细节需要你掌握。

第一个技术细节是字典的取值方式。如果我把一个字典定义为 dict1，并且想取得字典的值，就可以使用这样的代码：

dict1["abc"]=123

方括号中的字符串"abc"被称作字典的下标。通过下标，我们可以获得字典的值。为了定义字典以后可以反复使用，通常我们会给字典赋予一个变量名，以此作为字典名称。所以在这里 dict1 就是字典的名称。

当然，如果字典只使用一次，那也可以不使用字典名称。相应的，它的写法就变成了这样：

{"abc":123, "aaa":456}["abc"]

通过这一行代码，你可以取出直接使用字典的值，而不需要对字典进行声明，也不需要为字典再起一个变量名。

第二个技术细节是我为 replace_city() 增加了一个参数 city_name，以及一个关键字 return。city_name 作为城市的拼音传入函数后，会作为字典的 key，通过字典的映射功能得到中文城市名称。而 return 关键字返回字典映射的结果，就是城市的中文名称。

通过这两个技术细节，就可以让函数 replace_city() 实现接收拼音并返回中文的功能。这样实现映射关系的好处是：函数调用一次就返回一个值，编写好这类函数之后，其他人可以拿去直接使用，不用考虑函数内部使用了哪种数据类型，有利于代码的重复使用。

除了 replace_city() 之外，还有一个核心函数 replace_multi() 函数，它通过 for 循环来实现批量“一对一”的替换。它的作用是避免重复编写大量的 replace() 函数，提高代码的可读性。

我在设计 replace_multi() 函数的时候，为它准备了两个参数。第一个是要替换的城市的拼音，第二个是要替换的字符串。

第一个参数我具体指定了哪些城市需要替换，这样编写会让我的程序更加灵活，不必把所有城市的拼音都进行拼音到中文的替换操作。

第二个参数也是为了让 replace_multi() 函数更加灵活，如果对多段文字进行替换，可以多次调用 replace_multi() 函数。同样的，如果 replace_multi() 函数需要多次调用，也可以通过循环结构批量来优化代码。

总结来说，通过字典 + 自定义函数替代字符串默认的替换函数 replace() 函数，可以避免编写大量的 replace() 函数，提高了代码的灵活性和可读性。如果你在工作中涉及这类大量的“一对一”替换时，可以考虑采用我教的这个方法来优化你的替代效率。

用逻辑判断 + 自定义函数替代 replace() 函数实现“多对一”替换

除了刚才我提到的大量“一对一”的替换场景，还有一种替换场景你也会遇到，并且一旦遇到就很棘手。

比如在 Excel 中，你需要根据年龄这一列单元格来把你的客户划分为少年、青年、中年、老年。如果把年龄的每个整数都进行一次替换，这种写法会非常啰嗦，所以我们可以使用逻辑判断来实现这一替换。我先把代码给你演示出来。

age = 18
if age>0 and age<=6:
    value="少年"

elif age>7 and age<=18:
    value=青年"

elif age>19 and age<=65:
    value="中年"

else:
    value="老年"

这段代码通过逻辑判断实现了从年龄到少年、青年等年龄段的替换功能。在代码中，“if”“elif”“else”是构成逻辑判断的关键字，它们表示了如果关键字到“:”之间的结果为 True，则其他语句后面的代码不会被执行。

根据匹配的年龄要求，年龄是一个范围，所以我使用了 and 关键字连接两个判断逻辑。比如“age>0 and age<=6” 代码，意思就是当 age 同时满足大于 0，并且小于等于 6 时，判断的条件才成立，这段代码的返回结果为 True，因此 value 变量的值就是“少年”。

在这段代码中我使用了一个逻辑判断结构。逻辑判断结构用于判断 age 变量的范围，它可以根据判断的结果为 value 变量进行赋值。这种实现形式和城市的拼音替换不同，城市的拼音和汉字是逐一对应的,age 变量的多个值, 例如从 1 到 6 对应的都是“少年”, 它实现的是一个范围映射到一个值上面的形式。为了便于描述，我把这种形式称作“多对一”的替换形式。

虽然使用逻辑判断进行替换操作, 实现了的“多对一”的替换形式，但是我认为仍然存在着不利于代码复用（重复使用）的问题，因此在保证代码逻辑不变的前提下，我对这段程序进行了优化，将逻辑判断也放入函数中。代码如下：

def age_replace(age):
    if age > 0 and age <= 6:
        return "少年"

    elif age > 7 and age <= 18:
        return  "青年"

    elif age > 19 and age <= 65:
        return "中年"

    else:
        return "老年"

print(age_replace(80))

我为你解释一下为什么要把替换操作放在函数中。这样使用有两点好处：

提高代码的复用，当你下次需要做年龄到年龄段映射时，可以直接调用函数，不用重复编写逻辑判断的代码。

对代码进行再设计的时候，方便将逻辑判断中类似“age > 0 and age <= 6”的判断逻辑再封装成函数。

你可能会问了，代码不是应该先思考运行过程，经过设计之后再编码的吗？

在实际工作中，随着人们对代码的不断修改，原来设计好的代码结构，在整体结构上会增加很多的判断逻辑，代码质量会越来越混乱。这时候就需要你重新对代码逻辑进行优化。所以为了不让后续修改代码逻辑的行为破坏代码的可读性，就应该在初次设计和编写代码的时候考虑好代码的扩展性。

最后，我想再强调一下，使用 if 逻辑判断的目的是为了实现把一个范围映射到一个新的值，这样它就间接地实现了替换功能。所以当你解决替换问题的时候，不要把思维只局限在字符串自带的 replace() 函数中。

小结

我来给你总结一下今天的主要内容，围绕着“替换”这一功能，我给你讲解了三种实现替换的方法：

字符串的 replace() 函数；

使用字典做“一对一”映射，通过字典类型的键值对, 实现内容替换；

使用逻辑判断实现“多对一”映射，将 if 判断的条件替换为匹配成功的结果。

替换操作要根据被替换内容的形式，选择合适的方法，replace() 函数更适合单个替换，字典适合“一对一”替换，if 逻辑判断适合将一个范围替换成一个值。

除了灵活掌握不同的替换方式，我还建议你把字典和逻辑判断放入自定义函数当中，当你遇到类似需求的时候就可以直接复用代码。

思考题

通过将城市的拼音替换成汉字的功能，你是否能实现一个自己的自动多文件标点符号替换函数，将英文符号替换为中文符号呢?

05 | 图像处理库：如何实现长图拼接？

你好，我是尹会生。

我们在工作中，除了和文字、表格打交道之外，还会经常涉及到批量处理图片和视频的工作。比如：媒体从业者在发微博长图文时，需要把多个图片拼接成一幅长图；作为视频剪辑人员，需要从互联网下载多段视频，再进行合并。

这类工作可以用功能强大的商业软件实现，不过这些软件大都操作繁琐，而且还需要付费。为了降低学习成本和购买软件的成本，我们往往还会使用开源软件替代商业软件来实现图片和视频处理功能。但是开源软件通常都是以命令行方式运行的，所以我们不仅要记住命令，还得记住命令的常用参数。

不过，幸运的是，虽然直接使用开源软件不够友好，但如果通过 Python 来调用这些开源软件，那实现长图和视频拼接就轻而易举了，而且还能大批量地处理图片和视频。

Python 是如何调用外部命令的

为了让你了解 Python 是如何操作这些开源软件的，我先来给你介绍一下 Python 调用外部程序的原理。

我们要想使用 Python 语言之外的功能，要依靠两大途径：导入函数库和调用外部命令。

在第一讲我使用的 xlrd 库是通过 import xlrd 命令导入到 Python 语言中的，Python 语言默认是不支持 Excel 的。那么通过导入函数库，Python 就可以获得对 Excel 的操作能力。

还有一种情况是，需要操作 Python 语言之外的功能，但这个功能没有人将它开发成函数库，那如果我们想要使用这些功能，使用的途径就是调用外部命令了，而调用外部命令就需要 Python 内部函数库的 subprocess 模块来实现。

这个模块的实现机制是：它的 run() 函数的参数可以指定一个可以运行的程序的路径，而 Python 会根据这个路径来运行可执行文件，然后再根据运行结果，以及 Python 的逻辑判断去进行后续的自动化处理工作。

这个实现机制并不难，我给你写一段简单的程序，帮你理解 Python 是怎样调用外部命令的。这里以 macOS 系统为例，我们通过 Python 获取当前目录下所有文件的功能。

from subprocess import run, Popen, PIPE

cmd1 = ["ls", "."]
returncode = run(cmd1)

print(returncode)
# CompletedProcess(args=['ls', '.'], returncode=0)
# returncode是“ls .”的退出状态码.
# 通常来说, 一个为 0 的退出码表示进程运行正常

# 使用Popen获取程序运行结果
with Popen(cmd1, shell=True, stdout=PIPE, stderr=PIPE, encoding="utf-8") as fs:

    # 如果程序在 timeout 秒后未执行完成,会抛出 TimeoutExpired 异常
    fs.wait(2)

    # 从标准输出中读取数据,知道文件结束
    files = fs.communicate()[0]

print(files)

这段代码中最核心的函数是 run() 函数和 Popen 类。subprocess 模块就是通过这两个函数实现的外部程序调用。我来为你重点剖析一下它们的功能、参数，以及何时选择 run() 函数、何时选择 Popen 类。

为了实现 Python 调用可执行文件，首先在代码的第一行，我是这样编写的：

from subprocess import run, Popen, PIPE

这样一行代码，它和我第一讲使用的 import 方式导入函数库的区别是，这种形式可以让你直接使用模块中的类和方法。

如果你使用 “import subprocess”方式导入 subprocess 库的话，在调用 run() 函数的时候，就需要用 “库. 函数”的形式在 Python 中使用库当中的函数，即“subprocess.run()”。在你多次调用 run() 函数时，代码会较长，那么使用“from import”方式导入，就可以在当前代码文件中直接使用 run() 函数，为代码的阅读带来更好的体验。

接下来，我定义了一个变量 cmd1。这个变量的值是 macOS 命令行能够运行的“ls .”命令，这个命令的执行结果是显示当前目录下所有文件和文件夹的名称。

run() 函数的主要功能就是执行一个新的程序，它的用法非常简单，把第一个参数指定为要执行程序的路径就可以了。如果要执行的程序带有参数，那就可以使用列表数据类型存放可执行程序名称和参数，像是我在程序中定义的 cmd1 变量一样。如果你需要运行其他命令，把代码中的 ls 替换为你想要运行的其他程序就行了。

为了让 Python 自动化处理程序更强大，除了运行程序外，你还可以得到可执行程序的运行结果。在这种情况下，我们就需要使用 Popen 类替代 run() 函数实现外部程序的调用。

可以看到，我在代码的第 12 行先通过 Popen 类执行了“ls .”命令，接着通过参数 stdout=PIPE 将命令的执行结果放入到 PIPE 对象中, 最后再通过 communicate() 函数将 PIPE 中的内容读取出来，存放到 files 变量中，这样就实现了读取命令执行结果的功能。

这个功能是无法在 run() 函数实现的，因此在你需要通过 Python 读取程序执行结果的时候，就可以选择 Popen 类。不过如果只需要运行可执行程序，那使用 run() 函数就能满足你的要求了。如果你想更深入地了解它们，我建议你阅读subprocess 库的官方文档。

以上就是我用 subprocess 库实现 Python 调用可执行程序的方法。Python 之所以被我们称作最佳的“胶水语言”，就是因为它能轻易“粘合”可执行程序。利用 Python 灵活的逻辑判断、循环语法可以实现程序的批量执行和流程管理。

接下来，我们就使用 subprocess 来实现长图拼接和视频拼接的功能。

长图拼接

当我进行微博文案推广的时候，需要将多个图片拼接成一个长图。拼接图片的功能 Python 本身是不具备的，因此就需要引入外部命令来实现图片拼接功能。

我在 macOS 平台上找到了一个非常强大的图像处理软件叫做 ImageMagick，它能对图片进行编辑、合并、切割、旋转等 90 多种操作。 ImageMagick 软件实现图片拼接的命令格式是这样的：

composite 图片1.jpg 图片2.jpg ... 图片n.jpg 最终合成结果.jpg

在这段命令格式中，composite 命令的参数包含了多个图片文件，每个图片需要对照着文件将图片的路径和文件名写在参数中。如果手工输入图片名称，不仅效率低，而且容易遗漏。另外，如果需要大量重复使用 composite，还需要精细调整合并结果，给 composite 程序增加很多参数。

因此，我就可以通过 Python 调用可执行程序的 subprocess 库，对 composite 拼长图的工作进行脚本化编程。它的核心实现代码如下：

p = Path(jpg_path)

# 增加命令
cmd = ["composite",]

# 增加参数
for x in p.iterdir() if  PurePath(x).match('*.jpg'):
    cmd.append(x)

# 增加结果
cmd.append(result_path)

run(cmd)

由于 composite 可以把长图合成的结果直接输出为文件，因此采用 run() 函数即可实现程序执行的功能。另外，当你需要调整 composite 参数时，可以直接修改 cmd 变量的值，并不需要改动程序其他部分。当你要对新的一组图片进行合成的时候，重新设置 jpg_path 变量就行了。

总结来说，使用 Python 调用 composite 合并的好处就是：你不用记住程序使用的繁杂的命令行参数，也不用记住运行逻辑，因为 Python 程序已经事先把逻辑编写好了。

视频的拆分与合并

在了解了如何使用 subprocess 调用 composite 实现长图拼接之后，我再给你讲一下如何使用 subprocess 库调用可执行程序，来进行视频的拆分与合并。

我们先来学习下视频拆分的原理。

你在电脑本地经常见到的视频格式是 MP4，但如果要把视频放在互联网上，为了减少首次播放的加载时间，你就必须把一个 MP4 切分成多个文件，而且切分之后还需要把格式转换为.TS 格式的视频文件。

为什么不直接使用 MP4 格式，而是要把 MP4 格式改成.TS 格式呢？这是因为.TS 格式可以保证多个文件之间的视频无缝播放，而且还会保证视频不会在播放下一个文件的时候，出现破音或画面中断等影响用户体验的情况。

当我们将一个视频切分成多个文件的时候，就要考虑文件的播放顺序问题了。为了记录顺序，我们需要在切分之后引入一个索引文件，这个索引文件不用手动编写，我们直接使 FFmpeg 命令就行了，它可以实现视频格式的转换、合并和拆分。FFmpeg 命令会在切分之后，自动产生一个以.M3U8 结尾的索引文件。

我来解释一下这个索引文件。M3U8 文件是指 UDF-8 编码格式下的 M3U 视频索引，播放器通过这个索引文件就可以找到视频下所有的分段，并依次播放视频。

看到这儿你应该就能明白了，想要使用 Python 进行视频拆分，我们首先需要 FFmpeg 命令，然后通过 Python 设置 FFmpeg 的参数，最后再指定 MP4 文件和.TS 文件的路径，这样就能实现拆分视频的功能了。因此我使用这样的代码来实现视频拆分：

from subprocess import run
input_video = "/Users/edz/Desktop/05/xxx.mp4"
segment_time = 10
m3u8_list = "/Users/edz/Desktop/05/xxx.m3u8"
output_video = "/Users/edz/Desktop/05/video-%04d.ts"

cmd1 = ["ffmpeg", "-i", input_video, "-f", "segment", "-segment_time", str(segment_time), "-segment_format",
    "mpegts", "-segment_list", m3u8_list, "-c", "copy", "-bsf:v", "h264_mp4toannexb", "-map", "0", output_video]

run(cmd1)

在代码中，我通过 FFmpeg 把 MP4 切分成了多段 TS 文件。你要想实现相同功能，首先需要在电脑中安装 FFmpeg 命令，它的下载地址为：https://ffmpeg.org/download.html。

为了实现 MP4 文件格式的分割，需要使用 ffmpeg 非常多的参数。不过使用 Python 进行调用的好处，就是你不用记住复杂的参数。我们把输入文件路径、切分大小、输出的 M3U8 和 TS 文件指定为四个变量，这样只修改这四个变量，就可以实现拆分功能了。

如果你需要离线观看视频，就要将网络上的视频下载到本地，这时你会发现从互联网下载的格式是 M3U8 和 TS 文件。那又怎么把它们合并成 MP4 文件呢？

你同样可以使用 FFmpeg 命令，但是 FFmpeg 的参数不同。我将 FFmpeg 的命令写在这里：

ffmpeg -allowed_extensions ALL -protocol_whitelist "file,http,crypto,tcp,https" -i index.m3u8 -c copy out.mp4

如果你不想背诵这么长的参数，完全可以仿照 Python 整合拆分视频的代码来实现合并功能。先 FFmpeg 命令和参数放入列表，再把 M3U8 文件和 MP4 文件放入变量，便于你合并新的视频的时候进行重新赋值。

所以你看，相比直接使用 FFmpeg，subprocess 调用 FFmpeg 的优势就在于两点，一是不用记住复杂参数，二是对批量转换视频非常有利。举两个例子。

如果你是视频剪辑的专业工作者，肯定要大量使用 FFmpeg 更复杂的功能，这些功能对应的参数一般都比较多，而且参数很多都使用了简写和大小写, 很难记忆。但要是使用 Python 调用的话，你可以直接更改要操作的文件路径，就不必记录大量的参数。

另外需要进行视频的批量转换时，可以通过第一讲的循环操作对视频任务批量处理，这样就避免了手动逐个修改书写文件的操作，从而提高视频转换的效率。

小结

最后，我来为你总结一下这节课的主要内容。

通过对 subprocess 库的讲解，你知道了怎样通过它实现 Python 加载外部可执行程序，并且能够对程序执行的结果进行处理。

我也为你讲解了长图拼接和视频拆分合并的两个案例，帮你更好地理解 Python 为什么会被称作“胶水”语言。

我还想强调一下，通过 Python 调用可执行程序的用法非常常见，特别是在多媒体处理、自然科学、AI 等领域里。在这些专业领域，为了加快计算速度，通常会使用 C++ 语言实现专业程序。

这些专业程序参数多、功能单一，且使用命令行执行，当你需要多次执行这些程序，又不想背诵它们的参数的时候，就可以利用 Python 的判断循环功能，结合 C++ 语言实现的专业程序，来实现批量执行和减少参数手动输入的工作，提高你的工作效率。

最后，我也把这节课的代码附上，你可以查看。本讲代码

思考题

在最后也请你思考一下，你在工作当中是否会使用命令行工具呢？它们能否用 Python 进行包装，从而避免手写复杂参数呢？

如果你觉得这节课有用，能解决你的办公效率问题，欢迎你点击“请朋友读”，分享给你的朋友或同事。

06 | jieba分词：如何基于感情色彩进行单词数量统计？

你好，我是尹会生。

在涉及运营、市场的工作中，我们经常需要根据产品评论的情感分析，来了解某一产品的口碑。所谓的情感分析，就是指根据用户对产品的评论，分析出用户对产品的喜好程度。

最简单的，我们会区分产品的评价是正向还是负向的，然后根据反馈结果改变产品的特性。稍微复杂一点的，我们会根据情感色彩将产品的评价关键词提取出来，进行统计和分类（用于更深入的分析产品）。

如果靠人工对产品评价进行辨析，有很大的局限性：一个是不够公平，因为每个人对词语感情色彩的理解并不是完全一致的；另一个是产品评价有很多，而且还会不定期增加，人工分析很难保证及时性。

因此，在进行词语的情感分析时，我通常都会使用 Python 的 jieba 库，来自动化实现文本情感分析功能。一般需要经过三个步骤，分别是分词、优化分词结果和情感分析。

那我就先带你看看为什么要进行分词，以及如何进行分词操作。

如何分词？

要想判断一段话表达的情感是正向还是负向，就需要根据这句话中的关键词来得到情感的倾向。例如一段话中出现了“开心”“高兴”“物超所值”等正向的词语，我们就可以认定这条产品的评价是偏正向的。相反，出现“不喜欢”“差”等词语，评价就是偏负向的。

但是，要想从一句话中将这些表达情感的词一个一个找出来，就需要依靠专业的工具把一句话根据语义划分成多个词，再把表达情感的词语提取出来，进行情感分析。

为什么要先根据语义来划分词呢？这主要是因为中文句子里的每个词中间没有用空格进行分隔，没有分隔就没法进行之后的情感分析。而对中文句子按照语义进行切割的这种操作，我们就称为“分词”。

Python 中有非常成熟的分词库，其中最流行的库是 jieba 库。在计算机中，实现语义分割的技术有两种，一种是从统计学的角度分词，另一种是从词库的角度基于 TF-IDF 算法实现分词。jieba 就是采用第二种，基于词库的角度对文章进行自动分词的。

那我就以电商网站上的一段商品评论为例，给你演示一下 jieba 库是如何实现分词的。

import jieba

words1="速度快，包装好，看着特别好，喝着肯定不错！价廉物美"

words2 = jieba.cut(words1)

print("/".join(words2))
# 速度/快/，/包装/好/，/看着/特别/好/，/喝/着/肯定/不错/！/价廉物美

在这段代码中，我利用 jieba 库的 cut() 函数实现了自动分词功能。我刚才讲了，jieba 分词是依靠词库实现的，词库里包含了提前准备好的词和词性。下图就是 jieba 词库的内容：

一鼓 ru
一鼓作气 ru
一马当先 ru
... ...

这些词库中的词，jieba 是怎么识别的呢？

在你使用 pip 命令安装了 jieba 库之后，它会附带一个默认的词库。在官方文档中，将这个词库称作“字典”文件。这个文件包含了日常汉语的词语、词性。jieba 库会先基于“字典”对文章中所有可能出现的词进行匹配。匹配之后，会生成句子中的汉字所有可能形成的词。然后再将这些词构成的有向无环图（DAG），并采用动态规划算法查找最大概率路径，尽可能不会将一个词拆分成单个汉字。最后再从“字典”找出基于词频的最大切分组合，把这分词的组合从句子中找出来，形成一个一个的词。

而且，为了提高分词的准确率，jieba 对没有记录在字典的词（称作未登录词）也使用了分词的模型，它就是大名鼎鼎的基于汉字成词能力的 HMM 模型（隐马尔可夫模型）。对词库中的词和未登录词进行处理之后，jieba 就可以实现自动化分词了。

不过，分词之后，我们还需要对分词结果进行优化。因为在分词结果中存在着大量的标点符号，还有“看着”“喝着”“包装” 等和表达产品评价的情感无关的词语，为了加快计算词语的情感速度、避免无关词语影响情感倾向判断，我们就要优化分词的结果。

优化分词结果

优化分词结果主要从两个方面进行优化：一方面是移除标点符号；一方面是删除和情感无关的助词、名词等。

我先来带你学习下怎么从分词结果中移除标点符号。

移除标点符号一般有两种方法：

删除停止词（Stop Words）；

根据词性提取关键词。

先来看看第一种，删除停止词。

所谓的停止词，就是指为了节省空间和提高匹配词语情感倾向的效率，在进 **** 行情感分析前自动过滤掉的某些字或词。

停止词主要是标点符号，也可以是“啊呀呢”等语气助词。把标点符号写入停止词列表后，再使用 for 循环功能，将 jieba 分好的词和停止词列表依次匹配。如果 jieba 分好的词出现在列表中，就将这些词删掉。如果没有出现在列表中，就把这些词再组合成一个新的列表，后续就可以对新的列表进行情感分析。

删除停止词的代码如下。通过删除停止词，我们就可以得到只有汉字的分词结果。

words2 = jieba.cut(words1)
words3 = list(words2)
print("/".join(words3))
# 速度/快/，/包装/好/，/看着/特别/好/，/喝/着/肯定/不错/！/价廉物美

stop_words = ["，", "！"]
words4 =[x for x in words3 if x not in stop_words]
print(words4)
# ['速度', '快', '包装', '好', '看着', '特别', '好', '喝', '着', '肯定', '不错', '价廉物美']

另一种优化分词结果的方式叫做根据词性提取关键词。这种方式的优点在于不用事先准备停用词列表，jieba 库就能够根据每个词的词性对其进行标注。

我这里为你提供了一张 paddle（paddle 是百度开源的深度学习平台，jieba 使用了 paddle 的模型库）模式词性表作为参考，你可以根据 jieba 自动分析得到的词性结果，手动将助词、虚词（标点符号）移除。

我把这个基于词性移除标点符号的代码也提供给你：

# words5 基于词性移除标点符号
import jieba.posseg as psg
words5 = [ (w.word, w.flag) for w in psg.cut(words1) ]
# 保留形容词
saved = ['a',]
words5 =[x for x in words5 if x[1] in saved]
print(words5)
# [('快', 'a'), ('好', 'a'), ('好', 'a'), ('不错', 'a')]

在这段代码中，我在使用 jieba 库的 posseg 类实现分词的同时，也对词性进行了标注。为了让你看到更直接的结果，我只保留了形容词，因此，变量 saved 的列表参数就只有一个‘a’，表示保留的词类型为形容词。

如果你希望保留更多的词性，可以将词性表中代表每种词的英文缩写写入 saved 列表中，其中，我建议你在处理之后把形容词、副词、动词都保留下来，这些都有助于你进行下一步的语义情感分析。

在优化分词结果之后，我们就得到了只有形容词的处理结果。那么，接下来，我们需要基于这些形容词来获取产品评价的正向或负向结果，以及基于词语的情感色彩来统计单词的数量。

语义情感分析

对于已经分好词的语句，我们需要使用另一个库统计词的正向、负向情感倾向，这个库就是 snownlp 库。

snownlp 库既能实现分词，也能计算词出现频率，以及进行情感分析。那你可能就发出疑问了：为什么不直接使用 snownlp 进行分词，而要使用 jieba 分词呢？

原因就在于，snownlp 的算法问题，会让它对否定词划分得不够准确。例如“不喜欢”，snownlp 会把这个词划分为两个独立的词，分别是“不”和“喜欢”。那么，在计算语义情感时，就会产生较大的误差。所以我们会先采用 jieba 进行分词，分词之后再采用 snownlp 来实现语义情感分析功能。

接下来，我带你看一下如何使用 snownlp 得到完成分词之后的情感分析结果。代码如下：

from snownlp import SnowNLP
words6 = [ x[0] for x in words5 ]
s1 = SnowNLP(" ".join(words3))
print(s1.sentiments)
# 0.99583439264303

这段代码通过 snownlp 的 Bayes（贝叶斯）模型训练方法，将模块自带的正样本和负样本读入内存之后，再使用 Bayes 模型中的 classify() 函数进行分类，这样就得到了 sentiments 属性的值，sentiments 的值表示情感倾向的方向。在 snownlp 中：

如果情感倾向是正向的，sentiments 的结果会接近 1。

如果情感倾向是负向的，结果会接近 0。

可以看到，我们在刚刚的代码中得到的情感分析的结果是 0.9958，非常接近 1，因此这条产品的评价就是正向的。

情感倾向结果趋近于 1 或者趋近于 0 都是非常理想的情况，可以直接得到感情色彩比较强烈的产品评价。但是，有时候感情色彩不太强烈，在这种情况下，我们就需要根据评价的数值范围对评论进行分组，统计每组包含多少个评价。

这个功能也可以通过 snownlp 实现，我把代码写在这里，你可以参考：

positive = 0
negtive = 0
for word in words6:
    s2 = SnowNLP(word)

    if s2.sentiments > 0.7:
        positive+=1
    else:
        negtive+=1

    print(word,str(s2.sentiments))
print(f"正向评价数量:{positive}")
print(f"负向评价数量:{negtive}")
# 快 0.7164835164835165
# 好 0.6558628208940429
# 好 0.6558628208940429
# 不错 0.8612132352941176
# 价廉物美 0.7777777777777779
# 正向评价数量:3
# 负向评价数量:2

通过 snownlp 库配合 jieba 分词的结果，你就可以实现批量产品评论的自动语义情感分析了。同时，你还可以根据不断累积产品的评价，来持续优化你的产品。

小结

最后，我来为你总结一下对文件进行情感倾向分析的关键步骤和注意事项。实现语义情感分析功能，你必须掌握分词、优化分词结果、语义情感分析这三个步骤。

其中分词是实现中文语义分析的第一步，也是最基础的部分。分词的好坏决定了对每个词的情感进行标注的准确程度。如果默认的 jieba 分词没有正确地把词语划分，你也可以使用 jieba 自带的 suggest_freq() 函数进行词频调节。

举个小例子，“中”“将”两个字可以组成“中将”的词语，也可以拆开来用“我们中 / 将有人成功考上北大”。在不同的语言环境中，我们要通过词频调节来让它们以词的形式出现，还是以两个字的方式出现。调整的方法是：

jieba.suggest_freq(("中", "将"), tune = True)

可以看到，利用调节词频使“中”“将”都能被分出来，而不会被错误地识别为一个词“中将”，通过这种方式，就可以提升 jieba 的识别正确率。

在优化分词结果这一步，你可以通过减少虚词和标点符号，通过停止词、词性的选择，来降低它们对情感分析结果的干扰。

最后，你还可以为 snownlp 增加新的流行词和网络用语，帮你更准确地分析用户对产品的喜好程度，从而提高产品定位的精确度。

在 snownlp 中，通过 train() 和 save() 两个函数把模型训练和保存之后，就能实现扩展默认字典的功能了。此外，我在工作中还会利用这种方式增加 emoji 表情对应的情感倾向分析功能，以此来进一步提升 snownlp 分析情感倾向的准确度。

我将训练模型和保存训练后的模型的函数也写在这里供你参考，希望通过训练自己的模型，能够让你的产品分析更加准确。

sentiment.train(neg2.txt,pos2.txt);  #   训练用户自定义正负情感数据集
sentiment.save('sentiment2.marshal');  # 保存训练模型

今天用到的代码，我都放在了 GitHub 上，你可以点击这个链接查看。

思考题

我给你留一道思考题，我在最后一段代码分别统计了正向和负向评价的数量，你能否根据这段代码统计一段文字中包含了多少个动词、多少个名词和多少个形容词呢？欢迎你在课程评论区留言，和我一起讨论。

07｜快速读写文件：如何实现跨文件的字数统计？

你好，我是尹会生。这节课，我们来学习下统计多个文件字数的方法。

之前我在出版图书的时候，每个章节都写入了一个单独的 Word 中，这就导致我无法使用 Word 自带的字数统计功能，一次性统计所有章节的字数，自然也就不能分别统计多文件的汉字、英语和标点符号的字数了。如果你在工作中也遇到过类似的情况，需要一次性统计大量的文本文件的字数，应该怎么解决呢?

今天我就来教你，怎样使用 Python 来批量统计多个文件的字数和标点符号。

首先呢，我们先学习一下简单的操作：统计单个文件的字数。掌握了 Python 统计字数的基本操作，批量统计也就不在话下了。

怎样实现单个文件字数统计

统计单个文件的字数，需要用到 Python 的文件读写函数和变量这两个功能。Python 的读写函数是对文件内容的读取操作，变量呢，用来存储统计好的文件字数。

我把统计单个文件字数的操作分为三个步骤，分别为：

先把要统计的文件读入内存；

再对读入到内存的字数数量进行统计，并用变量保存；

最后是将结果写入统计字数的专用文件中。

我先来带你学习一下用 Python 读取文件需要使用哪些函数和语法。

1. 把文件内容读取到内存中

Python 实现读取文件内容到内存的函数有三个，这三个函数原理相同，但是读取的内容多少有些不同，分别为：

read() ，逐个字符读取，一直读取到文件全部内容结束；

readline()，读取一行内容；

readlines()，读取多行内容。

对于统计单个文件字数的需求，选择哪个读取函数更合适呢？由于统计字数需要逐个字读取再对字数进行累加，因此，我使用 read() 函数将文件中所有的文字读入到内存中，相比按行读取，实现起来更加简单。

将文件内容读入变量后，变量中的文字内容和保存在文件中的文字内容相同，接下来我再通过统计字数的函数对变量进行统计，就能统计出一个文件里的文字有多少字数了。

具体如何实现呢？我给你举一个例子。

例如，我这里有一个需要统计字数的文件 e.txt，它的内容为：

e约等于2.718281828

核心实现代码如下：

import pathlib

file_name = "e.txt"

# 取得脚本所在目录
current_path = pathlib.PurePath(__file__).parent

# 和脚本同目录下的文件绝对路径
file = current_path.joinpath(file_name)
# 打开文件
with open(file, encoding='utf-8') as f:
    # 读取文件
    content = f.read()
    words = content.rstrip()
    number = len(words)  # 统计字数
    print(number)
    # 15

通过这段代码，你会发现，文件的读取函数是实现字数统计的关键功能。下面，我重点给你讲解下 Python 是怎么读取文件的。

Python 要想读取一个文件，需要经过打开、读取和关闭这三个步骤。

在代码的第 11 行，我通过 open() 函数实现了文件的打开功能。需要注意的是，我为 open() 函数设置了两个参数，分别是：

file 变量，表示要打开文件的名称；

encoding 变量，表示文件的编码格式。

我来解释下这两个参数。第一个参数 file 变量的值是要操作的文件路径和文件名称，你需要正确指定它所在的路径和文件名称。第二个参数 encoding 变量的值指定了以哪种字符编码打开文件，错误的字符编码会导致内容读取异常，文字内容显示为“乱码”。

我们来具体看一下第一个参数 file 变量的路径是怎么正确进行设置的。

对 file 变量进行赋值时，需要注意赋值时的路径和文件的真实路径是否一致，如果 file 变量中的路径和真实路径不一致的话，Python 程序运行的时候会提示找不到文件的错误。那应该如何确保路径正确呢?

我们通常的做法是事先在字符终端上取得要操作的文件的完整路径，再将完整路径作为字符串赋值给变量 file，这样你就可以直接使用正确的文件路径了。但是这样操作步骤繁琐，且不够灵活。所以还有一种比较灵活的方法，那就是将要操作的文件和 Python 脚本文件放在相同的目录里，通过获取 Python 脚本文件的目录，间接获得要操作的文件目录。

例如我将 e.txt 和 Python 脚本文件放在同一目录，那么我在打开 e.txt 文件时，就要先获取 Python 脚本所在的目录位置了。我这里使用了一个特殊的变量 __file__，来取得当前脚本所在的路径。

__file__ 变量比较特殊，它是在 Python 中预先定义好的一个变量，默认情况下它的值会被 Python 自动设置为当前脚本的完整路径名称。有了它，我就可以通过 Python 脚本的路径取得 e.txt 的完整路径了。获取完整路径的具体的步骤如下：

在代码中使用 pathlib 库的 PurePath() 函数，把当前 Python 脚本的路径转化为标准的 pathlib 格式。

通过 parent 属性去掉脚本的文件名，取得当前脚本所在的路径。

再通过 joinpath() 函数把脚本所在的路径和 e.txt 文件名进行拼接，得到 e.txt 的完整路径。

这种取得脚本同目录下的其他文件完整路径的用法，要比直接指定文件完整路径用法更灵活，我在后续内容中会使用这种方法处理文件的路径，请你认真掌握。

我们继续来学习 open() 函数的第二个参数。第二个参数的作用是设置 open() 函数使用何种字符编码打开文件。我使用了"encoding=utf-8" 作为 open() 函数调用的第二个参数。

不过你可能会问了，为什么指定这个参数的时候，要特意写出变量名称 encoding 呢？我在04 讲的时候，给你讲过函数有定义和调用两种用法。

在函数定义的时候我们可以为函数指定一个或多个参数，如果使用了多个参数，你在调用函数的时候需要按顺序依次传入每一个参数。但是如果你想要使用某个参数默认值或不想按照顺序传入参数时怎么办呢？函数调用时，还可以根据参数的关键字来指定为函数传入哪些参数。比如说 open() 函数的参数有 7 个，为了便于你理解关键字参数，我将 open() 函数如何定义的前四个参数写在这里，open() 函数的定义如下：

def open(file, mode='r', buffering=-1, encoding=None, ...)：
    ...

在 open() 函数的定义中，encoding 参数是 open() 函数的第 4 个参数。如果我们只需要第 1 个和第 4 个参数，其他参数需要保持默认时，就可以使用如下格式调用 open() 函数：

open(file, encoding="utf-8", ...)：
    ...

在代码中，调用 open() 函数时，由于 file 参数是 open() 函数的第一个参数，因此可以不用指定参数的关键字。encoding 参数并非 open() 函数定义的第二个参数，而是第四个参数，所以需要指定它的参数名称为 encoding，根据 open() 函数的定义，encoding="utf-8"会作为 open() 函数的第四个参数使用。这种使用关键字作为函数的参数的方式，也被称作函数的关键字传参方式。

通过 encoding 参数可以指定文件的字符编码，一般在 macOS 系统文字的编码为 UTF-8 编码，Windows 下为 GB2312 编码。不同操作系统下打开文本文件需要使用不同的文件编码，这样可以避免文件打开出现乱码的问题。

在你正确使用了 open() 函数的两个参数以后，会返回一个表示文件的对象 f, 只有设置了正确的文件路径、文件名称和字符编码，才能继续进行文件读取操作。文件的读取操作使用的是 read() 函数，read() 函数会根据文件的对象 f，按照 open() 函数定义好的打开方式进行逐个字的读取操作。Python 对文件的其他操作，全都需要通过文件操作函数调用对象 f 来完成。

另外要特别注意的是，在 open() 函数这一行的开头，我还使用了一个关键字 with，with 关键字下面的代码是缩进形式，和 with 关键字形成了一个语句块（具体参见导读），在 with 语句块结束之后，不必手动调用 close() 函数关闭文件，Python 会自动将文件关闭。因此，你会看到我在程序中调用了 open() 函数打开文件，但是没有调用关闭文件的 close() 函数。

总的来说，相对于手动调用 close() 函数来关闭文件，使用 with 函数减少了打开过多文件造成系统资源浪费和数据丢失的风险。

2. 统计文件的字数

打开了文件之后，我们就可以来统计文件的字数了，我们可以直接利用 Python 的内置函数 len() 来实现。

len() 函数在 Python 中最初设计的功能就是统计字符串的长度，即字符串中有多少字符。因此我利用 len() 函数可以对字符串数据类型进行操作的特性，将文件一次性读入内存，放入字符串数据类型中。我在统计字数之前还增加了一个 rstrip() 函数，它会自动剔除出现在文件末尾的空行、空格，让你统计的结果更精确。

在进行文件字数统计的时候，需要注意，在代码中我使用了 read() 函数实现了对文件内容的一次性读取到内存的功能。如果你的文件较大，我建议你使用 readline() 函数按行读取，并逐行统计字数，否则容易出现内存不足的问题。

3. 将统计结果写入文件

如果你需要将统计结果写入文件，可以对 open() 函数打开文件方式进行简单的修改，就可以实现文件写入功能了，写入文件的代码如下：

with open("total.txt", "w", encodong="utf-8") as f:
    f.write("15个字符")

通过上面的代码，我把"15 个字符"这串字符串写入到空白文件 total.txt 中了。

open() 函数的第二个参数表示文件的打开方式，它默认值是“r”，代表了文件以读取方式进行打开。这时对象 f 只能进行文件读取操作，我们需要将第二个参数指定为"w"，就可以使用 wirte() 函数对文件进行写入操作了。而 write() 函数的第一个参数就是即将要写入文件的内容。

在为你介绍完单个文件的读写操作之后，我带你掌握了将文件读取到字符串变量，通过统计字符串的长度计算文件字数的方法，那么接下来我为你讲解一下如何统计多个文件的字数。

怎么统计多个文件的字数？

通过单个文件的字数统计功能的实现，我们发现每次对文件进行字数统计的时候，都需要进行文件的打开、读取和关闭操作。因此我们对多个文件的字数统计，就可以使用 for 循环来实现批量读取多个文件的内容。在这里我先直接给出代码。

p = Path(src_path)
files = [x for x in p.iterdir() if PurePath(x).match('*.txt')]
for file in files:
    with open(file, encoding='utf-8') as f:
    content = f.read()
 words = content.rstrip()
      number = len(words)

我来解释一下多文件统计的代码和单文件统计代码的区别。

你会发现，统计多个文件的字数时，除了使用到第一节课讲的 for 循环之外，还要使用合适的变量数据类型，用来存储多个文件字数统计过程中的文件字数。

在导读中我也给你介绍过，Python 支持的数据类型包括：数字、字符串、列表、元组、字典五种，我在这里比较一下它们各自的特点，让你知道该怎么选择合适的数据类型来存放计算的中间结果。

首先是我们在前面几讲用到最多的数字和字符串类型，这两种类型也被称作基本类型。就像它们的名字一样，分别用来存储数值和一串字符。

数字类型是一种非常适合进行整数、浮点数的转换和算数几何计算最常用的类型。在单文件字数统计的场景，用来储存每个文件的字数是最合适的类型。如果把场景扩展到统计多个文件的字数，使用一个数字类型的变量是不够的，你需要在循环中再增加一个新的数字类型的变量，对多个文件的统计结果进行依次累加就可以了。

那其他的类型适合用来进行字数统计吗?

字符串类型会使用英文的单引号或双引号来创建一串字符，它内置的功能一般为多个字符串进行连接和截取字符串里的某些字符的，显然不适合进行字数累加的统计。

那列表和元组有什么特性呢？当你需要记录多个并列结果的数字或字符串时，我们就可以把数字和字符串放在列表和元组数据类型中。而列表和元组中的每个元素都会被分配一个位置，也有人把这里的位置称作索引，第一个元素的索引是 0，第二个是 1，以此类推。

例如对多个文件进行字数统计的场景下，我们就可以将五个文件字数统计的结果分别放入到列表当中，它的写法如下：

list1 = [15, 20, 35, 40, 50 ]

list1 就是一个列表数据类型的变量，它包含了五个元素，第一个元素或索引为 0 的元素是 15，第二个是 20。

元组和列表极为相似，不同之处在于元组的元素不能修改，这个特性在 Python 中，叫做内容“不可变”。这是它与列表的主要区别，元组查找的效率要远远高于列表，因此在进行查找操作时，经常将列表类型转换为元组类型，再进行内容查找操作。

最后一种类型，也是我们在第二讲接触过的字典类型。字典的每个键值对都用冒号进行分隔，每个键值对之间用逗号分割，字典需要被包括在花括号 {} 中，字典类型强调键值对的映射关系。如果你需要记录文件名和统计结果时，可以使用字典方式保存。保存形式如下：

dict1 = {"file1.txt":15,"file2.txt": 20, "file3.txt":35}

好了，掌握了 python 支持的五种数据类型，以及它们各自的适用场景，我把它们的特点总结在下面的表格。

那我接下来就给你分享进行字数统计时的两种常见的场景需求。

多个文件记录字数，用列表数据类型更适合

由于我们的需求是实现跨文件的字数统计，那么就需要一个数据类型来记录每一个文件的字数有多少。根据我们刚才对不同数据类型的分析，列表就是非常适合存储每个文件字数的数据类型。

在多文件字数统计的场景下，我们只需要其中的 append() 内置方法，就可以实现对列表添加元素了。如果你想了解列表类型更多的内置方法，可以参考官方文档来查看它们的定义和用法。

append() 内置方法，可以在列表的最后一个位置添加一个新的元素。例如当前统计的五个文件字数就可以采用列表方式存储，它的定义方式如下：

list1 = [15, 20, 35, 40, 50 ]

list1 变量经过赋值，得到的列表包含了 5 个数字类型。当统计第 6 个文件的字数，假设字数为“30”个字时，就可以使用 list1.append(30) 的方式在列表后增加元素。增加后 list1 变量为：

list1 = [15, 20, 35, 40, 50, 30 ]

对于多个文件的字数统计，要操作的数据类型为列表，而 len() 函数只能支持字符串，不能支持列表。所以我们需要把统计列表元素和的函数由 len() 改为 sum() 函数。另外，sum() 是能够直接支持列表类型的求和函数，所以这 6 个文件的字数，就可以使用 sum() 函数进行累加，从而得到多个文件的总字数了。它的代码如下：

sum(list1)

上面就是我们通过 for 循环和列表数据类型实现了对多个文件字数的统计和累加。接下来我们把需求再扩展一下，如果我需要分类统计，比如说想要统计中文、英文、标点符号各自的数量，那应该选择什么样的数据类型呢?

扩展需求：统计中文、英文和标点符号各自的数量

想要实现中、英文和标点符号各自的数量统计，我们就需要考虑继续使用列表数据类型是否能够存储各自的数量统计结果？是否有更适合的数据类型？

关于存放每个文件的统计结果，我有两种考虑。

一种是中、英文和标点符号各自的数量统计以后，需要比列表更合适的数据类型，用来存放每个文件的统计结果。

一种是每个文件计算字数之后，如何和前一个文件的字数进行累加。

先来看第一种。

要想统计中、英文和标点符号各自的数量，你需要正确地书写每个字符。在 Pyhon 字符串类型中，有区分每个字符类型的内置函数。

因此，我使用字符串类型先对每个文件包含的字符进行中文、英文、数字、空格、特殊字符的划分，并使用数字类型的变量对每种类型的数量进行了记录。

我将实现这一功能的核心代码贴在这里供你参考：

import string
for s in str:
        # 英文
        if s in string.ascii_letters:
            count_en += 1
        # 数字
        elif s.isdigit():
            count_dg += 1
        # 空格
        elif s.isspace():
            count_sp += 1
        # 中文
        elif s.isalpha():
            count_zh += 1
        # 特殊字符
        else:
            count_pu += 1

在这段代码中，我使用了 for 循环来遍历每个字，然后通过 if 分支结构进行判断，根据不同的类型对每次指定的类型进行累加。这样就完成了每个文件对字符数量的统计。

完成统计后，又该如何对它们进行保存呢？我这里使用了字典 + 列表的方式，用列表存储每个文件每一种字符的数量，为了能区分列表，我为它们分别取了一个名字，并把它们统一存储在字典当中。形成了字典的 key 是字符串，value 是列表的数据类型，它的格式如下：

{'count_en': [7, 7], 'count_dg': [0, 0], 'count_sp': [1, 1], 'count_zh': [2, 4], 'count_pu': [5, 5]}

如何得到这种格式呢? 为了得到这种格式，我继续使用了列表的内置的 append() 函数，通过下面的代码实现了每个文件的每种类型分开进行统计的功能。

  word_count["count_en"].append(count_en)
    word_count["count_dg"].append(count_dg)
    word_count["count_sp"].append(count_sp)
    word_count["count_zh"].append(count_zh)
    word_count["count_pu"].append(count_pu)

总结来说，在进行不同类型的字数统计的过程中，我通过字符串和数字这两种数据类型实现了单个文件的字数统计。在多个文件的数量合并功能中，我又使用了字典和列表两种数据类型实现了多个文件中间结果的存储和最终字数统计。

Python 丰富的数据类型，可以让你更灵活地处理工作中的数据。

我将完整代码放在 github 中，你可以参考完整代码来理解我是如何在计算过程中使用字典、列表和字符串类型的。

总结

通过跨文件字数统计这个场景，我为你讲解了 Python 的文件操作函数和如何选择更合适的数据类型。

在进行文件的读写操作时，你需要注意被读写文件的路径、打开模式、字符编码这三个主要问题。同时，这三个问题也是新手在操作文件时会经常出现的错误的地方。

尤其是打开模式的错误的设置为“w”将重要文件覆盖写入的问题，在 python 初学者中经常出现，建议你先采用判断文件是否可读取的方式，避免对重要文件进行误写。

现实工作中的场景往往是比较复杂的，我们经常要根据不同的场景，把文件内容读取到不同的数据类型的变量中。Python 丰富的数据类型可以让你更灵活的操作工作中的数据，通过选择合适的数据类型，也能使用各种数据类型自带的函数，减少手动实现自定义函数的代码，节省你的代码编码时间。

思考题

最后我想留一道思考题给你，在进行多个文件的中、英文和标点符号各自的数量统计时，你会选择哪种数据类型，用来存放每个文件的字数统计数据呢？欢迎你来说出自己的想法。

08｜正则表达式：如何提高搜索内容的精确度？

你好，我是尹会生。

开始上课之前，我想先带你看两种常见的工作需求：

领导让你搜索出一个文档里所有的手机号码， 而你只知道手机号的模式是 11 位数字，那该怎样搜索呢？

你需要在一个文档中搜索一串带区号的电话号码，比如 010-12345678，099-1234567 ，不过在这个文档中，有很多组电话号码，它们的区号和电话号码长度都不同，你该怎么一次性把它们全部搜索出来呢？

其实，这两种场景具有一个共同点：就是你要一次搜索出所有的结果，且不能使用具体的数字，这该怎么办呢？答案是使用某种模式进行搜索。

就像搜索手机号，我可以使用 11 位数字这种模式。搜索电话号码我就可以用 3-4 个数字或 7-8 个数字加上“-”的模式。这就是利用模式匹配的方法把手机和电话号码从文档中找出来，提高文本搜索的效率。

对于模式匹配，使用 Python 的正则表达式是最便捷、也是最有效率的。这节课，我就来手把手教你用正则表达式来提高搜索效率。

使用正则表达式进行搜索的整个过程

首先，我先带你来看下使用正则表达式进行搜索的整个过程，利用正则表达式可以按照模式来匹配搜索结果，而且比自己编写函数搜索功能更加强大, 而且实现起来也更加简洁。

要实现搜索，我们就要用到 Python 的正则表达式库（叫做 re 库）中的 search() 函数，这是利用正则表达式实现搜索的核心函数。如果能够搜索到匹配模式的结果，就返回匹配字符串出现的文件的位置，以及匹配的字符串内容。接下来我们重点看一下它的参数。

re.search() 函数有三个参数，它的前两个参数我们在工作中较为常用。

参数 pattern：代表要匹配的模式。

参数 string：是要匹配的字符串。如果模式能够匹配成功，则会返回一个 re 的对象，re 对象里存储的是匹配位置和匹配内容；如果匹配失败，就会返回空，在 Python 中用变量“None”表示空。

参数 flags：表示 search() 函数在匹配之前可以进行各种特殊处理，比如使用 search() 函数进行搜索之前，可以通过 flag 参数忽略要匹配字符串的大小写功能。

那具体怎么使用呢？我以搜索手机号码为例来给你解释下。我可以使用 re.search() 按如下的代码搜索一串手机号码：

re.search("[0-9]{11}","13855556666")
<re.Match object; span=(0, 11), match='13855556666'> #执行结果

代码的第一行，search() 函数是一个基于正则表达式进行搜索的函数，它的第一个参数是正则表达式，第二个参数是要匹配的文字，目前它只有一个手机号码。

第二行代码是 search() 函数执行的结果。因为匹配成功之后会返回比较丰富的信息，所以我们需要根据搜索结果来不断优化正则表达式，达到精确匹配的目的，但是搜索结果还很复杂，所以我要带你分步骤地去分析匹配的结果都包含了哪些主要内容。

第一步：观察匹配的结果。

如果模式匹配成功，search() 函数会返回一个 re 对象。如果匹配不成功的话，它的返回结果则是 None。我们可以利用不同的返回结果，使用 Python 的 if 判断条件来控制程序的后续运行逻辑。

第二步：观察字符串的子串是在哪些位置被匹配成功的。

代码中的 span=(0, 11) 表示第 1 个字符到第 11 个字符匹配成功。这里需要注意的是 Python 字符串的下标是以 0 开头的。通过下标，你可以确认匹配的内容是否是你编写的模式，如果匹配的位置或者内容不正确，可以通过下标来帮你定位匹配错误的位置，以便优化匹配模式。

最后一步，观察匹配成功的子串是否是我们想要查找的字符串内容。

如果是的话，那么此次查找结束。如果不是，我们就需要根据匹配的位置和结果，使用 search() 函数重新编写正则表达式。

这就是使用正则表达式进行搜索的整个工作过程。那接下来我们就来学习一下正则表达式的两种匹配方式。

两种匹配方式

编写正则表达式之前，一定要根据字符出现的不同模式来选择不同的匹配方式。匹配方式分为精确匹配和模糊匹配两种。

精确匹配需要在匹配前，就知道将要匹配的字符是什么，且出现了多少次。这类匹配使用的是一组元字符。模糊匹配是在匹配前不清楚出现的字符具体内容或不清楚字符的重复次数，这种匹配需要使用另一组元字符。我们先来看看精确匹配方式。

精确匹配

在手机号码搜索的场景中，我使用了正则表达式 “[0-9]{11}”，你可以看到，我除了用数字外，还使用了很多特殊字符。这些特殊字符有一个专用的名字，叫做元字符，正则表达式就是利用元字符进行模式匹配工作的。

在正则表达式“[0-9]{11}”中包含了两组元字符，“[]”和“{}” 。

“[]”的作用是进行内容模式匹配；

“{}”的作用是指定匹配长度。

我们先从[0-9]来认识元字符吧。

在正则表示式“[0-9]” 中， “[]”表示同一个位置可能出现的各种字符。例如：我需要从文档中匹配到一个数字，可以使用[1234567890]的形式，也可以使用[0987654321]的形式。而在[]中，字符排列的顺序并不会影响它的功能，出现重复字符也不会影响它的功能。

不过为了代码更整洁，我不建议你在[]中使用重复的字符。

使用[]对一个数字进行匹配的时候，如果从 0 写到 9，一一列出比较麻烦，所以在[]中还支持一个“-”符号，叫做连字符。把连字符与数字写在一起，表示数字的范围。当然，你肯定会想到还有英文字母的场景，比如从 A 到 Z 或者从 a 到 z，表示英文字母的范围。具体写法如下：

[0-9]  #匹配任意一个数字
[a-zA-Z] #匹配任意一个字母

除了可以自己手写匹配字母和数字外，还有另外一种写法，就是把我们经常需要匹配的字符进行定义。我为你提供一张 POSIX 字符组合和 ASCII 字符组合的常用对照表，帮你来记住它们。

在你掌握了元字符“[]”的用法之后，我们接着来看怎么通过“{}”来限定“[]”的匹配长度。

在正则表达式 “[0-9]{11}”中， 我使用了{11}来限定它前边的字符出现 11 次。{}还可以使用 **{m，n}的形式，表示限定的次数从 m 到 n 次 **。这是在匹配不确定字数的情况下，经常用到的写法，例如进行电话号码的区号匹配，可以使用“[0-9]{3,4}”的形式匹配 3 个或 4 个数字。

使用“[]、{}” 两种元字符，属于精确的模式匹配。如果你在编写正则表达式之前就知道，即将匹配的字符是数字还是英语，以及它们出现的次数，那就使用这两个元字符对将要进行搜索字符进行匹配。

模糊匹配

不过还有一种情况就是，你只知道某个元素会重复出现多次，但是在匹配前不知道具体会出现多少次，或者只知道要匹配的是五个字符，但是不知道这五个字符到底是数字、汉字还是英文，这种匹配的行为就叫做模糊匹配。

例如下面三个元字符“+”、“*”、“?”可以实现出现次数的模糊匹配功能。

“+”表示前边的元素出现的是1 到无穷多次。

“*”表示前面的元素出现的是0 次到无穷多次。

“？”表示前面的元素出现的是0 次或一次。

我以正则表达式“ab+c”为例，它表示在这个模式中 b 至少出现了一次，最多出现无穷多次。也就是说，我们能够通过 search() 函数搜索到 abc、abbc、abbbc、abbbbc 等模式。

如果匹配的模式再复杂一些，假设说在匹配之前我们只知道第一个字母是 a，最后一个字母是 b，中间是五个任意的字母。在这种情况下，就需要使用元字符“.”符号， 它在正则表达式中能匹配任意的单个字符。

为了让你更好理解元字符“.”， 我们就来看一个搜索任意 5 个字符的例子，代码如下：

re.search(".....", "aaa13855557890bbb")
re.search(".{5}", "aaa13855557890bbb") # 也可以这样写

上面代码匹配出的结果是"aaa13"。

这里需要提醒你注意，如果你想匹配的字符串只包含 5 个字符，那就需要增加匹配的限定条件的元字符“＾”和“$”。“＾”表示从第 1 个字符开始匹配。“$”表示从结尾开始匹配。所以你需要将正则表达式"....."改为"＾.....$"形式，才能实现只匹配 5 个字符的模式。

通过上面的“[] {} . ? * +”这六种元字符组合，你就能够使用 search() 函数通过正则表达式搜索到大部分的模式了。

提取和替换：如何处理搜索到的结果

当你搜索到你想要的文字之后，往往还需要进行后续的内容提取和替换处理，这些后续处理如果手动完成也属于低效的工作。因此，接下来我来教你怎样将 search() 函数搜索到的内容进行提取，以及如何使用 sub() 函数实现将正则表达式匹配到的内容进行替换。我们先来看怎么进行内容的提取。

以电话号码为例，如果需要将搜索的结果提取出来，可以使用 group() 函数，进行 search() 函数搜索之后的内容提取操作。

我们先来看一下提取搜索结果的例子：

import re
re.search("[0-9]{3}-[0-9]{8}",
      "我的电话号码:010-12345678.").group(0)
'010-12345678' # 执行结果

通过 group(0) 函数的参数“0”，如果搜索过程中能够匹配成功，search() 函数就会把匹配到的第一个字符串作为执行结果，返回给用户继续进行自动化处理。

再进一步，如果我要替换匹配到的字符串，那就需要把 search() 函数改为 sub() 函数。sub() 函数包含三个参数，依次是：要匹配的正则表达式、匹配后被替换的字符串、匹配的原始文档。

那我就以替换字符串为例，来教你怎么把文档中的 yes 字符串替换为 no 字符串。代码如下：

re.sub("(Y|y)(es)*", "No", "aayesbb")
'aaNobb' # 执行结果

在这段代码中，为了将“Y、yes、Yes”，三个字符串都替换为“No”。同时，为了不用多次执行 sub() 替换函数，我使用了“()”分组功能和“|”选择功能。那么通过 (Y|y) 这一写法，就能实现第一个字符匹配“Y”和“y”；利用“()”分组功能，使用“(es)*”正则表达式实现了第二、三个字符是“es”或是没有字符的功能。

总结来说，通过 group() 函数和 re.sub() 函数，我们可以在搜索之后自动化实现内容的提取和替换操作。

小结

在最后我来为你总结一下搜索中的常见问题，以及对应的解决方案。

如果遇到了大量的需要按照某种模式才能得到搜索结果的场景，你可以在第一时间采用正则表达式的方法进行内容的匹配。如果字符长度固定，可以使用精确搜索的元字符“[]”和“{}” 。反之，在字符长度不固定的情况下，那就使用模糊搜索的元字符“+”、“*”、“?”“.”“＾”和“$”。

正则表达式的元字符组合非常灵活，为了方便你记忆，我把它们分为三类：

匹配单个字符，要使用“[ ]”和“.”元字符。

控制元字符出现次数，要使用“？”、“+”和“*”元字符。

控制元字符的顺序和位置，要使用“＾”、“$ ”、“ | ”和“ ()”元字符。

在掌握正则表达式的正确编写基础上，你就可以通过 re 库的函数实现按模式搜索、内容提取和内容替换了。正则表达式不但在 Python 中能够使用，它还能在搜索引擎以及大部分的编程语言中使用，被广泛应用与字符串搜索和替换工作中。

我把这节课相关的代码给你放在了 GitHub 中，你可以参考。

思考题

最后我给你留一道思考题，你将如何使用 re 库的 search() 函数，实现身份证的匹配和提取功能呢?

09｜扩展搜索：如何快速找到想要的文件？

你好，我是尹会生。

工作时间越久，你就会发现，自己电脑里存储的办公文件也越来越多。一旦需要紧急找到某个文件，你又恰巧忘记了文件被存放的具体位置，就只能通过 Windows 的搜索功能对文件名称进行搜索，不过你大概率需要长时间等 Windows 的搜索结果。

之所以查找文件的时间过长，主要有两个原因。

搜索范围太大。搜索过程中可能包含了大量的操作系统或应用软件的缓存文件等无效路径，导致搜索时间过长。

受到硬盘文件数量过多的影响。硬盘的文件数量越多，完整搜索一遍所有文件的时间就越长。

那有没有办法提高搜索的效率，快速找到你想要的文件呢？基于以上两种原因，相应的，我们可以在 Python 中采用指定搜索路径范围和提前建立文件索引的两种方案实现文件的搜索功能。

这两种方案都是基于 Python 的基本搜索方法实现的，因此我先来带你学习一下如何用 Python 的 pathlib 库实现基础文件的搜索。

基础搜索方法：用 pathlib 库搜索文件

用 Python 搜索文件时需要使用 pathlib 库的 glob() 函数和 rglob() 函数，glob() 函数可以实现基于文件名的搜索方法，rglob 函数可以实现基于扩展名的搜索方法。

我先来给你看一下 Python 实现基础搜索功能的代码，通过代码来为你讲解一下 Python 是如何搜索到一个文件的：

from pathlib import Path

base_dir = '/Users/edz/Desktop/'
keywords = '**/*BBC*'

# 遍历base_dir指向的目录下所有的文件
p = Path(base_dir)

# 当前目录下包含BBC的所有文件名称
files = p.glob(keywords)
# files的类型是迭代器
# 通过list()函数转换为列表输出
# print(list(files))

# xlsx结尾的文件
files2 = p.rglob('*.xlsx')
print(list(files2))

# 遍历子目录和所有文件
files3 = p.glob('**/*')
print(list(files3))

在这段代码中，我实现了 Python 的基础搜索功能。由于搜索的优化要基于 glob() 函数和 rglob() 函数进行，为了能更好的提升 Python 的搜索的效率，我来带你逐一分析一下 glob() 函数和 rglob() 函数的参数和返回值。

首先我来带你看一下 glob() 函数和它的参数, 由于 glob() 进行匹配的是文件的路径和文件名称方式，如: "c:\somepath\to\filename_include_BBC_voice.exe" , 而我们进行文件搜索的时候一般会使用关键字，如“BBC”，因此在搜索时我们需要为关键字加上通配符的形式，如“BBC” 。

通配符是类似正则表达式的元字符的一种特殊符号，它不能用在正则表达式中，只能用在 glob（全称 global）匹配模式中。

我将 glob() 和 rglob() 函数常用的通配符，以及它们的功能整理成一个表格，供你学习和参考。

通过表格我希望你能掌握如何将通配符和要搜索的关键字进行组合。比如说，通过使用“**/BBC”的方式，我就可以搜索到 Path() 函数指定目录下所有包含 BBC 关键字的文件名。

接下来我再来带你学习一下 rglob 函数和它的参数。rglob 函数是从文件路径末尾向前进行匹配的，这是它和 glob() 函数的主要区别, 基于 rglob() 函数的搜索顺序特点，经常被我们用于进行扩展名的搜索，比如说采用 rglob('*.xlsx') 就可以搜索所有的 xlsx 扩展名文件，要比使用 glob() 编写的模式匹配更简单，参数的含义也更清晰。

最后我再来带你看一下 glob() 和 rglob() 函数的返回值，有一点我需要提醒你：它们的执行结果是我们之前课程中没有接触过的一种新的数据类型，这种类型叫做“迭代器”。

顾名思义，迭代器的特点是这个数据类型可以支持迭代操作，执行一次 glob() 或 rglob() 函数只返回一个结果。要想得到迭代器所以的值可以使用两种方法。

一种是使用 list() 函数将迭代器转换为我们所熟知的列表数据类型，例如我在列表中就是用了“list(files3)”方式将迭代器转换为了列表。

还有一种方式是使用 for 循环的方式对迭代器的值进行逐一处理。

这两种用法不但可以获取 glob() 返回值，今后我们遇到的迭代器都可以使用 list() 函数和 for 循环取得它的全部值。

通过对 glob() 和 rglob() 函数的参数和返回值的学习，相信你已经掌握了使用功能 Python 搜索文件的基础方法，接下来我来带你通过指定搜索路径和建立索引文件提高搜索的效率。

提升搜索效率的两种方法

用 Python 的 pathlib 库实现文件搜索，只是在灵活性上比 Windows 默认的搜索更好，但是搜索效率上并不能带来任何提高。为了减少搜索的等待时间, 接下来，我就教你使用指定搜索路径和建立索引文件两个方法, 提高 pathlib 库的搜索效率。

指定搜索路径

我们先来看第一种，指定搜索路径。我们需要通过三个步骤实现：

先生成配置文件，把要搜索的路径写入到配置文件中；

再编写读取配置文件和搜索的自定义函数，把配置文件中的路径读取出来，逐个目录搜索；

最后，将多个目录的搜索结果合并输出，便于你通过结果快速找到自己想要的文件。

先说第一步，怎么使用 Python 读取配置文件。以往我们会把要搜索的路径写入到变量，并把定义路径的变量名称放在代码前几行的位置，便于下次修改搜索目录的时候找到这个变量。但是对于代码工程稍微复杂的程序来说，往往会有多个代码文件，仍然不利于每次搜索的时候进行搜索路径的修改。

现在我教你一个新的方法，就是把变量放入到一个单独的文件中，这个文件被称作该代码的配置文件。这种方法的好处是你修改搜索目录时不用打开代码文件。假设你的朋友也需要类似功能，那你就可以把代码和配置文件一起发给他，哪怕他完全不会 Python，也能使用你编写的程序实现高效搜索。

那怎么给 Python 脚本增加配置文件呢？

配置文件一般为文本文件。配置文件的格式，一般由软件作者基于软件的功能和自己的习惯来指定，不过也有通用的配置文件格式。比如在 Windows 系统中，最常见的配置文件是扩展名为.ini 的文件，在今天这节课，我们就把.ini 文件格式作为配置文件的标准格式。

.ini 文件格式包含三个部分，分别是节、参数和注释。格式如下：

节
[section]
参数
（键=值）
  name=value
注释
注释使用“;”分号表示。在分号后面的文字，直到该行结尾都全部为注解。
;注释内容

基于.ini 文件的格式，我把配置搜索路径的配置文件修改为如下：

[work]
;工作文件保存路径
searchpath=/Users/edz,/tmp

[game]
;娱乐文件保存路径
searchpath=/games,/movies,/music

在这段代码中，我设置了 work 和 game 两个“节”，分别代表工作和娱乐。这样设置的好处是，我可以根据不同的用途来搜索不同的目录。如果搜索时使用了更少的目录，也会相应减少搜索的等待时间。

另外，你会发现两个“节”中的参数我都指定成相同的名字 --searchpath，这样设置的好处是我将搜索范围从“工作”改为“娱乐”时，只需要在代码里修改搜索的“节”，不用修改搜索的参数。

除了“节”和“参数”，在配置文件中，你还应该关注我对参数 searchpath 设置值的方式，它的值是我想要进行搜索的路径范围，为了在程序中能够更方便得读取多个路径，我使用逗号来分隔多个路径。

在编写好配置文件之后，下一步就是取得.ini 文件的完整路径，我把.ini 文件和脚本放在了相同的目录，我在第七讲为你详细剖析过，怎样通过 __file__ 变量和 pathlib 库相结合，实现读取和脚本在同一目录的文件操作，我就不多说了，如果你不太记得了，可以回去复习下。

找到 search.ini 文件完整路径之后，接下来需要读取并分析.ini 文件格式，Python 有实现这个功能的的库，它叫做 configparser 库，通过这个库你可以直接读取.ini 文件中的 searchpath 参数，不用通过 read() 函数读取文件内容，手动编写分析.ini 文件的脚本了。

接下来，我来带你看一下 Python 读取.ini 文件的代码，帮你理解 Python 是怎样通过配置文件来实现在多个路径搜索文件功能的。

import configparser
import pathlib
from pathlib import Path

def read_dirs(ini_filename, section, arg):
    """
    通过ini文件名,节和参数取得要操作的多个目录
    """
    current_path = pathlib.PurePath(__file__).parent
    inifile = current_path.joinpath(ini_filename)

    # cf是类ConfigParser的实例
    cf = configparser.ConfigParser()

    # 读取.ini文件
    cf.read(inifile)

    # 读取work节 和 searchpath参数
    return cf.get(section, arg).split(",")

def locate_file(base_dir, keywords):
    p = Path(base_dir)
    files = p.glob(keywords)
    return list(files)

dirs = read_dirs('search.ini', 'work', 'searchpath')
# ['/Users/edz', '/tmp']
keywords = '**/*BBC*'

# 定义存放查找结果的列表
result = []

# 从每个文件夹中搜索文件
for dir in dirs:
    files = locate_file(dir, keywords)
    result += files

# 将PosixPath转为字符串
print( [str(r) for r in result] )

在这段代码中，读取配置文件和搜索这两个最主要的功能由两个自定义函数实现，它们分别是 read_dirs() 函数和 locate_file() 函数。

read_dirs() 函数实现了读取.ini 文件，并将返回的多个路径处理为列表类型。列表类型适合多组并列的数据，多个目录刚好可以使用列表这种数据类型来存放要搜索的目录名称。

locate_file() 函数通过代码的第 35 行循环功能，对每个目录进行了搜索，并将搜索的结果存入 result 变量。result 变量是一个列表数据类型，由于搜索到的文件可能包含多个匹配的文件路径，我需要将搜索到的结果依次存入 result 列表中，再继续搜索下一个目录，继续通过 append() 函数将结果放入列表，直到所有的目录搜索完成，整个搜索的程序才真正执行结束。

最后还有一点需要你注意，在进行路径处理的过程中，pathlib 库为了规避不同操作系统路径写法的差异，就把路径统一定义为 PosixPath() 对象。因此，你在使用这些路径的时候，需要先将 PosixPath 对象转换为字符串类型。我在代码最后一行通过 Python 内置函数 str() 函数把 PosixPath 对象逐个转换为字符串类型，并再次存入到列表当中。

通过限制在指定的目录搜索这个功能，我们就可以规避在搜索时错误匹配系统文件和软件缓存文件而导致的查找过慢问题。

不过如果我们指定的目录仍然有非常多文件的话，那用这个方法查找起来依然会很慢。别担心，接下来我就教你一种利用空间换时间的方法，提高对指定目录文件搜索效率的方法。

建立索引文件

什么是“空间换时间”呢？我来解释一下。

我们知道，文件越多，搜索的时间就越长，因为搜索的时间是随着文件数量呈线性增长的。就像一本书越厚，你从第一页读到最后一页的时间就越长一样。那怎样能快速搜索到书中的内容呢？你会想到图书都有目录功能，通过目录可以加快你找到你想看的图书内容的速度。

对于操作系统来说，一个文件也是由文件名称、大小、文件内容等多个部分组成的，搜索文件的功能只需要文件名称就行了，不需要其他的部分。因此，我们可以在硬盘中新开辟一块空间，将所有的文件名提前存储下来作为文件的索引，下次再查找的时候直接查找索引文件，就能得到搜索结果，而不必再通过硬盘查找真实的文件了。这就是“空间换时间”。

既然索引文件可以加快搜索速度，那我们去建立索引文件就可以了。

建立索引文件不需要你重新再写新的程序，我们可以基于指定搜索路径的程序进行改造：

先把配置文件目录下所有文件路径的保存方式由列表改为文件；

再把搜索功能改为从文件搜索。

我把改造后的代码写出来供你参考。

def locate_file(base_dir, keywords='**/*'):
    """
    迭代目录下所有文件
    """
    p = Path(base_dir)
    return p.glob(keywords)

def write_to_db():
    """
    写入索引文件
    """
    current_path = pathlib.PurePath(__file__).parent
    dbfile = current_path.joinpath("search.db")

    with open(dbfile, 'w', encoding='utf-8') as f:
        for r in result:
            f.write(f"{str(r)}\n")

# 读取配置文件
dirs = read_dirs('search.ini', 'work', 'searchpath')

# 遍历目录
result = []
for dir in dirs:
    for files in locate_file(dir):
        result.append(files)

# 将目录写入索引文件
write_to_db()

在代码中我增加了 write_to_db() 函数，它在代码的第 16-18 行，我通过写入文件方式替代了写入列表的功能。同时，为了能遍历所有的目录，我还修改了 locate_file() 函数的第二个参数，将它改为“keywords='/*'”。通过这两处的修改，就把所有文件路径全部保存到 search.db 文件中了。**

search.db 的文件内容如下，这里记录了配置文件指定的所有目录下的所有文件路径：

/tmp/com.apple.launchd.kZENgZTtVz
/tmp/com.google.Keystone
/tmp/mysql.sock
/tmp/com.adobe.AdobeIPCBroker.ctrl-edz
/tmp/com.apple.launchd.kZENgZTtVz/Listeners
/tmp/com.google.Keystone/.keystone_install_lock
... ...

接下来，我再把搜索功能从列表搜索改造为从文件搜索，为了避免每次搜索要重新生成一次 search.db 文件，我要编写一个新的脚本，专门用于从文本中搜索关键字，并将搜索的结果显示出来。

相信你一定想到了我在上一讲为你讲解的正则表达式功能，通过 re.search() 函数刚好可以实现文本的搜索。下面的 locate.py 脚本文件就是我使用正则表达式实现的文本搜索功能：

import pathlib
import re

keyword = "apple"

# 获取索引文件路径
current_path = pathlib.PurePath(__file__).parent
dbfile = current_path.joinpath("search.db")

# 在索引文件中搜索关键字
with open(dbfile, encoding='utf-8') as f:
    for line in f.readlines():
        if re.search(keyword, line):
            print(line.rstrip())

在代码中我利用正则表达式的 re.search() 搜索函数，以 keyword 变量作为搜索的关键字，对 search.db 索引文件的每一行进行了匹配，最后将符合关键字“apple”的文件路径和名称一起显示在屏幕上。

使用这种方式来搜索文件，要比使用操作系统自带的搜索工具快得多，因为我将原本 Windows 搜索硬盘上的文件所消耗的时间拆分成了两部分。

一部分是 updatedb.py 建立索引的时间；

一部分是从 search.db 索引文件查找关键字的时间。

当搜索等待的时间被提前转换为建立索引的时间后，搜索的效率自然就提高了。

但是请你注意，这种方式建立的索引文件会有时效性的问题，一旦硬盘中的文件被删除、移动或改名，你就得重新建立索引文件。而索引文件保存的文件路径和当前真实的文件并不能一一对应，所以你需要及时更新索引文件。

为了保证索引文件的时效性，你还可以把 updatedb.py 脚本加入到 Windows 的开机启动脚本中，每次打开计算机就会自动更新索引文件。通过这种方式就可以让索引文件里的文件路径更加准确。

总结

最后，我来为你总结一下。这节课，我为你讲解了如何使用 pathlib 库搜索文件，以及如何使用配置文件和索引文件加快搜索。

利用索引文件减少等待时间，实际上是将 Windows 的搜索进行了拆分，提前将搜索的路径保存到了索引文件中，从索引文件搜索时，就不用再进行真正的文件查找工作了，这就减少了搜索的等待的时间。

这种搜索方法在服务器领域已经被广泛使用，像 Linux 和 MacOS 操作系统中都存在着利用索引文件的搜索命令，对于服务器上变化频率较低的场景，利用索引文件搜索非常实用。

思考题

我给你留一道思考题。如何扩展 locate.py 的搜索功能，实现搜索.jpg 扩展名的文件呢?

欢迎把你的思考和想法分享在留言区，我们一起交流讨论。如果课程帮你解决了一些工作上的问题，也欢迎你把课程分享给你的朋友、同事。

10｜按指定顺序给词语排序，提高查找效率

你好，我是尹会生。

之前我在游戏行业工作的时候，经常需要取得用户在线时长 TOP3、用户战斗力 TOP5、用户完成任务数量 TOP10 等数据，相信你在工作中也会有遇到从大量数据取得 TopN 这类需求。

提取 TopN 的操作，本质上是对大量数据先进行排序，然后根据排序的结果取出前 N 个值。而实现提取 TopN 的值，用 Python 来实现会非常高效，这节课，我就来讲一讲具体怎么操作。

使用 sorted() 函数实现排序

在 Python 中，已经内置了排序函数 sorted() 函数，它是 Python 中实现排序最简单且最直接的形式，可以解决 80% 的排序问题。那么，我们就来学习一下怎么用 sorted() 函数对常见的数据类型进行排序。

先看 sorted() 函数的定义。

sorted(iterable, cmp=None, key=None, reverse=False)

sorted 函数共有四个参数，第一个参数是要排序的对象，后面三个参数是排序的方式。

如果要为某个对象排序的话，你可以直接将它作为 sorted() 函数的第一个参数，返回结果会将对象的值进行从小到大的排序。

如果 sorted() 返回的排序结果不满足你的需要，比如你想要从大到小的排序，那你就可以利用后面三个参数来改变排序的方式，实现自定义排序。

所以今天这节课，我会带你学习怎么使用 sorted() 函数实现默认排序和自定义排序，来解决你实际工作中遇到的多种排序问题。我们先来看怎么使用 sorted() 实现默认排序。

默认排序

sorted() 函数的默认排序是按照从小到大的顺序进行排序的。例如一家公司即将上市，需要在职的前 1000 名员工的工号和姓名，工号越小的员工配股越多。这时候就需要给所有员工的工号进行排序，并取得工号排在前 1000 个员工的名字。具体怎么做呢？

我们来看 sorted() 函数的用法。我们从它的第一个参数开始学习。第一个参数是 sorted() 函数要排序的对象，我以列表为例，把员工的工号放到列表中，我们来看一下 sorted() 函数是怎么对列表中的工号进行从小到大的排序的。

sorted_list = sorted([30, 50, 20, 10, 40])
print(sorted_list)
# [10, 20, 30, 40, 50]

我在代码中给 sorted() 函数指定了一个列表类型的参数“[30, 50, 20, 10, 40]”。按照 sorted() 函数的定义，这个列表会被 sorted() 函数按照数字从小到大进行排序，并返回排序后的结果。并且，我把结果放入 sorted_list 对象中，你从代码中可以看到 sorted_list 已经把数据按照默认从小到大的顺序排列好，并把结果放在一个列表数据类型中。

这就是 sorted() 的基本用法，其实是比较简单的，不过这里有三点需要你注意。

首先，sorted() 函数的默认排序方式是从小到大进行排序，那么对于列表中的数字就会按照默认的算法进行大小比较。但是除了数字外，列表中还会出现字母或字母与数字混合的方式，你也需要掌握这两种方式的排序处理方法。

比如说英文字母，会按照列表中字符串的第一个字母从 A 到 Z、从 a 到 z 进行排序。如果字符串的第一个字母相同，就会比较第二个字母，以此类推，直到比较完字符串中的所有字符。

由于默认排序的时候不支持同时对英文字母和数字进行排序，那么如果列表中既包含了字母，又包含了数字，在默认排序时程序就会提示一个异常，那就是无法使用“<”比较整数和字符串。

TypeError: '<' not supported between instances of 'int' and 'str'

因此，当列表中出现类似“["a", 1, "bb"]”这种，既包含字符串又包含数字的列表，进行排序操作的时候，你需要先统一类型，把数字使用 str() 函数转换为字符串类型，再对列表进行排序。我把代码写出来，供你参考：

sorted_list2 = sorted(["a", str(1), "bb"])
print(sorted_list2)

其次，你需要注意，sorted() 函数不会对原有的列表进行修改，它会把排序好的结果存入到一个新的列表当中。

很多人刚开始使用 Python 时，会把 sorted() 函数和列表自带的函数 sort() 函数混淆。

它们虽然名字很像，但支持的数据类型、调用的方式以及返回的结果都不同。

要记住，sort() 函数是列表数据类型自带的，所以只能对列表数据类型进行排序，不能对其他数据类型排序，但 sorted() 函数可以支持任何可迭代的对象。

在调用方式上，sort() 函数使用的方式是“列表.sort()”格式，这也是调用时和 sorted() 函数的差别。

还有一个最大的差别是，sort() 函数会直接修改当前列表（这种修改称作“原地修改”），并返回一个空值 None。而 sorted() 函数不会对原有的列表进行修改，它会把排序好的结果存入到一个新的列表当中。

为了让你更好地区分 sorted() 函数和 sort() 函数，我把 sort() 函数的执行结果也提供给你做参考。

list3 = ["a", "c", "bb"]
no_value = list3.sort()
print(list3)
# ['a', 'bb', 'c']  执行结果
print(no_value)
# None  执行结果

在代码的第二行，是 sort() 函数的调用格式。在第三行，我输出了排序之后的列表，并对列表的值进行了重新排序。在第五行，我们可以看到 no_value 变量的返回值为 None，也就是 sort() 函数会对列表进行原地修改，并使用 None 作为返回。

最后，你需要注意 sorted() 函数能够支持的数据类型非常多，既能支持基础数据类型，又能支持 Python 自带的内置函数。根据它的函数定义，第一个参数是“iterable”对象，表示只要该对象可迭代，sorted() 函数就能对它进行排序。

在基本数据类型中，序列和映射都可以迭代，序列是指列表、元组、字符串这三种基本数据类型的总称，映射就是我们使用过的字典。除了基本数据类型之外，像是 range() 、map()、zip() 等内置函数都是可迭代对象，因此掌握 sorted() 函数，可以对以上多种类型和函数进行排序。

掌握了这三点注意事项，在使用 sorted() 为可迭代类型进行排序时，就不会出现什么问题了。那接下来，我来带你学习一下它的自定义排序功能，通过自定义排序，可以让 sorted() 基于不同的数据类型实现更加灵活的排序。

自定义排序

自定义排序是在基本排序基础上，能够支持更多的排序方式和更复杂的数据类型。比如说：

在排序方式上，我们通过参数，可以把默认的从小到大的排序改为从大到小。

在对数据类型的支持上，像列表中包含元组这种复杂数据类型，可以通过指定元组的任意一列进行排序。

我们先学习自定义排序是怎么支持更多的排序方式的，它的前提是要先更改默认的排序方式。

例如我想要实现列表的从大到小的排序，并提取 Top 3 这一需求，我首先需要使用函数的第四个参数 reverse 参数改变默认的排序方式，排序后我需要使用列表的切片方式提取前三个元素。

我先把代码写出来，然后再给你详细解释。可以看到，我在代码中使用了 sorted_list4 作为排序后的结果，并对 sorted_list4 进行切片，从而得到指定下标对应的值。

sorted_list4 = sorted([30, 50, 20, 10, 40], reverse=True)
print(sorted_list4[:3])
# [50, 40, 30]

在代码中，我为 sorted() 函数增加了参数 reverse。reverse 参数在定义的时候是 sorted() 函数的第四个参数，由于 sorted() 函数的第二、三个参数我希望保持默认，不需要在调用的时候传入，因此 reverse 参数在调用时就要作为第二个参数来使用。

由于在调用时它的位置和定义的位置不同，那么我必须使用关键字参数 “reverse=True”的形式，把 reverse 指定为 sorted() 函数的第二个参数。

增加 reverse 参数之后，sorted() 执行的结果也和默认结果不同，sorted 会将默认排序的结果进行反转。而当五个数字的排序结果反转后，也就以从大到小的方式进行了输出。

你看，利用 sorted() 函数的 reverse 参数就能实现 TopN 场景的排序工作，工作中往往需要只得到 TopN 的结果，不需要将全部的排序结果进行输出，这时候你可以将列表按照你的工作需要提取其中的某一部分，这种操作也被称作列表的切片。

假如你想取得 sorted_list4 列表的 Top3 元素，可以对列表排序后，使用 sorted_list4[0:3]的写法提取列表前三个值，这种写法的“0”“3”表示列表的下标，“:”表示获取下标的范围。因为列表的下标从 0 开始，因此要写作[0:3]的形式，也可以使用它的省略形式[:3]，这样列表切片操作就会从 0 开始取值。

总结来说，通过 reverse 参数和列表切片，我们可以在默认排序的基础上，实现基于列表的 TopN 场景下的排序，以及 TopN 结果的提取。

不过现实场景往往都是比较复杂的，要排序的数据类型除了列表中包含数字和字符串外，你最经常见到的还有对列表中包含元组这种复合类型进行排序，以及对字典的键或值进行排序。接下来我来带你学习一下两种主要类型，一种是列表 + 元祖的类型，一种是字典类型。

列表 + 元组的排序方式

列表 + 元组形成的组合数据类型，适合存放包含多个属性的对象。我给你看个例子，

“姓名、性别、身份证号”和“学校、姓名、学号”这两组数据就适合用元组存储。因为元组存储的值是不可变的，而这些数据填入之后一般不会修改，刚好和元组的特性吻合。

而且，这些用户信息在工作中往往会被大量存储在一起，为了便于对它们进行排序和查找。你还需要将多个元组再保存到列表当中，这样就形成了列表 + 元组这种组合类型。

对列表 + 元组的形式进行排序，就需要用自定义排序的字段来实现。那就是在默认排序的基础上，增加 key 参数，并通过 lambda 表达式取得元组第三个位置的学号。

lambda 表达式是简化自定义函数的定义和调用而使用的一种语法，使用 lambda 表达式取得学号的字符串之后，sortd() 就可以实现对元组按照学号进行排序了。

在这里，key 没法通过下标对元组排序，而必须通过函数取得参数具体的值。这是 sorted() 函数为了更灵活地实现排序功能，把设置排序关键字这一功能全部开放给用户造成的。事实上，这也是一种典型的通过牺牲易用性来增加灵活性的设计模式。

这一知识点比较难理解，不过不用担心，我在后续课程陆续为你讲解这类函数，直到你能熟练使用它们为止。

到这里，我们就了解了 sorted() 函数的 key 参数必须使用函数做参数的原因，那就继续来看 key 参数的 lambda 表达式是怎么简化自定义函数的。

比如我想取得元组中包含“Jerry”的学号“1003”，以及包含“Tom”的元组学号的“1005”，可以使用这样一段代码来实现函数定义和调用方式：

def s(my_tuple):
    return my_tuple[2]
sorted(students, key=s)

sorted(students, key=lambda s: s[2])

在代码的 1-3 行、第 5 行，分别是通过自定义函数和 lambda 表达式实现的提取元组第三个参数的功能。对比来看，自定义函数的定义要比 lambda 表达式复杂。自定义函数的定义和调用部分在 Python 中要分开编写，这也是简单函数更适合用 lambda 表达式编写的原因。

lambda 表达式通常在函数只有一行语句，且不需要强调函数名称的时候使用，因此它还有一个名字叫做匿名函数。它的结构只包含四部分，即lambda 关键字、需要接收的参数、一个冒号和对接收参数的处理，并且它会把处理结果自动返回。

因此在 sorted() 函数中通过 lambda 表达式实现按关键字排序，会比使用自定义函数排序更加简洁，所以当你遇到只有一行语句的函数场景时，可以考虑使用 lambda 表达式替代自定义函数的定义和调用。

字典类型的排序方式

除了列表 + 元组的复合类型外，我们经常还需要对字典类型进行排序，字典类型包含键和值，所以排序的时候可以基于键来排序，也可以基于值来排序。

我们还是以学生信息为例。我把学生的姓名定义为字典的键，把学号定义为字典的值。接下来我们看看 sorted() 是怎么对字典的键和值进行排序的，实现排序的代码如下：

student_dict1 = {'Jerry':'1003',
                 'Tom':'1005',
                 'Beta':'2001',
                 'Shuke':'2003'
}

# 输出字典的键和值
print(student_dict1.items())
# dict_items([('Jerry', '1003'), ('Tom', '1005'), ('Beta', '2001'), ('Shuke', '2003')])

# 按照字典的键排序
print(sorted(student_dict1.items(), key=lambda d: d[0]))
# [('Beta', '2001'), ('Jerry', '1003'), ('Shuke', '2003'), ('Tom', '1005')]

# 按照字典的值排序
result = sorted(student_dict1.items(), key=lambda d: d[1])
print(result)
[('Jerry', '1003'), ('Tom', '1005'), ('Beta', '2001'), ('Shuke', '2003')]
# 将结果转换为字典
print(dict(result))
# {'Jerry': '1003', 'Tom': '1005', 'Beta': '2001', 'Shuke': '2003'}

在这段代码中，我实现了基于字典的键和字典的值进行排序的功能。由于字典是使用键值对的形式存储数据的，所以我先通过字典自带的函数 items()，把键值对的形式转换成列表 + 元组的形式。经过转换以后，字典的键就变成了元组的第一个参数，值就变成了元组的第二个参数。

因此在代码中，我把参数 key 的值设置为“lambda d: d[0]”，这样就取得了元组的第一个元素，排序之后就实现了按照字典的键进行排序的需求。相应的，使用“lambda d: d[1]”，可以取得元组第二个元素的值，就能实现按照字典的值进行排序的需求。

由于排序后的数据类型已经变成了列表 + 元组形式。所以我们在代码最后，可以通过 dict() 函数把排序的结果再转换为字典，和排序前的数据类型保持一致。

这就是通过 sorted() 函数对字典进行排序的解决方法，你在工作中或许还会遇到和这节课不一样的数据类型，但是它们的解决思路是相通的。

如果能转换成列表，可以采用更改 lambda 下标的方式，实现对指定字段的排序。

如果不能转换成列表，可以尝试将复杂的类型中，不需要进行排序的部分进行删减，简化成列表或字典类型，这样就也能使用 sorted 实现数据的排序功能了。

小结

最后，我来给你总结一下这节课的核心内容。我们通过 sorted() 函数实现了列表、列表 + 元组、字典类型的排序，通过排序后的结果，你可以快速提取 TopN，也可以利用你学习过的其他语言的算法实现更复杂的查找。

在 sorted() 函数中，我们通过编写 key 参数的值，使用了 lambda 表达式替代了简单的函数，让我们的程序更加简洁。当你在日后的工作场景中也出现了只有单个语句的函数时，也可以考虑采用 lambda 表达式替代自定义函数，增加代码的可读性。

正是因为 sorted() 功能的强大，我们在 python 中实现排序几乎不需要通过自行编写代码来实现排序，不过你在进行海量数据的时候，我还想给你提供两条有用的建议。

第一，如果对包含海量数据的列表进行排序时，建议将列表通过 tuple() 函数转换为元组，能够让查找效率有较大提升。

第二，在 Python 标准库 collections 库中提供了 OrderedDict 扩展数据类型，它的特点是对 OrderedDict 数据类型进行赋值时，会自动进行排序。当你需要一个有序字典时，可以考虑选择 OrderedDict 作为数据存储的类型，从而避免手工对数据排序。

我也把这节课的相关代码放在了GitHub上，你可以去学习查找。

思考题

在最后，我想给你留一道开放性的问题。为什么 Python 语言不将所有的数据类型都设计成默认有序的，即存入数据时自动进行排序？欢迎你说出自己的理由。

欢迎把你对问题的思考和想法分享在留言区，我们一起交流讨论。如果课程帮你解决了一些工作上的问题，也欢迎你把课程分享给你的朋友、同事。

11 ｜通过程序并行计算，避免CPU资源浪费

你好，我是尹会生。

在我为运营工作提供技术咨询的时候，遇到过这样一个场景：这场运营活动，需要在电脑和手机端的多个不同应用程序，同时推送产品宣传图片和视频。这些大量的图片需要有不同的格式和尺寸，视频也需要根据不同的 App 截取不同的时长。

如果这类需要大量计算的多个任务成为你的日常工作，会花费你不少的时间和精力。不过别担心，我们可以通过程序并行计算，来提升任务效率。

不过你可能会说，用 Python 自动化执行，也可以提高计算效率啊，那为什么还要学习并行计算呢？

要知道，Python 默认的自动化只能利用 CPU 的一个逻辑核心，如果采用并行计算，那就能够最大化地利用 CPU 资源，从而成倍提升大量计算的任务效率。接下来我就详细分析一下并行计算的高效之处。

为什么要进行并行计算

还是我在开头提出的运营工作的场景。如果你从这类任务消耗计算机主要资源的角度去考虑，会发现这类需求有两个共同的特点。

第一，它们都需要进行大量的计算，而计算主要是通过 CPU 来实现的。CPU 的硬件指标上有两个和计算效率最相关的概念，分别是主频和多核。

主频决定 CPU 处理任务的快慢，多核决定处理的时候是否可以并行运行。这和生活中超市的收银一样，收银员的工作效率和超市开放了多少个收银台的通道，都决定了你能否以最快的速度购买到你想要买的商品。

第二，这些任务往往都需要运行相同的程序，但是程序的参数却需要根据不同的需求进行调整。

虽然咱们可以使用 Python 自动化执行这些程序，从而减少手动操作时间，但是我们还可以利用 CPU 的多核特性，让程序尽可能并行执行，发挥 CPU 的全部计算能力，提高运行效率。

那么接下来，我就来教你怎样利用 Python 的多进程库，来实现程序的并行计算，以及怎么提高并行计算的效率。

怎样实现并行计算

要想实现程序的并行计算，需要使用到标准库中的 multiprocessing 多进程库。你可能会问，进程是什么呢？

进程，是计算机用来描述程序运行状态的名词。一个进程在运行时需要消耗一定的资源，包括 CPU 的时间、内存、设备 I/O 等。如果两个进程互相独立，在同一个任务处理过程中，没有前后依赖关系，那你可以利用 multiprocessing 库同时运行多个进程，这样就能成倍地减少多个任务执行的总时间。

接下来，我就以计算 1-100 的平方为例，看看怎么使用 multiprocessing 实现并行计算。代码如下：

from multiprocessing import Pool

# 计算平方
def f(x):
    return x*x

with Pool(8) as p:
    # 并行计算
    res = p.map(f, range(1, 101))
    print(f'计算平方的结果是:{res}')

在这段代码中，我通过 Pool 包的 map() 函数来求 1 到 100 平方计算，由于每次计算平方的过程和下一次计算没有直接关联，我就可以使用并行的方式进行计算，提高计算效率。

为了让 map() 函数能够实现并行计算，我们必须在使用它之前，通过 Pool() 包为它指定并行计算的进程数量，设置要执行的函数名称 f，以及 f() 函数所需参数。那么接下来，我就带你学习一下我是怎样使用 with 语句来设置函数的参数，并正确执行 map() 函数的。

首先来看最关键的 map() 函数，它是 Pool 包实现并行计算的函数之一。在代码中我为 map() 函数赋值了 f 和 range() 函数两个参数。

第一个参数是函数对象。

函数对象会作为 map() 函数创建进程以后，即将执行的主要任务。因此，由于这里的含义是指定 f 对象将要被创建的进程执行，而不是将 f() 函数执行的结果作为新的进程执行，所以第一个参数必须使用函数对象 f，而不能使用 f() 函数。

第二个参数要求必须是可迭代的对象。

例如我在代码中需要为 f 函数传递参数为 1-100 的整数，就可以使用 range() 函数产生 1 到 100 的整数并直接返回，因为它的返回值就是可迭代对象。

如果参数不是数字，就可以采用列表、元组、字典等支持迭代的数据类型，代替 range() 函数，作为 f() 函数的参数。举个例子，如果你需要并行调整多个视频的时长，就可以采用字典存储路径和要调整的视频时长，并把这个字典作为 map() 函数的第二个参数，map() 函数会为字典的每个键值对创建一个进程来并行处理它们。

接下来是 map() 函数中的三个主要部分，我来分析一下它们各自在并行计算中的功能。

第一，with 语句。这是我们在第七讲学习怎么使用 Python 打开文件之后，第二次用到 with 语句了。

和文件操作类似，进程打开后也需要妥善关闭，但是进程关闭属于较为底层的操作，如果你不进行操作系统层面的程序设计，是不需要对关闭进程的函数进行修改的，因为使用默认关闭进程的行为，就能满足编写并行计算的需求。

因此，multiprocessing 库对 Pool 包，支持了比较友好的进程打开和关闭方式，即 with 语句。也就是说，multiprocessing 库把对进程的操作写在 with 语句块中，而 with 语句就会自动处理进程的打开和关闭，这样在实现并行计算的代码中，你就不用学习进程的基本操作，也能减轻你学习并发程序的负担。

在了解了 with 语句可以操作进程的打开和关闭后，我们来看代码中我是怎么使用 with 语句的。

我在代码中使用了“ with Pool(8) as p ”这条语句，这里的 Pool() 类是多进程库支持的进程池功能，它的作用是指定一个多进程的程序，最多能够并行执行的进程数量。它的参数“8”，表示 map() 函数最多同时运行 8 个进程。

剩下两个部分是 range() 函数和 f() 函数。

range() 函数的作用是产生 1-100 的整数，这些整数会在每次创建新的进程时，依次作为 f() 函数的参数并赋值。而 f() 函数得到参数后，会把计算结果返回给 map() 函数。当 f() 函数处理完所有的参数后，map() 函数还会返回一个列表作为运行的结果，并进行输出。

以上就是实现并行计算的主要过程。

如何提高并行计算的效率

我们除了需要掌握并行计算的基本方法外，还可以继续提升并行计算的效率。所以在程序中还有两个地方需要优化。

一个是为并行程序自动指定并行度。在并行计算的基本方法中，我使用了手动指定并行度的方式，来指定进程最多能够运行多少个。不过手动指定的并行度并不能适合所有的电脑，因此就需要根据计算机的 CPU 核数设置合理的并行度。而且，每台计算机的 CPU 资源是固定不变的，那么设置合理的进程数量能让你的并行计算任务充分利用 CPU 资源。

另一个是统计程序运行的时间。当你对并行计算的数量做了修改后，那程序是否对计算效率起到了提升效果呢？就还需要更精确的测量，这样才能得到更准确的结果。所以我们还需要使用 Python 统计出程序执行过程一共消耗了多长的时间。

我们先来看怎么自动设置适合你的电脑的并行度。

为并行程序自动指定并行度

计算类的任务包括数字计算、数据统计、视频的编解码等，都属于计算密集型应用，它们最大的资源开销就是 CPU 时间的消耗，设置的并行度过大或过小都不能达到最好的运行效率。

如果并行度设置过小，比如运行的进程数量小于逻辑 CPU 的数量，就会造成部分逻辑 CPU 因为无法被充分利用而处于闲置状态。

如果并行度设置过大，由于现代的操作系统为了保证每个进程都能公平得到 CPU 资源，所以会造成 CPU 把时间大量消耗在进程切换上。那么并行度设置过大，会导致 CPU 还未完成一个进程的处理时，就得切换至下一个进程进行处理，而多进程之间来回切换也会消耗 CPU 时间，造成 CPU 资源的浪费。

那并行度该怎么设置才合理呢？通常情况下，我们会把并行度设置为逻辑 CPU 数量的两倍。不过假如计算任务达到小时级别（这类任务需要长时间占用 CPU 资源），为了减少切换任务时的开销，我建议计算的并行度和逻辑 CPU 数量保持相等。

这就又有一个问题了，该怎么获得计算机的逻辑 CPU 个数呢？Windows 可以通过任务管理器获得，MacOS 可以通过活动监视器获得。如果你希望取得逻辑 CPU 的个数之后，可以根据它的数量自动设置创建进程的数量，那么可以通过安装第三方包 psutils，利用其中的 cpu_count() 函数取得逻辑 CPU 个数。

我把并行度自动设置为当前逻辑 CPU 两倍的代码写在下面，供你参考。

from multiprocessing import Pool,Queue
import os
import psutil

# 逻辑cpu个数
count = psutil.cpu_count()

# 定义一个队列用于存储进程id
queue = Queue()

# 用于计算平方和将运行函数的进程id写入队列
def f(x):
    queue.put(os.getpid())
    return x*x

with Pool(count*2) as p:
    # 并行计算
    res = p.map(f, range(1, 101))
    print(f'计算平方的结果是:{res}')

# 并行计算用到的进程id
pids = set()
while not queue.empty():
    pids.add(queue.get())

print(f'用到的进程id是: {pids}')

在代码中，我使用了 psutil.cpu_count() 函数来获取逻辑 CPU 的个数，它把“count*2”作为参数传递给 Pool() 类，并以逻辑 CPU 两倍作为最大创建进程数量，从而计算 1-100 的平方。

这里有两点需要你注意。第一，psutils 是 process and system utilities 的缩写，所以它除了获取逻辑 CPU 数量外，还可以获取内存、硬盘、网络连接等和操作系统相关的信息。如果你在工作中需要取得操作系统的运行状态，就可以采用 psutils 包。

第二，psutils 是第三方库，因此，在 Windows 上你需要通过 cmd 命令行执行 pip3 install psutil 安装后，才能释放 psutils 包，否则会出现模块无法找到的错误。

由于 map() 函数的第二个参数可能会被传入不可迭代对象，这时有可能会导致只运行了一个进程，因此我就在多进程执行过程中，增加了记录进程 ID 的功能。而在这一功能中，我使用的是 os 库、队列库和集合数据类型，按照下面三个步骤来实现对所有创建的进程 ID 的统计。

首先，使用os 库的 getpid() 函数获取进程 ID。

由于 map() 函数会根据 Pool() 类的参数，事先创建好指定数量的进程，而每次运行 f() 函数都在创建好的进程中执行，所以我就采用 os 库的 getpid() 函数取得运行 f() 函数进程的唯一标识，这就是使用 os 库的用途。

接下来，使用队列库存储每次运行进程的 ID。

为了把每次运行的进程 ID 存到一个对象中，我使用了 multiprocessing 库的队列包。因为在多进程的程序中，不能采用标准数据类型来传递数据，所以 multiprocessing 库还提供了方便进程间通信的对象——Queue 队列。

map() 函数每执行一次 f() 函数，我就把进程 ID 作为队列的 put() 函数的参数，并把进程 ID 放入队 Queue 中，直到所有的 f() 函数执行完成，队列里就会记录每次执行的进程 ID 信息。

最后，使用集合数据类型存储本次 f() 函数运行的所有进程 ID。

为了实现这一功能，我需要通过 while 循环结构，根据队列不为空的条件，把队列中的进程 ID 使用 get() 函数取出来，放入 pids 变量中。

pids 变量是集合数据类型，集合是一个无序的不重复元素序列，需要使用 set() 创建。你可以把集合当作一个只有键没有值的字典来记忆，它的特点是集合里的元素不能重复。

由于 f() 函数会多次在一个进程中执行，因此在队列中会记录重复的进程 ID，我把进程 ID 从队列中取出后，放入集合数据类型中，自己就不用编写程序，自动把重复的进程 ID 去掉了。而且通过对集合 pids 中的进程 ID 进行输出，可以看到进程 ID 的数量刚好和 Pool() 类指定的并行进程数量相等。

这种用法是我经常在进行多进程程序调试的一种简单用法，我还会把它们的结果写入文件保存，以便程序出现异常执行结果时，可以根据调试的信息进行问题的定位。

统计程序运行的时间

我们除了需要掌握判断程序的并行度外，还可以统计并行计算比顺序计算节省了多少时间。那么再遇到相同场景的时候，你可以选择并行方式来运行程序，提高工作效率。接下来我来教你怎样统计 Python 程序运行的时间。

在 Python 中我们可以利用 time 库的 time() 函数，来记录当前时间的功能。

首先，需要在统计时间代码的前后各增加一次 time.time() 函数，并把它们统计时间的结果存放在 time1、time2 两个不同的变量中。

然后再把两个变量相减，这样就能取得程序的运行时间了。

我把核心实现代码写在下面供你参考。

# 并行计算时间统计
with Pool(4) as p:
    # 并行计算
    time1 = time.time()
    res = p.map(f, range(1, 10001))
    time2 = time.time()
    # print(f'计算平方的结果是:{res}')

print(str(time2-time1))

# 串行计算时间统计
list1 = []

time1 = time.time()
for i in range(1, 10001):
    list1.append(f(i))
time2 = time.time()

print(str(time2-time1))

在这段代码中，通过 time1 和 time2 的时间差就可以得到程序运行的时间了，那么根据运行时间，我们可以把并行程序和串行程序执行时间的性能进行对比。

这里你需要注意，由于计算平方的 CPU 开销较小，比较难体现并行计算的优势，你就可以采用并行访问网页，或其他 CPU 开销较高的程序，这样会让两个程序的时间差别更加明显。

总结

在最后，我来为你总结一下实现并行计算的基本方法和三个注意事项。

通过 multiprocessing 的 Pool 包可以实现基于进程的并行计算功能，Pool 包的 map() 函数会根据 Pool 包指定的进程数量实现并行运行。这里还有三点需要你注意：

作为 map() 函数的第一个参数你需要传递函数对象 f，不能传递函数的调用 f() 形式，这是初学者实现并行任务最容易出现的错误。

为了让并行度更适合你的电脑，应该根据逻辑 CPU 的个数设置并行度，并根据运行时间来对并行数量进一步优化。

实现并行计算任务的程序除了使用多进程模型外还可以使用多线程模型。多进程的并行计算更适用于计算密集型应用，即程序运行过程中主要为计算类 CPU 开销大的程序，多线程模型适合 I/O 密集型的应用，例如: 通过互联网进行批量网页访问和下载。如果你想将多进程的并发模型改为多线程的并发模型只需在导入库的时候将“multiprocessing”改为“multiprocessing.dummy”就能实现多线程并行访问网页。我将多进程和多线程两种方式导入库的代码贴在下方供你参考。

# 多进程模型
from multiprocessing import Pool

# 多线程模型
from multiprocessing.dummy import Pool

# multiprocessing.dummy的Pool用法和multiprocessing库相同

我把这节课的相关代码放在了GitHub上，你可以自行查找、学习。

思考题

我为你留一道思考题，有一个软件包 requests，可以通过 requests.get('http://www.baidu.com').text 方式访问一个网站，并能够得到网页的源代码。假设我为你提供了几十个需要访问的网站，你是如何实现这些网站的并行访问的，你又能否通过 Python 对比出逐个访问网页的时间是并行访问的几倍吗？

12｜文本处理函数：三招解决数据对齐问题

你好，我是尹会生。

当你在工作中利用 Excel 向同事展示业务数据时，肯定遇到过数据无法对齐的问题。

比如在展示日销售额报表数据时，如果数字、日期、文字这些常见的元素没有对齐，你不仅会因为格式显示混乱而难以分析数据变化的趋势，而且也会因为报表格式不够工整，给领导留下一个技术能力不扎实的不良印象。

而且，如果一个个手动来调整格式，不仅麻烦，而且还很容易出错。那么今天这节课，我就教你怎样用 Python 的文本处理函数，利用 format()、split()、join() 和 strip() 四个函数来对 Excel 中的数字、日期和文字进行长度调整和移除多余空格等处理，解决数据对齐问题。

数据对齐的思路

为了更好地掌握 Python 的文本处理函数，我先带你了解 Excel 中数据的默认对齐方式，以及需要使用 Python 中的哪些文本函数。

Excel 中的数据是有默认对齐方式的，对齐的方式一共有三种，分别是右对齐、左对齐和居中对齐。数据类型不同，默认对齐方式也不同。

数值型数据会自动靠右对齐，比如日期、时间、数字。

文本型数据会自动靠左对齐，比如汉字、字母、英文、引号开头的数字。

当你把数据粘贴到 Excel 中时，如果是同类型数据没有实现对齐，你就要考虑在粘贴到 Excel 之前对数据进行处理。

具体怎么处理呢？我们可以根据 Excel 的默认对齐方式，以及导入到 Excel 中的内容，来分情况考虑。

在右对齐的类型中，数字、日期、时间是我们接触最多的数值类型，而数字和日期、时间的处理方式又有差别，所以我们要分开来学习和掌握它们。

在左对齐的类型中，文字、字母、英文和引号开头的数字，它们的对齐方式相同，都是去掉前方的空格。所以在你掌握文字前后的空格处理之后，就可以举一反三地对其他三种数据类型实现左侧对齐。

还有最后一种对齐方式是居中对齐。居中对齐的场景只有一种，那就是作为主题使用。这种对齐方式可以直接通过手动操作来完成。

因此，我通过对数字、日期、文字三种类型的处理，可以让你应对大多数数据对齐的场景了。总结来说，那就是：

采用 format() 函数，实现数字对齐；

使用 split() 和 join() 函数，实现日期右侧对齐；

使用 strip() 函数，实现文本型数据的左侧对齐。

接下来我就详细讲解一下这三种解决方法。

数据对齐的办法

第一招 使用 format() 函数，实现数字对齐

数字没有对齐，主要是因为数字位数不同。所以我们可以通过补全数字位数的方法，来对齐数字。

我们来看一个例子。在这个例子中，我提供了 5 个浮点数，它们的内容和格式如下：

12.34
123.456
1.2345
123456.78
123.4

在这组浮点数中，它们各自的小数位数也不同。如果直接粘贴到 Excel，会因为小数位数不同而无法直观地比较它们的大小。

在这种情况下，你可以使用 Python 的内置 format() 函数，把它们都保留至小数点后四位，这样既能保证每个浮点数的精度，又能按右侧对齐小数点。

那我对这 5 个浮点统一使用 format() 函数进行处理，它的代码是：

string1 = 123.45
print("{:.4f}".format(string1))
# 123.4500

这段代码把“123.45”使用 format() 函数进行格式处理后，就会保留小数点后四位，输出“123.4500”。所以如果把浮点数用 format() 函数进行处理，再粘贴到 Excel 中，就可以实现自动右侧对齐了。

那我们接下来就学习一下 format() 函数的调用方式和格式，看format() 函数是怎么来补齐数字小数的位数，并且调整整数的位置的。

format() 函数是 Python 的 2.6 版本新增的内置函数，它增强了字符串的格式处理功能。你应该想到了，format() 函数是内置函数，所以它和我们之前学习过的 print()、int()、str() 函数一样，可以在 Python 中直接调用，不用预先定义和 import 导入。

使用 format() 函数对数字格式调整时，调用 format() 的 Python 语句是“{: 数字的格式}.format(要转换格式的数字)”，在这条语句里，“:”后面的数字的格式包含三个部分。

第一部分表示符号和空格，可以使用 +、-、(空格)。

“+”表示在正数前显示 +；

“-”表示负数前显示 -；

（空格）表示在正数前加空格。

第二部分表示宽度，可以使用. < > ^, “.”来保留小数点后的位数。

.4 表示保留小数点后四位；

^, <, > 分别是居中、左对齐、右对齐，后面也需要使用数字指定宽度，如果不满足宽度自动补充空格。

第三部分表示类型，常见的有“b、d、f”，分别表示二进制、十进制和浮点数。比如我可以使用如下代码，来表示整数 100 占用 6 个字符的宽度，并靠右侧对齐。

print("{:6>d}".format(100))
#执行结果
(空格)(空格)(空格)100

可以看到，format() 函数有着非常丰富的参数，除了可以进行数字对齐外，还可以调整字符串格式，所以我也经常用 format() 函数在工作中进行格式化字符串操作。不过这些参数和数字对齐的关系不大，我在这节课就不把它们一一列出来了，如果你想详细了解，可以参考官方文档。

接下来我们来看一下如果是日期类型，怎么使用字符串函数实现它的右侧对齐。

第二招 使用字符串函数，实现日期右侧对齐

日期对齐和数字对齐相比，最主要的区别就是它的年、月、日三个部分都可能出现不等长的情况。我给你举个例子，你一看就明白了。

2021-3-18
21-3-18
2021-12-21
2021-3-18
21-3-1

在这 5 个日期中，由于年、月、日的位数不同，导致粘贴到 Excel 后没法实现右侧对齐。因此我们实现日期对齐的前提，就是要补齐日期。而补齐日期的过程，我们需要按照先拆分日期、再调整格式、最后合并日期的顺序。

我先把它的代码写出来，供你参考，然后再给你讲解补齐日期的过程。

date_demo = [
    "2021-03-18",
    "21-3-18",
    "2021-12-21",
    "2021-3-8",
    "21-3-1",
]

for  dd in date_demo:
    # 拆分日期
    year, month, day = dd.split('-')
    # 调整格式
    if len(year)== 2 :
        new_year = 2021
    else:
        new_year = year

    month = "{:>02d}".format(int(month))
    day = "{:>02d}".format(int(day))
    # 合并日期
    new_date = [str(new_year), str(month), str(day)]
    new_date = "-".join(new_date)

    print(new_date)

# 执行结果
2021-03-18
2021-03-18
2021-12-21
2021-03-08
2021-03-01

通过这段代码的执行结果，可以看到我把长度不同的 5 个日期，按照年、月、日三个部分别进行了长度处理，并使用“-”作为日期分隔符，从而实现日期的等长。

接下来，我按照程序的执行顺序，为你详细讲解一下我是怎么对日期进行拆分、调整和合并的。

为什么要先进行拆分这一操作呢？因为在我们已知的函数中，没法对年、月、日分别进行调整。如果整体进行调整，比如调整年，很容易影响到月和日的数字，我把它称作调整的副作用。所以我就需要通过 split() 函数先对完整日期进行拆分。

split() 函数是我们这节课要重点掌握的一个函数，我来为你讲解一下它的主要用法。

按照学习其他函数的经验，你需要掌握 split() 函数的功能、参数和返回值。实际上对于所有函数的学习，我们都需要先关注函数功能、参数个数和类型、返回值个数和类型三个部分，之后再去学习参数函数里的业务逻辑功能。这其实也是掌握函数的通用做法。

split() 函数是对字符串进行拆分的函数，也是根据字符串中指定的分隔符，它的参数“-”就是拆分字符串的分隔符，它的返回值是拆分以后字符串形成的列表。

我在返回值中使用了连续赋值的功能，那么就可以通过以下代码来实现：

year, month, day = dd.split('-')

这种方式，让变量 year、month、day 依次得到了列表中的三个元素。

由于在代码的后续运行过程中，我需要对每个变量进行单独处理，而使用列表还需要通过下标访问每个列表。那么这里我就用三个变量代替列表来作为 split() 函数的返回值。不过我用变量代替列表还有一个原因，那就是在代码的阅读上，使用变量方式更易于理解。

接下来是调整内容阶段，我依然使用 format() 函数对日期的每个部分进行长度上的调整。你可以通过代码来学习，如何使用 format 进行日期的每个部分调整的。

最后，我把调整好的年、月、日，使用 join() 函数进行合并。为了和原始字符串保持一致，我继续使用“-”作为连接符号，对函数的参数 new_date 列表进行连接。而且，join() 函数返回的类型是字符串，刚好和处理之前的类型保持一致，如果你的代码里包含了对日期进行操作的功能，调整长度后这些代码就可以保持不变。

此外，这里还有两点需要你注意：

工作中经常会遇到一个字符串中多处需要修改的内容，例如刚才举的日期例子，你可以把 split() 和 join() 函数配合使用，拆分成多个变量，对变量进行处理后，再进行合并，减少处理的复杂度。

在使用 split() 前，你还可以通过正则表达式来处理不规范的分隔符，让字符串每个字段都能按照相同的分隔符进行拆分。

第三招 使用字符串函数，实现文本型数据的对齐

Excel 中的文字默认是左侧对齐，那文字类型为什么会出现没法对齐的情况呢？主要是因为文字在开头或结尾包含了一个或多个空格。

对于这些空格，可以使用字符串自带的 strip() 函数来去掉。我把删除文字前后空格的代码写在下方，供你参考：

string = "   广东省广州市   "
newstring = string.strip()
print(f"|{newstring}|")

# 输出结果
|广东省广州市|

在这段代码中，我通过内置的 strip() 函数，去掉了字符串前后的空格，并通过 f-string 调整了字符串的格式。

strip() 函数是我们这节课新学习的函数，它的主要功能就是自动移除字符串开头和结尾指定的字符，如果没有为 strip() 函数指定参数，默认会移除字符串开头和结尾连续的多个空格。

另一个新学习的函数是 f-string，我在代码的第三行使用了它，用于调整 newstring 字符串的输出结果。f-string 是在 Python3.6 版本引入的一种新的字符串，它的写法是在字符串前增加 f 关键字，增加后我们就能在一般的字符串中使用“{}”关键字，“{}”中允许使用变量、运算以及和 format() 相同的格式描述符。

为了让你更直观地看到空格是不是被移除掉了，我还利用 f-string 在 newstring 字符串前后增加了“|”，这里可以用任意一个字符，主要用于观察竖线和文字间是否有空格。这样你通过结果，就可以看到“|”和“ 广东省广州市 ”前后的空格已经被自动删除掉了。

可以看到，f-string 和 format() 函数都具有对字符串输出格式调整的功能，不过前者更适用于字符串和变量连接的场景，后者更适用于调整字符串的格式。此外，f-string 除了可以用来观察文字对齐的结果外，你还可以在输出变量的同时也输出字符串。

小结

最后，我来为你总结一下这节课的核心内容。今天这节课我们主要讲了四个字符串处理函数，它们也是文本处理工作中最经常用到的函数：

format() 函数，可以对字符串或数字进行格式化，对浮点数的小数位数进行调整，实现小数的对齐。

split() 函数，按指定的分隔符分隔字段，实现字符串中的部分字符串处理。

join() 函数，能够将列表、元组中的每个元素按照指定的分隔符连接成字符串，如果是字符串和变量的连接，你还可以使用 f-sring 方式实现字符串连接功能。

strip() 函数，它是自动去掉字符串前后空格的函数，经常用于从文本提取内容后，对文本前后空格进行删除，优化文字内容显示结果的函数。

我把今天这节课用到的代码都放在了 GitHub 上，文稿里也有链接，你可以去学习掌握。

思考题

按照惯例，最后我来为你留一道思考题。如果你需要将文本文件中的“小时: 分钟: 秒”处理成等长格式，再存入 Excel 中，你需要使用哪些函数做怎样的处理，才能实现时间的对齐呢?

欢迎把你的思考和想法写在评论区，我们一起交流讨论。此外，你还可以扫描课程详情页的二维码，加入我们的课程读者群，我也会在群里为你解疑答惑。

13｜Excel插件：如何扩展Excel的基本功能？

你好，我是尹会生。

我们在讲了怎么利用 Python 优化 Excel 的输入和计算效率之后，相信你已经知道该怎么使用 Python 的循环和文件库进行多个文件的处理，怎么使用正则表达式对内容进行查找和替换。

但是有时会因为临时的需求，为了得到查询的结果而进行一次性的表格处理，这时候如果用 Python 来解决，估计你有一种“杀鸡用牛刀”的感觉，未免小题大做了。

所以在接下来的三节课里，我会利用 Excel 中自带的插件和更简单的两个脚本工具，来完成这种临时的、简单的重复性任务，让你用更便捷的方式实现办公自动化。

今天这节课呢，我就给你介绍一个在 Excel 中非常著名的 Power Query 插件，利用这个插件你可以完成我们经常遇到的数据清理工作。

Power Query 的主要用途

我先来介绍一下 Power Query 这个插件。从它的名字，你应该就能猜到它的主要用途，那就是在查询方面对 Excel 进行优化。

我所说的查询优化是泛指，它的涵盖范围比较广。为了方便使用 Excel 统计数据，往往需要在统计数据前去调整 Excel 表格的格式、内容以及字段类型，这些在 Excel 中统称为查询操作。

如果能够把新增数据自动更新到已经处理的数据中，还能自动化地按照之前的操作步骤对表格中的数据进行调整，这就是 Power Query 比手动调整 Excel 更有效率的地方。

在查询大量数据中，最耗时的就是清理数据工作了，具体来说，就是把需要统计的数据从多个文件进行提取和合并，把不符合统计格式的数据进行自动化处理，最终形成符合数据统计的规范格式。

比如：数据被存储到多个文件里，而你需要把多个文件的内容手动合并到一个 Excel 中。再比如：Excel 中的日期包含了年月日和具体时间，如果要按日期汇总这些数据，那你就需要先把年月日和时间拆分。不方便的是，如果表格里的数据更新了，你就必须要重新再手动拆分一遍。

其实这两个例子，代表了清理数据工作中需要自动化最典型的两个场景，那就是文件导入和格式调整。而 Power Query 就是优化这两个主要功能的插件。

那怎么用 Power Query 来优化呢？你需要掌握三个主要步骤，分别是获取数据、转换和加载。只要是用 Power Query 来优化 Excel 的查询，这三个步骤是必须要掌握的。我也再来具体解释一下每个步骤的具体操作。

获取数据，指的是把数据传入 Power Query 的过程。通常我们会新建一个工作簿，把 Excel 打开之后，然后再切换到“数据”选项卡。接着，单击“获取数据”按钮，在下拉菜单中选择“自文件”命令，继续在下一级菜单中选择“从文件夹”命令，通过弹出的“文件夹”对话框，就可以加载文件夹中的所有 Excel 文件了。具体操作示范如下图。

转换，指的是对数据的清理工作。在这一步，你需要把你加载到 Power Query 中的数据从文件到 sheet，再从 sheet 到行列依次处理，对合并好的数据再进行添加、删除列、筛选、添加自定义计算等操作。

这一步是把不符合统计需求的数据调整为符合统计需求数据的最主要操作步骤，也是最繁琐的一步。例如把销售代表字段拆分成姓氏和名字，将订单日期的时间字段去掉，就可以使用转换这一操作来完成。

我给你画一幅图，用来帮你直观理解转换前后的变化，图片如下：

第三步是加载，指的是把已经清理和转换的数据返回到 Excel 中。这一步比较简单，这里我就不多讲了。

讲完了用 Power Query 优化 Excel 查询的三个核心步骤，那接下来我就用两个典型案例，来，带你实践一下具体的操作。

如何用 Power Query 优化 Excel 的查询

我选用了两个不同的案例，覆盖了我在第一部分所说的文件导入和格式调整这两种最经常需要手动操作的场景，这两个场景就是：

把多个文件合并到一个 Excel 中；

把单元格内容调整为统一格式。

那么接下来，我就教你怎么使用 Power Query 加快手工操作 Excel 的效率。

把多个文件合并成一个 Excel

把多个文件合并成一个 Excel 表格，是处理大量数据的先决条件。那么涉及到处理大量数据的工作，都要进行合并这一步操作。

像是数据被分成多个文件，每个文件里有多个表的情况，在工作中非常常见。如果数据是按月、按天分成不同的工作簿，合并它们是一件更麻烦的事情。

接下来我就以半年的销售数据表格为例，分 7 个步骤，来讲解怎么使用 Power Query 实现多个 Excel 文件的合并功能。

第一步，获取数据。

通过 Excel 的“数据选项卡”-“获取数据”-“自文件”-“从文件夹”命令，会弹出一个对话框。此时，你可以选中需要合并的文件夹，这样该文件夹内的所有文件都会被识别出来。

由于每个文件中可能会有不同的表，不能把它们直接合并。因此，我们需要点击“编辑”按钮，进入 Power Query 编辑界面。

这个界面是 Power Query 的主要工作界面，在你需要对多个文件进行自动化批量修改时，都需要通过这个界面来完成操作。同时，这个界面也会把你操作的中间结果显示出来。

我们来从左到右依次看一下界面的内容。由于 Power Query 的界面较大，我先把左侧和中间的界面放在下图：

界面的中间会显示导入的文件名称和数量，文件的内容会保存在 Content 列中。

另外，我要提醒一下，你还应该关注左侧和右侧的提示信息。左侧会显示你进行的多次清理操作，每次清理被称作查询。查询的名字可以在右侧的“查询设置”里进行名称修改，右侧的“应用的步骤”列表还能记录操作历史，如果你认为处理结果不符合期望，那就可以返回上一步，重新调整查询结果。

我把右侧的名称和步骤也贴在下方，供你参考。

第二步，我们需要把工作簿中的数据从 Content 列中解析出来，并且添加在现有内容的右侧。

在这里，我们可以使用一个公式来添加新的列。你需要切换到“添加列”选项卡，单击“自定义列”按钮，然后在弹出的对话框中输入公式“Excel.Workbook([Content],true) ”, 最后点击确定。

我把添加自定义列的操作图片也放在下方。

可以看到，通过自定义列的功能，你将会在原有表格的基础上再添加新的一列，而这一列的内容是 Table，表示文件中所有的表格内容。这里有一点你需要注意的，那就是自定义列的公式需要区分大小写。

第三步，调整每个 sheet 的每一行。

你需要点击第二步添加“自定义”列右侧的数据展开按钮，然后取消勾选“使用原始列名作为前缀”复选框，并点击确定。如图：

第四步，在调整完行之后，你还需要调整每个文件中要查询的 sheet 和列，它们也是以复选框的形式为你展示的。

例如我在每个文件只使用了一个 sheet，每个 sheet 中的所有列都需要进行查询，所以我就可以选中所有的 sheet 和列。我把操作截图贴在下面方便你学习。

选择指定的 sheet 功能，你可以点击 Item 列右侧的下拉菜单，勾选需要的 sheet。点击确定之后，Power Query 编辑器界面就会产生新的列，即“Data”列。

Data 列保存了我们在第二步选中的 sheet 的所有表格，在我们今天的案例中，由于我需要所有的列，因此我就“选择所有列”，并点击“确定”按钮，展开 Data 列，这样就得到了我需要查询的数据。

第五步，通过上面四个步骤，就可以把所有数据按照你需要的 sheet、行、列，导入到 Power Query 编辑器了。不过这时编辑器界面还有要处理的中间数据，因此我可以通过删除其他列的方式，来删除额外的数据。

具体做法是：使用主页选项卡的“删除列”下拉列表，选择“删除其他列”。我把删除前后的截图贴在下方，供你参考。

第六步，由于处理完数据内容后，Excel 的每一列类型会自动变成文本类型，导致最终执行结果显示错误，所以我们还需要继续修改类型。可以看到，我把“订单日期”改为日期时间类型，把销售额改为小数类型。具体修改方式是点击列标题前的类型图标，通过弹出的下拉列表，选择指定的类型，如图所示：

第七步，也是最后一步。我把编辑器的数据保存回 Excel 中，点击“主页”选项卡上的关闭并下载，这样就实现了把多个文件合并到一个 Excel 的功能。

通过这个例子，我把 Power Query 的三个主要处理步骤：获取数据、转换和加载，分解成了具体的七个操作步骤，来编写 Power Query 的主要流程。这七个步骤其实并不繁琐，并且你要是再细心点儿，就会发现我是把“转换”这一部分拆解成了五个步骤，其他两部分并没有变。

总结来说，相比较 Python，Power Query 不需要编程就能实现多个文件的合并，操作也更加简单。而且在每一步操作之后，你还能通过图形界面及时观察每一步骤的执行结果，这要比 Python 更加直观，也比 Python 更适用于一次性的多文件合并场景。

Power Query 不但在获取数据的时候可以实现自动化查询功能，而且在单元格的转换和添加列上，也能实现自动化查询功能。那么接下来我就以拆分列功能为例，给你演示一下 Power Query 的转换功能是如何实现自动化查询的。

单元格的拆分

拆分列，是自动化查询转换功能时最常用到的选项。例如你在按日期汇总数据时，发现日期这列除了年月日，还包括时间信息，这样就没法按照相同的年月日进行合并，而必须要先对日期单元格进行拆分，拆分之后才能按照年月日这一列汇总数据。

还有，当你需要对销售人员的姓氏、名字分别处理时，也要按照字数对姓名单元格拆开，然后分别处理。

拆分的功能在 Excel 中也是可以实现的，但是 Power Query 能够在拆分之后，对新导入的文件也能通过点击刷新实现自动拆分。那么接下来我就教你怎样在 Power Query 里，使用拆分列功能来拆分日期时间和姓名，并在增加文件后实现自动更新。

首先，我们需要再次打开 Power Query 编辑器。你可以在 Excel 的数据选项卡，通过“获取数据”下拉列表，选择“启动 Power Query 编辑器”。

其次，你需要在 Power Query 编辑器，选中“转换”选项卡。通过选中“dt_ 订单日期”列，使用拆分列下拉列表的“按分隔符拆分”按钮，把分隔符改为空格后，再点击确定，这样就可以把订单日期拆分为两个新的列。

最后，删除“小时”这一列后，订单日期就实现了拆分功能。

你看，通过对不必要信息的拆分删除，就可以对列的内容进行自动化调整了。

这里我也再补充一点，除了“按分隔符拆分”外，你还可以按字的个数进行拆分。例如，我把销售代表的“姓名”进行拆分，你可以使用“按字符数拆分列”选项，如下图：

通过这张图就可以看到，我通过调整字符数和拆分次数，就把“姓”和“名字”进行了拆分。拆分后的结果如下图，供你参考。

总结来说，通过日期的拆分，你可以自动化移除字符串中不必要的内容。而通过对姓名的拆分，你可以在后续操作中分别对姓名进行处理。

不过，如果后续有了新的文件，那该怎么在当前执行结果上，再把新的文件进行自动拆分呢？

具体做法是：你可以在存放半年销售数据的文件夹中，直接放入下一个月的销售数据。只要原始的 Excel 文件格式和之前的 6 个文件相同，那么当你点击“刷新”按钮后，Power Query 就会自动加载新的 Excel 文件，并自动把新的数据中的姓名、日期进行拆分。

这样就能实现自动查询的功能，不用重复执行获取数据和转换功能，也大大提高了数据的处理效率。

其他功能怎样学习

我利用了文件合并和单元格拆分，为你演示了 Power Query 的工作过程和常见功能。不过 Power Query 在自动化查询工作中，还能实现非常丰富的“转换”、“添加列”功能，他们分别在转换和添加列选项卡下，如果你需要掌握更多的功能，可以参考官方文档进行学习。

小结

在这节课中，我使用 Power Query 实现了多个文件的合并，以及单元格的拆分处理，为你演示了它的自动化查询功能。

如果你跟着操作下来，就会发现，Power Query 使用了图形界面，比 Python 处理数据更直观。通过 Power Query 编辑器，你可以一边观察处理结果，一边调整处理的功能。它的自动化体现在增加新的输入源或新的列，通过“刷新”功能，都能自动化识别和按照执行过的步骤对新增内容进行自动化处理。

此外，Power Query 除了使用界面外，还支持脚本语言，也叫 M 语言。通过 M 语言，Power Query 可以实现功能更丰富的转换和添加列功能，M 语言也有函数、判断和循环逻辑等脚本语言的语法，在你掌握 Python 之后，学习它就更加轻松了。

总结来说，Power Query 弥补了工作中处理一次性需求的短板，它比 Excel 更自动化，比 Python 更简单，为自动化查询工作提供了高效的解决方案。

我把这节课用到的 Excel 作为附件放在下方，你可以点击下载。

合并文件到 Excel.rar

思考题

最后，我给你留一道思考题：如何在 Power Query 编辑器中实现统计每个月的销售额，以及如何实现每种产品名称半年的销售额统计功能。

欢迎把你的思考和想法写在评论区，我们一起交流讨论。此外，你还可以点击课程详情页的“戳我进群”，扫描二维码，加入我们的课程读者群，我也会在群里为你解疑答惑。

14｜VBA脚本编程：如何扩展Excel，实现文件的批量打印？

你好，我是尹会生。

打印，是办公中必不可少的一步，比如在会议上，我们需要通过 Excel 表格向客户 / 领导展示工作成果。

但在使用 Python 对 Excel 进行打印的时候，我们还得给 Python 安装上 Excel、Windows 和硬件设备管理的库，过程极其复杂，远远达不到我们自动化办公的需要。尤其是面对类似的临时性需求，就更没必要使用 Python 了。

庆幸的是，Excel 自带了打印功能，而我们可以利用 Excel 的扩展——“宏”来实现打印，并且通过 VBA 脚本增强“宏”的功能，从而实现批量打印，满足我们自动化办公的要求。

那么在今天这节课，我就带着你学习 Excel 的另一个自动化功能：“宏”和 VBA 脚本。

宏和 VBA 脚本的用途

宏是 Excel 自带的扩展功能，可以记录的内容包括对 Excel 格式和文字的修改，它会像录像机一样记录下你在 Excel 中的操作。当你有一系列的动作需要多次执行，并且每次执行动作的顺序又完全相同，就可以重新播放，把这些操作自动再执行一遍。所以对于办公中临时性的需求，使用宏要比掌握每个 Excel 操作对应的 Python 函数要更简单。

你可以使用宏的录制功能，把格式调整、复制粘贴、打印等重复操作记录下来，并保存成一个快捷键。当你需要重复执行这条流水线作业时，就可以通过执行快捷键实现自动化操作。

不过，虽然宏能够像录像机一样通过重放功能实现自动化，但是它默认情况下只能实现部分功能的录制，也就是半自动化，要想把另一部分也自动化，就要使用宏的底层实现 VBA 脚本，例如像批量打印这些需求，就需要 VBA 脚本的循环扩充宏来实现自动化。

所以我在这节课，还会带你学习宏的底层实现 VBA 脚本，利用 VBA 脚本可以扩展宏的功能，把手动操作部分实现自动化。

那么接下来，我就通过对 Excel 的任意一个工作表进行打印的案例，给你具体讲解一下宏的录制和执行。

如何实现 Excel 的批量打印

使用宏，实现单个工作表的打印

为了方便你学习，我准备了一个包含 6 个工作表的 Excel 文件，这 6 个工作表命名分别为 sheet1-sheet6。要想基于宏实现自动化打印，需要通过这个步骤来实现：

录制宏；

手动执行一次操作；

停止宏录制；

通过快捷键执行宏。

首先我先来带你看一下，怎么把打印的过程录制为宏。

录制宏，需要指定宏名称和快捷键。你需要在 Excel 的“视图”菜单栏找到“宏”下拉列表，在其中选择“录制宏”按钮，点击按钮就会弹出“录制宏”窗口。此时你需要把宏名改为“打印工作表”，再把快捷键改为 Ctrl+Shift+P，点击确认之后，你接下来对 Excel 的操作就会被宏自动记录了。

这里以录制“打印 sheet2 工作表”这个操作为例，我把录制宏的窗口截图贴在下方，供你参考。

录制前有两点需要你注意：

快捷键如果和 Excel 默认的快捷键发生冲突， 那么默认的快捷键功能就会失效，因此在录制前，你在给宏指定快捷键的时候，应当避开默认快捷键。

另一点需要注意的是，点击确定后宏就开始录制了，因此任何需要重复操作之外的操作步骤，都有可能会影响执行宏时的结果，所以我们在录制过程中应尽量减少不必要的操作。

接下来，我需要手动执行一遍打印操作，并把操作过程录制为宏。具体操作步骤是：

录制开始前先激活 sheet2，以免把激活 sheet2 的步骤记录到打印过程。

在录制宏窗口点击“确定”按钮，开始录制。

选择文件 - 打印，为了让你能更好地观察到结果，我把打印机设置为打印到 PDF，然后点击打印按钮。

设置 pdf 的路径，并手动填入文件名 sheet2.pdf。

点击保存。

第三步是停止宏的录制。在点击 Excel 左下角方形按钮后，就可以停止录制。停止之后，一个新的宏就录制完成了。

停止录制的截图我放在下方，可以帮你快速找到这一功能在 Excel 中的位置。

最后一步是执行宏。例如我需要把 Sheet4 打印成 PDF，可以先激活 Sheet4，并执行快捷键 Ctrl+Shift+P，进行打印。

通过使用宏进行单个工作表打印操作，你会发现宏的优点和缺点。

它的优点是记录操作步骤的方式简单，尤其适合对 Excel 进行多次重复的格式和内容调整。而且掌握宏还不用学习 Excel 之外的编程技能，就能实现重复任务的自动化。

不过它的缺点也很明显，在使用宏之前，我们激活工作表和执行宏之后指定保存文件的名称，都需要手动操作。这就和我们使用 Python 自动化处理 Excel 是类似的，我们把自动化处理工作分成三个部分：为重复任务准备变量、为重复任务编写了一个 for 循环程序反复执行，为执行之后的结果自动保存结果。

可以看到，宏实现的就是 for 循环中的流水线操作。而对于使用宏之前以及使用宏之后的操作，我们是可以通过 VBA 脚本来进行优化，从而把相关操作实现自动化。

所以今天这节课，我们就再来学习一下 VBA 脚本。Excel 的宏是基于 VBA 脚本实现的，如果你需要将打印多个工作表的手工操作也使用宏自动实现，需要通过 VBA 脚本来扩展宏。

使用 VBA 脚本的循环，打印多个工作表

接下来，我就教你怎样查看当前宏的 VBA 脚本，并通过新的 VBA 脚本来扩展当前的宏，从而实现工作表的自动化批量打印。

首先，我们需要查看当前宏的 VBA 脚本。我们可以使用视图 - 宏 - 查看宏按钮，选中要查看的宏，并点击右侧的编辑按钮，这样就可以在打开的 VBA 脚本编辑器窗口查看当前宏的 VBA 代码。

我把查看方法和代码都贴在下方，供你学习。

这个 VBA 脚本就是宏实现打印单个文件的全部代码，它由三部分组成，分别是 Sub 过程、注释、实现打印的语句。

过程，是 VBA 代码完成一个任务的所有操作的集合。例如在上面这段代码中，实现打印任务的所有操作都被放在“Sub 打印工作表 ()”这一过程中，当你按快捷键 Ctrl+Shift+P 之后，Excel 会按照 Sub 过程中的代码来运行。而我们要想实现自动打印到文件，就需要在 Sub 过程中扩展宏记录的打印任务。

注释，是指用于向其他人描述“过程”实现的目的，注释的内容不会被 VBA 执行。它的格式是使用引号开头行，这一行都是注释的内容。这和 Python 中以 # 开头的行注释用法是相同的。

实现打印的语句。代码中这两行就是实现打印的语句，我把代码从截图中单独拿出来供你参考。

ActiveWindow.SelectedSheets.PrintOut Copies:=1, _ Collate:=True, IgnorePrintAreas:=False

这段代码由三部分组成，这三部分分别是要操作的对象，对象的属性和方法，方法的参数。我们依次来看下：

第一部分，对象是指工作簿、工作表、单元格、图片、图表、透视表等 Excel 中的具体元素。在 Sub过程中要对哪个元素做修改，就需要在过程中指定该对象。

例如代码中的“ActiveWindow 代表了活动的 Excel 窗口对象，它意味着宏将要对当前活动的窗口做一些动作。

第二部分对象的属性和方法，指的是对象的一部分或一种行为。这里需要注意，属性和方法是有区别的。

对象的属性用来描述对象的性质和特点。比如字体的颜色、字号等；

对象的方法是指在对象上执行的某个动作，比如要移动、删除、打印这个对象。

例如下面这一行代码：

ActiveWindow.SelectedSheets.PrintOut

在这一行代码中，SelectedSheets 是属性，它代表当前活动窗口下被选定的工作表，PrintOut 是方法，它被 ActiveWindow 对象调用，表示将要执行打印这一行为。总结来说，这条语句的作用就是打印当前活动的 Excel 窗口下选定的工作表。

第三部分是 PrintOut 方法的三个参数，它的三个参数更改了打印的默认行为。我把这三个参数写在下面供你参考：

Copies:=1, Collate:=True, IgnorePrintAreas:=False

Copies 参数指定打印份数为 1 份；

Collate 参数指定逐份打印；

IgnorePrintAreas 则忽略打印区域并打印整个对象。

宏就是按照上面这两行 VBA 代码实现打印的。不过在你对一个新的工作表执行宏的时候，会发现，你不仅需要手动选择要打印的工作表，还需要手动指定要保存的工作表的文件名称，所以我们可以扩展宏的默认功能，让 VBA 脚本从半自动化到自动化。

具体怎么做呢？在宏打印工作表的 VBA 语句的基础之上，我们只需要增加遍历工作表和自动指定输出文件名这两个功能，就可以实现工作表的自动化批量打印。

因此，我要在 Sub 过程中改造打印方法，增加这两项功能，增加之后的代码如下：

Sub 打印工作表()
'
' 打印工作表 宏
'
' 快捷键: Ctrl+Shift+P

    With Application.FileDialog(msoFileDialogFolderPicker)
         If .Show = -1 Then filepath = .SelectedItems(1) & "\"
    End With

    For Each sht In ActiveWorkbook.Worksheets
        sht.Select
        ActiveWindow.SelectedSheets.PrintOut Copies:=1, _
        Collate:=True, IgnorePrintAreas:=False, _
        printtofile:=True, _
        prtofilename:=filepath & sht.Name & ".pdf"
    Next

End Sub

通过快捷键“Ctrl+Shit+P”再次运行宏，你会发现改造之后的代码与改造前相比，有这样两个区别。

第一个区别是，改造前我们需要手动点击一个工作表，让它处于激活状态。而改造之后，宏会自动依次选择每个工作表。

第二个区别是，改造前每次执行宏的时候，需要手动输入要打印的 pdf 文件名。而改造后，宏会弹出选择存放打印文件的目录。当你选择了一个目录之后，宏会自动把所有的工作表，均以“工作表名称.PDF”作为文件名进行打印。

可以看到，改造之后的代码比默认的宏效率更高了。接下来我就来详细解释一下我是怎么使用 VBA 脚本遍历工作表，以及怎么自动输出文件名的。

在代码的第 11 行，“For Each ... In ”结构是 VBA 脚本的遍历功能，遍历的对象是“ActiveWorkbook.Worksheets”，这个对象表示当前激活的是工作簿中所有的工作表。那么我们把每个表存入 sht 后，就可以实现工作表的遍历操作了。

这里我还要再提醒你一下。VBA 的遍历和 Python 的主要区别是，前者遍历需要使用 Next 语句结束，而 Python 是依靠缩进实现循环语句块结束的，这也是使用 Python 编写程序的同学经常会遗漏 Next 语句的地方。

再来看怎么实现自动打印 PDF 文件的功能。这一功能的实现由弹出保存文件夹的对话框和整合文件路径两部分组成。

第一部分是在代码的第 7、8 行，我使用了 FileDialog 对象，这个对象会弹出对话框让用户选择路径。同时，filepath 变量会得到用户选择的文件保存路径。

第二部分在代码的 15、16 行，我为 PrintOut 打印方法增加了两个参数。分别是：printtofile 和 prtofilename。

printtofile 参数类似一个开关，使用这个参数的目的，是让打印函数由默认的弹出对话框让用户手动输入文件名改为“将对象打印到文件”。

另一个 prtofilename 参数指定了打印的对象将以什么文件名来保存打印结果。为了把保存的文件名设置为“路径 + 表名.pdf”的格式，我通过“&”符号连接了两个变量“filepath 、 sht.Name”和字符串 ".pdf"” ，这一就组成了“filepath & sht.Name & ".pdf"”的文件命名形式。

在这里我还想强调一下 FileDialog 的代码位置，我把 FileDialog 对象写在遍历之前，是因为我不希望每次读取工作表的时候，都需要选择一次存放路径，这样会让批量打印再次变成需要手动指定路径后才能执行，因此我就把 FileDialog 对象的代码放在批量读取工作表之前，这样也会提高 VBA 脚本的自动化程度 **。

通过 VBA 脚本增强了默认录制宏的功能，实现了批量打印工作表的功能。

小结

在今天这节课，我教你使用了“宏”这个强大的功能。通过宏的录制与回放，你可以实现 Excel 的自动化操作。此外，当有些操作没法被宏自动记录的时候，你还可以通过手动编写 VBA 脚本，来扩展宏默认的功能，让对工作表或单元格的批量操作从半自动化到自动化。

我在这节课中用批量打印的例子，给你展示了 VBA 的通过对话框指定保存路径、遍历工作表增强默认宏的代码，为你展示了迭代、判断和变量赋值，以及 VBA 中最重要的对象和操作对象的属性和方法。

需要说明的是，VBA 支持 Office 的所有对象，通过对象的属性和方法，再配合 VBA 语法的判断循环，就能弥补默认宏的不足，实现几乎所有 Office 办公的自动化操作。

不过 VBA 支持的对象多达上百个，我在这节课中只给你介绍了其中一个对象，也就是激活工作表这个对象。通过 VBA 脚本的语法配合激活工作表对象，你可以掌握 VBA 自动化的基本流程。如果你想了解 VBA 支持的所有对象及其属性方法，可以参考官方文档获得更详细的介绍。当你用到哪个对象，从文档里搜索关键字，找到它即可。

思考题

按照惯例，最后我要给你留一道思考题。你能否通过 InStr 函数（判断包含在字符串中的某个关键字是否存在，存在返回关键字位置，不存在返回 0 参考）改造批量打印脚本，让脚本实现包含关键字“汇总”，然后再打印报表？

欢迎把你的思考和想法写在评论区，我们一起交流讨论。此外，你还可以点击课程详情页的“戳我进群”，扫描二维码，加入我们的课程读者群，我也会在群里为你解疑答惑。

15｜PowerShell脚本：如何实现文件批量处理的自动化？

你好，我是尹会生。

在工作中，会遇到和 Windows 操作系统紧密结合又需要批量操作的工作需求，比如文件的批量重命名，还有按照扩展名搜索文件。那么今天这节课，我将给你介绍一个主要应用于 Windows 操作系统自动化的脚本 --PowerShell。

你肯定会有疑问，为什么我们不用 Python，而是要另外学习 PowerShell 脚本呢？原因就在于，遇到这类工作需求，PowerShell 脚本会比 Python 功能更强大，使用更方便，学习起来也更容易。

首先，Windows 的所有操作，都有对应的 PowerShell 操作，可以达到办公自动化的目标。而使用 Python，会因为 Windows 没有提供接口，有些功能就不能完全实现，或者有些操作还需要手动执行。

其次，PowerShell 的语法简洁，比 Python 更加友好，降低了你阅读代码的难度。这一点你在这节课我讲解的例子中会有更深刻的体会。

最后，PowerShell 在 Windows 上能做到开箱即用，安装完成后就可以正常运行了。而 Python 还需要安装解释器和配置环境。比如在金融和证券领域中，基于公司的规定，你可能无法安装操作系统之外的软件，这时候 PowerShell 的优势就体现出来了。

什么是 PowerShell？

PowerShell 是开源的终端命令解释器，之所以被称作解释器，是因为它会把用户输入的命令翻译给操作系统去执行，也能把操作系统执行的结果返回给用户。

作为 PowerShell 的初学者，你刚开始理解起来会有点难度，不过别担心，我举个例子你就明白了。

比如，在商场中的娃娃机，你需要通过对摇杆的控制来移动爪子的位置，抓取想要的礼物，因为你自己是没法直接移动爪子的。那么把你移动摇杆的操作翻译成爪子移动操作的功能，这就是解释器。

为了强化你对 PowerShell 的理解，我再把使用 PowerShell 前后的情况给你做个对比。

Windows 操作系统在日常工作中，我们会采用图形界面（GUI）进行各类办公操作。在没有使用 PowerShell 的时候，如果我要想添加、删除、修改用户，就得点击各种按钮、选择下拉菜单、鼠标右键点击用户图标。

想象一下，如果我给 100 个新员工创建账户，创建一个账户需要 5 分钟，那仅仅在用户添加这一个操作上，我就需要花费 500 分钟，效率是不是很低？

但是利用 PowerShell，就可以自动化完成用户添加这一重复性的操作了。

从 Windows Server 2012 版本（一听就是个很古老的操作系统版本）开始，所有的 GUI 操作就完全可以基于 PowerShell 实现了。这意味着，你能用 GUI 界面实现 Windows 操作系统中的所有功能。

你看，微软已经给了我们这么现成的自动化工具，所以我们可以放心大胆地把 Windows 操作系统上的重复操作，都交给 PowerShell，提高工作效率。像这些和 Windows 操作系统紧密结合又需要批量操作的工作需求，都是 PowerShell 最适用的工作场景。

那么接下来，我就以 Windows 中最常用的文件管理为例，为你讲解一下 PowerShell 如何对文件进行批量处理。

怎样使用 PowerShell 实现文件批量处理？

在对文件的批量处理中，我们经常会遇到两类场景。

文件夹下的文件按照某一规则批量重命名。比如，为了项目文件名称工整，需要批量给文件夹 / 文件按照某些规律进行重命名。

将符合多个条件的扩展名从大量文件中找出来。比如，为了找到 outlook 里附件中的所有的压缩包，你需要使用搜索功能，一次性查找包含.zip .rar .7z 多个扩展名的文件。

接下来就以文件批量的重命名，来为你讲解一下用 Powershell 怎么实现。

一行代码，实现文件批量重命名

使用 PowerShell 脚本实现文件的批量重命名，我们只需要一行代码。不过在这一行代码中，需要你掌握的 PowerShell 的概念比较多。所以我会通过一个案例，帮助你在学习的同时也能迅速掌握、内化。

首先我通过一行代码创建了 10 个文本文件，创建的 PowerShell 脚本是：

foreach($num in (1..10)) { New-Item $num".txt" -type file }

通过这行脚本，我在当前目录下创建了名字为“1.txt-10.txt”这样 10 个文件。由于批量改名操作也会用到类似的语法，所以我先给你详细讲解一下批量创建文件的脚本。这个脚本包含了循环、变量和命令三个部分。

第一个部分，是我在脚本中使用的 foreach 的循环功能。

循环的运行次数由 in 关键字后面的次数确定，这里我为了创建 10 个文本文件，就使用了“1..10”的形式来产生 1 到 10 这十个数字，这样的话，foreach 就会执行 10 次，而每一次循环 num 变量都会得到一个新的数字，并在 foreach 循环体“{ }”中被 New-Item 项执行一次。

第二个需要你掌握的部分叫做自定义变量。

在 PowerShell 中，变量名都是以””开头的，剩余字符可以是字母、数字和下划线等任意字符。并且PowerShell的变量名是不区分大小写的，比如“num”和“NUM”都表示相同的变量。我在代码中使用了“num”自定义变量来记录每次循环的数字。

第三个需要掌握的部分叫做命令。

PowerShell 的命令有三种类型，例如 New-Item 叫做 Cmdlet 类型， foreach() 叫做工作流类型，还有 PowerShell 内置的函数和自定义函数类型。这三种命令类型有一个共同的特点，就是都能被 PowerShell 执行。那么为了讲解方便，我在这节课中把这三种命令类型统称为“命令”。

在代码中，“New-Item”命令用来创建新的文本文件。我在创建时使用了两个参数，第一个参数“$num".txt"”用来指定文件名，文件名为 num 变量和字符串“.txt”连接后的名字。第二个参数“-type”用来指定创建的类型是文件（而非文件夹）。

PowerShell 的脚本编写完成之后，接下来我们就需要运行它。

它的执行方式非常简单。在 Windows 中使用快捷键“win+R”可以弹出运行界面，在运行界面输入“powershell”之后再按回车，便会弹出 PowerShell 的交互界面，此时我们只需要把代码输入到交互界面就行了。

如果你希望脚本内容能多次运行，你就可以把脚本保存为以“ps1”作为扩展名的文件中。例如，我可以把创建 10 个文件的脚本保存到“newtxt.ps1”文件中，反复执行时，可以进入 PowerShell 交互界面，并输入“.\路径\newtxt.ps1”来运行它。

我把执行的命令和结果截图，贴在下方供你参考。

创建了 10 个文本文件后，我想把它们统一进行改名，新的名字是“new_ 数字 _new.txt”，即在这 10 个文件的文件名前后，都增加“new”字符串，改名之后的格式如下图：

我把批量改名的代码从截图中单独拿出来，来重点讲解一下其中的 dir 命令、管道符和 Rename-Item 命令及其参数，因为 PowerShell 就是通过管道符将多个命令组合在一起，实现批量改名的。

dir *.txt | foreach { Rename-Item $_ -NewName ("new_"+$_.BaseName+"_new.txt")  }

我来带你按照代码的执行顺序，从左到右，依次讲解这行代码的主要实现逻辑。

首先，这行代码最左侧的 dir 命令，它的功能是在终端输出当前目录下所有文件和目录的名称。

如果你要操作的文件没有在当前目录，可以使用“cd 要访问的目录”方式进入该目录下，再使用“dir”命令查看。为了更改文本文件的名称，我需要先通过“dir *.txt”来获得当前目录下所有以“.txt”结尾的文件和目录。

接下来是这行代码中出现的“｜”，被称作管道符。就像它的名字一样，用一条管道把两个程序连接在一起。它的作用主要有两个：

连接两条命令，PowerShell 会从左到右依次执行。如果左侧程序没有执行完成，右侧程序则会进入阻塞状态，等待左侧程序执行完成后再执行。

把左侧命令的执行（输出）结果，通过管道，作为右侧程序的输入。在这行代码中，“dir”命令的输出，会通过管道符成为“Rename-Item”命令的输入，再通过 foreach 循环，实现对每个文本文件进行重命名的操作。

最后，我们来学习一下“Rename-Item”命令和它的参数。和“New-Item”一样，“Rename-Item”也是 PowerShell 独有的 Cmdlet 类型命令，它的功能是实现文件的改名。

PowerShell 中一共有 9 个与“Item”相关的 Cmdlet，你可以使用下面的命令获得所有的命令和它的帮助。

PS> Get-Command -Noun Item

CommandType     Name                            Definition
-----------     ----                            ----------
Cmdlet          Clear-Item                      Clear-Item [-Path] <String[]...
Cmdlet          Copy-Item                       Copy-Item [-Path] <String[]>...
Cmdlet          Get-Item                        Get-Item [-Path] <String[]> ...
Cmdlet          Invoke-Item                     Invoke-Item [-Path] <String[...
Cmdlet          Move-Item                       Move-Item [-Path] <String[]>...
Cmdlet          New-Item                        New-Item [-Path] <String[]> ...
Cmdlet          Remove-Item                     Remove-Item [-Path] <String[...
Cmdlet          Rename-Item                     Rename-Item [-Path] <String>...
Cmdlet          Set-Item                        Set-Item [-Path] <String[]> ...

“Item”被官方文档翻译为“项”，你可以和我一样，把它们理解为要操作的对象，通过上面这些 Cmdlet 命令，就可以实现这些“项”的增删改查。当你需要改名，就可以用“Rename-Item” Cmdlet 来实现。

我们了解了“Rename-Item”命令的功能后，再继续学习它的参数格式和内容。它的参数格式是： “Rename-Item 旧的文件名 -NewName 新的文件名”。我在代码中使用了这样一行命令对文件名称进行了修改：

Rename-Item $_ -NewName ("new_"+$_.BaseName+"_new.txt")

在“Rename-Item”命令的参数中，你需要关注“$_”这个内置变量。它的功能是表示当前对象。在我们的例子中，它表示 foreach 每次循环时，通过“｜”传入的文件名称。

不过当你需要改名时，还有另外一种方法，那就是使用“$.BaseName”来获得文件 (不包含扩展名的) 基本名称，以及通过“$.extension”来获得扩展名。例如：“10.txt”文件的基本名称是“10”，扩展名是“.txt”。当我把“10.txt”赋值给“$”之后，就可以使用“$.BaseName”取得基本名称“10”，使用“$_.extension”取得扩展名“.txt”。

在“Rename-Item”命令的参数“("new_"+$_.BaseName+"_new.txt")”，这部分代码中，除了提取了“10.txt”的基本名称“10”之外，我还利用“+”符号把"new"字符串和基本名称“10”连接，形成新的文件名“new_10_new.txt”字符串。也就是说，把将、新的字符串作为“Rename-Item”的参数，就实现了文件重命名。

在这里有一点需要你注意，“”​从PowerShell3.0版本开始，就可以使用“PSItem”

替代“$”，虽然“PSItem”在阅读上更加友好，但“”能够向后兼容，而且输入的内容更少。所以我会更推荐你使用“$_”。

通过对批量改名代码的例子与分析，你会发现虽然只有一行代码，但是其中包含了内置变量、循环、管道和重命名命令等功能的组合，PowerShell 就是这样一种简洁而功能强大的脚本语言。

为了让你能对 PowerShell 更加得心应手，我再为你讲解一个利用同样逻辑，就可以实现的场景：从一个文件夹找到.zip 和.rar 扩展名的文件，带你来一起看一下怎么使用 PowerShell 一行命令，来按扩展名搜索文件。

按扩展名搜索文件怎么实现

当工作时间久了，你会发现自己的 Outlook 文件夹里有海量的附件，特别是压缩包占用空间非常大。如果我们逐一搜索，就会像大海捞针一样，找到自己想要的文件非常困难。那么此时，我们就可以按照扩展名通过一行脚本将它们全部找出来进行备份或整理到新的文件夹中。

要想实现按扩展名搜索文件，我们需要继续利用“dir”命令来取得当前目录下所有文件，以及利用“$_.extension”来取得文件的扩展名。

所以我首先使用如下命令，来获取当前目录下的扩展名都有哪些，便于接下来的搜索和查找。

dir | foreach{$_.extension} | Get-Unique

执行结果
.txt
.zip
.rar

这行代码会把当前目录下所有文件的扩展名显示出来，为了避免重复的扩展名会影响我后续的脚本编写，那么在这里，我使用了“Get-Unique”命令，把相同的扩展名去重，只保留一个。

代码的执行过程依然是从左向右依次执行。

首先，通过“dir”命令获取当前目录下所有的文件名称，把所有的文件名称作为“foreach”循环的输入，赋值给“$_”。

然后再通过“$_.extension”取得每个文件的扩展名，之后进行输出，再次通过管道符，作为“Get-Unique”命令的输入。

最后，“Get-Unique”会把所有扩展名作为输入处理，输出不重复的扩展名。

通过观察所有扩展名的脚本执行结果，确定要搜索的目录中，包含了我们需要的“.zip”和“.rar”之后，我们再取出符合扩展名的文件名称。为了实现这一功能，我们还需要使用一个新的命令：“Where-Object”。

“Where-Object”也是 Cmdlet 类型的命令，它的用法是对管道中的每个对象进行筛选，把不符合条件的对象删除。那怎么判断管道中的对象是否符合条件呢？依据就是“Where-Object”命令后面“{}”中的参数。

“{}”中可以使用命令、也可以使用比较运算符。如果“{}”中使用命令，且命令执行结果为 True，则“Where-Object”会保留对象进行输出或传入后续的管道，如果命令结果为 False，则删除对象。

此外，如果“{}”中使用了比较运算符，就根据比较运算符的结果对对象进行相应操作。同命令结果一样，如果为 True，则保留对象；如果为 False，则删除对象。

比较运算符一共有 10 个，它是以“-”开头，跟着运算符名称，运算符的名称是大于、等于、小于的首字母缩写，我把比较运算符整理成表格，供你参考：

为了便于你理解比较运算符，我通过一个例子，来给你讲解怎么搜索“.zip”扩展名的文件。

dir | Where-Object{ $_.extension -eq ".zip" }

在这段代码中，我使用了“-eq”比较运算符，实现了文件扩展名是否和“.zip”字符串相等的判断。

脚本通过 dir 命令可以获得当前目录下所有的文件，再通过“Where-Object”命令，依次对管道中的对象进行判断。如果扩展名为“.zip”则比较的结果为 True，“Where-Object”命令执行完成后，PowerShell 会在终端显示该文件名称，否则就会删除该对象，不在终端进行显示。

如果你还需要对文件大小、文件名称相似性等其他方式比较，可以参考比较运算符表格进行实现。

我们实现了单个扩展名的搜索之后，再来让这一行脚本的功能继续增强，让它能搜索多个扩展名。

为了同时搜索到“.zip”和“.rar”文件，我们需要引入逻辑运算符。在“Where-Object”命令的参数中，使用逻辑运算符，然后根据它两侧的执行结果是否为 True，实现对扩展名的组合判断。例如下面的脚本：

dir | Where-Object{ ($_.extension -eq ".zip" ) -or ($_.extension -eq ".rar" ) }

这行代码，过滤了管道中文件的扩展名是否包含“.zip”或者包含“.rar”。我在代码中使用的“-or”逻辑运算符的含义是，它的两侧扩展名和“.zip”、“.rar”任意一个相等，返回结果就是 True，“Where-Object”命令就会把文件名称打印到屏幕上，如果返回的结果为 False，对象仍然会被删除掉。

标准逻辑运算符我也为你整理了一个表格，你可以参考。

总结来说，通过增加逻辑运算符，我们就能实现对多个扩展名的搜索功能。

小结

最后我来为你做个总结，我通过批量改名和按多个扩展名搜索文件这两个例子，为你展示了 PowerShell 脚本的命令和管道。

命令中的 Cmdlet 类型能够支持 Windows 中所有的对象，其中对文件操作最常用的四个 Cmdlet 类型命令是：New-Item、Rename-Item、Where-Object 和 Get-Unique。它们也是我在这节课中为你着重介绍的命令。在 PowerShell 中，你掌握的 Cmdlet 越多，能够实现的功能就越强大，因此，我把官方文档地址提供给你，你可以通过扩展学习，掌握更多的 Cmdlet 命令。

此外，命令中的工作流类型可以实现循环，我在这节课给你介绍了 foreach 循环，通过 foreach 就可以实现批量操作，优化办公效率。

不过在你掌握了足够多的命令后，还需要掌握管道符。因为通过管道符能够连接命令，让命令可以按顺序执行，而通过对命令的组合，就可以实现多个命名的自动化运行，今儿实现 Windows 操作系统相关操作的自动化。

思考题

按照惯例，我来为你留一道思考题，你能否通过官方文档查找到删除项的命令，并实现“.txt”扩展名文件的批量删除？请你大胆尝试一下。

16｜循环与文件目录管理：如何实现文件的批量重命名？

你好，我是尹会生。

作为产品经理 / 运营，你经常需要做竞品调研，来跟自家产品对比优劣。这时，你就需要下载大量与该类产品相关的图片，并进行整理归类。而归类操作中，必须要走的一步就是对这些图片进行重命名。

还有你在搜集资料时，往往会从网络中下载大量不同格式的文件，比如电子书、视频、音频等，为了方便归纳整理，你也需要对文件进行重命名。

通过我例举的这两个场景，你应该发现了，这些需求都是把批量改名和网络功能结合，而且还需要 Mac 和 Windows 系统支持重命名。那怎么来实现批量重命名的操作呢？

如果你还记得上节课的内容，肯定会说，使用 PowerShell 就可以了。不过你要是对 PowerShell 相关知识掌握得扎实的话，也会记得我说过，PowerShell 适合在 Windows 平台下独立运行的临时性任务。所以在非 Windows 系统，以及需要和网络下载功能结合的需求上，它就没有 Python 的兼容性好。

那么今天这节课，我会带你回到 Python，使用 Python 来实现文件的批量重命名。

如何使用 os 库实现文件批量重命名

还是老规矩，我们学习一个新的功能，首先要学习它所需要的 Python 库和函数。

例如，我把友商的产品介绍图片，通过网络批量下载后，存放在“/Users/user1/Desktop/pic”文件夹中，这些文件名称长短不一，那我希望能从数字 1 开始，为它们批量重命名。

在第 15 节课我就说了，重命名是一种贴近操作系统层面的操作。因此在 Python 标准库中，我把“文件”和“操作系统”作为关键字，从官方文档中找到了“os”库，os 库中包含了操作系统相关的操作。通过“os”库，你就可以轻松取得三个批量重命名必备操作，那就是目录中的文件名、文件名后缀处理以及文件改名。

那我先把批量重命名的代码提供给你，让你更直观地理解运行过程，然后再来帮你分析这三个操作是怎么通过 os 库实现的。

import os
# 保存图片的目录
file_path = "/Users/user1/Desktop/pic"
# 需要批量重命名的扩展名
old_ext = ".jpg"
# 取得指定文件夹下的文件列表
old_names = os.listdir(file_path)
# 新文件名称从1开始
new_name = 1

# 取得所有的文件名
for old_name in old_names:

    # 根据扩展名，判断文件是否需要改名
    if old_name.endswith(old_ext):

        # 完整的文件路径
        old_path = os.path.join(file_path, old_name)

        # 新的文件名
        new_path = os.path.join(file_path, str(new_name)+".JPG")

        # 重命名
        os.rename(old_path, new_path)

        # 文件名数字加1
        new_name = int(new_name)+1

# 显示改名后的结果
print(os.listdir(file_path))

这段代码会把"/Users/user1/Desktop/pic"目录中“jpg”扩展名的文件进行重命名，把它们分别命名为“1.jpg”，“2.jpg”，“3.jpg”，以此类推。

那批量改名的这三个操作具体是怎么实现的呢？其实是通过 os 库中的三个函数来实现的，分别是 listdir()、path.join() 、rename()。这是你在这节课要掌握的重点，我也会着重讲解。

第一个是 listdir() 函数，它的功能是打印指定目录下的文件名称。如果再给这个函数指定一个参数 file_path，那么它会把 file_path 中的所有文件名称以一个列表的类型返回。使用列表类型方便后续迭代，便于进行单个文件改名。

为了只对“.jpg”扩展名的文件改名，我使用了 endswith() 函数对列表进一步筛选，过滤掉不需要改名的文件。

第二个是 path.join() 函数，它可以连接路径和文件名，从而得到一个带有完整路径的文件名称格式。这里我要给你着重强调一下，改名操作必须要指定文件正确的路径，因此改名前必须要进行路径和文件名的连接。

最后是改名函数 rename() 函数，它的两个参数分别为改名前文件的路径 + 文件名和改名后文件的路径 + 文件名，均为 path.join() 函数处理过的完整文件名称。通过 rename() 函数改名后，文件名称会自动变为新的文件名称。

将上面三个操作放入 for 循环语句中，就能实现批量重命名的功能。

这一段代码虽然功能正常，但是继续增加新功能时，必然要再增加新的代码，如果只是按照执行的前后顺序把多个不同功能的代码写入一个文件，它的可读性会变差，别人阅读你写的代码就会有障碍。

就像我们从超市购买的各类商品一样，你一定会把调料放在厨房、把鸡蛋放在冰箱、把袜子放在衣柜，对商品分门别类放置，绝不会按照购买的时间顺序摆放在你的房间中。

同理，代码的摆放位置，也不应该按照执行顺序依次存放。正确的做法是，你要把每一组功能相同或相近的代码写入到一个函数中，并把该功能中经常变动的部分作为函数的参数，乃至整个脚本的参数，这样才能给有多个功能的脚本带来更好的阅读体验。代码的整洁程度高，也为你排除代码的故障带来更高的效率。

那么接下来，我就教你怎么重构批量改名脚本，提高代码的可读性。

重构程序

首先，我来带你学习一下什么是重构代码，以及怎么重构代码。

重构代码是指在代码可以正常实现的前提下，为了提高它的可维护性，需要对代码的结构进一步调整。就像你需要定期收拾房间一样，代码也需要进行维护。特别是经常修改和添加新的功能的代码，它的逻辑结构会像你炒菜之后的厨房一样，越来越混乱，为了代码和代码之间的逻辑关系更清晰，你需要掌握如何调整代码的结构。

我来给你举个例子，比如我在批量改名的程序中又增加了新的需求，要求将改名的路径和扩展名从变量赋值改为从命令行参数赋值。这样就不用进入到脚本中修改代码了。

根据这个新的需求，你会发现，当前的代码有 3 个地方需要调整。

第一个是代码的结构层次需要调整。当前的代码只包含了一个批量改名的功能，当你再为代码增加命令行参数处理功能时，新的代码和当前代码放在一起，会破坏改名功能的完整性，这时候，你就可以把每个功能单独放在一个函数中，通过函数来让一个功能和另一个功能代码相互独立。

第二个是代码开始执行的位置需要调整。由于函数定义的代码块会在函数调用以后才运行，但是根据 Python 的语法，你必须将函数定义写在函数调用的上方，这就导致了代码开始执行的位置出现在文件的中间和结尾。所以我需要一个更明显的标记，告诉阅读代码的人，从该位置开始阅读代码，该位置才是代码执行的第一条语句，而不是让阅读的人从代码文件开头一行以后的找程序的入口。

第三个是命令行参数的处理需要调整。由于 Python 默认是不去处理命令行的参数的，因此我们需要增加一个专门处理命令行参数的函数，来读取用户输入的正确参数，而对错误的参数，则需要提示用户。

这三个地方的调整，我会依次采用函数、内置变量和命令行参数来实现对代码的重构，我来依次带你看一下优化的具体代码。

封装到函数

为了让代码结构逻辑更加工整，我把每一个独立的功能都放入到单独的函数中。每个函数组成的语句块，就像自然段一样，将一整篇文章，按照功能进行了划分。由于当前的代码只有批量改名这一个功能，所以我就把改名功能的所有代码都放到一个函数当中。

封装函数的时候，一个是要考虑功能的完整性，另一个要考虑函数用到的其他变量如何与它进行交互。调用函数时使用参数，就是函数和其他变量交互最好的办法。

对于批量改名这一功能，主要交互的对象有两个，它们是批量改名的路径，以及要修改的文件扩展名。所以我就把这两个对象作为改名函数 rename() 函数的参数，rename() 函数得到这两个参数后，会按照函数的定义，把这两个参数传入 rename() 函数中实现改名的逻辑，对文件进行批量改名。封装之后的核心代码如下：

def rename(file_path, old_ext):
  # 批量改名的代码
  ... ...

rename("/Users/user1/Desktop/pic", ".jpg")

这段代码实现的功能和没有重构之前完全相同，都是对指定目录的指定扩展名文件进行批量重命名。但是在代码结构上，要比直接在文件实现的代码逻辑更清晰，可以看到，改名功能被放在函数定义中，执行的时候就可以直接调用 rename() 函数。

将改名功能封装为函数的好处就是，代码更工整了，新的功能也可以继续采用函数的形式添加到当前代码中。比起把所有代码按执行顺序都写在一个文件中，这样的格式会让你更容易区分开代码中的每一个功能。

明确执行位置

把批量改名的功能封装为函数之后，对程序的执行顺序也会带来一些变化。我把前后变化给你做个对比：

封装函数之前，程序的执行顺序是导入库之后依次执行。

封装为函数之后，执行顺序就变为导入库之后，就开始执行 rename() 函数的调用。

当这个脚本再陆续添加新的函数的话，那么找到哪一行是脚本第一个执行的命令，就非常麻烦了。因此在 Python 中有一个参考 C 语言设置代码入口的方法，让你能快速定位代码是从哪一行开始执行的。这个方法就是通过对内置变量“name”的值进行判断，判断它是不是和字符串“main” 相等。

在 Python 中，执行代码的方式有两种。

一种是单独运行，也就是用 Python 加脚本的名称方式运行。

另一种方式是把.py 结尾的脚本文件作为自定义的模块使用“import”关键字导入，导入后通过“模块. 函数 ()”的格式运行。

如果一个脚本文件独立运行，那么它的内置变量“name”的值就是“main”，通过“if name == "main"” 的判断，结果必然为 True，则该判断逻辑下的代码块就会执行。如果作为模块导入，那么“name”的值就是 False，则不被执行。

我们可以把函数的调用全部放入 “if name == "main"”语句块中，这样就可以指定这条 if 语句作为代码单独运行的入口，既方便你快速找到入口对程序进行修改，又方便你把它作为其他程序的模块进行导入。

我把实现对“name”变量判断的脚本写在下方，你可以对照代码学习。

def rename():
   ... ...
def func1():
   ... ...
def func2():
   ... ...
def func3():
   ... ...

# func1() # 在__name__之外执行，不推荐

if __name__ == "__main__":
    func3()
    rename("/Users/edz/Desktop/pic", ".jpg")
    func1()
    func2()

在代码中，我定义了 4 个函数，对于四个函数的调用，都放在了 “if name == "main"”语句块中。在使用这种方式设置程序入口时，有两点需要你特别注意。

一方面，这种设置方法是人为指定程序入口，因此你需要把代码中所有函数调用都放在 if 语句块下，这样才能实现作为入口的功能。虽然放在 if 语句块之外也可以运行，但函数调用写在 if 语句块之外，就很容易给代码阅读带来障碍。

另一方面，使用“name” 作为入口的判断变量，只能在单独运行的时候才为"main"，如果使用 Python 交互方式执行，就无法对“name” 变量进行判断。

我们通过指定代码的入口，让程序的逻辑更加清晰。那么接下来就是为这段代码添加命令行参数，在不修改代码的前提下，通过命令行参数来设置批量改名的目录和扩展名。

命令行参数处理

使用命令行参数的优点，就是在调用脚本的时候一并传入要操作的对象，这会比修改配置文件和变量更直接。那么在原有代码基础上，我们还需要增加两个参数，也就是要操作的目录和扩展名，并使用 argparse 库实现对这两个参数的处理。

参数处理是一个比较笼统的概念，它包括参数的接收、参数数量的判断和参数的解析三个部分。“argparse”库是命令行解析模块，它负责在脚本运行时，接收和处理脚本执行时的参数。

首先是参数的接收，在本讲之前，我们执行 Python 脚本的方式是：

python3 脚本名称.py

在脚本中使用“argparse”库后，脚本能够支持在该命令后面增加参数，并在脚本内获取参数的内容。哪些参数能够被脚本处理，需要使用 argparse 库的 add_argument() 函数指定。

接下来是参数的判断，add_argument() 函数可以接收两种参数格式，分别是“-”和“--”，后面再跟着英文。按照惯例，一个“-”一般后面会使用单个英文字母， 两个“--”后面是完整名称。

以对目录改名的参数为例，我需要接收“-p”或“--path”两种形式的参数指定的方法是：

add_argument("-p", "--path", required=True, help="path to rename")

同时，我还为“--path”参数所在的 add_argument() 增加了两个额外的参数，一个是要求用户执行程序，必须输入“-p”或“--path”，如果执行不指定会报错的 required 参数。

另一个“-p”或“--path”参数含义的帮助信息“help”参数。

增加参数处理后，如果你没有输入完整参数，argparse 库会自报错，并提示你如何正确使用该脚本的参数。你也可以直接使用“-h”得到执行帮助。

我把参数输入不完整和通过 -h 获取帮助的执行结果，贴在下面供你学习。

SHELL$ python3 rename_v2.py -p /path/to/rename/files -e
usage: rename_v2.py [-h] -p PATH -e EXT
rename_v2.py: error: argument -e/--ext: expected one argument

SHELL$ python3 rename_v2.py -h
usage: rename_v2.py [-h] -p PATH -e EXT

optional arguments:
  -h, --help            show this help message and exit
  -p PATH, --path PATH  path to rename
  -e EXT, --ext EXT     files name extension, eg: jpg

最后是参数的解析，它是在参数数量正确的前提下自动完成的。完成解析后，会以“--path”参数后的英文字面“path”作为属性名称，以“--path”后面的参数，作为属性值。

比如我在取得用户参数后，就可以使用“args.path”来得到命令行“-p”参数后面参数的值，以及使用“args.ext”得到“-e”参数后面参数的值。此外，我还把这两个属性作为批量改名函数 rename() 函数的参数，这样就可以把命令行参数作为重命名函数的参数使用了。

获取命令行参数的核心代码我也为你整理了出来，放在下方供你参考：

import os
import argparse

def rename(file_path, old_ext):
  """批量改名函数"""
  ... ...
def args_opt():
  """获取命令行参数函数"""

    #定义参数对象
    parser = argparse.ArgumentParser()

    # 增加参数选项、是否必须、帮助信息
    parser.add_argument("-p", "--path", required=True, help="path to rename")
    parser.add_argument("-e", "--ext", required=True, help="files name extension, eg: jpg")

    # 返回取得的所有参数
    return  parser.parse_args()

if __name__ == "__main__":

    # args 对象包含所有参数，属性是命令行参数的完整名称
    args = args_opt()

    # 调用重命名函数，将命令行参数作为重命名函数的参数
    rename(args.path, "."+args.ext)

    # 输出改名之后的结果
    print(os.listdir(args.path))

通过重构后，代码的整体结构就变成了导入库、函数定义、函数调用三个部分，对经常需要变动的替换路径和扩展名，也从修改变量改为命令行参数，无论从阅读代码还是后续继续扩展代码，整体结构都要比顺序执行代码逻辑更清晰。

小结

最后让我来为你总结一下，这一讲我为你讲解了使用 Python 如何实现批量改名，以及如何对越写越长的代码进行重构。

批量改名属于操作系统中的文件相关操作，这类功能在编程语言中往往会提供事先定义好的编程接口，无需自己实现从应用层到操作系统的全部代码，建议你在遇到这类需求时，先从标准库中搜索相关模块，再从第三方库搜索，尽量避免手工编写，提高工作效率。

为了让批量改名的脚本逻辑更清晰，也更方便执行，我对代码还进行了三个方面的重构：

通过使用函数增加代码的逻辑性。

通过“name”变量增加了程序入口，便于你直接找到程序开始执行的位置。

通过增加命令行参数，让你不用修改代码，就能实现函数的参数的修改。

增加程序的可读性、提高执行便利性也能为以后编写代码效率提升带来改进，这些改进会在后续章节代码越来越多的时候起到更明显的效果。

我把这节课的相关代码都放在了 GitHub 上，供你学习参考。

思考题

最后我来为你留一道比较有趣的思考题，你能否通过命令行为代码指定两个参数，当这两个参数为整数时，脚本自动计算这两个参数的“和”和“差”，并将执行结果打印到屏幕上。

欢迎把你的思考和想法写在评论区，我们一起交流讨论。此外，你还可以点击课程详情页的“戳我进群”，然后扫描二维码，加入我们的课程读者群，我也会在群里为你解疑答惑。我们下节课再见！

17｜不同操作系统下，如何通过网络同步文件？

你好，我是尹会生。

你有没有过这种经历：慌慌张张地去会议室开会，突然发现自己需要的文件却在工位的台式电脑中。因为文件比较大大，通过互联网下载需要很长时间。如果用网盘来中转放在两个电脑上的文件，传输速度又很慢。

我猜这个时候你就会想：如果能用一台电脑实现文件的上传和下载，那文件传输是不是就非常方便快速了。

这当然是可以实现的。所以今天，我就教你怎样用 Python 基于 HTTP 协议，来实现跨操作系统的文件上传和下载功能。

今天要学习的代码都不长，比较精简，又和文字相得益彰，所以学起来也不会太累。那接下来就跟着我的思路走，我们先来了解一下使用 Python 来实现文件上传和下载的优势。

为什么要使用 Python？

实现文件下载的软件，相信你也用过很多，比如微信、QQ。用这些软件来传输文件，不仅要连接互联网，而且还有文件大小的限制。那如果用商业软件呢？传输虽然方便，但是就不能跨操作系统，而且还有可能需要付费。

所以综合下来，既要免费，又要传输快、没有大小限制的实现方式有没有呢？聪慧如你，一定知道接下来我会开始讲怎么用 Python 来实现文件的上传和下载功能了。别着急，我还要再唠叨几句关于用 Python 来实现的优势，这样你会学得更有劲儿。

首先，用法简单。只要一行代码，就能实现文件的浏览和下载功能。

其次，跨操作系统，适用范围广。只要安装了 Python 默认支持的文件下载需要的模块，那么在 Windows、macOS、Linux 上就都能用。

最后，传输速度快。和网盘、微信不同，Python 的文件下载功能，是基于局域网通信的，不需要通过互联网中转，所以也就没有了传输速度和文件大小的限制。

知道了用 Python 来实现的优势，那接下来我们就进入正题。我会结合代码，来给你讲解用 Python 怎么实现文件的浏览和下载功能。代码不会很长，所以你学起来也不会很累。

一行代码，实现文件的浏览和下载

使用 Python 实现文件的浏览和下载，只需要一行代码。如下：

python3 -m http.server 8080

通过在命令行运行这行代码之后，就能通过“http:// 你的 IP 地址:8080”浏览和下载文件了。

这行代码很简单，但你要仔细看的话，会发现这行代码的执行过程和我们之前执行脚本的过程有比较大的差别，而且通过“http.server”模块的加载就直接运行了 Python 的脚本，这两种功能都是我们之前没有接触过的。

虽然没有接触过，但是学起来不会很难，那么接下来我就从怎么通过命令行运行模块，以及怎么使用模块提供一个 HTTP 服务这两方面来讲解这行代码。

如何通过命令行运行模块

要通过命令行运行一个模块，我们需要先通过 Python 命令找到“http.server”模块的第一条命令，然后再来执行。而找到“http.server”模块，非常关键的就是"-m"参数。我来重点讲解一下。

从执行方式上，这行代码和我们以往执行的代码不同。我在 Python 命令和模块之间使用了“-m”参数，而且“-m”参数后面会跟着要执行的 Python 的模块“http.server”。

“http.server”在你电脑中保存的路径是“/ 模块所在目录 /http/server.py”，它也是一个“.py”结尾的文件，会被保存在你电脑上 Python 文件夹中的“lib”文件夹下。

如果不使用“-m”参数，那就像我们之前执行的代码一样，Python 会执行当前目录下的.py 文件。所以在这里你要特别注意一下，增加了“-m”参数前后，执行的.py 文件位置是不同的。

如果要查看这个模块是怎样通过 Python 实现的，那么我们需要先找到这个模块的所在目录。核心实现代码我写了出来，供你参考。

$ python3
>>> import http
>>> http.__file__
'/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/http/__init__.py'

在这段代码中，我使用了我们之前学习过的内置变量“file”，得到了该模块所在的位置。在以后的工作中，你可以使用这种方式查找任意一个你想要了解的模块位置。

不过你也要知道，由于你在安装 Python 时，会根据自己的习惯选择自定义的目录，或者你使用的是 Windows 操作系统，所以你得到的目录可能会和我不同，但这并不影响你阅读查找该模块的实现代码。

如果你还想查看 Python 其他模块保存在哪个目录，可以在没有加载模块的前提下，获得所有模块的位置，代码如下：

import sys
sys.path

# 执行结果
['', '/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7', '/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages']

你会发现，在执行“sys.path”得到的列表中，还会出现一个“site-packages”目录，这个目录是你使用 pip3 安装的第三方模块默认的位置。当你需要学习 Python 库的代码时，可以使用这个方法找到所有安装的包的目录。

“-m”参数不但能让 Python 从“sys.path”中找到要执行的模块，还能够自动执行这个模块。以“http.server”为例，Python 会首先找到“lib/python3.7/http/server.py”， 然后运行“server.py”文件。

由于该文件中使用了“if name == 'main':”这样一条语句，所以 Python 会从这一行开始执行“server.py”文件。此外，我还为“http.server”指定了参数“8080”， “server.py”会通过“argparse”库对“8080”这个参数进行处理。

如何使用“http.server”模块提供 HTTP 服务

刚才我们讲了怎么通过命令行来运行“http.server”模块，事实上，这个模块 **** 是基于 HTTP 协议实现的文件浏览和下载功能。接下来我们就先了解一下HTTP 协议的主要工作过程。

使用 HTTP 协议的好处是它能够跨平台，而且还比其他协议简单。那么要想掌握 HTTP 协议，你得要知道 HTTP 协议提供了两种角色：

为其他人提供服务的服务端；

享受服务的客户端。

我们一般把“http.server”称为 HTTP 服务端，把浏览器称作 HTTP 客户端。服务端和客户端通信时，会采用它们的主要协议 --HTTP 协议。

它们的通信过程就像是在打电话，当你给对方打电话时，首先要输入对方的手机号码。同理，在计算机中，手机号码就是服务端的 IP 地址和端口，接通电话后，双方要想互相听懂要传递的信息，必须使用一种双方都能理解的语言，这个语言在计算机中就是 HTTP 协议。所以一句话总结就是，相同的语言就是文件传输的协议。

了解了 HTTP 协议的主要工作过程，那接下来就是它建立连接的过程了。就像我为你举的例子一样，对方的手机号码在 HTTP 协议就是 IP 地址和端口。

比如我为 HTTP 服务器指定的端口是 8090，我的 IP 地址是“192.168.0.100”，那我就可以通过浏览器使用“http://192.168.0.100:8090”进行访问。可以看到，在访问的时候，我手动指定了协议、IP 地址和端口。

所以“http.server“模块不仅可以提供 HTTP 协议，还是一个灵活指定 IP 和端口的 HTTP 服务端。这也就是说，http.server 模块运行后，能让浏览器访问到服务端。

由于客户端服务端都采用 HTTP 协议，那么服务端列出的文件目录会自动被浏览器翻译给客户端的用户，你也就能浏览器查看到服务器上的文件名称，并把服务器的文件下载到客户端的电脑上，这就是“http.server”模块能够实现下载的原理和过程了。

另外，我还要提醒你，在文件下载时，一定要注意共享的安全性。因为那些没有用户认证功能的 HTTP 文件下载方案，其他人都可以通过 IP 地址和端口直接获取你电脑中的文件，由此造成信息泄漏。因此在共享完成后，你需要把服务端及时关闭。

不过由于“http.server”默认没有提供文件上传的功能，手动编写也需要比较复杂的代码逻辑，因此，我来通过另一个 Flask 模块，它能通过简单的代码实现文件上传。

如何实现文件的上传

虽然我们要利用最精简的代码来把文件上传到服务端，但是它也要比下载功能复杂得多，因为基于 HTTP 协议的上传，我们需要自行编写 HTML 页面，来提示用户怎么上传，怎么使用 POST 方法访问服务器，以及怎么指定上传后文件的保存位置。

我根据Flask 模块的官方文档的上传代码，进行了精简，考虑到你目前对编程的理解还比较基础，所以我把用户验证和文件扩展名验证功能去掉后，得到了如下的代码。通过这段代码，可以实现基于 Python 的文件上传。

我把代码放在文稿中，供你学习和参考。同时，我也再给你详细讲解上传的过程，以及用到的代码。

import os
from flask import Flask, request
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = os.getcwd()

html = '''
    <!doctype html>
    <title>Upload new File</title>
    <h1>Upload new File</h1>
    <form action="" method=post enctype=multipart/form-data>
      <p><input type=file name=file>
         <input type=submit value=Upload>
    </form>
    '''

@app.route('/', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        file = request.files['file']
        filename = file.filename
        file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
    return html

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8090)

根据上传的常规流程，我把代码按照四个步骤来实现文件上传，这四个步骤分别是运行服务器、获得网页内容、上传文件、保存文件。这四个步骤就是我们上传文件的四个关卡，那接下来我们就一关一关过。

运行服务器

第一步是服务器的运行。Flask 是第三方函数库，因此需要用一行命令进行安装，安装后才能使用。命令如下：

pip3 install flask

Flask 要想正确运行，首先要对它进行初始化。所以我在代码第 2 行导入 Flask 后，紧接着就对它进行了初始化，并给初始化后的 Flask 取了个名字 App。这就意味着，在当前脚本下对 Flask 的操作都可以用过 app 对象来完成。

在代码第 4 行，我还给 App 对象增加了一个参数“UPLOAD_FOLDER”，这个参数用来指定上传的文件将会保存在哪一个目录中。

接下来，你就可以使用“app.run()”方法运行了。和下载使用的“http.server”是一样的，由于 Flask 也是作为 HTTP 服务端，所以在 Flask 运行时，也必须指定它运行的 IP 地址和端口。

在代码中我把 IP 地址指定为“0.0.0.0”可以让它监听服务器所有的 IP 地址，我把端口设置为“8090”端口，你可以通过这一端口访问到 Flask 服务端。

获得网页内容

了解完怎么运用 Flask 之后，我再带你看一下我是怎么把服务器上的网页传输到浏览器的。

浏览器要想获得网页内容，必须要用户发起到服务器的 HTTP 请求。发起请求后，浏览器会得到服务器经过 HTTP 协议传送回来的网页源代码。当你使用服务器的正确 IP 和端口访问到 Flask 服务器后，会看到这样一个界面，如下：

这个网页内容对应的是第 7-14 行的代码，这段代码是把 HTML 语言赋值给变量 html，并通过 upload_file() 函数传递给浏览器。

你看到的网页内容，其实就是 html 变量中的 HTML 语言，它被浏览器接收后，会被浏览器解析，解析之后的结果就是你看到的网页。

所以编写这段 HTML 语言的目的，也就是让你可以通过浏览器的“选择文件”按钮弹出窗口，选择要上传的文件，并通过点击“upload”按钮上传。

把 HTML 语言的代码传递给浏览器的函数是 upload_file() 函数，它是通过这 5 行代码实现的：

@app.route('/', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        ... ...
    return html

在第一行中，我们使用的是函数的装饰器，它的作用是在不改变被装饰函数的内容的前提下，给函数增加新的功能，装饰器也是由函数实现的，它的语法格式是在装饰器前增加一个“@”符号。这里的装饰器“@app.route()”就是用来增加“upload_file()”函数功能的。

我来详细为你讲解一下装饰器的两个参数。

一个是“/”, 它的作用是请求 URL 的路径为“/”时，调用 upload_file() 函数；

另一个是“methods”，限制请求“/”的方式只能是“GET”和“POST”方式。

我来举个例子，你会更容易理解。比如用浏览器访问“http://127.0.0.1:8090/”，由于浏览器默认的请求方式是“GET”方式，请求的 URL 是“/”，那么 Flask 会自动调用“upload_file()”函数。在函数中，再次判断请求的方式，由于默认是“GET”方式，所以函数会返回 html 变量，也就是你看到的网页内容。

上传文件

在你掌握了浏览器加载 HTML 之后，我们接下来就需要学习上传文件的代码执行过程。 要想上传一个文件，需要先点击浏览器的“upload”按钮，它是“form 表单”的提交功能。

“form 表单”的作用是在你点击按钮后，把文件以指定的方式和数据类型上传到服务器。指定方式和数据类型都是采用表单的参数进行指定的，它们分别是 method 和 enctype 参数。

第一个参数是“method=post”，它指定了表单是通过“POST”方式访问服务器的。通常上传文件会采用 POST 方式，主要原因 GET 方式最大只允许使用 1024 个字节，而 POST 方式在理论没有大小限制，完全取决于服务端的设置和内存大小。

第二个参数是“enctype=multipart/form-data”。这个参数是专门用来存放容量较大的文件的，它会把文件放到“request.FILES”中。

当你点击“upload”按钮之后，文件就开始上传到服务器的内存中。那接下来就到了最后一步，把内存中的数据保存成文件。

保存文件

要把内存中的数据保存到文件，我们可以通过“upload_file”函数的这 5 行代码来实现。

def upload_file():
    if request.method == 'POST':
        file = request.files['file']
        filename = file.filename
        file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))

在这段代码的第二行，对请求方式进行了判断：

如果是“GET”方式，会直接返回网页；

如果是“POST”方式，才会对文件进行处理。

具体的处理过程是：

先从“request.files”中取出上传到服务器的文件数据；

再通过“file.filename”取得上传时使用的文件名；

接着，通过 path.join() 函数将事先指定好的路径和文件名拼接组成当前目录下的文件名后；

最后，通过 file.save() 函数将内存中的文件内容，保存到服务器的同名文件中。

小结

今天的核心内容就是这些，我也再给你总结一下。今天这节课，我为你讲解了怎样使用 Python 的模块来实现最简单的文件上传和下载功能，这一功能在公司内进行跨操作系统的文件传输不但速度快，而且极为便捷。

在讲解文件上传下载的同时，我还给你讲解了两个库的使用，分别是 http.server 和 Flask 两个模块。其中 Flask 是 Python 中非常著名的 WEB 服务端模块，除了可以进行文件上传外，它还能作为 Web 服务器提供网页访问。

在文件的上传和下载场景下，我还给你介绍了两种请求 HTTP 服务器的方式，它们分别是 GET 和 POST 方式。

GET 方式一般用于获取服务器的信息，类似从服务器上查找数据；

POST 方式一般用于向服务器上传信息，类似向服务器写入。

对服务器的请求方式还有更新、删除、更改单个值等不同的方式，其中 GET、POST 是最常用的形式，日常应用中，所以你只需要记住这两个请求方式即可。

最后，我希望通过 Flask 库、HTTP 协议的请求方式、简单的表单及其实现它的 HTML 代码，能够让你对 HTTP 协议以及 WEB 服务器有初步的了解。

因为 Python 的高效便捷，一方面体现在可以通过简单的语法在一台电脑上实现提效，另一方面，它能够通过极少的代码开发出功能非常强大的 WEB 服务器，这对你在办公数据的集中管理和网页自动化管理上都会有非常有效的帮助。

思考题

按照惯例，我还要给你留一道思考题。题目是：如果我想在 Flask 展示表单的页面中，展示当前目录下的所有文件，那要怎么修改 Flask 的代码呢？

欢迎把你的思考和想法写在评论区，我们一起交流讨论。如果你学完有所收获，也欢迎你把课程分享给你的朋友、同事，一起提升办公效率。好了，那我们下节课再见！

18｜http库：如何批量下载在线内容，解放鼠标（上）？

你好，我是尹会生。

我在前段时间遇到了下载大量图片的需求。具体来说，是在我训练 AI 识别猫时，必须要在电脑中存储大量猫的图片。但搜索到的图片都在网页中，我就需要先把它们一个个手动下载下来，再保存，然后才能进行后续的工作。

而且，随着我对 AI 训练工作的日益增多，这类需求会越来越丰富，我不仅要下载猫的图片，还要下载大量其他各种动物的图片。相信类似这种批量下载网页中的图片的需求，你在工作中会经常遇到。而这类需求，刚好能够使用 Python 的“requests-html”库实现批量下载，提高我们的工作效率。

因此呢，这节课我就以搜索到的猫的图片为例，给你讲解一下，我是怎么来批量下载图片的。

批量下载图片的准备工作

我把实现图片自动化批量下载的过程，拆分成四段难度逐渐递增的代码。这四段代码分别实现的是以下四个功能：

访问 HTTP 服务器，得到搜索结果的整个网页；

在访问服务器之后下载一张图片；

找到多张图片的相似地址；

提取相似地址，下载多张图片。

前两个功能，是批量下载图片的准备工作和前提。掌握了这两个功能，那么批量下载图片实现起来就容易多了。所以接下来，我们先来学习这两项准备工作。

访问 HTTP 服务端的资源

我们从难度最低的一段代码开始，怎么通过访问 HTTP 服务器，从而得到猫的搜索结果的整个网页。

访问 HTTP 服务器，我需要使用 Python 的第三方库 requests-html 库来实现。虽然 Python 在标准库中也自带了访问服务器 urllib 库，但它的实现代码不够简洁，所以我一般数情况下会使用 requests-html 库替代它，来进行 HTTP 服务器的访问。

使用 reqeusts-html 库，你先需要通过“pip”来安装，再通过 get() 方法访问 HTTP 服务器。

安装 requests-html 库的方法，和我们上一讲安装 flask 库是一样的。这里我强调一下，在 Python 中，绝大多数的第三方库，它们的名称和安装包的名称是相同的，所以除了特殊的安装方法外，像这种通用的安装方法，我就不会每次都重复贴出来了。reqeusts 库的安装方法，代码如下：

pip3 install requests-html

在命令行执行后，requests-html 库就安装成功了。接下来，我们就使用它的 get() 方法去请求 HTTP 服务器，而得到的返回数据，就是网页的代码。

相信你还记得在上一讲中，我介绍了两种常见的 HTTP 服务端请求的方式，分别是“GET”和“POST”。requests-html 库就是使用了同名函数实现的“GET”方式访问。

那么接下来，我将使用 reqeusts 库来访问一个无版权的图片网站，并得到网页的源代码。我把代码写出来，供你参考。

from requests_html import HTMLSession

# URL
name = "猫"
url = f"https://unsplash.com/s/photos/{name}"

# 启动
session = HTMLSession()

# GET请求
result = session.get(url)

# 结果
print(result.status_code)
print(result.html.html)

在这段代码中，我用 requests-html 代替浏览器来作为 HTTP 客户端，通过“session.get()”函数，向 upsplash 网站提供的 HTTPS 服务器发起“GET”请求。发送请求后，“result”变量会得到 HTTP 服务端响应的结果，即网页的源代码。

你肯定会有疑问，为什么我要用 requests-html 来代替浏览器呢？原因就在于，浏览器得到了网页内容后，如果你想再通过网页下载图片，还需要通过鼠标和网页的交互。这就会带来一个新的问题：交互式命令是不能进行图片的自动化下载的。也就是说，我们必须让交互操作变成非交互操作，才能进行批量下载图片。

总之呢，在这段代码中，我使用了 requests-html 代替浏览器实现了一次完整的网址的访问，这一过程为后续的自动化下载打下了坚实的基础。

而在这次网址的访问中，最核心的就是发起“GET”请求了。我把这次完整的“GET”请求拆分成了四个步骤。

其中前两个步骤是准备工作。准备工作是指在发送正式的数据之前，需要确定你需要向哪一个互联网当中的服务器发起请求，以及尝试和他建立连接，判断是否能够正常访问。

这就像你跟其他人打电话一样，你要确定你输入的电话号码是正确的，而且在拨打电话以后，还可以根据电话那个提示音，知道对方的电话是否处于可接通状态。

那后两步呢，就是在这个基础上和对方正式开始通话，但是他和打电话的区别是 HTTP 协议会采用一问一答的形式，也就是发送一次请求会得到一次结果，不发请求也不会得到任何结果。接下来我们就来详细看一下这 4 个步骤。

第一步，设置请求的 URL。

通过使用 requests-html 请求该网站，你会发现请求的大部分地址都是固定的，只有搜索的关键字会随着搜索的内容不同而发生变化。

所以我把搜索的关键字单独提取出来，放在一个字符串“name”中，当你需要更改搜索的关键字时，直接修改“name”变量就可以了。而其他在请求过程中没有发生变化的字符串，则放在 url 字符串中。与此同时，我还把 url 设置为“f-string”，方便我进行变量“name”的替换。

这一步是比较简单的，不过我在里面花了点小心思。仔细观察字符串，像字符串中只有某几个位置发生了变化，那么其余的部分就是重复的逻辑。针对这种重复的逻辑，可以被定义成变量来重复使用，减少重复工作。

还有我们以前接触过的，比如把文件目录和文件名拼接后，会形成一个很长的完整文件名。当文件名被多个函数重复使用时，你就不必每次都执行拼接，而是可以在拼接后被定义成变量来重复使用。

我补充这一点是想告诉你，你在写代码时也要多观察代码中哪些部分是重复的逻辑，这些重复的逻辑往往都可以使用变量来进行定义。一个小小的技巧，有时候就能给你的重复工作带来非常大的效率提升。

第二步，启动一个会话。

会话是 HTTP 协议中的一个概念。当你的客户端和服务端建立了连接时，我们就称一个会话建立成功了。它就像你和你的小伙伴使用微信进行视频，当对方接通视频的那一刻，就建立了一个新的会话。

在这段代码中，我使用了“HTMLSession”类初始化了一个叫做“session”的会话，用来后续向 HTTP 服务端继续发送请求。

第三步，发送“GET”请求。

建立会话之后，就要可以开始传输数据了。在这段代码中，我使用了 get() 函数把数据包装成标准的 HTTP 协议，然后向我在第一步定义的“URL”发起“GET”方式请求。

第四步，得到返回结果。

请求发送完成之后，“result“变量会自动得到服务器的结果，无论结果是成功还是失败，“result”变量都会得到返回的状态码以及数据。

所以在这一步，我们需要着重掌握怎么判断服务器返回的是成功还是失败的结果。我使用了“result.status_code”来得到返回的状态码。状态码是 HTTP 协议规定的标准，正确返回为 200 ，其他返回你可以参考维基百科。

根据不同的状态码，你能判断网页返回的状态，并进行后续处理。比如我们经常使用状态码“200”确认网页是正确返回了内容，这意味着我得到“200”这个返回码之后，就可以继续处理网页数据了。

如果返回码是“4xx 或 5xx”，说明出现了请求过程的客户端或者服务端错误，HTTP 客户端并没有得到网页的内容，这时候你就必须停止程序后续的逻辑，并向执行者提示错误信息。具体的判断方法就是我们的老朋友 if 语句了。

在这段代码中，我们得到了状态码 200，那接下来我就使用“result.html.html”，得到了完整的网页源代码。

这四个步骤，就是使用“requests-html”库得到指定网页源代码的完整流程。接下来就要分析网站的行为，来下载单张图片。

使用 requests-html 下载一张图片

下载单张图片是图片批量下载的基础。为什么要这么说呢？因为我们可以通过观察一张图片的下载行为，把浏览器的下载操作转换成 Python 的代码，这样就可以循环批量下载图片了。而观察单张图片的下载行为，换句话说就是用 Python 来模拟浏览器。

具体思路是这样的：在使用 Python 按照浏览器的行为进行下载后，我们可以对服务端的 HTML 代码进行分析，从而得到多个图片在服务端存储的代码逻辑，之后就可以利用循环批量下载图片了。那接下来我就用例子带着你来学习一下。

使用浏览器下载一张图片的时候，当你把鼠标移动到下载按钮，你会发现它是在访问图片的 URL 后面增加了一串“download”字符。

我用一张图片的访问和下载给你举个例子，例如下载下面这张图片：

这张图片的访问地址和下载地址我贴了出来，供你学习。

在你清楚浏览器是如何下载一张图片的之后，我们就可以使用“requests-html”下载单张图片了。下载单张图片的完整代码如下：

from requests_html import HTMLSession

# URL
url = "https://unsplash.com/photos/NLzaiXOELFY/download"

# 启动
session = HTMLSession()

# GET请求
result = session.get(url)

# 结果
print(result.status_code)

# 保存图片
with open("one.jpg", "wb") as f:
    f.write(result.content)

这段代码中的保存文件功能是我想重点为你讲解的内容。在访问网页得到图片的下载地址的基础上，我增加了“with open” 语句，打开了一个文件，并以“wb”作为参数，实现文件的二进制写入。

由于图片是由“01010... ...”形式的二进制组成的，所以写入和下载都必须使用二进制方式实现。可以看到，在设置文件写入方式的时候，我增加了 b 参数，让文件以二进制方式写入文件，这样我们就能从服务器获得的图片内容的同时，也以二进制方式得到了返回结果的修改。

接着，我把“result.html”改成了“result.content”，使用二进制方式来获取图片内容。通过这样的方式，最终就把文件保存成“one.jpg”了。当你运行脚本后，在运行脚本的目录下就会产生一个“one.jpg”的图片。

这就是完整的使用“requests-html”下载单个图片的过程。

如何批量下载图片

请求网页和下载图片是实现批量下载的基础，这两部分已经使用 Python 自动完成了，接下来就是如何将它们组合起来。我来组合它们的思路是这样的：

首先，你要把它们当作两个独立的功能，找出两个功能的输入和输出是什么。

请求网页的输入是一个即将要请求的 URL 地址，输出则是网页源代码。那下载图片的输入输出是什么呢？我们以下载单个图片为例，下载图片的输入是图片的地址，执行下载图片的功能之后你会得到保存在硬盘上面的一张图片。因此，下载图片的输入是图片地址，输出是图片文件。

接下来，需要从前一个功能的输出找到后一个功能输入需要的数据。

这里的前后是指执行顺序的前后，那么从请求网页得到的网页源代码中找到下载图片的地址是关联这两个功能最关键的技术点。而在编码的时候，我会将这两个功能编写为两个函数，函数的执行结果就是功能的输出，而函数的参数实现的就是功能的输入，通过前一个函数的输出和后一个函数的参数就能实现它们之间的功能连接了。

最后是找规律。尽可能地找到多张图片之间的 HTML 代码的规律，从而实现批量下载。

根据上面我提到的组合两个函数的思路，你不难发现，要想实现批量下载图片函数，必须先得到每个图片的下载地址，而下载地址会出现在请求网页的函数执行结果中。接下来我们就从请求网页函数的执行结果 -- 网页的 HTML 代码入手，寻找图片的下载地址，并从这些下载地址中寻找规律，批量提取下载地址。

使用 XPath，匹配多张图片下载地址

想要找到图片的下载地址以及它们的规律，最直观的办法就是通过浏览器的调试界面，观察它们的代码。

你可以使用浏览器访问 “https://unsplash.com/s/photos/猫” URL 后，再通过浏览器的调试功能，打开调试界面。我以最流行的 Chrome 浏览器为例，它的调试窗口打开快捷键是“F12”，打开调试界面后的截图如下，供你参考：

截图左侧就是调试页面，截图的右侧是网页的内容，你可以通过截图左上角的“选择元素按钮”

点击图片，左侧的调试界面会自动定位到图片对应的 HTML 代码。而你向上查找，会发现图片下方的“<a>”标签的“href”属性，就记录了图片 ID 和下载地址。

这里我有必要为你介绍一下 HTML 的标。你在网页上看到的每个元素，都是由不同类型的标签组成的。

例如图片使用的是“<img>”标签，超链接使用的是“<a>”标签。每个标签还使用了相应的属性，来记录该标签的具体信息。比如把一个人作为标签，他可以有性别、身高、年龄等属性。“<a>”标签的“href”属性就记录了超链接的地址，我们可以使用超链接地址来完成图片的批量下载。

那用什么方法能一次性找到链接呢？聪明的你一定想到了我们在第 8 讲学过的正则表达式。正则表达式是分析网页的常用工具之一，不过还有一种比正则表达式更方便的网页内容搜索工具，那就是 XPath。我们可以使用 XPath 来找到超链接。

如果你对使用正则表达式分析网页也感兴趣，你可以把它跟 XPath 进行对比，选择一个自己顺手的工具，这也是提升工作效率的技巧之一。

我先把 XPath 查找的方法和结果写在下面的代码中， 然后再为你分析 XPath 的用法。

print(result.html.xpath('//figure[@itemprop="image"]//a[@rel="nofollow"]/@href'))

# ['https://unsplash.com/photos/NLzaiXOELFY/download?force=true',
#  'https://unsplash.com/photos/3JyEfhb8Zgo/download?force=true',
#  'https://unsplash.com/photos/4Y6UYds0cIo/download?force=true',
#  ... ...
# ]

在这段代码中，使用 result.html.xpath() 函数就能实现 XPath 查找网页的功能。XPath 查找网页时也有它自己的语法，我在这里使用了路径、标签、属性三个功能。我来依次带你学习一下它们的用途。

首先是路径，HTML 的整体结构是由头部标签“”和主体标签“”组成的，而网页的内容是在主体标签下逐层编写的，它的结构如下：

<head>
</head>
<body>
    <div num=1>
        <figure itemprop="image">
           ... ...
           <div num=2>
              ... ...
              <a rel="nofollow" href=http://...>
              ... ...
           </div>
        </figure>
    </div>
</body>

以上面的结构为例，如果网页内容简单，你可以像使用 windows 的路径一样，使用“/body/div/fugure”方式搜索到“figure”标签。但是当匹配路径超过 3 个时，把路径逐个写在 XPath 搜索表达式中不太容易，那我们就一般使用模糊路径来搜索“//”。比如要搜索一个

标签，就可以使用“//”的方式。

接着是标签。我在 xpath() 函数中，使用了 figure 和 a 标签。“finger”是自定义标签，“a”是超链接的标签，它们和路径组成了下面这行代码的形式：

xpath('//figure//a')

这一条 XPath 的功能是从任意位置开始搜索“figure”标签，并在它下面任意一级目录中找到“a”标签。这显然不符合我们只提取图片下载地址的要求，因此我们要通过属性增加这两个标签的限定条件。

接下来，我为“figure”标签增加了限定条件，要求它的属性和属性的值，必须和 xpath() 函数搜索的属性和值完全相同。

xpath() 的匹配规则中要求“figure”标签必须包含“itemprop”属性，且这一属性的值是“image”，为“a”标签设置查找的属性必须为“rel”， 且它的值必须是“nofollow”。这样 xpath() 中的两个标签就变成了如下的写法：

xpath('//figure[@itemprop="image"]//a[@rel="nofollow"]')

通过增加属性，限定了搜索的标签，可以得到需要下载的“<a>”标签，“<a>”标签的规范写法要求链接地址写 @href 属性，链接的内容要写在它的值中，因此我们再为 xpath() 提取的“<a>”标签增加“@href”属性，即可以实现提取只包含图片地址的列表。我把完整的 xpath() 函数写在下方。

xpath('//figure[@itemprop="image"]//a[@rel="nofollow"]/@href')

这就是利用 xpath() 的路径、标签、属性搜索超链接的完整实现思路。我建议你打开网页调试器，通过“选择元素按钮”点击网页内容，对照这段网页源代码和 xpath() 语法来学习，这样更有利于你快速掌握怎么使用 Xpath 匹配多张图片的下载地址。

使用循环，下载多张图片

在你得到了多张图片的下载地址列表后，接下来我们只需要把下载单个图片的代码和列表功能进行组合，这样就能实现图片的批量下载了。我先把核心代码写出来，供你参考，然后再讲解思路。

down_list = result.html.xpath('//figure[@itemprop="image"]//a[@rel="nofollow"]/@href')

def down_one_pic(url):
  result = session.get(url)
  filename = get_picID_from_url(url)
  with open(filename, "wb") as f:
    f.write(result.content)

for one_url in down_list:
    down_one_pic(one_url)

实现文件批量下载的主要思路，就是把下载地址列表使用 for 循环逐个进行处理。通过循环取得了每个下载地址的变量“one_url”之后，再把“one_url”作为下载函数的参数，并通过 URL 提取文件名后，把文件下载并保存。

小结

以上就是我们今天的全部内容了，我来为你总结一下。在本讲中主要以批量下载图片为例，为你讲解了 Python 中实现 HTTP 客户端常用的“requests-html”库、进行网页搜索的 XPath 搜索工具。

通过这两个库的配合使用，你可以利用 Python 代替浏览器，实现图片、文字、视频等资源的批量下载，在你熟练掌握“XPath“之后，再遇到需要批量下载的工作，你就可以利用 Python 解放你的鼠标，实现网页内容的批量自动化下载了。

思考题

在第 16 讲中，我们实现了文件的批量改名功能，你能否将批量改名功能也封装成一个函数，结合今天的批量下载，实现下载之后的文件自动改成你需要的文件名呢？

欢迎把你的思考和想法分享在留言区，我们一起交流、讨论。也欢迎你把我们的课程分享给你的朋友、同事，一起做职场上的效率人。我们下节课再见！

19｜http库：如何批量下载在线内容，解放鼠标（下）？

你好，我是尹会生。

在上节课，我们学习了怎么使用“requests-html”库来批量下载图片，这足以帮你应对大部分需要批量下载互联网资源的场景了。

但是除了批量下载这一典型场景外，还有两种场景“requests-html”工具无法覆盖，其中就包括一些网站的每日签到功能，明明登录网站后点击一个按钮就能领到虚拟货币，但是每次还要手动输入账户和密码，再用鼠标点击领取按钮。相信已经学会了如何用 Python 访问网页的你，早就想把签到进行自动化了吧。

那么今天，我就以京东自动签到领金豆为例，为你介绍一款比“requests-html”更加强大的浏览器工具“selenium”，通过 selenium 我将带你实现自动化的用户登录和模拟鼠标点击功能。

selenium 的适用场景

我刚才也提到了，在你把“requests-html”库应用到各种批量下载场景之后，你会发现有两种场景下使用“requests-html”无法实现批量下载。

一种场景是，有的网页为了防止你用工具下载，会对下载工具进行检测。如果你的 HTTP 客户端不是浏览器，那就不允许你访问该网站下的所有内容。

另一种场景是，一些网页为了更好的交互性，就使用了 JavaScript 脚本语言。而 JavaScript 脚本语言需要在浏览器运行，才能获得服务器的数据。所以如果使用“requests-html”来获取这些数据的话，你就必须再编写 Javascript 脚本。

基于这两种原因，我们可以使用“selenium”来解决。这也正是为什么有了“requests-html”库之后，我还要再带你学习“selenium”库。

它的实现原理，即：通过“WebDriver”组件，把 Python 与浏览器连接起来，让 Python 来控制浏览器的行为，向浏览器发送各种模拟用户操作的指令，此时，真实的浏览器会按照 Python 发的各种指令对服务器进行访问。

这样既可以解决网页加密的问题，又避免了再编写 Javascript 脚本，弥补了“requests-html”在批量下载场景中的缺憾。无论从操作的友好程度，还是实现功能的简单程度上，都要比“requests-html”强大。

那今天这节课，我们就来学习如何使用“selenium”来实现自动签到。不过考虑到你是第一次接触“selenium”，所以在学习实现方法之前，我还是先来带你学习怎么安装和执行它。

“selenium” 的安装与初次运行

“selenium”是基于“Webdriver”连接 Python 和浏览器的，而“Webdriver”的组件比较多，所以它的安装过程也要比一般的 Python 第三方库复杂一点。

考虑到你是第一次使用它，我就把具体的安装步骤以及注意事项，拆分成了 5 个步骤，保证你能顺利安装，并且能用它访问网页。

第一步：根据浏览器版本来下载“Webdriver”。

由于“selenium”会通过“Webdriver”调用你当前电脑上的浏览器访问 HTTP 服务器，所以必须找到和浏览器版本一致的“Webdirver”程序，然后下载下来。

我这里以工作中最常用的 Chrome 浏览器为例，首先要通过 Chrome 的“设置”—“关于”菜单查看浏览器的版本。我的 Chrome 浏览器版本如下图：

接着，根据我的浏览器版本“89.0”访问“Webdrive”国内的镜像站，下载与浏览器相匹配版本的“Webdriver”。这里要注意，不同的操作系统，“Webdriver”的安装包不同，所以需要你选择对应的操作系统版本。“Webdriver”的下载网页内容截图如下：

我这里以“mac”操作系统为例，下载了“chromedriver_mac64.zip”压缩包后，解压缩之后，会得到“chromedriver”文件。这个文件就是连接 Python 和浏览器的工具。

第二步，把“Webdriver”放在命令的搜索路径下。

“chromedriver”文件是一个可执行文件，我们需要把它放入命令的搜索路径中，这样文件才能在 Python 调用浏览器时被直接运行，我们就不用输入它的完整路径。

我在这里解释一下命令的搜索路径。它是每个操作系统都事先定义好的，就像它的名字一样，你在命令行运行的任意一条命令，都可以从命令搜索路径中依次查找到。

那么在 mac 操作系统下，我们可以使用命令行运行“echo $PATH”，来查看当前系统的命令搜索路径都有哪些。

在这个命令行中，变量“PATH”就是专门用于指定命令搜索路径的环境变量。我电脑上的命令搜索路径和搜索顺序如下。

SHELL$ echo $PATH
/Library/Frameworks/Python.framework/Versions/3.7/bin
:/usr/local/bin:/usr/bin
:/bin
:/usr/sbin
:/sbin

基于操作系统会按照上面的目录对命令进行搜索，我可以把“chromedriver”放在上面的任意一个目录当中。不过我更推荐你把它放在 Python 相同的目录中，便于你后续版本更新时，对它进行文件替换。

我的 Python 安装目录是“/Library/Frameworks/Python.framework/Versions/3.7/bin”，因此我就把“chromedriver” 放在这个目录中。放入之后，我就可以在命令行运行“chromedirver”。如果能够正确运行，则会提示运行成功。否则会提示找不到这条命令，你需要再检查一下它被存放的目录。正确运行的截图如下，供你参考。

在完成了最复杂的“Webdriver”安装之后，接下来就可以安装 Python 的库，并尝试使用 Python 库调用浏览器了。

第三步，安装“selenium”库。

安装“selenium”库可以继续使用 pip 命令。它的库和安装包同名，你可以在命令行运行以下命令，进行正确安装。

pip3 install selenium

第四步，使用“selenium”访问网页。

安装成功之后，我们可以通过任意一个网页的访问，来测试从 Python 到“Webdriver”再到浏览器，整个工具链是否能正确运行。我把代码贴在下方供你参考。

from selenium import webdriver
import time

browser =  webdriver.Chrome()

browser.get("http://www.jd.com")
time.sleep(10)
browser.quit()

这段代码实现了调用浏览器访问京东的网页，并在 10 秒后自动关闭浏览器的功能。如果能够正确运行，那么说明整个工作链是配置正确的。

这段用来访问京东来验证工作链的代码，由导入库、浏览器的初始化、控制浏览器行为三个部分组成。我来解释一下。

首先是导入库。我在代码的前 2 行，除了导入“selenium”库用来调用“Webdriver”外，还导入了“time”库。

导“time”库的原因就在于，我们需要使用“selenium”库模拟手工操作浏览器的流程，而手工操作会有一定的延迟。所以我就基于“time”库的“sleep()”函数，来模仿用户查看网页的行为，在打开网页等待了 10 秒后，再关闭网页。

其次是浏览器的初始化。浏览器的初始化在 Python 内部做了两件事情，第一个是找到之前安装的“chromedriver”文件，检查它是否可执行。第二件事情是根据调用的“Chrome()”函数，找到当前电脑中的浏览器并运行它。

最后控制浏览器的行为。我使用了“get() 和 quit()”两个函数，它们分别实现的是向服务器发起 HTTP 的“GET”请求，以及关闭浏览器的功能。

请求如果成功发送， 你会在“Webdriver”打开的浏览器看到网页内容，我把正确执行的结果截图放在下面供你参考。

第五步，取得网页的源代码并打印。

通过“selenium”打开浏览器之后，除了控制浏览器的行为，你还需要像上一讲的批量下载图片一样，批量获取网页的资源。这时你可以使用如下代码来获取当前浏览网页的源代码。这样在关闭浏览器之后，你仍然可以对网页的内容进行操作。代码如下：

content = browser.page_source
print(content)

掌握了“selenium”和“Webdriver”的安装和运行，接下来我们就可以利用“selenium”来实现京东的自动签到功能了。通过这一场景，来为你演示如何通过“selenium”模拟浏览器并实现自动签到。

如何实现京东自动签到

要想实现自动签到，你既要实现用户登录，又要模拟用户点击按钮。随着技术的日新月异，很多登录和按钮都不再使用简单的“POST”请求了。这时你就需要用“selenium”基于标签名字找到按钮的链接，像普通用户请求网站链接一样，不必理解登录和签到的内部的原理，就能实现自动化访问。

自动签到的原理与思路

“selenium”之所以能替代手动操作，达到自动化的效果，核心原因在于，我们可以通过“selenium”来模拟浏览器的操作。而我们可以通过对浏览器的功能拆解，把浏览器的交互行为，一一对应到“selenium”的非交互命令，之后就能实现自动化操作了。

这也正是自动签到的原理。再进一步说，“selenium”的行为越像浏览器，自动签到的功能就模拟得越完整。换言之，签到功能也就更自动化。那我们就来看看浏览器是如何实现登录和签到工作的。

使用浏览器进行登录有三个步骤，分别为：

打开登录页面；

切换到用户密码登录选项卡；

点击登录按钮。

我们依次来分析一下这三个步骤如果用“selenium”，各自是怎么实现的。

首先，打开登录页面，在浏览器中是鼠标点击“登录”按钮实现的，它的实现是向指定的 URL 发送“GET”请求，发送后服务端返回网页，就是你见到的登录页面了。

其次，打开登录页面后，默认的登录方式是二维码登录。

为了能让“selenium”能够模拟，我们需要通过用户名和密码方式登录。那接下来就切换到用户密码登录这一选项卡，这一切换动作在浏览器中可以通过使用鼠标实现。不过在“selenium”中，我们需要通过“GET”请求来替代它，才能实现非交互式的登录。

登录切换页面的网页源代码以及网页，我把相关截图一并放在下方，供你参考。

最后，你需要在浏览器输入用户名密码，而后点击“登录”按钮，实现登录。

由于京东的登录界面是经过加密的，所以不能使用“requests-html”直接发送“POST”请求来实现登录，这里仍然要继续使用“selenium”获取用户名、密码的输入框，输入之后，模拟点击按钮的“GET”请求到服务器之后，才能实现登录。

使用“selenium”模拟浏览器，实现自动登录

在了解了浏览器的原理之后，你就可以使用“selenium”模拟浏览器实现登录了。我把“selenium”登录京东的代码写在下方供你参考。

from selenium import webdriver
import time

browser =  webdriver.Chrome()

# 访问主页
browser.get("http://www.jd.com")
time.sleep(2)

# 访问登录页
browser.get("https://passport.jd.com/new/login.aspx?ReturnUrl=https%3A%2F%2Fwww.jd.com%2F")
time.sleep(2)

# 切换为用户密码登录
r = browser.find_element_by_xpath(
    '//div[@class="login-tab login-tab-r"]')
browser.execute_script('arguments[0].click()', r)
time.sleep(2)

# 发送要输入的用户名和密码
browser.find_element_by_xpath(
    "//input[@id='loginname']").send_keys("username")
time.sleep(1)
for i in "password":
    browser.find_element_by_xpath(
         "//input[@id='nloginpwd']").send_keys(i)
    time.sleep(1)

# 点击登录按钮
browser.find_element_by_xpath(
    '//div[@class="login-btn"]/a').click()
time.sleep(10)

# 退出浏览器
browser.quit()

我在代码中，为了模拟浏览器的登录行为，一共使用了 5 个函数，每个函数都对应着“selenium”的一种操作。按照代码的运行顺序，我分别使用了请求网页、使用 XPath 查找标签、执行脚本、模拟键盘输入以及模拟鼠标点击五个动作，来实现登录的模拟操作。接下来我们先从请求网页开始，来为你逐个分析这五个函数。

第一个函数 get() ，使用“GET”方式请求网页。当你需要点击超链接来到达新的页面时，都可以使用“get()”函数来实现。所以在代码中，请求主页和请求登录页面都是用了这个函数。

第二个函数 find_element_by_xpath() 。在网页中，你需要对某一标签进行操作时，可以使用“XPath”先找到该标签。类似地，在浏览器中，“find_element_by_xpath() ”就是找到你想操作的标签。

比如我在代码中找到用户名和密码的选项卡、用户名密码的输入框、登录按钮等元素，都是用过它进行定位的。定位的方法和上节课讲的方式相同，通过浏览器的调试页面找到网页元素对应的代码，再把代码利用“XPath”提取出来即可。

第三个函数 execute_script() ，是用来执行网页中自带的 JavaScript 脚本的。当你切换用户密码选项卡时，会发现网站的代码是用 JavaScript 来实现的，而你又不想再去手动编写脚本（执行“class”属性为“login-tab login-tab-r”的“div”标签下面的 JavaScript 脚本），那就可以使用这个函数，对第一个参数的 JavaScript 进行点击。

第四个函数 send_keys()，用于在输入框填写内容。我在脚本中把“username”和“password”作为用户名和密码自动填入输入框中，你可以把它们替换为真正的用户名和密码。

这里需要注意的是，我对密码的填入增加了间隔时间，否则登录时会弹出图形验证码，阻止你进行自动登录。

第五个函数 click() ，用于模拟鼠标点击超链接的动作，它和取得超链接中的 href 属性，并用“GET”方式访问是相同的功能，但是“click()”函数会比“get()”函数更直接，减少了从“a”标签再提取“href”属性的麻烦。

以上就是使用“selenium”模拟浏览器实现登录的主要思路。登录之后，我们想要实现签到的障碍就全都解决了。那么接下来我来带你继续实现自动签到功能。

利用“selenium”，实现自动签到

要想实现自动签到，还需要访问签到页面和点击签到按钮。这两个功能相信你在我为你分析了登录的代码之后，一定会想到访问页面可以使用“get()”函数，签到按钮可以使用“click()”函数。

我把自动签到的代码放在下面，一并提供给你。通过和登录代码的整合，你就可以利用“selenium”实现自动签到的功能了。

# 访问签到页面
browser.get("https://mall.jd.com/index-1000002826.html")
time.sleep(2)

# 签到并领金豆
browser.find_element_by_xpath('//div[@class="jSign"]/a').click()
time.sleep(10)

这段代码就是访问签到页面并领取金豆的代码。需要你注意的是，访问页面和签到两个操作之间需要等待几秒，否则会因为网页加载过慢，导致签到按钮还没加载完就发送了点击动作，让自动签到失败。

总结

最后，我来为你总结一下本节课的主要内容。在这一讲中，我通过“selenium”实现了浏览器的模拟，并把浏览器的点击链接、用户登录、切换标签等常用功能使用“selenium”的函数和 XPath 转换为用 Python 可以控制的操作。转换之后，你就可以利用 Python 控制浏览器的行为，把这些需要鼠标的交互式操作编写为非交互式的代码，达到自动化控制浏览器的目的了。

除了实现自动签到以外，通过“selenium”的函数组合，你就能模拟浏览器的绝大部分操作。我也希望你能够在学完这节课后，想一想你的工作当中是否也使用了浏览器进行重复的工作，希望你能通过对本讲的学习，把它们改为自动化。

思考题

按照惯例，最后我来为你留一道思考题，你能否利用今天讲的模拟浏览器的方法，自动登录 Github，并以“Python”作为关键字进行搜索，然后把搜索的结果保存到一个文件呢？

欢迎把你的想法和思考分享在留言区，我们一起交流讨论。也欢迎你把课程分享给你的同事、朋友，我们一起做职场中的效率人。我们下节课再见！

20｜不同文件混在一起，怎么快速分类？

你好，我是尹会生。

今天我们的内容要从一碗香喷喷的蛋炒饭开始。要想做一份传说中的蛋炒饭，肯定要放胡萝卜、黄瓜、火腿肠还有葱花等好多种类的食材。

这是不是像你的桌面一样，为了完成某一项目，需要将音频、视频、文档、图片等各种格式组合在一起。但你在你完成了项目之后，想要将它们进行整理的时候，会发现各类文件堆满了桌面，要想从桌面找都某个文件就像从蛋炒饭里将所有的葱花挑出来一样困难。

对于这种情况，我们可以采用 Python 按照扩展名，分门别类地整理到不同的目录，方法虽然找到了，但是在你动手写代码的时候发现也不容易，就像从蛋炒饭中把鸡蛋、米饭、胡萝卜丁、火腿肠等食材挑出来，分类型放在不同的盘子中。这无疑会让你非常头痛。

所以在今天这节课中，我就带你来学习一下，怎么用我们之前学习过的自定义函数、队列，来实现按照扩展名对文件的自动分类。

批量分类的方法与思路

在带你学习代码之前，我要先为你讲解一下解决这类问题的思路，因为像自动分类这种场景，可以被拆解成判断类型逻辑和移动逻辑，而这两个逻辑有前后顺序，还有前后依赖关系。这一大类问题，你在工作中会经常遇到，当你学会了这类问题的解决思路之后，再遇到同类问题，就能非常容易的想到处理逻辑，那再通过代码将你的思路实现出来，也就不在话下了。

要想实现自动分类，就要设计好分类的规则 ****，以及按照规则对每一个文件分类的通用模式。我们先来学习设计分类规则。

怎样设计合理的数据类型

分类规则是指将扩展名和要移动的目录建立对应关系，而想要保存对应关系，就必须设计一个合理的数据类型，既能保存这种对应关系，又不能太复杂，为自己编码带来困扰。由此来看，分类规则的核心就是设计合理的数据类型。

这么说，你可能有点难以理解。我先把代码提供给你，然后我来带着你分析，我们为什么要设计数据类型。代码比较长，为了让你有更好的学习效果，我把分类前和分类后的文件目录结构都提供给你。通过对照分析分类前后文件所在的目录，帮你理解自动分类代码的实现思路。

分类前的目录和文件结构如下：

$ ls -R files
dir1 a.mp4 c.rm d.avi b.mp3

files/dir1:
aa.exe  bb.bat

分类后的目录和文件结构如下：

$ ls -R files
dir1  execute  movie  music

files/dir1:

files/execute:
aa.exe  bb.bat

files/movie:
a.mp4  c.rm  d.avi

files/music:
b.mp3

对比分类前后的目录和文件结构，可以看到，我并没有把每一种扩展名保存在一个独立的文件夹中，而是把这些文件按照音乐、视频和可执行文件的方式进行了分类，然后再把同一类型的文件放在相同目录中。

这样的实现方式为工作中查找文件带来了便利，但是不可避免地，会增加我们编码工作的复杂度，因为你不能通过循环遍历一次文件来实现分类了。

我这么表述，你可能还不太理解它的难度具体在哪里，我们还是回到蛋炒饭的例子。如果把每个扩展名都放在一个目录中，就类似把蛋炒饭中的每种原材料都放在一个碗里。你只要准备和原材料类型相同数量的碗，去分类就好了。

而分类方式如果变成了只有三个碗，我们此时需要把材料要把主食、素菜、荤菜分别放在三个碗中，那你在遍历蛋炒饭的时候，就需要二次分类。

对于这样的需求，你在编写代码前需要设计合理的数据类型，把碗的数量和蛋炒饭的原料对应关系事先确定好。而确定的这一对应关系在编程语言中就被称作设计数据类型。

那怎样来设计合理的数据类型呢？让我们来看看文件自动分类中的“碗和原材料”。

文件自动分类功能中的“碗”是多个文件夹。但是在 Python 中，表示多个文件夹的时候，我们会采用字符串，方便和文件夹名称建立对应关系。而且你还可以通过创建文件夹的库，把字符串作为文件夹名字，实现字符串到文件夹的对应关系。

文件自动分类功能中的“原材料”是扩展名。扩展名也要使用字符串类型。那么每组文件夹到扩展名对应关系都是一个字符串对应多个字符串，相信你一定想到了这种对应关系应该使用字典保存它们的映射关系了吧，那作为字典值的多个扩展名，由于在运行程序前指定好就不会再修改了，所以我将扩展名字符串组成元组，将元组作为字典的值。

那么根据我们的分析，我把扩展名和文件类型定义为如下字典，如果你的工作场景使用到了更多的扩展名，或者使用了和我不同的分类，也可以修改这个字典。

# 定义文件类型和它的扩展名
file_type = {
    "music": ("mp3", "wav"),
    "movie": ("mp4", "rmvb", "rm", "avi"),
    "execute": ("exe", "bat")
}

通过刚才对字典的定义，我们给扩展名自动分类制定好了分类的规则。那接下来我们继续设计程序，按照这一分类规则，进行文件的读取和分类。

怎样设计生产者消费者模式

文件的读取和分类是两个不同的功能，你在编写代码时可以把它们编写成两个不同的函数。

但是由于今天的程序比以往要复杂，所以实现这两个函数的思路也会比较多。比如：

你可以把所有的文件全部读取之后，再按照分类规则移动到新的目录；

也可以读取一个紧接着将它移动到一个新的文件夹里；

当然还可以读取一部分文件之后，将一部分文件进行移动，然后继续读取第二批文件。

到底选择哪种方案才是最佳实践呢？

在这种情况下，你最希望的是能够向有丰富开发经验的开发人员请教，看他是怎么实现类似需求的，然后按照他的实现逻辑来编写你的代码。这也是专业软件开发人员面对这一问题时的通常做法：去寻找和当前场景相似的“设计模式”。因为设计模式是众多软件开发人员经过相当长的时间，对某一场景进行大量试错总结出来的经验。那我们可以利用它，来解决我们当前场景下的问题。

我们当前的场景刚好和设计模式中的“生产者消费者模式”比较吻合。生产者消费者模式的描述是这样的：有两个进程共用一个缓冲区，两个进程分别是生产数据和消费数据的。而缓冲区，用于存放生产进程产生的数据，并让消费进程从缓冲区读取数据进行消费。

使用生产者消费者模式刚好能解决文件读取和文件分离的逻辑。我把读取当前文件名称和路径函数作为生产者，把分类和移动文件的逻辑作为消费者。在生产者消费者中间，我再使用队列作为它们中间的缓冲区。

可以看到，使用生产消费者模式，我主要是增加了一个队列，而不是从生产者直接把数据交给消费者。这样做主要有三个好处：

如果生产者比消费者快，可以把多余的生产数据放在缓冲区中，确保生产者可以继续生产数据。

如果生产者比消费者慢，消费者处理完缓冲区中所有数据后，会自动进入到阻塞状态，等待继续处理任务。

缓冲区会被设置为一定的大小，当生产者的速度远远超过消费者，生产者数据填满缓冲区后，生产者也会进入到阻塞状态，直到缓冲区中的数据被消费后，生产者才可以继续写入。而当消费性能不足时，可以等待消费者运行，减少生产者和消费者在进度上相互依赖的情况。

通过分析我们发现，可以采用生产者消费者模式来编写文件的读取和分类代码。

考虑到是你初次接触设计模式，为了不让你产生较大的学习心理负担，我把其中的多线程并发访问缓冲区简化成单线程版本，这样你能根据代码的执行过程，先学会简单的生产者和消费者模式。

在分类规则的“file_type”字典之后，我增加了以下代码，实现了单线程版本的生产者消费者设计模式。如下：

from queue import Queue
# 建立新的文件夹
make_new_dir(source_dir, file_type)

# 定义一个用于记录扩展名放在指定目录的队列
filename_q = Queue()

# 遍历目录并存入队列
write_to_q(source_dir, filename_q)

# 将队列的文件名分类并写入新的文件夹
classify_from_q(filename_q， file_type)

上面的代码实现了从定义队列到文件处理的完整函数调用。在后续（第 22 节、28 节）我为你讲完面向对象、类和多线程后，我会带你再实现一个多线程版本的生产者消费者模型，让你完全掌握这一设计模式，并应用到更多的场景中。

在确定了分类规则用到的数据模型，以及分类流程用到的设计模式之后，接下来就到了具体实现代码的环节了。

在生产者消费模式下，我通过定义三个函数来分别实现三个功能，如下：

定义函数 make_new_dir()，实现新建分类文件夹的功能；

定义函数 write_to_q()，实现写入当前文件路径到队列的功能；

定义函数 classify_from_q()，实现把队列中的文件分类并移动的功能。

接下来，我带你依次学习一下它们各自的实现代码。

如何实现分类

要想实现分类，首先要先创建分类需要的文件夹。这里需要注意，创建文件夹的操作要在批量分类前完成，否则在每次移动文件前，你还得对要移动的文件夹进行判断，这会影响程序的运行效率。

我们来看一下怎样利用分类规则的字典“file_type”，以及 make_new_dir() 函数来批量分类文件夹。

如何建立分类文件夹

批量建立文件夹操作的前提是建立哪几个文件夹，以及在哪个目录下建立它。基于这样的考虑，我为 make_new_dir() 函数增加了两个参数：

使用 dir 指定建立文件夹的目录；

使用 type_dir 指定按照哪个字典建立。

而建立文件夹，可以使用我们学习过的 os 模块，通过 os.mkdirs() 函数建立一个新的文件夹。代码如下：

import os
# 定义文件类型和它的扩展名
file_type = {
    "music": ("mp3", "wav"),
    "movie": ("mp4", "rmvb", "rm", "avi"),
    "execute": ("exe", "bat")
}

source_dir = "/Users/user1/Desktop/files

def make_new_dir(dir, type_dir):
    for td in type_dir:
        new_td = os.path.join(dir, td)
        if not os.path.isdir(new_td):
            os.makedirs(new_td)

# 建立新的文件夹
make_new_dir(source_dir, file_type)

这段代码把字典的 key 作为文件夹名称，通过遍历字典来批量创建文件夹。这里还有两个你需要注意的技巧。

第一个是文件路径的拼接。代码中要新建的文件夹路径，是由“source_dir”和遍历字典得到的“字典的 key”两部分连接组成的。如果你使用字符串的连接函数“join()”函数来连接这两部分，你需要增加路径连接符号"/"，而如果你的操作系统从 mac 换成 windows，则需要使用反斜线""，这时候你就要再修改代码，把斜线改为正确的路径分隔符。

因此我采用了“os.path.join()”函数，这个函数会自动判断操作系统并增加斜线"/"，它还避免了你为已经有“/”的路径重复添加的问题。

另一个小技巧是判断目录是否存在。我在创建目录前，使用了 os.path.isdir() 函数，判断了目录是否存在，这样做的好处是避免重复创建目录。

另外，我还想教给你和它功能相近的两个函数，它们分别是 os.path.isfile() 和 os.path.isexist()。

前者用来判断该路径是否存在，并且是否是同一个文件类型。

后者用来判断路径是否存在，并且这个路径可以是文件也可以是目录。

结合代码中出现的 isdir() 函数，你就可以对一个目录到底是文件还是目录，以及是否存在进行判断了。

创建目录之后，我们就要开始对当前的文件进行遍历，并存入缓冲区中。

怎样遍历目录并写入队列

我先把遍历目录的代码写在下面，然后再为你详细讲解它。

from queue import Queue

# 遍历目录并存入队列
def write_to_q(path_to_write, q: Queue):
    for full_path, dirs, files in os.walk(path_to_write):
        # 如果目录下没有文件，就跳过该目录
        if not files:
            continue
        else:
            q.put(f"{full_path}::{files}")

#########
source_dir = "/Users/user1/Desktop/files

# 定义一个用于记录扩展名放在指定目录的队列
filename_q = Queue()

# 遍历目录并存入队列
write_to_q(source_dir, filename_q)

这段代码实现了定义队列，并把指定目录下所有的文件名称和路径写入到队列中的功能。在这里有两个关键的知识点需要你掌握，它们分别是如何遍历目录，以及如何写入队列。

先来看如何遍历目录的函数。它在代码的第 5 行，叫做 os.walk() 函数，和之前我们学习过的 pathlib() 函数一样，都能实现对目录的遍历，但是它的返回值值得你学习一下。

我使用 for 循环遍历 walk() 函时，分别使用了 full_path、dirs 和 files 三个变量，因此 walk() 函数的返回值有三个。这三个变量分别对应每次遍历的文件的完整路径、文件所在的目录，以及该目录下所有文件名称的列表。

你可以根据你的工作场景灵活组合这三个变量，由于我在移动的场景需要文件的完整路径和文件名，所以我只使用了第一个参数 full_path 和第三个参数 files。

此外，我在实现遍历时，也像创建目录一样增加了容错。如果某一目录下没有文件，就不需要对该目录进行移动了，所以我使用了“if not files” 来判断 files 列表的值。

由于我增加了 not 关键字，if 的判断条件就从列表中包含文件，变成了列表中没包含任何一个文件。当条件成立时，则执行 continue 语句，跳过当前这次循环。而 else 语句中，是当 files 列表中包含了文件名称的处理过程，在这种情况下，我会将文件的完整路径和该路径下的文件列表放到缓冲区中。

在当前代码，我把队列这一数据类型作为缓冲区，它和我们之前学习过的多进程通信的队列功能和用法完全相同，区别则是我们导入的库名称不同。

要想把对象存入队列，可以使用 put() 函数。从队列取出数据，则可以使用 get() 函数。我把循环遍历得到的路径和文件名称均使用了 put() 函数存放到队列中，实现了生产者这一角色。

接下来，我们来学习消费者这一角色实现的代码，学习如何实现分类并将文件移动到新的文件夹的。

分类并移动到新的文件夹

同样的，我先把代码写在下面，然后再为你详细分析如何实现从队列取出文件名并进行分类的功能。

# 移动文件到新的目录
def move_to_newdir(filename_withext, file_in_path, type_to_newpath):
    # 取得文件的扩展名
    filename_withext = filename_withext.strip(" \'")
    ext = filename_withext.split(".")[1]

    for new_path in type_to_newpath:
        if ext in type_to_newpath[new_path]:
            oldfile = os.path.join(file_in_path, filename_withext)
            newfile = os.path.join(source_dir, new_path, filename_withext)
            shutil.move(oldfile, newfile)

# 将队列的文件名分类并写入新的文件夹
def classify_from_q(q: Queue, type_to_classify):
    while not q.empty():
        # 从队列里取目录和文件名
        item = q.get()

        # 将路径和文件分开
        filepath, files = item.split("::")

        # 剔除文件名字符串出现的"[" "]",并用"，"做分隔转换为列表
        files = files.strip("[]").split(",")
        # 对每个文件进行处理
        for filename in files:
            # 将文件移动到新的目录
            move_to_newdir(filename, filepath, type_to_classify)

在这段代码中，我实现了从队列取出文件名称和目录，并根据分类将文件移动到新的目录。

由于消费者的逻辑是从队列读取内容和移动文件两个功能组成的，所以我把消费者拆分成了两个函数进行编写：

classify_from_q() 函数，用来实现从队列读取文件列表，并遍历列表，得到每一个文件名称；

move_to_newdir() 函数，把文件名称、路径、分类规则作为参数，真正实现移动。

相应的，如果你在编写包含多个功能的程序时，也要尽量保持每个功能的独立性，把每一个功能尽量放在一个函数中，这样能有效提升你的代码的可读性。

这两个函数虽然比较长，但是大部分都是我们学过的内容，我想为你重点讲解一下第一次接触到的两个知识点，一个是 in 操作，一个是利用 shutil 库的 move() 函数实现的重命名。

in 操作叫做成员操作符，它能支持目前我们学习过的所有基础数据类型，用来判断一个值是否是列表、元组、字典等基础数据类型中的一员。如果这个值是基础类型的成员之一就会直接返回 True，如果不是成员之一返回的就是 False。有了 in 操作符，你就不用手动遍历基础数据类型，再使用“==”逐个去判断某个值和数据类型中的成员是否相等了。

我举个例子，你会更容易理解。我在代码中使用了这样一行代码：“if ext in type_to_newpath[new_path]” ：

“ext” 就是文件的扩展名，就像是“a.mp3”的扩展名是“mp3”；

“type_to_newpath[new_path]”是字典“type_to_newpath”中，以“new_path”作为 key 的值，就像是“type_to_newpath = { "music": ("mp3", "wav") }”的“("mp3", "wav")”。

我把变量改成具体的变量值，那这行代码就变成了下面的样子：

"mp3" in ("mp3", "wav")

如果扩展名在元组中，那么 if 条件的返回结果就是 True，就可以进行文件的移动了，如果结果是 False 则从字典中继续取下一个 key，直到所有的 key 遍历完成之后，仍然没有匹配的扩展名，就把文件保持在原地，不做任何移动操作。

还有一个我们第一次接触到的函数是 shutil 库的 move() 函数，这个函数是直接对系统上的文件进行操作的，所以你需要注意移动以后的文件名不要和已有的文件名冲突，这样会导致重名覆盖已有的文件，从而丢失文件。因此在你没有十足的把握之前，建议你在移动前增加一个判断功能，判断移动的文件是否存在，如果存在则提示使用脚本的人此情况，或移动前将文件进行改名。

以上就是如何对混在一起的多个扩展名的文件，进行自动分类的完整过程。这节课的完整代码比较长，我一并贴在了下方，帮你理解多个函数之间的调用关系和执行顺序。

import os
import shutil
from queue import Queue

# 建立新的目录
def make_new_dir(dir, type_dir):
    for td in type_dir:
        new_td = os.path.join(dir, td)
        if not os.path.isdir(new_td):
            os.makedirs(new_td)

# 遍历目录并存入队列
def write_to_q(path_to_write, q: Queue):
    for full_path, dirs, files in os.walk(path_to_write):
        # 如果目录下没有文件，就跳过该目录
        if not files:
            continue
        else:
            q.put(f"{full_path}::{files}")

# 移动文件到新的目录
def move_to_newdir(filename_withext, file_in_path, type_to_newpath):
    # 取得文件的扩展名
    filename_withext = filename_withext.strip(" \'")
    ext = filename_withext.split(".")[1]

    for new_path in type_to_newpath:
        if ext in type_to_newpath[new_path]:
            oldfile = os.path.join(file_in_path, filename_withext)
            newfile = os.path.join(source_dir, new_path, filename_withext)
            shutil.move(oldfile, newfile)

# 将队列的文件名分类并写入新的文件夹
def classify_from_q(q: Queue, type_to_classify):
    while not q.empty():
        item = q.get()

        # 将路径和文件分开
        filepath, files = item.split("::")

        files = files.strip("[]").split(",")
        # 对每个文件进行处理
        for filename in files:
            # 将文件移动到新的目录
            move_to_newdir(filename, filepath, type_to_classify)

if __name__ == "__main__":
    # 定义要对哪个目录进行文件扩展名分类
    source_dir = "/Users/edz/Desktop/files"

    # 定义文件类型和它的扩展名
    file_type = {
        "music": ("mp3", "wav"),
        "movie": ("mp4", "rmvb", "rm", "avi"),
        "execute": ("exe", "bat")
    }

    # 建立新的文件夹
    make_new_dir(source_dir, file_type)

    # 定义一个用于记录扩展名放在指定目录的队列
    filename_q = Queue()

    # 遍历目录并存入队列
    write_to_q(source_dir, filename_q)

    # 将队列的文件名分类并写入新的文件夹
    classify_from_q(filename_q, file_type)

小结

最后让我来为你做个总结，实现文件自动分类是目前我们编写代码量最多的一讲。面对功能复杂、代码量增多时，你就需要通过函数设计合理的功能封装，还要考虑如何使用参数进行函数的通信。

当你的多个函数之间的工作流程也可以进行多种组合时，你可以借助开发高手的代码经验 -- 设计模式，来实现工作逻辑上的函数组合。在本讲中我为你介绍的这种普遍应用于产品生产、销售的生产者消费者模式就是设计模式中最常用的一种。

希望你能在掌握如何使用 Python 提高工作效率的同时也能掌握设计模式、函数这些编写 Python 的思路。这样，你在面对更庞大的需求时，也会更快地设计出结构清晰、逻辑清楚的代码。高效编程也是高效办公的一部分！

思考题

我来为你留一道思考题，如果我按照文件的大小对文件分成三类，将“大于 1GB”“1GB 到 100MB”“小于 100MB”三类的文件名和大小，依次显示在屏幕上，你会怎样实现呢？

21｜SQLite文本数据库：如何进行数据管理（上）？

你好，我是尹会生。

你在办公中，一定遇到过需要数据持久化的问题。数据持久化，简单来说，就是当你关闭程序的时候，数据依然可以完整地保存在电脑中。你可能会想到用文本文件、Excel 来存储这些数据，文本呢，没有办法按列读写数据，Excel 呢，支持的默认 API 无法进行复杂查询。所以我今天要给你介绍一个功能强大，但编写代码又简单的数据库 SQLite。

你可以用 SQLite 存储结构化的数据，把程序的处理结果保存到电脑中，便于下次或使用新的程序对这些数据进行访问。

用 SQLite 存储结构化的数据，包括增删改查这些操作。所以今天这节课，我就教你怎么来使用函数封装 SQL 语句，实现数据的读取和写入，下一节课我们再来学习如何通过类实现复杂的 SQL 语句的封装，以及如何更新和删除数据。

在讲解这些之前，考虑到 SQLite 在持久化数据存储的重要性，我想要先给你介绍 SQLite 的优势。

使用 SQLite 代替文本和 Excel 有哪些优势

也许你对 SQLite 这个名字还很陌生，但其实你早就在手机、智能电视、机顶盒等电子设备中用到过它了，比如手机上的通讯录，就是使用 SQLite 存储联系人的。

SQLite 中存储的内容是结构化数据，像是通讯录、企业 ERP 数据、财务数据等这些存储和排列很有规律的数据，就被称作结构化数据。类似 Excel 的格式一样，分为“行”和“列”。以存储通讯录为例，每一列会提前指定好哪一列存放姓名、哪那一列存放电话号码，而每一行就是一个联系人的姓名和电话的具体记录。

在自动化办公中，你可以把结构化数据存放到 SQLite 中，它的处理速度和文件体积上要比文本文件和 Excel 更有优势。它会比文本文件的处理速度更快，比 Excel 需要的空间更少。甚至有人将 SQLite 应用到了每天点击量在 10 万次的网站上，足见它的稳定和高效的处理效率。

SQLite 和你听说过的大型数据库 Oracle、MySQL 数据库不同，它更多是面向办公场景和移动设备的，所以它有大型数据库的稳定、高效、支持 SQL 语言的特性，但是也要比大型数据库学习起来更加简单。

正是由于它拥有数据库的特性，所以从 SQLite 中查找数据要比从文本文件中更快。而且它的数据还满足数据库必需的“增删改查”，但不支持设置字体、字号，所以存储一条数据所使用的空间会比 Excel 更小。这些特性叠加起来就刚好形成了 SQLite 的特有优势：

数据查询速度快；

存放数据的空间占用少；

实现了一般数据库能够支持的（基于 SQL 语言的）增删改查。

总结来说，就是如果你需要存放结构化的数据，而且只关注数据读取的效率，不关注数据的样式，而且还需要编程语言来进行数据访问的话，使用 SQLite 要比文本文件、Excel 更适合你的办公场景。

我为你介绍了 SQLite 这么多好处，那赶快带着你把它用起来吧。

为 SQLite 建立数据表

要想使用 SQLite 数据库，光有文件还不够，你还要为它建立数据表，类似你新建了一个 Excel 工作簿的文件，与此同时你还要建立一个工作表，把数据写在工作表上， 再将多张工作表放在工作簿上面。

因此，要想实现对数据库的操作，我也需要为 SQLite 创建一张工作表，接下来我就用一个创建手机通讯录数据表的脚本，为你演示一下我是如何为 SQLite 数据库创建它的“工作表”的。

SQLite 建立数据表的一般流程

为了能够让你更具体地学习数据表是怎样创建和使用的，我先把创建数据表的代码写出来给你，对照代码我来为你讲解，SQLite 建立数据表的流程。

import sqlite3
import pathlib

# 数据库文件的路径和文件名称
dir = pathlib.PurePath(__file__).parent
db = pathlib.PurePath(dir, "contents.db")

# 创建连接
conn = sqlite3.connect(db)

# 创建游标
cur = conn.cursor()

# 定义要执行的SQL语句
sql = '''CREATE TABLE address_book(
        id INT PRIMARY KEY NOT NULL,
        name TEXT NOT NULL,
        phone INT NOT NULL
       )'''

# 执行SQL
try:
    cur.execute(sql)
    print("创建成功")
except Exception as e:
    print("创建失败")
    print(f"失败原因是：{e}")
finally:
    # 关闭游标
    cur.close()
    # 关闭连接
    conn.close()

这段代码实现了连接 SQLite 数据库，并创建通讯录数据表的功能。要想使用 SQLite 数据库，你需要掌握四个步骤，我来带你依次学习。

第一步，连接数据库文件。

连接数据库文件有两种情况，数据库文件不存在和数据库文件已经存在。如果数据库文件不存在，那么第一次连接时就会建立一个空的数据库文件，你需要注意的是，创建时要确保 Python 程序对操作数据库文件的目录有写入权限，否则会因权限不足而创建失败。另一种情况，如果数据库文件已经存在，则会直接打开数据库文件，相应的数据库文件中已经建立的表和数据，也可以直接进行操作。

我在代码中演示的是数据库文件不存在的情况。为了便于你观察数据库是否被创建成功，我在代码中使用了之前学习过的“file”变量，将数据库文件 contents.db 和脚本放在了相同的目录。

第二步，创建游标。

游标，它的官方定义叫做操作行指针。它是第一次接触数据库的人，最难理解的概念，其实你可以想象一下，当你只能按行操作 Excel 时，你可以一次选中一行，也可以一次选中多行。而游标就是记录你当前选中的到底是哪一行。

在计算机中记录当前选中的行，是需要占用内存的，因此，你必须先声明一个游标对象，利用这一对象申请计算机内存来记录你当前的行数，用于后续对选中行继续操作。

创建游标的函数是 cursor()，创建之后，一般会将它赋值给一个变量，后续操作可以利用变量来再次引用创建好的游标。

第三步，执行 SQL 语句。

SQL 语句是和 SQLite 数据库“打交道”的主要形式，你需要通过 SQL 语句来操纵数据库。在上面的代码中，我就在代码的 15 行定义了一个创建表的 SQL 语句；在代码的第 23 行通过“excute()”函数运行该语句，运行之后就可以为 SQLite 创建表了。

第四步，关闭游标和连接。

当你完成对数据库的操作后，就需要手动关闭游标和连接，来正确退出数据库，这样可以避免在内存中数据还未来得及写入磁盘时，由于突然关闭进程导致数据丢失。

以上四个步骤是创建数据表的基本操作，也是 SQLite 乃至其他数据库使用 SQL 语句的通用步骤，你需要记住这种模式，这样你在使用 Python 来操作各种数据库时，保证数据的持久化。

我在代码中还使用了一个小技巧，叫做异常捕获。通过异常捕获你可以在程序运行过程出现问题时，及时对问题进行处理。

捕获异常就像是你在森里中捕猎，猎物就是森林中你无法预知什么时候会出现的小动物。而捕获它们的方法，就是在猎物必经之路上设置陷阱。如果没有出现猎物，一切照常；如果出现了猎物，就会被陷阱捕获到。

例如我在代码中设置的陷阱就是“try”语句块，当这一语句块中的“excute()”在执行时如果出错，那么它后面的语句不会再继续执行，而是直接转向 except 语句。并且你会得到你的“猎物”-- 产生异常的原因“Exception”类。你可以像我一样将它重新命名为“e”，并输出到终端，帮你定位这一错误产生的原因。

相信你还记得我在讲操作 SQLite 四个必要步骤时提到的最后一个步骤，你不能因为出现错误，而“忘记”关闭游标和数据库，所以在“try”“except”语句中还有一个和它们配合使用的“finally”语句，它的作用是无论是否抛出异常，“finally”语句块下的代码都会被执行。

你看，增加了异常处理的数据库处理流程就要比原始的代码更加健壮。因此我建议你在进行数据库操作时，务必要为执行 SQL 语句的部分增加异常处理，因为它关系到你的数据是否能够安全的被保存到硬盘中。

建立数据表的 SQL 语句

在你了解了 SQLite 的一般流程之后，我再为你讲解一下建立数据表的 SQL 语句。

SQL 语句，类似你使用“open()”函数打开文件之后，对文件内容执行的各种操作。根据不同的标准，它有不同的语法格式。SQLite 能够支持的是大部分的 SQL92 标准（触发器和可写视图不被支持），因此，你可以使用符合 SQL92 标准的语句创建、删除表，并对数据进行增删改查的基本操作。

创建一张数据表，使用的 SQL 语句是“CREATE TABLE 表名称 （包括表的字段、字段类型和约束条件）。我还是以创建通讯录的表为例：

 CREATE TABLE address_book(
        id INT PRIMARY KEY NOT NULL,
        name TEXT NOT NULL,
        phone INT NOT NULL
        )

这条语句的“address_book”是表名称，它有三个字段（也可以称作是三列数据），分别为 id、name 和 phone。而每个字段都由字段名称、字段类型和可选的约束条件三部分组成。

字段名称，是用于操作表时方便进行列操作的名字。

字段类型，会限制你对该字段写入数据的类型，如果类型不正确会报错或被填入空值。SQLite 能支持的类型非常多，我在示例中使用了最常见的整数和字符串，其他类型你可以通过官方文档来了解。

约束条件，是对该列强制执行的规则。例如我使用了“NOT NULL”约束，确保在数据写入时，该字段不能为空值。同时，我还使用了“PRIMARY KEY”，确保了字段唯一性，确保该字段的值不会重复。更多的约束条件，你同样可以根据官方文档来了解。

通过创建 SQLite 的数据表，我为你讲解了操作 SQLite 的基本流程中已经用到的 SQL 语句，以及如何使用异常捕获来使程序更加健壮，特别是像操作数据的程序，你要保证它在运行时出现的任何错误能在第一时间发现，这样才可以尽可能保证你的数据完整。

为了对数据实现操作，你还要在掌握创建表的基础上实现数据的增删改查，而其中的写入和查询是实际工作场景中对数据库最多的操作，接下来我就来带你学习一下写入和查询操作需要用到的 SQL 语句。

使用 SQL 实现对 SQLite 的写入和查询

对一个数据库的各种操作，查询一般会占到 70%，而且大部分对数据的修改操作其实都隐含着查询。所以说查询是数据库中最重要的操作也不为过。

对 SQLite 数据库的表，实现写入和查询分别要使用“INSERT”和“SELECT” SQL 语句，接下来我还是以通讯录的数据表为例，先为你插入通讯录汇总的联系人，然后通过查询的“SELECT”语句取出表中的联系人信息，通过这样的例子来给你演示一下怎么为 SQLite 数据库进行数据的写入和查询。

数据的写入

首先，我为通讯录添加一个联系人 Tom，它的实现代码如下：

import sqlite3
import pathlib

# 数据库文件的路径和文件名称
dir = pathlib.PurePath(__file__).parent
db = pathlib.PurePath(dir, "contents.db")

# 创建连接
conn = sqlite3.connect(db)

# 创建游标
cur = conn.cursor()

# 定义要执行的SQL语句
sql1 = '''INSERT INTO address_book
          VALUES (?, ?, ?)'''
v =  (1, "Tom", 12377778888)

# 执行SQL
try:
    cur.execute(sql1, v)
    conn.commit()

except Exception as e:
    print(f"失败原因是：{e}")

finally:
    # 关闭游标
    cur.close()
    # 关闭连接
    conn.close()

通过执行这段代码，你可以给通讯录增加一个用户“Tom”，以及他的电话号码“12377778888”。在这段数据写入的代码中，有两点需要你注意，分别是 SQL 语句和数据的拆分，以及写入后的提交功能。

SQL 语句和数据的拆分，是指用于写入数据的 SQL 语句“INSERT”语句与真正要写入表中的数据需要保存在不同的变量中。当它们在真正执行 SQL 语句的那一刻，才会由 excute () 函数进行合并。

写入两个变量的好处在于，数据往往是经过计算或用户输入得到的，而 SQL 语句是可以复用的，把数值脱离 SQL 语句可以更方便地在 Python 中进行处理。所以我也建议你在进行数据写入时，都采用这种拆分数据和 SQL 语句的方法，这样会比一条 SQL 语句中既包含 INSERT 语句，又包含数据，灵活性上要提高很多。

另一个需要注意的是在执行 SQL 语句写入表之后，需要使用 commit() 函数进行数据提交。这是 SQLite 的事务机制导致的，因为使用事务可以避免数据不一致的情况。

事务其实是个比较大的话题，也是一个比较系统的工程，要想把事务的细节讲清楚，那就能写一本书了。不过由于 SQLite 没有支持较为完整的事务以及办公自动化场景，所以今天这节课我就不展开为你讲解事务的概念了，这也并不影响你对今天这节课的理解。

数据的查询

在实现了数据写入之后，我们继续来看一下如何使用 SQL 实现数据的读取。从 SQLite 读取数据的代码如下：

import sqlite3
import pathlib

# 数据库文件的路径和文件名称
dir = pathlib.PurePath(__file__).parent
db = pathlib.PurePath(dir, "contents.db")

# 创建连接
conn = sqlite3.connect(db)

# 创建游标
cur = conn.cursor()

# 定义要执行的SQL语句
sql2 = '''SELECT phone FROM address_book WHERE name = "Tom" '''

# 执行SQL
try:
    result = cur.execute(sql2)
    print(result.fetchone())

except Exception as e:
    print(f"失败原因是：{e}")

finally:
    # 关闭游标
    cur.close()
    # 关闭连接
    conn.close()

这段代码使用了“SELECT”SQL 语句实现了联系人“Tom”的手机号码读取。“SELECT”语句执行后，会把结果放到“result”变量中。由于取出的结果是一个对象，因此还需要通过 fetchone() 函数，把对象中的一行取出，得到用户的手机号码。

那在这一查找流程中，我们要重点关注的就是“SELECT”语句，它是实现读取内容的语句。在任何一个数据库中，它都是最常用的 SQL 语句。我们从它的结构开始分析。

SELECT phone FROM address_book WHERE name = "Tom"

这条语句的执行顺序和书写顺序是不同的，它的执行顺序是从“FROM”到“WHERE”，再到“SELECT”关键字。它们三个的含义分别是：

FROM：从一张表中查找数据，用于来指定查询的表。

WHERE：表示查询的条件是什么。在这个例子中，查询条件是 name 字段的值为“Tom”字符串。

SELECT：显示哪一列。在这个例子中，只显示 phone 这一字段的值。

“SELECT”语句按照代码中的顺序实现了电话号码的查找。当然了，这只是个非常简单的查找，而你在工作中肯定会遇到更加复杂的查找，我也会在下一讲教你怎样使用 Python 来为更加复杂的查询编写高效的代码。如果你想要更加深入的学习 SELECT 语句，可以参考官方文档。

官方文档中会有“SELECT”中其他语句的执行顺序和功能介绍，并且还采用了流程图方式来为你介绍它的子句之间是如何进行组合的。如下图：

小结

最后让我来为你总结一下，在本讲中，我使用了 SQLite 数据库为你讲解了数据库对比文本文件和 Excel 的优势，它在存储结构化数据方面，已经作为了业界的标准解决方案。

想要用好 SQLite，你还要掌握数据库的四个操作步骤，那就是：

连接数据库文件

创建游标

执行 SQL 语句

关闭游标和连接

这四个步骤是实现一切数据库操作的基础，在掌握四个操作步骤的基础上实现数据操作的 SQL 语句，包括增删改查四种操作，在本讲中我为你讲解了如何使用 SQL 实现 SQLite 的读写，下一讲将为你讲解更加复杂的 SQL 语句以及修改和删除的 SQL 语句。

查找和写入作为最常用的 SQL 语句，是需要你重点掌握的。除了存储通讯录数据，像是我们从网络批量下载的数据，以及工作中的员工信息表等数据都可以采用 SQLite 存放。它将是以后你存储结构化数据最实用的工具。

思考题

按照惯例，我为你出一道思考题，如果使用 SELECT 语句查询到的结果不止一条，使用 fetchone() 函数得到的是什么样的呢？你能否通过官方文档再找到获得多条查询结果的函数呢？

欢迎把你的想法和思考写在留言区，我们一起交流讨论。如果这节课在数据保存上帮你提高了办公效率，那也欢迎你把课程分享给你的朋友或同事，我们一起做职场上的效率人。

22｜SQLite文本数据库：如何进行数据管理（下）？

你好，我是尹会生。

在上节课，我提到了使用比较简单的 SQL 来操作 SQLite，并为你讲解了数据库的基本操作步骤。

不过当你的程序功能越来越强大的时候，随之而来的就是代码的复杂度越来越高。像是上一讲，我们在进行 SQLite 数据库搜索的时候，你需要建立连接、申请游标对象，才能进行查询。而这些准备工作，我们更希望在程序运行的时候就准备好，这样就不必多次重复编写。

而且对数据库进行增删改查能够通过尽可能少的 SQL 来实现数据库的操作。那么能实现这一功能的就是类。

通过类，你可以为越来越复杂的程序编写结构更清晰的代码。同时也能更好地把 SQLite 的增删改查封装成一个独立的对象，便于你调用数据库时能进行数据持久化。

那么今天这节课，我就带你使用类来实现 SQLite 数据的读取和写入。与此同时，我会继续以通讯录为例，来给你讲解，如果使用了比较复杂的 SQL 来操作 SQLite 时，怎么合理组织代码结构，让你更优雅地书写代码。

使用类实现 SQLite 的读写

由于类这个概念比较抽象，我还是采用老办法帮你理解它，我将使用“类”对 SQLite 的读写 SQL 操作进行封装，并将类进行实例化以后进行调用，得到 SQLite 中的通讯录数据。我先把代码贴出来，供你参考：

import sqlite3
import pathlib

class OptSqlite(object):
    def __init__(self, dbname = "new.db"):
        """
        :param dbname  数据库名称
        """
        self.dir = pathlib.PurePath(__file__).parent
        self.db = pathlib.PurePath(self.dir, dbname)
        self.conn = sqlite3.connect(self.db)
        self.cur = self.conn.cursor()

    def close(self):
        """
        关闭连接
        """
        self.cur.close()
        self.conn.close()

    def get_one_phone(self, username):
        """
        获取一个联系人的电话
        """

        self.get_user_phone_sql = f"""
            SELECT phone FROM address_book WHERE name = "{username}" """
        try:
            self.result = self.cur.execute(self.get_user_phone_sql)
            return self.result.fetchone()
        except Exception as e:
            print(f"失败原因是：{e}")

    def set_one_phone(self, name, phone):
        """
        增加一个联系人
        """
        self.set_user_phone_sql = '''INSERT INTO address_book
          VALUES (?, ?, ?)'''
        self.v =  (2, str(name), int(phone))
        try:
            self.cur.execute(self.set_user_phone_sql, self.v)
            self.conn.commit()
        except Exception as e:
            print(f"失败原因是：{e}")

if __name__ == "__main__":

    my_query = OptSqlite("contents.db")

    my_query.set_one_phone("Jerry","12344445555")

    phone = my_query.get_one_phone("Tom")
    phone2 = my_query.get_one_phone("Jerry")

    my_query.close()

    print(phone)
    print(phone2)

# 输出结果
# (12377778888,)
# (12344445555,)

在这段代码中，我使用类实现了两个连续操作：添加新的联系人“Jerry”，并取出联系人“Tom”和“Jerry”的手机号码。

通过代码，你会发现类的实现思路和语法，跟函数有非常大的区别，因此在你第一次使用类代替函数实现通讯录时，我要通过实现方式和语法方面来为你做个详细的对比，并且为你讲解类的初始化函数，在类实例化时是如何实现接收参数并自动初始化的。

总体来说，与使用函数实现数据库操作相比，类的最大优势就是完善的封装。

在使用类实现“SELECT”和“INSERT”这两个 SQL 操作的时候，你只需进行了一次初始化和关闭连接，后续的 SQL 操作都可以复用这次的连接，类能有效减少重复建立连接和重复初始化的工作。

因此在类似数据库封装这种功能复杂的代码中，你会看到更多的人选择用类代替自定义函数，实现开发需求。

从具体来讲，对比函数，类除了在封装方式上不同、语法和调用方式都不相同，我还是基于通讯录代码的封装和调用，为你讲解一下它和自定义函数的三个主要区别。

类和自定义函数的区别

首先，类和函数第一点区别就在于它们的对代码的封装方式上不同。

编写自定义函数，它的实现思路是通过函数去描述程序运行的过程，比如：代码的下一步需要做什么、需要什么参数。

而编写基于类的程序，它的实现思路更多要关注相同的一类数据，都有哪些属性和相同的动作。比如在代码中，我把数据库作为了一个类，因为类具有数据库名称这一属性，也具有查询和写入数据两个动作。而类在语法层面上，对属性和动作的封装要比函数更加完善。

在我工作中对建立数据库连接，以及执行查询、关闭数据库连接上都做过运行时间的测试，最终得出的结论是频繁地建立、关闭会给数据库带来较大的资源开销。因此，我在工作中会经常使用类把建立连接和关闭分别封装在多个查询动作之前和之后，确保这两个动作在多次查询时只执行一次，减少资源开销。

其次它们的语法结构也不同。函数是通过“def”关键字定义的，而类是通过“class”关键字定义的。

在编写一个新的类时，Python 语法还强制要求它必须继承父类，例如，我在编写的数据库类“OptSqlite”，就继承了父类“object”。继承父类意味这你可以在当前类中执行父类定义过的方法，而不需要再重新去编写一个定义过的方法。那如果你不需要继承其他类呢？这时候你就可以使用 object 作为你自定义类的父类使用。

同时，object 的关键字可以和定义类语法的“()”一起省略掉，因此你会看到其他人的代码出现，会有下面两种不同的写法，但含义却 (在 Python3.x 版本) 是完全相同的。我将两种写法写在下面供你参考。

class OptSqlite(object):
class OptSqlite:

最后它们的调用方式也不同。这一点主要表现在各自成员能否被访问和运行方式两方面。

类的定义中，可以定义当前类的属性和方法。属性就是类具有的数据状态，方法就是类对数据可以执行哪些操作。

在类中，可以设置哪些属性和方法能够被类以外的代码访问到，比如：我定一个了“鸟”类。并且定义了它的属性是黄色，它的动作是可以飞、可以叫。那么你可以借用变量这种形式来实现鸟类的属性，借用函数的形式实现鸟类能飞、能叫的动作。

此外，在定义属性和方法时，你还能限制它们的访问范围。像函数的调用，你只能访问它的函数名称和参数、中间的变量是不能被函数外的程序访问的。

是否能访问，在计算机中也被称作作用范围。在这一方面，类要比函数拥有更灵活的作用范围控制。

那在执行方式，类也和函数不同。函数执行时可以直接使用函数名 + 括号的方式调用它，如果需要多次执行可以使用变量存放多次执行的结果。

而类在执行时，一般要进行实例化。例如鸟类，在需要使用时，会实例化为一个对象“鸟 001”，对象就具有类的所有属性和方法。当你需要多次使用鸟类时，可以多次将鸟类实例化成不同的小鸟。

再回到通讯录的代码。类似的在通讯录的代码中，我将 SQLite 数据库定义为类以后，如果你的工作需要一个通讯录，就实例化一次。实例化之后的代码我单独拎了出来，如下：

my_query = OptSqlite("contents.db")

如果需要多个通讯录，就把它实例化多次，并指定不同的 SQLite 数据库即可。每个数据库实例，都会有一个“get_one_phone()”方法和一个“set_one_phone()”方法，来实现通讯录中联系人的读取和写入。

而为了表示属性和方法是在实例化中使用的，你还需要对它增加 self 关键字，即：使用实例的属性时，要用“self. 属性”的写法。使用方法时，要是将实例的方法第一个参数设置为 self，代码为“方法 (self)”。

类能够在封装和调用上提供比函数更灵活的方式，因此你会发现当功能复杂，代码数量增多了以后，很多软件都采用了类方式实现代码的设计。

类中的特殊方法“init”

在类中，有一个内置的方法叫做“init”, 它叫做类的初始化方法，能实现类在执行的时候接收参数，还能为类预先执行变量赋值、初始化等，实现在类一运行就需要完成的工作。

“init()”方法的作用有两个，分别是：

为实例接收参数；

实例化时立即运行该方法中的代码。

当一个类实例化时，它可以像函数调用一样，接收参数。类实例化时，它的后面需要增加括号“()”，括号中可以指定实例化的参数。这个参数将交给“init()”方法，作为“init()”方法的参数，进行使用。

我来为你举个例子，来说明类是如何实现接收参数的。例如我在通讯录的例子中，实例化一个 SQLite 的“OptSqlite”类，实例化的代码如下：

my_query = OptSqlite("contents.db")

这段代码中的“OptSqlite”就是类的名称，而“contents.db”是该类初始化时，输入的参数，也是 SQLite 数据库文件的名称。

要想实现“my_query”实例在“OptSqlite”类实例化时获得参数，就需要在类中使用初始化方法：

def __init__(self, dbname = "new.db"):

在这段代码中，我定义了“init()”方法，并指定它的参数“dbname”之后，那么实例“my_query”就能够得到参数 dbname 变量的值“contents.db”了。

这就是一个实例化一个类，并如何在第一时间获得参数的完整过程。不过获得参数之后，你还要对参数继续使用和处理，以及需要在实例化之后就立即运行一些代码，这些功能就可以写在“init()”方法中来实现。

例如我就将数据库文件的路径处理、初始化连接、初始化游标的代码写入到了初始化函数。代码如下：

class OptSqlite(object):
    def __init__(self, dbname = "new.db"):
        """
        :param dbname  数据库名称
        """
        self.dir = pathlib.PurePath(__file__).parent
        self.db = pathlib.PurePath(self.dir, dbname)
        self.conn = sqlite3.connect(self.db)
        self.cur = self.conn.cursor()

通过上面的写法，实例不但能够接受参数，还能在初始化时做很多主要逻辑前的预备操作。这些初始化操作让实例被调用时的主要逻辑更加清晰。

为了能够让你对类有更深刻的理解，也为了能让你将数据库的代码直接拿来在工作中使用，我们在对数据库的写入和读取基础上，再增加修改和删除功能，这样，SQLite 的类就能完整实现数据库的增删改查功能了。

使用类实现完整的 SQLite 增删改查

SQLite 的增删改查，都需要依赖 SQL 语句完成，在编写代码前，我们先来学习一些更新和删除的 SQL，在掌握增删改查 SQL 基础上，你会更好地理解我编写操作 SQLite 类的代码逻辑。

更新和删除记录的 SQL 语句

首先，我先来带你学习一些更新的 SQL 语句。更新一般是对单个记录进行操作，因此更新的 SQL 语句会带有筛选条件的关键字“WHERE”。以更新“Tom”手机号码的 SQL 语句为例，我将更新需要用到的 SQL 语句，单独写出来供你参考：

UPDATE address_book SET phone=12300001111 WHERE id=1;

在这条 SQL 语句中：

“UPDATE”是指即将更新的数据表。

“WHERE”是指更新的条件，由于“id”的主键约束条件限制，它的值在这张表中是唯一的，因此通过“WHERE id=1”会读取该表的“id”字段，得到唯一的一条记录。

“SET”用于指定记录中的“phone”字段将被更新的具体值。

这就是更新语句的各关键字的作用，那我们再来看看删除操作的 SQL 语句。例如我希望删除通讯录中的“Jerry”用户，就可以使用如下的 SQL 语句。

DELETE FROM address_book WHERE id=1;

在这条 SQL 语句中，“DELETE FROM”用于指定表，“WHERE”用于指定过滤条件。

我想你肯定还发现了，无论更新还是删除操作中，都包含了“WHERE”关键字。使用了“WHERE”关键字，也就意味这“UPDATE 和 DELETE”也读取了数据库。因此，我们将插入和删除也称作是“使用 SQL 语句对数据库执行了一次查询”。当你为以后工作中编写复杂的“UPDATE 和 DELETE”语句时，如果遇到它们的性能达不到你预期的要求，可以从“查询”方面先对你的 SQL 语句进行优化。

在你对 SQL 语句不熟练的时候，我有一个建议提供给你，由于 UPDATE 和 DELETE 语句在没有指定条件时，会将整张表都进行更新和删除，所以我建议你在编写代码时，先通过 SELECT 得到要操作的数据，再将 SELECT 改写为 UPDATE 或 DELETE 语句，避免因手动操作失误导致数据发生丢失。

接下来我们就把修改和删除功能也加入到“OptSqlite”类中，实现对数据库的增删改查操作。

实现增删改查的类

实现了增删改查的“OptSqlite”类代码如下：

import sqlite3
import pathlib

class OptSqlite(object):
    def __init__(self, dbname = "new.db"):
        """
        :param dbname  数据库名称
        """
        self.dir = pathlib.PurePath(__file__).parent
        self.db = pathlib.PurePath(self.dir, dbname)
        self.conn = sqlite3.connect(self.db)
        self.cur = self.conn.cursor()

    def close(self):
        """
        关闭连接
        """
        self.cur.close()
        self.conn.close()

    def new_table(self, table_name):
        """
        新建联系人表
        """

        sql = f'''CREATE TABLE {table_name}(
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT NOT NULL,
            phone INT NOT NULL
            )'''

        try:
            self.cur.execute(sql)
            print("创建表成功")
        except Exception as e:
            print("创建表失败")
            print(f"失败原因是：{e}")

    def get_one_phone(self, username):
        """
        获取一个联系人的电话
        """

        self.get_user_phone_sql = f"""
            SELECT phone FROM address_book WHERE name = "{username}" """
        try:
            self.result = self.cur.execute(self.get_user_phone_sql)
            return self.result.fetchone()
        except Exception as e:
            print(f"失败原因是：{e}")

    def get_all_contents(self):
        """
        取得所有的联系人
        """
        try:
            self.result = self.cur.execute("SELECT * FROM address_book")
            return self.result.fetchall()
        except Exception as e:
            print(f"失败原因是：{e}")

    def set_one_phone(self, name, phone):
        """
        增加或修改一个联系人的电话
        """
        if self.get_one_phone(name):
            self.set_user_phone_sql = '''UPDATE address_book
            SET phone= ? WHERE name=?'''
            self.v =  (int(phone), str(name))
        else:
            self.set_user_phone_sql = '''INSERT INTO address_book
            VALUES (?, ?, ?)'''
            self.v =  (None, str(name), int(phone))
        try:
            self.cur.execute(self.set_user_phone_sql, self.v)
            self.conn.commit()
        except Exception as e:
            print(f"失败原因是：{e}")

    def delete_one_content(self, name):
        """
        删除一个联系人的电话
        """
        self.delete_user_sql = f'''DELETE FROM address_book
                WHERE name="{name}"'''

        try:
            self.cur.execute(self.delete_user_sql)
            self.conn.commit()
        except Exception as e:
            print(f"删除失败原因是：{e}")

if __name__ == "__main__":

    # 实例化
    my_query = OptSqlite("contents.db")

    # 创建一张表
    # my_query.new_table("address_book")

    # 增加或修改一个联系人的电话
    my_query.set_one_phone("Jerry","12344445556")

    # 查询一个联系人的电话
    phone = my_query.get_one_phone("Jerry")
    print(phone)

    # 查询所有人的电话
    contents = my_query.get_all_contents()
    print(contents)

    # 删除一个联系人
    my_query.delete_one_content("Jerry")

    contents = my_query.get_all_contents()
    print(contents)

    # 关闭连接
    my_query.close()

在这段代码中，实现的主要逻辑，是将代码的相似功能尽量封装成一个方法，将数据库初始化连接放在“init()”方法，并尽量复用这个连接。为此，我编写类“OptSqlite”实现通讯录操作的时候，使用了四个方法，我按照这四个方法在代码里的定义顺序依次为你分析一下。

第一个方法是创建通讯录的数据表。我把创建通讯录数据表的功能定义成类的一个方法。定义类的方法我刚才已经教过你了，它是借用函数的语法格式来定义的。

不过我在定义通讯录表的时候，还对 id 这个主键增加了一个新的修饰条件，叫做自增“AUTOINCREMENT”，它的用途是每插入一条记录，它的值就会自动 +1。“SQL92 标准”中规定自增只能修饰整数类型的主键，所以我把 id 的类型改为“INTEGER” ，否则在创建表时，SQLite 会提示类型不符合要求而报错。

第二个方法是查看通讯录所有的联系人。这和我们学习过的查看单个联系人时，使用的“SELECT 某个字段”在 SQL 语句是有区别的。当你需要匹配所有字段时，不用把所有字段逐一写在“SELECT”SQL 语句后面，你可以使用“*”来代替所有的字段，这样实现起来更便捷。

此外，在查询结果上面，由于 fetchone() 函数只返回多个结果中的第一条，因此我把它改为 fetchall() 函数，这样就能把查询到的所有联系人都显示出来。

而且 Python 比较友好的一点是，它会把整个通讯录显示为一个列表，每个联系人显示为元组，联系人的各种属性都放在相同的元组中，方便你能对取出来的数据再次处理。它的执行结果是：

[(1, 'Tom', 12344445555)， (2, 'Jerry', 12344445556)]

第三个方法是更新用户手机号码，由于更新操作的 UPDATE 语句和新增操作 INSERT 语句，对通讯录这一场景，实现起来非常相似。因此我没为它们两个功能编写两个方法，而是都放在了同一个方法 --“set_one_phone()”方法中了。

这样做的好处是，使用“set_one_phone()”方法的人不用区分联系人是否存在，如果用户不存在，则通过条件判断语句，使用“INSERT”语句新建一个联系人。如果联系人存在，则改用“UPDATE”语句更新联系人的手机号码。

第四个方法是删除某个联系人，使用的是“DELETE”SQL 语句。由于这里的 SQL 语句拼接比较简单，我没有单独使用一个变量 v 来保存，而是使用了 f-string 字符串把变量直接替换到字符串中，拼接为一个 SQL 语句。

对于以后工作中遇到的简单的字符串替换，你也可以采用这种方式，会对代码阅读上带来比较流畅的阅读体验。

通过这四个方法，我实现了“OptSqlite”类的增删改查功能。实例化“OptSqlite”类之后，你只需了解每个方法的名称和参数，就能利用我编写的四个方法实现通讯录的完整操作。这也是采用类替代了函数实现更完善的封装，最大的优势。

小结

最后，我来为你总结一下本讲的主要内容。我们在这节课第一次编写了基于类的代码。通过对比类和函数的差别，我们了解到类的编写方法。这些差别体现在如何定义类、类中的成员属性和方法、以及一个用于接收参数、在实例化类时完成初始化的特殊方法“init()”。当你接触更多的其他人编写的 Python 代码时，就会慢慢发现代码量较大的程序，都会采用基于类的方式封装代码。也希望你在掌握类之后能够通过读懂其他人的代码，对自己的编码能力进行提升。

此外，我还用类重新封装了基于 SQLit 的通讯录的基本功能，其中就包括增删改查。相信你在掌握了对数据库的封装之后，可以把原有需要用 SQL 与数据库打交道的接口，封装为类的方法，这样也有助于你能够把 SQLite 更多的应用于自己的办公优化中来。

思考题

按照惯例，最后我来为你留一道思考题，在本讲的代码中，我使用“INSERT”增加联系人之前没有判断该联系人是否存在。你能否利用判断语句实现增加联系人前对联系人是否存在进行判断，并提示用户对重复联系人进行合并操作呢？

欢迎把你的思考和想法放在留言区，我们一起交流讨论。如果这节课学习的数据透视表对你的工作有帮助，也欢迎你把课程推荐给你的朋友或同事，一起做职场中的效率人。

23｜怎么用数据透视表更直观地展示汇报成果？

你好， 我是尹会生。

从这节课开始，我们将进入课程的最后一个模块“输出模块”的学习了。在这一模块中，我们要把“控制、存储等模块”的数据，以更加智能化的方式展现到你的面前。比如用图表、图形代替数字进行工作成果汇报；利用压缩包、PDF 把工作成果定时发送邮件等。这些都是你在输出模块能够学习到的自动化办公技巧。

通过对这些技巧的掌握，能够为你的工作成果输出带来更高效的处理方式，也能让你把工作成果输出这件事做得更出色。

那今天这节课，我想和你聊一聊怎样把你的工作成果更好地向领导汇报的问题。

你在工作中肯定遇到过这样的情况：精心处理了好几天的数据，需要把数据汇报给你的领导时，往往不知道采用什么样的格式更合适。比如只提交了整理过的原始数据，很容易被领导误认为工作只完成了半成品。而提交了最终结果时，领导又可能会提出合理的疑问，比如如果按月汇总、按某项汇总会得到哪些不同的结果等问题。这个时候，你就不得不再进行一次统计结果汇报。

那么今天这节课，我就要给你介绍一款 Excel 中的插件，叫做“Power Pivot”，这个插件可以根据需要随时调整数据处理结果。而你可以利用“Power Pivot”生成自己需要的数据模型，还能基于模型生成更灵活的、可随时调整的数据透视表。

等下次再遇到需要随时调整数据汇总结果的时候，你就可以直接拖拽想要的列，按月、按年、或按照任意你想要的字段进行数据聚合了。

不过在讲解之前，考虑到你可能初次使用数据透视表，我想先为你介绍一下如何将数据转换为数据透视表的操作步骤，这样你才能更好的理解数据透视表展示的表格和数据之间的对应关系。在掌握了数据透视表的操作基础之上，我再教你怎样使用“Power Pivot”插件来生成支持多表和自定义排序的数据透视表。

利用数据透视表进行灵活的数据分析

数据透视表能用来做什么呢？一句话来描述它，数据透视表是一种能够随意对数据明细表进行分类汇总，并能随时调整汇总结果的交互式报表。

可能这样描述它，你仍然觉得不够直观，还不能对数据透视表的作用有具体的感知。那么我就用一个 Excel 文件，来给你演示一下怎么把数据表生成数据透视表，并进行数据的统计和分析。

为了让你对数据透视表有个直观的印象，我先把原始的数据表和制作成透视表的表格一起提供给你，原始的数据表记录了 5 台计算机硬件负载情况的明细数据，透视表按照每台计算机每种硬件在每小时的最大利用率进行展示。两张表格内容如下：

同时，我也把生成数据透视表后的截图，给你放在下方，供你参考：

数据表中包含了 1 点 -4 点，一共 5 台计算机的 CPU、内存、磁盘利用率，如果把数据交给你，你能从中分析出哪些有趣的结果呢？

那么我在这里用数据透视表提供两种将数据组合起来有意义的结果，它们是每台电脑每小时各种资源利用率的最大值，以及每台电脑每天资源利用率的平均值。

当然你还能用数据组合出更多有用且有趣的结果。那接下来我们就看看怎样把明细数据转换为数据透视表、怎样组合行和列，才能得到我刚才提到的两个有意义的结果。

使用数据透视表实现数据统计

使用 Excel 的数据透视表来实现数据统计，有三个简单的步骤，分别是数据预处理、生成透视表和调整透视表结果。让我们逐个来学习一下它们吧！

首先是数据预处理，它是输出数据的基础。数据预处理的目标就是把每一类数据放在单独的一列，因为数据透视表只能支持这种格式的数据，这样才能输出有意义的透视结果。另外，你可以使用第 13 讲的“Power Query”对表格进行调整，也可以使用咱们学习过的 Python 对 Excel 的数据内容进行调整。

我给你举一个调整好格式的例子，帮你理解数据透视表能支持的数据明细的格式。比如刚才的计算机利用率的表格，假设“pc-1”这台计算机的利用率明细表中包含“CPU”、“磁盘”和“内存”的利用率数据，如果你需要使用数据透视表，则必须为它建立“类型和利用率”两个列，并把“类型和利用率”作为表头。

与此同时，在类型列中把“CPU、内存、磁盘”作为数据，在利用率中分别写入每种类型不同时间各自对应的利用率，而不能把“CPU、内存、磁盘”作为三列，把利用率作为每一列的数据。

如果你还是无法理解需要把数据处理成什么样的格式，才能让数据透视表正常工作，那我再给你提供一个既简便又不会出错的方法：把第一列指定为时间，而其他的列，每一列存放一个类型的数据。把需要透视的数据，按照时间递增依次填入到 Excel 中。

这种数据有一个专有的名称，叫做时序数据。数据预处理完成后，如果你的透视表不需要时间字段，可以将时间这一列的数据再删掉。

接下来是生成透视表。数据准备好之后，下一步就是在新的工作表中插入数据透视表了。插入数据透视表的命令在“插入”菜单栏的“表格”命令组，你只要找到“数据透视表”按钮，点击后就会打开创建数据透视表的菜单了。

在这一步骤，有两个选项你需要格外关注一下。

一个是“选择要分析的数据”选项，Excel 会自动为你选择当前表中的所有数据，如果数据表中的数据都是你要分析的内容，那你可以不做修改。如果你只希望将表格部分数据制作成数据透视表，可以根据自己的需要选取数据明细表中的具体数据范围。

另一个是“请选择放置数据透视表的位置”选项，我们可以把透视表放在“新工作表”中。可以保持原始数据不被破坏外，便于你再次利用原始数据做其他的数据处理。

这两个选项一般保持默认即可。同时，我也把创建透视表的截图放在下方，供你参考。

最后是调整透视表结果。创建好默认的数据透视表之后，你会发现它的内容是空的，这时只需要把“报表的字段”拖动到透视表区域就行了。同样的，我先给你看一下截图，帮你更直观地感知数据透视表的操作。

在截图的左侧是透视表的结果，而右侧就是透视表的控制区域，你需要根据自己的分析需求，把字段拖入到右下角的“透视表区域”，包括“行、列、值”三个方框。例如我希望得到每台电脑的 CPU、内存、磁盘在每小时和每天最大利用率，就可以把“主机、类型”字段拖入“列”，把“时间”字段拖入“行”，把“利用率”字段拖入“值”。

此外，你还要注意，“值”中的字段默认计算方式是“求和”，如果你像我一样，在这里需要最大值，就可以用鼠标点击“值字段设置”，把“求和”改为最大值即可。当然，你也可以根据自己的需求，设置为其他的。

这就是利用数据透视表进行数据统计的完整做法。你看，是不是很简单。总结来说，你可以利用数据透视表自由组合数据，得到你想要的统计结果，它的结果也会实时显示在左侧的 Excel 表中。而且它的最大优势就是你可以随时调整结果，当你需要结果能够行列互换时，直接拖动“透视表区域”的字段就能实现新的数据透视表了。它能解决你工作中大部分常用的数据分析和展示场景。

不过当你遇到数据在多张数据表中，或者数据量更大（超过百万行）时，你就需要使用“Power Pivot”插件来扩展数据透视表，从而实现多表的数据统计了。

使用 Power Pivot 插件扩展数据透视表，实现多表统计

数据透视表的英文叫做“Pivot Table”，把它和“Power Pivot”的名字进行对比，相信你就看得出来，“Power Pivot”插件是数据透视表的增强版本。为了方便你的阅读，在接下来的讲述中，我把它简写为“PP”。

“PP”插件主要扩展了数据透视表在处理数据的维度、数据的容量限制和自定义汇总的公式这三方面的功能。在一般的办公场景下，默认汇总的公式是能够满足日常需求的，因此我要重点给你讲一下“PP”在维度和容量方面能够支持哪些特性。

在维度上，它在单张数据表的维度上增加了数据表维度，即：能支持多张表格进行数据透视。

容量上，从默认的 100 万行数据透视表的默认大小，扩展到能够支持上亿行的数据。

那么接下来，我就带你看看怎样使用“PP”来加载多张表的数据，来实现扩展的数据透视功能。

使用 PowerPivot 加载多张数据表

加载多张表需要使用“PP”插件，因此我们需要在生成数据透视表之前，先打开“PP”插件的窗口，再把多张表加载到“PP”插件中，最后再生成由“PP”插件制作的数据透视表。

首先，我们先看看怎么打开“PP”插件。“PP”在 Office2009 以后，已经成为 Excel 的默认插件，在 Office 最新的 Office 365 版本中，更是作为独立的菜单栏使用，因此你可以在菜单栏找到“PP”插件的选项卡。

为了演示“PP”对多张数据表的操作，我把当前的数据表再复制一份出来，形成两张一样的数据表，这样就可以测试它的多表加载功能了。

“PP”加载多张表的方式是使用“添加到数据模型”按钮。点击按钮后会弹出“创建表”窗口。这个窗口可以选择两张表中的其中一张，先作为数据模型。如图所示：

图片中“创建表”窗口会默认把当前表全部选中，点击“确定”后，会弹出“PP”的工作窗口。在这一窗口下，你可以对数据进行筛选和处理。

接下来我再导入另一个工作表。导入更多表的操作方法和导入当前工作表是相同的。这样，在“PP”工作窗口的左下角，就会出现两张要进行数据透视的表格。需要说明的是，“PP”还能支持从其他数据源导入数据，例如可以从数据库或“Access”导入数据，和 Excel 中的数据一起进行数据统计。

最后，生成由“PP”制作的数据透视表。你在“PP”窗口的任意一张表点击“数据透视表”按钮之后，就可以创建新的数据透视表了。这时你会发现，数据透视表字段的右上方出现了两张表，你可以把这两张表按照需求拖入到右下方的行、列和值，从而实现多表的数据统计。我把截图放在下方供你参考。

通过使用“PP”，你可以把多个数据源作为数据透视表的源数据，进行数据的混合输出。而且它没有 Excel 默认的只能处理 10 万条数据的限制，能够让你在工作中对较大的数据模型进行处理。

以上就是如何使用“PP”扩展默认的数据透视表，实现多表数据透视以及更大容量的数据透视表插件。

另外，我也为你推荐一款 Python 中能够实现数据透视表的库，叫做“Pandas”（也叫“潘大师”），它也有着和 Excel 数据透视表功能相同、原理相同的函数“pivot_table()”函数。但是“pivot_table()”函数是通过参数“index、column、values、aggfunc”四个参数来指定行、列、数据和数据处理函数，所以你只能看到结果，无法直观看到中间过程。

Pandas 更适用于你已经掌握数据透视表的原理，并根据参数能在头脑中形成数据透视表的基本样式后，通过编程实现数据透视表，因为 Pandas 比 Excel 更复杂，因此 Pandas 更适用于长期且多次将数据明细处理为数据透视表的场景使用，Excel 更适合数据短期使用，形成的透视表也无法以同样的模式应用到另一份数据的场景。相较于办公自动化的场景，Excel 比 Pandas 更适用于数据的快速、灵活输出工作。

小结

最后，让我来为你总结一下今天的主要内容。今天这一讲我主要围绕着怎么灵活输出统计结果的问题，为你讲解了 Excel 中的数据透视表，以及它的增强插件“Power Pivot”。

通过数据透视表展示你的工作结果，既能实时修改，又能实时展示。虽然它操作简单，但是功能还是非常强大的。像是你对工作中，自动化处理的大量数据结果，如果采用邮件的附件形式发送给领导和同事的话，那么数据透视表就是直观的。而且，当面对工作需求变化快的情况，使用数据透视表还能够帮你快速改变数据统计逻辑，快速响应新的工作统计需求。

思考题

最后，我再来为你留一道思考题，如果你需要使用数据透视表分析一个学校中的 10 个班级的学生平均成绩，每个班级学生的成绩包括语文、数学、外语。你要怎么设计 Excel 表格，才能通过数据透视表进行数据分析呢？

欢迎把你的思考和想法放在留言区，我们一起交流讨论。如果这节课学习的数据透视表对你的工作有帮助，也欢迎你把课程推荐给你的朋友或同事，一起做职场中的效率人。

24｜条形、饼状、柱状图最适合用在什么场景下？

你好，我是尹会生。

提起图表，你一定会想到 Excel 和 PPT 中的条形图、饼状图、柱状图，这在我们进行工作汇报的时候会经常用到，是我们最经常打交道的图表了。除此之外，还有很多其他种类的图表，比如折线图、热力图等等。

但是，不管你通过哪一种图表，它们都是为了让你能够更直观、更简洁地表达自己的想法，也能让我们更好地从一堆杂乱无章的数字中找出规律。

虽然图表比直接展示数据多了这么多优势，但是也存在一个问题，那就是使用 Excel 制作一张精美的图表，需要消耗大量的时间。而且这些精美的图表，如果因为临时需要再加载新的数据，又要重复花费时间来制作。别担心，这些问题都可以通过 Python 中的 seaborn 库来解决。

所以在今天这节课当中，我就来教你怎么使用 seaborn 库实现图表的重复生成，并根据不同的场景使用不同类型的图表。

生成统一风格的图表

在 Python 的图表库中，最著名的库叫做 matplotlib，它的语法简单，而且支持的图表类型丰富，是数据分析场景中经常用到的图表工具。

但是如果你直接把它应用到办公自动化场景中，虽然 matplotlib 的功能是强大的，不过美观程度相对就比较差了。因此，我今天就带你学习一个基于 matplotlib 库，并且在外观上进行了优化的扩展库，叫做 seaborn，它能弥补 matplotlib 在外观上的不足。

那么接下来，我就以为鸢尾花分类为例，为你讲解一下 seaborn 库的安装，以及绘图的基本流程。

鸢尾花分类是深度学习用于自动分类的经典问题。我们使用它的数据集是因为它的数据量适中，而且包含了必备的花萼和花瓣的长宽数据，以及长宽数据对应的三个品种的鸢尾花。既能通过 seaborn 观察到分类结果，又能将用于绘图的代码应用到自己的工作场景中。

用 seaborn 生成图表的基本流程

seaborn 库的安装非常简单，由于它的安装包和软件同名，所以使用 pip 命令安装即可。安装之后，就可以使用它来生成图表了。你可以按照导入库、设置图表样式、绘制图形三个步骤来实现图表绘制功能。我们来依次学习一下。

首先是导入库。在这一步骤中，你需要格外注意导入 seaborn 库的名称，以及导入的方法。

由于 seaborn 的功能是基于 matplotlib 实现的图表基本绘制功能，所以这两个库必须都要导入，否则就没法生成图表。

在导入的方法上，我发现导入库的名字很长，这就意味这你在调用库的时候也需要输入比较长的字符。因此我在导入的时候增加了一个“as”关键字，它可以将库的名称简写为更简单的“别名”，以此来简化代码的编写。你需要注意，别名要尽可能有意义，而且不要和保留字或当前代码中的变量重复，以免引发运行时的报错。

在导入库的代码中，我为名字比较长的两个库分别起了新的名字叫“sns”和“plt”，那么当前代码就可以利用“sns.XXX”和“plt.XXX”的方式导入这两个库的代码了，这样会比使用原始的名字更精简。

我把导入库的代码写在下方，供你参考。

import seaborn as sns
import matplotlib.pyplot as plt

接下来是设置图表样式。图表样式是由工作场景确定的，包括背景样式和图表的类型。同时在这一步骤，你还要为图表加载数据。所以设置图表样式是 seaborn 库绘图最关键的部分。

图表样式由背景风格和图表类型两部分组成。

我们先来说风格。风格其实包含了背景色、字体、字形等样式，它们是图表展示时最底层的样子。而这些风格样式通通由 seaborn.set() 函数的参数控制，所以一旦其中任意一个参数发生了变化，那绘图的效果也会发生变化。

不过由于设置风格的参数比较多，自由组合并展示到用户面前的话，界面不一定会美观，这也会违背我们使用 seaborn 生成图表的初衷，因此我们需要经过精心调整样式的搭配。那怎么来搭配它们最合理呢？

其实你不必纠结，去逐一尝试每个参数，因为在 seaborn 的 set() 函数的参数中提供了五种默认风格，这五种风格不说满足特别高的审美要求，但是在一般绘图中，它的美观程度还是可圈可点的。五种默认风格分别是：“darkgrid”“whitegrid”“dark”“white”“ticks”，它们分别代表了“暗黑网格”“白色网格””暗黑无网格”“白色无网格””空白背景”，这五种风格可以通过 set() 函数的 style 关键字参数来指定。

如果你想在这五种默认风格上继续修改，就可以在指定风格后，通过 set() 函数的其他参数继续进行修改。我把 set() 函数的参数提供给你，方便你在工作中找到适合你的图表风格。如下：

seaborn.set(context='notebook', style=''darkgrid', palette=''deep'', font='sans-serif', font_scale=1, color_codes=True, rc=None)

我们再来看图表类型。图表类型是由工作场景来决定的。例如：我想根据花瓣的长宽度，以及花萼的长宽度数据，通过图表来区分三种花的类型。因此我需要一种图形来表示花瓣和花萼的长短分布情况。

显然，我们是希望能根据花瓣和花萼的长短宽窄来得到分布情况，而不是观察变化趋势，因此我会采用散点图，而不是折线图来表达花的数据。那么散点图的绘制，是通过函数 seaborn.pairplot() 来实现图表类型的设定的。

seaborn.pairplot() 函数不但能够指定图表类型，还能为图表加载数据和设置图表的类型、点样式。主要包括三部分，分别是加载的数据源、指定图表类型以及该类型需要绘制的点的样式。

第一部分，加载的数据源，数据源可以由二维元组组成类似 Excel 一样的多行多列的数据，数据中的第一行和第一列会作为标题，被 seaborn 自动处理。我在代码中使用了示例数据“鸢尾花分类”来为你展示数据的加载。

它的示例数据是通过 seaborn.load_dataset() 函数导入的，这个函数会自动访问 GitHub 下载数据。如果你无法访问 GitHub，我就再为你提供一个示例数据的镜像站。除了鸢尾花数据外，镜像站这里还包括房产价格预测等经典示例数据，你可以利用它们来学习不同的图表。我把示例数据的地址放在这里，另外，我把部分示例数据也贴在下方，供你参考：

   sepal_length  sepal_width  petal_length  petal_width    species
0             5.1          3.5           1.4          0.2     setosa
1             4.9          3.0           1.4          0.2     setosa
2             4.7          3.2           1.3          0.2     setosa
3             4.6          3.1           1.5          0.2     setosa
4             5.0          3.6           1.4          0.2     setosa
..            ...          ...           ...          ...        ...
145           6.7          3.0           5.2          2.3  virginica
146           6.3          2.5           5.0          1.9  virginica
147           6.5          3.0           5.2          2.0  virginica
148           6.2          3.4           5.4          2.3  virginica
149           5.9          3.0           5.1          1.8  virginica

我再来为你解释一下示例数据。它是由五列组成的，分别表示鸢尾花的花萼长度、宽度，鸢尾花的花瓣长度、宽度（你可以通过百度来搜索鸢尾花的图片，来了解什么是花萼的长宽，什么是花瓣的长宽），以及三种鸢尾花品种（setosa 山鸢尾，versicolor 杂色鸢尾，virginica 维吉尼亚鸢尾）。我通过散点图的方式采用不同维度展示花的特性，让你能根据颜色把三种花区分出来。

第二部分是指定图表类型，它是由“kind = 'scatter'”参数指定的。因为 pairplot() 函数支持散点图和回归图 (kind='reg')，我们需要关注分布情况，所以使用了散点图的方式来展示数据。

第三部分是点的样式。绘制的散点图中的每个点，也可以单独设置它们的样式。例如我指定了每个点的大小“height=2”，以及指定了色彩样式“palette='husl'”，并为每个列指定不同的颜色“hue = 'species'”。

以上是如何设置图表的样式的核心代码，为了让你更好地理解设置的参数，我将这一步骤的代码一并写在下方，供你参考。

# 设置背景
sns.set(style="darkgrid", color_codes=True)
# 使用示例数据
iris = sns.load_dataset('iris',data_home='seaborn-data',cache=True)
# 加载数据，使用散点图，设置点的颜色和样式
sns.pairplot(iris,
kind = 'scatter', #散点图
diag_kind = 'hist', #直方图
hue = 'species', #按照某一字段进行分类
palette = 'husl', #设置调色板
markers = ['o', 's', 'D'], #设置不同系列的点样式
height = 2 #图标大小
)

最后一步是绘制图形，由于 seaborn 基于 matplotlib 实现图形，因此需要使用 plt.show() 函数进行图形的绘制，那么鸢尾花数据的散点图绘制结果如下：

在截图中，基于花的四个属性，我采用了不同的维度进行绘图。同时你会发现，在某一维度下，其中一种颜色和其他颜色有明显的分界，非常容易把其中一种和另外两种花区分开。

通过观察散点图，你会得到这样一个结论，使用合理的图形，能够帮你更好地解释某个晦涩难懂的概念，也能更容易从数据中发现规律。那既然不同的图表能带来不同的价值，接下来，我就来为你讲解一下，如何使用 seaborn 生成其他类型的图表，比如可以通过 histplot() 函数生成柱状图、heatmmap() 生成热力图、kdeplot() 生成核密度图等等。

用 seaborn 生成不同类型的图表

要想使用 seaborn 生成其他类型的图表，你需要学会如何使用官方文档。我以折线图为例，为你讲解一下官方文档的正确用法。

在seaborn 的官方文档地址API 页面下，所有的图表都先按照不同的用途进行了分类，折线图在表示关系的分类中，你可以参考如下截图。

当你需要绘制折线图时，可以点击“lineplot”，进入折线图的函数解释网页。它的网页采用了和 Python 官方文档风格一致的 API 解释方法，即函数定义、一般场景案例和特殊场景案例。

如果你是第一次使用折线图，那你可以按照一般场景案例、函数定义和特殊场景案例的顺序来学习这个函数。如果你对折线图已经有了较多的使用经验，可以从函数定义，按照网页顺序阅读官方文档。为什么要按照这样的方式来学习呢？

在你对某一图表有了初步的使用经验后，会对该图形的样子有一个感性的认识，这时候再通过函数的定义、参数去学习它们，会比通过一般场景案例来学习的效率更高。而且通过学习函数的参数，能够了解哪些技术点会影响图形的展示。

而对于第一次使用某一图表的话，你没法通过图表的名字想象出这类图形的优缺点，因此我会建议你对初次使用的图形，先按照一般场景案例把图形展示出来，有个直观的印象。

不只是 seaborn 的文档，在学习其他库甚至 Python 语言，或其他任何编程语言，都需要通过阅读官方文档来掌握扩展知识。而阅读官方文档最佳的时机，是当你掌握了该软件的基本应用之后，例如在你掌握 seaborn 的散点图，以及它的基本运行过程之后，这时你就需要通过官方文档的学习来掌握更多的图表。当你掌握足够多的图表后，用 seaborn 绘图才能更加得心应手。

为不同的应用场景选择合适的图表

由于 seaborn 支持的图表非常丰富，在有经验的开发工程师进行图表选择时，绝不会逐个尝试。他们会根据图表的应用场景来选择适合的种类，再通过适合的种类再细化到图表的具体样式。

但是你可能并没有使用过 seaborn 的图表，甚至也不了解图表会有多少种类型、每种类型里包含着哪些具体的图表。因此根据是否有图表的使用经验，你可以按照我给你提供的两种方法来根据工作场景，找到最适合你的图表。这两种解决办法总结来说就是参考图例和参考分类。

第一种解决办法是参考图例，我把这种情况称作是“手中有剑、心中无剑”，“手中有剑”代表着你能看到图表一共有哪些，但是心中还不清楚哪种更适合你的场景。在 seaborn 的官方文档中，列举了各种图例，它的地址和截图如下：

你可以根据截图，找到离你的工作场景最相近的图表，通过点击图表之后，你就可以得到官方网站的演示代码了。演示代码就是你的“宝剑”。通过修改演示代码来完善你的工作场景的图表。

第二种解决办法是参考分类，我把这种情况称作“心中有剑，手中无剑”，“心中有剑”也就意味着你在心里已经把应用场景锁定在某一个图表的大类中，但是这一大类里包含了哪些具体的图表，要看 seaborn 能否支持。

这时候，你就要根据你的业务场景，分析出它都对应了以下四个分类中的哪一类，再按照分类通过官方文档 API 页面找到具体的图表函数。四个分类如下。

关系类，用于展示数据集中多个变量之间的关系，relplot()、scatterplot()、lineplot() 都属于关系类。

分布类，用于展示数据集中多个变量的分布情况，displot()、kdeplot() 是这一类经常使用的图表类型。

线性关系类，是把多个变量联系起来，观察每个采样的线性变化趋势。regplot() 和 lmplot() 经常用于表示线性关系。

结构化多图，用于把多种方式的分析数据放在一起进行展示。例如我们分析鸢尾花就使用了散点图 + 柱状图的方式，但是散点图更能体现出它的各种属性之间的关系。

你在心中掌握的图表分类就是“宝剑”，通过分类能够更快找到特定的图表类型。

这两种方式是基于不同场景，快速选择图表的方法。因为选择图表最核心的思路还是要基于场景，而不能基于个人的喜好或结果的美观性来选择图表，避免以偏概全。

小结

最后，我来为你总结一下这一讲的主要内容。在本讲中，我通过 seaborn 生成图表的过程，为你讲解了如何在 Python 中使用图表。相对于其他软件，Python 的图表样式由参数组成，你可以为多次产生图表指定相同的样式、也能为不同的数据重复使用图表来提高绘制图表的效率。

在为你讲解了散点图之外，我还为你讲解了如何基于场景选择合适的图表，你可以基于目前对图表掌握的深度，选择更适合你的图表深入学习路线。

同时，我还着重为你强调了文档的重要性，它也是很多专业从事开发的工程师必需掌握的技能之一。如果你希望更加深入学习 seaborn 以及更加深入学习 Python，你应该从现在开始阅读官方文档，它会是你未来编写代码最权威的参考资料。

思考题

我来为你留一道思考题，如果我的工作场景需要展示当前地区的房价走势，你会选择什么样的图表进行展示呢？你能否用 seaborn 将这一图表绘制出来呢？

欢迎你把思考和想法分享在留言区，我们一起交流讨论。如果今天的内容对你展示工作成果有帮助，也欢迎你把课程分享给你的同事和朋友，我们一起做职场上的效率人。

25｜图表库：想要生成动态图表，用Echarts就够了

你好，我是尹会生。

在上一讲中，我们学习了怎么使用 Seaborn 来生成图片格式的图表。事实上，图片格式的图表也被称作静态图表，它能通过数据来更直观地展示结果。

不过很多时候，我们不仅要通过图片直观地展示数据，还要让图片容纳更多种类、更丰富的数据信息。这个时候，静态图表能展示的结果就十分有限了。比如你希望能给领导和同事在会议上演示数据的分析结果时，需要通过一张图来容纳更多的数据。

别担心，这时候我们可以采用动态图表的方法，来增强图片的表现力。因为动态图表展示的结果，相当于静态图表和数据这两者的混合，所以容纳的内容信息也就更丰富。

举个例子，我希望用一张图片来展示全国新冠确诊病例的分布。如果采用动态图，我就可以把鼠标移动到我需要查看的省份上面，显示该地区的确诊人数等相关信息。

就像下面这张截图一样。这张分布图不但基于颜色深浅显示了确诊人数的变化，还能通过鼠标悬停来显示具体的数据。使用起来是不是很方便？

这张动态图表是使用 HTML 网页文件格式来展示的。同时，它也采用了 Python 的库 pyecharts 进行了绘制，其中的图形、数据都可以基于你的需要进行调整。最重要的是，绘制这样一张图片，操作起来和 seaborn 生成静态图表一样简单。

那接下来，我就为你具体讲解一下怎么使用 pyecharts 来绘制疫情实时地图，并以此为例，让你掌握怎么通过pyecharts 来绘制其他的动态图。

要想使用 pyecharts 绘制动态图，必须要先对它进行安装，再为 Pyecharts 加载数据，最后才能进行绘制动态图。所以我们参考静态图表的学习方法，我来先带你从安装开始学习 pyecharts。

安装：使用 pip 安装 pyecharts

pyecharts 库和它的安装包同名，因此你依旧可以使用 pip 命令进行安装。不过在这一步，我们需要验证 pyecharts 库是否被成功安装。这是非常重要的一步。

因为和之前安装的软件包最大的不同是，pyecharts 库的依赖包非常多。这里我要对依赖包的概念多做些补充。

依赖包是指软件为了支持某些功能，而这一功能刚好有其他的第三方库已经实现了，那么该软件就不必再次编写该功能。但是你在使用 pyecharts 库时，它所依赖的库也必须被安装在你的电脑上面，你才能正常使用它。

这就像你操作 excel 在你的脚本使用了“xlwt”库，而其他人使用你的脚本，必须也在他的电脑上安装这个库是相同的道理。因此当你对 pyecharts 进行安装的时候，会有很多被依赖的安装包一起被安装在你的电脑上。不过不用担心，依赖包并不影响 pyecharts 库的安装。

由于安装 pyecharts 的过程，pip 命令需要同时安装很多依赖包，你大概率还会在安装过程看到“Requirement already satisfied”这样的提示，这个提示是指你安装的库已经被安装在你的计算机中，有可能是你之前使用 pip 命令安装的这些被依赖的包，也有可能是因为其他库的依赖，它们被安装到了你的计算机中，但是这些提示都不影响 pyecharts 库的安装。这些提示也并非安装错误，你可以直接忽略该提示即可。

不过依赖包过多，虽然不影响安装，但是会带来一个主要的问题。那就是在安装的终端界面，会显示很多提示信息，如果提示信息超过一个屏幕的长度，会把安装成功或失败的安装结果覆盖掉，导致你很难确认 pyecharts 是否安装成功，因此我们需要一种可以确认 pyecharts 乃至任何一个第三方库是否被成功安装在当前计算机的方法。

想要验证 pyecharts 库是否被成功安装，可以在命令行执行下面这个命令，来帮助你验证：

SHELL$ pip3 freeze | grep pyecharts
pyecharts==1.9.0

在这条命令中，有三个地方需要你格外注意：

命令中的“freeze”，用于查看 pip 命令在当前计算机安装的所有软件包都有哪些；

“|”叫做管道符，用于连接左右两条命令，并把左边命令的执行结果作为右边命令的输出；

通过 grep 命令，过滤只包含“pyecharts”的一行。如果你不使用 grep 命令，“pip3 freeze”会将当前计算机中所有的第三方库及其版本显示在终端上，如果第三方库非常多，很难手动确认 pyecharts 是否被成功安装了。

这就是查看某一个库是否被成功安装在当前计算机，以及查看被安装版本的命令，我经常使用这一命令来确认依赖关系较多的库是否被成功安装。

不过你肯定会有疑问了，查看库是否安装这一做法是为了避免有些库存在不兼容，导致安装失败。那为什么还要查看安装的版本呢？

主要是考虑到版本兼容问题。我以 pyecharts 举例，pyecharts 是 Python 和 Echarts 的结合体（Echarts 是由百度开源的交互式可视化图表工具，基于 JavaScript 脚本实现）。因此 Python 提供的接口更新和 Echarts 工具更新，都会导致使用 pyecharts 的函数不同。而 pyecharts 分为 v0.5.X 和 v1 两个大版本，且 v0.5.X 和 v1 不兼容，v1 又是一个全新的版本，这两个版本支持的 Python 最低版本也不同。简而言之：

v0.5.X 版本的 pyecharts 能支持 Python2.7、Python3.4 及以上版本；

v1 版本的 pyecharts 能支持 Python3.6 及以上版本。

如果基于公司规定，你必须使用默认的 Python3.4 版本的话，可以使用如下命令安装 0.5 版本的 pyecharts：

pip install pyecharts==0.5

解决完 pyecharts 的版本兼容问题后，相信你的 pyecharts 的正常运行肯定不在话下了，那接下来我就带你学习怎么给 pyecharts 加载数据。

数据：为 pyecharts 加载数据

在给 pyecharts 加载数据前，我们还要确认数据的格式和数据来源。这样做是为了把从网站中得到的数据转换为符合 pyecharts 绘图的数据。

数据格式，用于传入数据前要把来源数据转换成被 pyecharts 支持的格式；

数据来源，决定数据的准确性和详细程度。

例如网站中的数据包含了省、市、区的确诊人数，以及成功被治愈的人数，而我们只需要每个省被确诊的人数。因此数据格式和来源都需要经过你的精心处理，才能被 pyecharts 展示给使用者。

确认数据的格式和来源

pyecharts 的数据格式，要基于不同的图形类型，使用不同的格式。但是一般情况下，是多行多列组成的类似 Excel 表格的格式，这种格式在 Python 中一般使用嵌套元组的形式进行保存。

以绘制疫情地图数据为例，我们需要每个省的名称以及现有确诊人数，那么我们可以把省的名称和人数放在一个元组中，并把多个省的数据再组成一个更大的元组，作为 pyecharts 的源数据。

这种并列数据，你可能第一时间想到的不是元组，而是列表，但是我要告诉你的是，列表的查询效率要远远低于元组。为了让你的图形在展示数据时能够更加流畅，我更建议你使用元组，具体方法是在把列表作为源数据使用前，使用“tuple()”函数把列表类型转换为元组。

确定数据格式之后，我们还需要一个数据来源，为了确保数据的准确性和实时性，我们得从腾讯新闻的网站引入外部数据，数据地址的链接我先贴出来，接下来我再给你讲解一下怎么得到数据地址。

为了得到数据的地址，我分析了网站加载数据的过程，找到了数据接口的地址。这个分析方法你学会以后，也可以应用到其他需要抓取网页数据的工作中。我把抓取的步骤分成四步，分别是开启浏览器调试、请求网页、确认接口和确认返回数据。

先看第一步，开启浏览器调试。这一步骤是为了在请求过程中，能记录网页都请求了哪些数据接口。

以 Chrome 浏览器为例，使用快捷键 F12 可以打开调试模式，把选项卡调整至“Network”，调整后就进入接口的监听状态了。

第二步，请求网页。以腾讯疫情实时网页为例，可以在地址栏输入“https://news.qq.com/zt2020/page/feiyan.htm#/”，输入后，调试界面会显示该网页都请求了哪些地址。我把调试页面放在下方供你参考。

截图的左侧就是请求的所有数据接口的地址，从最开始请求的地址向下找，除了 JS、JPG 等网页图片和样式数据外，其他的请求接口就是我们要重点查看的接口。

第三步，确认接。由于网页中包含了多次从“getOnsInfo”接口取得数据，所以要逐个查看接口的返回数据。你可以通过鼠标点击接口的链接，然后再点击“Response”按钮，最后根据返回接口的返回内容，查看是否为“疫情实时数据”。

第四部，确认返回数据。当你从接口初步确认了该数据是“疫情实时数据”后，可以把该地址复制到浏览器中，进行访问。访问的具体办法就是通过模拟网页请求接口，这样就可以得到以下数据了。我将数据放在截图中，供你参考。

截图中的数据，可以通过查找省份和数据来确认数据的正确性。如果数据不正确，你需要回到第二步，再重新找下一个接口。

调整数据格式

当你确认了数据格式和数据源之后，接下来就需要把数据源的格式转换为 pyecharts 需要的嵌套元组格式。

首先，我先来分析通过网页直接请求数据接口之后的格式，请求后，你会看到网页上面的格式类似于 Python 的多个字典嵌套在一起，这种格式被称作 JSON 格式。在 Python 中你可以通过“json”库去解析这种格式，并把它转换为 Python 中的字典。解析的方法如下：

import requests
import json
url = 'https://view.inews.qq.com/g2/getOnsInfo?name=disease_h5'
data = requests.get(url)
alldata = json.loads(data.json()['data']

在这段代码中，我使用了 requests.get() 方法获取了接口的内容，并使用 json.loads() 方法把接口的数据转换成了字典。由于所有的数据都在下标为“data”的字典中，通过“字典['data']”取得字典值的用法，把所有数据存放到 alldata 变量中。

接下来，要把 alldata 变量中的字典转换为嵌套元组形式，这个转换过程需要遍历字典，来取得省份名称和对应的确诊人数。不过为了保存多个省份，我们还需要使用一个新的列表来存储多个省份的数据。最后再把列表转换为元组，作为 pyecharts 的绘图数据使用。我们依次来看一下：

chinadata = []
for province in alldata['areaTree'][0]['children']:
    provincedata = (
        province['name'],
        province['total']['nowConfirm']
    )
    chinadata.append(provincedata)

在代码的第 2 行，我使用了 for 循环，从 alldata 字典的多层嵌套结构中取出省份和该省份的确诊、累计、新增等人数信息。

在代码的第 3 行，我使用了一个新的元组，provincedata 变量只保存每次遍历时得到的省份和确诊人数。

在代码的第 7 行，我把每次遍历生成的元组增加到 chinadata 列表中。这个列表就是最终处理好的数据内容，可以把 chinadata 数据直接作为 pyecharts 的源数据来进行绘图。

这段代码比较简单，不过我还是有两个小的使用建议提供给你。

第一个是，我在代码的第 3 行定义了一个 provincedata 的元组，从代码的正确执行角度来说，这个变量可以不用定义，完全可以把元组直接写入到 chinadata.append() 函数的参数中。

但是从方便阅读代码的角度来看，这样书写代码不利于理解，因为 append() 函数的参数中包含了较为复杂的类型。因此我建议你在某个函数的参数中，如果传入了多种数据类型时，不妨增加一个临时变量。

另一个小建议是，在数据量较多的时候，应当尽量把列表转换为元组，加快查询效率。例如：我在代码的第 7 行先使用了列表对象用来存储变化的数据，然后通过 for 循环迭代 alldata 变量修改 chinadata 列表。最后，直到 chindata 不再需要改变内容时，我立即使用了 tuple() 把 chinadata 列表强制转换为了元组，那么后续的查询操作就都可以使用元组了。

通过对数据的抓取、分析和处理，源数据的格式和内容就准备完成了。接下来我需要将源数据加载到 pyecharts 中，并指定图形的类型和样式。

绘图：使用 pyecharts 绘制动态图表

当你准备好数据源并处理格式之后，就可以进行绘图了，主要有三个步骤，分别是确定图表类型、加载数据和设置图表样式。那我们先来看看怎么确定图表类型。

和我们学习 seaborn 类似，你可以参考图例，也可以参考分类来学习 pyecharts 支持的动态图表。与 seaborn 不同的是，pyecharts 的官方文档没有图例，不过不要忘了，pyecharts 是基于 Echarts 编写的，因此图例可以参考Echarts 的官方网站。

Echarts 的图表指定函数和 pyecharts 相同，找到你需要的图例函数之后，就可以拿到 pyecharts 中直接使用。

那针对老手的图表分类和 API 可以参考这个地址。以最常用的图表，折线图为例，你可以打开地址，其中会包括图表的完整调用代码、测试数据和图例，通过参考示例可以让你掌握更多类型的图表。折线图的截图如下：

再让我们回到疫情地图的案例中，由于我们需要绘制中国地图，因此直接使用 pyecharts 库的 Map() 类，它是绘制动态地图的类。它的官方网站链接我贴在这里。

通过参考官方网站的案例代码，我们可知在将 Map() 类实例化之后，进行绘图时，调用了 add()、set_global_opts() 和 render() 三个方法，它们分别是增加数据、设置样式和渲染。我们来依次学习一下。

add() 方法用来为图表增加图表名称、加载数据、指定地图的区域，其中加载数据的参数我使用了 tuple() 函数，可以直接把列表转换为元组，这样在查找效率上会比列表更高。

set_global_opts() 方法用来指定图表的样式和格式，这里我做了两个格式的调整操作，它们是：图表的标题和图表的颜色。图表的标题部分，我将“数据最后更新时间”作为图表的标题，是通过字典“alldata['lastUpdateTime']”实现的。图表的颜色是通过 pieces 参数实现的，根据不同省份确诊人数在什么数量范围，显示该省份的颜色深浅。颜色越深，表示当前周期采集到的确诊人数越多。

render() 方法，用来指定图表保存的网页路径和名称。这里需要注意的是，add() 和 set_global_opts() 方法如果出现错误并不会马上报错，只有在调用 render() 方法时才会出现错误。所以一旦出现错误，你应该从导入数据的格式、add() 和 set_global_opts() 方法参数中检查。

执行 render() 方法之后，动态图就会以网页的形式保存至“covid19_map.html”文件中，你可以在浏览器里查看，并通过鼠标移动展示不同省份确诊人数的具体信息。

最后，我把 pyecharts 绘制图形的完整代码贴在下方供你参考，你可以直接将代码复制到自己的计算机执行。

import requests
import json
from pyecharts.charts import Map
from pyecharts import options as opts

url = 'https://view.inews.qq.com/g2/getOnsInfo?name=disease_h5'
data = requests.get(url)
alldata = json.loads(data.json()['data'])

chinadata = []
for province in alldata['areaTree'][0]['children']:
    provincedata = (
        province['name'],
        province['total']['nowConfirm']
    )
    chinadata.append(provincedata)

map_chart = Map()
map_chart.add(
        "全国确诊病例分布图",
        tuple(chinadata),
        "china",
        is_map_symbol_show=False
    )

map_chart.set_global_opts(
    title_opts=opts.TitleOpts(
        title=f"全国疫情地图( {alldata['lastUpdateTime']} )"),
        visualmap_opts=opts.VisualMapOpts(
            is_piecewise=True,
            pieces=[
                {"min": 1, "max": 9, "label": "1-9人", "color": "#FFE6BE"},
                {"min": 10, "max": 99, "label": "10-99人", "color": "#FFB769"},
                {"min": 100, "max": 499, "label": "100-499人", "color": "#FF8F66"},
                {"min": 500, "max": 999, "label": "500-999人", "color": "#ED514E"},
                {"min": 1000, "max": 9999, "label": "1000-9999人", "color": "#CA0D11"},
                {"min": 10000, "max": 100000, "label": "10000人以上", "color": "#A52A2A"}
            ]))

map_chart.render('covid19_map.html')

小结

今天的主要内容就这么多，我来为你最后总结一下本讲的主要内容。

在本讲中，我通过 pyecharts 展示了疫情实时信息的动态图表，并为你介绍了图表的制作方法。对比静态图表，动态图表的制作方法要复杂，但是一张图能容纳的信息也要比静态图表多，你需要根据自己的工作场景合理的选择图表种类。

同时，你还会发现，图表的绘制难点在数据格式的处理上，通过网络采集数据往往要经过从 JSON 格式到字典再到元组的嵌套；图表绘制过程可以重复利于的部分就是图表的创建过程，包括数据添加、样式设置和渲染。掌握这些通用的使用原则，可让让你从熟练的操作中，加快自动办公的效率。

而你掌握的图表越丰富，在进行工作汇报和演示时，能够通过图形表达的信息就越清晰。这是我建议你能够掌握更多图表类型的原因。

思考题

今天的思考题是个开放的问题，在你使用 pyecharts 绘制的图表需要每天更新时，如何自动删除上一个生成的文件？那有没有办法让网页自动更新呢？

欢迎你把思考和想法分享在留言区，我们一起交流讨论。如果今天的内容对你展示工作成果有帮助，也欢迎你把课程分享给你的同事和朋友，我们一起做职场上的效率人。

26｜快速提取图片中的色块，模仿一张大师的照片

你好，我是尹会生。

当你进行工作汇报使用各种图表时，除了要考虑精确的数据、正确的结论外，合理的配色方案也是精确表达数据的重要部分。

正确的配色不但能让用户更容易 get 到你演示产品的商业用途，还能为你的数据增光添彩。而不合理的配色，不但会降低用户对你演示产品的兴趣，还容易产生误解。

因此你要为你的数据搭配上合适的色彩方案，来增加数据的表现力。那么你要不要找专门的美工或自己分析那些商业模版用了什么配色呢？其实这两点都不需要，有很多设计高手和名画大师已经为我们提供了太多经典的作品了，我们只需要使用相似的配色，将这些配色应用到你的演示图片和文稿中，就能实现非常好的配色方案了。

我今天就来教你一种利用 Pillow 库自动分析图片中出现最多的颜色，并自动提取出来的方法。当你再遇到那些为配色发愁的工作场景，就可以找一个你喜欢的商业模版进行模仿了。

在本节课，我打算使用一幅莫奈的名画《日出·印象》来教你，如何使用 Pillow 库提取图片中被用到最多的五种颜色并将它们作为色块标注在图片上。下面的截图就是我们要实现的最终效果，接下来我就教你如何来实现它。

安装 Pillow

在进行图片的大小、格式以及颜色调整时，我们通常选择 Pillow，它是 Python 用于图片处理的第三方库。

那我们就看看怎么安装 Pillow。它的安装包是同名的包，但是作为库导入的时候，需要使用 PIL 关键字，这是它和一般第三方库差异最大的地方。我把它的安装和导入命令写在下方，供你参考。

SHELL$ pip3 install pillow
SHELL$ python3
python3> import PIL  #注意导入的库的名称

安装完成后，我们就可以提取图片中使用最多的五种颜色了。不过别心急，在提取色块之前，有一个步骤非常重要，那就是把图片颜色转化为数据。为什么要进行这一步呢？

这主要是为了方便接下来提取色块的操作。你想啊，我们的目的是提取图片中的颜色，而颜色的“多”和“少”，是需要“数出来”的，所以我们就需要把图片转换为可以量化的数字才行。

那么接下来，我就教你怎么把图片转化为数据，并统计出一张图片都有哪些颜色，以及每种颜色的数量。

把图片颜色转换为数据

要想转换图片颜色为数据，并统计它用了多少种颜色，以及哪种颜色用得较多，你要先理解计算机是如何存储一张图片的。

举个例子你会更好理解。假如说你想知道一个硬盘能够存多少张图片，肯定得先知道磁盘的大小，除此之外还要知道图片的大小。与此同时，为了比较两者的大小，你还需要知道它们的大小单位，进而得知一个硬盘里最多能容纳的图片数量。

那我们用来表示一张图片内颜色数量的单位，是一种称作位（bits）的单位。每一“位”对应着一个像素的颜色值，而这些像素则按照一定的顺序排列就形成了数字图像。

计算机可以使用一“位”或者多“位”来组成一个像素，图像的色彩越丰富，每个像素使用的“位”就越多。为了方便表示“位”的数量，我们经常会使用一个叫做“位深度“的概念，来表示当前图像的颜色丰富程度。像是我们常说的 8 位颜色的图片，那么“位深度”就是 8，而每个像素可以用 2 的 8 次幂来表示，也就是 256 种颜色（专业术语叫做 256 种灰度等级）。

8“位深度”的图像经常用于图片色彩提取和图片预览。不过我们经常见到的图片都是 24“位深度”的，这是因为它能表示 2 的 24 位颜色，可以把红、绿、蓝（RGB）三基色以 2 的 8 次幂表示出来，这些颜色已经超过了人眼能分辨的颜色数量，所以也被称作真彩色，多用于图片的展示。

但是 24“位深度”的图像不适合统计颜色数量，因为 24 位和 8 位图像存储颜色的方式不同，导致前者在计算准确性和计算性能上都没有 8 位图像处理起来方便：

在计算准确性上，24 位图像保存的数据多，所以在进行像素与像素之间的计算中，会产生浮点数，出现计算数据溢出的情况，并因此导致计算的结果异常。

在计算性能上，8 位深度图像和 24 位表示颜色的方式不同。因为在 8 位深度的图像中，有一个“调色板（palette）”的概念，这一概念只在 8 位图像才有，16 位以上就都把图像颜色记录到图片自身的数据中了。所以 8 位图像要想表示一个具体的颜色，需要通过调色板中记录的颜色模版和图片中的模版索引计算之后才能得到，而针对24 位图像，就可以直接把具体的颜色写入到了图片中。

我给你举个例子，例如我把调色板保存在一个列表中，那么由于 8 位图像能保存 2 的 8 次幂位深度，并且每个位深度有 R、G、B 三种颜色，所以调色板中就可以保存在一个包含了 768 个元素的列表里。

当你的图片需要使用 RGB 的某个颜色时，可以在图片文件中只记录该颜色 RGB 值对应的索引，也就是一个整数数字。而你可以直接通过该数字，找到调色板列表上的 R、G、B 三个颜色对应的值，这就相当于用一个整数的空间，存储了三个整数。

这样做既可以节省空间，不用真正把 RGB 颜色存放在图片里，又方便了我们后续来统计该颜色被使用了多少次。

相应的 24 位图像没有调色板，而是把 RGB 颜色数据之间记录到文件中，要想统计哪些颜色使用的较多时，进行排序的工作也要比 8 位图像更加复杂。

这就是一张图片存储颜色数据的单位和不同的存储形式。因此，要想把图片颜色转换为数据，首先要知道当前图片是采用了哪种“位深度”。

如果是 8 位深度，那么图片转化的数字就是该图片在调色板的索引。而你要想得到 8 位图片的 RGB 颜色，就需要使用索引与调色板进行二次计算，从而得到 RGB 颜色。

如果是 24 位深度，那么图片转化为数据的结果就是 RGB 颜色。

由于我们的需求是统计一张图片中，哪些是使用次数最高的颜色，所以我们只需要把一张图片转化为 8 位数据来处理就可以了。那么接下来，我就带你学习一下把图片转化为数据的具体操作步骤，从而提取到色块。

提取色块的四个步骤

根据对 8“位深度”图像存储颜色的原理，我们可以按照这样的步骤来提取色块。

先把 24“位深度”的图像转换为 8“位深度”；

再使用调色板的索引，把 8 位图像按照使用次数的多少进行排序；

之后再把前五个索引对应的调色板的颜色提取出来；

最后把色块与图片整合。

转换为 8 位图像

把 24 位图像转换为 8 位图像，我们可以使用 Pillow 库的 Image 包来实现。在 Image 包中，有一个 convert() 方法，是图像处理经常用到的方法，它可以把图片转换为 24 位、8 位、灰度图以及黑白图片。

在一般的场景中，转换为 8 位图像是为了加快图片的处理速度。我在这里就以《日出·印象》为例，把 24 位图像的图片转换为 8 位图像。代码如下：

from PIL import Image
# pip3 instlal pillow

# 打开图片文件
image = Image.open("./文章26代码/sunrise.jpg")

# 模式P为8位深度图像，每个像素用8个bit表示
image_p = image.convert(
    "P", palette=Image.ADAPTIVE
    )
image_p.show()

在这段代码中，首先，我们通过“from import”的形式把 PIL 库的 Image 包进行导入。由于 Image 包是一个类，所以我们需要先使用 open() 函数打开文件，并实例化为 image。

需要注意的是，这里 Image 类的 open() 方法不但能够以 2 进制打开图片文件，并且打开后，还可以读取图片包含的像素数量、图片的长宽以及位深度。读取之后，我们后续所有对图片的操作就都可以通过 image 实例进行操作了。

接着，在代码的第 8 行，我就对图片进行了位深度转换。代码中的转换为“P”模式，指的就是 8“位深度”的图像，palette 指的则是转换时指定的调色盘。我们可以通过参数“palette”指定调色盘类型，在统计使用颜色的数量这一场景，使用默认调色盘即可。

转换之后，你可以通过 show() 函数直接查看转换后的图片，并且会把操作后的图片原图和对比发现，这张画的主体颜色仍然保持不变，但是会加快接下来的图片处理速度。

对出现最多的五种颜色索引进行排序

接下来，我们就要提取图片中被用到最多的 5 个颜色索引了。在这一步中，我们需要把 8 位图像的颜色索引都提取出来，并对每个索引的使用次数进行统计和排序。

这两项工作我们可以使用 Pillow 库的 getcolors() 方法和 sorted() 内置函数来实现，我先把代码贴出来，然后再来解释。

# 图像中使用的颜色列表,maxcolors默认256
color_counts = sorted(image_p.getcolors(maxcolors=9999), reverse=True)

这行代码中的“color_counts”保存了从多到少排序之后的颜色索引，其中 getcolors() 方法会以嵌套元组的形式显示“image_p”的颜色索引使用的次数，以及颜色的索引值。

每个颜色的次数和值组成一个元组，这些元组被包含在一个更大的元组中。而这个嵌套的元组，刚好可以通过 sorted() 按照从大到小的顺序排序，就得到了图片中哪个色块被使用的区域最多，也就是图片中的出现最多的颜色了。

这里有一点需要你特别注意，由于 getcolors() 方法默认是被拿来处理 8 位图像的，因此它的参数 maxcolors 默认为 256，即只能对索引小于等于 256 的索引进行正确处理。如果颜色索引超过了 256，则 getcolors() 方法会返回“None”，如果你在其他的应用场景中，使用到了 24 位图像操作，需要手动指定 maxcolors 的值为更大的数量。

通过对索引从大到小排序，我们就能知道哪些颜色在图片中被使用得多，哪个颜色在图片中被使用得少。最重要的是，我们可以根据索引在调色板中取得颜色真正的 RGB 值，比如你可以通过 RGB 值来让 PPT 模仿图片的颜色。我把 PPT 中设置 RGB 值的对话框贴在下面，供你参考。

查表取出 RGB 的颜色

图片中的颜色索引和 RGB 值是通过调色板建立对应关系的，这意味着索引不能直接用于 PPT 等图像软件进行颜色设定，必须转换为 RGB 颜色，才能对 PPT 等软件进行颜色设置。所以，接下来我来给你讲一下怎样通过索引取得 RGB 颜色的思路和代码。

调色板记录的颜色是按照“RGBRGBRGB......”的顺序记录的，每三个为一组。当你通过索引取得调色板的 RGB 颜色，就可以使用“下标3”得到“R”，“下标3+1”得到“G”，“下标 *3+2”得到“B”。这样就可以得到该索引对应的 RGB 颜色值了。

在莫奈这幅画中，我将提取图像中被使用最多的前五个索引，并通过调色板提取它们的 RGB 颜色 。代码如下：

# 以列表形式返回图像调色板
palette = image_p.getpalette()

# 通过颜色列表查找到真正的颜色
colors = []
for i in range(5):
    palette_index = color_counts[i][1]
    dominant_color = palette[palette_index * 3 : palette_index * 3 + 3]
    colors.append(tuple(dominant_color))

# 输出颜色
print(colors)
# [(204, 154, 86), (230, 237, 226), (213, 213, 212), (251, 238, 206), (82, 167, 204)]

这段代码的 colors 变量，就是用来得到图片中被使用最多像素的颜色的方法。同时，我再来为你解释一下这段代码，是怎么通过下标得到具体颜色的。

首先，getpalette() 是图像调色板的列表形式，我们可以通过下标取得列表中指定的 RGB 颜色。

接下来，color_counts[i][1]是下标的索引，i 的值从 0 到 4，表示图片中颜色从多到少排序的前五个索引。由于 color_counts 元组包含两个元素，分别是该索引在图片中的次数和索引的值，所以我就使用 color_counts[0][1]，来取得当前图片中哪个像素的颜色索引被使用得最多，以及这个颜色对应的具体的值。

最后，我们通过“palette[索引3 : 索引3+3]”的形式，得到该索引对应的 RGB 值为“(204, 154, 86)”，再把五个索引依次转换为五个 RGB 颜色之后，加入到 colors 列表中。那么 colors 列表的五个元素，就是《日出·印象》这幅画用得最多的五种颜色。

当我把 colors 列表中的五个颜色 RGB 值提取出来后，就可以直接用于颜色设置了。接下来我继续利用 Pillow 把这五个颜色绘制到图片中。

将色块与图片整合

在把取出来的五种颜色绘制到图片中这一步，其实我们要做的是把色块与图片进行整合。我来解释一下这个操作。因为我们取出来的是 RGB 值，这些数字不够直观，那么最好的办法是把数字作为图片，和原画放在一起进行比较，看看提取的颜色是不是原画出现最多的颜色，而利用出现最多的颜色，我们就能模仿大师的配色了

要想把色块和图片整合，需要使用 Pillow 的 paste() 方法，它能在指定的图片位置按照指定的 RGB 颜色与原图片合并。

为了让你更直观地看到这幅画中用得最多的五种颜色，我把它们制作成 100*100 像素的正方形，放在原始图像上，方便你和原画进行比较。代码如下：

for i, val in enumerate(colors):
    image.paste(val,(0+i*120, 0 ,100+i*120, 100))

#  保存并显示图片
image.save("./文章26代码/sunrise2.jpg")
image.show()

这段代码展示了合并后的图像，并把图像保存成“sunrise2.jpg”文件。

在这段代码中，最主要的功能是就是使用“paste()”方法按照颜色和位置进行图像绘制。这幅画使用最多的五个颜色是使用 for 循环迭代，从 colors 变量取出的，为了避免每次与图片合并的正方形色块被覆盖，我把它的输出位置每执行一次向右侧移动 120 个像素。所以我使用了“enumerate(colors)”形式读取 colors 变量的值。在遍历过程中，i 的值是从 0 开始，每执行一次 i 的值 +1,val 的值每次为 colors 迭代的 RGB 颜色元组。

代码最后两行是对新生成的图片进行保持保存，以及在当前运行界面显示图片的操作。通过观察图片，我们发现提取的五个颜色刚好和主色调吻合。

我把合并好的图片和完整代码放在下面，供你参考。

from PIL import Image
# pip3 instlal pillow

# 打开图片文件
image = Image.open("./文章26代码/sunrise.jpg")

# 模式P为8位深度图像
image_p = image.convert(
    "P", palette=Image.ADAPTIVE
    )
# image_p.show()

# 以列表形式返回图像调色板
palette = image_p.getpalette()

# 图像中使用的颜色列表,maxcolors默认256
color_counts = sorted(image_p.getcolors(maxcolors=9999), reverse=True)

# 通过颜色列表查找到真正的颜色
colors = []
for i in range(5):
    palette_index = color_counts[i][1]
    dominant_color = palette[palette_index * 3 : palette_index * 3 + 3]
    colors.append(tuple(dominant_color))

# 输出颜色
print(colors)
# [(204, 154, 86), (230, 237, 226), (213, 213, 212), (251, 238, 206), (82, 167, 204)]
for i, val in enumerate(colors):
    image.paste(val,(0+i*120, 0 ,100+i*120, 100))

#  保存并显示图片
image.save("./文章26代码/sunrise2.jpg")
image.show()

小结

最后，让我来为你总结一下本讲的主要内容。在本讲中，我通过 Pillow 带你学习了如何提取图片中出现最多的像素，并将像素的 RGB 值进行打印的功能。

通过对提取名画的颜色主题功能，你还可以将提取颜色应用于商业模版，或任何你感兴趣的颜色搭配上。利用更讨喜的颜色搭配，来为你的 PPT 增色添彩。

本讲中除了让你掌握如何使用 Python 对图像操作外，更想让你明白，在计算机中对很多非数字化的元素，也是可以进行计算的，当然计算的前提是将它们转化成可量化的数字，像是深度学习领域中，对语音、文字、图像的处理，都是以将它们数字化为前提进行的。因此当你遇到非数字化的对象，需要进行自动化操作时，也可以使用将非数字化转化为数字化的思路去解决办公难题。

思考题

按照惯例，我来为你留一道思考题，如果有多张图片需要提取其中的主要颜色，你怎样自动化实现它们的颜色提取和文件保存操作呢？

欢迎把你的思考和想法分享在留言区，我们一起交流讨论。如果今天的内容对你展示工作成果有帮助，也欢迎你把课程分享给你的同事和朋友，我们一起做职场上的效率人。

27｜zipfile压缩库：如何给数据压缩&加密备份？

你好，我是尹会生。

你在日常工作中，肯定和压缩文件打过交道，它们能把文件夹制作成一个体积更小的压缩文件，不仅方便数据备份，还方便作为邮件附件来传输，或者与他人共享。

但是如果你需要每天都进行数据备份，或者把压缩包作为每天工作的日报发送给领导，你肯定希望它能自动化的压缩。面对这个需求，我们同样可以通过 python 来解决。我们可以用 Python 来自动压缩文件夹，并为压缩包设置密码，保证备份数据的安全。

在 Python 中，要想实现数据的压缩，一般可以采用基于标准库 zipfile 的方式来实现，也可以采用命令行方式来实现。

当我们希望能够用 Python 自动压缩一个无需密码保护的文件夹时，可以通过 zipfile 来实现，它的好处是使用简单，而且不用安装任何的软件包，就能制作出“zip”格式的压缩包。不过 zipfile 没法对压缩文件进行加密，因此当你需要对压缩文件加密时，还需要调用可执行命令。

这两种实现方式就是我们今天要学习的重点了，接下来我们分别看一下这两种方式的具体操作方法。

使用 zipfile 实现无密码压缩

如果我想要把“C:\data\”文件夹压缩为“当前日期.zip”文件，就可以使用目录遍历、按日期自动生成压缩包的文件名、把文件夹写入压缩文件这三个步骤来实现。

目录遍历

我们先来学习怎么实现目录遍历功能。我在第 16 讲已经为你讲解过它的技术细节了，这里我就继续使用 os 库来实现目录的遍历。

由于目录遍历的功能与其他功能之间的调用关系耦合得比较宽松，所以我就把目录遍历功能单独定义成一个 getAllFiles() 函数，并把要遍历的目录作为函数的参数，把该目录下的所有文件以及所在路径作为函数的返回值。

我把 getAllFiles() 函数的代码放在下方，供你参考。

import os

# 遍历目录，得到该目录下所有的子目录和文件
def getAllFiles(dir):
    for root,dirs,files in os.walk(dir):
            for file in files:
                yield os.path.join(root, file)

细心的你一定发现了，在函数 getAllFiles() 的返回语句中，我使用 yield 语句代替了之前学习过的 return 语句返回文件路径和名称。为什么我要使用 yield 语句呢？

原因就在于，一个函数如果使用 yield 语句来返回的话，这个函数则被称作生成器。yield 的返回数据类型以及对类型的访问方式，都和 return 不同。我来为你解释一下 yield 和 return 的具体区别，以及使用 yield 的好处。

首先从返回类型来看，yield 返回的数据类型叫做生成器类型，这一类型的好处是调用 getAllFiles() 一次，函数就会返回一个文件路径和文件名。而 return 返回的是一个列表类型，需要一次性把要备份目录下的所有文件都访问一次，一旦要备份的文件数量非常多，就会导致计算机出现程序不响应的问题。

除了返回类型，还有调用方式也和 return 不同。使用 yield 返回的对象被称作生成器对象，该对象没法像列表一样，一次性获得对象中的所有数据，你必须使用 for 循环迭代访问，才能依次获取数据。

此外，当所有的数据访问完成，还会引发一个“StopIteration”异常，告知当前程序，这个生成器对象的内容已经全部被取出来，那么这个生成器将会在最后一次访问完成被计算机回收，这样 yield 就能够知道对象是否已经全部被读取完。

从 yield 和 return 的行为对比，可以说，yield 返回对象最大的好处是可以逐个处理，而不是一次性处理大量的磁盘读写操作，这样就有效减少了程序因等待磁盘 IO 而出现不响应的情况。这就意味着你不必在调用 getAllFiles() 函数时，因为需要备份的文件过多，而花费较长的时间等待它执行完成。

按日期自动生成压缩包的文件名

接下来我们来学习一下按日期自动生成压缩包的函数genZipfilename()。按日期生成文件名，在定时备份的场景中经常被用到，我们希望每天产生一个新的备份文件，及时保存计算机每天文件的变化。

这就要求今天的备份的文件名称不能和昨天的同名，避免覆盖上次备份的文件。

所以 genZipfilename() 函数就把程序执行的日期作为文件名来进行备份，例如当前的日期是 2021 年 4 月 12 日，那么备份文件会自动以“20210412.zip”作为文件名称。我把代码贴在下方，供你参考。

import datetime

# 以年月日作为zip文件名
def genZipfilename():
    today = datetime.date.today()
    basename = today.strftime('%Y%m%d')
    extname = "zip"
    return f"{basename}.{extname}"

在这段代码中，“datetime.date.today()”函数能够以元组格式取得今天的日期，不过它的返回格式是元组，且年、月、日默认采用了三个元素被存放在元组中，这种格式是没法直接作为文件名来使用的。因此你还需要通过 strftime() 函数把元组里的年、月、日三个元素转换为一个字符串，再把字符串作为文件的名称来使用。

把文件夹写入压缩文件

最后，准备工作都完成之后，你就可以使用 zipfile 库把要备份的目录写入到 zip 文件了。zipfile 库是 Python 的标准库，所以不需要安装软件包，为了让这个完整脚本都不需要安装第三方软件包，我在实现文件遍历的时候同样采用 os 库代替 pathlib 库。

除了不需要安装之外，zipfile 库在使用上也比较友好，它创建和写入 zip 文件的方式就是模仿普通文件的操作流程，使用 with 关键字打开 zip 文件，并使用 write() 函数把要备份的文件写入 zip 文件。

所以通过学习一般文件的操作，你会发现 Python 在对其他格式的文件操作上，都遵循着相同的操作逻辑，这也体现出 Python 语言相比其他语言更加优雅和简单。

那么我把使用 zipfile 库实现创建 zip 文件的功能写入 zipWithoutPassword() 函数中，你可以对照一般文件的写入逻辑来学习和理解这段代码，代码如下：

from zipfile import ZipFile

def zipWithoutPassword(files,backupFilename):
    with ZipFile(backupFilename, 'w') as zf:
        for f in files:
            zf.write(f)

对比一般的文件写入操作，zip 文件的打开使用了“ZipFile() 函数”，而一般文件的打开使用了 open 函数。写入方法与一般文件相同，都是调用“write()”函数实现写入。

这三个函数，也就是函数 getAllFiles()、genZipfilename() 和 zipWithoutPassword()，就是把备份目录到 zip 文件的核心函数了。我们以备份“C:\data”文件夹为“20210412.zip”压缩文件为例，依次调用三个函数就能实现自动备份目录了，我把调用的代码也写在下方供你参考。

if __name__ == '__main__':
    # 要备份的目录
    backupDir = r"C:\data"
    # 要备份的文件
    backupFiles = getAllFiles(backupDir)
    # zip文件的名字“年月日.zip”
    zipFilename = genZipfilename()
    # 自动将要备份的目录制作成zip文件
    zipWithoutPassword(backupFiles, zipFilename)

在执行这段代码后，就会在代码运行的目录下产生“20210412.zip”文件，你通过计算机上的 winrar 等压缩软件查看，就会发现其中会有“C:\data”文件夹下的所有文件。由于文件名称是以当前日期自动产生的，所以每天执行一次备份脚本，就能实现按天备份指定的文件夹为压缩包了。

不过在备份时，除了要保证数据的可用性，你还有考虑数据的安全性，最好的办法就是在备份时为压缩包指定密码。接下来我就带你使用命令行调用实现有密码的文件压缩。

使用可执行命令实现有密码压缩

在制作有密码的压缩包时，我们必须使用命令代替 zipfile 来压缩文件，因为 zipfile 默认是不支持密码压缩功能的。当你需要对压缩数据有保密性的要求时，可以使用 7zip、winrar 这些知名压缩软件的命令行进加密压缩。

我在本讲中就以 7zip 压缩工具为例，带你学习一下怎么使用 Python 通过命令行方式调用 7zip 实现文件的加密压缩。

执行方式和执行参数

要想使用 7zip 实现压缩并被 Python 直接调用，你除了需要在 Windows 上安装 7zip 外，还需要知道它的执行方式和执行的参数。

我先来带你学习一下执行方式。7zip 软件 Windows 安装成功后，它的命令行可执行程序叫做“7z.exe”。但是它想要在命令行运行的话，需要指定程序的完整路径。例如：“c:\path\to\installed\7zip\7z.exe”。如果你希望在命令行直接输入“7z.exe”运行，需要你把可执行程序放在命令搜索路径中。我在这里有必要为你解释一下命令搜索路径的概念，有助于你以后在各种操作系统上执行命令行工具。

一条命令要想运行，必须要使用路径 + 可执行文件的名称才可以。例如我 Windows 中，需要把 Python 的可执行命令“python.exe”安装到“C:\python3.8\scripts\python.exe”这一位置。

那么，一般情况下当你需要运行 Python 解释器时，必须输入很长的路径。这种做法在经常使用命令行参数时没法接受的，一个是你需要记住大量命令的所在路径，另一个是较长的路径也会降低你的执行效率。

因此在各种操作系统上，都有“命令搜索路径”的概念。在 Windows 中，命令搜索路径被保存在 Path 环境变量中，Path 变量的参数是由分号分隔开的文件夹，即：当你在命令行输入“python.exe”并回车运行它时，操作系统会遍历 Path 变量参数中的每个文件夹。如果找到了“python.exe”文件，就可以直接运行它，如果没有找到，则会提示用户该命令不存在。这就避免你每次执行一条命令时都需要输入较长的路径。

再回到 7zip 的命令行执行文件“7z.exe”上，我把它安装在“C:\7zip\”文件夹下，如果你希望执行运行 7z.exe，且不输入路径，那么根据上面的分析，现在有两种解决办法。

把 7z.exe 放到现有的命令搜索路径中，例如“C:\python3.8\scripts\”文件夹。

把 7z.exe 所在的文件夹“C:\7zip\”加入到命令搜索路径 Path 变量的参数中。加入的方法是在 Windows 的搜索栏搜索关键字“环境变量，然后在弹出的环境变量菜单，把路径加入到 Path 变量参数即可。

设置完成环境变量后，7z.exe 就不必在命令行中输入路径，直接运行即可。

在你掌握了执行方式后，我再来带你学习一下它的参数，要想使用支持密码加密方式的 zip 压缩包，你需要使用四个参数，它们分别是：

a 参数：7z.exe 能够把文件夹压缩为压缩包，也能解压一个压缩包。a 参数用来指定 7z 将要对一个目录进行的压缩操作。

-t 参数：用来指定 7z.exe 制作压缩包的类型和名称。为了制作一个 zip 压缩包，我将把该参数指定为 -tzip，并在该参数后指定 zip 压缩包的名称。

-p 参数：用来指定制作的压缩包的密码。

“目录”参数：用来指定要把哪个目录制作为压缩包。

如果我希望把压缩包“20210412.zip”的密码制作为“password123”，可以把这四个压缩包的参数组合在一起，使用如下命令行：

7z.exe a -tzip 20210412.zip -ppassword123 C:\data

扩展 zipfile

由于命令的参数较多，且记住它的顺序也比较复杂，所以我们可以利用 Python 的 popen() 函数，把“7z.exe”封装在 Python 代码中，会更容易使用。

因此我在无密码压缩的代码中，就可以再增加一个函数 zipWithPassword()，用来处理要压缩的目录、压缩文件名和密码参数，并通过这个函数，再去调用 popen() 函数，封装命令行调用 7z.exe 的代码，从而实现有密码的压缩功能。代码如下：

import os
def zipWithPassword(dir, backupFilename, password=None):
    cmd = f"7z.exe a -tzip {backupFilename} -p{password} {dir}"
    status = os.popen(cmd)
    return status

我来解释一下这段代码。在实现有密码压缩的函数中，为了调用函数更加方便，我把“压缩的文件夹、zip 文件名称、密码”作为该函数的参数，这样当在你调用 zipWithPassword() 函数时，就能指定所有需要加密的文件和目录了。此外，在执行命令时，我还通过 os.popen() 函数产生了一个新的子进程（如果你不记得这个概念，可以参考第五讲）用来执行 7z.exe，这样 7z.exe 会按照函数的参数，把文件夹压缩成 zip 文件并增加密码。

通过 zipWithPassword() 函数，你就能够实现 zipfile 的扩展，实现有密码文件压缩功能了。

小结

最后，我来为你总结一下今天这节课的主要内容。我通过 zipfile 库和 7zip 软件，分别实现了无密码压缩文件和有密码压缩文件。

无密码压缩文件更加简单方便，而有密码压缩文件更加安全，配合自动根据当前日期改变压缩文件名称，可以作为你进行每日数据自动化备份的主要工具。

除了备份功能的学习外，我还为你讲解了新的函数返回方式 yield，和 return 不同的是，yield 返回的是生成器对象，需要使用 for 迭代方式访问它的全部数据。yield 语句除了可以和 zipfile 库一起实现数据备份外，还经常被应用于互联网上的图片批量下载压缩场景中。

以上内容就是怎么实现无密码和有密码压缩的全部内容了，我将完整代码贴在下方中，一起提供给你，你可以直接修改需要备份的目录，完成你自己文件夹的一键备份脚本。

from zipfile import ZipFile
import os
import datetime

# 以年月日作为zip文件名
def genZipfilename():
    today = datetime.date.today()
    basename = today.strftime('%Y%m%d')
    extname = "zip"
    return f"{basename}.{extname}"

# 遍历目录，得到该目录下所有的子目录和文件
def getAllFiles(dir):
    for root,dirs,files in os.walk(dir):
            for file in files:
                yield os.path.join(root, file)

# 无密码生成压缩文件
def zipWithoutPassword(files,backupFilename):
    with ZipFile(backupFilename, 'w') as zf:
        for f in files:
            zf.write(f)

def zipWithPassword(dir, backupFilename, password=None):
    cmd = f"7z.exe a -tzip {backupFilename} -p{password} {dir}"
    status = os.popen(cmd)
    return status

if __name__ == '__main__':
    # 要备份的目录
    backupDir = "/data"
    # 要备份的文件
    backupFiles = getAllFiles(backupDir)
    # zip文件的名字“年月日.zip”
    zipFilename = genZipfilename()
    # 自动将要备份的目录制作成zip文件
    zipWithoutPassword(backupFiles, zipFilename)
    # 使用密码进行备份
    zipWithPassword(backupDir, zipFilename, "password123")

思考题

按照惯例，我来为你留一道思考题，如果需要备份的是两个甚至更多的目录，你会怎么改造脚本呢？

欢迎把你的想法和思考分享在留言区，我们一起交流讨论。也欢迎你把课程分享给你的同事、朋友，我们一起做职场中的效率人。我们下节课再见！

28｜Celery库：让计算机定时执行任务，解放人力

你好，我是尹会生。

上一讲我教你怎么把一个文件夹自动打包成压缩包，学会之后，你肯定会想到可以利用这个功能，把每日工作报告的文件夹制作成压缩包，作为邮件的附件发送给领导。

那像是每日发送邮件、每周定时提醒、每月填写报表等等，类似这些按照某一周期完成的重复性任务，你肯定希望也能用 Python 来自动化完成它们，再也不用设置闹钟，提醒自己手动发送邮件和手动提交周报了。

那么今天我就教你如何使用 Windows 任务计划和 Python 的 Celery 库实现周期任务的自动执行。

定时执行任务的注意事项

说起计算机执行任务的方式，我们一般分为两种，分别是手动执行和定时执行，

定时执行任务，在自动化上的要求要比手动执行任务更高，所以两者在程序执行方式和输出结果上有较大的区别。为了让你能正确运行定时任务，我先来带你区分两种执行方式上有什么差别。

首先，执行任务的对象不同。这一点很好理解。手动执行的任务往往是人为操作的，而定时执行的任务启动往往是计算机按照我们设定的时间自动发起的。

其次，基于不同的执行对象，执行过程也有不同的要求。手动执行一个任务时，那么任务弹出的对话框、提示信息等中断任务执行过程的消息，可以人工处理。

而定时运行的任务，由于是由计算机发起的，因此不会有人工来处理。所以定时执行的任务一定要把代码编写成从任务开始到结束都不需要和“人”进行交互，简而言之，定时任务运行的程序必须是“非交互程序”。

最后，定时运行的程序是不支持把任务的结果输出到字符终端的，因此如果需要定时执行任务，并输出执行结果的话，需要把输出结果保存在文件中，否则定时任务会执行失败，导致该定时任务不会被计算机执行。

以上是手动执行任务和定时执行任务的差别，由于我们一般接触的都是手动执行，那么第一次接触定时执行任务时，就要避免把“交互式的程序，需要将执行结果输出到终端的程序”，直接放在定时任务中运行。

在你了解了定时执行任务的方式和注意事项后，我就来带你学习怎么基于 Windows 的任务计划和 Python 的 Celery 库实现定时任务。

需要说明的是，Windows 的任务计划只能支持在 Windows 操作系统且在当前计算机定时运行任务，而且采用了图形界面配置定时任务的时间，比其他的任务计划工具操作更简。

但是如果你要是有多个计算机需要同时配置定时任务，又不想逐个计算机手动配置，那么使用 Celery 会比 Windows 任务计划更适合。

使用 Windows 任务计划，执行定时任务

首先，我先带你学习如何使用“Windows 任务计划”，实现定时任务。

当你的定时任务只需要在一台计算机运行，我建议你使用 Windows 系统自带的“任务计划”实现定时任务，步骤简单、直观，通过三个操作步骤就可以完成。

第一步，自然是启动任务计划程序了。你可以在两个地方找到它的启动入口：

一个是“管理工具” 里的“任务计划程序”图标；

另一个是“控制面板”的“计划任务”图标。

启动以后的界面如下：

界面显示了当前已经在运行的定时任务，你可以点击“名称”来修改它们被执行的时间。如果你需要新建一个定时任务，还可以点击界面右侧的“创建基本任务”按钮。

第二步，“创建基本任务”，指的是设置任务的名称、执行时间，以及要执行的是哪一类程序。

其中“任务名称”是用来在“任务计划程序”界面区分多个任务的，如上图。而设定任务在什么时间执行的功能，叫做“触发器”，它可以按照你期望程序重复的周期，设置为“每天、每周、每月”以及更详细的时间运行。

我把“触发器”的设置截图放在下方，帮你更直观地理解定时任务的设置方式。

除了“每天、每月、每周”等比较宽泛的设置，你还可以通过点击“下一步”进行更具体的执行时间设置，比如我希望每周一至周五的 00:09:01 执行任务，就可以按照下图的方式进行设置。

在截图中，我勾选了周一至周五的复选框，并把开始时间设置为 0 点 09 分 01 秒。

第三步，设置指定时间运行的任务。定时执行的任务可以是一个可执行程序，也可以是终端的一条命令。你只需输入可执行文件或命令的完整路径，以及要执行的文件名称就可以了。点击确认后，就可以按你设定的时间运行了。我把截图贴在下面供你参考。

由于 Windows 的“任务计划”使用了图形界面，所以操作简单，容易上手。但是你在学习它的“实现步骤”时，也会发现跨计算机运行的问题。

跨计算机运行分为相同操作系统和不同操作系统两种情况：

在相同操作系统下，要想编写定时任务，你需要给每台计算机编写一次“任务计划”程序，一旦你的定时任务时间基于新的需求发生了改变，你就需要去手动修改逐台计算机；

针对不同操作系统，你还要掌握 Linux 和 Mac 操作系统上定时任务的不同写法。一旦计划任务多起来，修改计划任务也会影响你的工作效率。

鉴于这些情况，你可以使用 Python 的 Celery 库实现分布式的定时任务，你只需配置一次定时任务，就能让它在多台计算机上同时运行，而且当你的任务执行时间发生变化时，只需要修改一次，所有计算机和该时间关联的任务都会同时更新，解决了 windows 任务计划需要对每台计算机逐个设置定时任务的问题。接下来我就为你讲解如何实现 Python 的 Celery 库实现定时任务。

使用 Python 的 Celery 执行定时任务

按照惯例，我还是先来带你学习 Celery 的安装，再带你学习如何使用它设置定时任务。

安装

Celery 的安装，相较于之前我们学习的第三方库，要稍微复杂些。你需要在安装前，先给 Celery 安装一个用于处理定时任务队列的数据库软件 Redis，因为 Celery 库本身是没有任务排队功能的，所以就需要借用 Redis 等数据库来实现任务队列。换言之，Celery 的安装包括两个步骤，分别是安装 Celery 库和安装并启动 Redis 数据库。

安装 Celery 库：Celery 的安装包和软件同名，使用 pip3 命令安装即可。这里需要说明的是，Celery 库和 Redis 的安装先后顺序不用作区分。

安装并启动 Redis 数据库：Redis 可以从“https://github.com/MicrosoftArchive/redis/releases”地址下载，根据提示多次执行“下一步”即可安装成功。安装成功后，打开 Redis 的安装文件夹，找到“redis-server.exe”双击，即可启动 Redis 服务端，用于 Celery 存储计划任务。

这里需要补充的是，在 Mac 系统下，你可以利用 brew 工具，使用“brew install redis” 进行安装，并运行“brew services start redis” 来启动 Mac 版本的 Redis 服务端。

安装成功后，我们就可以开始为 Celery 配置定时任务了。不过为了让你更深刻地理解和掌握 Celery 定时任务的代码，我先带你学习一下 Celery 和 Redis 内部的工作过程，Celery 和我们以前学习的库不同的是，它已经替我们实现了定时任务的执行和任务管理功能，你只需要像搭积木一样，把需要运行的任务和任务的时间与 Celery 组合即可，我们先来看看 Celery 已经实现的四个组件以及它们的工作过程。

我把 Celery 和 Redis 之间的定时任务工作过程画成一张图，其中包括 Celery 的 Beat、Broker、Worker 和 Backend 四个组件。

前三个是 Celery 用于执行任务的组件，由 Celery 软件自身实现。其中 Broker 是中间件，用来连接 Beat 和多个 Worker 组件，Beat 作为任务的生产者、Worker 作为任务的消费者，可以通过 Broker 进行通信，从而定时运行 Worker 组件的多个任务。

最后一个是 Celery 的数据存储组件，基于 Redis 实现。在定时任务场景中，Redis 用于存放用户添加的任务计划到数据库中，并把数据库文件保存到磁盘，这样前三个组件所在的进程即使遇到意外故障，重新运行，也不会造成定时任务的丢失。

此外，Celery 和 Redis 数据还是通过 TCP 网络协议连接的，所以你可以给多个定时任务指定一个 Redis 数据库，以此来保证多个计算机上的定时任务需要进行修改时，只修改一次就可以实现自动数据同步了。

根据对 Celery 执行定时任务的工作过程分析，相信你已经对它的四个组件有所了解了。那接下来我就以每周六 22 点 01 分定时备份“c:\data”文件夹为例，给你讲解一下怎么通过编写 Celery 四个组件的代码来定时备份数据。

定时备份文件夹

实现文件夹的定时备份功能**，需要编写代码的组件主要 **是 Worker 组件和 Beat 组件。

Worker 组件是真正执行计划任务的组件，它执行的计划任务对象是一个函数，因此你可以把上一讲我们学习的备份功能封装成函数，由 Worker 组件按照存储 Redis 中的时间进行定时备份。

而 Beat 组件编写的代码主要是任务的执行时间和该时间运行的任务名称。而其他两个组件中的 Broker 承担的是 Beat 和 Worker 之间的调度工作，即接收 Beat 发送的任务，和等待 Worker 来取任务，因此它和存储定时任务的 Backend 组件不需要进行编程。

那接下来，我们将编写定时运行任务的代码和 Worker、Beat 两个组件进行整合，来实现完整的 Celery 定时任务。定时运行任务的代码保护了两部分功能，分别是设置 Celery 的定时任务日期，以及编写要运行的定时任务函数。我们首先来看一下如何设置定时任务的日期。

设置定时任务日期

定时任务的日期，是由指定任务名称、任务要执行的函数名称、执行时间和执行函数的参数四个部分组成的嵌套字典类型。任务名称作为字典的“key”，其他三个部分作为字典的值，并分别以“task”、“schedule”、“args”作为内部字典的“key”。

如果我以每周六 22 点 01 分执行 test1.py 文件的 run1() 函数编写定时任务，那么嵌套字典应该为如下格式：

 from celery.schedules import crontab
   "test1": {
        "task": "tasks.jobs.test1.run1",  #执行的函数
        "schedule": crontab(minute=1, hour=22, day_of_week=6),
        "args": ()
    }

我来详细解释一下三个字典的 value 值。首先，task 字典的值"tasks.jobs.test1.run1"中的“tasks.jobs”是指 tasks 文件夹下的 jobs 文件夹，tasks 文件夹用来存放定时任务的脚本文件，jobs 文件夹用于存放要执行的 python 脚本文件。

你肯定会问，为什么要使用这么多不同的文件夹呢？原因就在于，使用不同的文件夹有助于区分不同组件的功能。像“test1.run1”表示 jobs 文件夹下的 test1.py 文件中的 run1 函数，而这个函数就是 Celery 的 Worker 组件将要执行的定时任务。

其次，“schedule”字典的值“value”表示任务“test1”将会在何时运行。它的参数是一个 crontab 函数。该函数定义如下：

crontab(minute='*', hour='*', day_of_week='*',
                 day_of_month='*', month_of_year='*')

crontab() 函数的参数，就是用来灵活配置定时任务的执行时间的。我以 minute 来为你举例，它可以有四种写法：

如果 minute 为默认的“*”，表示每分钟执行一次。

如果 minute 为具体的数字，例如 10，表示将会在第 10 分钟运行任务。不过这里要注意的是，当你使用具体数字时需要配合其他参数一起生效。假设小时的参数为“*”，则表示每小时的第 10 分钟运行任务，假设小时的参数为 5，则表示 5 点 10 分运行任务。

如果 minute 参数为“*/2” ，表示每 2 分钟运行一次任务。

如果 minute 参数为“1，3，5”，表示每小时第 1、3、5 分钟运行一次任务。

四种写法也同样可以应用于参数 hour(小时)、day_of_week(星期)、day_of_month(月)、month_of_year(年)，因此其他位置的参数我就不再一一为你进行讲解了。

最后一个参数是 args，它的参数是元组， 用来指定定时运行任务时，为该任务对应的函数传递哪些参数，如果不需要传递参数，则需要保留该值为空元组“()”格式。

设置好定时任务的日期之后，需要继续编写该日期所需要运行的任务函数。定时运行的任务函数需要按照“task”的字典 value 保存在“tasks.jobs”目录中，我把目录结构的截图放在下方，帮你更直观地找到每个功能所在的文件目录。

在截图中，config.py 保存了设置定时任务的代码，jobs 文件夹下保存了需要运行的 Python 脚本文件。

编写任务函数

接下来，我来继续为你讲解 jobs 中的 test1.py 文件中的任务函数 run1() 函数是如何编写的。

回到我们备份 data 目录的例子，run1() 函数中就是实现备份目录的全部功能，在定义 run1() 函数时，为了能够支持被 Celery 调用，必须在定义时为它增加一个装饰器“@app.task”。

装饰器就是在不改变函数原有的定义前提下，为函数运行之前和之后增加新的功能。而这里为 run1() 增加装饰器的目的，就是为了在函数原有功能不变的前提下，让 run1() 函数增加被 Worker 组件执行的功能。

类似某一天天气突然降温，你依然想要穿着昨天帅气的格子衬衫去上班时，就可以在格子衬衫外面再增加一个酷酷的皮衣，既有温度又有风度。

增加后的代码如下：

app = Celery("tasks")

@app.task
def run1():
    print("这里是备份data文件夹的代码")

代码中的“app”是 Celery 的实例，“@app.task”是修饰 run1() 函数的装饰器，当你需要为 Celery 增加更多需要运行的函数时，也必须使用“@app.task”对任务函数进行修饰。

这里还有一个小问题需要你注意，如果你需要运行的任务函数都在同一脚本文件，实例化 Celery 类可以放在当前脚本文件中，但是当你需要运行多个脚本文件时，Celery 会被实例化多次，导致运行出现异常。

为了避免这一问题，你需要把“app = Celery("tasks")”实例化的代码写入到 tasks 目录下的“init.py”中。当一个目录中如果包含了“init.py”文件时，Python 会将这个目录当作我们学习过的包来按照 Python 内置的语法进行处理。Python 会在进入 tasks 包所在的文件夹是，自动运行一次“init.py”文件，且第二次进入也不会重复运行，只会在一个程序中执行一次。总结来说，就是包含“init.py”的文件夹是 Python 的包，包在被 (导入) 使用时，它的“init.py”文件会第一时间自动执行一次，且无论导入多少次，“init.py”只被 Python 执行一次。这种特性带来的好处是：由 Python 解释器保证该实例化只能运行一次，避免了一个类被多次实例带来的运行异常问题。

调整完成的三个代码文件内容如下：

# __init__.py

from celery import Celery

# 创建celery应用对象
app = Celery("tasks")

# 导入celery的配置信息
app.config_from_object("tasks.config")

在这段代码中，我同样不想让 Celery 访问 Redis 数据库会进行多次初始化，因此在“init.py”初始化文件中，我还增加了“app.config_from_object("tasks.config")”设置项，确保同一个 Celery 只和 Redis 数据库建立一次连接，避免因多次连接带来数据库没有必要的资源开销。

# config.py

from celery.schedules import crontab

# 指定Redis数据库的地址和端口
broker_url = "redis://127.0.0.1:6379/1"

# 时区
timezone = "Asia/Shanghai"

# 导入任务所在文件
imports = [
    "tasks.jobs.test1",
    "tasks.jobs.test2",
]

# 需要执行任务的配置
beat_schedule = {
   "test1": {
        "task": "tasks.jobs.test1.run1",  #执行的函数
        "schedule": crontab(minute=1, hour=22, day_of_week=6),
        "args": ()
    },
   "test2": {
        "task": "tasks.jobs.test2.run1",  #执行的函数
        "schedule": crontab(minute="*"),
        "args": ()
    },

}

为了你能更快地看到定时任务执行的结果，我增加了任务 test2，实现了每分钟运行一次，因此当你执行 Worker 组件后，就可以在 1 分钟内观察到 test2 任务的输出。

# test1.py

from tasks import app

@app.task
def run1():
    print("开始备份data文件夹")

if __name__ == '__main__':
    tasks()

# test2.py

from tasks import app

@app.task
def run1():
    print("开始备份data2文件夹")

if __name__ == '__main__':
    tasks()

编写完成任务和设置完成执行时间后，接下来需要发布任务和执行任务。发布任务需要运行 Beat 组件，执行任务则需要运行 Worker 组件，我来带你运行它们，并观察运行的结果，以此来分析定时任务是否被成功执行。

运行 Beat 和 Worker 组件

运行 Beat 组件需要在终端使用“celery -A tasks beat”命令。其中 tasks 是我们保存 Celery 实例化的目录，beat 参数表示发布的是定时运行的任务。它的正确执行结果如下：

SHELL$ celery -A tasks  beat
celery beat v5.0.5 (singularity) is starting.
__    -    ... __   -        _
LocalTime -> 2021-04-13 01:40:31
Configuration ->
    . broker -> redis://127.0.0.1:6379/1
    . loader -> celery.loaders.app.AppLoader
    . scheduler -> celery.beat.PersistentScheduler
    . db -> celerybeat-schedule
    . logfile -> [stderr]@%WARNING
    . maxinterval -> 5.00 minutes (300s)

发布任务后，需要运行定时任务的计算机可以执行对应的 Worker 组件，执行的命令和结果如下：

SHELL$ celery -A tasks worker

 -------------- celery@edzdeMacBook-Pro-2.local v5.0.5 (singularity)
--- ***** -----
-- ******* ---- Darwin-20.3.0-x86_64-i386-64bit 2021-04-13 01:41:47
- *** --- * ---
- ** ---------- [config]
- ** ---------- .> app:         tasks:0x7ff03c9549d0
- ** ---------- .> transport:   redis://127.0.0.1:6379/1
- ** ---------- .> results:     disabled://
- *** --- * --- .> concurrency: 4 (prefork)
-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)
--- ***** -----
 -------------- [queues]
                .> celery           exchange=celery(direct) key=celery

[2021-04-13 01:42:00,046: WARNING/ForkPoolWorker-2] 开始备份data2文件夹
[2021-04-13 01:43:00,004: WARNING/ForkPoolWorker-2] 开始备份data2文件夹
[2021-04-13 01:44:00,003: WARNING/ForkPoolWorker-2] 开始备份data2文件夹
[2021-04-13 01:45:00,004: WARNING/ForkPoolWorker-2] 开始备份data2文件夹

由于我把“test2”任务设置为每分钟运行一次，因此在 Worker 组件可以看到每分钟会显示一次提示信息。当你确认定时任务能够正常运行后，可以使用下面两条命令实现不占用终端的 Worker 组件启动和停止，这样的话，在你关闭终端之后，Celery 的 Worker 依然可以正常运行。

celery multi start worker -A appcelery
celery multi stop worker

小结

最后，我来为你总结一下本讲的主要内容。通过对比任务的执行方式，我给你讲解了定时任务需要使用非交互方式，并且不能把程序的结果输出到终端中，否则会导致在终端没法显示运行结果，或者定时任务执行异常。

同时我还为你讲解了基于 Windows 的任务计划功能和 Python 的 Celery 实现的定时任务，它们在配置定时任务上的逻辑是相同的，但是在配置方法和执行方法上有较大区别：Windows 的配置更加直接，而 Celery 的配置则需要掌握 crontab() 函数的参数。

此外，它们支持的操作系统也不同，Windows 任务计划只能在 Windows 中使用，而 Celery 可以应用于 Windows、Linux 和 Mac 操作系统，但也因为需要有丰富的扩展性和灵活性，损失了它的易用性。

因此，我建议你在选择类似计划任务的某些功能时，应当优先考虑场景和软件的复杂度，基于不同的场景选择合适的软件，才能最大限地提高办公自动化。

思考题

按照惯例，我要为你留一道思考题，如果我希望定时任务能帮我实现每周一、三、五的 18:00 提交工作报告，你需要如何设置 crontab() 函数呢？

欢迎把你的想法和思考分享在留言区，我们一起交流讨论。也欢迎你把课程分享给你的同事、朋友，我们一起做职场中的效率人。我们下节课再见！

29｜网络和邮件库：定时收发邮件，减少手动操作

你好，我是尹会生。

相信在你的日常办公工作当中，对邮件肯定早就不陌生了。我们通过邮件既可以发送和接收正式的公文，也能够利用邮件编写周报、月报，以及订阅定期发布的新闻或者期刊，等等。

在这么多的应用场景当中，你会发现有些收发邮件的工作是周期性的。那我们就可以利用 Python，将这些重复的邮件收发工作进行自动化。

设想一下：在我们接收邮件的时候，我们可以定时监察邮箱中的邮件，例如根据邮件中特定的主题，来自动判断是否为重要邮件。如果是的话，可以通过 Python 调用钉钉等即时通讯软件马上通知自己，实现邮件的额外通知功能。

还有在发送邮件的场景中，如果你发现周报、月报等邮件模版是可以通用的，那你可以利用 Python 的字符串编写来实现邮件内容的自动替换功能，让你在使用邮件发送周报、月报工作中节约大量的时间。

那么今天我就教你如何使用 Python 的 yagmail、imaplib 两个库，分别实现邮件的自动发送和自动接收功能，并利用正则表达式、字符串和变量功能，来替代手工的重复工作。

自动收邮件

我们先从如何自动收邮件开始学习。今天的案例是这样的：我希望能每隔五分钟检查一次收件箱，判断收件箱中是否有 30 天内未读的邮件。并利用正则表达式根据邮件主题判断其中是否包含“故障”这一关键字。如果包含的话，就通过钉钉等即时通讯工具通知到我，实现高优先级邮件处理的功能。

要想利用 Python 的正则表达式判断邮件主题是否出现了“故障”关键字，你必须要让 Python 实现邮件接收和主题读取功能。在 Python 中，poplib 和 imaplib 库都支持邮件的接收协议，可以让我们登陆服务器接收邮件，从而实现邮件接收和主题读取。那这两个库该选择哪一个呢？

imaplib 库支持 IMAP 协议，而 poplib 库支持 POP3 协议，IMAP 协议在支持双向操作的功能上更加强大，并且能把客户端对邮件的删除等操作同步到服务端，也能把服务端对邮件删除的操作同步到客户端。与 POP3 协议只能把服务端的操作单向同步给客户端相比，会更加灵活。所以我在本讲中，就以 imaplib 库为例，为你讲解通过 IMAP 协议进行邮件的自动接收。

我们在确定采用 IMAP 协议接收邮件之后，接下来就要按照 IMAP 协议的要求，编写一个从邮件服务器下载邮件并分析邮件主题的代码。获取邮件主题的代码分为三个主要步骤，分别是指定邮件服务器的 IMAP 地址和端口、验证用户名和密码的正确性以及下载邮件到本地并解析邮件得到邮件主题。我们依次来学习一下。

获取邮件主题

第一步是指定邮件服务器的 IMAP 地址和端口。大部分对邮件安全比较重视的公司，为了防止黑客暴力发现邮件服务器用户的弱口令密码，默认是将 IMAP 服务的功能关闭的。你需要联系邮件服务器管理员或通过网页管理功能打开 IMAP 服务，允许你在家里连接 IMAP 服务器。

我以 QQ 邮箱为例，打开 IMAP 服务的方法是在QQ 邮箱的网页端登陆成功后，通过设置 - 账号 -IMAP 服务，打开 IMAP/SMTP 服务。打开功能后，可以参考官方文档将 IMAP 服务器的地址指定为：“imap.qq.com”，“使用 SSL”保证数据传输过程的安全，并将连接 IMAP 服务器的端口指定为“993”。打开 IMAP 服务的截图和官方文档的截图如下。

第二步是使用用户名密码登录。当服务器允许你从远程使用 IMAP 协议登陆服务器接收邮件后，就可以使用 Python 的 imaplib 库进行连接和登陆了。

imaplib 库是 Python 的内置库，连接服务器可以使用 IMAP4_SSL() 函数，登录可以使用 login() 函数，连接和登陆的代码如下：

import imaplib
conn = imaplib.IMAP4_SSL(host="imap.qq.com", port = 993)
conn.login("username@qq.com","password")
print(conn.list())

在这段代码中的第三行，你需要把“username 和 password”替换为你的用户名和密码，替换之后才能正常登陆。如果登陆成功，可以通过 list() 函数查看邮箱中默认包含了哪些文件夹，默认的邮件都被放在“INBOX”文件夹中，而“INBOX”就是我们经常使用的收件箱。

如果没有登陆成功，在运行代码后会被提示连接超时或密码错误，这个时候你就需要根据错误提示，进一步优化你的网络质量或使用正确的密码。

最后一步是解析邮件主题。当你成功登录邮件服务器之后，你并不能直接读取邮件的内容，必须要将邮件下载到本地才能对邮件内容进行解码和内容查看。这时，你如果对收件箱中的邮件进行查询，只能得到未读邮件的唯一 ID，我们称它为“邮件 ID”，你需要通过 IMAP 的 fetch() 命令将邮件 ID 对应的邮件内容下载本地后，才能进行解码，解码后才能真正取得邮件的主题、内容和附件等邮件里的具体内容。

你还要注意的是，通过邮件 ID 下载的邮件内容需要解码两次，才能看到邮件的主题。这是因为每一封邮件都采用了邮件的标准编码方式 MIME 编码，MIME 编码可以让邮件在服务器和客户端直接实现正常的传输，但是你无法查看使用了 MIME 编码之后的邮件内容，因此需要先对邮件的 MIME 编码进行解码。

而第二次解码呢，是把 MIME 编码的邮件解码后的内容，转换成符合你当前操作系统的编码，否则在查看邮件主题时会出现乱码，无法使用正则表达式进行内容匹配。

总结来说，在 Windows 中，默认的编码为“GBK”编码，mac 为“UTF-8”编码。你需要把内容按照 Python 所在的操作系统再解码一次，这样才能得到正确的邮件标题，之后才能使用正则表达式处理我们得到的邮件主题。

使用的函数

虽然获得邮件的主题的步骤比较繁琐，无法一次性得到邮件主题，不过你不用担心，因为每个处理步骤中只需要使用一个函数就可以搞定了。那么接下来我先把从进入收件箱到取得主题的完整执行过程的代码提供给你，然后再为你具体讲解每个函数的作用。

import email

# 默认为INBOX
conn.select("INBOX")
# 搜索邮件，ALL为全部，可以按照发件人使用FROM过滤，也可以使用日期过滤
_, data = conn.search(None, 'unseen')

for mailid in data[0].decode().split(" "):
    # 取回每一封未读邮件的内容
    # data = [b'1 2 3 4 5']
    _, maildata = conn.fetch(str(mailid), '(RFC822)')
    # 对每一封邮件的内容进行解析
    msg = email.message_from_string(maildata[0][1].decode('utf-8'))
    # 取得标题
    subject_tmp = msg.get('subject')
    # 为标题解码
    sj_decode = email.header.decode_header(subject_tmp)[0][0]
    #打印每一封标题
    subject = sj_decode.decode('utf-8')
    print(subject)

    # 将邮件标记为已读
    conn.store(mailid, '+FLAGS','\\seen')

我来为你依次解释一下 imaplib 是如何读取邮件并得到邮件主题的。

首先，我们需要从收件箱中找到 30 天内未读的邮件 ID，通过邮件的 ID 才能从 IMAP 服务器下载邮件的内容。

我在代码第 4 行，使用了 select() 函数，指定要读取的文件夹为收件箱“INBOX”；再利用第 6 行的 search() 函数的“unseen”参数，来取得 30 天内未读的邮件。这里的“INBOX 和 unseen”都是 IMAP 协议定义的关键字，Python 会将它们转译为 IMAP 的语法，并发送给服务器，而服务器则会把 30 天以内未读邮件的 ID 以列表形式返回，并把 ID 以字节方式存放在 data 列表的第一个元素中，data 列表的具体格式，你可以参考第 10 行注释。

接下来，我们需要根据每个邮件 ID 得到邮件的主题。由于邮件 ID 为字节类型，因此我将它转换为列表之后，使用 for 循环进行遍历，把每个邮件 ID 用 mailid 变量进行了保存。同时，我在第 11 行通过 fetch() 函数，使用邮件 ID 向服务器发起请求，得到该邮件的所有数据。

最后，我们把从服务器得到的邮件内容进行解析、取出标题部分，并进行解码。当你使用 fetch() 函数取得了邮件的内容后，如果使用 print() 进行输出，会发现你无法看到邮件里真正的内容。为什么会出现这种情况呢？

原因就在于邮件采用的是 MIME 类型，这种类型是邮件的标准格式，需要专门的工具进行内容的解析。就像你无法通过记事本查看一张图片一样，通过记事本只能看到图片中混乱的数据，无法得知图片上面的颜色和内容。因此我需要再使用一个标准库 email 的 message_from_string() 方法，对 MIME 类型进行解析。

解析之后，你就能够得到主题、内容、附件等邮件的不同部分了，由于我在当前案例需要提取邮件的主题，所以使用第 15 行的 get() 函数通过参数“subject”取得了当前邮件的头部信息，并利用 decode_header() 取得了邮件的主题。

为了能够在 Mac 系统上也可以进行处理，我将主题采用“utf-8”编码进行解码后，就能够正常显示汉字了。如果你想要判断该主题是否包含“故障”关键字，可以使用我们多次使用到的正则表达式，使用 re.search('故障', subject) 进行正则匹配，并且你还可以增加钉钉通知、短信通知、自动修复故障等等各种自动化操作。

在学习了自动接收邮件并对主题进行判断的功能之后，还有两点需要你注意，这也是初次使用 imaplib 库的同学最容易犯的两个错。

第一个是如果你没有将存储在 IMAP 服务器上的邮件标记为已读，会导致自动接收邮件程序重复处理该邮件。由于 fetch() 函数的功能是从服务器下载邮件内容，并对邮件进行自动化处理，所以服务器上的邮件状态仍然为“未读”状态。这会导致你的程序陷入死循环，对匹配的主题进行无限重复的处理。

为了避免这一问题，你应该在处理完当前的邮件后，使用“conn.store(mailid, '+FLAGS','\seen')”方法，将当前操作的 mailid 在服务器设置为已读邮件。这样每个邮件就会只被处理一次了。

另一个经常出现的问题是，当你的所有邮件都为已读状态时，应当在遍历邮件 ID 功能前增加对 data 变量的判断，避免向服务器提交空 ID，导致运行到 fetch() 函数时，服务器接收空 ID 报错。

具体的操作是：你可以在得到 data 变量后，使用 if 判断该变量是否为“None”，如果为 None，则本次执行到此结束。如果有未读邮件，则再将邮件 ID 通过 fetch() 提交到服务器进行处理。

当你已经掌握了自动接收邮件的步骤之后，再来学习自动发送邮件就非常简单了。自动发送邮件采用了 SMTP 协议，而且也需要指定服务器地址、用户名、密码以及收件人、主题、内容和附件。由于发邮件和接收邮件的大部分概念相同，所以我们可以对比接收邮件来学习实现自动发送邮件的步骤。

自动发邮件

和自动接收邮件类似，自动发送邮件的步骤也是三个，分别是连接邮件服务器、编写邮件正文和发送邮件。

连接邮件服务器

在邮件服务的协议规范中，规定发送邮件采用的是 SMTP 协议，因此，在自动发送邮件这一步，我们需要采用和 imaplib 不同的包实现。

在标准库中发送邮件的包叫做 smtplib，由于 smtplib 需要配置较多的通用参数，所以还有一个对它进行了更高级的封装的第三方库 yagmail 库。yagmail 库将大部分的默认参数在底层实现了，发送邮件时，你只需要关注必须填写的服务器 IP、用户验证以及邮件的内容即可。

yagmail 第三方库的安装包和它同名，那么你可以使用 pip 命令直接安装，安装成功后把它导入并连接服务器即可。连接 SMTP 服务器的代码如下：

import yagmail
conn = yagmail.SMTP(
        user="username@qq.com",
        password="password",
        host="smtp.qq.com",
        port=465
        )

yagmail 库使用 SMTP() 函数与服务器建立连接，并在连接时指定用户名、密码、主机地址以及端口。

这里需要注意的是，SMTP() 函数通过默认参数“smtp_ssl=True”使用了 SSL 协议，如果你所使用的邮件服务器采用了不同版本的 SSL 传输加密协议，你需要先将默认端口从 465 改为 587。如果没有提示连接异常，表明建立连接是成功的，接下来就可以为这封邮件编写内容了。

编写邮件正文

编写邮件正文时，可以采用我们学习过的 f-string字符串的形式来存放邮件的内容。例如你经常要发送的周报、月报都是相同的邮件格式，不同的数据内容或文字。这时候可以使用 f-string 字符串的变量替换功能，将格式编写为 f-string 的字符串，再将每次变动的内容使用变量进行替换，它的代码格式如下：

content = "内容填充"
body = f"模版 {content}"

当你编写好邮件的正文后，需要使用 send() 函数来发送邮件。send() 函数一般会使用四个参数，按照参数定义的顺序，它们分别是收件人邮箱、主题、邮件正文和附件。我将这四个部分依次作为 send() 函数的参数后，就可以将邮件发送到 SMTP 服务器了。

这里我有一个小的建议，我会建议你先把邮件发给自己，如果出现发送失败，或发送内容与自己期望不符时，更方便对内容进行调整。我把发送命令和发送成功后的截图贴在下面，供你参考。

conn.send("receiver@qq.com", "主题", body, "one.jpg")

conn.send("receiver@qq.com", "主题", body, "one.jpg")

这就是利用 yagmail 实现自动发送邮件的完整过程，掌握之后，你可以把上节课学习的定时任务，以及自动生成图形的 matplotlib 库结合起来使用，将发送邮件功能定义为一个函数，从而实现周报和月报的自动发送功能。

小结

最后，我来为你总结一下这一讲的主要内容。在本讲，我使用了 yagmail 库、imaplib 库以及 email 库实现了邮件自动收发的功能。与 Foxmail 和 Outlook 比起来，使用 Python 实现的邮件客户端，能够在收取邮件后对主题等元素自行判断，并与正则表达式、IM 通知等其他工具组合，实现更加自动化的功能。

利用 yagmail 自动发邮件前，你还可以为你的周期发送的邮件指定模版，通过 Celery 实现定时发送和周期发送邮件。

除了可以自动收发邮件外，我还为你详细讲解了接收邮件的处理过程，这一过程遵循了 IMAP 的协议规范，决定了代码编写的先后顺序，如果邮件接收或发送是你自动化工作中主要优化的工具，那么我建议你用更多的时间来掌握 IMAP 与 SMTP 协议规范。

当你掌握了 IMAP 协议能够支持哪些操作以及不能支持哪些操作之后，才能更好地进行邮件 API 的学习。对于 IMAP 支持的功能，你可以参考官方文档，找到函数及其参数，对于没有支持的功能，你需要自己实现自定义的函数。

最后的最后，除了微信、钉钉外，邮件可以说是我们职场中使用最广泛的通讯工具了。并且也是我们工作中最正式的通讯工具。因此我建议你能够多练习怎么通过 Python 更加熟练地自动化收发邮件，相信我，这会为你的工作带来更高效的输出。

思考题

按照惯例，我来为你留一道思考题，如果我希望每周六 10 点整，能够自动的将 C 盘上的一个目录作为邮件的附件发送到一个指定邮箱，你会使用哪些库来实现，你能否将实现的思路用自己的语言讲出来呢？

欢迎把你的想法和思考分享在留言区，我们一起交流讨论。也欢迎你把课程分享给你的同事、朋友，我们一起做职场中的效率人。我们下节课再见！

30｜怎么快速把任意文件格式转成PDF，并批量加水印？

你好，我是尹会生。

在办公场景中，我们打交道最多的软件，要数 Office 的办公套件了，功能丰富且强大，使用方便。不过你可能也会发现，我们经常用于文字编辑的 Word 软件，它使用的 docx 扩展名的文件无论是在不同操作系统平台、不同的设备上，还是在内容安全性和格式的兼容性上，都没有 PDF 文件强大。Excel 和 PowerPoint 中的文件也是如此。

例如：你需要把公司的合同范本发给其他公司审阅，为了保证文本不会被随意篡改，往往需要把 Word 或 PowerPoint 的文件转换为 PDF 文件，再通过邮件发给其他公司。而且，随着数字化转型的开始，原本在计算机上正常显示的表格，拿到手机上可能会缺少字体；也可能因为屏幕宽度不同，导致格式无法对齐；还可能会出现无法显示文字等格式问题。

不过这些问题呢，PDF 统统可以解决。所以像是商业的条款、合同、产品介绍，既要保证安全，又要确保格式正确，如果你不想限制用户必须要用多宽的显示器，或者必须安装好特定的字体，才能看到你的 Word、Excel、PowerPoint 里的内容的话，那么使用 PDF 文件就是非常必要的了。

所以在今天这一讲中，我将带你学习如何把 Word、Excel、PowerPoint 的默认文件格式批量转换为 PDF 文件，并教你怎么给 PDF 文件增加水印，既保证样式美观，又确保文档的安全。

将常用办公文件转换为 PDF 格式

如果你以前遇到过类似转换为 PDF 文件的需求，那么在你搜索 Python 的第三方库时，就会发现，将 Word、Excel、PowerPoint 的默认文件保存格式转换为 PDF 的库非常多。由于我要将多种格式的文件转换为 PDF，那么为了我就需要使用一个能支持 Office 所有组件的库，而这个库就是 pywin32 库。

pywin32 库支持了绝大多数的 Windows API，因此它只能运行在操作系统是 Windows 上，当你需要使用 pywin32 操作 Office 各个组件时，可以利用 pywin32 库调用 Offcie 组件中的 VBA 实现 Office 组件的大部分功能。

接下来，我将从 pywin32 库的安装开始，来为你逐步讲解一下如何把 Office 组件中的 Word、Excel、PowerPoint 的默认文件保存格式转换为 PDF 文件。

虽然你要学习三种不同的软件格式转换为 PDF 文件，但是它们三个文件格式转换的思路和被调用的函数是相同的，你可以通过对照 Word 文件转换为 PDF 来去掌握其他两个软件的文件格式转换代码，来学习自动格式转换，这样学起来会更加轻松。

将 Word 文档转换为 PDF

我先以 Word 为例，来为你讲解一下 pywin32 库的安装、导入，以及将 Word 文档进行转换的操作步骤。

由于 pywin32 是第三方库，所以你要使用 pip 命令把它安装到你的 Windows 计算机中。这里需要注意，pywin32 的安装包和软件名称不同，而且导入的库名称也和 pywin32 不同。所以我把 pywin32 库的安装和导入时，使用的名称写在文档中，供你进行参考：

SHELL$ pip3 install pypiwin32
PYTHON> import win32com

我们用于格式转换的库叫做“win32com”，它是 pywin32 软件的库名称。安装它时，你要使用 pypiwin32，作为它的安装包名称来使用。这是第一次接触该库最容易混淆的地方，我建议你在阅读后续内容前，先对“pywin32、pypiwin32、win32com”这三个概念进行区分。

在你正确区分上面三个概念之后，我们就可以开始导入 win32com 库，并调用 VBA 脚本，来把 Word 文档转换为 PDF 格式了。

为了让你更好地理解 win32com 库的执行过程，我来为你举个例子。我在"C:\data"文件夹下有一个 Word 格式的 a.doc 文件，现在要将它自动转换为 a.pdf 文件，并继续存储在该目录中。如果你手动进行格式转换，只是需要以下四个步骤：

进入到 C:\data 文件夹；

使用 Office 的 Word 组件打开 a.doc 文件；

使用“文件另存为”功能，保存为 PDF 格式，并指定保存目录；

保存并关闭 Word 文件，退出 Word 进程。

由于 win32com 库是调用 Word 的 VBA 脚本实现的格式转换功能，因此转换格式的 Python 代码步骤也和手动操作 Word 几乎相同，少数不同的地方是因为 win32com 支持的组件较多，需要指定当前转换采用的 VBA 脚本为 Word 文件的脚本。我将 Word 转换 PDF 的代码写在下方，供你参考。

from win32com import client

def word2pdf(filepath, wordname, pdfname):
    worddir = filepath
    # 指定Word类型
    word = client.DispatchEx("Word.Application")
    # 使用Word软件打开a.doc
    file = word.Documents.Open(f"{worddir}\{wordname}", ReadOnly=1)
    # 文件另存为当前目录下的pdf文件
    file.ExportAsFixedFormat(f"{worddir}\{pdfname}", FileFormat=17， Item=7, CreateBookmarks=0)
    # 关闭文件
    file.Close()
    # 结束word应用程序进程
    word.Quit()

我来为你详细解释一下上面这段代码。这段代码中我定义了一个 word2pdf() 函数，它被 Python 调用时，会根据自己的参数将 word 文件的路径、word 文件的名称和 pdf 名称传入参数中。

根据这些参数 word2pdf() 函数会调用 DispatchEx() 打开 Word 程序，再使用 Open() 函数打开 a.doc 文件，并使用 ExportAsFixedFormat() 函数将 Word 文件另存为 PDF 文件之后，使用 Close() 和 Quit() 关闭 a.doc 文件并结束 Word 进程。

由于 win32com 是用过 Word 的 VBA 脚本实现对 Word 进程行为的控制的，所以它的操作步骤和手动操作非常相似，所以这段代码也非常容易理解。

那在这里我需要提醒你注意两个容易被忽视的问题，第一个是由于 win32com 库在 Windows 系统上是直接调用 Word 进程实现格式转换的，因此你必须为当前的 Windows 安装 Word 以及 Office 的办公组件。那另一个是由于 win32com 对 Word 操作的方式是基于 Word 的 VBA 脚本，所以你想在转换时为 ExportAsFixedFormat() 函数增加新的参数，需要参考 Office 官方网站的文档。

我也将 Office 官方网站关于 Word 的 VBA 脚本所在网页提供给你做参考（https://docs.microsoft.com/zh-cn/office/vba/api/word.document.exportasfixedformat）。当你增加新的功能时，可以通过网页的内容来获得更多功能。

将 Excel 表格转换为 PDF

在你掌握了如何使用 win32com 实现 Word 文档转换为 PDF 之后，我再来带你实现 Excel 表格自动转换为 PDF 的功能，你也可以对比着来学习掌握，同时也注意观察把 Excel 和 Word 文件转换为 PDF 的主要差别。

Excel 表格默认保存的文件格式是 xls 或 xlsx，将 xls 或 xlsx 格式转换为 PDF 的步骤和思路与 Word 文档转换为 PDF 相同，所以我先把代码提供给你，然后再为你讲解。代码如下：

from win32com import client

def excel2pdf(filepath, excelname, pdfname):
    exceldir = filepath
    # 指定Excel类型
    excel = client.DispatchEx("Excel.Application")
    # 使用Excel软件打开a.xls
    file = excel.Workbooks.Open(f"{exceldir}\{excelname}", False)
    # 文件另存为当前目录下的pdf文件
    file.ExportAsFixedFormat(0, f"{excel}dir}\{pdfname}")
    # 关闭文件
    file.Close()
    # 结束excel应用程序进程
    excel.Quit()

对比 word2pdf() 和 excel2pdf()，你会发现实现的基本逻辑是相同的，但是在实现 Excel 转换上，有两个主要函数的参数不同。

DispatchEx() 函数的参数，这里使用了“Excel.Application”字符串作为该函数的参数，“Excel.Application”作为 DispatchEx() 参数的目的是，让 pywin32 库启动 Excel 进程，并让它读取“a.xls”文件。

ExportAsFixedFormat 函数的第一个参数从 pdf 路径变为保存的类型，你可以参考Office 的 Excel VBA 官方文档，从中学习函数的每个参数。

将 PowerPoint 幻灯片转换为 PDF

在你学习了 Word 文档和 Excel 表格转换 PDF 文件的基础上，我再来带你你对比学习一下如何将 PowerPoint 的默认保存文件 ppt 格式转换为 PDF。参考 word2pdf() 和 excel2pdf() 两个函数，我直接将 PowerPoint 的幻灯片转换 PDF 文件的代码提供给你，你可以通过官方文档 (https://docs.microsoft.com/zh-cn/office/vba/api/powerpoint.presentation.exportasfixedformat)，来试着分析 ppt2pdf() 函数的参数和函数里的每行代码。

from win32com import client

def ppt2pdf(filepath, pptname, pdfname):
    pptdir = filepath
    # 指定PPT类型
    ppt = client.DispatchEx("PowerPoint.Application")
    # 使用ppt软件打开a.ppt
    file = ppt.Presentations.Open(f"{pptdir}\{pptname}", False)
    # 文件另存为当前目录下的pdf文件
    file.ExportAsFixedFormat(f"{pptdir}\{pdfname}")
    # 关闭文件
    file.Close()
    # 结束excel应用程序进程
    excel.Quit()

显而易见，PowerPoint 幻灯片转换为 PDF 文件的代码逻辑和 word2pdf() 和 excel2pdf() 函数的逻辑完全相同，只有两处有所不同，一个是 DispatchEx() 的参数为“PowerPoint.Application”，它的功能是让 win32com 库打开 PowerPoint 进程。另一个是 ppt.Presentations.Open() 打开的对象不同，它打开的是每一页的 PPT，而 Excel 打开的是每个 sheet 对象。

以上就是如何将 Office 组件的 Word、Excel 与 PowerPoint 的默认保存文件格式转换为 PDF 格式。在转换之后，我们经常为了保护知识产权，或者增强文件的安全性，需要给 PDF 文件增加水印。所以接下来我就带你学习如何通过增加水印提高 PDF 文件的安全性。

提高 PDF 文件的安全性

安全性包括很多方面，比如为文档增加密码，从而增强保密性；也可以为文档增加水印，提升它的不可伪造性。如果你对前面课程熟悉的话，就能联想到我们在第 27 节课讲过，可以利用我们自动压缩的方式来给文件增加密码，所以我在今天这节课，主要以如何给 PDF 文件增加水印。

为 PDF 增加水印

为 PDF 文件增加水印，你可以通过 pyPDF2 库来实现。使用 pyPDF2 库的好处是，你不需要在当前计算机上安装任何 PDF 编辑器，就可以给 PDF 文件增加水印。

基于 pyPDF2 库来给 PDF 文件增加水印的原理是，把只带有水印的 PDF 文件，和需要增加水印的 PDF 文件合并即可。根据这一原理，你大概就能想到增加水印的步骤了。主要有三步，分别是：准备水印文件、安装 pyPDF2 库以及合并两个 PDF 文件。

老规矩，我们还是先从准备水印文件开始学习。带有水印的 PDF 文件，可以使用 Word 软件来生成。具体操作方法是：通过 Word 的“设计”-“水印”功能，来定制你自己的水印文件，接着再把它另存为 PDF 格式，之后这个带有水印的文件你就可以反复使用了。

接下来是安装 pyPDF2 的第三方库。由于它的安装包和软件同名，所以可以使用 pip 命令直接安装。安装后，我需要通过该库实现 PDF 文件的读写。我使用了这个库用于读写 PDF 文件的“PdfFileReader, PdfFileWriter”两个包，导入的代码如下：

from PyPDF2 import PdfFileReader, PdfFileWriter

第三步，也是最重要的一步，合并两个 PDF 文件。合并文件需要使用 pyPDF2 库的mergePage() 函数 ** 实现。在实际工作中，我们通常需要给 PDF 文件的每一页都增加水印，此时我们需要使用 for 循环来迭代每一页，迭代之后，再把合并好的 PDF 文件保存为新的文件即可。

我把合并两个 PDF 文件的代码写在下面，然后再带你分析整个合并流程。

from PyPDF2 import PdfFileReader, PdfFileWriter

def watermark(pdfWithoutWatermark, watermarkfile, pdfWithWatermark):

    # 准备合并后的文件对象
    pdfWriter = PdfFileWriter()

    # 打开水印文件
    with open(watermarkfile, 'rb') as f:
        watermarkpage = PdfFileReader(f, strict=False)

        # 打开需要增加水印的文件
        with open(pdfWithoutWatermark, 'rb') as f:
            pdf_file = PdfFileReader(f, strict=False)

            for i in range(pdf_file.getNumPages()):
                # 从第一页开始处理
                page = pdf_file.getPage(i)
                # 合并水印和当前页
                page.mergePage(watermarkpage.getPage(0))
                # 将合并后的PDF文件写入新的文件
                pdfWriter.addPage(page)

    # 写入新的PDF文件
    with open(pdfWithWatermark, "wb") as f:
        pdfWriter.write(f)

在这段代码中，我定义了函数 watermark()，用来实现为 PDF 文件增加水印的功能。它的实现思路是先读取水印 PDF 文件，再把水印 PDF 文件与需要增加水印的文件的每一页进行合并。合并之后，我通过使用 PdfFileWriter() 类，产生了新的对象 pdfWriter，并将合并后产生的新 PDF 文件存储在 pdfWriter 对象中。最后，在所有页处理完成后，将合并后的 PDF 文件保存到“pdfWithWatermark”对象指向的文件中。

在这段代码中你需要注意的是，我使用了“with 方式”打开了文件，在文件处理完成前如果关闭文件的化，会出现“file close error”异常。因此你需要注意代码第 9、13 行的 with 缩进，而写入新的文件可以在水印 PDF 文件和要增加水印的文件关闭后进行，所以代码的第 25 行“with 语句”缩进可以在它上面的两个 with 代码块以外进行编写。

为了让你更直接地感知到增加水印后的结果，我把增加水印后的结果贴在下方，供你参考。

以上就是我使用 PyPDF2 库为 PDF 增加水印的全部流程。不过除了增加水印外，你还能使用 pdfWriter 对象，来实现很多实用的功能，比如为 PDF 文件设置密码、插入新的页面、旋转 PDF 页面等等。

此外，由于 pyPDF2 库封装得非常好，所以它的调用很简单，你只需一个函数就能实现我刚才提到的这些功能了。我将 pyPDF2 库的官方文档链接（https://pythonhosted.org/PyPDF2/）贴在这里，当你需要操作 PDF 文件实现其他功能时，可以参考官方文档中 PdfFileWriter() 函数的参数，为不同的功能增加相应参数即可。

小结

最后，我来为你总结一下今天的主要内容。在本讲中，我为你讲解了如何通过 pywin32 库把 Offce 组件常用的 doc、docx、xls、xlsx、ppt、pptx 等文件转换为 PDF 文件格式的通用方法。这个通用的方法就是通过 pywin32 库的 COM 编程接口，调用 VBA 脚本实现格式的转换。

你学会了 pywin32 库之后，除了能把这些办公文件转换为 PDF 文件外，还能对 Office 组件中的任意一个软件进行常见功能的调用，因为 **pywin32 调用的 VBA 脚本和 Office 宏的脚本是完全相同的。

我在本讲中除了为你讲解了 pywin32 库外，还讲解了 pyPDF2 库。pyPDF2 库能够在你将文件转换为 PDF 之后，还能对 PDF 的格式和内容进行微调，让你的 PDF 文件批量处理能达到手动处理的文件精细程度。

最后，你还可以把 PDF 文件和上一讲中的自动收发邮件，以及我们学习过的 Word 自动处理相结合，把 PDF 格式的合同作为邮件附件进行文件的自动生成和邮件的自动发送。

思考题

按照惯例，我来为你留一道思考题。如果在一个文件夹中既有 Word 文件，又有 PowerPoint 文件，你该如何将文件夹中的这些类型的文件，批量转换为 PDF 文件呢？

结束语｜和我一起成为10X效率职场人

你好，我是尹会生。

这是咱们的最后一节课了，在前面的课程中，你跟着我学了 30 种提高办公效率的方式，那么现在回过头来，我想请你再思考一下：你认为什么样的办公方式效率是更高的呢？可以把你的想法写在评论区，我们一起互动交流。

我先来分享一下我的想法，聊聊我在这么多年的职场工作中，对办公效率的一些思考。

什么样的办公方式效率更高？

我对办公效率的理解经过了两个阶段，也可以称之为两个层面。

第一个阶段是多写非交互程序，让计算机能在没有人工干预的情况下，运行主要工作。

那时，我在一线做运维工程师，大部分时间我都在和服务器打交道，而且是海量的服务器。为了能批量控制服务器上的软件，我就要用各种编程技巧来让脚本批量自动运行。

所以，在这个阶段，我所理解的效率呢，就是多编写脚本。这样原本需要人工来完成的工作，我都可以编写一个定时任务，让它们自动运行，从而让流程可控不出错，工作高效还省心。

但是，在我开始接触更深入的研发工作后，我对效率的认知就进入到了第二个阶段。只有提升项目全路径和全流程的效率，才能真正把办公效率提升 10 倍。

因为我发现，不同的软件架构有不同的业务特点，而它们又是相互依赖的，那么如果只针对某个业务进行提效，还是不能大幅度提升一个完整研发项目的落地效率。

换句话说，当你学会用某个编程技巧写出一个编程脚本时，你的工作效率会有所提升。但是如果项目的其他部分还是手动操作，那么对于整个项目来说，效率的提升是很微不足道的。

那你肯定会问了，怎样才能优化整个项目，来进一步提升办公效率呢？答案是用计算机思维。“思维”这个词可能乍一听，你会感觉很高级，学起来很难。其实不然。

我说的计算机思维，指的就是：当我们解决编程或计算机相关项目的时候，你可以将一个完整的任务，拆解成计算机可操作的基本单元。而这些基本单元，其实就是我在开篇词中所说的输入、运算、控制、存储、输出五个部分。当我们能够根据项目任务进行拆分，让一整套工作都能实现自动化的时候，工作效率也就能提升 10 倍，我们也就能成为 10X 效率的职场人了。

像我在第 29 节课讲用自动化发送周报时，提醒你可以把定时发送功能、编写周报模版等知识串联起来。这其实就是一个计算机思维的简单应用。

那你肯定会接着问了：那怎么才能有计算机思维呢？这正是我接下来想要分享的，计算机思维不是一朝一夕可以修炼成的，你需要走过三个阶段，这也正是从 1X 效率，到 10X 效率职场人的三个阶段。

成为 10X 效率职场人的三个阶段

在我看来，通过编程提高效率有三个阶段，那就是：先模仿，再理解，最后连接。

这么说可能有点抽象，我想结合一个运营的案例，来给你讲一讲每个阶段应该具体怎么去做。

在运营方法论中，有一个“漏斗模型”。假如我现在要用漏斗模型来分析一个游戏玩家的留存率，那怎么将这个过程进行自动化呢？

我们先来分析一下。在这个过程中，我需要采用漏斗模型来依次分析用户点击广告、用户下载游戏、用户登陆游戏、7 日留存和付费的情况。这就要用到咱们在课程中学过的利用 Python 来进行数据提取、数据处理和图形展示。

现在，我们来看怎么将整个过程自动化。

首先是第一个阶段：模仿，模仿高手的代码。

在模仿阶段，你可以模仿我在课程中讲过的代码。比如咱们学习过的利用 SQL 语句从数据库提取数据，存入 Excel 中；接着，可以用 Python 代替 Excel 处理人工观察数据，统计结果；最后，你可以把结果用 Seaborn 进行绘图并展示。

第二个阶段是理解，理解代码的底层逻辑。

在理解阶段，你需要通过计算机的原理，把数据提取、处理和展示，按照计算机的输入、处理、输出逻辑，拆解为这样几个步骤：

从数据库读取数据，这是输入；

在内存中处理数据，这是运算；

最后通过图形整理好数据，并按照字典、列表等数据类型输出成图表，这是输出。

你看，虽然这个场景我在课程中并没有讲过，但是通过拆解计算机的基本单元，你会发现内容都是相似的，也就可以举一反三了。

第三个阶段是连接，将手工的工作转变为由代码执行。

在连接阶段，你需要把工作中的需求在拆解计算机的逻辑基础上，再将它们转化为代码，通过编码来实现一整套工作的自动化。

就像我们刚才拆分的读取数据、处理数据和整体数据这样几步，我们可以将这些内容都整合在一起，转化为代码，让一整套工作都自动化。

看到这儿你肯定就明白了，提高办公效率，根本的解决方法不在于学习多少小技巧或小软件，而在于理解底层逻辑，加快人和计算机的交互过程，这样自然就能解决大部分的效率问题。

所以我提出这三个阶段，是希望你不仅能学会这 30 节课，还能掌握其中的思想和方法。问题千变万化，但计算机思维是不变的，这样你才能触类旁通，应对你遇到的绝大部分办公效率问题。

当然，这个目标不可能一蹴而就，你需要不断地学习、应用。我也想再给你分享两条学习这门课程的建议。

首先，如果你有时间，或者基础比较薄弱的话，我建议你去抄写每一段代码。要知道，看一节课和把看到的知识写出来的感觉是完全不一样的。

其次，在结课后，你还要学以致用。

“圣人之道无异于百姓之日用。”你在这门课中学到那么多东西，如果不用起来，那么最后的价值还是零。所以你需要把学会的知识、方法、代码，用到你的办公场景中，在提升工作效率的同时，也能锻炼提升效率的思维。

当你用得多了之后，再面对这些问题，你就会形成一种肌肉反应。这就像你虽然不记得键盘上从 D 到 P 中间都有哪些字母，但是你能在用到这些字母时，熟练地将它们输入进去一样。

有人说这是凭借经验不假，但是更本质的是，你需要有更洞察的思维。这是需要你把拼音、五笔烂熟于心才能做到的，而绝不是用什么样的指法敲击得更快。

我记得在第 19 节讲了怎么利用 Python 领取京东金豆时，有同学在评论区分享到，学会之后就可以去研究自动签到打卡了。你看，这就是一个很好的应用计算机思维的例子。

当你把自己的时间从重复的劳动解脱出来时，你会发现，自己的技术和问题处理能力都上了一大台阶，职场进阶速度会更快。此外，你也会更有时间、有心情去提升自己其他方面的能力，以及享受生活。提升工作效率，也就是在提升你的职场进阶效率，提高你生活的幸福指数。

最后，我也为你准备了一份毕业问卷，想听一下你对这门课的想法和建议。题目不多，希望你可以花两分钟填一下。十分期待你的反馈。