This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,127 @@
<audio id="audio" title="23怎么用数据透视表更直观地展示汇报成果" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/81/72/8144eddcbc8f4431dbbyye1234709c72.mp3"></audio>
你好, 我是尹会生。
从这节课开始我们将进入课程的最后一个模块“输出模块”的学习了。在这一模块中我们要把“控制、存储等模块”的数据以更加智能化的方式展现到你的面前。比如用图表、图形代替数字进行工作成果汇报利用压缩包、PDF把工作成果定时发送邮件等。这些都是你在输出模块能够学习到的自动化办公技巧。
通过对这些技巧的掌握,能够为你的工作成果输出带来更高效的处理方式,也能让你把工作成果输出这件事做得更出色。
那今天这节课,我想和你聊一聊怎样把你的工作成果更好地向领导汇报的问题。
你在工作中肯定遇到过这样的情况:精心处理了好几天的数据,需要把数据汇报给你的领导时,往往不知道采用什么样的格式更合适。比如只提交了整理过的原始数据,很容易被领导误认为工作只完成了半成品。而提交了最终结果时,领导又可能会提出合理的疑问,比如如果按月汇总、按某项汇总会得到哪些不同的结果等问题。这个时候,你就不得不再进行一次统计结果汇报。
那么今天这节课我就要给你介绍一款Excel中的插件叫做“Power Pivot”这个插件可以根据需要随时调整数据处理结果。而你可以利用“Power Pivot”生成自己需要的数据模型还能基于模型生成更灵活的、可随时调整的数据透视表。
等下次再遇到需要随时调整数据汇总结果的时候,你就可以直接拖拽想要的列,按月、按年、或按照任意你想要的字段进行数据聚合了。
不过在讲解之前考虑到你可能初次使用数据透视表我想先为你介绍一下如何将数据转换为数据透视表的操作步骤这样你才能更好的理解数据透视表展示的表格和数据之间的对应关系。在掌握了数据透视表的操作基础之上我再教你怎样使用“Power Pivot”插件来生成支持多表和自定义排序的数据透视表。
## 利用数据透视表进行灵活的数据分析
数据透视表能用来做什么呢?一句话来描述它,数据透视表是一种能够随意对数据明细表进行分类汇总,并能随时调整汇总结果的交互式报表。
可能这样描述它你仍然觉得不够直观还不能对数据透视表的作用有具体的感知。那么我就用一个Excel文件来给你演示一下怎么把数据表生成数据透视表并进行数据的统计和分析。
为了让你对数据透视表有个直观的印象我先把原始的数据表和制作成透视表的表格一起提供给你原始的数据表记录了5台计算机硬件负载情况的明细数据透视表按照每台计算机每种硬件在每小时的最大利用率进行展示。两张表格内容如下
<img src="https://static001.geekbang.org/resource/image/95/c6/955d8acb66123d7e344a6083dee15cc6.png" alt="">
<img src="https://static001.geekbang.org/resource/image/08/e1/08e7yy1427400227847fc9fdcd2759e1.png" alt="">
同时,我也把生成数据透视表后的截图,给你放在下方,供你参考:
<img src="https://static001.geekbang.org/resource/image/99/1a/99d82fca8fea04506b74e575f2d63f1a.png" alt="">
数据表中包含了1点-4点一共5台计算机的CPU、内存、磁盘利用率如果把数据交给你你能从中分析出哪些有趣的结果呢
那么我在这里用数据透视表提供两种将数据组合起来有意义的结果,它们是每台电脑每小时各种资源利用率的最大值,以及每台电脑每天资源利用率的平均值。
当然你还能用数据组合出更多有用且有趣的结果。那接下来我们就看看怎样把明细数据转换为数据透视表、怎样组合行和列,才能得到我刚才提到的两个有意义的结果。
### 使用数据透视表实现数据统计
使用Excel的数据透视表来实现数据统计有三个简单的步骤分别是数据预处理、生成透视表和调整透视表结果。让我们逐个来学习一下它们吧
**首先是数据预处理**它是输出数据的基础。数据预处理的目标就是把每一类数据放在单独的一列因为数据透视表只能支持这种格式的数据这样才能输出有意义的透视结果。另外你可以使用第13讲的“Power Query”对表格进行调整也可以使用咱们学习过的Python对Excel的数据内容进行调整。
我给你举一个调整好格式的例子帮你理解数据透视表能支持的数据明细的格式。比如刚才的计算机利用率的表格假设“pc-1”这台计算机的利用率明细表中包含“CPU”、“磁盘”和“内存”的利用率数据如果你需要使用数据透视表则必须为它建立“类型和利用率”两个列并把“类型和利用率”作为表头。
与此同时在类型列中把“CPU、内存、磁盘”作为数据在利用率中分别写入每种类型不同时间各自对应的利用率而不能把“CPU、内存、磁盘”作为三列把利用率作为每一列的数据。
如果你还是无法理解需要把数据处理成什么样的格式才能让数据透视表正常工作那我再给你提供一个既简便又不会出错的方法把第一列指定为时间而其他的列每一列存放一个类型的数据。把需要透视的数据按照时间递增依次填入到Excel中。
这种数据有一个专有的名称,叫做**时序数据**。数据预处理完成后,如果你的透视表不需要时间字段,可以将时间这一列的数据再删掉。
**接下来是生成透视表**。数据准备好之后,下一步就是在新的工作表中插入数据透视表了。插入数据透视表的命令在“插入”菜单栏的“表格”命令组,你只要找到“数据透视表”按钮,点击后就会打开创建数据透视表的菜单了。
在这一步骤,有两个选项你需要格外关注一下。
- 一个是“选择要分析的数据”选项Excel会自动为你选择当前表中的所有数据如果数据表中的数据都是你要分析的内容那你可以不做修改。如果你只希望将表格部分数据制作成数据透视表可以根据自己的需要选取数据明细表中的具体数据范围。
- 另一个是“请选择放置数据透视表的位置”选项,我们可以把透视表放在“新工作表”中。可以保持原始数据不被破坏外,便于你再次利用原始数据做其他的数据处理。
这两个选项一般保持默认即可。同时,我也把创建透视表的截图放在下方,供你参考。
<img src="https://static001.geekbang.org/resource/image/48/c9/480b3c6bbdb7bb56270e624868f2b8c9.png" alt="">
**最后是调整透视表结果**。创建好默认的数据透视表之后,你会发现它的内容是空的,这时只需要把“报表的字段”**拖动到透视表区域**就行了。同样的,我先给你看一下截图,帮你更直观地感知数据透视表的操作。
<img src="https://static001.geekbang.org/resource/image/e4/8a/e4effabd5a9600fa469c224a521d588a.png" alt="">
在截图的左侧是透视表的结果而右侧就是透视表的控制区域你需要根据自己的分析需求把字段拖入到右下角的“透视表区域”包括“行、列、值”三个方框。例如我希望得到每台电脑的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”加载多张表的方式是使用“添加到数据模型”按钮。点击按钮后会弹出“创建表”窗口。这个窗口可以选择两张表中的其中一张先作为数据模型。如图所示
<img src="https://static001.geekbang.org/resource/image/64/7e/64a66179c5ee8cafaacc317633a7cd7e.png" alt="">
图片中“创建表”窗口会默认把当前表全部选中点击“确定”后会弹出“PP”的工作窗口。在这一窗口下你可以对数据进行筛选和处理。
**接下来我再导入另一个工作表****。****导入更多表的操作方法和导入当前工作表是相同的。**这样在“PP”工作窗口的左下角就会出现两张要进行数据透视的表格。需要说明的是“PP”还能支持从其他数据源导入数据例如可以从数据库或“Access”导入数据和Excel中的数据一起进行数据统计。
**最后生成由“PP”制作的数据透视表。你在“PP”窗口的任意一张表点击“数据透视表”按钮之后**,就可以创建新的数据透视表了。这时你会发现,数据透视表字段的右上方出现了两张表,你可以把这两张表按照需求拖入到右下方的行、列和值,从而实现多表的数据统计。我把截图放在下方供你参考。
<img src="https://static001.geekbang.org/resource/image/f2/9c/f24a28001d02c836bf673ee696f2f69c.png" alt="">
通过使用“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表格才能通过数据透视表进行数据分析呢
欢迎把你的思考和想法放在留言区,我们一起交流讨论。如果这节课学习的数据透视表对你的工作有帮助,也欢迎你把课程推荐给你的朋友或同事,一起做职场中的效率人。

