21214 字
106 分钟
页面浏览--访问次数--
Day21-30 教程全文(10 篇合并)

本篇目录#

目录里每一条都可以点开直达对应章节;右侧(移动端为悬浮按钮)的「文章目录」也会列出同一批标题。


21 文件读写和异常处理#

实际开发中常常会遇到对数据进行持久化的场景,所谓持久化是指将数据从无法长久保存数据的存储介质(通常是内存)转移到可以长久保存数据的存储介质(通常是硬盘)中。实现数据持久化最直接简单的方式就是通过文件系统将数据保存到文件中。

计算机的文件系统是一种存储和组织计算机数据的方法,它使得对数据的访问和查找变得容易,文件系统使用文件和树形目录的抽象逻辑概念代替了硬盘、光盘、闪存等物理设备的数据块概念,用户使用文件系统来保存数据时,不必关心数据实际保存在硬盘的哪个数据块上,只需要记住这个文件的路径和文件名。在写入新数据之前,用户不必关心硬盘上的哪个数据块没有被使用,硬盘上的存储空间管理(分配和释放)功能由文件系统自动完成,用户只需要记住数据被写入到了哪个文件中。

打开和关闭文件#

有了文件系统,我们可以非常方便的通过文件来读写数据;在 Python 中要实现文件操作是非常简单的。我们可以使用 Python 内置的open函数来打开文件,在使用open函数时,我们可以通过函数的参数指定文件名、操作模式和字符编码等信息,接下来就可以对文件进行读写操作了。这里所说的操作模式是指要打开什么样的文件(字符文件或二进制文件)以及做什么样的操作(读、写或追加),具体如下表所示。

操作模式具体含义
'r'读取 (默认)
'w'写入(会先截断之前的内容)
'x'写入,如果文件已经存在会产生异常
'a'追加,将内容写入到已有文件的末尾
'b'二进制模式
't'文本模式(默认)
'+'更新(既可以读又可以写)

下图展示了如何根据程序的需要来设置open函数的操作模式。

在使用open函数时,如果打开的文件是字符文件(文本文件),可以通过encoding参数来指定读写文件使用的字符编码。如果对字符编码和字符集这些概念不了解,可以看看《字符集和字符编码》一文,此处不再进行赘述。

使用open函数打开文件成功后会返回一个文件对象,通过这个对象,我们就可以实现对文件的读写操作;如果打开文件失败,open函数会引发异常,稍后会对此加以说明。如果要关闭打开的文件,可以使用文件对象的close方法,这样可以在结束文件操作时释放掉这个文件。

读写文本文件#

用open函数打开文本文件时,需要指定文件名并将文件的操作模式设置为'r',如果不指定,默认值也是'r';如果需要指定字符编码,可以传入encoding参数,如果不指定,默认值是None,那么在读取文件时使用的是操作系统默认的编码。需要提醒大家,如果不能保证保存文件时使用的编码方式与encoding参数指定的编码方式是一致的,那么就可能因无法解码字符而导致读取文件失败。

下面的例子演示了如何读取一个纯文本文件(一般指只有字符原生编码构成的文件,与富文本相比,纯文本不包含字符样式的控制元素,能够被最简单的文本编辑器直接读取)。

file = open('致橡树.txt', 'r', encoding='utf-8')
print(file.read())
file.close()

说明:《致橡树》是舒婷老师在 1977 年 3 月创作的爱情诗,也是我最喜欢的现代诗之一。内容如下:

我如果爱你 绝不像攀援的凌霄花, 借你的高枝炫耀自己; 我如果爱你 绝不学痴情的鸟儿, 为绿荫重复单调的歌曲; 也不止像泉源, 常年送来清凉的慰藉; 也不止像险峰, 增加你的高度,衬托你的威仪。 甚至日光, 甚至春雨。

不,这些都还不够! 我必须是你近旁的一株木棉, 作为树的形象和你站在一起。 根,紧握在地下; 叶,相触在云里。 每一阵风过, 我们都互相致意, 但没有人, 听懂我们的言语。 你有你的铜枝铁干, 像刀,像剑,也像戟; 我有我红硕的花朵, 像沉重的叹息, 又像英勇的火炬。

我们分担寒潮、风雷、霹雳; 我们共享雾霭、流岚、虹霓。 仿佛永远分离, 却又终身相依。 这才是伟大的爱情, 坚贞就在这里: 爱 不仅爱你伟岸的身躯, 也爱你坚持的位置, 足下的土地。

除了使用文件对象的read方法读取文件之外,还可以使用for-in循环逐行读取或者用readlines方法将文件按行读取到一个列表容器中,代码如下所示。

file = open('致橡树.txt', 'r', encoding='utf-8')
for line in file:
print(line, end='')
file.close()
file = open('致橡树.txt', 'r', encoding='utf-8')
lines = file.readlines()
for line in lines:
print(line, end='')
file.close()

如果要向文件中写入内容,可以在打开文件时使用w或者a作为操作模式,前者会截断之前的文本内容写入新的内容,后者是在原来内容的尾部追加新的内容。

file = open('致橡树.txt', 'a', encoding='utf-8')
file.write('\n标题:《致橡树》')
file.write('\n作者:舒婷')
file.write('\n时间:1977年3月')
file.close()

异常处理机制#

请注意上面的代码,如果open函数指定的文件并不存在或者无法打开,那么将引发异常状况导致程序崩溃。为了让代码具有健壮性和容错性,我们可以使用 Python 的异常机制对可能在运行时发生状况的代码进行适当的处理。Python 中和异常相关的关键字有五个,分别是try、except、else、finally和raise,我们先看看下面的代码,再来为大家介绍这些关键字的用法。

file = None
try:
file = open('致橡树.txt', 'r', encoding='utf-8')
print(file.read())
except FileNotFoundError:
print('无法打开指定的文件!')
except LookupError:
print('指定了未知的编码!')
except UnicodeDecodeError:
print('读取文件时解码错误!')
finally:
if file:
file.close()

在 Python 中,我们可以将运行时会出现状况的代码放在try代码块中,在try后面可以跟上一个或多个except块来捕获异常并进行相应的处理。例如,在上面的代码中,文件找不到会引发FileNotFoundError,指定了未知的编码会引发LookupError,而如果读取文件时无法按指定编码方式解码文件会引发UnicodeDecodeError,所以我们在try后面跟上了三个except分别处理这三种不同的异常状况。在except后面,我们还可以加上else代码块,这是try 中的代码没有出现异常时会执行的代码,而且else中的代码不会再进行异常捕获,也就是说如果遇到异常状况,程序会因异常而终止并报告异常信息。最后我们使用finally代码块来关闭打开的文件,释放掉程序中获取的外部资源。由于finally块的代码不论程序正常还是异常都会执行,甚至是调用了sys模块的exit函数终止 Python 程序,finally块中的代码仍然会被执行(因为exit函数的本质是引发了SystemExit异常),因此我们把finally代码块称为“总是执行代码块”,它最适合用来做释放外部资源的操作。

Python 中内置了大量的异常类型,除了上面代码中用到的异常类型以及之前的课程中遇到过的异常类型外,还有许多的异常类型,其继承结构如下所示。

BaseException
+-- SystemExit
+-- KeyboardInterrupt
+-- GeneratorExit
+-- Exception
+-- StopIteration
+-- StopAsyncIteration
+-- ArithmeticError
| +-- FloatingPointError
| +-- OverflowError
| +-- ZeroDivisionError
+-- AssertionError
+-- AttributeError
+-- BufferError
+-- EOFError
+-- ImportError
| +-- ModuleNotFoundError
+-- LookupError
| +-- IndexError
| +-- KeyError
+-- MemoryError
+-- NameError
| +-- UnboundLocalError
+-- OSError
| +-- BlockingIOError
| +-- ChildProcessError
| +-- ConnectionError
| | +-- BrokenPipeError
| | +-- ConnectionAbortedError
| | +-- ConnectionRefusedError
| | +-- ConnectionResetError
| +-- FileExistsError
| +-- FileNotFoundError
| +-- InterruptedError
| +-- IsADirectoryError
| +-- NotADirectoryError
| +-- PermissionError
| +-- ProcessLookupError
| +-- TimeoutError
+-- ReferenceError
+-- RuntimeError
| +-- NotImplementedError
| +-- RecursionError
+-- SyntaxError
| +-- IndentationError
| +-- TabError
+-- SystemError
+-- TypeError
+-- ValueError
| +-- UnicodeError
| +-- UnicodeDecodeError
| +-- UnicodeEncodeError
| +-- UnicodeTranslateError
+-- Warning
+-- DeprecationWarning
+-- PendingDeprecationWarning
+-- RuntimeWarning
+-- SyntaxWarning
+-- UserWarning
+-- FutureWarning
+-- ImportWarning
+-- UnicodeWarning
+-- BytesWarning
+-- ResourceWarning

从上面的继承结构可以看出,Python 中所有的异常都是BaseException的子类型,它有四个直接的子类,分别是:SystemExit、KeyboardInterrupt、GeneratorExit和Exception。其中,SystemExit表示解释器请求退出,KeyboardInterrupt是用户中断程序执行(按下Ctrl+c),GeneratorExit表示生成器发生异常通知退出,不理解这些异常没有关系,继续学习就好了。值得一提的是Exception类,它是常规异常类型的父类型,很多的异常都是直接或间接的继承自Exception类。如果 Python 内置的异常类型不能满足应用程序的需要,我们可以自定义异常类型,而自定义的异常类型也应该直接或间接继承自Exception类,当然还可以根据需要重写或添加方法。

在 Python 中,可以使用raise关键字来引发异常(抛出异常对象),而调用者可以通过try...except...结构来捕获并处理异常。例如在函数中,当函数的执行条件不满足时,可以使用抛出异常的方式来告知调用者问题的所在,而调用者可以通过捕获处理异常来使得代码从异常中恢复,定义异常和抛出异常的代码如下所示。

class InputError(ValueError):
"""自定义异常类型"""
pass
def fac(num):
"""求阶乘"""
if num < 0:
raise InputError('只能计算非负整数的阶乘')
if num in (0, 1):
return 1
return num * fac(num - 1)

调用求阶乘的函数fac,通过try...except...结构捕获输入错误的异常并打印异常对象(显示异常信息),如果输入正确就计算阶乘并结束程序。

flag = True
while flag:
num = int(input('n = '))
try:
print(f'{num}! = {fac(num)}')
flag = False
except InputError as err:
print(err)

上下文管理器语法#

对于open函数返回的文件对象,还可以使用with上下文管理器语法在文件操作完成后自动执行文件对象的close方法,这样可以让代码变得更加简单优雅,因为不需要再写finally代码块来执行关闭文件释放资源的操作。需要提醒大家的是,并不是所有的对象都可以放在with上下文语法中,只有符合上下文管理器协议(有__enter__和__exit__魔术方法)的对象才能使用这种语法,Python 标准库中的contextlib模块也提供了对with上下文语法的支持,后面用到的时候再为大家进行讲解。

