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,278 @@
<audio id="audio" title="16循环与文件目录管理如何实现文件的批量重命名" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fb/2e/fb7369fe7d6421184f52eed444bdf52e.mp3"></audio>
你好,我是尹会生。
作为产品经理/运营,你经常需要做竞品调研,来跟自家产品对比优劣。这时,你就需要下载大量与该类产品相关的图片,并进行整理归类。而归类操作中,必须要走的一步就是对这些图片进行重命名。
还有你在搜集资料时,往往会从网络中下载大量不同格式的文件,比如电子书、视频、音频等,为了方便归纳整理,你也需要对文件进行重命名。
通过我例举的这两个场景,你应该发现了,这些需求都是把**批量改名和网络功能**结合而且还需要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 = &quot;/Users/user1/Desktop/pic&quot;
# 需要批量重命名的扩展名
old_ext = &quot;.jpg&quot;
# 取得指定文件夹下的文件列表
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)+&quot;.JPG&quot;)
# 重命名
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(&quot;/Users/user1/Desktop/pic&quot;, &quot;.jpg&quot;)
```
这段代码实现的功能和没有重构之前完全相同都是对指定目录的指定扩展名文件进行批量重命名。但是在代码结构上要比直接在文件实现的代码逻辑更清晰可以看到改名功能被放在函数定义中执行的时候就可以直接调用rename()函数。
将改名功能封装为函数的好处就是,代码更工整了,新的功能也可以继续采用函数的形式添加到当前代码中。比起把所有代码按执行顺序都写在一个文件中,这样的格式会让你更容易区分开代码中的每一个功能。
### 明确执行位置
把批量改名的功能封装为函数之后,对程序的执行顺序也会带来一些变化。我把前后变化给你做个对比:
- 封装函数之前,程序的执行顺序是导入库之后依次执行。
- 封装为函数之后执行顺序就变为导入库之后就开始执行rename()函数的调用。
当这个脚本再陆续添加新的函数的话那么找到哪一行是脚本第一个执行的命令就非常麻烦了。因此在Python中有一个参考C语言设置代码入口的方法让你能快速定位代码是从哪一行开始执行的。这个方法就是通过对内置变量“**name**”的值进行判断,判断它是不是和字符串“**main**” 相等。
在Python中执行代码的方式有两种。
1. 一种是单独运行也就是用Python加脚本的名称方式运行。
1. 另一种方式是把.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__ == &quot;__main__&quot;:
func3()
rename(&quot;/Users/edz/Desktop/pic&quot;, &quot;.jpg&quot;)
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(&quot;-p&quot;, &quot;--path&quot;, required=True, help=&quot;path to rename&quot;)
```
同时,我还为“--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):
&quot;&quot;&quot;批量改名函数&quot;&quot;&quot;
... ...
def args_opt():
&quot;&quot;&quot;获取命令行参数函数&quot;&quot;&quot;
#定义参数对象
parser = argparse.ArgumentParser()
# 增加参数选项、是否必须、帮助信息
parser.add_argument(&quot;-p&quot;, &quot;--path&quot;, required=True, help=&quot;path to rename&quot;)
parser.add_argument(&quot;-e&quot;, &quot;--ext&quot;, required=True, help=&quot;files name extension, eg: jpg&quot;)
# 返回取得的所有参数
return parser.parse_args()
if __name__ == &quot;__main__&quot;:
# args 对象包含所有参数,属性是命令行参数的完整名称
args = args_opt()
# 调用重命名函数,将命令行参数作为重命名函数的参数
rename(args.path, &quot;.&quot;+args.ext)
# 输出改名之后的结果
print(os.listdir(args.path))
```
通过重构后,代码的整体结构就变成了导入库、函数定义、函数调用三个部分,对经常需要变动的替换路径和扩展名,也从修改变量改为命令行参数,无论从阅读代码还是后续继续扩展代码,整体结构都要比顺序执行代码逻辑更清晰。
## 小结
最后让我来为你总结一下这一讲我为你讲解了使用Python如何实现批量改名以及如何对越写越长的代码进行重构。
批量改名属于操作系统中的文件相关操作,这类功能在编程语言中往往会提供事先定义好的编程接口,无需自己实现从应用层到操作系统的全部代码,建议你在遇到这类需求时,先从标准库中搜索相关模块,再从第三方库搜索,尽量避免手工编写,提高工作效率。
为了让批量改名的脚本逻辑更清晰,也更方便执行,我对代码还进行了三个方面的重构:
1. 通过使用函数增加代码的逻辑性。
1. 通过“**name**”变量增加了程序入口,便于你直接找到程序开始执行的位置。
1. 通过增加命令行参数,让你不用修改代码,就能实现函数的参数的修改。
增加程序的可读性、提高执行便利性也能为以后编写代码效率提升带来改进,这些改进会在后续章节代码越来越多的时候起到更明显的效果。
我把这节课的相关[代码](https://github.com/wilsonyin123/python_productivity/blob/main/%E6%96%87%E7%AB%A016%E4%BB%A3%E7%A0%81.zip)都放在了GitHub上供你学习参考。
## 思考题
最后我来为你留一道比较有趣的思考题,你能否通过命令行为代码指定两个参数,当这两个参数为整数时,脚本自动计算这两个参数的“和”和“差”,并将执行结果打印到屏幕上。
欢迎把你的思考和想法写在评论区,我们一起交流讨论。此外,你还可以点击课程详情页的“戳我进群”,然后扫描二维码,加入我们的课程读者群,我也会在群里为你解疑答惑。我们下节课再见!

View File

@@ -0,0 +1,257 @@
<audio id="audio" title="17不同操作系统下如何通过网络同步文件" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/47/19/472fcbaa58e68c2bcf92f34768915819.mp3"></audio>
你好,我是尹会生。
你有没有过这种经历:慌慌张张地去会议室开会,突然发现自己需要的文件却在工位的台式电脑中。因为文件比较大大,通过互联网下载需要很长时间。如果用网盘来中转放在两个电脑上的文件,传输速度又很慢。
我猜这个时候你就会想:如果能用一台电脑实现文件的上传和下载,那文件传输是不是就非常方便快速了。
这当然是可以实现的。所以今天我就教你怎样用Python基于HTTP协议来实现跨操作系统的文件上传和下载功能。
今天要学习的代码都不长比较精简又和文字相得益彰所以学起来也不会太累。那接下来就跟着我的思路走我们先来了解一下使用Python来实现文件上传和下载的优势。
## 为什么要使用Python
实现文件下载的软件相信你也用过很多比如微信、QQ。用这些软件来传输文件不仅要连接互联网而且还有文件大小的限制。那如果用商业软件呢传输虽然方便但是就不能跨操作系统而且还有可能需要付费。
所以综合下来既要免费又要传输快、没有大小限制的实现方式有没有呢聪慧如你一定知道接下来我会开始讲怎么用Python来实现文件的上传和下载功能了。别着急我还要再唠叨几句关于用Python来实现的优势这样你会学得更有劲儿。
首先,用法简单。**只要一行代码,就能实现文件的浏览和下载功能。**
其次跨操作系统适用范围广。只要安装了Python默认支持的文件下载需要的模块那么在Windows、macOS、Linux上就都能用。
最后传输速度快。和网盘、微信不同Python的文件下载功能是基于局域网通信的不需要通过互联网中转所以也就没有了传输速度和文件大小的限制。
知道了用Python来实现的优势那接下来我们就进入正题。我会结合代码来给你讲解用Python怎么实现文件的浏览和下载功能。代码不会很长所以你学起来也不会很累。
## 一行代码,实现文件的浏览和下载
使用Python实现文件的浏览和下载只需要一行代码。如下
```
python3 -m http.server 8080
```
通过在命令行运行这行代码之后,就能通过“[http://你的IP地址:8080](http://xn--IP-0p3ck01akcu41v: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
&gt;&gt;&gt; import http
&gt;&gt;&gt; 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](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模块的官方文档](http://docs.jinkan.org/docs/flask/patterns/fileuploads.html)的上传代码进行了精简考虑到你目前对编程的理解还比较基础所以我把用户验证和文件扩展名验证功能去掉后得到了如下的代码。通过这段代码可以实现基于Python的文件上传。
我把代码放在文稿中,供你学习和参考。同时,我也再给你详细讲解上传的过程,以及用到的代码。
```
import os
from flask import Flask, request
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = os.getcwd()
html = '''
&lt;!doctype html&gt;
&lt;title&gt;Upload new File&lt;/title&gt;
&lt;h1&gt;Upload new File&lt;/h1&gt;
&lt;form action=&quot;&quot; method=post enctype=multipart/form-data&gt;
&lt;p&gt;&lt;input type=file name=file&gt;
&lt;input type=submit value=Upload&gt;
&lt;/form&gt;
'''
@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服务器后会看到这样一个界面如下
<img src="https://static001.geekbang.org/resource/image/9a/2f/9a13a3461fbf0d87b399ecf07f738e2f.png" alt="">
这个网页内容对应的是第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](http://127.0.0.1:8090)[/](http://127.0.0.1/)”由于浏览器默认的请求方式是“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”方式才会对文件进行处理。
具体的处理过程是:
1. 先从“request.files”中取出上传到服务器的文件数据
1. 再通过“file.filename”取得上传时使用的文件名
1. 接着通过path.join()函数将事先指定好的路径和文件名拼接组成当前目录下的文件名后;
1. 最后通过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的代码呢
欢迎把你的思考和想法写在评论区,我们一起交流讨论。如果你学完有所收获,也欢迎你把课程分享给你的朋友、同事,一起提升办公效率。好了,那我们下节课再见!

View File

@@ -0,0 +1,293 @@
<audio id="audio" title="18http库如何批量下载在线内容解放鼠标" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/11/54/116c4cd848527aba0e42c70120164654.mp3"></audio>
你好,我是尹会生。
我在前段时间遇到了下载大量图片的需求。具体来说是在我训练AI识别猫时必须要在电脑中存储大量猫的图片。但搜索到的图片都在网页中我就需要先把它们一个个手动下载下来再保存然后才能进行后续的工作。
而且随着我对AI训练工作的日益增多这类需求会越来越丰富我不仅要下载猫的图片还要下载大量其他各种动物的图片。相信类似这种批量下载网页中的图片的需求你在工作中会经常遇到。而这类需求刚好能够使用Python的“requests-html”库实现批量下载提高我们的工作效率。
因此呢,这节课我就以搜索到的猫的图片为例,给你讲解一下,我是怎么来批量下载图片的。
## 批量下载图片的准备工作
我把实现图片自动化批量下载的过程,拆分成四段难度逐渐递增的代码。这四段代码分别实现的是以下四个功能:
1. 访问HTTP服务器得到搜索结果的整个网页
1. 在访问服务器之后下载一张图片;
1. 找到多张图片的相似地址;
1. 提取相似地址,下载多张图片。
前两个功能,是批量下载图片的准备工作和前提。掌握了这两个功能,那么批量下载图片实现起来就容易多了。所以接下来,我们先来学习这两项准备工作。
### 访问HTTP服务端的资源
我们从难度最低的一段代码开始怎么通过访问HTTP服务器从而得到猫的搜索结果的整个网页。
访问HTTP服务器我需要使用Python的第三方库**requests-html库**来实现。虽然Python在标准库中也自带了访问服务器urllib库但它的实现代码不够简洁所以我一般数情况下会使用requests-html库替代它来进行HTTP服务器的访问。
使用reqeusts-html库你先需要通过**“pip”来安装**,再通过**get()方法访问HTTP服务器**。
安装requests-html库的方法和我们[上一讲](https://time.geekbang.org/column/article/353826)安装flask库是一样的。这里我强调一下在Python中绝大多数的第三方库它们的名称和安装包的名称是相同的所以除了特殊的安装方法外像这种通用的安装方法我就不会每次都重复贴出来了。reqeusts库的安装方法代码如下
```
pip3 install requests-html
```
在命令行执行后requests-html库就安装成功了。接下来我们就使用它的get()方法去请求HTTP服务器而得到的返回数据就是网页的代码。
相信你还记得在[上一讲](https://time.geekbang.org/column/article/353826)中我介绍了两种常见的HTTP服务端请求的方式分别是“GET”和“POST”。requests-html库就是使用了同名函数实现的“GET”方式访问。
那么接下来我将使用reqeusts库来访问一个[无版权的图片网站](http://www.upsplash.com),并得到网页的源代码。我把代码写出来,供你参考。
```
from requests_html import HTMLSession
# URL
name = &quot;猫&quot;
url = f&quot;https://unsplash.com/s/photos/{name}&quot;
# 启动
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 ,其他返回你可以参考[维基百科](https://www.wiki-wiki.top/baike-HTTP%E7%8A%B6%E6%80%81%E7%A0%81?wprov=srpw1_0)。
根据不同的状态码你能判断网页返回的状态并进行后续处理。比如我们经常使用状态码“200”确认网页是正确返回了内容这意味着我得到“200”这个返回码之后就可以继续处理网页数据了。
如果返回码是“4xx或5xx”说明出现了请求过程的客户端或者服务端错误HTTP客户端并没有得到网页的内容这时候你就必须停止程序后续的逻辑并向执行者提示错误信息。具体的判断方法就是我们的老朋友if语句了。
在这段代码中我们得到了状态码200那接下来我就使用“result.html.html”得到了完整的网页源代码。
这四个步骤就是使用“requests-html”库得到指定网页源代码的完整流程。接下来就要分析网站的行为来下载单张图片。
### 使用requests-html下载一张图片
下载单张图片是图片批量下载的基础。为什么要这么说呢因为我们可以通过观察一张图片的下载行为把浏览器的下载操作转换成Python的代码这样就可以循环批量下载图片了。而观察单张图片的下载行为换句话说就是用Python来模拟浏览器。
具体思路是这样的在使用Python按照浏览器的行为进行下载后我们可以对服务端的HTML代码进行分析从而得到多个图片在服务端存储的代码逻辑之后就可以利用循环批量下载图片了。那接下来我就用例子带着你来学习一下。
使用浏览器下载一张图片的时候当你把鼠标移动到下载按钮你会发现它是在访问图片的URL后面增加了一串“download”字符。
我用一张图片的访问和下载给你举个例子,例如下载下面这张图片:
<img src="https://static001.geekbang.org/resource/image/25/72/2579790ce800ef4fea833a8eb6475872.png" alt="">
这张图片的[访问地址](https://unsplash.com/photos/NLzaiXOELFY)和[下载地址](https://unsplash.com/photos/NLzaiXOELFY/download)我贴了出来,供你学习。
在你清楚浏览器是如何下载一张图片的之后我们就可以使用“requests-html”下载单张图片了。下载单张图片的完整代码如下
```
from requests_html import HTMLSession
# URL
url = &quot;https://unsplash.com/photos/NLzaiXOELFY/download&quot;
# 启动
session = HTMLSession()
# GET请求
result = session.get(url)
# 结果
print(result.status_code)
# 保存图片
with open(&quot;one.jpg&quot;, &quot;wb&quot;) 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/](https://unsplash.com/s/photos/%E7%8C%AB)猫” URL后再通过浏览器的调试功能打开调试界面。我以最流行的Chrome浏览器为例它的调试窗口打开快捷键是“F12”打开调试界面后的截图如下供你参考
<img src="https://static001.geekbang.org/resource/image/31/53/31757b9f98904fee2acae68ba3c58f53.png" alt="">
截图左侧就是调试页面,截图的右侧是网页的内容,你可以通过截图左上角的“选择元素按钮”<br>
<img src="https://uploader.shimo.im/f/0FFEmWfYRSiY0t0g.png!thumbnail" alt=""><br>
点击图片左侧的调试界面会自动定位到图片对应的HTML代码。而你向上查找会发现图片下方的“&lt;a&gt;”标签的“href”属性就记录了图片ID和下载地址。
这里我有必要为你介绍一下HTML的标。你在网页上看到的每个元素都是由不同类型的标签组成的。
例如图片使用的是“&lt;img&gt;”标签,超链接使用的是“&lt;a&gt;”标签。每个标签还使用了相应的属性,来记录该标签的具体信息。比如把一个人作为标签,他可以有性别、身高、年龄等属性。“&lt;a&gt;”标签的“href”属性就记录了超链接的地址我们可以使用超链接地址来完成图片的批量下载。
那用什么方法能一次性找到链接呢?聪明的你一定想到了我们在[第8讲](https://time.geekbang.org/column/article/346724)学过的正则表达式。正则表达式是分析网页的常用工具之一,不过还有一种比正则表达式更方便的**网页内容搜索工具那就是XPath。**我们可以使用XPath来找到超链接。
如果你对使用正则表达式分析网页也感兴趣你可以把它跟XPath进行对比选择一个自己顺手的工具这也是提升工作效率的技巧之一。
我先把XPath查找的方法和结果写在下面的代码中 然后再为你分析XPath的用法。
```
print(result.html.xpath('//figure[@itemprop=&quot;image&quot;]//a[@rel=&quot;nofollow&quot;]/@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的整体结构是由头部标签“”和主体标签“”组成的而网页的内容是在主体标签下逐层编写的它的结构如下
```
&lt;head&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div num=1&gt;
&lt;figure itemprop=&quot;image&quot;&gt;
... ...
&lt;div num=2&gt;
... ...
&lt;a rel=&quot;nofollow&quot; href=http://...&gt;
... ...
&lt;/div&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;/body&gt;
```
以上面的结构为例如果网页内容简单你可以像使用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=&quot;image&quot;]//a[@rel=&quot;nofollow&quot;]')
```
通过增加属性,限定了搜索的标签,可以得到需要下载的“&lt;a&gt;”标签,“&lt;a&gt;”标签的规范写法要求链接地址写@href属性链接的内容要写在它的值中因此我们再为xpath()提取的“&lt;a&gt;”标签增加“@href”属性即可以实现提取只包含图片地址的列表。我把完整的xpath()函数写在下方。
```
xpath('//figure[@itemprop=&quot;image&quot;]//a[@rel=&quot;nofollow&quot;]/@href')
```
这就是利用xpath()的**路径、标签、属性搜索超链接的完整实现思路。**我建议你打开网页调试器通过“选择元素按钮”点击网页内容对照这段网页源代码和xpath()语法来学习这样更有利于你快速掌握怎么使用Xpath匹配多张图片的下载地址。
### 使用循环,下载多张图片
在你得到了多张图片的下载地址列表后,接下来我们只需要把下载单个图片的代码和列表功能进行组合,这样就能实现图片的批量下载了。我先把核心代码写出来,供你参考,然后再讲解思路。
```
down_list = result.html.xpath('//figure[@itemprop=&quot;image&quot;]//a[@rel=&quot;nofollow&quot;]/@href')
def down_one_pic(url):
result = session.get(url)
filename = get_picID_from_url(url)
with open(filename, &quot;wb&quot;) 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讲](https://time.geekbang.org/column/article/353153)中,我们实现了文件的批量改名功能,你能否将批量改名功能也封装成一个函数,结合今天的批量下载,实现下载之后的文件自动改成你需要的文件名呢?
欢迎把你的思考和想法分享在留言区,我们一起交流、讨论。也欢迎你把我们的课程分享给你的朋友、同事,一起做职场上的效率人。我们下节课再见!

View File

@@ -0,0 +1,250 @@
<audio id="audio" title="19http库如何批量下载在线内容解放鼠标" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ea/dd/ea0d70ab6e088353460878ec263b56dd.mp3"></audio>
你好,我是尹会生。
在上节课我们学习了怎么使用“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浏览器版本如下图
<img src="https://static001.geekbang.org/resource/image/21/0c/211e46d304be4ec29709295396dbc70c.png" alt="">
接着根据我的浏览器版本“89.0”访问“Webdrive”[国内的镜像站](http://npm.taobao.org/mirrors/chromedriver/)下载与浏览器相匹配版本的“Webdriver”。这里要注意不同的操作系统“Webdriver”的安装包不同所以需要你选择对应的操作系统版本。“Webdriver”的下载网页内容截图如下
<img src="https://static001.geekbang.org/resource/image/72/e7/724055e74881a83f406fb4b777607be7.png" alt="">
我这里以“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”。如果能够正确运行则会提示运行成功。否则会提示找不到这条命令你需要再检查一下它被存放的目录。正确运行的截图如下供你参考。
<img src="https://static001.geekbang.org/resource/image/ca/9e/ca388b78ff7a7b2ae52bec099f78a99e.png" alt="">
在完成了最复杂的“Webdriver”安装之后接下来就可以安装Python的库并尝试使用Python库调用浏览器了。
第三步安装“selenium”库。
安装“selenium”库可以继续使用pip命令。它的库和安装包同名你可以在命令行运行以下命令进行正确安装。
```
pip3 install selenium
```
第四步使用“selenium”访问网页。
安装成功之后我们可以通过任意一个网页的访问来测试从Python到“Webdriver”再到浏览器整个工具链是否能正确运行。我把代码贴在下方供你参考。
```
from selenium import webdriver
import time
browser = webdriver.Chrome()
browser.get(&quot;http://www.jd.com&quot;)
time.sleep(10)
browser.quit()
```
这段代码实现了调用浏览器访问京东的网页并在10秒后自动关闭浏览器的功能。如果能够正确运行那么说明整个工作链是配置正确的。
这段用来访问京东来验证工作链的代码,由**导入库、浏览器的初始化、控制浏览器行为**三个部分组成。我来解释一下。
**首先是导入库。**我在代码的前2行除了导入“selenium”库用来调用“Webdriver”外还导入了“time”库。
导“time”库的原因就在于我们需要使用“selenium”库模拟手工操作浏览器的流程而手工操作会有一定的延迟。所以我就基于“time”库的“sleep()”函数来模仿用户查看网页的行为在打开网页等待了10秒后再关闭网页。
**其次是浏览器的初始化。**浏览器的初始化在Python内部做了两件事情第一个是找到之前安装的“chromedriver”文件检查它是否可执行。第二件事情是根据调用的“Chrome()”函数,找到当前电脑中的浏览器并运行它。
**最后控制浏览器的行为**。我使用了“get()和quit()”两个函数它们分别实现的是向服务器发起HTTP的“GET”请求以及关闭浏览器的功能。
请求如果成功发送, 你会在“Webdriver”打开的浏览器看到网页内容我把正确执行的结果截图放在下面供你参考。
<img src="https://static001.geekbang.org/resource/image/bc/43/bc0cc885646c5e26c3b1ced7e03b7443.png" alt="">
**第五步,****取得网页的源代码并打印。**
通过“selenium”打开浏览器之后除了控制浏览器的行为你还需要像上一讲的批量下载图片一样批量获取网页的资源。这时你可以使用如下代码来获取当前浏览网页的源代码。这样在关闭浏览器之后你仍然可以对网页的内容进行操作。代码如下
```
content = browser.page_source
print(content)
```
掌握了“selenium”和“Webdriver”的安装和运行接下来我们就可以利用“selenium”来实现京东的自动签到功能了。通过这一场景来为你演示如何通过“selenium”模拟浏览器并实现自动签到。
## 如何实现京东自动签到
要想实现自动签到你既要实现用户登录又要模拟用户点击按钮。随着技术的日新月异很多登录和按钮都不再使用简单的“POST”请求了。这时你就需要用“selenium”基于标签名字找到按钮的链接像普通用户请求网站链接一样不必理解登录和签到的内部的原理就能实现自动化访问。
### 自动签到的原理与思路
“selenium”之所以能替代手动操作达到自动化的效果核心原因在于我们可以通过“selenium”来模拟浏览器的操作。而我们可以通过对浏览器的功能拆解把浏览器的交互行为一一对应到“selenium”的非交互命令之后就能实现自动化操作了。
这也正是自动签到的原理。再进一步说“selenium”的行为越像浏览器自动签到的功能就模拟得越完整。换言之签到功能也就更自动化。那我们就来看看浏览器是如何实现登录和签到工作的。
使用浏览器进行登录有三个步骤,分别为:
1. 打开登录页面;
1. 切换到用户密码登录选项卡;
1. 点击登录按钮。
我们依次来分析一下这三个步骤如果用“selenium”各自是怎么实现的。
首先打开登录页面在浏览器中是鼠标点击“登录”按钮实现的它的实现是向指定的URL发送“GET”请求发送后服务端返回网页就是你见到的登录页面了。
其次,打开登录页面后,默认的登录方式是二维码登录。
为了能让“selenium”能够模拟我们需要通过用户名和密码方式登录。那接下来就切换到用户密码登录这一选项卡这一切换动作在浏览器中可以通过使用鼠标实现。不过在“selenium”中我们需要通过“GET”请求来替代它才能实现非交互式的登录。
登录切换页面的网页源代码以及网页,我把相关截图一并放在下方,供你参考。
<img src="https://static001.geekbang.org/resource/image/fb/31/fbde90ae5cacc859ac05d09b63159a31.png" alt="">
最后,你需要在浏览器输入用户名密码,而后点击“登录”按钮,实现登录。
由于京东的登录界面是经过加密的所以不能使用“requests-html”直接发送“POST”请求来实现登录这里仍然要继续使用“selenium”获取用户名、密码的输入框输入之后模拟点击按钮的“GET”请求到服务器之后才能实现登录。
### 使用“selenium”模拟浏览器实现自动登录
在了解了浏览器的原理之后你就可以使用“selenium”模拟浏览器实现登录了。我把“selenium”登录京东的代码写在下方供你参考。
```
from selenium import webdriver
import time
browser = webdriver.Chrome()
# 访问主页
browser.get(&quot;http://www.jd.com&quot;)
time.sleep(2)
# 访问登录页
browser.get(&quot;https://passport.jd.com/new/login.aspx?ReturnUrl=https%3A%2F%2Fwww.jd.com%2F&quot;)
time.sleep(2)
# 切换为用户密码登录
r = browser.find_element_by_xpath(
'//div[@class=&quot;login-tab login-tab-r&quot;]')
browser.execute_script('arguments[0].click()', r)
time.sleep(2)
# 发送要输入的用户名和密码
browser.find_element_by_xpath(
&quot;//input[@id='loginname']&quot;).send_keys(&quot;username&quot;)
time.sleep(1)
for i in &quot;password&quot;:
browser.find_element_by_xpath(
&quot;//input[@id='nloginpwd']&quot;).send_keys(i)
time.sleep(1)
# 点击登录按钮
browser.find_element_by_xpath(
'//div[@class=&quot;login-btn&quot;]/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(&quot;https://mall.jd.com/index-1000002826.html&quot;)
time.sleep(2)
# 签到并领金豆
browser.find_element_by_xpath('//div[@class=&quot;jSign&quot;]/a').click()
time.sleep(10)
```
这段代码就是访问签到页面并领取金豆的代码。需要你注意的是,访问页面和签到两个操作之间需要等待几秒,否则会因为网页加载过慢,导致签到按钮还没加载完就发送了点击动作,让自动签到失败。
## 总结
最后我来为你总结一下本节课的主要内容。在这一讲中我通过“selenium”实现了浏览器的模拟并把浏览器的点击链接、用户登录、切换标签等常用功能使用“selenium”的函数和XPath转换为用Python可以控制的操作。转换之后你就可以利用Python控制浏览器的行为把这些需要鼠标的交互式操作编写为非交互式的代码达到自动化控制浏览器的目的了。
除了实现自动签到以外通过“selenium”的函数组合你就能模拟浏览器的绝大部分操作。我也希望你能够在学完这节课后想一想你的工作当中是否也使用了浏览器进行重复的工作希望你能通过对本讲的学习把它们改为自动化。
## 思考题
按照惯例最后我来为你留一道思考题你能否利用今天讲的模拟浏览器的方法自动登录Github并以“Python”作为关键字进行搜索然后把搜索的结果保存到一个文件呢
欢迎把你的想法和思考分享在留言区,我们一起交流讨论。也欢迎你把课程分享给你的同事、朋友,我们一起做职场中的效率人。我们下节课再见!

View File

@@ -0,0 +1,391 @@
<audio id="audio" title="20不同文件混在一起怎么快速分类" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/00/56/0059beae4de7cb3d406af9fb8e09fc56.mp3"></audio>
你好,我是尹会生。
今天我们的内容要从一碗香喷喷的蛋炒饭开始。要想做一份传说中的蛋炒饭,肯定要放胡萝卜、黄瓜、火腿肠还有葱花等好多种类的食材。
这是不是像你的桌面一样,为了完成某一项目,需要将音频、视频、文档、图片等各种格式组合在一起。但你在你完成了项目之后,想要将它们进行整理的时候,会发现各类文件堆满了桌面,要想从桌面找都某个文件就像从蛋炒饭里将所有的葱花挑出来一样困难。
对于这种情况我们可以采用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 = {
&quot;music&quot;: (&quot;mp3&quot;, &quot;wav&quot;),
&quot;movie&quot;: (&quot;mp4&quot;, &quot;rmvb&quot;, &quot;rm&quot;, &quot;avi&quot;),
&quot;execute&quot;: (&quot;exe&quot;, &quot;bat&quot;)
}
```
通过刚才对字典的定义,我们给扩展名自动分类制定好了分类的规则。那接下来我们继续设计程序,按照这一分类规则,进行文件的读取和分类。
### 怎样设计生产者消费者模式
**文件的读取和分类**是两个不同的功能,你在编写代码时可以把它们编写成两个不同的函数。
但是由于今天的程序比以往要复杂,所以实现这两个函数的思路也会比较多。比如:
- 你可以把所有的文件全部读取之后,再按照分类规则移动到新的目录;
- 也可以读取一个紧接着将它移动到一个新的文件夹里;
- 当然还可以读取一部分文件之后,将一部分文件进行移动,然后继续读取第二批文件。
到底选择哪种方案才是最佳实践呢?
在这种情况下,你最希望的是能够向有丰富开发经验的开发人员请教,看他是怎么实现类似需求的,然后按照他的实现逻辑来编写你的代码。这也是专业软件开发人员面对这一问题时的通常做法:去寻找和当前场景相似的“设计模式”。因为设计模式是众多软件开发人员经过相当长的时间,对某一场景进行大量试错总结出来的经验。那我们可以利用它,来解决我们当前场景下的问题。
我们当前的场景刚好和设计模式中的“生产者消费者模式”比较吻合。生产者消费者模式的描述是这样的:有两个进程共用一个缓冲区,两个进程分别是生产数据和消费数据的。而缓冲区,用于存放生产进程产生的数据,并让消费进程从缓冲区读取数据进行消费。
使用生产者消费者模式刚好能解决文件读取和文件分离的逻辑。我把读取当前文件名称和路径函数作为生产者,把分类和移动文件的逻辑作为消费者。在生产者消费者中间,我再使用队列作为它们中间的缓冲区。
可以看到,使用生产消费者模式,我主要是增加了一个队列,而不是从生产者直接把数据交给消费者。这样做主要有三个好处:
1. 如果生产者比消费者快,可以把多余的生产数据放在缓冲区中,确保生产者可以继续生产数据。
1. 如果生产者比消费者慢,消费者处理完缓冲区中所有数据后,会自动进入到阻塞状态,等待继续处理任务。
1. 缓冲区会被设置为一定的大小,当生产者的速度远远超过消费者,生产者数据填满缓冲区后,生产者也会进入到阻塞状态,直到缓冲区中的数据被消费后,生产者才可以继续写入。而当消费性能不足时,可以等待消费者运行,减少生产者和消费者在进度上相互依赖的情况。
通过分析我们发现,可以采用生产者消费者模式来编写文件的读取和分类代码。
考虑到是你初次接触设计模式,为了不让你产生较大的学习心理负担,我把其中的多线程并发访问缓冲区简化成单线程版本,这样你能根据代码的执行过程,先学会简单的生产者和消费者模式。
在分类规则的“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()函数增加了两个参数:
1. 使用dir指定建立文件夹的目录
1. 使用type_dir指定按照哪个字典建立。
而建立文件夹可以使用我们学习过的os模块通过os.mkdirs()函数建立一个新的文件夹。代码如下:
```
import os
# 定义文件类型和它的扩展名
file_type = {
&quot;music&quot;: (&quot;mp3&quot;, &quot;wav&quot;),
&quot;movie&quot;: (&quot;mp4&quot;, &quot;rmvb&quot;, &quot;rm&quot;, &quot;avi&quot;),
&quot;execute&quot;: (&quot;exe&quot;, &quot;bat&quot;)
}
source_dir = &quot;/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&quot;{full_path}::{files}&quot;)
#########
source_dir = &quot;/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(&quot; \'&quot;)
ext = filename_withext.split(&quot;.&quot;)[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(&quot;::&quot;)
# 剔除文件名字符串出现的&quot;[&quot; &quot;]&quot;,并用&quot;&quot;做分隔转换为列表
files = files.strip(&quot;[]&quot;).split(&quot;,&quot;)
# 对每个文件进行处理
for filename in files:
# 将文件移动到新的目录
move_to_newdir(filename, filepath, type_to_classify)
```
在这段代码中,我实现了从队列取出文件名称和目录,并根据分类将文件移动到新的目录。
由于消费者的逻辑是从队列读取内容和移动文件两个功能组成的,所以我把消费者拆分成了两个函数进行编写:
1. classify_from_q()函数,用来实现从队列读取文件列表,并遍历列表,得到每一个文件名称;
1. move_to_newdir()函数,把文件名称、路径、分类规则作为参数,真正实现移动。
相应的,如果你在编写包含多个功能的程序时,也要尽量保持每个功能的独立性,把每一个功能尽量放在一个函数中,这样能有效提升你的代码的可读性。
这两个函数虽然比较长但是大部分都是我们学过的内容我想为你重点讲解一下第一次接触到的两个知识点一个是in操作一个是利用shutil库的move()函数实现的重命名。
in操作叫做成员操作符它能支持目前我们学习过的所有基础数据类型用来判断一个值是否是列表、元组、字典等基础数据类型中的一员。如果这个值是基础类型的成员之一就会直接返回True如果不是成员之一返回的就是False。有了in操作符你就不用手动遍历基础数据类型再使用“==”逐个去判断某个值和数据类型中的成员是否相等了。
我举个例子你会更容易理解。我在代码中使用了这样一行代码“if ext in type_to_newpath[new_path]”
- “ext” 就是文件的扩展名就像是“a.mp3”的扩展名是“mp3”
<li>“type_to_newpath[new_path]”是字典“type_to_newpath”中以“new_path”作为key的值就像是“type_to_newpath = { "music": ("mp3", "wav") }”的“("mp3", "wav")”。<br>
我把变量改成具体的变量值,那这行代码就变成了下面的样子:</li>
```
&quot;mp3&quot; in (&quot;mp3&quot;, &quot;wav&quot;)
```
如果扩展名在元组中那么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&quot;{full_path}::{files}&quot;)
# 移动文件到新的目录
def move_to_newdir(filename_withext, file_in_path, type_to_newpath):
# 取得文件的扩展名
filename_withext = filename_withext.strip(&quot; \'&quot;)
ext = filename_withext.split(&quot;.&quot;)[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(&quot;::&quot;)
files = files.strip(&quot;[]&quot;).split(&quot;,&quot;)
# 对每个文件进行处理
for filename in files:
# 将文件移动到新的目录
move_to_newdir(filename, filepath, type_to_classify)
if __name__ == &quot;__main__&quot;:
# 定义要对哪个目录进行文件扩展名分类
source_dir = &quot;/Users/edz/Desktop/files&quot;
# 定义文件类型和它的扩展名
file_type = {
&quot;music&quot;: (&quot;mp3&quot;, &quot;wav&quot;),
&quot;movie&quot;: (&quot;mp4&quot;, &quot;rmvb&quot;, &quot;rm&quot;, &quot;avi&quot;),
&quot;execute&quot;: (&quot;exe&quot;, &quot;bat&quot;)
}
# 建立新的文件夹
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”三类的文件名和大小依次显示在屏幕上你会怎样实现呢

View File

@@ -0,0 +1,277 @@
<audio id="audio" title="21SQLite文本数据库如何进行数据管理" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b7/cf/b7fe2b2162399d839257089a0524fbcf.mp3"></audio>
你好,我是尹会生。
你在办公中一定遇到过需要数据持久化的问题。数据持久化简单来说就是当你关闭程序的时候数据依然可以完整地保存在电脑中。你可能会想到用文本文件、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的特有优势
1. 数据查询速度快;
1. 存放数据的空间占用少;
1. 实现了一般数据库能够支持的基于SQL语言的增删改查。
总结来说就是如果你需要存放结构化的数据而且只关注数据读取的效率不关注数据的样式而且还需要编程语言来进行数据访问的话使用SQLite要比文本文件、Excel更适合你的办公场景。
我为你介绍了SQLite这么多好处那赶快带着你把它用起来吧。
## 为SQLite建立数据表
要想使用SQLite数据库光有文件还不够你还要为它建立数据表类似你新建了一个Excel工作簿的文件与此同时你还要建立一个工作表把数据写在工作表上 再将多张工作表放在工作簿上面。
因此要想实现对数据库的操作我也需要为SQLite创建一张工作表接下来我就用一个创建手机通讯录数据表的脚本为你演示一下我是如何为SQLite数据库创建它的“工作表”的。
### SQLite建立数据表的一般流程
为了能够让你更具体地学习数据表是怎样创建和使用的我先把创建数据表的代码写出来给你对照代码我来为你讲解SQLite建立数据表的流程。
```
import sqlite3
import pathlib
# 数据库文件的路径和文件名称
dir = pathlib.PurePath(__file__).parent
db = pathlib.PurePath(dir, &quot;contents.db&quot;)
# 创建连接
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(&quot;创建成功&quot;)
except Exception as e:
print(&quot;创建失败&quot;)
print(f&quot;失败原因是:{e}&quot;)
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。而每个字段都由字段名称、字段类型和可选的约束条件三部分组成。
1. 字段名称,是用于操作表时方便进行列操作的名字。
1. 字段类型会限制你对该字段写入数据的类型如果类型不正确会报错或被填入空值。SQLite能支持的类型非常多我在示例中使用了最常见的整数和字符串其他类型你可以通过[官方文档](https://sqlite.org/lang_createtable.html)来了解。
1. 约束条件是对该列强制执行的规则。例如我使用了“NOT NULL”约束确保在数据写入时该字段不能为空值。同时我还使用了“PRIMARY KEY”确保了字段唯一性确保该字段的值不会重复。更多的约束条件你同样可以根据[官方文档](https://sqlite.org/lang_createtable.html)来了解。
通过创建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, &quot;contents.db&quot;)
# 创建连接
conn = sqlite3.connect(db)
# 创建游标
cur = conn.cursor()
# 定义要执行的SQL语句
sql1 = '''INSERT INTO address_book
VALUES (?, ?, ?)'''
v = (1, &quot;Tom&quot;, 12377778888)
# 执行SQL
try:
cur.execute(sql1, v)
conn.commit()
except Exception as e:
print(f&quot;失败原因是:{e}&quot;)
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, &quot;contents.db&quot;)
# 创建连接
conn = sqlite3.connect(db)
# 创建游标
cur = conn.cursor()
# 定义要执行的SQL语句
sql2 = '''SELECT phone FROM address_book WHERE name = &quot;Tom&quot; '''
# 执行SQL
try:
result = cur.execute(sql2)
print(result.fetchone())
except Exception as e:
print(f&quot;失败原因是:{e}&quot;)
finally:
# 关闭游标
cur.close()
# 关闭连接
conn.close()
```
这段代码使用了“SELECT”SQL语句实现了联系人“Tom”的手机号码读取。“SELECT”语句执行后会把结果放到“result”变量中。由于取出的结果是一个对象因此还需要通过fetchone() 函数,把对象中的一行取出,得到用户的手机号码。
那在这一查找流程中我们要重点关注的就是“SELECT”语句它是实现读取内容的语句。在任何一个数据库中它都是最常用的SQL语句。我们从它的结构开始分析。
```
SELECT phone FROM address_book WHERE name = &quot;Tom&quot;
```
这条语句的执行顺序和书写顺序是不同的它的执行顺序是从“FROM”到“WHERE”再到“SELECT”关键字。它们三个的含义分别是
1. FROM从一张表中查找数据用于来指定查询的表。
1. WHERE表示查询的条件是什么。在这个例子中查询条件是name字段的值为“Tom”字符串。
1. SELECT显示哪一列。在这个例子中只显示phone这一字段的值。
“SELECT”语句按照代码中的顺序实现了电话号码的查找。当然了这只是个非常简单的查找而你在工作中肯定会遇到更加复杂的查找我也会在下一讲教你怎样使用Python来为更加复杂的查询编写高效的代码。如果你想要更加深入的学习SELECT语句可以参考[官方文档](https://sqlite.org/lang_select.html)。
[官方文档](https://sqlite.org/lang_select.html)中会有“SELECT”中其他语句的执行顺序和功能介绍并且还采用了流程图方式来为你介绍它的子句之间是如何进行组合的。如下图
<img src="https://static001.geekbang.org/resource/image/51/8d/5173a9565yy6ee55a8296dd5c60ecd8d.png" alt="">
## 小结
最后让我来为你总结一下在本讲中我使用了SQLite数据库为你讲解了数据库对比文本文件和Excel的优势它在存储结构化数据方面已经作为了业界的标准解决方案。
想要用好SQLite你还要掌握数据库的四个操作步骤那就是
1. 连接数据库文件
1. 创建游标
1. 执行SQL语句
1. 关闭游标和连接
这四个步骤是实现一切数据库操作的基础在掌握四个操作步骤的基础上实现数据操作的SQL语句包括增删改查四种操作在本讲中我为你讲解了如何使用SQL实现SQLite的读写下一讲将为你讲解更加复杂的SQL语句以及修改和删除的SQL语句。
查找和写入作为最常用的SQL语句是需要你重点掌握的。除了存储通讯录数据像是我们从网络批量下载的数据以及工作中的员工信息表等数据都可以采用SQLite存放。它将是以后你存储结构化数据最实用的工具。
## 思考题
按照惯例我为你出一道思考题如果使用SELECT语句查询到的结果不止一条使用fetchone()函数得到的是什么样的呢?你能否通过官方文档再找到获得多条查询结果的函数呢?
欢迎把你的想法和思考写在留言区,我们一起交流讨论。如果这节课在数据保存上帮你提高了办公效率,那也欢迎你把课程分享给你的朋友或同事,我们一起做职场上的效率人。

View File

@@ -0,0 +1,395 @@
<audio id="audio" title="22SQLite文本数据库如何进行数据管理" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d3/70/d3c634ceb8b32dfbfb9886c95f3d7070.mp3"></audio>
你好,我是尹会生。
在上节课我提到了使用比较简单的SQL来操作SQLite并为你讲解了数据库的基本操作步骤。
不过当你的程序功能越来越强大的时候随之而来的就是代码的复杂度越来越高。像是上一讲我们在进行SQLite数据库搜索的时候你需要建立连接、申请游标对象才能进行查询。而这些准备工作我们更希望在程序运行的时候就准备好这样就不必多次重复编写。
而且对数据库进行增删改查能够通过尽可能少的SQL来实现数据库的操作。那么能实现这一功能的就是**类**。
通过类你可以为越来越复杂的程序编写结构更清晰的代码。同时也能更好地把SQLite的增删改查封装成一个独立的对象便于你调用数据库时能进行数据持久化。
那么今天这节课我就带你使用类来实现SQLite数据的读取和写入。与此同时我会继续以通讯录为例来给你讲解如果使用了比较复杂的SQL来操作SQLite时怎么合理组织代码结构让你更优雅地书写代码。
## **使用类实现SQLite的读写**
由于类这个概念比较抽象我还是采用老办法帮你理解它我将使用“类”对SQLite的读写SQL操作进行封装并将类进行实例化以后进行调用得到SQLite中的通讯录数据。我先把代码贴出来供你参考
```
import sqlite3
import pathlib
class OptSqlite(object):
def __init__(self, dbname = &quot;new.db&quot;):
&quot;&quot;&quot;
:param dbname 数据库名称
&quot;&quot;&quot;
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):
&quot;&quot;&quot;
关闭连接
&quot;&quot;&quot;
self.cur.close()
self.conn.close()
def get_one_phone(self, username):
&quot;&quot;&quot;
获取一个联系人的电话
&quot;&quot;&quot;
self.get_user_phone_sql = f&quot;&quot;&quot;
SELECT phone FROM address_book WHERE name = &quot;{username}&quot; &quot;&quot;&quot;
try:
self.result = self.cur.execute(self.get_user_phone_sql)
return self.result.fetchone()
except Exception as e:
print(f&quot;失败原因是:{e}&quot;)
def set_one_phone(self, name, phone):
&quot;&quot;&quot;
增加一个联系人
&quot;&quot;&quot;
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&quot;失败原因是:{e}&quot;)
if __name__ == &quot;__main__&quot;:
my_query = OptSqlite(&quot;contents.db&quot;)
my_query.set_one_phone(&quot;Jerry&quot;,&quot;12344445555&quot;)
phone = my_query.get_one_phone(&quot;Tom&quot;)
phone2 = my_query.get_one_phone(&quot;Jerry&quot;)
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(&quot;contents.db&quot;)
```
如果需要多个通讯录就把它实例化多次并指定不同的SQLite数据库即可。每个数据库实例都会有一个“get_one_phone()”方法和一个“set_one_phone()”方法,来实现通讯录中联系人的读取和写入。
而为了表示属性和方法是在实例化中使用的你还需要对它增加self关键字使用实例的属性时要用“self.属性”的写法。使用方法时要是将实例的方法第一个参数设置为self代码为“方法(self)”。
类能够在封装和调用上提供比函数更灵活的方式,因此你会发现当功能复杂,代码数量增多了以后,很多软件都采用了类方式实现代码的设计。
### **类中的特殊方法“<strong>init**”</strong>
在类中,有一个内置的方法叫做“**init**”,它叫做类的初始化方法,能实现类在执行的时候接收参数,还能为类预先执行变量赋值、初始化等,实现在类一运行就需要完成的工作。
“**init**()”方法的作用有两个,分别是:
1. 为实例接收参数;
1. 实例化时立即运行该方法中的代码。
当一个类实例化时,它可以像函数调用一样,接收参数。类实例化时,它的后面需要增加括号“()”,括号中可以指定实例化的参数。这个参数将交给“**init**()”方法,作为“**init**()”方法的参数,进行使用。
我来为你举个例子来说明类是如何实现接收参数的。例如我在通讯录的例子中实例化一个SQLite的“OptSqlite”类实例化的代码如下
```
my_query = OptSqlite(&quot;contents.db&quot;)
```
这段代码中的“OptSqlite”就是类的名称而“contents.db”是该类初始化时输入的参数也是SQLite数据库文件的名称。
要想实现“my_query”实例在“OptSqlite”类实例化时获得参数就需要在类中使用初始化方法
```
def __init__(self, dbname = &quot;new.db&quot;):
```
在这段代码中,我定义了“**init**()”方法并指定它的参数“dbname”之后那么实例“my_query”就能够得到参数dbname变量的值“contents.db”了。
这就是一个实例化一个类,并如何在第一时间获得参数的完整过程。不过获得参数之后,你还要对参数继续使用和处理,以及需要在实例化之后就立即运行一些代码,这些功能就可以写在“**init**()”方法中来实现。
例如我就将数据库文件的路径处理、初始化连接、初始化游标的代码写入到了初始化函数。代码如下:
```
class OptSqlite(object):
def __init__(self, dbname = &quot;new.db&quot;):
&quot;&quot;&quot;
:param dbname 数据库名称
&quot;&quot;&quot;
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 = &quot;new.db&quot;):
&quot;&quot;&quot;
:param dbname 数据库名称
&quot;&quot;&quot;
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):
&quot;&quot;&quot;
关闭连接
&quot;&quot;&quot;
self.cur.close()
self.conn.close()
def new_table(self, table_name):
&quot;&quot;&quot;
新建联系人表
&quot;&quot;&quot;
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(&quot;创建表成功&quot;)
except Exception as e:
print(&quot;创建表失败&quot;)
print(f&quot;失败原因是:{e}&quot;)
def get_one_phone(self, username):
&quot;&quot;&quot;
获取一个联系人的电话
&quot;&quot;&quot;
self.get_user_phone_sql = f&quot;&quot;&quot;
SELECT phone FROM address_book WHERE name = &quot;{username}&quot; &quot;&quot;&quot;
try:
self.result = self.cur.execute(self.get_user_phone_sql)
return self.result.fetchone()
except Exception as e:
print(f&quot;失败原因是:{e}&quot;)
def get_all_contents(self):
&quot;&quot;&quot;
取得所有的联系人
&quot;&quot;&quot;
try:
self.result = self.cur.execute(&quot;SELECT * FROM address_book&quot;)
return self.result.fetchall()
except Exception as e:
print(f&quot;失败原因是:{e}&quot;)
def set_one_phone(self, name, phone):
&quot;&quot;&quot;
增加或修改一个联系人的电话
&quot;&quot;&quot;
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&quot;失败原因是:{e}&quot;)
def delete_one_content(self, name):
&quot;&quot;&quot;
删除一个联系人的电话
&quot;&quot;&quot;
self.delete_user_sql = f'''DELETE FROM address_book
WHERE name=&quot;{name}&quot;'''
try:
self.cur.execute(self.delete_user_sql)
self.conn.commit()
except Exception as e:
print(f&quot;删除失败原因是:{e}&quot;)
if __name__ == &quot;__main__&quot;:
# 实例化
my_query = OptSqlite(&quot;contents.db&quot;)
# 创建一张表
# my_query.new_table(&quot;address_book&quot;)
# 增加或修改一个联系人的电话
my_query.set_one_phone(&quot;Jerry&quot;,&quot;12344445556&quot;)
# 查询一个联系人的电话
phone = my_query.get_one_phone(&quot;Jerry&quot;)
print(phone)
# 查询所有人的电话
contents = my_query.get_all_contents()
print(contents)
# 删除一个联系人
my_query.delete_one_content(&quot;Jerry&quot;)
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”增加联系人之前没有判断该联系人是否存在。你能否利用判断语句实现增加联系人前对联系人是否存在进行判断并提示用户对重复联系人进行合并操作呢
欢迎把你的思考和想法放在留言区,我们一起交流讨论。如果这节课学习的数据透视表对你的工作有帮助,也欢迎你把课程推荐给你的朋友或同事,一起做职场中的效率人。