View File

@@ -0,0 +1,174 @@
<audio id="audio" title="24条形、饼状、柱状图最适合用在什么场景下" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/20/f5/20950ae917242932af04012fc9e60cf5.mp3"></audio>
你好,我是尹会生。
提起图表你一定会想到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我就再为你提供一个示例数据的镜像站。除了鸢尾花数据外镜像站这里还包括房产价格预测等经典示例数据你可以利用它们来学习不同的图表。我把[示例数据的地址](https://codechina.csdn.net/mirrors/mwaskom/seaborn-data?utm_source=csdn_github_accelerator)放在这里,另外,我把部分示例数据也贴在下方,供你参考:
```
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=&quot;darkgrid&quot;, 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()函数进行图形的绘制,那么鸢尾花数据的散点图绘制结果如下:**
<img src="https://static001.geekbang.org/resource/image/4c/e6/4c942198475e24b0f5e7448c39a126e6.png" alt="">
在截图中,基于花的四个属性,我采用了不同的维度进行绘图。同时你会发现,在某一维度下,其中一种颜色和其他颜色有明显的分界,非常容易把其中一种和另外两种花区分开。
通过观察散点图你会得到这样一个结论使用合理的图形能够帮你更好地解释某个晦涩难懂的概念也能更容易从数据中发现规律。那既然不同的图表能带来不同的价值接下来我就来为你讲解一下如何使用seaborn生成其他类型的图表比如可以通过histplot()函数生成柱状图、heatmmap()生成热力图、kdeplot()生成核密度图等等。
### 用seaborn生成不同类型的图表
要想使用seaborn生成其他类型的图表你需要学会如何使用官方文档。我以折线图为例为你讲解一下官方文档的正确用法。
在[seaborn的官方文档地址](https://seaborn.pydata.org/api.html)API页面下所有的图表都先按照不同的用途进行了分类折线图在表示关系的分类中你可以参考如下截图。
<img src="https://static001.geekbang.org/resource/image/65/37/651e97ffd566d55671be4678c8d36437.png" alt="">
当你需要绘制折线图时,可以**点击“lineplot”进入折线图的函数解释网页**。它的网页采用了和Python官方文档风格一致的API解释方法即函数定义、一般场景案例和特殊场景案例。
如果你是第一次使用折线图,那你可以按照一般场景案例、函数定义和特殊场景案例的顺序来学习这个函数。如果你对折线图已经有了较多的使用经验,可以从函数定义,按照网页顺序阅读官方文档。为什么要按照这样的方式来学习呢?
在你对某一图表有了初步的使用经验后,会对该图形的样子有一个感性的认识,这时候再通过函数的定义、参数去学习它们,会比通过一般场景案例来学习的效率更高。而且通过学习函数的参数,能够了解哪些技术点会影响图形的展示。
而对于第一次使用某一图表的话,你没法通过图表的名字想象出这类图形的优缺点,因此我会建议你对初次使用的图形,先按照一般场景案例把图形展示出来,有个直观的印象。
不只是seaborn的文档在学习其他库甚至Python语言或其他任何编程语言都需要通过阅读官方文档来掌握扩展知识。而阅读官方文档最佳的时机是当你掌握了该软件的基本应用之后例如在你掌握seaborn的散点图以及它的基本运行过程之后这时你就需要通过官方文档的学习来掌握更多的图表。当你掌握足够多的图表后用seaborn绘图才能更加得心应手。
## 为不同的应用场景选择合适的图表
由于seaborn支持的图表非常丰富在有经验的开发工程师进行图表选择时绝不会逐个尝试。他们会根据图表的应用场景来选择适合的种类再通过适合的种类再细化到图表的具体样式。
但是你可能并没有使用过seaborn的图表甚至也不了解图表会有多少种类型、每种类型里包含着哪些具体的图表。因此根据是否有图表的使用经验你可以按照我给你提供的两种方法来根据工作场景找到最适合你的图表。这两种解决办法总结来说就是参考图例和参考分类。
第一种解决办法是参考图例,我把这种情况称作是**“手中有剑、心中无剑”**,“**手中有剑”代表着你能看到图表一共有哪些,但是心中还不清楚哪种更适合你的场景**。在seaborn的官方文档中列举了各种图例它的[地址](https://seaborn.pydata.org/examples/index.html)和截图如下:
<img src="https://static001.geekbang.org/resource/image/b3/79/b38fc214255db346fba37a6f7b15cf79.png" alt="">
你可以根据截图,找到离你的工作场景最相近的图表,通过点击图表之后,你就可以得到官方网站的演示代码了。演示代码就是你的“宝剑”。通过修改演示代码来完善你的工作场景的图表。
第二种解决办法是参考分类,我把这种情况称作“心中有剑,手中无剑”,“心中有剑”也就意味着你在心里已经**<strong>把**</strong>应用场景锁定在某一个图表的大类中但是这一大类里包含了哪些具体的图表要看seaborn能否支持。
这时候你就要根据你的业务场景分析出它都对应了以下四个分类中的哪一类再按照分类通过官方文档API页面找到具体的图表函数。四个分类如下。
1. 关系类用于展示数据集中多个变量之间的关系relplot()、scatterplot()、lineplot()都属于关系类。
1. 分布类用于展示数据集中多个变量的分布情况displot()、kdeplot()是这一类经常使用的图表类型。
1. 线性关系类是把多个变量联系起来观察每个采样的线性变化趋势。regplot() 和 lmplot()经常用于表示线性关系。
1. 结构化多图,用于把多种方式的分析数据放在一起进行展示。例如我们分析鸢尾花就使用了散点图+柱状图的方式,但是散点图更能体现出它的各种属性之间的关系。
你在心中掌握的图表分类就是“宝剑”,通过分类能够更快找到特定的图表类型。
这两种方式是基于不同场景,快速选择图表的方法。因为选择图表最核心的思路还是要基于场景,而不能基于个人的喜好或结果的美观性来选择图表,避免以偏概全。
## 小结
最后我来为你总结一下这一讲的主要内容。在本讲中我通过seaborn生成图表的过程为你讲解了如何在Python中使用图表。相对于其他软件Python的图表样式由参数组成你可以为多次产生图表指定相同的样式、也能为不同的数据重复使用图表来提高绘制图表的效率。
在为你讲解了散点图之外,我还为你讲解了如何基于场景选择合适的图表,你可以基于目前对图表掌握的深度,选择更适合你的图表深入学习路线。
同时我还着重为你强调了文档的重要性它也是很多专业从事开发的工程师必需掌握的技能之一。如果你希望更加深入学习seaborn以及更加深入学习Python你应该从现在开始阅读官方文档它会是你未来编写代码最权威的参考资料。
## 思考题
我来为你留一道思考题如果我的工作场景需要展示当前地区的房价走势你会选择什么样的图表进行展示呢你能否用seaborn将这一图表绘制出来呢
欢迎你把思考和想法分享在留言区,我们一起交流讨论。如果今天的内容对你展示工作成果有帮助,也欢迎你把课程分享给你的同事和朋友,我们一起做职场上的效率人。

View File

@@ -0,0 +1,243 @@
<audio id="audio" title="25图表库想要生成动态图表用Echarts就够了" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/9d/90/9d651ed8bbc2dc5ca66dc0f2f4006e90.mp3"></audio>
你好,我是尹会生。
在上一讲中我们学习了怎么使用Seaborn来生成图片格式的图表。事实上图片格式的图表也被称作静态图表它能通过数据来更直观地展示结果。
不过很多时候,我们不仅要通过图片直观地展示数据,还要让图片容纳更多种类、更丰富的数据信息。这个时候,静态图表能展示的结果就十分有限了。比如你希望能给领导和同事在会议上演示数据的分析结果时,需要通过一张图来容纳更多的数据。
别担心,这时候我们可以**采用动态图表的方法,来增强图片的表现力**。因为动态图表展示的结果,相当于静态图表和数据这两者的混合,所以容纳的内容信息也就更丰富。
举个例子,我希望用一张图片来展示全国新冠确诊病例的分布。如果采用动态图,我就可以把鼠标移动到我需要查看的省份上面,显示该地区的确诊人数等相关信息。
就像下面这张截图一样。这张分布图不但基于颜色深浅显示了确诊人数的变化,还能通过鼠标悬停来显示具体的数据。使用起来是不是很方便?
<img src="https://static001.geekbang.org/resource/image/60/01/6063f764e3493cb69b2922b1988dcf01.png" alt="">
这张动态图表是使用HTML网页文件格式来展示的。同时它也采用了Python的库pyecharts进行了绘制其中的图形、数据都可以基于你的需要进行调整。最重要的是绘制这样一张图片操作起来和seaborn生成静态图表一样简单。
那接下来,我就为你具体讲解一下**怎么<strong><strong>使用pyecharts来**</strong>绘制疫情实时地图</strong>,并以此为例,让你掌握**怎么通过<strong><strong>pyecharts来**</strong>绘制其他的动态图</strong>
要想**使用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()”函数把列表类型转换为元组。
确定数据格式之后,我们还需要一个**数据来源**,为了确保数据的准确性和实时性,我们得从腾讯新闻的网站引入外部数据,[数据地址](https://view.inews.qq.com/g2/getOnsInfo?name=disease_h5)的链接我先贴出来,接下来我再给你讲解一下怎么得到数据地址。
为了得到数据的地址,我分析了网站加载数据的过程,找到了数据接口的地址。这个分析方法你学会以后,也可以应用到其他需要抓取网页数据的工作中。我把抓取的步骤分成四步,分别是开启浏览器调试、请求网页、确认接口和确认返回数据。
先看第一步,**开启浏览器调试**。这一步骤是为了在请求过程中,能记录网页都请求了哪些数据接口。
以Chrome浏览器为例使用快捷键F12可以打开调试模式把选项卡调整至“Network”调整后就进入接口的监听状态了。
第二步,**请求网页**。以腾讯疫情实时网页为例,可以在地址栏输入“[https://news.qq.com/zt2020/page/feiyan.htm#/](https://news.qq.com/zt2020/page/feiyan.htm#/)”,输入后,调试界面会显示该网页都请求了哪些地址。我把调试页面放在下方供你参考。
<img src="https://static001.geekbang.org/resource/image/c7/44/c7349yyae70d9bcf369e95644fe4f344.png" alt="">
截图的左侧就是请求的所有数据接口的地址从最开始请求的地址向下找除了JS、JPG等网页图片和样式数据外其他的请求接口就是我们要重点查看的接口。
第三步,**确认接****。**由于网页中包含了多次从“getOnsInfo”接口取得数据所以要逐个查看接口的返回数据。你可以通过鼠标点击接口的链接然后再点击“Response”按钮最后根据返回接口的返回内容查看是否为“疫情实时数据”。
第四部,**确认返回数据**。当你从接口初步确认了该数据是“疫情实时数据”后,可以把该地址复制到浏览器中,进行访问。访问的具体办法就是通过模拟网页请求接口,这样就可以得到以下数据了。我将数据放在截图中,供你参考。
<img src="https://static001.geekbang.org/resource/image/99/e5/99040018387067600f77ae5d603e4fe5.png" alt="">
截图中的数据,可以通过查找省份和数据来确认数据的正确性。如果数据不正确,你需要回到第二步,再重新找下一个接口。
### 调整数据格式
当你确认了数据格式和数据源之后接下来就需要把数据源的格式转换为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的官方网站](https://echarts.apache.org/examples/en/index.html)。
Echarts的图表指定函数和pyecharts相同找到你需要的图例函数之后就可以拿到pyecharts中直接使用。
那针对老手的图表分类和API可以[参考](https://gallery.pyecharts.org/#/README)[这个地址](https://gallery.pyecharts.org/#/README)。以最常用的图表,折线图为例,你可以打开[地址](https://gallery.pyecharts.org/#/Line/temperature_change_line_chart),其中会包括图表的完整调用代码、测试数据和图例,通过参考示例可以让你掌握更多类型的图表。折线图的截图如下:<br>
<img src="https://static001.geekbang.org/resource/image/b0/06/b018bf7f0d7b34bfdce2c89fd8c8a006.png" alt=""><img src="https://static001.geekbang.org/resource/image/06/65/06926268b01a96be2aaedaa2720d2d65.png" alt="">
再让我们回到疫情地图的案例中由于我们需要绘制中国地图因此直接使用pyecharts库的Map()类,它是绘制动态地图的类。它的[官方网站](https://gallery.pyecharts.org/#/Map/map_base)链接我贴在这里。
通过参考官方网站的案例代码我们可知在将Map()类实例化之后进行绘图时调用了add()、set_global_opts()和render()三个方法,它们分别是增加数据、设置样式和渲染。我们来依次学习一下。
<li>
add()方法用来为图表增加图表名称、加载数据、指定地图的区域其中加载数据的参数我使用了tuple()函数,可以直接把列表转换为元组,这样在查找效率上会比列表更高。
</li>
<li>
set_global_opts()方法用来指定图表的样式和格式这里我做了两个格式的调整操作它们是图表的标题和图表的颜色。图表的标题部分我将“数据最后更新时间”作为图表的标题是通过字典“alldata['lastUpdateTime']”实现的。图表的颜色是通过pieces参数实现的根据不同省份确诊人数在什么数量范围显示该省份的颜色深浅。颜色越深表示当前周期采集到的确诊人数越多。
</li>
<li>
render()方法用来指定图表保存的网页路径和名称。这里需要注意的是add()和set_global_opts()方法如果出现错误并不会马上报错只有在调用render()方法时才会出现错误。所以一旦出现错误你应该从导入数据的格式、add()和set_global_opts()方法参数中检查。
</li>
执行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(
&quot;全国确诊病例分布图&quot;,
tuple(chinadata),
&quot;china&quot;,
is_map_symbol_show=False
)
map_chart.set_global_opts(
title_opts=opts.TitleOpts(
title=f&quot;全国疫情地图( {alldata['lastUpdateTime']} )&quot;),
visualmap_opts=opts.VisualMapOpts(
is_piecewise=True,
pieces=[
{&quot;min&quot;: 1, &quot;max&quot;: 9, &quot;label&quot;: &quot;1-9人&quot;, &quot;color&quot;: &quot;#FFE6BE&quot;},
{&quot;min&quot;: 10, &quot;max&quot;: 99, &quot;label&quot;: &quot;10-99人&quot;, &quot;color&quot;: &quot;#FFB769&quot;},
{&quot;min&quot;: 100, &quot;max&quot;: 499, &quot;label&quot;: &quot;100-499人&quot;, &quot;color&quot;: &quot;#FF8F66&quot;},
{&quot;min&quot;: 500, &quot;max&quot;: 999, &quot;label&quot;: &quot;500-999人&quot;, &quot;color&quot;: &quot;#ED514E&quot;},
{&quot;min&quot;: 1000, &quot;max&quot;: 9999, &quot;label&quot;: &quot;1000-9999人&quot;, &quot;color&quot;: &quot;#CA0D11&quot;},
{&quot;min&quot;: 10000, &quot;max&quot;: 100000, &quot;label&quot;: &quot;10000人以上&quot;, &quot;color&quot;: &quot;#A52A2A&quot;}
]))
map_chart.render('covid19_map.html')
```
## 小结
今天的主要内容就这么多,我来为你最后总结一下本讲的主要内容。
在本讲中我通过pyecharts展示了疫情实时信息的动态图表并为你介绍了图表的制作方法。对比静态图表动态图表的制作方法要复杂但是一张图能容纳的信息也要比静态图表多你需要根据自己的工作场景合理的选择图表种类。
同时你还会发现图表的绘制难点在数据格式的处理上通过网络采集数据往往要经过从JSON格式到字典再到元组的嵌套图表绘制过程可以重复利于的部分就是图表的创建过程包括数据添加、样式设置和渲染。掌握这些通用的使用原则可让让你从熟练的操作中加快自动办公的效率。
而你掌握的图表越丰富,在进行工作汇报和演示时,能够通过图形表达的信息就越清晰。这是我建议你能够掌握更多图表类型的原因。
## 思考题
今天的思考题是个开放的问题在你使用pyecharts绘制的图表需要每天更新时如何自动删除上一个生成的文件那有没有办法让网页自动更新呢
欢迎你把思考和想法分享在留言区,我们一起交流讨论。如果今天的内容对你展示工作成果有帮助,也欢迎你把课程分享给你的同事和朋友,我们一起做职场上的效率人。

View File

@@ -0,0 +1,246 @@
<audio id="audio" title="26快速提取图片中的色块模仿一张大师的照片" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/79/ac/79455c6d804a0bb7513084de8b72feac.mp3"></audio>
你好,我是尹会生。
当你进行工作汇报使用各种图表时,除了要考虑精确的数据、正确的结论外,**合理的配色方案也是精确表达数据的重要部分。**
正确的配色不但能让用户更容易get到你演示产品的商业用途还能为你的数据增光添彩。而不合理的配色不但会降低用户对你演示产品的兴趣还容易产生误解。
因此你**要为你的数据搭配上合适的色彩方案,来增加数据的表现力**。那么你要不要找专门的美工或自己分析那些商业模版用了什么配色呢?其实这两点都不需要,有很多设计高手和名画大师已经为我们提供了太多经典的作品了,我们只需要使用相似的配色,将这些配色应用到你的演示图片和文稿中,就能实现非常好的配色方案了。
我今天就来教你一种利用Pillow库自动分析图片中出现最多的颜色并自动提取出来的方法。当你再遇到那些为配色发愁的工作场景就可以找一个你喜欢的商业模版进行模仿了。
在本节课我打算使用一幅莫奈的名画《日出·印象》来教你如何使用Pillow库提取图片中被用到最多的五种颜色并将它们作为色块标注在图片上。下面的截图就是我们要实现的最终效果接下来我就教你如何来实现它。
<img src="https://static001.geekbang.org/resource/image/07/4c/079d7c9459a9cd5770919e2923eab54c.png" alt="">
## 安装Pillow
在进行图片的大小、格式以及颜色调整时我们通常选择Pillow它是Python用于图片处理的第三方库。
那我们就看看怎么安装Pillow。它的安装包是同名的包但是作为库导入的时候需要使用PIL关键字这是它和一般第三方库差异最大的地方。我把它的安装和导入命令写在下方供你参考。
```
SHELL$ pip3 install pillow
SHELL$ python3
python3&gt; import PIL #注意导入的库的名称
```
安装完成后,我们就可以提取图片中使用最多的五种颜色了。不过别心急,在提取色块之前,有一个步骤非常重要,那就是把图片颜色转化为数据。为什么要进行这一步呢?
这主要是为了方便接下来提取色块的操作。你想啊,我们的目的是提取图片中的颜色,而颜色的“多”和“少”,是需要“数出来”的,所以我们就需要把图片转换为可以量化的数字才行。
那么接下来,我就教你怎么把图片转化为数据,并统计出一张图片都有哪些颜色,以及每种颜色的数量。
## 把图片颜色转换为数据
要想转换图片颜色为数据,并统计它用了多少种颜色,以及哪种颜色用得较多,你要先理解计算机是如何存储一张图片的。
举个例子你会更好理解。假如说你想知道一个硬盘能够存多少张图片,肯定得先知道磁盘的大小,除此之外还要知道图片的大小。与此同时,为了比较两者的大小,你还需要知道它们的大小单位,进而得知一个硬盘里最多能容纳的图片数量。
那我们用来表示一张图片内颜色数量的单位是一种称作位bits的单位。每一“位”对应着一个像素的颜色值而这些像素则按照一定的顺序排列就形成了数字图像。
计算机可以使用一“位”或者多“位”来组成一个像素图像的色彩越丰富每个像素使用的“位”就越多。为了方便表示“位”的数量我们经常会使用一个叫做“位深度“的概念来表示当前图像的颜色丰富程度。像是我们常说的8位颜色的图片那么“位深度”就是8而每个像素可以用2的8次幂来表示也就是256种颜色专业术语叫做256种灰度等级
8“位深度”的图像经常用于图片色彩提取和图片预览。不过我们经常见到的图片都是24“位深度”的这是因为它能表示2的24位颜色可以把红、绿、蓝RGB三基色以2的8次幂表示出来这些颜色已经超过了人眼能分辨的颜色数量所以也被称作真彩色多用于图片的展示。
但是24“位深度”的图像不适合统计颜色数量因为24位和8位图像存储颜色的方式不同导致前者在计算准确性和计算性能上都没有8位图像处理起来方便
<li>
在计算准确性上24位图像保存的数据多所以在进行像素与像素之间的计算中会产生浮点数出现计算数据溢出的情况并因此导致计算的结果异常。
</li>
<li>
在计算性能上8位深度图像和24位表示颜色的方式不同。因为在8位深度的图像中有一个“调色板palette”的概念这一概念只在8位图像才有16位以上就都把图像颜色记录到图片自身的数据中了。所以**8位图像要想表示一个具体的颜色需要通过调色板中记录的颜色模版和图片中的模版索引计算之后才能得到****<strong>而**</strong>针对**<strong>24位图像**</strong>,就可以**直接<strong><strong>把**</strong>具体的颜色写入到了图片中</strong>
</li>
我给你举个例子例如我把调色板保存在一个列表中那么由于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(&quot;./文章26代码/sunrise.jpg&quot;)
# 模式P为8位深度图像每个像素用8个bit表示
image_p = image.convert(
&quot;P&quot;, 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值的对话框贴在下面供你参考。
<img src="https://static001.geekbang.org/resource/image/ae/64/ae0881eaee48a24322850e7b2b3a6064.png" alt="">
### 查表取出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(&quot;./文章26代码/sunrise2.jpg&quot;)
image.show()
```
这段代码展示了合并后的图像并把图像保存成“sunrise2.jpg”文件。
在这段代码中最主要的功能是就是使用“paste()”方法按照颜色和位置进行图像绘制。这幅画使用最多的五个颜色是使用for循环迭代从colors变量取出的为了避免每次与图片合并的正方形色块被覆盖我把它的输出位置每执行一次向右侧移动120个像素。所以我使用了“enumerate(colors)”形式读取colors变量的值。在遍历过程中i的值是从0开始每执行一次i的值+1,val的值每次为colors迭代的RGB颜色元组。
代码最后两行是对新生成的图片进行保持保存,以及在当前运行界面显示图片的操作。通过观察图片,我们发现提取的五个颜色刚好和主色调吻合。
我把合并好的图片和完整代码放在下面,供你参考。
<img src="https://static001.geekbang.org/resource/image/07/4c/079d7c9459a9cd5770919e2923eab54c.png" alt="">
```
from PIL import Image
# pip3 instlal pillow
# 打开图片文件
image = Image.open(&quot;./文章26代码/sunrise.jpg&quot;)
# 模式P为8位深度图像
image_p = image.convert(
&quot;P&quot;, 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(&quot;./文章26代码/sunrise2.jpg&quot;)
image.show()
```
## 小结
最后让我来为你总结一下本讲的主要内容。在本讲中我通过Pillow带你学习了如何提取图片中出现最多的像素并将像素的RGB值进行打印的功能。
通过对提取名画的颜色主题功能你还可以将提取颜色应用于商业模版或任何你感兴趣的颜色搭配上。利用更讨喜的颜色搭配来为你的PPT增色添彩。
本讲中除了让你掌握如何使用Python对图像操作外更想让你明白在计算机中对很多非数字化的元素也是可以进行计算的当然计算的前提是将它们转化成可量化的数字像是深度学习领域中对语音、文字、图像的处理都是以将它们数字化为前提进行的。因此当你遇到非数字化的对象需要进行自动化操作时也可以使用将非数字化转化为数字化的思路去解决办公难题。
## 思考题
按照惯例,我来为你留一道思考题,如果有多张图片需要提取其中的主要颜色,你怎样自动化实现它们的颜色提取和文件保存操作呢?
欢迎把你的思考和想法分享在留言区,我们一起交流讨论。如果今天的内容对你展示工作成果有帮助,也欢迎你把课程分享给你的同事和朋友,我们一起做职场上的效率人。

View File

@@ -0,0 +1,230 @@
<audio id="audio" title="27zipfile压缩库如何给数据压缩&加密备份?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d8/08/d82cb048fd6c33a1ce5aa829a524ed08.mp3"></audio>
你好,我是尹会生。
你在日常工作中,肯定和压缩文件打过交道,它们能把文件夹制作成一个体积更小的压缩文件,不仅方便数据备份,还方便作为邮件附件来传输,或者与他人共享。
但是如果你需要每天都进行数据备份或者把压缩包作为每天工作的日报发送给领导你肯定希望它能自动化的压缩。面对这个需求我们同样可以通过python来解决。我们可以用Python来自动压缩文件夹并为压缩包设置密码保证备份数据的安全。
在Python中要想实现数据的压缩一般可以采用基于标准库zipfile的方式来实现也可以采用命令行方式来实现。
当我们希望能够用Python自动压缩一个无需密码保护的文件夹时可以通过zipfile来实现它的好处是使用简单而且不用安装任何的软件包就能制作出“zip”格式的压缩包。不过zipfile没法对压缩文件进行加密因此当你需要对压缩文件加密时还需要调用可执行命令。
这两种实现方式就是我们今天要学习的重点了,接下来我们分别看一下这两种方式的具体操作方法。
## 使用zipfile实现无密码压缩
如果我想要把“C:\data\”文件夹压缩为“当前日期.zip”文件就可以使用**目录遍历、按日期自动生成压缩包的文件名、<strong><strong>把**</strong>文件夹写入压缩文件**<strong>这**</strong>三个步骤来实现。</strong>
### 目录遍历
**我们先来学习<strong><strong>怎么实现**</strong>目录遍历功能</strong>。我在第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语句呢
原因就在于,**<strong>一个函数如果使用yield语句来返回的话这个函数则被称作生成器**</strong>。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 = &quot;zip&quot;
return f&quot;{basename}.{extname}&quot;
```
在这段代码中“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&quot;C:\data&quot;
# 要备份的文件
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外还需要知道它的**执行方式和执行的参数。**
**我先来带你学习一下执行方式。<strong>7zip软件Windows安装成功后它的命令行可执行程序叫做“7z.exe”。但是它想要在命令行运行的话需要指定程序的完整路径。例如“c:\path\to\installed\7zip\7z.exe”。如果你希望在命令行直接输入“7z.exe”运行需要你把可执行程序放在命令搜索路径中。我在这里有必要为你解释一下**命令搜索路径</strong>的概念,有助于你以后在各种操作系统上执行命令行工具。
一条命令要想运行,必须要使用**路径+可执行文件的名称**才可以。例如我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且不输入路径那么根据上面的分析现在有两种解决办法。
1. 把7z.exe放到现有的命令搜索路径中例如“C:\python3.8\scripts\”文件夹。
1. 把7z.exe所在的文件夹“C:\7zip\”加入到命令搜索路径Path变量的参数中。加入的方法是在Windows的搜索栏搜索关键字“环境变量然后在弹出的环境变量菜单把路径加入到Path变量参数即可。
设置完成环境变量后7z.exe就不必在命令行中输入路径直接运行即可。
在你掌握了执行方式后我再来带你学习一下它的参数要想使用支持密码加密方式的zip压缩包你需要使用四个参数它们分别是
1. a参数7z.exe能够把文件夹压缩为压缩包也能解压一个压缩包。a参数用来指定7z将要对一个目录进行的压缩操作。
1. -t参数用来指定7z.exe制作压缩包的类型和名称。为了制作一个zip压缩包我将把该参数指定为-tzip并在该参数后指定zip压缩包的名称。
1. -p参数用来指定制作的压缩包的密码。
1. “目录”参数:用来指定要把哪个目录制作为压缩包。
如果我希望把压缩包“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&quot;7z.exe a -tzip {backupFilename} -p{password} {dir}&quot;
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 = &quot;zip&quot;
return f&quot;{basename}.{extname}&quot;
# 遍历目录,得到该目录下所有的子目录和文件
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&quot;7z.exe a -tzip {backupFilename} -p{password} {dir}&quot;
status = os.popen(cmd)
return status
if __name__ == '__main__':
# 要备份的目录
backupDir = &quot;/data&quot;
# 要备份的文件
backupFiles = getAllFiles(backupDir)
# zip文件的名字“年月日.zip”
zipFilename = genZipfilename()
# 自动将要备份的目录制作成zip文件
zipWithoutPassword(backupFiles, zipFilename)
# 使用密码进行备份
zipWithPassword(backupDir, zipFilename, &quot;password123&quot;)
```
## 思考题
按照惯例,我来为你留一道思考题,如果需要备份的是两个甚至更多的目录,你会怎么改造脚本呢?
欢迎把你的想法和思考分享在留言区,我们一起交流讨论。也欢迎你把课程分享给你的同事、朋友,我们一起做职场中的效率人。我们下节课再见!

View File

@@ -0,0 +1,336 @@
<audio id="audio" title="28Celery库让计算机定时执行任务解放人力" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/41/fe/41a01cbe2a0aed5f07284f334a3007fe.mp3"></audio>
你好,我是尹会生。
上一讲我教你怎么把一个文件夹自动打包成压缩包,学会之后,你肯定会想到可以利用这个功能,把每日工作报告的文件夹制作成压缩包,作为邮件的附件发送给领导。
那像是每日发送邮件、每周定时提醒、每月填写报表等等类似这些按照某一周期完成的重复性任务你肯定希望也能用Python来自动化完成它们再也不用设置闹钟提醒自己手动发送邮件和手动提交周报了。
那么今天我就教你如何使用Windows任务计划和Python的Celery库实现周期任务的自动执行。
## 定时执行任务的注意事项
说起计算机执行任务的方式,我们一般分为两种,分别是**手动执行和定时执行**
定时执行任务,在自动化上的要求要比手动执行任务更高,所以两者在程序执行方式和输出结果上有较大的区别。为了让你能正确运行定时任务,我先来带你区分两种执行方式上有什么差别。
首先,执行任务的对象不同。这一点很好理解。手动执行的任务往往是人为操作的,而定时执行的任务启动往往是计算机按照我们设定的时间自动发起的。
其次,**基于不同的执行对象,执行过程也有不同的要求**。手动执行一个任务时,那么任务弹出的对话框、提示信息等中断任务执行过程的消息,可以人工处理。
而定时运行的任务,由于是由计算机发起的,因此不会有人工来处理。所以定时执行的任务一定要把代码编写成从任务开始到结束都不需要和“人”进行交互,简而言之,定时任务运行的程序必须是“非交互程序”。
最后,定时运行的程序是不支持把任务的结果输出到字符终端的,因此如果需要定时执行任务,并输出执行结果的话,需要把输出结果保存在文件中,否则定时任务会执行失败,导致该定时任务不会被计算机执行。
以上是手动执行任务和定时执行任务的差别,由于我们一般接触的都是手动执行,那么第一次接触定时执行任务时,就要避免把“交互式的程序,需要将执行结果输出到终端的程序”,直接放在定时任务中运行。
在你了解了定时执行任务的方式和注意事项后我就来带你学习怎么基于Windows的任务计划和Python的Celery库实现定时任务。
需要说明的是Windows的任务计划只能支持在Windows操作系统且在当前计算机定时运行任务而且采用了图形界面配置定时任务的时间比其他的任务计划工具操作更简。
但是如果你要是有多个计算机需要同时配置定时任务又不想逐个计算机手动配置那么使用Celery会比Windows任务计划更适合。
## 使用Windows任务计划执行定时任务
首先我先带你学习如何使用“Windows 任务计划”,实现定时任务。
当你的定时任务只需要在一台计算机运行我建议你使用Windows系统自带的“任务计划”实现定时任务步骤简单、直观通过三个操作步骤就可以完成。
第一步,自然是**启动任务计划程序**了。你可以在两个地方找到它的启动入口:
- 一个是“管理工具” 里的“任务计划程序”图标;
- 另一个是“控制面板”的“计划任务”图标。
启动以后的界面如下:
<img src="https://static001.geekbang.org/resource/image/f9/22/f9cac554c59659e6ef5a2af1e195dd22.png" alt="">
界面显示了当前已经在运行的定时任务,你可以点击“名称”来修改它们被执行的时间。如果你需要新建一个定时任务,还可以点击界面右侧的“创建基本任务”按钮。
第二步,“**创建基本任务**”,指的是设置任务的名称、执行时间,以及要执行的是哪一类程序。
其中“任务名称”是用来在“任务计划程序”界面区分多个任务的,如上图。而设定任务在什么时间执行的功能,叫做“触发器”,它可以按照你期望程序重复的周期,设置为“每天、每周、每月”以及更详细的时间运行。
我把“触发器”的设置截图放在下方,帮你更直观地理解定时任务的设置方式。
<img src="https://static001.geekbang.org/resource/image/c2/c8/c2624d0955106fd662abc4a68262f2c8.png" alt=""><br>
除了“每天、每月、每周”等比较宽泛的设置你还可以通过点击“下一步”进行更具体的执行时间设置比如我希望每周一至周五的00:09:01执行任务就可以按照下图的方式进行设置。
<img src="https://static001.geekbang.org/resource/image/2a/60/2a7f37917dda07e0a2cb1d5293717460.png" alt=""><br>
在截图中我勾选了周一至周五的复选框并把开始时间设置为0点09分01秒。
第三步,**设置指定时间运行的任务**。定时执行的任务可以是一个可执行程序,也可以是终端的一条命令。你只需输入可执行文件或命令的完整路径,以及要执行的文件名称就可以了。点击确认后,就可以按你设定的时间运行了。我把截图贴在下面供你参考。
<img src="https://static001.geekbang.org/resource/image/c3/32/c38c3c6cd45196dbca26673f4ddc3932.png" alt="">
由于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](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四个组件。<br>
<img src="https://static001.geekbang.org/resource/image/27/12/27f5c8802a2741169c40c8c832cf2812.png" alt=""><br>
前三个是Celery用于执行任务的组件由Celery软件自身实现。其中Broker是中间件用来连接Beat和多个Worker组件Beat作为任务的生产者、Worker作为任务的消费者可以通过Broker进行通信从而定时运行Worker组件的多个任务。
最后一个是Celery的数据存储组件基于Redis实现。在定时任务场景中Redis用于存放用户添加的任务计划到数据库中并把数据库文件保存到磁盘这样前三个组件所在的进程即使遇到意外故障重新运行也不会造成定时任务的丢失。
此外Celery和Redis数据还是通过TCP网络协议连接的所以你可以给多个定时任务指定一个Redis数据库以此来保证多个计算机上的定时任务需要进行修改时只修改一次就可以实现自动数据同步了。
根据对Celery执行定时任务的工作过程分析相信你已经对它的四个组件有所了解了。那接下来我就以每周六22点01分定时备份“c:\data”文件夹为例给你讲解一下怎么通过编写Celery四个组件的代码来定时备份数据。
### 定时备份文件夹
**实现文件夹的定时备份功能****需要编写代码的组件<strong><strong>主要**</strong>是Worker组件和Beat组件。</strong>
**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
&quot;test1&quot;: {
&quot;task&quot;: &quot;tasks.jobs.test1.run1&quot;, #执行的函数
&quot;schedule&quot;: crontab(minute=1, hour=22, day_of_week=6),
&quot;args&quot;: ()
}
```
我来详细解释一下三个字典的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来为你举例它可以有四种写法
1. 如果minute为默认的“*”,表示每分钟执行一次。
1. 如果minute为具体的数字例如10表示将会在第10分钟运行任务。不过这里要注意的是当你使用具体数字时需要配合其他参数一起生效。假设小时的参数为“*”则表示每小时的第10分钟运行任务假设小时的参数为5则表示5点10分运行任务。
1. 如果minute参数为“*/2” 表示每2分钟运行一次任务。
1. 如果minute参数为“135”表示每小时第1、3、5分钟运行一次任务。
四种写法也同样可以应用于参数hour(小时)、day_of_week(星期)、day_of_month(月)、month_of_year(年),因此其他位置的参数我就不再一一为你进行讲解了。
最后一个参数是args它的参数是元组 用来指定定时运行任务时,为该任务对应的函数传递哪些参数,如果不需要传递参数,则需要保留该值为空元组“()”格式。
设置好定时任务的日期之后需要继续编写该日期所需要运行的任务函数。定时运行的任务函数需要按照“task”的字典value保存在“tasks.jobs”目录中我把目录结构的截图放在下方帮你更直观地找到每个功能所在的文件目录。
<img src="https://static001.geekbang.org/resource/image/4f/8a/4f1d0de86fb10586548f2456cc13258a.png" alt="">
在截图中config.py保存了设置定时任务的代码jobs文件夹下保存了需要运行的Python脚本文件。
**编写任务函数**
接下来我来继续为你讲解jobs中的test1.py文件中的任务函数run1()函数是如何编写的。
回到我们备份data目录的例子run1()函数中就是实现备份目录的全部功能在定义run1()函数时为了能够支持被Celery调用必须在定义时为它增加一个装饰器“@app.task”。
装饰器就是在不改变函数原有的定义前提下为函数运行之前和之后增加新的功能。而这里为run1()增加装饰器的目的就是为了在函数原有功能不变的前提下让run1()函数增加被Worker组件执行的功能。
类似某一天天气突然降温,你依然想要穿着昨天帅气的格子衬衫去上班时,就可以在格子衬衫外面再增加一个酷酷的皮衣,既有温度又有风度。
增加后的代码如下:
```
app = Celery(&quot;tasks&quot;)
@app.task
def run1():
print(&quot;这里是备份data文件夹的代码&quot;)
```
代码中的“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(&quot;tasks&quot;)
# 导入celery的配置信息
app.config_from_object(&quot;tasks.config&quot;)
```
在这段代码中我同样不想让Celery访问Redis数据库会进行多次初始化因此在“**init**.py”初始化文件中我还增加了“app.config_from_object("tasks.config")”设置项确保同一个Celery只和Redis数据库建立一次连接避免因多次连接带来数据库没有必要的资源开销。
```
# config.py
from celery.schedules import crontab
# 指定Redis数据库的地址和端口
broker_url = &quot;redis://127.0.0.1:6379/1&quot;
# 时区
timezone = &quot;Asia/Shanghai&quot;
# 导入任务所在文件
imports = [
&quot;tasks.jobs.test1&quot;,
&quot;tasks.jobs.test2&quot;,
]
# 需要执行任务的配置
beat_schedule = {
&quot;test1&quot;: {
&quot;task&quot;: &quot;tasks.jobs.test1.run1&quot;, #执行的函数
&quot;schedule&quot;: crontab(minute=1, hour=22, day_of_week=6),
&quot;args&quot;: ()
},
&quot;test2&quot;: {
&quot;task&quot;: &quot;tasks.jobs.test2.run1&quot;, #执行的函数
&quot;schedule&quot;: crontab(minute=&quot;*&quot;),
&quot;args&quot;: ()
},
}
```
为了你能更快地看到定时任务执行的结果我增加了任务test2实现了每分钟运行一次因此当你执行Worker组件后就可以在1分钟内观察到test2任务的输出。
```
# test1.py
from tasks import app
@app.task
def run1():
print(&quot;开始备份data文件夹&quot;)
if __name__ == '__main__':
tasks()
# test2.py
from tasks import app
@app.task
def run1():
print(&quot;开始备份data2文件夹&quot;)
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 -&gt; 2021-04-13 01:40:31
Configuration -&gt;
. broker -&gt; redis://127.0.0.1:6379/1
. loader -&gt; celery.loaders.app.AppLoader
. scheduler -&gt; celery.beat.PersistentScheduler
. db -&gt; celerybeat-schedule
. logfile -&gt; [stderr]@%WARNING
. maxinterval -&gt; 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]
- ** ---------- .&gt; app: tasks:0x7ff03c9549d0
- ** ---------- .&gt; transport: redis://127.0.0.1:6379/1
- ** ---------- .&gt; results: disabled://
- *** --- * --- .&gt; concurrency: 4 (prefork)
-- ******* ---- .&gt; task events: OFF (enable -E to monitor tasks in this worker)
--- ***** -----
-------------- [queues]
.&gt; 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()函数呢?
欢迎把你的想法和思考分享在留言区,我们一起交流讨论。也欢迎你把课程分享给你的同事、朋友,我们一起做职场中的效率人。我们下节课再见!

View File

@@ -0,0 +1,183 @@
<audio id="audio" title="29网络和邮件库定时收发邮件减少手动操作" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4d/88/4dc142f6d6eaccbced8f3f2bc2d2f488.mp3"></audio>
你好,我是尹会生。
相信在你的日常办公工作当中,对邮件肯定早就不陌生了。我们通过邮件既可以发送和接收正式的公文,也能够利用邮件编写周报、月报,以及订阅定期发布的新闻或者期刊,等等。
在这么多的应用场景当中,你会发现有些**收发邮件的工作是周期性**的。那我们就可以利用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邮箱的网页端](https://mail.qq.com)登陆成功后,通过设置-账号-IMAP服务打开IMAP/SMTP服务。打开功能后可以参考[官方文档](https://service.mail.qq.com/cgi-bin/help?subtype=1&amp;&amp;id=28&amp;&amp;no=331)将IMAP服务器的地址指定为“imap.qq.com”“使用SSL”保证数据传输过程的安全并将连接IMAP服务器的端口指定为“993”。打开IMAP服务的截图和官方文档的截图如下。
<img src="https://static001.geekbang.org/resource/image/8d/4b/8d41dd963723da23c2daee2c7672ba4b.png" alt=""><br>
<img src="https://static001.geekbang.org/resource/image/5a/e6/5ac71084beb997a4251114a12ce8fde6.png" alt=""><br>
第二步是**使用用户名密码登录**。当服务器允许你从远程使用IMAP协议登陆服务器接收邮件后就可以使用Python的imaplib库进行连接和登陆了。
imaplib库是Python的内置库连接服务器可以使用IMAP4_SSL()函数登录可以使用login()函数,连接和登陆的代码如下:
```
import imaplib
conn = imaplib.IMAP4_SSL(host=&quot;imap.qq.com&quot;, port = 993)
conn.login(&quot;username@qq.com&quot;,&quot;password&quot;)
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(&quot;INBOX&quot;)
# 搜索邮件ALL为全部可以按照发件人使用FROM过滤也可以使用日期过滤
_, data = conn.search(None, 'unseen')
for mailid in data[0].decode().split(&quot; &quot;):
# 取回每一封未读邮件的内容
# 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=&quot;username@qq.com&quot;,
password=&quot;password&quot;,
host=&quot;smtp.qq.com&quot;,
port=465
)
```
yagmail库使用SMTP()函数与服务器建立连接,并在连接时指定用户名、密码、主机地址以及端口。
这里需要注意的是SMTP()函数通过默认参数“smtp_ssl=True”使用了SSL协议如果你所使用的邮件服务器采用了不同版本的SSL传输加密协议你需要先将默认端口从465改为587。如果没有提示连接异常表明建立连接是成功的接下来就可以为这封邮件编写内容了。
### 编写邮件正文
编写邮件正文时可以采用我们学习过的f-string**字符串的形式**来存放邮件的内容。例如你经常要发送的周报、月报都是相同的邮件格式不同的数据内容或文字。这时候可以使用f-string字符串的变量替换功能将格式编写为f-string的字符串再将每次变动的内容使用变量进行替换它的代码格式如下
```
content = &quot;内容填充&quot;
body = f&quot;模版 {content}&quot;
```
当你编写好邮件的正文后需要使用send()函数来发送邮件。**send()函数一般会使用四个参数,按照参数定义的顺序,它们分别是收件人邮箱、主题、邮件正文和附件**。我将这四个部分依次作为send()函数的参数后就可以将邮件发送到SMTP服务器了。
这里我有一个小的建议,我会建议你先把邮件发给自己,如果出现发送失败,或发送内容与自己期望不符时,更方便对内容进行调整。我把发送命令和发送成功后的截图贴在下面,供你参考。
```
conn.send(&quot;receiver@qq.com&quot;, &quot;主题&quot;, body, &quot;one.jpg&quot;)
```
<img src="https://static001.geekbang.org/resource/image/86/51/866793bf73c8ac8773ab3b49a2dba651.png" alt="">
这就是利用yagmail实现自动发送邮件的完整过程掌握之后你可以把上节课学习的定时任务以及自动生成图形的matplotlib库结合起来使用将发送邮件功能定义为一个函数从而实现周报和月报的自动发送功能。
## 小结
最后我来为你总结一下这一讲的主要内容。在本讲我使用了yagmail库、imaplib库以及email库实现了邮件自动收发的功能。与Foxmail和Outlook比起来使用Python实现的邮件客户端能够在收取邮件后对主题等元素自行判断并与正则表达式、IM通知等其他工具组合实现更加自动化的功能。
利用yagmail自动发邮件前你还可以为你的周期发送的邮件指定模版通过Celery实现定时发送和周期发送邮件。
除了可以自动收发邮件外我还为你详细讲解了接收邮件的处理过程这一过程遵循了IMAP的协议规范决定了代码编写的先后顺序**如果邮件接收或发送是你自动化工作中主要优化的工具那么我建议你用更多的时间来掌握IMAP与SMTP协议规范**。
当你掌握了IMAP协议能够支持哪些操作以及不能支持哪些操作之后才能更好地进行邮件API的学习。对于IMAP支持的功能你可以参考官方文档找到函数及其参数对于没有支持的功能你需要自己实现自定义的函数。
最后的最后除了微信、钉钉外邮件可以说是我们职场中使用最广泛的通讯工具了。并且也是我们工作中最正式的通讯工具。因此我建议你能够多练习怎么通过Python更加熟练地自动化收发邮件相信我这会为你的工作带来更高效的输出。
## 思考题
按照惯例我来为你留一道思考题如果我希望每周六10点整能够自动的将C盘上的一个目录作为邮件的附件发送到一个指定邮箱你会使用哪些库来实现你能否将实现的思路用自己的语言讲出来呢
欢迎把你的想法和思考分享在留言区,我们一起交流讨论。也欢迎你把课程分享给你的同事、朋友,我们一起做职场中的效率人。我们下节课再见!

View File

@@ -0,0 +1,209 @@
<audio id="audio" title="30怎么快速把任意文件格式转成PDF并批量加水印" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/79/88/795d285a9b4659af3cb429fa50eeb488.mp3"></audio>
你好,我是尹会生。
在办公场景中我们打交道最多的软件要数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&gt; import win32com
```
我们**用于格式转换的库**叫做**“win32com”**它是pywin32软件的库名称。安装它时你要使用pypiwin32作为它的安装包名称来使用。这是第一次接触该库最容易混淆的地方我建议你在阅读后续内容前先对“pywin32、pypiwin32、win32com”这三个概念进行区分。
在你正确区分上面三个概念之后我们就可以开始导入win32com库并调用VBA脚本来把Word文档转换为PDF格式了。
为了让你更好地理解win32com库的执行过程我来为你举个例子。我在"C:\data"文件夹下有一个Word格式的a.doc文件现在要将它自动转换为a.pdf文件并继续存储在该目录中。如果你手动进行格式转换只是需要以下四个步骤
1. 进入到C:\data文件夹
1. 使用Office的Word组件打开 a.doc文件
1. 使用“文件另存为”功能保存为PDF格式并指定保存目录
1. 保存并关闭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(&quot;Word.Application&quot;)
# 使用Word软件打开a.doc
file = word.Documents.Open(f&quot;{worddir}\{wordname}&quot;, ReadOnly=1)
# 文件另存为当前目录下的pdf文件
file.ExportAsFixedFormat(f&quot;{worddir}\{pdfname}&quot;, 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](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(&quot;Excel.Application&quot;)
# 使用Excel软件打开a.xls
file = excel.Workbooks.Open(f&quot;{exceldir}\{excelname}&quot;, False)
# 文件另存为当前目录下的pdf文件
file.ExportAsFixedFormat(0, f&quot;{excel}dir}\{pdfname}&quot;)
# 关闭文件
file.Close()
# 结束excel应用程序进程
excel.Quit()
```
对比word2pdf()和excel2pdf()你会发现实现的基本逻辑是相同的但是在实现Excel转换上有两个主要函数的参数不同。
1. DispatchEx()函数的参数这里使用了“Excel.Application”字符串作为该函数的参数“Excel.Application”作为DispatchEx()参数的目的是让pywin32库启动Excel进程并让它读取“a.xls”文件。
1. ExportAsFixedFormat函数的第一个参数从pdf路径变为保存的类型你可以参考[Office的Excel VBA官方文档](https://docs.microsoft.com/zh-cn/office/vba/api/excel.workbook.exportasfixedformat),从中学习函数的每个参数。
### 将PowerPoint幻灯片转换为PDF
在你学习了Word文档和Excel表格转换PDF文件的基础上我再来带你你对比学习一下如何将PowerPoint的默认保存文件ppt格式转换为PDF。参考word2pdf()和excel2pdf()两个函数我直接将PowerPoint的幻灯片转换PDF文件的代码提供给你你可以通过官方文档([https://docs.microsoft.com/zh-cn/office/vba/api/powerpoint.presentation.exportasfixedformat](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(&quot;PowerPoint.Application&quot;)
# 使用ppt软件打开a.ppt
file = ppt.Presentations.Open(f&quot;{pptdir}\{pptname}&quot;, False)
# 文件另存为当前目录下的pdf文件
file.ExportAsFixedFormat(f&quot;{pptdir}\{pdfname}&quot;)
# 关闭文件
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, &quot;wb&quot;) 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代码块以外进行编写。
为了让你更直接地感知到增加水印后的结果,我把增加水印后的结果贴在下方,供你参考。
<img src="https://uploader.shimo.im/f/LR66ArSYYCVTazC6.png!thumbnail" alt="">
以上就是我使用PyPDF2库为PDF增加水印的全部流程。不过除了增加水印外你还能使用pdfWriter对象来实现很多实用的功能比如为PDF文件设置密码、插入新的页面、旋转PDF页面等等。
此外由于pyPDF2库封装得非常好所以它的调用很简单你只需一个函数就能实现我刚才提到的这些功能了。我将pyPDF2库的官方文档链接[https://pythonhosted.org/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文件呢