用with上下文语法改写后的代码如下所示。

try:
with open('致橡树.txt', 'r', encoding='utf-8') as file:
print(file.read())
except FileNotFoundError:
print('无法打开指定的文件!')
except LookupError:
print('指定了未知的编码!')
except UnicodeDecodeError:
print('读取文件时解码错误!')

读写二进制文件#

读写二进制文件跟读写文本文件的操作类似,但是需要注意,在使用open函数打开文件时,如果要进行读操作,操作模式是'rb',如果要进行写操作,操作模式是'wb'。还有一点,读写文本文件时,read方法的返回值以及write方法的参数是str对象(字符串),而读写二进制文件时,read方法的返回值以及write方法的参数是bytes-like对象(字节串)。下面的代码实现了将当前路径下名为guido.jpg的图片文件复制到吉多.jpg文件中的操作。

try:
with open('guido.jpg', 'rb') as file1:
data = file1.read()
with open('吉多.jpg', 'wb') as file2:
file2.write(data)
except FileNotFoundError:
print('指定的文件无法打开.')
except IOError:
print('读写文件时出现错误.')
print('程序执行结束.')

如果要复制的图片文件很大,一次将文件内容直接读入内存中可能会造成非常大的内存开销,为了减少对内存的占用,可以为read方法传入size参数来指定每次读取的字节数,通过循环读取和写入的方式来完成上面的操作,代码如下所示。

try:
with open('guido.jpg', 'rb') as file1, open('吉多.jpg', 'wb') as file2:
data = file1.read(512)
while data:
file2.write(data)
data = file1.read()
except FileNotFoundError:
print('指定的文件无法打开.')
except IOError:
print('读写文件时出现错误.')
print('程序执行结束.')

总结#

通过读写文件的操作,我们可以实现数据持久化。在 Python 中可以通过open函数来获得文件对象,可以通过文件对象的read和write方法实现文件读写操作。程序在运行时可能遭遇无法预料的异常状况,可以使用 Python 的异常机制来处理这些状况。Python 的异常机制主要包括try、except、else、finally和raise这五个核心关键字。try后面的except语句不是必须的,finally语句也不是必须的,但是二者必须要有一个;except语句可以有一个或多个,多个except会按照书写的顺序依次匹配指定的异常,如果异常已经处理就不会再进入后续的except语句;except语句中还可以通过元组同时指定多个异常类型进行捕获;except语句后面如果不指定异常类型,则默认捕获所有异常;捕获异常后可以使用raise要再次抛出,但是不建议捕获并抛出同一个异常;不建议在不清楚逻辑的情况下捕获所有异常,这可能会掩盖程序中严重的问题。最后强调一点,不要使用异常机制来处理正常业务逻辑或控制程序流程,简单的说就是不要滥用异常机制,这是初学者常犯的错误。


22 对象的序列化和反序列化#

JSON 概述#

通过上面的讲解,我们已经知道如何将文本数据和二进制数据保存到文件中,那么这里还有一个问题,如果希望把一个列表或者一个字典中的数据保存到文件中又该怎么做呢?在 Python 中,我们可以将程序中的数据以 JSON 格式进行保存。JSON 是“JavaScript Object Notation”的缩写,它本来是 JavaScript 语言中创建对象的一种字面量语法,现在已经被广泛的应用于跨语言跨平台的数据交换。使用 JSON 的原因非常简单,因为它结构紧凑而且是纯文本,任何操作系统和编程语言都能处理纯文本,这就是实现跨语言跨平台数据交换的前提条件。目前 JSON 基本上已经取代了 XML(可扩展标记语言)作为异构系统间交换数据的事实标准。可以在 JSON 的官方网站找到更多关于 JSON 的知识,这个网站还提供了每种语言处理 JSON 数据格式可以使用的工具或三方库。

{
name: "骆昊",
age: 40,
friends: ["王大锤", "白元芳"],
cars: [
{"brand": "BMW", "max_speed": 240},
{"brand": "Benz", "max_speed": 280},
{"brand": "Audi", "max_speed": 280}
]
}

上面是 JSON 的一个简单例子,大家可能已经注意到了,它跟 Python 中的字典非常类似而且支持嵌套结构,就好比 Python 字典中的值可以是另一个字典。我们可以尝试把下面的代码输入浏览器的控制台(对于 Chrome 浏览器,可以通过“更多工具”菜单找到“开发者工具”子菜单,就可以打开浏览器的控制台),浏览器的控制台提供了一个运行 JavaScript 代码的交互式环境(类似于 Python 的交互式环境),下面的代码会帮我们创建出一个 JavaScript 的对象,我们将其赋值给名为obj的变量。

let obj = {
name: "骆昊",
age: 40,
friends: ["王大锤", "白元芳"],
cars: [
{"brand": "BMW", "max_speed": 240},
{"brand": "Benz", "max_speed": 280},
{"brand": "Audi", "max_speed": 280}
]
}
image-20210820143756353

上面的obj就是 JavaScript 中的一个对象,我们可以通过obj.name或obj["name"]两种方式获取到name对应的值,如下图所示。可以注意到,obj["name"]这种获取数据的方式跟 Python 字典通过键获取值的索引操作是完全一致的,而 Python 中也通过名为json的模块提供了字典与 JSON 双向转换的支持。

我们在 JSON 中使用的数据类型(JavaScript 数据类型)和 Python 中的数据类型也是很容易找到对应关系的,大家可以看看下面的两张表。

表1:JavaScript 数据类型(值)对应的 Python 数据类型(值)

JSONPython
objectdict
arraylist
stringstr
number int / float
number (real)float
boolean (true / false)bool (True / False)
nullNone

表2:Python 数据类型(值)对应的JavaScript数据类型(值)

PythonJSON
dictobject
list / tuplearray
strstring
int / floatnumber
bool (True / False)boolean (true / false)
Nonenull

读写 JSON 格式的数据#

在 Python 中,如果要将字典处理成 JSON 格式(以字符串形式存在),可以使用json模块的dumps函数,代码如下所示。

import json
my_dict = {
'name': '骆昊',
'age': 40,
'friends': ['王大锤', '白元芳'],
'cars': [
{'brand': 'BMW', 'max_speed': 240},
{'brand': 'Audi', 'max_speed': 280},
{'brand': 'Benz', 'max_speed': 280}
]
}
print(json.dumps(my_dict))

运行上面的代码,输出如下所示,可以注意到中文字符都是用 Unicode 编码显示的。

{"name": "\u9a86\u660a", "age": 40, "friends": ["\u738b\u5927\u9524", "\u767d\u5143\u82b3"], "cars": [{"brand": "BMW", "max_speed": 240}, {"brand": "Audi", "max_speed": 280}, {"brand": "Benz", "max_speed": 280}]}

如果要将字典处理成 JSON 格式并写入文本文件,只需要将dumps函数换成dump函数并传入文件对象即可,代码如下所示。

import json
my_dict = {
'name': '骆昊',
'age': 40,
'friends': ['王大锤', '白元芳'],
'cars': [
{'brand': 'BMW', 'max_speed': 240},
{'brand': 'Audi', 'max_speed': 280},
{'brand': 'Benz', 'max_speed': 280}
]
}
with open('data.json', 'w') as file:
json.dump(my_dict, file)

执行上面的代码,会创建data.json文件,文件的内容跟上面代码的输出是一样的。

json模块有四个比较重要的函数,分别是:

  • dump - 将 Python 对象按照 JSON 格式序列化到文件中
  • dumps - 将 Python 对象处理成 JSON 格式的字符串
  • load - 将文件中的 JSON 数据反序列化成对象
  • loads - 将字符串的内容反序列化成 Python 对象

这里出现了两个概念,一个叫序列化,一个叫反序列化,维基百科上的解释是:“序列化(serialization)在计算机科学的数据处理中,是指将数据结构或对象状态转换为可以存储或传输的形式,这样在需要的时候能够恢复到原先的状态,而且通过序列化的数据重新获取字节时,可以利用这些字节来产生原始对象的副本(拷贝)。与这个过程相反的动作,即从一系列字节中提取数据结构的操作,就是反序列化(deserialization)”。

我们可以通过下面的代码,读取上面创建的data.json文件,将 JSON 格式的数据还原成 Python 中的字典。

import json
with open('data.json', 'r') as file:
my_dict = json.load(file)
print(type(my_dict))
print(my_dict)

包管理工具 pip#

Python 标准库中的json模块在数据序列化和反序列化时性能并不是非常理想,为了解决这个问题,可以使用三方库ujson来替换json。所谓三方库,是指非公司内部开发和使用的,也不是来自于官方标准库的 Python 模块,这些模块通常由其他公司、组织或个人开发,所以被称为三方库。虽然 Python 语言的标准库虽然已经提供了诸多模块来方便我们的开发,但是对于一个强大的语言来说,它的生态圈一定也是非常繁荣的。

之前安装 Python 解释器时,默认情况下已经勾选了安装 pip,大家可以在命令提示符或终端中通过pip --version来确定是否已经拥有了 pip。Pip 是 Python 的包管理工具,通过 pip 可以查找、安装、卸载、更新 Python 的三方库或工具,例如要安装替代json模块的ujson,可以使用下面的命令。

Terminal window
pip install ujson

在默认情况下,pip 会访问https://pypi.org/simple/来获得三方库相关的数据,但是国内访问这个网站的速度并不是十分理想,因此国内用户可以使用豆瓣网或清华大学提供的镜像来替代这个默认的下载源,再执行安装三方库的操作,命令如下所示。

Terminal window
pip config set global.index-url https://pypi.doubanio.com/simple
pip install ujson

可以通过pip search命令根据名字查找需要的三方库,可以通过pip list命令来查看已经安装过的三方库。如果想更新某个三方库,可以使用pip install -U或pip install --upgrade;如果要删除某个三方库,可以使用pip uninstall命令。

搜索ujson三方库。

Terminal window
pip search ujson
micropython-cpython-ujson (0.2) - MicroPython module ujson ported to CPython
pycopy-cpython-ujson (0.2) - Pycopy module ujson ported to CPython
ujson (3.0.0) - Ultra fast JSON encoder and decoder for Python
ujson-bedframe (1.33.0) - Ultra fast JSON encoder and decoder for Python
ujson-segfault (2.1.57) - Ultra fast JSON encoder and decoder for Python. Continuing
development.
ujson-ia (2.1.1) - Ultra fast JSON encoder and decoder for Python (Internet
Archive fork)
ujson-x (1.37) - Ultra fast JSON encoder and decoder for Python
ujson-x-legacy (1.35.1) - Ultra fast JSON encoder and decoder for Python
drf_ujson (1.2) - Django Rest Framework UJSON Renderer
drf-ujson2 (1.6.1) - Django Rest Framework UJSON Renderer
ujsonDB (0.1.0) - A lightweight and simple database using ujson.
fast-json (0.3.2) - Combines best parts of json and ujson for fast serialization
decimal-monkeypatch (0.4.3) - Python 2 performance patches: decimal to cdecimal, json to
ujson for psycopg2

