mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-17 22:53:42 +08:00
del
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
<audio id="audio" title="04 | 函数与字典:如何实现多次替换" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d8/8a/d8d0dea8f03d78a11ffbb34a2b307e8a.mp3"></audio>
|
||||
|
||||
你好,我是尹会生。
|
||||
|
||||
“替换”是我们日常办公经常遇到的操作,比较常见是把文件中的英文标点符号(,.""”)替换成中文标点符号(,。“”)。有时候不仅是标点符号,还需要替换好几个词。还有一种情况不太常见,但是一碰到就很棘手,那就是根据数字范围进行替换,比如“0-6岁”替换成“少年”,“7-17岁”替换成“青年”,“18-65岁”替换成“中年”。
|
||||
|
||||
如果直接使用替换函数,你需要编写大量的代码逻辑,但是使用逻辑判断和字典就可以用更高效的代码来实现快速替换功能。那么今天这节课,我们就来学习下怎么实现快速替换。
|
||||
|
||||
## 用Python实现“替换”功能的原理
|
||||
|
||||
为了让你更直观地理解编程语言里的替换,我先来给你讲一讲用Python实现替换的原理。我用一个例子来给你讲解。比如我需要把字符串“新年快乐”替换为“恭喜发财”,在Python中,我是通过replace()函数来实现的:
|
||||
|
||||
```
|
||||
string1="aaa新年快乐bbb"
|
||||
string2=string1.replace("新年快乐", "恭喜发财")
|
||||
print(string2)
|
||||
# aaa恭喜发财bbb
|
||||
|
||||
string3="aaa新年快乐bbb新年快乐ccc"
|
||||
string4=string3.replace("新年快乐", "恭喜发财", 2)
|
||||
print(string4)
|
||||
# aaa恭喜发财bbb恭喜发财ccc
|
||||
|
||||
```
|
||||
|
||||
你可以看到,在这段代码中我使用了replace()函数来实现文件内容的替换。为什么使用的是字符串的替换函数呢?因为在编程语言中,我们通常会把文件内容读取到内存用变量临时储存,再进行处理。为了便于对文字进行查找替换这类的操作,通常会使用字符串这种数据类型的变量来存储文字内容。
|
||||
|
||||
实现字符串替换的replace()函数,是Python字符串内置的函数。它既可以实现匹配关键字进行一次替换,也能支持对多次出现的字符串进行替换。比如在代码的第7行我就增加了参数“2”,这就实现了“新年快乐”两次的替换。
|
||||
|
||||
刚刚说的replace()函数,实现字符串替换操作的原理,就是从字符串中从前向后,逐个字符去和replace()函数的第一个参数去比较,如果文字内容相同,就把匹配的内容替换为replace()函数第二个参数的字符串内容。如果不匹配,就继续对比下一个字符,直到整个字符串比对完成. 为了便于描述,我把这种逐一比对,找到相同字符进行替换的操作,称作“一对一替换”。
|
||||
|
||||
在实际工作中,你遇到的替换场景会更复杂。比如需要把字符串中出现的所有英文标点符号,全部一对一地替换成中文的标点符号。那么这个问题其实就变成了多个“一对一”的替换操作了。该怎么解决呢?
|
||||
|
||||
相信你会想到通过多个replace()替换函数来实现多个“一对一”替换操作。为了让你更好地理解这一逻辑,我使用了如下代码,给你演示一下它的基本功能。
|
||||
|
||||
```
|
||||
string5='aaa,."bbb'
|
||||
string6=string5.replace(',', ',')
|
||||
string6=string6.replace('.', '。')
|
||||
string6=string6.replace('"', '“')
|
||||
# 需要更多的replace()匹配更多的标点符号
|
||||
print(string6)
|
||||
# aaa,。“bbb
|
||||
|
||||
```
|
||||
|
||||
在这段代码当中,我使用了三个replace()函数实现“,."” 三个符号的替换。不过一旦考虑替换更多的符号时,就要编写更多个replace()函数。这一行为虽然不会在运行效率上产生问题,但是会带来代码阅读上的障碍。
|
||||
|
||||
比如需要你通过Python把全国的省市地县的汉语拼音替换成汉字,像把“GUANGDONG”替换成“广东省”,你至少要编写上百个替换函数。那面对如此大量的“一对一”替换,我们该怎么高效地编写代码呢?
|
||||
|
||||
## 怎样实现批量替换?
|
||||
|
||||
我来解决这类问题一般会采用两种方式实现,一种方式是用字典+自定义函数替代replace()函数,另一种是用逻辑判断+自定义函数替代replace()函数。我们先来看字典+自定义函数的方式是怎么对带有大量replace()的程序进行优化的。
|
||||
|
||||
#### 用字典+自定义函数替代replace函数实现批量“一对一”替换
|
||||
|
||||
我们还是用把城市名称的拼音替换成汉字的例子来讲解。为了让你更直观地比较字典+自定义函数方式和replace()函数的区别,我先给你演示一下实现替换功能的代码:
|
||||
|
||||
```
|
||||
# 保存映射关系的函数,函数的主要功能是通过字典实现的
|
||||
def replace_city(city_name):
|
||||
return {
|
||||
"GUANGDONG":"广东省",
|
||||
"HEBEI":"河北省",
|
||||
"HUNAN":"湖南省",
|
||||
"HANGZHOU":"杭州市"
|
||||
}[city_name]
|
||||
|
||||
# 根据映射关系实现批量循环
|
||||
def replace_multi(my_citys, replaced_string):
|
||||
for pinyin_city in my_citys:
|
||||
replaced_string = replaced_string.replace(
|
||||
pinyin_city,replace_city(pinyin_city))
|
||||
return replaced_string
|
||||
|
||||
# 哪些城市要替换
|
||||
citys = ("GUANGDONG", "HUNAN")
|
||||
|
||||
# 需要替换的字符串
|
||||
string1 = """
|
||||
GUANGDONG,简称“粤”,中华人民共和国省级行政区,省会广州。
|
||||
因古地名广信之东,故名“GUANGDONG”。位于南岭以南,南海之滨,
|
||||
与香港、澳门、广西、HUNAN、江西及福建接壤,与海南隔海相望。"""
|
||||
|
||||
string2 = replace_multi(citys, string1)
|
||||
print(string2)
|
||||
# 广东省,简称“粤”,中华人民共和国省级行政区,省会广州。
|
||||
# 因古地名广信之东,故名“广东省”。位于南岭以南,南海之滨,
|
||||
# 与香港、澳门、广西、湖南省、江西及福建接壤,与海南隔海相望。
|
||||
|
||||
```
|
||||
|
||||
我在代码里是通过两个核心函数来实现替换的,它们分别是replace_city()和replace_multi()函数。
|
||||
|
||||
我们先来分析一下replace_city()函数。它实现的是城市拼音和城市中文名称的全部对应关系,其中有两个技术细节需要你掌握。
|
||||
|
||||
第一个技术细节是字典的取值方式。如果我把一个字典定义为dict1,并且想取得字典的值,就可以使用这样的代码:
|
||||
|
||||
```
|
||||
dict1["abc"]=123
|
||||
|
||||
```
|
||||
|
||||
方括号中的字符串"abc"被称作字典的下标。通过下标,我们可以获得字典的值。为了定义字典以后可以反复使用,通常我们会给字典赋予一个变量名,以此作为字典名称。所以在这里dict1就是字典的名称。
|
||||
|
||||
当然,如果字典只使用一次,那也可以不使用字典名称。相应的,它的写法就变成了这样:
|
||||
|
||||
```
|
||||
{"abc":123, "aaa":456}["abc"]
|
||||
|
||||
```
|
||||
|
||||
通过这一行代码,你可以取出直接使用字典的值,而不需要对字典进行声明,也不需要为字典再起一个变量名。
|
||||
|
||||
第二个技术细节是我为replace_city()增加了一个参数city_name,以及一个关键字return。city_name作为城市的拼音传入函数后,会作为字典的key,通过字典的映射功能得到中文城市名称。而return关键字返回字典映射的结果,就是城市的中文名称。
|
||||
|
||||
通过这两个技术细节,就可以让函数replace_city()实现接收拼音并返回中文的功能。这样实现映射关系的好处是:函数调用一次就返回一个值,编写好这类函数之后,其他人可以拿去直接使用,不用考虑函数内部使用了哪种数据类型,有利于代码的重复使用。
|
||||
|
||||
除了replace_city()之外,还有一个核心函数replace_multi()函数,它通过for循环来实现批量“一对一”的替换。它的作用是避免重复编写大量的replace()函数,提高代码的可读性。
|
||||
|
||||
我在设计replace_multi()函数的时候,为它准备了两个参数。第一个是要替换的城市的拼音,第二个是要替换的字符串。
|
||||
|
||||
第一个参数我具体指定了哪些城市需要替换,这样编写会让我的程序更加灵活,不必把所有城市的拼音都进行拼音到中文的替换操作。
|
||||
|
||||
第二个参数也是为了让replace_multi()函数更加灵活,如果对多段文字进行替换,可以多次调用replace_multi()函数。同样的,如果replace_multi()函数需要多次调用,也可以通过循环结构批量来优化代码。
|
||||
|
||||
总结来说,通过字典+自定义函数替代字符串默认的替换函数replace()函数,可以避免编写大量的replace()函数,提高了代码的灵活性和可读性。如果你在工作中涉及这类大量的“一对一”替换时,可以考虑采用我教的这个方法来优化你的替代效率。
|
||||
|
||||
#### 用逻辑判断+自定义函数替代replace()函数实现“多对一”替换
|
||||
|
||||
除了刚才我提到的大量“一对一”的替换场景,还有一种替换场景你也会遇到,并且一旦遇到就很棘手。
|
||||
|
||||
比如在Excel中,你需要根据年龄这一列单元格来把你的客户划分为少年、青年、中年、老年。如果把年龄的每个整数都进行一次替换,这种写法会非常啰嗦,所以我们可以使用逻辑判断来实现这一替换。我先把代码给你演示出来。
|
||||
|
||||
```
|
||||
age = 18
|
||||
if age>0 and age<=6:
|
||||
value="少年"
|
||||
|
||||
elif age>7 and age<=18:
|
||||
value=青年"
|
||||
|
||||
elif age>19 and age<=65:
|
||||
value="中年"
|
||||
|
||||
else:
|
||||
value="老年"
|
||||
|
||||
```
|
||||
|
||||
这段代码通过逻辑判断实现了从年龄到少年、青年等年龄段的替换功能。在代码中,“if”“elif”“else”是构成逻辑判断的关键字,它们表示了如果关键字到“:”之间的结果为True,则其他语句后面的代码不会被执行。
|
||||
|
||||
根据匹配的年龄要求,年龄是一个范围,所以我使用了and关键字连接两个判断逻辑。比如“age>0 and age<=6” 代码,意思就是当age同时满足大于0,并且小于等于6时,判断的条件才成立,这段代码的返回结果为True,因此value变量的值就是“少年”。
|
||||
|
||||
在这段代码中我使用了一个逻辑判断结构。逻辑判断结构用于判断age变量的范围,它可以根据判断的结果为value变量进行赋值。这种实现形式和城市的拼音替换不同,城市的拼音和汉字是逐一对应的,age变量的多个值,例如从1到6对应的都是“少年”,它实现的是一个范围映射到一个值上面的形式。为了便于描述,我把这种形式称作“多对一”的替换形式。
|
||||
|
||||
虽然使用逻辑判断进行替换操作,实现了的“多对一”的替换形式,但是我认为仍然存在着不利于代码复用(重复使用)的问题,因此在保证代码逻辑不变的前提下,我对这段程序进行了优化,将逻辑判断也放入函数中。代码如下:
|
||||
|
||||
```
|
||||
def age_replace(age):
|
||||
if age > 0 and age <= 6:
|
||||
return "少年"
|
||||
|
||||
elif age > 7 and age <= 18:
|
||||
return "青年"
|
||||
|
||||
elif age > 19 and age <= 65:
|
||||
return "中年"
|
||||
|
||||
else:
|
||||
return "老年"
|
||||
|
||||
|
||||
print(age_replace(80))
|
||||
|
||||
|
||||
```
|
||||
|
||||
我为你解释一下为什么要把替换操作放在函数中。这样使用有两点好处:
|
||||
|
||||
1. 提高代码的复用,当你下次需要做年龄到年龄段映射时,可以直接调用函数,不用重复编写逻辑判断的代码。
|
||||
1. 对代码进行再设计的时候,方便将逻辑判断中类似“age > 0 and age <= 6”的判断逻辑再封装成函数。
|
||||
|
||||
你可能会问了,代码不是应该先思考运行过程,经过设计之后再编码的吗?
|
||||
|
||||
在实际工作中,随着人们对代码的不断修改,原来设计好的代码结构,在整体结构上会增加很多的判断逻辑,代码质量会越来越混乱。这时候就需要你重新对代码逻辑进行优化。所以为了不让后续修改代码逻辑的行为破坏代码的可读性,就应该在初次设计和编写代码的时候考虑好代码的扩展性。
|
||||
|
||||
最后,我想再强调一下,使用if逻辑判断的目的是为了实现把一个范围映射到一个新的值,这样它就间接地实现了替换功能。所以当你解决替换问题的时候,不要把思维只局限在字符串自带的replace()函数中。
|
||||
|
||||
## 小结
|
||||
|
||||
我来给你总结一下今天的主要内容,围绕着“替换”这一功能,我给你讲解了三种实现替换的方法:
|
||||
|
||||
1. 字符串的replace()函数;
|
||||
1. 使用字典做“一对一”映射,通过字典类型的键值对,实现内容替换;
|
||||
1. 使用逻辑判断实现“多对一”映射,将if判断的条件替换为匹配成功的结果。
|
||||
|
||||
替换操作要根据被替换内容的形式,选择合适的方法,replace()函数更适合单个替换,字典适合“一对一”替换,if逻辑判断适合将一个范围替换成一个值。
|
||||
|
||||
除了灵活掌握不同的替换方式,我还建议你把字典和逻辑判断放入自定义函数当中,当你遇到类似需求的时候就可以直接复用代码。
|
||||
|
||||
## 思考题
|
||||
|
||||
通过将城市的拼音替换成汉字的功能,你是否能实现一个自己的自动多文件标点符号替换函数,将英文符号替换为中文符号呢?
|
||||
@@ -0,0 +1,183 @@
|
||||
<audio id="audio" title="05 | 图像处理库:如何实现长图拼接?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/1c/41/1c049b4b05ced45b00a5ece105e67941.mp3"></audio>
|
||||
|
||||
你好,我是尹会生。
|
||||
|
||||
我们在工作中,除了和文字、表格打交道之外,还会经常涉及到批量处理图片和视频的工作。比如:媒体从业者在发微博长图文时,需要把多个图片拼接成一幅长图;作为视频剪辑人员,需要从互联网下载多段视频,再进行合并。
|
||||
|
||||
这类工作可以用功能强大的商业软件实现,不过这些软件大都操作繁琐,而且还需要付费。为了降低学习成本和购买软件的成本,我们往往还会使用开源软件替代商业软件来实现图片和视频处理功能。但是开源软件通常都是以命令行方式运行的,所以我们不仅要记住命令,还得记住命令的常用参数。
|
||||
|
||||
不过,幸运的是,虽然直接使用开源软件不够友好,但如果通过Python来调用这些开源软件,那实现长图和视频拼接就轻而易举了,而且还能大批量地处理图片和视频。
|
||||
|
||||
# Python是如何调用外部命令的
|
||||
|
||||
为了让你了解Python是如何操作这些开源软件的,我先来给你介绍一下Python调用外部程序的原理。
|
||||
|
||||
我们要想使用Python语言之外的功能,要依靠两大途径:导入函数库和调用外部命令。
|
||||
|
||||
在第一讲我使用的xlrd库是通过import xlrd命令导入到Python语言中的,Python语言默认是不支持Excel的。那么通过导入函数库,Python就可以获得对Excel的操作能力。
|
||||
|
||||
还有一种情况是,需要操作Python语言之外的功能,但这个功能没有人将它开发成函数库,那如果我们想要使用这些功能,使用的途径就是调用外部命令了,而调用外部命令就需要Python内部函数库的subprocess模块来实现。
|
||||
|
||||
这个模块的实现机制是:它的run()函数的参数可以指定一个可以运行的程序的路径,而Python会根据这个路径来运行可执行文件,然后再根据运行结果,以及Python的逻辑判断去进行后续的自动化处理工作。
|
||||
|
||||
这个实现机制并不难,我给你写一段简单的程序,帮你理解Python是怎样调用外部命令的。这里以macOS系统为例,我们通过Python获取当前目录下所有文件的功能。
|
||||
|
||||
```
|
||||
from subprocess import run, Popen, PIPE
|
||||
|
||||
cmd1 = ["ls", "."]
|
||||
returncode = run(cmd1)
|
||||
|
||||
print(returncode)
|
||||
# CompletedProcess(args=['ls', '.'], returncode=0)
|
||||
# returncode是“ls .”的退出状态码.
|
||||
# 通常来说, 一个为 0 的退出码表示进程运行正常
|
||||
|
||||
# 使用Popen获取程序运行结果
|
||||
with Popen(cmd1, shell=True, stdout=PIPE, stderr=PIPE, encoding="utf-8") as fs:
|
||||
|
||||
# 如果程序在 timeout 秒后未执行完成,会抛出 TimeoutExpired 异常
|
||||
fs.wait(2)
|
||||
|
||||
# 从标准输出中读取数据,知道文件结束
|
||||
files = fs.communicate()[0]
|
||||
|
||||
print(files)
|
||||
|
||||
|
||||
```
|
||||
|
||||
这段代码中最核心的函数是run()函数和Popen类。subprocess模块就是通过这两个函数实现的外部程序调用。我来为你重点剖析一下它们的功能、参数,以及何时选择run()函数、何时选择Popen类。
|
||||
|
||||
为了实现Python调用可执行文件,首先在代码的第一行,我是这样编写的:
|
||||
|
||||
```
|
||||
from subprocess import run, Popen, PIPE
|
||||
|
||||
```
|
||||
|
||||
这样一行代码,它和我第一讲使用的import方式导入函数库的区别是,这种形式可以让你直接使用模块中的类和方法。
|
||||
|
||||
如果你使用 “import subprocess”方式导入subprocess库的话,在调用run()函数的时候,就需要用 “库.函数”的形式在Python中使用库当中的函数,即“subprocess.run()”。在你多次调用run()函数时,代码会较长,那么使用“from import”方式导入,就可以在当前代码文件中直接使用run()函数,为代码的阅读带来更好的体验。
|
||||
|
||||
接下来,我定义了一个变量cmd1。这个变量的值是macOS命令行能够运行的“ls .”命令,这个命令的执行结果是显示当前目录下所有文件和文件夹的名称。
|
||||
|
||||
run()函数的主要功能就是执行一个新的程序,它的用法非常简单,把第一个参数指定为要执行程序的路径就可以了。如果要执行的程序带有参数,那就可以使用列表数据类型存放可执行程序名称和参数,像是我在程序中定义的cmd1变量一样。如果你需要运行其他命令,把代码中的ls替换为你想要运行的其他程序就行了。
|
||||
|
||||
为了让Python自动化处理程序更强大,除了运行程序外,你还可以得到可执行程序的运行结果。在这种情况下,我们就需要使用Popen类替代run()函数实现外部程序的调用。
|
||||
|
||||
可以看到,我在代码的第12行先通过Popen类执行了“ls .”命令,接着通过参数stdout=PIPE 将命令的执行结果放入到PIPE对象中, 最后再通过communicate()函数将PIPE中的内容读取出来,存放到files变量中,这样就实现了读取命令执行结果的功能。
|
||||
|
||||
这个功能是无法在run()函数实现的,因此在你需要通过Python读取程序执行结果的时候,就可以选择Popen类。不过如果只需要运行可执行程序,那使用run()函数就能满足你的要求了。如果你想更深入地了解它们,我建议你阅读[subprocess库的官方文档](https://docs.python.org/zh-cn/3.7/library/subprocess.html#subprocess.Popen)。
|
||||
|
||||
以上就是我用subprocess库实现Python调用可执行程序的方法。Python之所以被我们称作最佳的“胶水语言”,就是因为它能轻易“粘合”可执行程序。利用Python灵活的逻辑判断、循环语法可以实现程序的批量执行和流程管理。
|
||||
|
||||
接下来,我们就使用subprocess来实现长图拼接和视频拼接的功能。
|
||||
|
||||
# 长图拼接
|
||||
|
||||
当我进行微博文案推广的时候,需要将多个图片拼接成一个长图。拼接图片的功能Python本身是不具备的,因此就需要引入外部命令来实现图片拼接功能。
|
||||
|
||||
我在macOS平台上找到了一个非常强大的图像处理软件叫做**ImageMagick****,**它能对图片进行编辑、合并、切割、旋转等90多种操作。 ImageMagick软件实现图片拼接的命令格式是这样的:
|
||||
|
||||
```
|
||||
composite 图片1.jpg 图片2.jpg ... 图片n.jpg 最终合成结果.jpg
|
||||
|
||||
```
|
||||
|
||||
在这段命令格式中,composite命令的参数包含了多个图片文件,每个图片需要对照着文件将图片的路径和文件名写在参数中。如果手工输入图片名称,不仅效率低,而且容易遗漏。另外,如果需要大量重复使用composite,还需要精细调整合并结果,给composite程序增加很多参数。
|
||||
|
||||
因此,我就可以通过Python调用可执行程序的subprocess库,对composite拼长图的工作进行脚本化编程。它的核心实现代码如下:
|
||||
|
||||
```
|
||||
p = Path(jpg_path)
|
||||
|
||||
# 增加命令
|
||||
cmd = ["composite",]
|
||||
|
||||
# 增加参数
|
||||
for x in p.iterdir() if PurePath(x).match('*.jpg'):
|
||||
cmd.append(x)
|
||||
|
||||
# 增加结果
|
||||
cmd.append(result_path)
|
||||
|
||||
run(cmd)
|
||||
|
||||
```
|
||||
|
||||
由于composite可以把长图合成的结果直接输出为文件,因此采用run()函数即可实现程序执行的功能。另外,当你需要调整composite参数时,可以直接修改cmd变量的值,并不需要改动程序其他部分。当你要对新的一组图片进行合成的时候,重新设置jpg_path变量就行了。
|
||||
|
||||
总结来说,使用Python调用composite合并的好处就是:你不用记住程序使用的繁杂的命令行参数,也不用记住运行逻辑,因为Python程序已经事先把逻辑编写好了。
|
||||
|
||||
# 视频的拆分与合并
|
||||
|
||||
在了解了如何使用subprocess调用composite实现长图拼接之后,我再给你讲一下如何使用subprocess库调用可执行程序,来进行视频的拆分与合并。
|
||||
|
||||
我们先来学习下视频拆分的原理。
|
||||
|
||||
你在电脑本地经常见到的视频格式是MP4,但如果要把视频放在互联网上,为了减少首次播放的加载时间,你就必须把一个MP4切分成多个文件,而且切分之后还需要把格式转换为.TS格式的视频文件。
|
||||
|
||||
为什么不直接使用MP4格式,而是要把MP4格式改成.TS格式呢?这是因为.TS格式可以保证多个文件之间的视频无缝播放,而且还会保证视频不会在播放下一个文件的时候,出现破音或画面中断等影响用户体验的情况。
|
||||
|
||||
当我们将一个视频切分成多个文件的时候,就要考虑文件的播放顺序问题了。为了记录顺序,我们需要在切分之后引入一个索引文件,这个索引文件不用手动编写,我们直接使FFmpeg命令就行了,它可以实现视频格式的转换、合并和拆分。FFmpeg命令会在切分之后,自动产生一个以.M3U8结尾的索引文件。
|
||||
|
||||
我来解释一下这个索引文件。M3U8文件是指UDF-8编码格式下的M3U视频索引,播放器通过这个索引文件就可以找到视频下所有的分段,并依次播放视频。
|
||||
|
||||
看到这儿你应该就能明白了,想要使用Python进行视频拆分,我们首先需要FFmpeg命令,然后通过Python设置FFmpeg的参数,最后再指定MP4文件和.TS文件的路径,这样就能实现拆分视频的功能了。因此我使用这样的代码来实现视频拆分:
|
||||
|
||||
```
|
||||
from subprocess import run
|
||||
input_video = "/Users/edz/Desktop/05/xxx.mp4"
|
||||
segment_time = 10
|
||||
m3u8_list = "/Users/edz/Desktop/05/xxx.m3u8"
|
||||
output_video = "/Users/edz/Desktop/05/video-%04d.ts"
|
||||
|
||||
cmd1 = ["ffmpeg", "-i", input_video, "-f", "segment", "-segment_time", str(segment_time), "-segment_format",
|
||||
"mpegts", "-segment_list", m3u8_list, "-c", "copy", "-bsf:v", "h264_mp4toannexb", "-map", "0", output_video]
|
||||
|
||||
run(cmd1)
|
||||
|
||||
|
||||
```
|
||||
|
||||
在代码中,我通过FFmpeg把MP4切分成了多段TS文件。你要想实现相同功能,首先需要在电脑中安装FFmpeg命令,它的下载地址为:[https://ffmpeg.org/download.html](https://ffmpeg.org/download.html)。
|
||||
|
||||
为了实现MP4文件格式的分割,需要使用ffmpeg非常多的参数。不过使用Python进行调用的好处,就是你不用记住复杂的参数。我们把输入文件路径、切分大小、输出的M3U8和TS文件指定为四个变量,这样只修改这四个变量,就可以实现拆分功能了。
|
||||
|
||||
如果你需要离线观看视频,就要将网络上的视频下载到本地,这时你会发现从互联网下载的格式是M3U8和TS文件。那又怎么把它们合并成MP4文件呢?
|
||||
|
||||
你同样可以使用FFmpeg命令,但是FFmpeg的参数不同。我将FFmpeg的命令写在这里:
|
||||
|
||||
```
|
||||
ffmpeg -allowed_extensions ALL -protocol_whitelist "file,http,crypto,tcp,https" -i index.m3u8 -c copy out.mp4
|
||||
|
||||
```
|
||||
|
||||
如果你不想背诵这么长的参数,完全可以仿照Python整合拆分视频的代码来实现合并功能。先FFmpeg命令和参数放入列表,再把M3U8文件和MP4文件放入变量,便于你合并新的视频的时候进行重新赋值。
|
||||
|
||||
所以你看,相比直接使用FFmpeg,subprocess调用FFmpeg的优势就在于两点,一是不用记住复杂参数,二是对批量转换视频非常有利。举两个例子。
|
||||
|
||||
如果你是视频剪辑的专业工作者,肯定要大量使用FFmpeg更复杂的功能,这些功能对应的参数一般都比较多,而且参数很多都使用了简写和大小写, 很难记忆。但要是使用Python调用的话,你可以直接更改要操作的文件路径,就不必记录大量的参数。
|
||||
|
||||
另外需要进行视频的批量转换时,可以通过第一讲的循环操作对视频任务批量处理,这样就避免了手动逐个修改书写文件的操作,从而提高视频转换的效率。
|
||||
|
||||
# 小结
|
||||
|
||||
最后,我来为你总结一下这节课的主要内容。
|
||||
|
||||
通过对subprocess库的讲解,你知道了怎样通过它实现Python加载外部可执行程序,并且能够对程序执行的结果进行处理。
|
||||
|
||||
我也为你讲解了长图拼接和视频拆分合并的两个案例,帮你更好地理解Python为什么会被称作“胶水”语言。
|
||||
|
||||
我还想强调一下,通过Python调用可执行程序的用法非常常见,特别是在多媒体处理、自然科学、AI等领域里。在这些专业领域,为了加快计算速度,通常会使用C++语言实现专业程序。
|
||||
|
||||
这些专业程序参数多、功能单一,且使用命令行执行,当你需要多次执行这些程序,又不想背诵它们的参数的时候,就可以利用Python的判断循环功能,结合C++语言实现的专业程序,来实现批量执行和减少参数手动输入的工作,提高你的工作效率。
|
||||
|
||||
最后,我也把这节课的代码附上,你可以查看。[本讲代码](https://github.com/wilsonyin123/python_productivity/blob/main/%E6%96%87%E7%AB%A05%E4%BB%A3%E7%A0%81.zip)
|
||||
|
||||
# 思考题
|
||||
|
||||
在最后也请你思考一下,你在工作当中是否会使用命令行工具呢?它们能否用Python进行包装,从而避免手写复杂参数呢?
|
||||
|
||||
如果你觉得这节课有用,能解决你的办公效率问题,欢迎你点击“请朋友读”,分享给你的朋友或同事。
|
||||
@@ -0,0 +1,204 @@
|
||||
<audio id="audio" title="06 | jieba分词:如何基于感情色彩进行单词数量统计?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ab/0f/ab3a9a6782818bce7e0dcfbe2c2e370f.mp3"></audio>
|
||||
|
||||
你好,我是尹会生。
|
||||
|
||||
在涉及运营、市场的工作中,我们经常需要根据产品评论的情感分析,来了解某一产品的口碑。所谓的情感分析,就是指根据用户对产品的评论,分析出用户对产品的喜好程度。
|
||||
|
||||
最简单的,我们会**区分产品的评价是正向还是负向**的,然后根据反馈结果改变产品的特性。稍微复杂一点的,我们会**根据情感色彩将产品的评价关键词提取出来,进行统计和分类(用于更深入的分析产品)。**
|
||||
|
||||
如果靠人工对产品评价进行辨析,有很大的局限性:一个是不够公平,因为每个人对词语感情色彩的理解并不是完全一致的;另一个是产品评价有很多,而且还会不定期增加,人工分析很难保证及时性。
|
||||
|
||||
因此,在进行词语的情感分析时,我通常都会使用Python的jieba库,来自动化实现文本情感分析功能。一般需要经过三个步骤,分别是**分词、优化分词结果和情感分析**。
|
||||
|
||||
那我就先带你看看**为什么要进行分词,以及如何进行分词操作。**
|
||||
|
||||
# 如何分词?
|
||||
|
||||
要想判断一段话表达的情感是正向还是负向,就需要根据这句话中的关键词来得到情感的倾向。例如一段话中出现了“开心”“高兴”“物超所值”等正向的词语,我们就可以认定这条产品的评价是偏正向的。相反,出现“不喜欢”“差”等词语,评价就是偏负向的。
|
||||
|
||||
但是,要想从一句话中将这些表达情感的词一个一个找出来,就需要依靠专业的工具把一句话根据语义划分成多个词,再把表达情感的词语提取出来,进行情感分析。
|
||||
|
||||
为什么要先根据语义来划分词呢?这主要是因为中文句子里的每个词中间没有用空格进行分隔,没有分隔就没法进行之后的情感分析。而对中文句子按照语义进行切割的这种操作,我们就称为“分词”。
|
||||
|
||||
Python中有非常成熟的分词库,其中最流行的库是jieba库。在计算机中,实现语义分割的技术有两种,一种是从统计学的角度分词,另一种是从词库的角度基于TF-IDF算法实现分词。jieba就是采用第二种,基于词库的角度对文章进行自动分词的。
|
||||
|
||||
那我就以电商网站上的一段商品评论为例,给你演示一下jieba库是如何实现分词的。
|
||||
|
||||
```
|
||||
import jieba
|
||||
|
||||
words1="速度快,包装好,看着特别好,喝着肯定不错!价廉物美"
|
||||
|
||||
words2 = jieba.cut(words1)
|
||||
|
||||
print("/".join(words2))
|
||||
# 速度/快/,/包装/好/,/看着/特别/好/,/喝/着/肯定/不错/!/价廉物美
|
||||
|
||||
```
|
||||
|
||||
在这段代码中,我利用jieba库的cut()函数实现了自动分词功能。我刚才讲了,jieba分词是依靠词库实现的,词库里包含了提前准备好的词和词性。下图就是jieba词库的内容:
|
||||
|
||||
```
|
||||
一鼓 ru
|
||||
一鼓作气 ru
|
||||
一马当先 ru
|
||||
... ...
|
||||
|
||||
```
|
||||
|
||||
这些词库中的词,jieba是怎么识别的呢?
|
||||
|
||||
在你使用pip命令安装了jieba库之后,它会附带一个默认的词库。在官方文档中,将这个词库称作“字典”文件。这个文件包含了日常汉语的词语、词性。jieba库会先基于“字典”对文章中所有可能出现的词进行匹配。匹配之后,会生成句子中的汉字所有可能形成的词。然后再将这些词构成的有向无环图(DAG),并采用动态规划算法查找最大概率路径,尽可能不会将一个词拆分成单个汉字。最后再从“字典”找出基于词频的最大切分组合,把这分词的组合从句子中找出来,形成一个一个的词。
|
||||
|
||||
而且,为了提高分词的准确率,jieba对没有记录在字典的词(称作未登录词)也使用了分词的模型,它就是大名鼎鼎的基于汉字成词能力的HMM模型(隐马尔可夫模型)。对词库中的词和未登录词进行处理之后,jieba就可以实现自动化分词了。
|
||||
|
||||
不过,分词之后,我们还需要对分词结果进行优化。因为在分词结果中存在着大量的标点符号,还有“看着”“喝着”“包装” 等和表达产品评价的情感无关的词语,为了加快计算词语的情感速度、避免无关词语影响情感倾向判断,我们就要优化分词的结果。
|
||||
|
||||
# 优化分词结果
|
||||
|
||||
优化分词结果主要从两个方面进行优化:一方面是移除标点符号;一方面是删除和情感无关的助词、名词等。
|
||||
|
||||
我先来带你学习下怎么从分词结果中移除标点符号。
|
||||
|
||||
移除标点符号一般有两种方法:
|
||||
|
||||
1. 删除停止词(Stop Words);
|
||||
1. 根据词性提取关键词。
|
||||
|
||||
先来看看第一种,删除停止词。
|
||||
|
||||
所谓的停止词,就是指**为了节省空间和提高匹配词语情感倾向的效率,在进****行情感分析前自动过滤掉的某些字或词。**
|
||||
|
||||
停止词主要是标点符号,也可以是“啊呀呢”等语气助词。把标点符号写入停止词列表后,再使用for循环功能,将jieba分好的词和停止词列表依次匹配。如果jieba分好的词出现在列表中,就将这些词删掉。如果没有出现在列表中,就把这些词再组合成一个新的列表,后续就可以对新的列表进行情感分析。
|
||||
|
||||
删除停止词的代码如下。通过删除停止词,我们就可以得到只有汉字的分词结果。
|
||||
|
||||
```
|
||||
words2 = jieba.cut(words1)
|
||||
words3 = list(words2)
|
||||
print("/".join(words3))
|
||||
# 速度/快/,/包装/好/,/看着/特别/好/,/喝/着/肯定/不错/!/价廉物美
|
||||
|
||||
stop_words = [",", "!"]
|
||||
words4 =[x for x in words3 if x not in stop_words]
|
||||
print(words4)
|
||||
# ['速度', '快', '包装', '好', '看着', '特别', '好', '喝', '着', '肯定', '不错', '价廉物美']
|
||||
|
||||
```
|
||||
|
||||
另一种优化分词结果的方式叫做根据词性提取关键词。这种方式的优点在于不用事先准备停用词列表,jieba库就能够根据每个词的词性对其进行标注。
|
||||
|
||||
我这里为你提供了一张paddle(paddle是百度开源的深度学习平台,jieba使用了paddle的模型库)模式词性表作为参考,你可以根据jieba自动分析得到的词性结果,手动将助词、虚词(标点符号)移除。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/de/5a/dec72024563b5179f08b5c314a59da5a.png" alt=""><br>
|
||||
我把这个基于词性移除标点符号的代码也提供给你:
|
||||
|
||||
```
|
||||
# words5 基于词性移除标点符号
|
||||
import jieba.posseg as psg
|
||||
words5 = [ (w.word, w.flag) for w in psg.cut(words1) ]
|
||||
# 保留形容词
|
||||
saved = ['a',]
|
||||
words5 =[x for x in words5 if x[1] in saved]
|
||||
print(words5)
|
||||
# [('快', 'a'), ('好', 'a'), ('好', 'a'), ('不错', 'a')]
|
||||
|
||||
|
||||
```
|
||||
|
||||
在这段代码中,我在使用jieba库的posseg类实现分词的同时,也对词性进行了标注。为了让你看到更直接的结果,我只保留了形容词,因此,变量saved的列表参数就只有一个‘a’,表示保留的词类型为形容词。
|
||||
|
||||
如果你希望保留更多的词性,可以将词性表中代表每种词的英文缩写写入saved列表中,其中,我建议你在处理之后把形容词、副词、动词都保留下来,这些都有助于你进行下一步的语义情感分析。
|
||||
|
||||
在优化分词结果之后,我们就得到了只有形容词的处理结果。那么,接下来,我们需要基于这些形容词来获取产品评价的正向或负向结果,以及基于词语的情感色彩来统计单词的数量。
|
||||
|
||||
# 语义情感分析
|
||||
|
||||
对于已经分好词的语句,我们需要使用另一个库统计词的正向、负向情感倾向,这个库就是snownlp库。
|
||||
|
||||
snownlp库既能实现分词,也能计算词出现频率,以及进行情感分析。那你可能就发出疑问了:为什么不直接使用snownlp进行分词,而要使用jieba分词呢?
|
||||
|
||||
原因就在于,snownlp的算法问题,会让它对否定词划分得不够准确。例如“不喜欢”,snownlp会把这个词划分为两个独立的词,分别是“不”和“喜欢”。那么,在计算语义情感时,就会产生较大的误差。所以我们会先采用jieba进行分词,分词之后再采用snownlp来实现语义情感分析功能。
|
||||
|
||||
接下来,我带你看一下如何使用snownlp得到完成分词之后的情感分析结果。代码如下:
|
||||
|
||||
```
|
||||
from snownlp import SnowNLP
|
||||
words6 = [ x[0] for x in words5 ]
|
||||
s1 = SnowNLP(" ".join(words3))
|
||||
print(s1.sentiments)
|
||||
# 0.99583439264303
|
||||
|
||||
```
|
||||
|
||||
这段代码通过snownlp的Bayes(贝叶斯)模型训练方法,将模块自带的正样本和负样本读入内存之后,再使用Bayes模型中的classify()函数进行分类,这样就得到了sentiments属性的值,sentiments的值表示情感倾向的方向。在snownlp中:
|
||||
|
||||
- 如果情感倾向是正向的,sentiments的结果会接近1。
|
||||
- 如果情感倾向是负向的,结果会接近0。
|
||||
|
||||
可以看到,我们在刚刚的代码中得到的情感分析的结果是0.9958,非常接近1,因此这条产品的评价就是正向的。
|
||||
|
||||
情感倾向结果趋近于1或者趋近于0都是非常理想的情况,可以直接得到感情色彩比较强烈的产品评价。但是,有时候感情色彩不太强烈,在这种情况下,我们就需要根据评价的数值范围对评论进行分组,统计每组包含多少个评价。
|
||||
|
||||
这个功能也可以通过snownlp实现,我把代码写在这里,你可以参考:
|
||||
|
||||
```
|
||||
positive = 0
|
||||
negtive = 0
|
||||
for word in words6:
|
||||
s2 = SnowNLP(word)
|
||||
|
||||
if s2.sentiments > 0.7:
|
||||
positive+=1
|
||||
else:
|
||||
negtive+=1
|
||||
|
||||
print(word,str(s2.sentiments))
|
||||
print(f"正向评价数量:{positive}")
|
||||
print(f"负向评价数量:{negtive}")
|
||||
# 快 0.7164835164835165
|
||||
# 好 0.6558628208940429
|
||||
# 好 0.6558628208940429
|
||||
# 不错 0.8612132352941176
|
||||
# 价廉物美 0.7777777777777779
|
||||
# 正向评价数量:3
|
||||
# 负向评价数量:2
|
||||
|
||||
```
|
||||
|
||||
通过snownlp库配合jieba分词的结果,你就可以实现批量产品评论的自动语义情感分析了。同时,你还可以根据不断累积产品的评价,来持续优化你的产品。
|
||||
|
||||
# 小结
|
||||
|
||||
最后,我来为你总结一下对文件进行情感倾向分析的关键步骤和注意事项。实现语义情感分析功能,你必须掌握分词、优化分词结果、语义情感分析这三个步骤。
|
||||
|
||||
其中分词是实现中文语义分析的第一步,也是最基础的部分。分词的好坏决定了对每个词的情感进行标注的准确程度。如果默认的jieba分词没有正确地把词语划分,你也可以使用jieba自带的suggest_freq()函数进行词频调节。
|
||||
|
||||
举个小例子,“中”“将”两个字可以组成“中将”的词语,也可以拆开来用“我们中/将有人成功考上北大”。在不同的语言环境中,我们要通过词频调节来让它们以词的形式出现,还是以两个字的方式出现。调整的方法是:
|
||||
|
||||
```
|
||||
jieba.suggest_freq(("中", "将"), tune = True)
|
||||
|
||||
```
|
||||
|
||||
可以看到,利用调节词频使“中”“将”都能被分出来,而不会被错误地识别为一个词“中将”,通过这种方式,就可以提升jieba的识别正确率。
|
||||
|
||||
在优化分词结果这一步,你可以通过减少虚词和标点符号,通过停止词、词性的选择,来降低它们对情感分析结果的干扰。
|
||||
|
||||
最后,你还可以为snownlp增加新的流行词和网络用语,帮你更准确地分析用户对产品的喜好程度,从而提高产品定位的精确度。
|
||||
|
||||
在snownlp中,通过train()和 save()两个函数把模型训练和保存之后,就能实现扩展默认字典的功能了。此外,我在工作中还会利用这种方式增加emoji表情对应的情感倾向分析功能,以此来进一步提升snownlp分析情感倾向的准确度。
|
||||
|
||||
我将训练模型和保存训练后的模型的函数也写在这里供你参考,希望通过训练自己的模型,能够让你的产品分析更加准确。
|
||||
|
||||
```
|
||||
sentiment.train(neg2.txt,pos2.txt); # 训练用户自定义正负情感数据集
|
||||
sentiment.save('sentiment2.marshal'); # 保存训练模型
|
||||
|
||||
```
|
||||
|
||||
今天用到的代码,我都放在了 GitHub 上,你可以点击[这个链接](https://github.com/wilsonyin123/python_productivity/blob/main/%E6%96%87%E7%AB%A06%E4%BB%A3%E7%A0%81.zip)查看。
|
||||
|
||||
# 思考题
|
||||
|
||||
我给你留一道思考题,我在最后一段代码分别统计了正向和负向评价的数量,你能否根据这段代码统计一段文字中包含了多少个动词、多少个名词和多少个形容词呢?欢迎你在课程评论区留言,和我一起讨论。
|
||||
@@ -0,0 +1,309 @@
|
||||
<audio id="audio" title="07|快速读写文件:如何实现跨文件的字数统计?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b3/bf/b333aef54c97c54bce346b7465325cbf.mp3"></audio>
|
||||
|
||||
你好,我是尹会生。这节课,我们来学习下统计多个文件字数的方法。
|
||||
|
||||
之前我在出版图书的时候,每个章节都写入了一个单独的Word中,这就导致我无法使用Word自带的字数统计功能,一次性统计所有章节的字数,自然也就不能分别统计多文件的汉字、英语和标点符号的字数了。如果你在工作中也遇到过类似的情况,需要一次性统计大量的文本文件的字数,应该怎么解决呢?
|
||||
|
||||
今天我就来教你,**怎样使用Python来批量统计多个文件的字数和标点符号。**
|
||||
|
||||
首先呢,我们先学习一下简单的操作:统计单个文件的字数。掌握了Python统计字数的基本操作,批量统计也就不在话下了。
|
||||
|
||||
## 怎样实现单个文件字数统计
|
||||
|
||||
统计单个文件的字数,需要用到Python的文件读写函数和变量这两个功能。Python的读写函数是对文件内容的读取操作,变量呢,用来存储统计好的文件字数。
|
||||
|
||||
我把统计单个文件字数的操作分为三个步骤,分别为:
|
||||
|
||||
1. 先把要统计的文件读入内存;
|
||||
1. 再对读入到内存的字数数量进行统计,并用变量保存;
|
||||
1. 最后是将结果写入统计字数的专用文件中。
|
||||
|
||||
我先来带你学习一下用Python读取文件需要使用哪些函数和语法。
|
||||
|
||||
### 1.把文件内容读取到内存中
|
||||
|
||||
Python实现读取文件内容到内存的函数有三个,这三个函数原理相同,但是读取的内容多少有些不同,分别为:
|
||||
|
||||
- read() ,逐个字符读取,一直读取到文件全部内容结束;
|
||||
- readline(),读取一行内容;
|
||||
- readlines(),读取多行内容。
|
||||
|
||||
对于统计单个文件字数的需求,选择哪个读取函数更合适呢?由于统计字数需要逐个字读取再对字数进行累加,因此,我使用read()函数将文件中所有的文字读入到内存中,相比按行读取,实现起来更加简单。
|
||||
|
||||
将文件内容读入变量后,变量中的文字内容和保存在文件中的文字内容相同,接下来我再通过统计字数的函数对变量进行统计,就能统计出一个文件里的文字有多少字数了。
|
||||
|
||||
具体如何实现呢?我给你举一个例子。
|
||||
|
||||
例如,我这里有一个需要统计字数的文件e.txt,它的内容为:
|
||||
|
||||
```
|
||||
e约等于2.718281828
|
||||
|
||||
```
|
||||
|
||||
核心实现代码如下:
|
||||
|
||||
```
|
||||
import pathlib
|
||||
|
||||
file_name = "e.txt"
|
||||
|
||||
# 取得脚本所在目录
|
||||
current_path = pathlib.PurePath(__file__).parent
|
||||
|
||||
# 和脚本同目录下的文件绝对路径
|
||||
file = current_path.joinpath(file_name)
|
||||
# 打开文件
|
||||
with open(file, encoding='utf-8') as f:
|
||||
# 读取文件
|
||||
content = f.read()
|
||||
words = content.rstrip()
|
||||
number = len(words) # 统计字数
|
||||
print(number)
|
||||
# 15
|
||||
|
||||
```
|
||||
|
||||
通过这段代码,你会发现,文件的读取函数是实现字数统计的关键功能。下面,我重点给你讲解下Python是怎么读取文件的。
|
||||
|
||||
Python要想读取一个文件,需要经过打开、读取和关闭这三个步骤。
|
||||
|
||||
在代码的第11行,我通过open()函数实现了文件的打开功能。需要注意的是,我为open()函数设置了两个参数,分别是:
|
||||
|
||||
1. file变量,表示要打开文件的名称;
|
||||
1. encoding变量,表示文件的编码格式。
|
||||
|
||||
我来解释下这两个参数。第一个参数file变量的值是要操作的文件路径和文件名称,你需要正确指定它所在的路径和文件名称。第二个参数encoding变量的值指定了以哪种字符编码打开文件,错误的字符编码会导致内容读取异常,文字内容显示为“乱码”。
|
||||
|
||||
我们来具体看一下第一个参数file变量的路径是怎么正确进行设置的。
|
||||
|
||||
对file变量进行赋值时,需要注意赋值时的路径和文件的真实路径是否一致,如果file变量中的路径和真实路径不一致的话,Python程序运行的时候会提示找不到文件的错误。那应该如何确保路径正确呢?
|
||||
|
||||
我们通常的做法是事先在字符终端上取得要操作的文件的完整路径,再将完整路径作为字符串赋值给变量file,这样你就可以直接使用正确的文件路径了。但是这样操作步骤繁琐,且不够灵活。所以还有一种比较灵活的方法,那就是将要操作的文件和Python脚本文件放在相同的目录里,通过获取Python脚本文件的目录,间接获得要操作的文件目录。
|
||||
|
||||
例如我将e.txt和Python脚本文件放在同一目录,那么我在打开e.txt文件时,就要先获取Python脚本所在的目录位置了。我这里使用了一个特殊的变量__**file__,**来取得当前脚本所在的路径。
|
||||
|
||||
__file__变量比较特殊,它是在Python中预先定义好的一个变量,默认情况下它的值会被Python自动设置为当前脚本的完整路径名称。有了它,我就可以通过Python脚本的路径取得e.txt的完整路径了。获取完整路径的具体的步骤如下:
|
||||
|
||||
1. 在代码中使用pathlib库的PurePath()函数,把当前Python脚本的路径转化为标准的pathlib格式。
|
||||
1. 通过parent属性去掉脚本的文件名,取得当前脚本所在的路径。
|
||||
1. 再通过joinpath()函数把脚本所在的路径和e.txt文件名进行拼接,得到e.txt的完整路径。
|
||||
|
||||
这种取得脚本同目录下的其他文件完整路径的用法,要比直接指定文件完整路径用法更灵活,我在后续内容中会使用这种方法处理文件的路径,请你认真掌握。
|
||||
|
||||
我们继续来学习open()函数的第二个参数。第二个参数的作用是设置open()函数使用何种字符编码打开文件。我使用了"encoding=utf-8" 作为open()函数调用的第二个参数。
|
||||
|
||||
不过你可能会问了,为什么指定这个参数的时候,要特意写出变量名称encoding呢?我在[04讲](https://time.geekbang.org/column/article/343457)的时候,给你讲过函数有定义和调用两种用法。
|
||||
|
||||
在函数定义的时候我们可以为函数指定一个或多个参数,如果使用了多个参数,你在调用函数的时候需要按顺序依次传入每一个参数。但是如果你想要使用某个参数默认值或不想按照顺序传入参数时怎么办呢?函数调用时,还可以根据参数的关键字来指定为函数传入哪些参数。比如说open()函数的参数有7个,为了便于你理解关键字参数,我将open()函数如何定义的前四个参数写在这里,open()函数的定义如下:
|
||||
|
||||
```
|
||||
def open(file, mode='r', buffering=-1, encoding=None, ...):
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
在open()函数的定义中,encoding参数是open()函数的第4个参数。如果我们只需要第1个和第4个参数,其他参数需要保持默认时,就可以使用如下格式调用open()函数:
|
||||
|
||||
```
|
||||
open(file, encoding="utf-8", ...):
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
在代码中,调用open()函数时,由于file参数是open()函数的第一个参数,因此可以不用指定参数的关键字。encoding参数并非open()函数定义的第二个参数,而是第四个参数,所以需要指定它的参数名称为encoding,根据open()函数的定义,encoding="utf-8"会作为open()函数的第四个参数使用。这种使用关键字作为函数的参数的方式,也被称作函数的关键字传参方式。
|
||||
|
||||
通过encoding参数可以指定文件的字符编码,一般在macOS系统文字的编码为UTF-8编码,Windows下为GB2312编码。不同操作系统下打开文本文件需要使用不同的文件编码,这样可以避免文件打开出现乱码的问题。
|
||||
|
||||
在你正确使用了open()函数的两个参数以后,会返回一个表示文件的对象f, 只有设置了正确的文件路径、文件名称和字符编码,才能继续进行文件读取操作。文件的读取操作使用的是read()函数,read()函数会根据文件的对象f,按照open()函数定义好的打开方式进行逐个字的读取操作。Python对文件的其他操作,全都需要通过文件操作函数调用对象f来完成。
|
||||
|
||||
另外要特别注意的是,在open()函数这一行的开头,我还使用了一个关键字with,with关键字下面的代码是缩进形式,和with关键字形成了一个语句块(具体参见[导读](https://time.geekbang.org/column/article/340385)),在with语句块结束之后,不必手动调用close()函数关闭文件,Python会自动将文件关闭。因此,你会看到我在程序中调用了open()函数打开文件,但是没有调用关闭文件的close()函数。
|
||||
|
||||
总的来说,相对于手动调用close()函数来关闭文件,使用with函数减少了打开过多文件造成系统资源浪费和数据丢失的风险。
|
||||
|
||||
### 2.统计文件的字数
|
||||
|
||||
打开了文件之后,我们就可以来统计文件的字数了,我们可以直接利用Python的内置函数len()来实现。
|
||||
|
||||
len()函数在Python中最初设计的功能就是统计字符串的长度,即字符串中有多少字符。因此我利用len()函数可以对字符串数据类型进行操作的特性,将文件一次性读入内存,放入字符串数据类型中。我在统计字数之前还增加了一个rstrip()函数,它会自动剔除出现在文件末尾的空行、空格,让你统计的结果更精确。
|
||||
|
||||
在进行文件字数统计的时候,需要注意,在代码中我使用了read()函数实现了对文件内容的一次性读取到内存的功能。如果你的文件较大,我建议你使用readline()函数按行读取,并逐行统计字数,否则容易出现内存不足的问题。
|
||||
|
||||
### 3.将统计结果写入文件
|
||||
|
||||
如果你需要将统计结果写入文件,可以对open()函数打开文件方式进行简单的修改,就可以实现文件写入功能了,写入文件的代码如下:
|
||||
|
||||
```
|
||||
with open("total.txt", "w", encodong="utf-8") as f:
|
||||
f.write("15个字符")
|
||||
|
||||
```
|
||||
|
||||
通过上面的代码,我把"15个字符"这串字符串写入到空白文件total.txt中了。
|
||||
|
||||
open()函数的第二个参数表示文件的打开方式,它默认值是“r”,代表了文件以读取方式进行打开。这时对象f只能进行文件读取操作,我们需要将第二个参数指定为"w",就可以使用wirte()函数对文件进行写入操作了。而write()函数的第一个参数就是即将要写入文件的内容。
|
||||
|
||||
在为你介绍完单个文件的读写操作之后,我带你掌握了将文件读取到字符串变量,通过统计字符串的长度计算文件字数的方法,那么接下来我为你讲解一下如何统计多个文件的字数。
|
||||
|
||||
## 怎么统计多个文件的字数?
|
||||
|
||||
通过单个文件的字数统计功能的实现,我们发现每次对文件进行字数统计的时候,都需要进行文件的打开、读取和关闭操作。因此我们对多个文件的字数统计,就可以使用for循环来实现批量读取多个文件的内容。在这里我先直接给出代码。
|
||||
|
||||
```
|
||||
p = Path(src_path)
|
||||
files = [x for x in p.iterdir() if PurePath(x).match('*.txt')]
|
||||
for file in files:
|
||||
with open(file, encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
words = content.rstrip()
|
||||
number = len(words)
|
||||
|
||||
|
||||
```
|
||||
|
||||
我来解释一下多文件统计的代码和单文件统计代码的区别。
|
||||
|
||||
你会发现,统计多个文件的字数时,除了使用到[第一节课](https://time.geekbang.org/column/article/340388)讲的for循环之外,还要使用合适的变量数据类型,用来存储多个文件字数统计过程中的文件字数。
|
||||
|
||||
在[导读](https://time.geekbang.org/column/article/340385)中我也给你介绍过,Python支持的数据类型包括:数字、字符串、列表、元组、字典五种,我在这里比较一下它们各自的特点,让你知道该怎么选择合适的数据类型来存放计算的中间结果。
|
||||
|
||||
**首先是我们在前面几讲用到最多的数字和字符串类型**,这两种类型也被称作基本类型。就像它们的名字一样,分别用来存储数值和一串字符。
|
||||
|
||||
数字类型是一种非常适合进行整数、浮点数的转换和算数几何计算最常用的类型。在单文件字数统计的场景,用来储存每个文件的字数是最合适的类型。如果把场景扩展到统计多个文件的字数,使用一个数字类型的变量是不够的,你需要在循环中再增加一个新的数字类型的变量,对多个文件的统计结果进行依次累加就可以了。
|
||||
|
||||
那其他的类型适合用来进行字数统计吗?
|
||||
|
||||
字符串类型会使用英文的单引号或双引号来创建一串字符,它内置的功能一般为多个字符串进行连接和截取字符串里的某些字符的,显然不适合进行字数累加的统计。
|
||||
|
||||
**那列表和元组有什么特性呢?**当你需要记录多个并列结果的数字或字符串时,我们就可以把数字和字符串放在列表和元组数据类型中。而列表和元组中的每个元素都会被分配一个位置,也有人把这里的位置称作索引,第一个元素的索引是0,第二个是1,以此类推。
|
||||
|
||||
例如对多个文件进行字数统计的场景下,我们就可以将五个文件字数统计的结果分别放入到列表当中,它的写法如下:
|
||||
|
||||
```
|
||||
list1 = [15, 20, 35, 40, 50 ]
|
||||
|
||||
```
|
||||
|
||||
list1 就是一个列表数据类型的变量,它包含了五个元素,第一个元素或索引为0的元素是15,第二个是20。
|
||||
|
||||
**元组**和列表极为相似,不同之处在于元组的元素不能修改,这个特性在Python中,叫做内容“不可变”。这是它与列表的主要区别,元组查找的效率要远远高于列表,因此在进行查找操作时,经常将列表类型转换为元组类型,再进行内容查找操作。
|
||||
|
||||
最后一种类型,也是我们在[第二讲](https://time.geekbang.org/column/article/341220)接触过的**字典类型**。字典的每个键值对都用冒号进行分隔,每个键值对之间用逗号分割,字典需要被包括在花括号 **{}** 中,字典类型强调键值对的映射关系。如果你需要记录文件名和统计结果时,可以使用字典方式保存。保存形式如下:
|
||||
|
||||
```
|
||||
dict1 = {"file1.txt":15,"file2.txt": 20, "file3.txt":35}
|
||||
|
||||
```
|
||||
|
||||
好了,掌握了python支持的五种数据类型,以及它们各自的适用场景,我把它们的特点总结在下面的表格。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/45/d4/45yyd7813680c730431c9c3eb1d145d4.png" alt="">
|
||||
|
||||
那我接下来就给你分享进行字数统计时的两种常见的场景需求。
|
||||
|
||||
### 多个文件记录字数,用列表数据类型更适合
|
||||
|
||||
由于我们的需求是实现跨文件的字数统计,那么就需要一个数据类型来记录每一个文件的字数有多少。根据我们刚才对不同数据类型的分析,列表就是非常适合存储每个文件字数的数据类型。
|
||||
|
||||
在多文件字数统计的场景下,我们只需要其中的append()内置方法,就可以实现对列表添加元素了。如果你想了解列表类型更多的内置方法,可以参考[官方文档](https://docs.python.org/zh-cn/3.9/library/stdtypes.html#sequence-types-list-tuple-range)来查看它们的定义和用法。
|
||||
|
||||
append()内置方法,可以在列表的最后一个位置添加一个新的元素。例如当前统计的五个文件字数就可以采用列表方式存储,它的定义方式如下:
|
||||
|
||||
```
|
||||
list1 = [15, 20, 35, 40, 50 ]
|
||||
|
||||
```
|
||||
|
||||
list1 变量经过赋值,得到的列表包含了5个数字类型。当统计第6个文件的字数,假设字数为“30”个字时,就可以使用list1.append(30) 的方式在列表后增加元素。增加后list1变量为:
|
||||
|
||||
```
|
||||
list1 = [15, 20, 35, 40, 50, 30 ]
|
||||
|
||||
```
|
||||
|
||||
对于多个文件的字数统计,要操作的数据类型为列表,而len()函数只能支持字符串,不能支持列表。所以我们需要把统计列表元素和的函数由len()改为sum()函数。另外,sum()是能够直接支持列表类型的求和函数,所以这6个文件的字数,就可以使用sum()函数进行累加,从而得到多个文件的总字数了。它的代码如下:
|
||||
|
||||
```
|
||||
sum(list1)
|
||||
|
||||
```
|
||||
|
||||
上面就是我们通过for循环和列表数据类型实现了对多个文件字数的统计和累加。接下来我们把需求再扩展一下,如果我需要分类统计,比如说想要统计中文、英文、标点符号各自的数量,那应该选择什么样的数据类型呢?
|
||||
|
||||
### 扩展需求:统计中文、英文和标点符号各自的数量
|
||||
|
||||
想要实现中、英文和标点符号各自的数量统计,我们就需要考虑继续使用列表数据类型是否能够存储各自的数量统计结果?是否有更适合的数据类型?
|
||||
|
||||
关于存放每个文件的统计结果,我有两种考虑。
|
||||
|
||||
- 一种是中、英文和标点符号各自的数量统计以后,需要比列表更合适的数据类型,用来存放每个文件的统计结果。
|
||||
- 一种是每个文件计算字数之后,如何和前一个文件的字数进行累加。
|
||||
|
||||
先来看第一种。
|
||||
|
||||
要想统计中、英文和标点符号各自的数量,你需要正确地书写每个字符。在Pyhon字符串类型中,有区分每个字符类型的内置函数。
|
||||
|
||||
因此,我使用字符串类型先对每个文件包含的字符进行中文、英文、数字、空格、特殊字符的划分,并使用数字类型的变量对每种类型的数量进行了记录。
|
||||
|
||||
我将实现这一功能的核心代码贴在这里供你参考:
|
||||
|
||||
```
|
||||
import string
|
||||
for s in str:
|
||||
# 英文
|
||||
if s in string.ascii_letters:
|
||||
count_en += 1
|
||||
# 数字
|
||||
elif s.isdigit():
|
||||
count_dg += 1
|
||||
# 空格
|
||||
elif s.isspace():
|
||||
count_sp += 1
|
||||
# 中文
|
||||
elif s.isalpha():
|
||||
count_zh += 1
|
||||
# 特殊字符
|
||||
else:
|
||||
count_pu += 1
|
||||
|
||||
```
|
||||
|
||||
在这段代码中,我使用了for循环来遍历每个字,然后通过if分支结构进行判断,根据不同的类型对每次指定的类型进行累加。这样就完成了每个文件对字符数量的统计。
|
||||
|
||||
完成统计后,又该如何对它们进行保存呢?我这里使用了字典+列表的方式,用列表存储每个文件每一种字符的数量,为了能区分列表,我为它们分别取了一个名字,并把它们统一存储在字典当中。形成了字典的key是字符串,value是列表的数据类型,它的格式如下:
|
||||
|
||||
```
|
||||
{'count_en': [7, 7], 'count_dg': [0, 0], 'count_sp': [1, 1], 'count_zh': [2, 4], 'count_pu': [5, 5]}
|
||||
|
||||
```
|
||||
|
||||
如何得到这种格式呢? 为了得到这种格式,我继续使用了列表的内置的append()函数,通过下面的代码实现了每个文件的每种类型分开进行统计的功能。
|
||||
|
||||
```
|
||||
word_count["count_en"].append(count_en)
|
||||
word_count["count_dg"].append(count_dg)
|
||||
word_count["count_sp"].append(count_sp)
|
||||
word_count["count_zh"].append(count_zh)
|
||||
word_count["count_pu"].append(count_pu)
|
||||
|
||||
```
|
||||
|
||||
总结来说,在进行不同类型的字数统计的过程中,我通过字符串和数字这两种数据类型实现了单个文件的字数统计。在多个文件的数量合并功能中,我又使用了字典和列表两种数据类型实现了多个文件中间结果的存储和最终字数统计。
|
||||
|
||||
Python丰富的数据类型,可以让你更灵活地处理工作中的数据。
|
||||
|
||||
我将完整代码放在github中,你可以参考[完整代码](https://github.com/wilsonyin123/python_productivity/blob/main/%E6%96%87%E7%AB%A07%E4%BB%A3%E7%A0%81.zip)来理解我是如何在计算过程中使用字典、列表和字符串类型的。
|
||||
|
||||
## 总结
|
||||
|
||||
通过跨文件字数统计这个场景,我为你讲解了Python的文件操作函数和如何选择更合适的数据类型。
|
||||
|
||||
在进行文件的读写操作时,你需要注意被读写文件的路径、打开模式、字符编码这三个主要问题。同时,这三个问题也是新手在操作文件时会经常出现的错误的地方。
|
||||
|
||||
尤其是打开模式的错误的设置为“w”将重要文件覆盖写入的问题,在python初学者中经常出现,建议你先采用判断文件是否可读取的方式,避免对重要文件进行误写。
|
||||
|
||||
现实工作中的场景往往是比较复杂的,我们经常要根据不同的场景,把文件内容读取到不同的数据类型的变量中。Python丰富的数据类型可以让你更灵活的操作工作中的数据,通过选择合适的数据类型,也能使用各种数据类型自带的函数,减少手动实现自定义函数的代码,节省你的代码编码时间。
|
||||
|
||||
## 思考题
|
||||
|
||||
最后我想留一道思考题给你,在进行多个文件的中、英文和标点符号各自的数量统计时,你会选择哪种数据类型,用来存放每个文件的字数统计数据呢?欢迎你来说出自己的想法。
|
||||
@@ -0,0 +1,171 @@
|
||||
<audio id="audio" title="08|正则表达式:如何提高搜索内容的精确度?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7d/ee/7d65866cbf816a76200233fd29496eee.mp3"></audio>
|
||||
|
||||
你好,我是尹会生。
|
||||
|
||||
开始上课之前,我想先带你看两种常见的工作需求:
|
||||
|
||||
1. 领导让你搜索出一个文档里所有的手机号码, 而你只知道手机号的模式是11位数字,那该怎样搜索呢?
|
||||
1. 你需要在一个文档中搜索一串带区号的电话号码,比如010-12345678,099-1234567 ,不过在这个文档中,有很多组电话号码,它们的区号和电话号码长度都不同,你该怎么一次性把它们全部搜索出来呢?
|
||||
|
||||
其实,这两种场景具有一个共同点:就是你要一次搜索出所有的结果,且不能使用具体的数字,这该怎么办呢?答案是使用某种模式进行搜索。
|
||||
|
||||
就像搜索手机号,我可以使用11位数字这种模式。搜索电话号码我就可以用3-4个数字或7-8个数字加上“-”的模式。这就是**利用模式匹配的方法把**手机和电话号码从文档中找出来,提高文本搜索的效率。
|
||||
|
||||
对于模式匹配,使用Python的**正则表达式**是最便捷、也是最有效率的。这节课,我就来手把手教你用正则表达式来提高搜索效率。
|
||||
|
||||
## 使用正则表达式进行搜索的整个过程
|
||||
|
||||
首先,我先带你来看下使用正则表达式进行搜索的整个过程,利用正则表达式可以按照模式来匹配搜索结果,而且比自己编写函数搜索功能更加强大,而且实现起来也更加简洁。
|
||||
|
||||
要实现搜索,我们就要用到Python的正则表达式库(叫做re库)中的search()函数,这是利用正则表达式实现搜索的核心函数。如果能够搜索到匹配模式的结果,就返回匹配字符串出现的文件的位置,以及匹配的字符串内容。接下来我们重点看一下它的参数。
|
||||
|
||||
re.search()函数有三个参数,它的前两个参数我们在工作中较为常用。
|
||||
|
||||
1. 参数pattern:代表要匹配的模式。
|
||||
1. 参数string:是要匹配的字符串。如果模式能够匹配成功,则会返回一个re的对象,re对象里存储的是匹配位置和匹配内容;如果匹配失败,就会返回空,在Python中用变量“None”表示空。
|
||||
1. 参数flags:表示search()函数在匹配之前可以进行各种特殊处理,比如使用search()函数进行搜索之前,可以通过flag参数忽略要匹配字符串的大小写功能。
|
||||
|
||||
那具体怎么使用呢?我以搜索手机号码为例来给你解释下。我可以使用re.search()按如下的代码搜索一串手机号码:
|
||||
|
||||
```
|
||||
re.search("[0-9]{11}","13855556666")
|
||||
<re.Match object; span=(0, 11), match='13855556666'> #执行结果
|
||||
|
||||
```
|
||||
|
||||
代码的第一行,search()函数是一个基于正则表达式进行搜索的函数,它的第一个参数是正则表达式,第二个参数是要匹配的文字,目前它只有一个手机号码。
|
||||
|
||||
第二行代码是search()函数执行的结果。因为匹配成功之后会返回比较丰富的信息,所以我们需要根据搜索结果来不断优化正则表达式,达到精确匹配的目的,但是搜索结果还很复杂,所以我要带你分步骤地去分析匹配的结果都包含了哪些主要内容。
|
||||
|
||||
**第一步:观察匹配的结果。**
|
||||
|
||||
**如果<strong><strong>模式**</strong>匹</strong>配成功,search()函数会返回一个re对象。如果匹配不成功的话,它的返回结果则是None。我们可以利用不同的返回结果,使用Python的if判断条件来控制程序的后续运行逻辑。
|
||||
|
||||
**第二步:观察字符串的子串是在哪些位置被匹配成功的。**
|
||||
|
||||
代码中的span=(0, 11) 表示第1个字符到第11个字符匹配成功。这里需要注意的是Python字符串的下标是以0开头的。通过下标,你可以确认匹配的内容是否是你编写的模式,如果匹配的位置或者内容不正确,可以通过下标来帮你定位匹配错误的位置,以便优化匹配模式。
|
||||
|
||||
**最后一步,观察匹配成功的子串是否是我们想要查找的字符串内容。**
|
||||
|
||||
如果是的话,那么此次查找结束。如果不是,我们就需要根据匹配的位置和结果,使用search()函数重新编写正则表达式。
|
||||
|
||||
这就是使用正则表达式进行搜索的整个工作过程。那接下来我们就来学习一下正则表达式的两种匹配方式。
|
||||
|
||||
## 两种匹配方式
|
||||
|
||||
编写正则表达式之前,一定要根据字符出现的不同模式来选择不同的匹配方式。匹配方式分为精确匹配和模糊匹配两种。
|
||||
|
||||
精确匹配需要在匹配前,就知道将要匹配的字符是什么,且出现了多少次。这类匹配使用的是一组元字符。模糊匹配是在匹配前不清楚出现的字符具体内容或不清楚字符的重复次数,这种匹配需要使用另一组元字符。我们先来看看精确匹配方式。
|
||||
|
||||
### 精确匹配
|
||||
|
||||
在手机号码搜索的场景中,我使用了正则表达式 “[0-9]{11}”,你可以看到,我除了用数字外,还使用了很多特殊字符。**这些特殊字符**有一个专用的名字,**叫做元字符,正则表达式就是利用元字符进行模式匹配工作的**。
|
||||
|
||||
在正则表达式“[0-9]{11}”中包含了两组元字符,“[]”和“{}” 。
|
||||
|
||||
- “[]”的作用是进行内容模式匹配;
|
||||
- “{}”的作用是指定匹配长度。
|
||||
|
||||
我们先从[0-9]来认识元字符吧。
|
||||
|
||||
在正则表示式“[0-9]” 中, “[]”表示同一个位置可能出现的各种字符。例如:我需要从文档中匹配到一个数字,可以使用[1234567890]的形式,也可以使用[0987654321]的形式。而在[]中,字符排列的顺序并不会影响它的功能,出现重复字符也不会影响它的功能。
|
||||
|
||||
不过为了代码更整洁,我不建议你在[]中使用重复的字符。
|
||||
|
||||
使用[]对一个数字进行匹配的时候,如果从0写到9,一一列出比较麻烦,所以在[]中还支持一个**“-”符号,叫做连字符**。把连字符与数字写在一起,表示数字的范围。当然,你肯定会想到还有英文字母的场景,比如从A到Z或者从a到z,表示英文字母的范围。具体写法如下:
|
||||
|
||||
```
|
||||
[0-9] #匹配任意一个数字
|
||||
[a-zA-Z] #匹配任意一个字母
|
||||
|
||||
```
|
||||
|
||||
除了可以自己手写匹配字母和数字外,还有另外一种写法,就是把我们经常需要匹配的字符进行定义。我为你提供一张POSIX字符组合和ASCII字符组合的常用对照表,帮你来记住它们。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/21/3b/2192329c5985b9yye91906ec14832f3b.png" alt="">
|
||||
|
||||
在你掌握了元字符“[]”的用法之后,我们接着来看怎么通过“{}”来限定“[]”的匹配长度。
|
||||
|
||||
在正则表达式 “[0-9]{11}”中, 我使用了{11}来限定它前边的字符出现11次。{}还可以使用**{m,n}的形式,表示限定的次数从m到n次**。这是在匹配不确定字数的情况下,经常用到的写法,例如进行电话号码的区号匹配,可以使用“[0-9]{3,4}”的形式匹配3个或4个数字。
|
||||
|
||||
**使用“[]、{}” 两种元字符,属于精确的模式匹配。如果**你在编写正则表达式之前就知道,即将匹配的字符是数字还是英语,以及它们出现的次数,那就使用这两个元字符对将要进行搜索字符进行匹配。
|
||||
|
||||
### 模糊匹配
|
||||
|
||||
不过还有一种情况就是,你只知道某个元素会重复出现多次,但是在匹配前不知道具体会出现多少次,或者只知道要匹配的是五个字符,但是不知道这五个字符到底是数字、汉字还是英文,这种匹配的行为就叫做**模糊匹配**。
|
||||
|
||||
例如下面三个元字符“+”、“*”、“?”可以实现出现次数的模糊匹配功能。
|
||||
|
||||
- **“+”<strong>表示前边的元素出现的是**1到无穷多次</strong>。
|
||||
- **“*”<strong>表示前面的元素出现的是**0次到无穷多次</strong>。
|
||||
- **“?”<strong>表示前面的元素出现的是**0次或一次</strong>。
|
||||
|
||||
我以正则表达式“ab+c”为例,它表示在这个模式中b至少出现了一次,最多出现无穷多次。也就是说,我们能够通过search()函数搜索到abc、abbc、abbbc、abbbbc等模式。
|
||||
|
||||
如果匹配的模式再复杂一些,假设说在匹配之前我们只知道第一个字母是a,最后一个字母是b,中间是五个任意的字母。在这种情况下,就需要使用元字符“.”符号, 它在正则表达式中能匹配任意的单个字符。
|
||||
|
||||
为了让你更好理解元字符“.”, 我们就来看一个搜索任意5个字符的例子,代码如下:
|
||||
|
||||
```
|
||||
re.search(".....", "aaa13855557890bbb")
|
||||
re.search(".{5}", "aaa13855557890bbb") # 也可以这样写
|
||||
|
||||
```
|
||||
|
||||
上面代码匹配出的结果是"aaa13"。
|
||||
|
||||
这里需要提醒你注意,如果你想匹配的字符串只包含5个字符,那就需要增加匹配的限定条件的元字符“^”和“$”。**“^”表示从第1个字符开始匹配。“$”表示从结尾开始匹配。**所以你需要将正则表达式"....."改为"^.....$"形式,才能实现只匹配5个字符的模式。
|
||||
|
||||
通过上面的“[] {} . ? * +”这六种元字符组合,你就能够使用search()函数通过正则表达式搜索到大部分的模式了。
|
||||
|
||||
## 提取和替换:如何处理搜索到的结果
|
||||
|
||||
当你搜索到你想要的文字之后,往往还需要进行后续的内容提取和替换处理,这些后续处理如果手动完成也属于低效的工作。因此,接下来我来教你怎样将search()函数搜索到的内容进行提取,以及如何使用sub()函数实现将正则表达式匹配到的内容进行替换。我们先来看怎么进行内容的提取。
|
||||
|
||||
以电话号码为例,如果需要将搜索的结果提取出来,可以使用group()函数,进行search()函数搜索之后的内容提取操作。
|
||||
|
||||
我们先来看一下提取搜索结果的例子:
|
||||
|
||||
```
|
||||
import re
|
||||
re.search("[0-9]{3}-[0-9]{8}",
|
||||
"我的电话号码:010-12345678.").group(0)
|
||||
'010-12345678' # 执行结果
|
||||
|
||||
```
|
||||
|
||||
通过group(0)函数的参数“0”,如果搜索过程中能够匹配成功,search()函数就会把匹配到的第一个字符串作为执行结果,返回给用户继续进行自动化处理。
|
||||
|
||||
再进一步,如果我要替换匹配到的字符串,那就需要**把search()函数改为sub()函数。**sub()函数包含三个参数,依次是:**要匹配的正则表达式、匹配后被替换的字符串、匹配的原始文档。**
|
||||
|
||||
那我就以替换字符串为例,来教你怎么把文档中的yes字符串替换为no字符串。代码如下:
|
||||
|
||||
```
|
||||
re.sub("(Y|y)(es)*", "No", "aayesbb")
|
||||
'aaNobb' # 执行结果
|
||||
|
||||
```
|
||||
|
||||
在这段代码中,为了将“Y、yes、Yes”,三个字符串都替换为“No”。同时,为了不用多次执行sub()替换函数,我使用了“()”分组功能和“|”选择功能。那么通过(Y|y)这一写法,就能实现第一个字符匹配“Y”和“y”;利用“()”分组功能,使用“(es)*”正则表达式实现了第二、三个字符是“es”或是没有字符的功能。
|
||||
|
||||
总结来说,通过group()函数和re.sub()函数,我们可以在搜索之后自动化实现内容的提取和替换操作。
|
||||
|
||||
## 小结
|
||||
|
||||
在最后我来为你总结一下搜索中的常见问题,以及对应的解决方案。
|
||||
|
||||
如果遇到了大量的需要按照某种模式才能得到搜索结果的场景,你可以在第一时间采用正则表达式的方法进行内容的匹配。如果字符长度固定,可以使用精确搜索的元字符“[]”和“{}” 。反之,在字符长度不固定的情况下,那就使用模糊搜索的元字符“+”、“*”、“?”“.”“^”和“$”。
|
||||
|
||||
正则表达式的元字符组合非常灵活,为了方便你记忆,我把它们分为三类:
|
||||
|
||||
1. 匹配单个字符,要使用“[ ]”和“.”元字符。
|
||||
1. 控制元字符出现次数,要使用“?”、“+”和“*”元字符。
|
||||
1. 控制元字符的顺序和位置,要使用“^”、“$ ”、“ | ”和“ ()”元字符。
|
||||
|
||||
在掌握正则表达式的正确编写基础上,你就可以通过re库的函数实现按模式搜索、内容提取和内容替换了。正则表达式不但在Python中能够使用,它还能在搜索引擎以及大部分的编程语言中使用,被广泛应用与字符串搜索和替换工作中。
|
||||
|
||||
我把这节课[相关的代码](https://github.com/wilsonyin123/python_productivity/blob/main/%E6%96%87%E7%AB%A08%E4%BB%A3%E7%A0%81.zip)给你放在了GitHub中,你可以参考。
|
||||
|
||||
## 思考题
|
||||
|
||||
最后我给你留一道思考题,你将如何使用re库的search()函数,实现身份证的匹配和提取功能呢?
|
||||
@@ -0,0 +1,302 @@
|
||||
<audio id="audio" title="09|扩展搜索:如何快速找到想要的文件?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0c/25/0c925ef9a054d7692b525d29f39f9f25.mp3"></audio>
|
||||
|
||||
你好,我是尹会生。
|
||||
|
||||
工作时间越久,你就会发现,自己电脑里存储的办公文件也越来越多。一旦需要紧急找到某个文件,你又恰巧忘记了文件被存放的具体位置,就只能通过Windows的搜索功能对文件名称进行搜索,不过你大概率需要长时间等Windows的搜索结果。
|
||||
|
||||
之所以查找文件的时间过长,主要有两个原因。
|
||||
|
||||
1. 搜索范围太大。搜索过程中可能包含了大量的操作系统或应用软件的缓存文件等无效路径,导致搜索时间过长。
|
||||
1. 受到硬盘文件数量过多的影响。硬盘的文件数量越多,完整搜索一遍所有文件的时间就越长。
|
||||
|
||||
那有没有办法提高搜索的效率,快速找到你想要的文件呢?基于以上两种原因,相应的,我们可以在Python中采用指定搜索路径范围和提前建立文件索引的两种方案实现文件的搜索功能。
|
||||
|
||||
这两种方案都是基于Python的基本搜索方法实现的,因此我先来带你学习一下如何用Python的pathlib库实现基础文件的搜索。
|
||||
|
||||
## 基础搜索方法:用pathlib库搜索文件
|
||||
|
||||
用Python搜索文件时需要使用pathlib库的glob()函数和rglob()函数,glob()函数可以实现基于文件名的搜索方法,rglob函数可以实现基于扩展名的搜索方法。
|
||||
|
||||
我先来给你看一下Python实现基础搜索功能的代码,通过代码来为你讲解一下Python是如何搜索到一个文件的:
|
||||
|
||||
```
|
||||
from pathlib import Path
|
||||
|
||||
base_dir = '/Users/edz/Desktop/'
|
||||
keywords = '**/*BBC*'
|
||||
|
||||
# 遍历base_dir指向的目录下所有的文件
|
||||
p = Path(base_dir)
|
||||
|
||||
# 当前目录下包含BBC的所有文件名称
|
||||
files = p.glob(keywords)
|
||||
# files的类型是迭代器
|
||||
# 通过list()函数转换为列表输出
|
||||
# print(list(files))
|
||||
|
||||
# xlsx结尾的文件
|
||||
files2 = p.rglob('*.xlsx')
|
||||
print(list(files2))
|
||||
|
||||
# 遍历子目录和所有文件
|
||||
files3 = p.glob('**/*')
|
||||
print(list(files3))
|
||||
|
||||
```
|
||||
|
||||
在这段代码中,我实现了Python的基础搜索功能。由于搜索的优化要基于glob()函数和rglob()函数进行,为了能更好的提升Python的搜索的效率,我来带你逐一分析一下glob()函数和rglob()函数的参数和返回值。
|
||||
|
||||
**首先我来带你看一下glob()函数和它的参数**, 由于glob()进行匹配的是文件的路径和文件名称方式,如: "c:\somepath\to\filename_include_BBC_voice.exe" , 而我们进行文件搜索的时候一般会使用关键字,如“BBC”,因此在搜索时我们需要为关键字加上通配符的形式,如“**BBC**” 。
|
||||
|
||||
**通配符是类似正则表达式的元字符的一种特殊符号**,它不能用在正则表达式中,只能用在glob(全称global)匹配模式中。
|
||||
|
||||
我将glob()和rglob()函数常用的通配符,以及它们的功能整理成一个表格,供你学习和参考。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c2/d5/c23a69ea88eda1644df17b734df491d5.png" alt="">
|
||||
|
||||
通过表格我希望你能掌握**如何将通配符和要搜索的关键字进行组合**。比如说,通过使用“**/**BBC**”的方式,我就可以搜索到Path()函数指定目录下所有包含BBC关键字的文件名。
|
||||
|
||||
**接下来我再来带你学习一下rglob函数和它的参数**。rglob函数是从文件路径末尾向前进行匹配的,这是它和glob()函数的主要区别, 基于rglob()函数的搜索顺序特点,经常被我们用于进行扩展名的搜索,比如说采用rglob('*.xlsx')就可以搜索所有的xlsx扩展名文件,要比使用glob()编写的模式匹配更简单,参数的含义也更清晰。
|
||||
|
||||
**最后我再来带你看一下glob()和rglob()函数的返回值**,有一点我需要提醒你:它们的执行结果是我们之前课程中没有接触过的一种新的数据类型,这种类型叫做“迭代器”。
|
||||
|
||||
顾名思义,迭代器的特点是这个数据类型可以支持迭代操作,执行一次glob()或rglob()函数只返回一个结果。要想得到迭代器所以的值可以使用两种方法。
|
||||
|
||||
一种是使用list()函数将迭代器转换为我们所熟知的列表数据类型,例如我在列表中就是用了“list(files3)”方式将迭代器转换为了列表。
|
||||
|
||||
还有一种方式是使用for循环的方式对迭代器的值进行逐一处理。
|
||||
|
||||
这两种用法不但可以获取glob()返回值,今后我们遇到的迭代器都可以使用list()函数和 for循环取得它的全部值。
|
||||
|
||||
通过对glob()和rglob()函数的参数和返回值的学习,相信你已经掌握了使用功能Python搜索文件的基础方法,接下来我来带你通过指定搜索路径和建立索引文件提高搜索的效率。
|
||||
|
||||
## 提升搜索效率的两种方法
|
||||
|
||||
用Python的pathlib库实现文件搜索,只是在灵活性上比Windows默认的搜索更好,但是搜索效率上并不能带来任何提高。为了减少搜索的等待时间,接下来,我就教你使用指定搜索路径和建立索引文件两个方法,提高pathlib库的搜索效率。
|
||||
|
||||
### 指定搜索路径
|
||||
|
||||
我们先来看第一种,指定搜索路径。我们需要通过三个步骤实现:
|
||||
|
||||
1. 先生成配置文件,把要搜索的路径写入到配置文件中;
|
||||
1. 再编写读取配置文件和搜索的自定义函数,把配置文件中的路径读取出来,逐个目录搜索;
|
||||
1. 最后,将多个目录的搜索结果合并输出,便于你通过结果快速找到自己想要的文件。
|
||||
|
||||
先说第一步,怎么使用Python读取配置文件。以往我们会把要搜索的路径写入到变量,并把定义路径的变量名称放在代码前几行的位置,便于下次修改搜索目录的时候找到这个变量。但是对于代码工程稍微复杂的程序来说,往往会有多个代码文件,仍然不利于每次搜索的时候进行搜索路径的修改。
|
||||
|
||||
现在我教你一个新的方法,就是把变量放入到一个单独的文件中,这个文件被称作该代码的配置文件。这种方法的好处是你修改搜索目录时不用打开代码文件。假设你的朋友也需要类似功能,那你就可以把代码和配置文件一起发给他,哪怕他完全不会Python,也能使用你编写的程序实现高效搜索。
|
||||
|
||||
那怎么给Python脚本增加配置文件呢?
|
||||
|
||||
配置文件一般为文本文件。配置文件的格式,一般由软件作者基于软件的功能和自己的习惯来指定,不过也有通用的配置文件格式。比如在Windows系统中,最常见的配置文件是扩展名为.ini的文件,在今天这节课,我们就把.ini文件格式作为配置文件的标准格式。
|
||||
|
||||
.ini文件格式包含三个部分,分别是节、参数和注释。格式如下:
|
||||
|
||||
```
|
||||
节
|
||||
[section]
|
||||
参数
|
||||
(键=值)
|
||||
name=value
|
||||
注释
|
||||
注释使用“;”分号表示。在分号后面的文字,直到该行结尾都全部为注解。
|
||||
;注释内容
|
||||
|
||||
```
|
||||
|
||||
基于.ini文件的格式,我把配置搜索路径的配置文件修改为如下:
|
||||
|
||||
```
|
||||
[work]
|
||||
;工作文件保存路径
|
||||
searchpath=/Users/edz,/tmp
|
||||
|
||||
[game]
|
||||
;娱乐文件保存路径
|
||||
searchpath=/games,/movies,/music
|
||||
|
||||
```
|
||||
|
||||
在这段代码中,我设置了work和game两个“节”,分别代表工作和娱乐。这样设置的好处是,我可以根据不同的用途来搜索不同的目录。如果搜索时使用了更少的目录,也会相应减少搜索的等待时间。
|
||||
|
||||
另外,你会发现两个“节”中的参数我都指定成相同的名字--searchpath,这样设置的好处是我将搜索范围从“工作”改为“娱乐”时,只需要在代码里修改搜索的“节”,不用修改搜索的参数。
|
||||
|
||||
除了“节”和“参数”,在配置文件中,你还应该关注我对参数searchpath设置值的方式,它的值是我想要进行搜索的路径范围,为了在程序中能够更方便得读取多个路径,我使用逗号来分隔多个路径。
|
||||
|
||||
**在编写好配置文件之后,下一步就是取得.ini文件的完整路径**,我把.ini文件和脚本放在了相同的目录,我在第七讲为你详细剖析过,怎样通过__file__变量和pathlib库相结合,实现读取和脚本在同一目录的文件操作,我就不多说了,如果你不太记得了,可以回去复习下。
|
||||
|
||||
找到search.ini文件完整路径之后,接下来需要读取并分析.ini文件格式,Python有实现这个功能的的库,它叫做configparser库,通过这个库你可以直接读取.ini文件中的searchpath参数,不用通过read()函数读取文件内容,手动编写分析.ini文件的脚本了。
|
||||
|
||||
接下来,我来带你看一下Python读取.ini文件的代码,帮你理解Python是怎样通过配置文件来实现在多个路径搜索文件功能的。
|
||||
|
||||
```
|
||||
import configparser
|
||||
import pathlib
|
||||
from pathlib import Path
|
||||
|
||||
def read_dirs(ini_filename, section, arg):
|
||||
"""
|
||||
通过ini文件名,节和参数取得要操作的多个目录
|
||||
"""
|
||||
current_path = pathlib.PurePath(__file__).parent
|
||||
inifile = current_path.joinpath(ini_filename)
|
||||
|
||||
# cf是类ConfigParser的实例
|
||||
cf = configparser.ConfigParser()
|
||||
|
||||
# 读取.ini文件
|
||||
cf.read(inifile)
|
||||
|
||||
# 读取work节 和 searchpath参数
|
||||
return cf.get(section, arg).split(",")
|
||||
|
||||
def locate_file(base_dir, keywords):
|
||||
p = Path(base_dir)
|
||||
files = p.glob(keywords)
|
||||
return list(files)
|
||||
|
||||
|
||||
dirs = read_dirs('search.ini', 'work', 'searchpath')
|
||||
# ['/Users/edz', '/tmp']
|
||||
keywords = '**/*BBC*'
|
||||
|
||||
# 定义存放查找结果的列表
|
||||
result = []
|
||||
|
||||
# 从每个文件夹中搜索文件
|
||||
for dir in dirs:
|
||||
files = locate_file(dir, keywords)
|
||||
result += files
|
||||
|
||||
# 将PosixPath转为字符串
|
||||
print( [str(r) for r in result] )
|
||||
|
||||
```
|
||||
|
||||
在这段代码中,读取配置文件和搜索这两个最主要的功能由两个自定义函数实现,它们分别是read_dirs()函数和locate_file()函数。
|
||||
|
||||
read_dirs()函数实现了读取.ini文件,并将返回的多个路径处理为列表类型。列表类型适合多组并列的数据,多个目录刚好可以使用列表这种数据类型来存放要搜索的目录名称。
|
||||
|
||||
locate_file()函数通过代码的第35行循环功能,对每个目录进行了搜索,并将搜索的结果存入result变量。result变量是一个列表数据类型,由于搜索到的文件可能包含多个匹配的文件路径,我需要将搜索到的结果依次存入result列表中,再继续搜索下一个目录,继续通过append()函数将结果放入列表,直到所有的目录搜索完成,整个搜索的程序才真正执行结束。
|
||||
|
||||
最后还有一点需要你注意,在进行路径处理的过程中,pathlib库为了规避不同操作系统路径写法的差异,就把路径统一定义为PosixPath()对象。因此,你在使用这些路径的时候,需要先将PosixPath对象转换为字符串类型。我在代码最后一行通过Python内置函数str()函数把PosixPath对象逐个转换为字符串类型,并再次存入到列表当中。
|
||||
|
||||
通过限制在指定的目录搜索这个功能,我们就可以规避在搜索时错误匹配系统文件和软件缓存文件而导致的查找过慢问题。
|
||||
|
||||
不过如果我们指定的目录仍然有非常多文件的话,那用这个方法查找起来依然会很慢。别担心,接下来我就教你一种利用空间换时间的方法,提高对指定目录文件搜索效率的方法。
|
||||
|
||||
### 建立索引文件
|
||||
|
||||
什么是“空间换时间”呢?我来解释一下。
|
||||
|
||||
我们知道,文件越多,搜索的时间就越长,因为搜索的时间是随着文件数量呈线性增长的。就像一本书越厚,你从第一页读到最后一页的时间就越长一样。那怎样能快速搜索到书中的内容呢?你会想到图书都有目录功能,通过目录可以加快你找到你想看的图书内容的速度。
|
||||
|
||||
对于操作系统来说,一个文件也是由文件名称、大小、文件内容等多个部分组成的,搜索文件的功能只需要文件名称就行了,不需要其他的部分。因此,我们可以在硬盘中新开辟一块空间,将所有的文件名提前存储下来作为文件的索引,下次再查找的时候直接查找索引文件,就能得到搜索结果,而不必再通过硬盘查找真实的文件了。这就是“空间换时间”。
|
||||
|
||||
既然索引文件可以加快搜索速度,那我们去**建立索引文件**就可以了。
|
||||
|
||||
建立索引文件不需要你重新再写新的程序,我们可以基于指定搜索路径的程序进行改造:
|
||||
|
||||
- 先把配置文件目录下所有文件路径的保存方式**由列表改为文件**;
|
||||
- 再把搜索功能改为**从文件搜索**。
|
||||
|
||||
我把改造后的代码写出来供你参考。
|
||||
|
||||
```
|
||||
def locate_file(base_dir, keywords='**/*'):
|
||||
"""
|
||||
迭代目录下所有文件
|
||||
"""
|
||||
p = Path(base_dir)
|
||||
return p.glob(keywords)
|
||||
|
||||
def write_to_db():
|
||||
"""
|
||||
写入索引文件
|
||||
"""
|
||||
current_path = pathlib.PurePath(__file__).parent
|
||||
dbfile = current_path.joinpath("search.db")
|
||||
|
||||
with open(dbfile, 'w', encoding='utf-8') as f:
|
||||
for r in result:
|
||||
f.write(f"{str(r)}\n")
|
||||
|
||||
# 读取配置文件
|
||||
dirs = read_dirs('search.ini', 'work', 'searchpath')
|
||||
|
||||
# 遍历目录
|
||||
result = []
|
||||
for dir in dirs:
|
||||
for files in locate_file(dir):
|
||||
result.append(files)
|
||||
|
||||
# 将目录写入索引文件
|
||||
write_to_db()
|
||||
|
||||
|
||||
```
|
||||
|
||||
在代码中我增加了write_to_db()函数,它在代码的第16-18行,我通过写入文件方式替代了写入列表的功能。同时,为了能遍历所有的目录,我还修改了locate_file()函数的第二个参数,将它改为“keywords='**/*'”。通过这两处的修改,就把所有文件路径全部保存到search.db文件中了**。**
|
||||
|
||||
search.db的文件内容如下,这里记录了配置文件指定的所有目录下的所有文件路径:
|
||||
|
||||
```
|
||||
/tmp/com.apple.launchd.kZENgZTtVz
|
||||
/tmp/com.google.Keystone
|
||||
/tmp/mysql.sock
|
||||
/tmp/com.adobe.AdobeIPCBroker.ctrl-edz
|
||||
/tmp/com.apple.launchd.kZENgZTtVz/Listeners
|
||||
/tmp/com.google.Keystone/.keystone_install_lock
|
||||
... ...
|
||||
|
||||
```
|
||||
|
||||
接下来,我再把搜索功能从列表搜索改造为**从文件搜索,**为了避免每次搜索要重新生成一次search.db文件,我要编写一个新的脚本,专门用于从文本中搜索关键字,并将搜索的结果显示出来。
|
||||
|
||||
相信你一定想到了我在上一讲为你讲解的正则表达式功能,通过re.search()函数刚好可以实现文本的搜索。下面的locate.py脚本文件就是我使用正则表达式实现的文本搜索功能:
|
||||
|
||||
```
|
||||
import pathlib
|
||||
import re
|
||||
|
||||
keyword = "apple"
|
||||
|
||||
# 获取索引文件路径
|
||||
current_path = pathlib.PurePath(__file__).parent
|
||||
dbfile = current_path.joinpath("search.db")
|
||||
|
||||
# 在索引文件中搜索关键字
|
||||
with open(dbfile, encoding='utf-8') as f:
|
||||
for line in f.readlines():
|
||||
if re.search(keyword, line):
|
||||
print(line.rstrip())
|
||||
|
||||
|
||||
```
|
||||
|
||||
在代码中我利用正则表达式的re.search()搜索函数,以keyword变量作为搜索的关键字,对search.db索引文件的每一行进行了匹配,最后将符合关键字“apple”的文件路径和名称一起显示在屏幕上。
|
||||
|
||||
使用这种方式来搜索文件,要比使用操作系统自带的搜索工具快得多,因为我将原本Windows搜索硬盘上的文件所消耗的时间拆分成了两部分。
|
||||
|
||||
- 一部分是updatedb.py建立索引的时间;
|
||||
- 一部分是从search.db索引文件查找关键字的时间。
|
||||
|
||||
当搜索等待的时间被提前转换为建立索引的时间后,搜索的效率自然就提高了。
|
||||
|
||||
但是请你注意,这种方式建立的索引文件会有时效性的问题,一旦硬盘中的文件被删除、移动或改名,你就得重新建立索引文件。而索引文件保存的文件路径和当前真实的文件并不能一一对应,所以你需要及时更新索引文件。
|
||||
|
||||
为了保证索引文件的时效性,你还可以把updatedb.py脚本加入到Windows的开机启动脚本中,每次打开计算机就会自动更新索引文件。通过这种方式就可以让索引文件里的文件路径更加准确。
|
||||
|
||||
### 总结
|
||||
|
||||
最后,我来为你总结一下。这节课,我为你讲解了如何使用pathlib库搜索文件,以及如何使用配置文件和索引文件加快搜索。
|
||||
|
||||
利用索引文件减少等待时间,实际上是将Windows的搜索进行了拆分,提前将搜索的路径保存到了索引文件中,从索引文件搜索时,就不用再进行真正的文件查找工作了,这就减少了搜索的等待的时间。
|
||||
|
||||
这种搜索方法在服务器领域已经被广泛使用,像Linux和MacOS操作系统中都存在着利用索引文件的搜索命令,对于服务器上变化频率较低的场景,利用索引文件搜索非常实用。
|
||||
|
||||
### 思考题
|
||||
|
||||
我给你留一道思考题。如何扩展locate.py的搜索功能,实现搜索.jpg扩展名的文件呢?
|
||||
|
||||
欢迎把你的思考和想法分享在留言区,我们一起交流讨论。如果课程帮你解决了一些工作上的问题,也欢迎你把课程分享给你的朋友、同事。
|
||||
@@ -0,0 +1,225 @@
|
||||
<audio id="audio" title="10|按指定顺序给词语排序,提高查找效率" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/be/83/be6d5dd5e1939e9c39b3a1697f0d8983.mp3"></audio>
|
||||
|
||||
你好,我是尹会生。
|
||||
|
||||
之前我在游戏行业工作的时候,经常需要取得用户在线时长TOP3、用户战斗力TOP5、用户完成任务数量TOP10等数据,相信你在工作中也会有遇到从大量数据取得TopN这类需求。
|
||||
|
||||
提取TopN的操作,本质上是对大量数据先进行排序,然后根据排序的结果取出前N个值。而实现提取TopN的值,用Python来实现会非常高效,这节课,我就来讲一讲具体怎么操作。
|
||||
|
||||
## 使用sorted()函数实现排序
|
||||
|
||||
在Python中,已经内置了排序函数sorted()函数,它是Python中实现排序最简单且最直接的形式,可以解决80%的排序问题。那么,我们就来学习一下怎么用sorted()函数对常见的数据类型进行排序。
|
||||
|
||||
先看sorted()函数的定义。
|
||||
|
||||
```
|
||||
sorted(iterable, cmp=None, key=None, reverse=False)
|
||||
|
||||
```
|
||||
|
||||
sorted函数共有四个参数,第一个参数是要排序的对象,后面三个参数是排序的方式。
|
||||
|
||||
如果要为某个对象排序的话,你可以直接将它作为sorted()函数的第一个参数,返回结果会将对象的值进行从小到大的排序。
|
||||
|
||||
如果sorted()返回的排序结果不满足你的需要,比如你想要从大到小的排序,那你就可以利用后面三个参数来改变排序的方式,实现自定义排序。
|
||||
|
||||
所以今天这节课,我会带你学习怎么使用sorted()函数实现默认排序和自定义排序,来解决你实际工作中遇到的多种排序问题。我们先来看怎么使用sorted()实现默认排序。
|
||||
|
||||
### 默认排序
|
||||
|
||||
**sorted()函数的默认排序是按照从小到大的顺序进行排序的**。例如一家公司即将上市,需要在职的前1000名员工的工号和姓名,工号越小的员工配股越多。这时候就需要给所有员工的工号进行排序,并取得工号排在前1000个员工的名字。具体怎么做呢?
|
||||
|
||||
我们来看sorted()函数的用法。我们从它的第一个参数开始学习。第一个参数是sorted()函数要排序的对象,我以列表为例,把员工的工号放到列表中,我们来看一下sorted()函数是怎么对列表中的工号进行从小到大的排序的。
|
||||
|
||||
```
|
||||
sorted_list = sorted([30, 50, 20, 10, 40])
|
||||
print(sorted_list)
|
||||
# [10, 20, 30, 40, 50]
|
||||
|
||||
```
|
||||
|
||||
我在代码中给sorted()函数指定了一个列表类型的参数“[30, 50, 20, 10, 40]”。按照sorted()函数的定义,这个列表会被sorted()函数按照数字从小到大进行排序,并返回排序后的结果。并且,我把结果放入sorted_list对象中,你从代码中可以看到sorted_list已经把数据按照默认从小到大的顺序排列好,并把结果放在一个列表数据类型中。
|
||||
|
||||
这就是sorted()的基本用法,其实是比较简单的,不过这里**有三点需要你注意。**
|
||||
|
||||
**首先,sorted()函数的默认排序方式是从小到大进行排序,那么对于列表中的数字就会按照默认的算法进行大小比较。但是除了数字外,列表中还会出现字母或字母与数字混合的方式,你也需要掌握这两种方式的排序处理方法。**
|
||||
|
||||
比如说英文字母,会按照列表中字符串的第一个字母从A到Z、从a到z进行排序。如果字符串的第一个字母相同,就会比较第二个字母,以此类推,直到比较完字符串中的所有字符。
|
||||
|
||||
由于默认排序的时候不支持同时对英文字母和数字进行排序,那么如果列表中既包含了字母,又包含了数字,在默认排序时程序就会提示一个异常,那就是无法使用“<”比较整数和字符串。
|
||||
|
||||
```
|
||||
TypeError: '<' not supported between instances of 'int' and 'str'
|
||||
|
||||
```
|
||||
|
||||
因此,当列表中出现类似“["a", 1, "bb"]”这种,**既包含字符串又包含数字的列表,进行排序操作的时候**,你需要先统一类型,把数字使用str()函数转换为字符串类型,再对列表进行排序。我把代码写出来,供你参考:
|
||||
|
||||
```
|
||||
sorted_list2 = sorted(["a", str(1), "bb"])
|
||||
print(sorted_list2)
|
||||
|
||||
```
|
||||
|
||||
**其次,你需要注意,sorted()函数不会对原有的列表进行修改,它会把排序好的结果存入到一个新的列表当中。**
|
||||
|
||||
很多人刚开始使用Python时,会把sorted()函数和列表自带的函数sort()函数混淆。
|
||||
|
||||
它们虽然名字很像,但支持的数据类型、调用的方式以及返回的结果都不同。
|
||||
|
||||
要记住,sort()函数是列表数据类型自带的,所以只能对列表数据类型进行排序,不能对其他数据类型排序,但sorted()函数可以支持任何可迭代的对象。
|
||||
|
||||
在调用方式上,sort()函数使用的方式是“列表.sort()”格式,这也是调用时和sorted()函数的差别。
|
||||
|
||||
还有一个最大的差别是,sort()函数会直接修改当前列表(这种修改称作“原地修改”),并返回一个空值None。而**sorted()函数不会对原有的列表进行修改,它会把排序好的结果存入到一个新的列表当中**。
|
||||
|
||||
为了让你更好地区分sorted()函数和sort()函数,我把sort()函数的执行结果也提供给你做参考。
|
||||
|
||||
```
|
||||
list3 = ["a", "c", "bb"]
|
||||
no_value = list3.sort()
|
||||
print(list3)
|
||||
# ['a', 'bb', 'c'] 执行结果
|
||||
print(no_value)
|
||||
# None 执行结果
|
||||
|
||||
```
|
||||
|
||||
在代码的第二行,是sort()函数的调用格式。在第三行,我输出了排序之后的列表,并对列表的值进行了重新排序。在第五行,我们可以看到no_value变量的返回值为None,也就是sort()函数会对列表进行原地修改,并使用None作为返回。
|
||||
|
||||
**最后,你需要注意sorted()函数能够支持的数据类型非常多**,既能支持基础数据类型,又能支持Python自带的内置函数。根据它的函数定义,第一个参数是“iterable”对象,表示只要该对象可迭代,sorted()函数就能对它进行排序。
|
||||
|
||||
在基本数据类型中,**序列和映射都可以迭代****,**序列是指列表、元组、字符串这三种基本数据类型的总称,映射就是我们使用过的字典。除了基本数据类型之外,像是range() 、map()、zip()等内置函数都是可迭代对象,因此掌握sorted()函数,可以对以上多种类型和函数进行排序。
|
||||
|
||||
掌握了这三点注意事项,在使用sorted()为可迭代类型进行排序时,就不会出现什么问题了。那接下来,我来带你学习一下它的自定义排序功能,通过自定义排序,可以让sorted()基于不同的数据类型实现更加灵活的排序。
|
||||
|
||||
### 自定义排序
|
||||
|
||||
自定义排序是在基本排序基础上,能够支持**更多<strong><strong>的**</strong>排序方式和更复杂的数据类型。比如说:</strong>
|
||||
|
||||
- 在排序方式上,我们通过参数,可以把默认的从小到大的排序改为从大到小。
|
||||
- 在对数据类型的支持上,像列表中包含元组这种复杂数据类型,可以通过指定元组的任意一列进行排序。
|
||||
|
||||
我们先学习自定义排序是怎么支持更多的排序方式的,它的前提是要先更改默认的排序方式。
|
||||
|
||||
例如我想要实现列表的从大到小的排序,并提取Top 3这一需求,我首先需要使用函数的第四个参数reverse参数改变默认的排序方式,排序后我需要使用列表的切片方式提取前三个元素。
|
||||
|
||||
我先把代码写出来,然后再给你详细解释。可以看到,我在代码中使用了sorted_list4作为排序后的结果,并对sorted_list4进行切片,从而得到指定下标对应的值。
|
||||
|
||||
```
|
||||
sorted_list4 = sorted([30, 50, 20, 10, 40], reverse=True)
|
||||
print(sorted_list4[:3])
|
||||
# [50, 40, 30]
|
||||
|
||||
```
|
||||
|
||||
在代码中,我为sorted()函数增加了参数reverse。reverse参数在定义的时候是sorted()函数的第四个参数,由于sorted()函数的第二、三个参数我希望保持默认,不需要在调用的时候传入,因此reverse参数在调用时就要作为第二个参数来使用。
|
||||
|
||||
由于在调用时它的位置和定义的位置不同,那么我必须使用关键字参数 “reverse=True”的形式,把reverse指定为sorted()函数的第二个参数。
|
||||
|
||||
增加reverse参数之后,sorted()执行的结果也和默认结果不同,sorted会将默认排序的结果进行反转。而当五个数字的排序结果反转后,也就以从大到小的方式进行了输出。
|
||||
|
||||
你看,利用sorted()函数的reverse参数就能实现TopN场景的排序工作,工作中往往需要只得到TopN的结果,不需要将全部的排序结果进行输出,这时候你可以将列表按照你的工作需要提取其中的某一部分,这种操作也被称作列**表的切片。**
|
||||
|
||||
假如你想取得sorted_list4列表的Top3元素,可以对列表排序后,使用sorted_list4[0:3]的写法提取列表前三个值,这种写法的“0”“3”表示列表的下标,“:”表示获取下标的范围。因为列表的下标从0开始,因此要写作[0:3]的形式,也可以使用它的省略形式[:3],这样列表切片操作就会从0开始取值。
|
||||
|
||||
总结来说,通过reverse参数和列表切片,我们可以在默认排序的基础上,实现基于列表的TopN场景下的排序,以及TopN结果的提取。
|
||||
|
||||
不过现实场景往往都是比较复杂的,要排序的数据类型除了列表中包含数字和字符串外,你最经常见到的还有对列表中包含元组这种复合类型进行排序,以及对字典的键或值进行排序。接下来我来带你学习一下两种主要类型,一种是列表+元祖的类型,一种是字典类型。
|
||||
|
||||
#### 列表+元组的排序方式
|
||||
|
||||
列表+元组形成的组合数据类型,适合存放包含多个属性的对象。我给你看个例子,
|
||||
|
||||
“姓名、性别、身份证号”和“学校、姓名、学号”这两组数据就适合用元组存储。因为元组存储的值是不可变的,而这些数据填入之后一般不会修改,刚好和元组的特性吻合。
|
||||
|
||||
而且,这些用户信息在工作中往往会被大量存储在一起,为了便于对它们进行排序和查找。你还需要将多个元组再保存到列表当中,这样就形成了列表+元组这种组合类型。
|
||||
|
||||
对列表+元组的形式进行排序,就需要用自定义排序的字段来实现。那就是在默认排序的基础上,增加key参数,并通过lambda表达式取得元组第三个位置的学号。
|
||||
|
||||
**lambda表达式是简化自定义函数的定义和调用而使用的一种语法**,使用lambda表达式取得学号的字符串之后,sortd()就可以实现对元组按照学号进行排序了。
|
||||
|
||||
在这里,key没法通过下标对元组排序,而必须通过函数取得参数具体的值。这是sorted()函数为了更灵活地实现排序功能,把设置排序关键字这一功能全部开放给用户造成的。事实上,这也是一种典型的通过牺牲易用性来增加灵活性的设计模式。
|
||||
|
||||
这一知识点比较难理解,不过不用担心,我在后续课程陆续为你讲解这类函数,直到你能熟练使用它们为止。
|
||||
|
||||
到这里,我们就了解了sorted()函数的key参数必须使用函数做参数的原因,那就继续来看**key参数的lambda表达式是<strong><strong>怎么**</strong>简化自定义函数的</strong>。
|
||||
|
||||
比如我想取得元组中包含“Jerry”的学号“1003”,以及包含“Tom”的元组学号的“1005”,可以使用这样一段代码来实现函数定义和调用方式:
|
||||
|
||||
```
|
||||
def s(my_tuple):
|
||||
return my_tuple[2]
|
||||
sorted(students, key=s)
|
||||
|
||||
sorted(students, key=lambda s: s[2])
|
||||
|
||||
```
|
||||
|
||||
在代码的1-3行、第5行,分别是通过自定义函数和lambda表达式实现的提取元组第三个参数的功能。对比来看,自定义函数的定义要比lambda表达式复杂。自定义函数的定义和调用部分在Python中要分开编写,这也是简单函数更适合用lambda表达式编写的原因。
|
||||
|
||||
lambda表达式通常在函数只有一行语句,且不需要强调函数名称的时候使用,因此它还有一个名字叫做**匿名函数。<strong>它的结构只包含四部分,即**lambda关键字、需要接收的参数、一个冒号**<strong>和**</strong>对接收参数的处理</strong>,并且它会把处理结果自动返回。
|
||||
|
||||
因此在sorted()函数中通过lambda表达式实现按关键字排序,会比使用自定义函数排序更加简洁,所以当你遇到只有一行语句的函数场景时,可以考虑使用lambda表达式替代自定义函数的定义和调用。
|
||||
|
||||
#### 字典类型的排序方式
|
||||
|
||||
除了列表+元组的复合类型外,我们经常还需要对字典类型进行排序,**字典类型包含键和值**,**<strong>所以排序的时候可以基于键来排序**</strong>,**也可以基于值来排序**。
|
||||
|
||||
我们还是以学生信息为例。我把学生的姓名定义为字典的键,把学号定义为字典的值。接下来我们看看sorted()是怎么对字典的键和值进行排序的,实现排序的代码如下:
|
||||
|
||||
```
|
||||
student_dict1 = {'Jerry':'1003',
|
||||
'Tom':'1005',
|
||||
'Beta':'2001',
|
||||
'Shuke':'2003'
|
||||
}
|
||||
|
||||
# 输出字典的键和值
|
||||
print(student_dict1.items())
|
||||
# dict_items([('Jerry', '1003'), ('Tom', '1005'), ('Beta', '2001'), ('Shuke', '2003')])
|
||||
|
||||
# 按照字典的键排序
|
||||
print(sorted(student_dict1.items(), key=lambda d: d[0]))
|
||||
# [('Beta', '2001'), ('Jerry', '1003'), ('Shuke', '2003'), ('Tom', '1005')]
|
||||
|
||||
# 按照字典的值排序
|
||||
result = sorted(student_dict1.items(), key=lambda d: d[1])
|
||||
print(result)
|
||||
[('Jerry', '1003'), ('Tom', '1005'), ('Beta', '2001'), ('Shuke', '2003')]
|
||||
# 将结果转换为字典
|
||||
print(dict(result))
|
||||
# {'Jerry': '1003', 'Tom': '1005', 'Beta': '2001', 'Shuke': '2003'}
|
||||
|
||||
```
|
||||
|
||||
在这段代码中,我实现了基于字典的键和字典的值进行排序的功能。由于字典是使用键值对的形式存储数据的,所以我先通过字典自带的函数items(),把键值对的形式转换成列表+元组的形式。经过转换以后,字典的键就变成了元组的第一个参数,值就变成了元组的第二个参数。
|
||||
|
||||
因此在代码中,我把参数key的值设置为“lambda d: d[0]”,这样就取得了元组的第一个元素,排序之后就实现了按照字典的键进行排序的需求。相应的,使用“lambda d: d[1]”,可以取得元组第二个元素的值,就能实现按照字典的值进行排序的需求。
|
||||
|
||||
由于排序后的数据类型已经变成了列表+元组形式。所以我们在代码最后,可以通过dict()函数把排序的结果再转换为字典,和排序前的数据类型保持一致。
|
||||
|
||||
这就是通过sorted()函数对字典进行排序的解决方法,你在工作中或许还会遇到和这节课不一样的数据类型,但是它们的解决思路是相通的。
|
||||
|
||||
1. 如果能转换成列表,可以采用更改lambda下标的方式,实现对指定字段的排序。
|
||||
1. 如果不能转换成列表,可以尝试将复杂的类型中,不需要进行排序的部分进行删减,简化成列表或字典类型,这样就也能使用sorted实现数据的排序功能了。
|
||||
|
||||
## 小结
|
||||
|
||||
最后,我来给你总结一下这节课的核心内容。我们通过sorted()函数实现了列表、列表+元组、字典类型的排序,通过排序后的结果,你可以快速提取TopN,也可以利用你学习过的其他语言的算法实现更复杂的查找。
|
||||
|
||||
在sorted()函数中,我们通过编写key参数的值,使用了lambda表达式替代了简单的函数,让我们的程序更加简洁。当你在日后的工作场景中也出现了只有单个语句的函数时,也可以考虑采用lambda表达式替代自定义函数,增加代码的可读性。
|
||||
|
||||
正是因为sorted()功能的强大,我们在python中实现排序几乎不需要通过自行编写代码来实现排序,不过你在进行海量数据的时候,我还想给你提供两条有用的建议。
|
||||
|
||||
第一,如果对包含海量数据的列表进行排序时,建议将列表通过tuple()函数转换为元组,能够让查找效率有较大提升。
|
||||
|
||||
第二,在Python标准库collections库中提供了OrderedDict扩展数据类型,它的特点是对OrderedDict数据类型进行赋值时,会自动进行排序。当你需要一个有序字典时,可以考虑选择OrderedDict作为数据存储的类型,从而避免手工对数据排序。
|
||||
|
||||
我也把这节课的相关代码放在了[GitHub](https://github.com/wilsonyin123/python_productivity/blob/main/%E6%96%87%E7%AB%A010%E4%BB%A3%E7%A0%81.zip)上,你可以去学习查找。
|
||||
|
||||
## 思考题
|
||||
|
||||
在最后,我想给你留一道开放性的问题。为什么Python语言不将所有的数据类型都设计成默认有序的,即存入数据时自动进行排序?欢迎你说出自己的理由。
|
||||
|
||||
欢迎把你对问题的思考和想法分享在留言区,我们一起交流讨论。如果课程帮你解决了一些工作上的问题,也欢迎你把课程分享给你的朋友、同事。
|
||||
@@ -0,0 +1,228 @@
|
||||
<audio id="audio" title="11 |通过程序并行计算,避免CPU资源浪费" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/bc/da/bcf8f4cf356847d1b53ff6cd08f1ffda.mp3"></audio>
|
||||
|
||||
你好,我是尹会生。
|
||||
|
||||
在我为运营工作提供技术咨询的时候,遇到过这样一个场景:这场运营活动,需要在电脑和手机端的多个不同应用程序,同时推送产品宣传图片和视频。这些大量的图片需要有不同的格式和尺寸,视频也需要根据不同的App截取不同的时长。
|
||||
|
||||
如果这类需要大量计算的多个任务成为你的日常工作,会花费你不少的时间和精力。不过别担心,我们可以通过程序并行计算,来提升任务效率。
|
||||
|
||||
不过你可能会说,用Python自动化执行,也可以提高计算效率啊,那为什么还要学习并行计算呢?
|
||||
|
||||
要知道,Python默认的自动化只能利用CPU的一个逻辑核心,如果采用并行计算,那就能够最大化地利用CPU资源,从而成倍提升大量计算的任务效率。接下来我就详细分析一下并行计算的高效之处。
|
||||
|
||||
## 为什么要进行并行计算
|
||||
|
||||
还是我在开头提出的运营工作的场景。如果你从这类任务消耗计算机主要资源的角度去考虑,会发现这类需求有两个共同的特点。
|
||||
|
||||
第一,它们都需要进行大量的计算,而计算主要是通过CPU来实现的。CPU的硬件指标上有两个和计算效率最相关的概念,分别是主频和多核。
|
||||
|
||||
主频决定CPU处理任务的快慢,多核决定处理的时候是否可以并行运行。这和生活中超市的收银一样,收银员的工作效率和超市开放了多少个收银台的通道,都决定了你能否以最快的速度购买到你想要买的商品。
|
||||
|
||||
第二,这些任务往往都需要运行相同的程序,但是程序的参数却需要根据不同的需求进行调整。
|
||||
|
||||
虽然咱们可以使用Python自动化执行这些程序,从而减少手动操作时间,但是我们还可以利用CPU的多核特性,让程序尽可能并行执行,发挥CPU的全部计算能力,提高运行效率。
|
||||
|
||||
那么接下来,我就来教你怎样利用Python的多进程库,来实现程序的并行计算,以及怎么提高并行计算的效率。
|
||||
|
||||
## 怎样实现并行计算
|
||||
|
||||
要想实现程序的并行计算,需要使用到标准库中的multiprocessing多进程库。你可能会问,进程是什么呢?
|
||||
|
||||
**进程,是计算机用来描述程序运行状态的名词**。一个进程在运行时需要消耗一定的资源,包括CPU的时间、内存、设备I/O等。如果两个进程互相独立,在同一个任务处理过程中,没有前后依赖关系,那你可以利用multiprocessing库同时运行多个进程,这样就能成倍地减少多个任务执行的总时间。
|
||||
|
||||
接下来,我就以计算1-100的平方为例,看看怎么使用multiprocessing实现并行计算。代码如下:
|
||||
|
||||
```
|
||||
from multiprocessing import Pool
|
||||
|
||||
# 计算平方
|
||||
def f(x):
|
||||
return x*x
|
||||
|
||||
with Pool(8) as p:
|
||||
# 并行计算
|
||||
res = p.map(f, range(1, 101))
|
||||
print(f'计算平方的结果是:{res}')
|
||||
|
||||
```
|
||||
|
||||
在这段代码中,我通过Pool包的map()函数来求1到100平方计算,由于每次计算平方的过程和下一次计算没有直接关联,我就可以使用并行的方式进行计算,提高计算效率。
|
||||
|
||||
为了让map()函数能够实现并行计算,我们必须在使用它之前,通过Pool()包为它指定并行计算的进程数量,设置要执行的函数名称f,以及f()函数所需参数。那么接下来,我就带你学习一下我是怎样使用with语句来设置函数的参数,并正确执行map()函数的。
|
||||
|
||||
**首先来看最关键的map()函数**,它是Pool包实现并行计算的函数之一。在代码中我为map()函数赋值了f和range()函数两个参数。
|
||||
|
||||
第一个参数是函数对象。
|
||||
|
||||
函数对象会作为map()函数创建进程以后,即将执行的主要任务。因此,由于这里的含义是指定f对象将要被创建的进程执行,而不是将f()函数执行的结果作为新的进程执行,所以第一个参数必须使用函数对象f,而不能使用f()函数。
|
||||
|
||||
第二个参数要求必须是可迭代的对象。
|
||||
|
||||
例如我在代码中需要为f函数传递参数为1-100的整数,就可以使用range()函数产生1到100的整数并直接返回,因为它的返回值就是可迭代对象。
|
||||
|
||||
如果参数不是数字,就可以采用列表、元组、字典等支持迭代的数据类型,代替range()函数,作为f()函数的参数。举个例子,如果你需要并行调整多个视频的时长,就可以采用字典存储路径和要调整的视频时长,并把这个字典作为map()函数的第二个参数,map()函数会为字典的每个键值对创建一个进程来并行处理它们。
|
||||
|
||||
**接下来**是map()函数中的三个主要部分,我来分析一下它们各自在并行计算中的功能。
|
||||
|
||||
第一,with语句。这是我们在第七讲学习怎么使用Python打开文件之后,第二次用到with语句了。
|
||||
|
||||
和文件操作类似,进程打开后也需要妥善关闭,但是进程关闭属于较为底层的操作,如果你不进行操作系统层面的程序设计,是不需要对关闭进程的函数进行修改的,因为使用默认关闭进程的行为,就能满足编写并行计算的需求。
|
||||
|
||||
因此,multiprocessing库对Pool包,支持了比较友好的进程打开和关闭方式,即with语句。也就是说,multiprocessing库把对进程的操作写在with语句块中,而with语句就会自动处理进程的打开和关闭,这样在实现并行计算的代码中,你就不用学习进程的基本操作,也能减轻你学习并发程序的负担。
|
||||
|
||||
在了解了with语句可以操作进程的打开和关闭后,我们来看代码中我是怎么使用with语句的。
|
||||
|
||||
我在代码中使用了“ with Pool(8) as p ”这条语句,这里的Pool()类是多进程库支持的进程池功能,它的作用是指定一个多进程的程序,最多能够并行执行的进程数量。它的参数“8”,表示map()函数最多同时运行8个进程。
|
||||
|
||||
剩下两个部分是range()函数和f()函数。
|
||||
|
||||
range()函数的作用是产生1-100的整数,这些整数会在每次创建新的进程时,依次作为f()函数的参数并赋值。而f()函数得到参数后,会把计算结果返回给map()函数。当f()函数处理完所有的参数后,map()函数还会返回一个列表作为运行的结果,并进行输出。
|
||||
|
||||
以上就是实现并行计算的主要过程。
|
||||
|
||||
## 如何提高并行计算的效率
|
||||
|
||||
我们除了需要掌握并行计算的基本方法外,还可以继续提升并行计算的效率。所以在程序中还有两个地方需要优化。
|
||||
|
||||
一个是为并行程序自动指定并行度。在并行计算的基本方法中,我使用了手动指定并行度的方式,来指定进程最多能够运行多少个。不过手动指定的并行度并不能适合所有的电脑,因此就需要根据计算机的CPU核数设置合理的并行度。而且,每台计算机的CPU资源是固定不变的,那么设置合理的进程数量能让你的并行计算任务充分利用CPU资源。
|
||||
|
||||
另一个是统计程序运行的时间。当你对并行计算的数量做了修改后,那程序是否对计算效率起到了提升效果呢?就还需要更精确的测量,这样才能得到更准确的结果。所以我们还需要使用Python统计出程序执行过程一共消耗了多长的时间。
|
||||
|
||||
我们先来看怎么自动设置适合你的电脑的并行度。
|
||||
|
||||
### 为并行程序自动指定并行度
|
||||
|
||||
计算类的任务包括数字计算、数据统计、视频的编解码等,都属于计算密集型应用,它们最大的资源开销就是CPU时间的消耗,设置的并行度过大或过小都不能达到最好的运行效率。
|
||||
|
||||
- 如果并行度设置过小,比如运行的进程数量小于逻辑CPU的数量,就会造成部分逻辑CPU因为无法被充分利用而处于闲置状态。
|
||||
- 如果并行度设置过大,由于现代的操作系统为了保证每个进程都能公平得到CPU资源,所以会造成CPU把时间大量消耗在进程切换上。那么并行度设置过大,会导致CPU还未完成一个进程的处理时,就得切换至下一个进程进行处理,而多进程之间来回切换也会消耗CPU时间,造成CPU资源的浪费。
|
||||
|
||||
那并行度该怎么设置才合理呢?通常情况下,我们会把并行度设置为逻辑CPU数量的两倍。不过假如计算任务达到小时级别(这类任务需要长时间占用CPU资源),为了减少切换任务时的开销,我建议计算的并行度和逻辑CPU数量保持相等。
|
||||
|
||||
这就又有一个问题了,该怎么获得计算机的逻辑CPU个数呢?Windows可以通过任务管理器获得,MacOS可以通过活动监视器获得。如果你希望取得逻辑CPU的个数之后,可以根据它的数量自动设置创建进程的数量,那么可以通过安装第三方包psutils,利用其中的cpu_count()函数取得逻辑CPU个数。
|
||||
|
||||
我把并行度自动设置为当前逻辑CPU两倍的代码写在下面,供你参考。
|
||||
|
||||
```
|
||||
from multiprocessing import Pool,Queue
|
||||
import os
|
||||
import psutil
|
||||
|
||||
# 逻辑cpu个数
|
||||
count = psutil.cpu_count()
|
||||
|
||||
# 定义一个队列用于存储进程id
|
||||
queue = Queue()
|
||||
|
||||
# 用于计算平方和将运行函数的进程id写入队列
|
||||
def f(x):
|
||||
queue.put(os.getpid())
|
||||
return x*x
|
||||
|
||||
with Pool(count*2) as p:
|
||||
# 并行计算
|
||||
res = p.map(f, range(1, 101))
|
||||
print(f'计算平方的结果是:{res}')
|
||||
|
||||
# 并行计算用到的进程id
|
||||
pids = set()
|
||||
while not queue.empty():
|
||||
pids.add(queue.get())
|
||||
|
||||
print(f'用到的进程id是: {pids}')
|
||||
|
||||
```
|
||||
|
||||
在代码中,我使用了 psutil.cpu_count() 函数来获取逻辑CPU的个数,它把“count*2”作为参数传递给Pool()类,并以逻辑CPU两倍作为最大创建进程数量,从而计算1-100的平方。
|
||||
|
||||
这里有两点需要你注意。第一,**psutils是process and system utilities**的缩写,所以它除了获取逻辑CPU数量外,还可以获取内存、硬盘、网络连接等和操作系统相关的信息。如果你在工作中需要取得操作系统的运行状态,就可以采用psutils包。
|
||||
|
||||
第二,psutils是第三方库,因此,在Windows上你需要通过cmd命令行执行pip3 install psutil安装后,才能释放psutils包,否则会出现模块无法找到的错误。
|
||||
|
||||
由于map()函数的第二个参数可能会被传入不可迭代对象,这时有可能会导致只运行了一个进程,因此我就在多进程执行过程中,增加了记录进程ID的功能。而在这一功能中,我使用的是**os库、队列库和集合数据类型****,**按照下面三个步骤来实现对所有创建的进程ID的统计。
|
||||
|
||||
**首先,<strong>使用**os库的getpid()函数获取进程ID。</strong>
|
||||
|
||||
由于map()函数会根据Pool()类的参数,事先创建好指定数量的进程,而每次运行f()函数都在创建好的进程中执行,所以我就采用os库的getpid()函数取得运行f()函数进程的唯一标识,这就是使用os库的用途。
|
||||
|
||||
**接下来,<strong><strong>使用队列库存储每次运行进程的ID**</strong>。</strong>
|
||||
|
||||
为了把每次运行的进程ID存到一个对象中,我使用了multiprocessing库的队列包。因为在多进程的程序中,不能采用标准数据类型来传递数据,所以multiprocessing库还提供了方便进程间通信的对象——Queue队列。
|
||||
|
||||
map()函数每执行一次f()函数,我就把进程ID作为队列的put()函数的参数,并把进程ID放入队Queue中,直到所有的f()函数执行完成,队列里就会记录每次执行的进程ID信息。
|
||||
|
||||
**最后,<strong><strong>使用集合数据类型存储本次f()函数运行的所有进程ID**</strong>。</strong>
|
||||
|
||||
为了实现这一功能,我需要通过while循环结构,根据队列不为空的条件,把队列中的进程ID使用get()函数取出来,放入pids变量中。
|
||||
|
||||
pids变量是集合数据类型,集合是一个无序的不重复元素序列,需要使用set()创建。你可以把集合当作一个只有键没有值的字典来记忆,它的特点是集合里的元素不能重复。
|
||||
|
||||
由于f()函数会多次在一个进程中执行,因此在队列中会记录重复的进程ID,我把进程ID从队列中取出后,放入集合数据类型中,自己就不用编写程序,自动把重复的进程ID去掉了。而且通过对集合pids中的进程ID进行输出,可以看到进程ID的数量刚好和Pool()类指定的并行进程数量相等。
|
||||
|
||||
这种用法是我经常在进行多进程程序调试的一种简单用法,我还会把它们的结果写入文件保存,以便程序出现异常执行结果时,可以根据调试的信息进行问题的定位。
|
||||
|
||||
### 统计程序运行的时间
|
||||
|
||||
我们除了需要掌握判断程序的并行度外,还可以统计并行计算比顺序计算节省了多少时间。那么再遇到相同场景的时候,你可以选择并行方式来运行程序,提高工作效率。接下来我来教你怎样统计Python程序运行的时间。
|
||||
|
||||
在Python中我们可以利用time库的time()函数,来记录当前时间的功能。
|
||||
|
||||
- 首先,需要在统计时间代码的前后各增加一次time.time()函数,并把它们统计时间的结果存放在time1、time2两个不同的变量中。
|
||||
- 然后再把两个变量相减,这样就能取得程序的运行时间了。
|
||||
|
||||
我把核心实现代码写在下面供你参考。
|
||||
|
||||
```
|
||||
# 并行计算时间统计
|
||||
with Pool(4) as p:
|
||||
# 并行计算
|
||||
time1 = time.time()
|
||||
res = p.map(f, range(1, 10001))
|
||||
time2 = time.time()
|
||||
# print(f'计算平方的结果是:{res}')
|
||||
|
||||
print(str(time2-time1))
|
||||
|
||||
|
||||
# 串行计算时间统计
|
||||
list1 = []
|
||||
|
||||
time1 = time.time()
|
||||
for i in range(1, 10001):
|
||||
list1.append(f(i))
|
||||
time2 = time.time()
|
||||
|
||||
print(str(time2-time1))
|
||||
|
||||
```
|
||||
|
||||
在这段代码中,通过time1和time2的时间差就可以得到程序运行的时间了,那么根据运行时间,我们可以把并行程序和串行程序执行时间的性能进行对比。
|
||||
|
||||
这里你需要注意,由于计算平方的CPU开销较小,比较难体现并行计算的优势,你就可以采用并行访问网页,或其他CPU开销较高的程序,这样会让两个程序的时间差别更加明显。
|
||||
|
||||
## 总结
|
||||
|
||||
在最后,我来为你总结一下实现并行计算的**基本方法和三个注意事项。**
|
||||
|
||||
通过multiprocessing的Pool包可以实现基于进程的并行计算功能,Pool包的map()函数会根据Pool包指定的进程数量实现并行运行。这里还有三点需要你注意:
|
||||
|
||||
1. 作为map()函数的第一个参数你需要传递函数对象f,不能传递函数的调用f()形式,这是初学者实现并行任务最容易出现的错误。
|
||||
1. 为了让并行度更适合你的电脑,应该根据逻辑CPU的个数设置并行度,并根据运行时间来对并行数量进一步优化。
|
||||
1. 实现并行计算任务的程序除了使用多进程模型外还可以使用多线程模型。多进程的并行计算更适用于计算密集型应用,即程序运行过程中主要为计算类CPU开销大的程序,多线程模型适合I/O密集型的应用,例如:通过互联网进行批量网页访问和下载。如果你想将多进程的并发模型改为多线程的并发模型只需在导入库的时候将“multiprocessing”改为“multiprocessing.dummy”就能实现多线程并行访问网页。我将多进程和多线程两种方式导入库的代码贴在下方供你参考。
|
||||
|
||||
```
|
||||
# 多进程模型
|
||||
from multiprocessing import Pool
|
||||
|
||||
# 多线程模型
|
||||
from multiprocessing.dummy import Pool
|
||||
|
||||
# multiprocessing.dummy的Pool用法和multiprocessing库相同
|
||||
|
||||
```
|
||||
|
||||
我把这节课的相关代码放在了[GitHub](https://github.com/wilsonyin123/python_productivity/blob/main/%E6%96%87%E7%AB%A011%E4%BB%A3%E7%A0%81.zip)上,你可以自行查找、学习。
|
||||
|
||||
## 思考题
|
||||
|
||||
我为你留一道思考题,有一个软件包requests,可以通过requests.get('[http://www.baidu.com](http://www.baidu.com)').text 方式访问一个网站,并能够得到网页的源代码。假设我为你提供了几十个需要访问的网站,你是如何实现这些网站的并行访问的,你又能否通过Python对比出逐个访问网页的时间是并行访问的几倍吗?
|
||||
Reference in New Issue
Block a user