查看已经安装的三方库。

Terminal window
pip list
Package Version
----------------------------- ----------
aiohttp 3.5.4
alipay 0.7.4
altgraph 0.16.1
amqp 2.4.2
... ...

更新ujson三方库。

Terminal window
pip install -U ujson

删除ujson三方库。

Terminal window
pip uninstall -y ujson

提示:如果要更新pip自身,对于 macOS 系统来说,可以使用命令pip install -U pip。在 Windows 系统上,可以将命令替换为python -m pip install -U --user pip。

使用网络API获取数据#

如果想在我们自己的程序中显示天气、路况、航班等信息,这些信息我们自己没有能力提供,所以必须使用网络数据服务。目前绝大多数的网络数据服务(或称之为网络 API)都是基于 HTTP 或 HTTPS 提供 JSON 格式的数据,我们可以通过 Python 程序发送 HTTP 请求给指定的 URL(统一资源定位符),这个 URL 就是所谓的网络 API,如果请求成功,它会返回 HTTP 响应,而 HTTP 响应的消息体中就有我们需要的 JSON 格式的数据。关于 HTTP 的相关知识,可以看看阮一峰的《HTTP协议入门》一文。

国内有很多提供网络 API 接口的网站,例如聚合数据、百度智能云等,这些网站上有免费的和付费的数据接口,国外的{API}Search网站也提供了类似的功能,有兴趣的可以自行研究。下面的例子演示了如何使用requests库(基于 HTTP 进行网络资源访问的三方库)访问网络 API 获取国内新闻并显示新闻标题和链接。在这个例子中,我们使用了名为天聚数行的网站提供的国内新闻数据接口,其中的身份标识(API Key)需要自己到网站上注册申请。在天聚数行网站注册账号后会自动分配 API Key,但是要访问接口获取数据,需要绑定验证邮箱或手机,然后还要申请需要使用的接口,如下图所示。

image-20210820151134034

Python 通过 URL 接入网络,我们推荐大家使用requests三方库,它简单且强大,但需要自行安装。

Terminal window
pip install requests

获取国内新闻并显示新闻标题和链接。

import requests
resp = requests.get('http://api.tianapi.com/guonei/?key=APIKey&num=10')
if resp.status_code == 200:
data_model = resp.json()
for news in data_model['newslist']:
print(news['title'])
print(news['url'])
print('-' * 60)

上面的代码通过requests模块的get函数向天聚数行的国内新闻接口发起了一次请求,如果请求过程没有出现问题,get函数会返回一个Response对象,通过该对象的status_code属性表示 HTTP 响应状态码,如果不理解没关系,你只需要关注它的值,如果值等于200或者其他2字头的值,那么我们的请求是成功的。通过Response对象的json()方法可以将返回的 JSON 格式的数据直接处理成 Python 字典,非常方便。天聚数行国内新闻接口返回的 JSON 格式的数据(部分)如下图所示。

提示:上面代码中的APIKey需要换成自己在天聚数行网站申请的API Key。天聚数行网站上还有提供了很多非常有意思的 API 接口,例如:垃圾分类、周公解梦等,大家可以仿照上面的代码来调用这些接口。每个接口都有对应的接口文档,文档中有关于如何使用接口的详细说明。

总结#

Python 中实现序列化和反序列化除了使用json模块之外,还可以使用pickle和shelve模块,但是这两个模块是使用特有的序列化协议来序列化数据,因此序列化后的数据只能被 Python 识别,关于这两个模块的相关知识,有兴趣的读者可以自己查找网络上的资料。处理 JSON 格式的数据很显然是程序员必须掌握的一项技能,因为不管是访问网络 API 接口还是提供网络 API 接口给他人使用,都需要具备处理 JSON 格式数据的相关知识。


23 Python读写CSV文件#

CSV 文件介绍#

CSV(Comma Separated Values)全称逗号分隔值文件是一种简单、通用的文件格式,被广泛的应用于应用程序(数据库、电子表格等)数据的导入和导出以及异构系统之间的数据交换。因为 CSV 是纯文本文件,不管是什么操作系统和编程语言都是可以处理纯文本的,而且很多编程语言中都提供了对读写 CSV 文件的支持,因此 CSV 格式在数据处理和数据科学中被广泛应用。

CSV 文件有以下特点:

  1. 纯文本,使用某种字符集(如 ASCII、Unicode、GB2312等);
  2. 由一条条的记录组成(典型的是每行一条记录);
  3. 每条记录被分隔符(如逗号、分号、制表符等)分隔为字段(列);
  4. 每条记录都有同样的字段序列。

CSV 文件可以使用文本编辑器或类似于 Excel 电子表格这类工具打开和编辑,当使用 Excel 这类电子表格打开 CSV 文件时,你甚至感觉不到 CSV 和 Excel 文件的区别。很多数据库系统都支持将数据导出到 CSV 文件中,当然也支持从 CSV 文件中读入数据保存到数据库中,这些内容并不是现在要讨论的重点。

将数据写入 CSV 文件#

现有五个学生三门课程的考试成绩需要保存到一个 CSV 文件中,要达成这个目标,可以使用 Python 标准库中的csv模块,该模块的writer函数会返回一个csvwriter对象,通过该对象的writerow或writerows方法就可以将数据写入到 CSV 文件中,具体的代码如下所示。

import csv
import random
with open('scores.csv', 'w') as file:
writer = csv.writer(file)
writer.writerow(['姓名', '语文', '数学', '英语'])
names = ['关羽', '张飞', '赵云', '马超', '黄忠']
for name in names:
scores = [random.randrange(50, 101) for _ in range(3)]
scores.insert(0, name)
writer.writerow(scores)

生成的 CSV 文件的内容。

姓名,语文,数学,英语
关羽,98,86,61
张飞,86,58,80
赵云,95,73,70
马超,83,97,55
黄忠,61,54,87

需要说明的是上面的writer函数,除了传入要写入数据的文件对象外,还可以dialect参数,它表示 CSV 文件的方言,默认值是excel。除此之外,还可以通过delimiter、quotechar、quoting参数来指定分隔符(默认是逗号)、包围值的字符(默认是双引号)以及包围的方式。其中,包围值的字符主要用于当字段中有特殊符号时,通过添加包围值的字符可以避免二义性。大家可以尝试将上面第5行代码修改为下面的代码,然后查看生成的 CSV 文件。

writer = csv.writer(file, delimiter='|', quoting=csv.QUOTE_ALL)

生成的 CSV 文件的内容。

"姓名"|"语文"|"数学"|"英语"
"关羽"|"88"|"64"|"65"
"张飞"|"76"|"93"|"79"
"赵云"|"78"|"55"|"76"
"马超"|"72"|"77"|"68"
"黄忠"|"70"|"72"|"51"

从 CSV 文件读取数据#

如果要读取刚才创建的 CSV 文件,可以使用下面的代码,通过csv模块的reader函数可以创建出csvreader对象,该对象是一个迭代器,可以通过next函数或for-in循环读取到文件中的数据。

import csv
with open('scores.csv', 'r') as file:
reader = csv.reader(file, delimiter='|')
for data_list in reader:
print(reader.line_num, end='\t')
for elem in data_list:
print(elem, end='\t')
print()

注意:上面的代码对csvreader对象做for循环时,每次会取出一个列表对象,该列表对象包含了一行中所有的字段。

总结#

将来如果大家使用Python做数据分析,很有可能会用到名为pandas的三方库,它是Python数据分析的神器之一。pandas中封装了名为read_csv和to_csv的函数用来读写 CSV 文件,其中read_csv会将读取到的数据变成一个DataFrame对象,而DataFrame就是pandas库中最重要的类型,它封装了一系列用于数据处理的方法(清洗、转换、聚合等);而to_csv会将DataFrame对象中的数据写入 CSV 文件,完成数据的持久化。read_csv函数和to_csv函数远远比原生的csvreader和csvwriter强大。


24 Python读写Excel文件-1#

Excel简介#

Excel 是 Microsoft(微软)为使用 Windows 和 macOS 操作系统开发的一款电子表格软件。Excel 凭借其直观的界面、出色的计算功能和图表工具,再加上成功的市场营销,一直以来都是最为流行的个人计算机数据处理软件。当然,Excel 也有很多竞品,例如 Google Sheets、LibreOffice Calc、Numbers 等,这些竞品基本上也能够兼容 Excel,至少能够读写较新版本的 Excel 文件,当然这些不是我们讨论的重点。掌握用 Python 程序操作 Excel 文件,可以让日常办公自动化的工作更加轻松愉快,而且在很多商业项目中,导入导出 Excel 文件都是特别常见的功能。

Python 操作 Excel 需要三方库的支持,如果要兼容 Excel 2007 以前的版本,也就是xls格式的 Excel 文件,可以使用三方库xlrd和xlwt,前者用于读 Excel 文件,后者用于写 Excel 文件。如果使用较新版本的 Excel,即xlsx格式的 Excel 文件,可以使用openpyxl库,当然这个库不仅仅可以操作Excel,还可以操作其他基于 Office Open XML 的电子表格文件。

本章我们先讲解基于xlwt和xlrd操作 Excel 文件,大家可以先使用下面的命令安装这两个三方库以及配合使用的工具模块xlutils。

Terminal window
pip install xlwt xlrd xlutils

读Excel文件#

例如在当前文件夹下有一个名为“阿里巴巴2020年股票数据.xls”的 Excel 文件,如果想读取并显示该文件的内容,可以通过如下所示的代码来完成。

import xlrd
# 使用xlrd模块的open_workbook函数打开指定Excel文件并获得Book对象(工作簿)
wb = xlrd.open_workbook('阿里巴巴2020年股票数据.xls')
# 通过Book对象的sheet_names方法可以获取所有表单名称
sheetnames = wb.sheet_names()
print(sheetnames)
# 通过指定的表单名称获取Sheet对象(工作表)
sheet = wb.sheet_by_name(sheetnames[0])
# 通过Sheet对象的nrows和ncols属性获取表单的行数和列数
print(sheet.nrows, sheet.ncols)
for row in range(sheet.nrows):
for col in range(sheet.ncols):
# 通过Sheet对象的cell方法获取指定Cell对象(单元格)
# 通过Cell对象的value属性获取单元格中的值
value = sheet.cell(row, col).value
# 对除首行外的其他行进行数据格式化处理
if row > 0:
# 第1列的xldate类型先转成元组再格式化为“年月日”的格式
if col == 0:
# xldate_as_tuple函数的第二个参数只有0和1两个取值
# 其中0代表以1900-01-01为基准的日期,1代表以1904-01-01为基准的日期
value = xlrd.xldate_as_tuple(value, 0)
value = f'{value[0]}年{value[1]:>02d}月{value[2]:>02d}日'
# 其他列的number类型处理成小数点后保留两位有效数字的浮点数
else:
value = f'{value:.2f}'
print(value, end='\t')
print()
# 获取最后一个单元格的数据类型
# 0 - 空值,1 - 字符串,2 - 数字,3 - 日期,4 - 布尔,5 - 错误
last_cell_type = sheet.cell_type(sheet.nrows - 1, sheet.ncols - 1)
print(last_cell_type)
# 获取第一行的值(列表)
print(sheet.row_values(0))
# 获取指定行指定列范围的数据(列表)
# 第一个参数代表行索引,第二个和第三个参数代表列的开始(含)和结束(不含)索引
print(sheet.row_slice(3, 0, 5))

提示:上面代码中使用的Excel文件“阿里巴巴2020年股票数据.xls”可以通过后面的百度云盘地址进行获取。链接:https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g 提取码。

相信通过上面的代码,大家已经了解到了如何读取一个 Excel 文件,如果想知道更多关于xlrd模块的知识,可以阅读它的官方文档。

写Excel文件#

写入 Excel 文件可以通过xlwt 模块的Workbook类创建工作簿对象,通过工作簿对象的add_sheet方法可以添加工作表,通过工作表对象的write方法可以向指定单元格中写入数据,最后通过工作簿对象的save方法将工作簿写入到指定的文件或内存中。下面的代码实现了将5 个学生 3 门课程的考试成绩写入 Excel 文件的操作。

import random
import xlwt
student_names = ['关羽', '张飞', '赵云', '马超', '黄忠']
scores = [[random.randrange(50, 101) for _ in range(3)] for _ in range(5)]
# 创建工作簿对象(Workbook)
wb = xlwt.Workbook()
# 创建工作表对象(Worksheet)
sheet = wb.add_sheet('一年级二班')
# 添加表头数据
titles = ('姓名', '语文', '数学', '英语')
for index, title in enumerate(titles):
sheet.write(0, index, title)
# 将学生姓名和考试成绩写入单元格
for row in range(len(scores)):
sheet.write(row + 1, 0, student_names[row])
for col in range(len(scores[row])):
sheet.write(row + 1, col + 1, scores[row][col])
# 保存Excel工作簿
wb.save('考试成绩表.xls')

调整单元格样式#

在写Excel文件时,我们还可以为单元格设置样式,主要包括字体(Font)、对齐方式(Alignment)、边框(Border)和背景(Background)的设置,xlwt对这几项设置都封装了对应的类来支持。要设置单元格样式需要首先创建一个XFStyle对象,再通过该对象的属性对字体、对齐方式、边框等进行设定,例如在上面的例子中,如果希望将表头单元格的背景色修改为黄色,可以按照如下的方式进行操作。

header_style = xlwt.XFStyle()
pattern = xlwt.Pattern()
pattern.pattern = xlwt.Pattern.SOLID_PATTERN
# 0 - 黑色、1 - 白色、2 - 红色、3 - 绿色、4 - 蓝色、5 - 黄色、6 - 粉色、7 - 青色
pattern.pattern_fore_colour = 5
header_style.pattern = pattern
titles = ('姓名', '语文', '数学', '英语')
for index, title in enumerate(titles):
sheet.write(0, index, title, header_style)

如果希望为表头设置指定的字体,可以使用Font类并添加如下所示的代码。

font = xlwt.Font()
# 字体名称
font.name = '华文楷体'
# 字体大小(20是基准单位,18表示18px)
font.height = 20 * 18
# 是否使用粗体
font.bold = True
# 是否使用斜体
font.italic = False
# 字体颜色
font.colour_index = 1
header_style.font = font

注意:上面代码中指定的字体名(font.name)应当是本地系统有的字体,例如在我的电脑上有名为“华文楷体”的字体。

如果希望表头垂直居中对齐,可以使用下面的代码进行设置。

align = xlwt.Alignment()
# 垂直方向的对齐方式
align.vert = xlwt.Alignment.VERT_CENTER
# 水平方向的对齐方式
align.horz = xlwt.Alignment.HORZ_CENTER
header_style.alignment = align

如果希望给表头加上黄色的虚线边框,可以使用下面的代码来设置。

borders = xlwt.Borders()
props = (
('top', 'top_colour'), ('right', 'right_colour'),
('bottom', 'bottom_colour'), ('left', 'left_colour')
)
# 通过循环对四个方向的边框样式及颜色进行设定
for position, color in props:
# 使用setattr内置函数动态给对象指定的属性赋值
setattr(borders, position, xlwt.Borders.DASHED)
setattr(borders, color, 5)
header_style.borders = borders

如果要调整单元格的宽度(列宽)和表头的高度(行高),可以按照下面的代码进行操作。

# 设置行高为40px
sheet.row(0).set_style(xlwt.easyxf(f'font:height {20 * 40}'))
titles = ('姓名', '语文', '数学', '英语')
for index, title in enumerate(titles):
# 设置列宽为200px
sheet.col(index).width = 20 * 200
# 设置单元格的数据和样式
sheet.write(0, index, title, header_style)

公式计算#

对于前面打开的“阿里巴巴2020年股票数据.xls”文件,如果要统计全年收盘价(Close字段)的平均值以及全年交易量(Volume字段)的总和,可以使用Excel的公式计算即可。我们可以先使用xlrd读取Excel文件夹,然后通过xlutils三方库提供的copy函数将读取到的Excel文件转成Workbook对象进行写操作,在调用write方法时,可以将一个Formula对象写入单元格。

实现公式计算的代码如下所示。

import xlrd
import xlwt
from xlutils.copy import copy
wb_for_read = xlrd.open_workbook('阿里巴巴2020年股票数据.xls')
sheet1 = wb_for_read.sheet_by_index(0)
nrows, ncols = sheet1.nrows, sheet1.ncols
wb_for_write = copy(wb_for_read)
sheet2 = wb_for_write.get_sheet(0)
sheet2.write(nrows, 4, xlwt.Formula(f'average(E2:E{nrows})'))
sheet2.write(nrows, 6, xlwt.Formula(f'sum(G2:G{nrows})'))
wb_for_write.save('阿里巴巴2020年股票数据汇总.xls')

说明:上面的代码有一些小瑕疵,有兴趣的读者可以自行探索并思考如何解决。

总结#

掌握了 Python 程序操作 Excel 的方法,可以解决日常办公中很多繁琐的处理 Excel 电子表格工作,最常见就是将多个数据格式相同的 Excel 文件合并到一个文件以及从多个 Excel 文件或表单中提取指定的数据。当然,如果要对表格数据进行处理,使用 Python 数据分析神器之一的 pandas 库可能更为方便。


25 Python读写Excel文件-2#

Excel简介#

Excel 是 Microsoft(微软)为使用 Windows 和 macOS 操作系统开发的一款电子表格软件。Excel 凭借其直观的界面、出色的计算功能和图表工具,再加上成功的市场营销,一直以来都是最为流行的个人计算机数据处理软件。当然,Excel 也有很多竞品,例如 Google Sheets、LibreOffice Calc、Numbers 等,这些竞品基本上也能够兼容 Excel,至少能够读写较新版本的 Excel 文件,当然这些不是我们讨论的重点。掌握用 Python 程序操作 Excel 文件,可以让日常办公自动化的工作更加轻松愉快,而且在很多商业项目中,导入导出 Excel 文件都是特别常见的功能。

本章我们继续讲解基于另一个三方库openpyxl如何进行 Excel 文件操作,首先需要先安装它。

Terminal window
pip install openpyxl

openpyxl的优点在于,当我们打开一个 Excel 文件后,既可以对它进行读操作,又可以对它进行写操作,而且在操作的便捷性上是优于xlwt和xlrd的。此外,如果要进行样式编辑和公式计算,使用openpyxl也远比上一个章节我们讲解的方式更为简单,而且openpyxl还支持数据透视和插入图表等操作,功能非常强大。有一点需要再次强调,openpyxl并不支持操作 Office 2007 以前版本的 Excel 文件。

读取Excel文件#

例如在当前文件夹下有一个名为“阿里巴巴2020年股票数据.xlsx”的 Excel 文件,如果想读取并显示该文件的内容,可以通过如下所示的代码来完成。

import datetime
import openpyxl
# 加载一个工作簿 ---> Workbook
wb = openpyxl.load_workbook('阿里巴巴2020年股票数据.xlsx')
# 获取工作表的名字
print(wb.sheetnames)
# 获取工作表 ---> Worksheet
sheet = wb.worksheets[0]
# 获得单元格的范围
print(sheet.dimensions)
# 获得行数和列数
print(sheet.max_row, sheet.max_column)
# 获取指定单元格的值
print(sheet.cell(3, 3).value)
print(sheet['C3'].value)
print(sheet['G255'].value)
# 获取多个单元格(嵌套元组)
print(sheet['A2:C5'])
# 读取所有单元格的数据
for row_ch in range(2, sheet.max_row + 1):
for col_ch in 'ABCDEFG':
value = sheet[f'{col_ch}{row_ch}'].value
if type(value) == datetime.datetime:
print(value.strftime('%Y年%m月%d日'), end='\t')
elif type(value) == int:
print(f'{value:<10d}', end='\t')
elif type(value) == float:
print(f'{value:.4f}', end='\t')
else:
print(value, end='\t')
print()

提示:上面代码中使用的Excel文件“阿里巴巴2020年股票数据.xlsx”可以通过后面的百度云盘地址进行获取。链接:https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g 提取码。

需要提醒大家一点,openpyxl获取指定的单元格有两种方式,一种是通过cell方法,需要注意,该方法的行索引和列索引都是从1开始的,这是为了照顾用惯了 Excel 的人的习惯;另一种是通过索引运算,通过指定单元格的坐标,例如C3、G255,也可以取得对应的单元格,再通过单元格对象的value属性,就可以获取到单元格的值。通过上面的代码,相信大家还注意到了,可以通过类似sheet['A2:C5']或sheet['A2':'C5']这样的切片操作获取多个单元格,该操作将返回嵌套的元组,相当于获取到了多行多列。

写Excel文件#

下面我们使用openpyxl来进行写 Excel 操作。

import random
import openpyxl
# 第一步:创建工作簿(Workbook)
wb = openpyxl.Workbook()
# 第二步:添加工作表(Worksheet)
sheet = wb.active
sheet.title = '期末成绩'
titles = ('姓名', '语文', '数学', '英语')
for col_index, title in enumerate(titles):
sheet.cell(1, col_index + 1, title)
names = ('关羽', '张飞', '赵云', '马超', '黄忠')
for row_index, name in enumerate(names):
sheet.cell(row_index + 2, 1, name)
for col_index in range(2, 5):
sheet.cell(row_index + 2, col_index, random.randrange(50, 101))
# 第四步:保存工作簿
wb.save('考试成绩表.xlsx')

调整样式和公式计算#

在使用openpyxl操作 Excel 时,如果要调整单元格的样式,可以直接通过单元格对象(Cell对象)的属性进行操作。单元格对象的属性包括字体(font)、对齐(alignment)、边框(border)等,具体的可以参考openpyxl的官方文档。在使用openpyxl时,如果需要做公式计算,可以完全按照 Excel 中的操作方式来进行,具体的代码如下所示。

import openpyxl
from openpyxl.styles import Font, Alignment, Border, Side
# 对齐方式
alignment = Alignment(horizontal='center', vertical='center')
# 边框线条
side = Side(color='ff7f50', style='mediumDashed')
wb = openpyxl.load_workbook('考试成绩表.xlsx')
sheet = wb.worksheets[0]
# 调整行高和列宽
sheet.row_dimensions[1].height = 30
sheet.column_dimensions['E'].width = 120
sheet['E1'] = '平均分'
# 设置字体
sheet.cell(1, 5).font = Font(size=18, bold=True, color='ff1493', name='华文楷体')
# 设置对齐方式
sheet.cell(1, 5).alignment = alignment
# 设置单元格边框
sheet.cell(1, 5).border = Border(left=side, top=side, right=side, bottom=side)
for i in range(2, 7):
# 公式计算每个学生的平均分
sheet[f'E{i}'] = f'=average(B{i}:D{i})'
sheet.cell(i, 5).font = Font(size=12, color='4169e1', italic=True)
sheet.cell(i, 5).alignment = alignment
wb.save('考试成绩表.xlsx')

生成统计图表#

通过openpyxl库,可以直接向 Excel 中插入统计图表,具体的做法跟在 Excel 中插入图表大体一致。我们可以创建指定类型的图表对象,然后通过该对象的属性对图表进行设置。当然,最为重要的是为图表绑定数据,即横轴代表什么,纵轴代表什么,具体的数值是多少。最后,可以将图表对象添加到表单中,具体的代码如下所示。

from openpyxl import Workbook
from openpyxl.chart import BarChart, Reference
wb = Workbook(write_only=True)
sheet = wb.create_sheet()
rows = [
('类别', '销售A组', '销售B组'),
('手机', 40, 30),
('平板', 50, 60),
('笔记本', 80, 70),
('外围设备', 20, 10),
]
# 向表单中添加行
for row in rows:
sheet.append(row)
# 创建图表对象
chart = BarChart()
chart.type = 'col'
chart.style = 10
# 设置图表的标题
chart.title = '销售统计图'
# 设置图表纵轴的标题
chart.y_axis.title = '销量'
# 设置图表横轴的标题
chart.x_axis.title = '商品类别'
# 设置数据的范围
data = Reference(sheet, min_col=2, min_row=1, max_row=5, max_col=3)
# 设置分类的范围
cats = Reference(sheet, min_col=1, min_row=2, max_row=5)
# 给图表添加数据
chart.add_data(data, titles_from_data=True)
# 给图表设置分类
chart.set_categories(cats)
chart.shape = 4
# 将图表添加到表单指定的单元格中
sheet.add_chart(chart, 'A10')
wb.save('demo.xlsx')

运行上面的代码,打开生成的 Excel 文件,效果如下图所示。

总结#

掌握了 Python 程序操作 Excel 的方法,可以解决日常办公中很多繁琐的处理 Excel 电子表格工作,最常见就是将多个数据格式相同的Excel 文件合并到一个文件以及从多个 Excel 文件或表单中提取指定的数据。如果数据体量较大或者处理数据的方式比较复杂,我们还是推荐大家使用 Python 数据分析神器之一的 pandas 库。


26 Python操作Word和PowerPoint文件#

在日常工作中,有很多简单重复的劳动其实完全可以交给 Python 程序,比如根据样板文件(模板文件)批量的生成很多个 Word 文件或 PowerPoint 文件。Word 是微软公司开发的文字处理程序,相信大家都不陌生,日常办公中很多正式的文档都是用 Word 进行撰写和编辑的,目前使用的 Word 文件后缀名一般为.docx。PowerPoint 是微软公司开发的演示文稿程序,是微软的 Office 系列软件中的一员,被商业人士、教师、学生等群体广泛使用,通常也将其称之为“幻灯片”。在 Python 中,可以使用名为python-docx 的三方库来操作 Word,可以使用名为python-pptx的三方库来生成 PowerPoint。

操作Word文档#

我们可以先通过下面的命令来安装python-docx三方库。

Terminal window
pip install python-docx

按照官方文档的介绍,我们可以使用如下所示的代码来生成一个简单的 Word 文档。

from docx import Document
from docx.shared import Cm, Pt
from docx.document import Document as Doc
# 创建代表Word文档的Doc对象
document = Document() # type: Doc
# 添加大标题
document.add_heading('快快乐乐学Python', 0)
# 添加段落
p = document.add_paragraph('Python是一门非常流行的编程语言,它')
run = p.add_run('简单')
run.bold = True
run.font.size = Pt(18)
p.add_run('而且')
run = p.add_run('优雅')
run.font.size = Pt(18)
run.underline = True
p.add_run('。')
# 添加一级标题
document.add_heading('Heading, level 1', level=1)
# 添加带样式的段落
document.add_paragraph('Intense quote', style='Intense Quote')
# 添加无序列表
document.add_paragraph(
'first item in unordered list', style='List Bullet'
)
document.add_paragraph(
'second item in ordered list', style='List Bullet'
)
# 添加有序列表
document.add_paragraph(
'first item in ordered list', style='List Number'
)
document.add_paragraph(
'second item in ordered list', style='List Number'
)
# 添加图片(注意路径和图片必须要存在)
document.add_picture('resources/guido.jpg', width=Cm(5.2))
# 添加分节符
document.add_section()
records = (
('骆昊', '男', '1995-5-5'),
('孙美丽', '女', '1992-2-2')
)
# 添加表格
table = document.add_table(rows=1, cols=3)
table.style = 'Dark List'
hdr_cells = table.rows[0].cells
hdr_cells[0].text = '姓名'
hdr_cells[1].text = '性别'
hdr_cells[2].text = '出生日期'
# 为表格添加行
for name, sex, birthday in records:
row_cells = table.add_row().cells
row_cells[0].text = name
row_cells[1].text = sex
row_cells[2].text = birthday
# 添加分页符
document.add_page_break()
# 保存文档
document.save('demo.docx')

提示:上面代码第7行中的注释# type: Doc是为了在PyCharm中获得代码补全提示,因为如果不清楚对象具体的数据类型,PyCharm 无法在后续代码中给出Doc对象的代码补全提示。

执行上面的代码,打开生成的 Word 文档,效果如下图所示。

  

对于一个已经存在的 Word 文件,我们可以通过下面的代码去遍历它所有的段落并获取对应的内容。

from docx import Document
from docx.document import Document as Doc
doc = Document('resources/离职证明.docx') # type: Doc
for no, p in enumerate(doc.paragraphs):
print(no, p.text)

提示:如果需要上面代码中的 Word 文件,可以通过下面的百度云盘地址进行获取。链接:https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g 提取码。

读取到的内容如下所示。

0
1 离 职 证 明
2
3 兹证明 王大锤 ,身份证号码: 100200199512120001 ,于 2018 年 8 月 7 日至 2020 年 6 月 28 日在我单位 开发部 部门担任 Java开发工程师 职务,在职期间无不良表现。因 个人 原因,于 2020 年 6 月 28 日起终止解除劳动合同。现已结清财务相关费用,办理完解除劳动关系相关手续,双方不存在任何劳动争议。
4
5 特此证明!
6
7
8 公司名称(盖章):成都风车车科技有限公司
9 2020 年 6 月 28 日

讲到这里,相信很多读者已经想到了,我们可以把上面的离职证明制作成一个模板文件,把姓名、身份证号、入职和离职日期等信息用占位符代替,这样通过对占位符的替换,就可以根据实际需要写入对应的信息,这样就可以批量的生成 Word 文档。

按照上面的思路,我们首先编辑一个离职证明的模板文件,如下图所示。

接下来我们读取该文件,将占位符替换为真实信息,就可以生成一个新的 Word 文档,如下所示。

from docx import Document
from docx.document import Document as Doc
# 将真实信息用字典的方式保存在列表中
employees = [
{
'name': '骆昊',
'id': '100200198011280001',
'sdate': '2008年3月1日',
'edate': '2012年2月29日',
'department': '产品研发',
'position': '架构师',
'company': '成都华为技术有限公司'
},
{
'name': '王大锤',
'id': '510210199012125566',
'sdate': '2019年1月1日',
'edate': '2021年4月30日',
'department': '产品研发',
'position': 'Python开发工程师',
'company': '成都谷道科技有限公司'
},
{
'name': '李元芳',
'id': '2102101995103221599',
'sdate': '2020年5月10日',
'edate': '2021年3月5日',
'department': '产品研发',
'position': 'Java开发工程师',
'company': '同城企业管理集团有限公司'
},
]
# 对列表进行循环遍历,批量生成Word文档
for emp_dict in employees:
# 读取离职证明模板文件
doc = Document('resources/离职证明模板.docx') # type: Doc
# 循环遍历所有段落寻找占位符
for p in doc.paragraphs:
if '{' not in p.text:
continue
# 不能直接修改段落内容,否则会丢失样式
# 所以需要对段落中的元素进行遍历并进行查找替换
for run in p.runs:
if '{' not in run.text:
continue
# 将占位符换成实际内容
start, end = run.text.find('{'), run.text.find('}')
key, place_holder = run.text[start + 1:end], run.text[start:end + 1]
run.text = run.text.replace(place_holder, emp_dict[key])
# 每个人对应保存一个Word文档
doc.save(f'{emp_dict["name"]}离职证明.docx')

执行上面的代码,会在当前路径下生成三个 Word 文档,如下图所示。

生成PowerPoint#

首先我们需要安装名为python-pptx的三方库,命令如下所示。

Terminal window
pip install python-pptx

用 Python 操作 PowerPoint 的内容,因为实际应用场景不算很多,我不打算在这里进行赘述,有兴趣的读者可以自行阅读python-pptx的官方文档,下面仅展示一段来自于官方文档的代码。

from pptx import Presentation
# 创建幻灯片对象
pres = Presentation()
# 选择母版添加一页
title_slide_layout = pres.slide_layouts[0]
slide = pres.slides.add_slide(title_slide_layout)
# 获取标题栏和副标题栏
title = slide.shapes.title
subtitle = slide.placeholders[1]
# 编辑标题和副标题
title.text = "Welcome to Python"
subtitle.text = "Life is short, I use Python"
# 选择母版添加一页
bullet_slide_layout = pres.slide_layouts[1]
slide = pres.slides.add_slide(bullet_slide_layout)
# 获取页面上所有形状
shapes = slide.shapes
# 获取标题和主体
title_shape = shapes.title
body_shape = shapes.placeholders[1]
# 编辑标题
title_shape.text = 'Introduction'
# 编辑主体内容
tf = body_shape.text_frame
tf.text = 'History of Python'
# 添加一个一级段落
p = tf.add_paragraph()
p.text = 'X\'max 1989'
p.level = 1
# 添加一个二级段落
p = tf.add_paragraph()
p.text = 'Guido began to write interpreter for Python.'
p.level = 2
# 保存幻灯片
pres.save('test.pptx')

运行上面的代码,生成的 PowerPoint 文件如下图所示。

总结#

用 Python 程序解决办公自动化的问题真的非常酷,它可以将我们从繁琐乏味的劳动中解放出来。写这类代码就是去做一件一劳永逸的事情,写代码的过程即便不怎么愉快,使用这些代码的时候应该是非常开心的。


27 Python操作PDF文件#

PDF 是 Portable Document Format 的缩写,这类文件通常使用.pdf作为其扩展名。在日常开发工作中,最容易遇到的就是从 PDF 中读取文本内容以及用已有的内容生成PDF文档这两个任务。

从PDF中提取文本#

在 Python 中,可以使用名为PyPDF2的三方库来读取 PDF 文件,可以使用下面的命令来安装它。

Terminal window
pip install PyPDF2

PyPDF2没有办法从 PDF 文档中提取图像、图表或其他媒体,但它可以提取文本,并将其返回为 Python 字符串。

import PyPDF2
reader = PyPDF2.PdfReader('test.pdf')
for page in reader.pages:
print(page.extract_text())

提示:本章代码使用到的 PDF 文件都可以通过下面的百度云盘地址进行获取,链接:https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g,提取码:e7b4。

当然,PyPDF2并不是什么样的 PDF 文档都能提取出文字来,这个问题就我所知并没有什么特别好的解决方法,尤其是在提取中文的时候。网上也有很多讲解从 PDF 中提取文字的文章,推荐大家自行阅读《三大神器助力Python提取pdf文档信息》一文进行了解。

要从 PDF 文件中提取文本也可以直接使用三方的命令行工具,具体的做法如下所示。

Terminal window
pip install pdfminer.six
pdf2text.py test.pdf

旋转和叠加页面#

上面的代码中通过创建PdfFileReader对象的方式来读取 PDF 文档,该对象的getPage方法可以获得PDF文档的指定页并得到一个PageObject对象,通过PageObject对象的rotateClockwise和rotateCounterClockwise方法可以实现页面的顺时针和逆时针方向旋转,通过PageObject对象的addBlankPage方法可以添加一个新的空白页,代码如下所示。

reader = PyPDF2.PdfReader('XGBoost.pdf')
writer = PyPDF2.PdfWriter()
for no, page in enumerate(reader.pages):
if no % 2 == 0:
new_page = page.rotate(-90)
else:
new_page = page.rotate(90)
writer.add_page(new_page)
with open('temp.pdf', 'wb') as file_obj:
writer.write(file_obj)

加密PDF文件#

使用PyPDF2中的PdfFileWrite对象可以为PDF文档加密,如果需要给一系列的PDF文档设置统一的访问口令,使用Python程序来处理就会非常的方便。

import PyPDF2
reader = PyPDF2.PdfReader('XGBoost.pdf')
writer = PyPDF2.PdfWriter()
for page in reader.pages:
writer.add_page(page)
writer.encrypt('foobared')
with open('temp.pdf', 'wb') as file_obj:
writer.write(file_obj)

批量添加水印#

上面提到的PageObject对象还有一个名为mergePage的方法,可以两个 PDF 页面进行叠加,通过这个操作,我们很容易实现给PDF文件添加水印的功能。例如要给上面的“XGBoost.pdf”文件添加一个水印,我们可以先准备好一个提供水印页面的 PDF 文件,然后将包含水印的PageObject读取出来,然后再循环遍历“XGBoost.pdf”文件的每个页,获取到PageObject对象,然后通过mergePage方法实现水印页和原始页的合并,代码如下所示。

reader1 = PyPDF2.PdfReader('XGBoost.pdf')
reader2 = PyPDF2.PdfReader('watermark.pdf')
writer = PyPDF2.PdfWriter()
watermark_page = reader2.pages[0]
for page in reader1.pages:
page.merge_page(watermark_page)
writer.add_page(page)
with open('temp.pdf', 'wb') as file_obj:
writer.write(file_obj)

如果愿意,还可以让奇数页和偶数页使用不同的水印,大家可以自己思考下应该怎么做。

创建PDF文件#

创建 PDF 文档需要三方库reportlab的支持,安装的方法如下所示。

Terminal window
pip install reportlab

下面通过一个例子为大家展示reportlab的用法。

from reportlab.lib.pagesizes import A4
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfgen import canvas
pdf_canvas = canvas.Canvas('resources/demo.pdf', pagesize=A4)
width, height = A4
# 绘图
image = canvas.ImageReader('resources/guido.jpg')
pdf_canvas.drawImage(image, 20, height - 395, 250, 375)
# 显示当前页
pdf_canvas.showPage()
# 注册字体文件
pdfmetrics.registerFont(TTFont('Font1', 'resources/fonts/Vera.ttf'))
pdfmetrics.registerFont(TTFont('Font2', 'resources/fonts/青呱石头体.ttf'))
# 写字
pdf_canvas.setFont('Font2', 40)
pdf_canvas.setFillColorRGB(0.9, 0.5, 0.3, 1)
pdf_canvas.drawString(width // 2 - 120, height // 2, '你好,世界!')
pdf_canvas.setFont('Font1', 40)
pdf_canvas.setFillColorRGB(0, 1, 0, 0.5)
pdf_canvas.rotate(18)
pdf_canvas.drawString(250, 250, 'hello, world!')
# 保存
pdf_canvas.save()

上面的代码如果不太理解也没有关系,等真正需要用 Python 创建 PDF 文档的时候,再好好研读一下reportlab的官方文档就可以了。

提示:上面代码中用到的图片和字体可以通过下面的百度云盘链接获取,链接:https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g,提取码:e7b4。

总结#

在学习完上面的内容之后,相信大家已经知道像合并多个 PDF 文件这样的工作应该如何用 Python 代码来处理了,赶紧自己动手试一试吧。


28 Python处理图像#

入门知识#

  1. 颜色。如果你有使用颜料画画的经历,那么一定知道混合红、黄、蓝三种颜料可以得到其他的颜色,事实上这三种颜色就是美术中的三原色,它们是不能再分解的基本颜色。在计算机中,我们可以将红、绿、蓝三种色光以不同的比例叠加来组合成其他的颜色,因此这三种颜色就是色光三原色。在计算机系统中,我们通常会将一个颜色表示为一个 RGB 值或 RGBA 值(其中的 A 表示 Alpha 通道,它决定了透过这个图像的像素,也就是透明度)。

    名称RGB值名称RGB值
    White(白)(255, 255, 255)Red(红)(255, 0, 0)
    Green(绿)(0, 255, 0)Blue(蓝)(0, 0, 255)
    Gray(灰)(128, 128, 128)Yellow(黄)(255, 255, 0)
    Black(黑)(0, 0, 0)Purple(紫)(128, 0, 128)
  2. 像素。对于一个由数字序列表示的图像来说,最小的单位就是图像上单一颜色的小方格,这些小方块都有一个明确的位置和被分配的色彩数值,而这些一小方格的颜色和位置决定了该图像最终呈现出来的样子,它们是不可分割的单位,我们通常称之为像素(pixel)。每一个图像都包含了一定量的像素,这些像素决定图像在屏幕上所呈现的大小,大家如果爱好拍照或者自拍,对像素这个词就不会陌生。

用Pillow处理图像#

Pillow 是由从著名的 Python 图像处理库 PIL 发展出来的一个分支,通过 Pillow 可以实现图像压缩和图像处理等各种操作。可以使用下面的命令来安装 Pillow。

Terminal window
pip install pillow

Pillow 中最为重要的是Image类,可以通过Image模块的open函数来读取图像并获得Image类型的对象。

  1. 读取和显示图像

    from PIL import Image
    # 读取图像获得Image对象
    image = Image.open('guido.jpg')
    # 通过Image对象的format属性获得图像的格式
    print(image.format) # JPEG
    # 通过Image对象的size属性获得图像的尺寸
    print(image.size) # (500, 750)
    # 通过Image对象的mode属性获取图像的模式
    print(image.mode) # RGB
    # 通过Image对象的show方法显示图像
    image.show()
  2. 剪裁图像

    # 通过Image对象的crop方法指定剪裁区域剪裁图像
    image.crop((80, 20, 310, 360)).show()
  3. 生成缩略图

    # 通过Image对象的thumbnail方法生成指定尺寸的缩略图
    image.thumbnail((128, 128))
    image.show()
  4. 缩放和黏贴图像

    # 读取骆昊的照片获得Image对象
    luohao_image = Image.open('luohao.png')
    # 读取吉多的照片获得Image对象
    guido_image = Image.open('guido.jpg')
    # 从吉多的照片上剪裁出吉多的头
    guido_head = guido_image.crop((80, 20, 310, 360))
    width, height = guido_head.size
    # 使用Image对象的resize方法修改图像的尺寸
    # 使用Image对象的paste方法将吉多的头粘贴到骆昊的照片上
    luohao_image.paste(guido_head.resize((int(width / 1.5), int(height / 1.5))), (172, 40))
    luohao_image.show()
  5. 旋转和翻转

    image = Image.open('guido.jpg')
    # 使用Image对象的rotate方法实现图像的旋转
    image.rotate(45).show()
    # 使用Image对象的transpose方法实现图像翻转
    # Image.FLIP_LEFT_RIGHT - 水平翻转
    # Image.FLIP_TOP_BOTTOM - 垂直翻转
    image.transpose(Image.FLIP_TOP_BOTTOM).show()
  6. 操作像素

    for x in range(80, 310):
    for y in range(20, 360):
    # 通过Image对象的putpixel方法修改图像指定像素点
    image.putpixel((x, y), (128, 128, 128))
    image.show()
  7. 滤镜效果

    from PIL import ImageFilter
    # 使用Image对象的filter方法对图像进行滤镜处理
    # ImageFilter模块包含了诸多预设的滤镜也可以自定义滤镜
    image.filter(ImageFilter.CONTOUR).show()

使用Pillow绘图#

Pillow 中有一个名为ImageDraw的模块,该模块的Draw函数会返回一个ImageDraw对象,通过ImageDraw对象的arc、line、rectangle、ellipse、polygon等方法,可以在图像上绘制出圆弧、线条、矩形、椭圆、多边形等形状,也可以通过该对象的text方法在图像上添加文字。

要绘制如上图所示的图像,完整的代码如下所示。

import random
from PIL import Image, ImageDraw, ImageFont
def random_color():
"""生成随机颜色"""
red = random.randint(0, 255)
green = random.randint(0, 255)
blue = random.randint(0, 255)
return red, green, blue
width, height = 800, 600
# 创建一个800*600的图像,背景色为白色
image = Image.new(mode='RGB', size=(width, height), color=(255, 255, 255))
# 创建一个ImageDraw对象
drawer = ImageDraw.Draw(image)
# 通过指定字体和大小获得ImageFont对象
font = ImageFont.truetype('Kongxin.ttf', 32)
# 通过ImageDraw对象的text方法绘制文字
drawer.text((300, 50), 'Hello, world!', fill=(255, 0, 0), font=font)
# 通过ImageDraw对象的line方法绘制两条对角直线
drawer.line((0, 0, width, height), fill=(0, 0, 255), width=2)
drawer.line((width, 0, 0, height), fill=(0, 0, 255), width=2)
xy = width // 2 - 60, height // 2 - 60, width // 2 + 60, height // 2 + 60
# 通过ImageDraw对象的rectangle方法绘制矩形
drawer.rectangle(xy, outline=(255, 0, 0), width=2)
# 通过ImageDraw对象的ellipse方法绘制椭圆
for i in range(4):
left, top, right, bottom = 150 + i * 120, 220, 310 + i * 120, 380
drawer.ellipse((left, top, right, bottom), outline=random_color(), width=8)
# 显示图像
image.show()
# 保存图像
image.save('result.png')

注意:上面代码中使用的字体文件需要根据自己准备,可以选择自己喜欢的字体文件并放置在代码目录下。

总结#

使用 Python 语言做开发,除了可以用 Pillow 来处理图像外,还可以使用更为强大的 OpenCV 库来完成图形图像的处理,OpenCV(Open Source Computer Vision Library)是一个跨平台的计算机视觉库,可以用来开发实时图像处理、计算机视觉和模式识别程序。在我们的日常工作中,有很多繁琐乏味的任务其实都可以通过 Python 程序来处理,编程的目的就是让计算机帮助我们解决问题,减少重复乏味的劳动。通过本章节的学习,相信大家已经感受到了使用 Python 程序绘图改图的乐趣,其实 Python 能做的事情还远不止这些,继续你的学习吧。


29 Python发送邮件和短信#

在前面的课程中,我们已经教会大家如何用 Python 程序自动的生成 Excel、Word、PDF 文档,接下来我们还可以更进一步,就是通过邮件将生成好的文档发送给指定的收件人,然后用短信告知对方我们发出了邮件。这些事情利用 Python 程序也可以轻松愉快的解决。

发送电子邮件#

在即时通信软件如此发达的今天,电子邮件仍然是互联网上使用最为广泛的应用之一,公司向应聘者发出录用通知、网站向用户发送一个激活账号的链接、银行向客户推广它们的理财产品等几乎都是通过电子邮件来完成的,而这些任务应该都是由程序自动完成的。

我们可以用 HTTP(超文本传输协议)来访问网站资源,HTTP 是一个应用级协议,它建立在 TCP(传输控制协议)之上,TCP 为很多应用级协议提供了可靠的数据传输服务。如果要发送电子邮件,需要使用 SMTP(简单邮件传输协议),它也是建立在 TCP 之上的应用级协议,规定了邮件的发送者如何跟邮件服务器进行通信的细节。Python 通过名为smtplib的模块将这些操作简化成了SMTP_SSL对象,通过该对象的login和send_mail方法,就能够完成发送邮件的操作。

我们先尝试一下发送一封极为简单的邮件,该邮件不包含附件、图片以及其他超文本内容。发送邮件首先需要接入邮件服务器,我们可以自己架设邮件服务器,这件事情对新手并不友好,但是我们可以选择使用第三方提供的邮件服务。例如,我在<www.126.com>已经注册了账号,登录成功之后,就可以在设置中开启 SMTP 服务,这样就相当于获得了邮件服务器,具体的操作如下所示。

image-20210820190306861

用手机扫码上面的二维码可以通过发送短信的方式来获取授权码,短信发送成功后,点击“我已发送”就可以获得授权码。授权码需要妥善保管,因为一旦泄露就会被其他人冒用你的身份来发送邮件。接下来,我们就可以编写发送邮件的代码了,如下所示。

import smtplib
from email.header import Header
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
# 创建邮件主体对象
email = MIMEMultipart()
# 设置发件人、收件人和主题
email['From'] = 'xxxxxxxxx@126.com'
email['To'] = 'yyyyyy@qq.com;zzzzzz@1000phone.com'
email['Subject'] = Header('上半年工作情况汇报', 'utf-8')
# 添加邮件正文内容
content = """据德国媒体报道,当地时间9日,德国火车司机工会成员进行了投票,
定于当地时间10日起进行全国性罢工,货运交通方面的罢工已于当地时间10日19时开始。
此后,从11日凌晨2时到13日凌晨2时,德国全国范围内的客运和铁路基础设施将进行48小时的罢工。"""
email.attach(MIMEText(content, 'plain', 'utf-8'))
# 创建SMTP_SSL对象(连接邮件服务器)
smtp_obj = smtplib.SMTP_SSL('smtp.126.com', 465)
# 通过用户名和授权码进行登录
smtp_obj.login('xxxxxxxxx@126.com', '邮件服务器的授权码')
# 发送邮件(发件人、收件人、邮件内容(字符串))
smtp_obj.sendmail(
'xxxxxxxxx@126.com',
['yyyyyy@qq.com', 'zzzzzz@1000phone.com'],
email.as_string()
)

如果要发送带有附件的邮件,只需要将附件的内容处理成 BASE64 编码,那么它就和普通的文本内容几乎没有什么区别。BASE64 是一种基于 64 个可打印字符来表示二进制数据的表示方法,常用于某些需要表示、传输、存储二进制数据的场合,电子邮件就是其中之一。对这种编码方式不理解的同学,推荐阅读《Base64笔记》一文。在之前的内容中,我们也提到过,Python 标准库的base64模块提供了对 BASE64 编解码的支持。

下面的代码演示了如何发送带附件的邮件。

import smtplib
from email.header import Header
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from urllib.parse import quote
# 创建邮件主体对象
email = MIMEMultipart()
# 设置发件人、收件人和主题
email['From'] = 'xxxxxxxxx@126.com'
email['To'] = 'zzzzzzzz@1000phone.com'
email['Subject'] = Header('请查收离职证明文件', 'utf-8')
# 添加邮件正文内容(带HTML标签排版的内容)
content = """<p>亲爱的前同事:</p>
<p>你需要的离职证明在附件中,请查收!</p>
<br>
<p>祝,好!</p>
<hr>
<p>孙美丽 即日</p>"""
email.attach(MIMEText(content, 'html', 'utf-8'))
# 读取作为附件的文件
with open(f'resources/王大锤离职证明.docx', 'rb') as file:
attachment = MIMEText(file.read(), 'base64', 'utf-8')
# 指定内容类型
attachment['content-type'] = 'application/octet-stream'
# 将中文文件名处理成百分号编码
filename = quote('王大锤离职证明.docx')
# 指定如何处置内容
attachment['content-disposition'] = f'attachment; filename="{filename}"'
# 创建SMTP_SSL对象(连接邮件服务器)
smtp_obj = smtplib.SMTP_SSL('smtp.126.com', 465)
# 通过用户名和授权码进行登录
smtp_obj.login('xxxxxxxxx@126.com', '邮件服务器的授权码')
# 发送邮件(发件人、收件人、邮件内容(字符串))
smtp_obj.sendmail(
'xxxxxxxxx@126.com',
'zzzzzzzz@1000phone.com',
email.as_string()
)

为了方便大家用 Python 实现邮件发送,我将上面的代码封装成了函数,使用的时候大家只需要调整邮件服务器域名、端口、用户名和授权码就可以了。

import smtplib
from email.header import Header
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from urllib.parse import quote
# 邮件服务器域名(自行修改)
EMAIL_HOST = 'smtp.126.com'
# 邮件服务端口(通常是465)
EMAIL_PORT = 465
# 登录邮件服务器的账号(自行修改)
EMAIL_USER = 'xxxxxxxxx@126.com'
# 开通SMTP服务的授权码(自行修改)
EMAIL_AUTH = '邮件服务器的授权码'
def send_email(*, from_user, to_users, subject='', content='', filenames=[]):
"""发送邮件
:param from_user: 发件人
:param to_users: 收件人,多个收件人用英文分号进行分隔
:param subject: 邮件的主题
:param content: 邮件正文内容
:param filenames: 附件要发送的文件路径
"""
email = MIMEMultipart()
email['From'] = from_user
email['To'] = to_users
email['Subject'] = subject
message = MIMEText(content, 'plain', 'utf-8')
email.attach(message)
for filename in filenames:
with open(filename, 'rb') as file:
pos = filename.rfind('/')
display_filename = filename[pos + 1:] if pos >= 0 else filename
display_filename = quote(display_filename)
attachment = MIMEText(file.read(), 'base64', 'utf-8')
attachment['content-type'] = 'application/octet-stream'
attachment['content-disposition'] = f'attachment; filename="{display_filename}"'
email.attach(attachment)
smtp = smtplib.SMTP_SSL(EMAIL_HOST, EMAIL_PORT)
smtp.login(EMAIL_USER, EMAIL_AUTH)
smtp.sendmail(from_user, to_users.split(';'), email.as_string())

发送短信#

发送短信也是项目中常见的功能,网站的注册码、验证码、营销信息基本上都是通过短信来发送给用户的。发送短信需要三方平台的支持,下面我们以螺丝帽平台为例,为大家介绍如何用 Python 程序发送短信。注册账号和购买短信服务的细节我们不在这里进行赘述,大家可以咨询平台的客服。

接下来,我们可以通过requests库向平台提供的短信网关发起一个 HTTP 请求,通过将接收短信的手机号和短信内容作为参数,就可以发送短信,代码如下所示。

import random
import requests
def send_message_by_luosimao(tel, message):
"""发送短信(调用螺丝帽短信网关)"""
resp = requests.post(
url='http://sms-api.luosimao.com/v1/send.json',
auth=('api', 'key-注册成功后平台分配的KEY'),
data={
'mobile': tel,
'message': message
},
timeout=10,
verify=False
)
return resp.json()
def gen_mobile_code(length=6):
"""生成指定长度的手机验证码"""
return ''.join(random.choices('0123456789', k=length))
def main():
code = gen_mobile_code()
message = f'您的短信验证码是{code},打死也不能告诉别人哟!【Python小课】'
print(send_message_by_luosimao('13500112233', message))
if __name__ == '__main__':
main()

上面请求螺丝帽的短信网关http://sms-api.luosimao.com/v1/send.json会返回JSON格式的数据,如果返回{'error': 0, 'msg': 'OK'}就说明短信已经发送成功了,如果error的值不是0,可以通过查看官方的开发文档了解到底哪个环节出了问题。螺丝帽平台常见的错误类型如下图所示。

目前,大多数短信平台都会要求短信内容必须附上签名,下图是我在螺丝帽平台配置的短信签名“【Python小课】”。有些涉及到敏感内容的短信,还需要提前配置短信模板,有兴趣的读者可以自行研究。一般情况下,平台为了防范短信被盗用,还会要求设置“IP 白名单”,不清楚如何配置的可以咨询平台客服。

当然国内的短信平台很多,读者可以根据自己的需要进行选择(通常会考虑费用预算、短信达到率、使用的难易程度等指标),如果需要在商业项目中使用短信服务建议购买短信平台提供的套餐服务。

总结#

其实,发送邮件和发送短信一样,也可以通过调用三方服务来完成,在实际的商业项目中,建议自己架设邮件服务器或购买三方服务来发送邮件,这个才是比较靠谱的选择。


30 正则表达式的应用#

正则表达式相关知识#

在编写处理字符串的程序时,经常会遇到在一段文本中查找符合某些规则的字符串的需求,正则表达式就是用于描述这些规则的工具,换句话说,我们可以使用正则表达式来定义字符串的匹配模式,即如何检查一个字符串是否有跟某种模式匹配的部分或者从一个字符串中将与模式匹配的部分提取出来或者替换掉。

举一个简单的例子,如果你在 Windows 操作系统中使用过文件查找并且在指定文件名时使用过通配符(*和?),那么正则表达式也是与之类似的用 来进行文本匹配的工具,只不过比起通配符正则表达式更强大,它能更精确地描述你的需求,当然你付出的代价是书写一个正则表达式比使用通配符要复杂得多,因为任何给你带来好处的东西都需要你付出对应的代价。

再举一个例子,我们从某个地方(可能是一个文本文件,也可能是网络上的一则新闻)获得了一个字符串,希望在字符串中找出手机号和座机号。当然我们可以设定手机号是 11 位的数字(注意并不是随机的 11 位数字,因为你没有见过“25012345678”这样的手机号),而座机号则是类似于“区号-号码”这样的模式,如果不使用正则表达式要完成这个任务就会比较麻烦。最初计算机是为了做数学运算而诞生的,处理的信息基本上都是数值,而今天我们在日常工作中处理的信息很多都是文本数据,我们希望计算机能够识别和处理符合某些模式的文本,正则表达式就显得非常重要了。今天几乎所有的编程语言都提供了对正则表达式操作的支持,Python 通过标准库中的re模块来支持正则表达式操作。

关于正则表达式的相关知识,大家可以阅读一篇非常有名的博文叫《正则表达式30分钟入门教程》,读完这篇文章后你就可以看懂下面的表格,这是我们对正则表达式中的一些基本符号进行的扼要总结。

符号解释示例说明
.匹配任意字符b.t可以匹配bat / but / b#t / b1t等
\w匹配字母/数字/下划线b\wt可以匹配bat / b1t / b_t等
但不能匹配b#t
\s匹配空白字符(包括\r、\n、\t等)love\syou可以匹配love you
\d匹配数字\d\d可以匹配01 / 23 / 99等
\b匹配单词的边界\bThe\b
^匹配字符串的开始^The可以匹配The开头的字符串
$匹配字符串的结束.exe$可以匹配.exe结尾的字符串
\W匹配非字母/数字/下划线b\Wt可以匹配b#t / b@t等
但不能匹配but / b1t / b_t等
\S匹配非空白字符love\Syou可以匹配love#you等
但不能匹配love you
\D匹配非数字\d\D可以匹配9a / 3# / 0F等
\B匹配非单词边界\Bio\B
[]匹配来自字符集的任意单一字符[aeiou]可以匹配任一元音字母字符
[^]匹配不在字符集中的任意单一字符[^aeiou]可以匹配任一非元音字母字符
*匹配0次或多次\w*
+匹配1次或多次\w+
?匹配0次或1次\w?
{N}匹配N次\w{3}
{M,}匹配至少M次\w{3,}
{M,N}匹配至少M次至多N次\w{3,6}
|分支foo|bar可以匹配foo或者bar
(?#)注释
(exp)匹配exp并捕获到自动命名的组中
(?<name>exp)匹配exp并捕获到名为name的组中
(?:exp)匹配exp但是不捕获匹配的文本
(?=exp)匹配exp前面的位置\b\w+(?=ing)可以匹配I’m dancing中的danc
(?<=exp)匹配exp后面的位置(?<=\bdanc)\w+\b可以匹配I love dancing and reading中的第一个ing
(?!exp)匹配后面不是exp的位置
(?<!exp)匹配前面不是exp的位置
*?重复任意次,但尽可能少重复a.*b
a.*?b
将正则表达式应用于aabab,前者会匹配整个字符串aabab,后者会匹配aab和ab两个字符串
+?重复1次或多次,但尽可能少重复
??重复0次或1次,但尽可能少重复
{M,N}?重复M到N次,但尽可能少重复
{M,}?重复M次以上,但尽可能少重复

说明: 如果需要匹配的字符是正则表达式中的特殊字符,那么可以使用\进行转义处理,例如想匹配小数点可以写成\.就可以了,因为直接写.会匹配任意字符;同理,想匹配圆括号必须写成\(和\),否则圆括号被视为正则表达式中的分组。

Python对正则表达式的支持#

Python 提供了re模块来支持正则表达式相关操作,下面是re模块中的核心函数。

函数说明
compile(pattern, flags=0)编译正则表达式返回正则表达式对象
match(pattern, string, flags=0)用正则表达式匹配字符串 成功返回匹配对象 否则返回None
search(pattern, string, flags=0)搜索字符串中第一次出现正则表达式的模式 成功返回匹配对象 否则返回None
split(pattern, string, maxsplit=0, flags=0)用正则表达式指定的模式分隔符拆分字符串 返回列表
sub(pattern, repl, string, count=0, flags=0)用指定的字符串替换原字符串中与正则表达式匹配的模式 可以用count指定替换的次数
fullmatch(pattern, string, flags=0)match函数的完全匹配(从字符串开头到结尾)版本
findall(pattern, string, flags=0)查找字符串所有与正则表达式匹配的模式 返回字符串的列表
finditer(pattern, string, flags=0)查找字符串所有与正则表达式匹配的模式 返回一个迭代器
purge()清除隐式编译的正则表达式的缓存
re.I / re.IGNORECASE忽略大小写匹配标记
re.M / re.MULTILINE多行匹配标记

说明: 上面提到的re模块中的这些函数,实际开发中也可以用正则表达式对象(Pattern对象)的方法替代对这些函数的使用,如果一个正则表达式需要重复的使用,那么先通过compile函数编译正则表达式并创建出正则表达式对象无疑是更为明智的选择。

下面我们通过一系列的例子来告诉大家在Python中如何使用正则表达式。

例子1:验证输入用户名和QQ号是否有效并给出对应的提示信息。#
"""
要求:用户名必须由字母、数字或下划线构成且长度在6~20个字符之间,QQ号是5~12的数字且首位不能为0
"""
import re
username = input('请输入用户名: ')
qq = input('请输入QQ号: ')
# match函数的第一个参数是正则表达式字符串或正则表达式对象
# match函数的第二个参数是要跟正则表达式做匹配的字符串对象
m1 = re.match(r'^[0-9a-zA-Z_]{6,20}$', username)
if not m1:
print('请输入有效的用户名.')
# fullmatch函数要求字符串和正则表达式完全匹配
# 所以正则表达式没有写起始符和结束符
m2 = re.fullmatch(r'[1-9]\d{4,11}', qq)
if not m2:
print('请输入有效的QQ号.')
if m1 and m2:
print('你输入的信息是有效的!')

提示: 上面在书写正则表达式时使用了“原始字符串”的写法(在字符串前面加上了r),所谓“原始字符串”就是字符串中的每个字符都是它原始的意义,说得更直接一点就是字符串中没有所谓的转义字符啦。因为正则表达式中有很多元字符和需要进行转义的地方,如果不使用原始字符串就需要将反斜杠写作\\,例如表示数字的\d得书写成\\d,这样不仅写起来不方便,阅读的时候也会很吃力。

例子2:从一段文字中提取出国内手机号码。#

下面这张图是截止到 2017 年底,国内三家运营商推出的手机号段。

import re
# 创建正则表达式对象,使用了前瞻和回顾来保证手机号前后不应该再出现数字
pattern = re.compile(r'(?<=\D)1[34578]\d{9}(?=\D)')
sentence = '''重要的事情说8130123456789遍,我的手机号是13512346789这个靓号,
不是15600998765,也不是110或119,王大锤的手机号才是15600998765。'''
# 方法一:查找所有匹配并保存到一个列表中
tels_list = re.findall(pattern, sentence)
for tel in tels_list:
print(tel)
print('--------华丽的分隔线--------')
# 方法二:通过迭代器取出匹配对象并获得匹配的内容
for temp in pattern.finditer(sentence):
print(temp.group())
print('--------华丽的分隔线--------')
# 方法三:通过search函数指定搜索位置找出所有匹配
m = pattern.search(sentence)
while m:
print(m.group())
m = pattern.search(sentence, m.end())

说明: 上面匹配国内手机号的正则表达式并不够好,因为像 14 开头的号码只有 145 或 147,而上面的正则表达式并没有考虑这种情况,要匹配国内手机号,更好的正则表达式的写法是:(?<=\D)(1[38]\d{9}|14[57]\d{8}|15[0-35-9]\d{8}|17[678]\d{8})(?=\D),国内好像已经有 19 和 16 开头的手机号了,但是这个暂时不在我们考虑之列。

例子3:替换字符串中的不良内容#
import re
sentence = 'Oh, shit! 你是傻逼吗? Fuck you.'
purified = re.sub('fuck|shit|[傻煞沙][比笔逼叉缺吊碉雕]',
'*', sentence, flags=re.IGNORECASE)
print(purified) # Oh, *! 你是*吗? * you.

说明: re模块的正则表达式相关函数中都有一个flags参数,它代表了正则表达式的匹配标记,可以通过该标记来指定匹配时是否忽略大小写、是否进行多行匹配、是否显示调试信息等。如果需要为flags参数指定多个值,可以使用按位或运算符进行叠加,如flags=re.I | re.M。

例子4:拆分长字符串#
import re
poem = '窗前明月光,疑是地上霜。举头望明月,低头思故乡。'
sentences_list = re.split(r'[,。]', poem)
sentences_list = [sentence for sentence in sentences_list if sentence]
for sentence in sentences_list:
print(sentence)

总结#

正则表达式在字符串的处理和匹配上真的非常强大,通过上面的例子相信大家已经感受到了正则表达式的魅力,当然写一个正则表达式对新手来说并不是那么容易,但是很多事情都是熟能生巧,大胆的去尝试就行了,有一个在线的正则表达式测试工具相信能够在一定程度上帮到大家。

Python100天

已完结

第 2 篇,共 11 篇本文属于系列

Day21-30 教程全文(10 篇合并)
https://www.klucen.cn/posts/python/day21-30-full/
作者
骆昊(jackfrued)
发布于
2025-12-14

分享文章

生成精美分享图或复制链接,与更多人分享本文。

继续阅读

沿着主题读

基于共同的标签与分类

换条路线

从其他文章中稳定抽取

评论

正在加载评论...

最后更新于 ,距今已过 301 天

部分内容可能已过时