更新了第22课到第25课的文档

pull/15/head
jackfrued 2020-07-12 17:31:34 +08:00
parent 204cae3d75
commit b0df909c28
7 changed files with 848 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -0,0 +1,208 @@
## 第022课Python标准库初探
Python语言最可爱的地方在于它的标准库和三方库实在是太丰富了日常开发工作中的很多任务都可以通过这些标准库或者三方库直接解决。下面我们先介绍Python标准库中的一些常用模块后面的课程中再陆陆续续为大家介绍Python常用三方库的用途和用法。
### base64 - Base64编解码模块
**Base64**是一种基于64个可打印字符来表示二进制数据的方法。由于$log _{2}64=6$所以Base64以6个比特二进制位可以表示0或1为一个单元每个单元对应一个可打印字符。对于3字节24比特的二进制数据我们可以将其处理成对应于4个Base64单元即3个字节可由4个可打印字符来表示。Base64编码可用来作为电子邮件的传输编码也可以用于其他需要将二进制数据转成文本字符的场景这使得在XML、JSON、YAML这些文本数据格式中传输二进制内容成为可能。在Base64中的可打印字符包括`A-Z`、`a-z`、`0-9`这里一共是62个字符另外两个可打印符号通常是`+`和`/``=`用于在Base64编码最后进行补位。
关于Base64编码的细节大家可以参考[《Base64笔记》](http://www.ruanyifeng.com/blog/2008/06/base64.html)一文Python标准库中的`base64`模块提供了`b64encode`和`b64decode`两个函数专门用于实现Base64的编码和解码下面演示了在**Python的交互式环境**中执行这两个函数的效果。
```Python
>>> import base64
>>>
>>> content = 'Man is distinguished, not only by his reason, but by this singular passion from other animals, which is a lust of the mind, that by a perseverance of delight in the continued and indefatigable generation of knowledge, exceeds the short vehemence of any carnal pleasure.'
>>> base64.b64encode(content.encode())
b'TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlzIHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2YgdGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGludWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRoZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4='
>>> content = b'TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlzIHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2YgdGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGludWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRoZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4='
>>> base64.b64decode(content).decode()
'Man is distinguished, not only by his reason, but by this singular passion from other animals, which is a lust of the mind, that by a perseverance of delight in the continued and indefatigable generation of knowledge, exceeds the short vehemence of any carnal pleasure.'
```
### collections - 容器数据类型模块
`collections`模块提供了诸多非常好用的数据结构,主要包括:
- `namedtuple`:命令元组,它是一个类工厂,接受类型的名称和属性列表来创建一个类。
- `deque`双端队列是列表的替代实现。Python中的列表底层是基于数组来实现的而`deque`底层是双向链表,因此当你需要在头尾添加和删除元素是,`deque`会表现出更好的性能,渐近时间复杂度为$O(1)$。
- `Counter``dict`的子类,键是元素,值是元素的计数,它的`most_common()`方法可以帮助我们获取出现频率最高的元素。`Counter`和`dict`的继承关系我认为是值得商榷的按照CARP原则`Counter`跟`dict`的关系应该设计为关联关系更为合理。
- `OrderedDict``dict`的子类,它记录了键值对插入的顺序,看起来既有字典的行为,也有链表的行为。
- `defaultdict`:类似于字典类型,但是可以通过默认的工厂函数来获得键对应的默认值,相比字典中的`setdefault()`方法,这种做法更加高效。
下面是在**Python交互式环境中**使用`namedtuple`创建扑克牌类的例子。
```Python
>>> from collections import namedtuple
>>>
>>> Card = namedtuple('Card', ('suite', 'face'))
>>> card1 = Card('红桃', 5)
>>> card2 = Card('草花', 9)
>>> card1
Card(suite='红桃', face=5)
>>> card2
Card(suite='草花', face=9)
>>> print(f'{card1.suite}{card1.face}')
红桃5
>>> print(f'{card2.suite}{card2.face}')
草花9
```
下面是使用`Counter`类统计列表中出现次数最多的三个元素的例子。
```Python
from collections import Counter
words = [
'look', 'into', 'my', 'eyes', 'look', 'into', 'my', 'eyes',
'the', 'eyes', 'the', 'eyes', 'the', 'eyes', 'not', 'around',
'the', 'eyes', "don't", 'look', 'around', 'the', 'eyes',
'look', 'into', 'my', 'eyes', "you're", 'under'
]
counter = Counter(words)
# 打印words列表中出现频率最高的3个元素及其出现次数
for elem, count in counter.most_common(3):
print(elem, count)
```
### hashlib - 哈希函数模块
哈希函数又称哈希算法或散列函数,是一种为已有的数据创建“数字指纹”(哈希摘要)的方法。哈希函数把数据压缩成摘要,对于相同的输入,哈希函数可以生成相同的摘要(数字指纹),需要注意的是这个过程并不可逆(不能通过摘要计算出输入的内容)。一个优质的哈希函数能够为不同的输入生成不同的摘要,出现哈希冲突(不同的输入产生相同的摘要)的概率极低,[MD5](https://zh.wikipedia.org/wiki/MD5)、[SHA家族]([https://zh.wikipedia.org/wiki/SHA%E5%AE%B6%E6%97%8F](https://zh.wikipedia.org/wiki/SHA家族))就是这类好的哈希函数。
> **说明**在2011年的时候RFC 6151中已经禁止将MD5用作密钥散列消息认证码这个问题不在我们讨论的范围内。
Python标准库的`hashlib`模块提供了对哈希函数的封装,通过使用`md5`、`sha1`、`sha256`等类,我们可以轻松的生成“数字指纹”。举一个简单的例子,用户注册时我们希望在数据库中保存用户的密码,很显然我们不能将用户密码直接保存在数据库中,这样可能会导致用户隐私的泄露,所以在数据库中保存用户密码时,通常都会将密码的“指纹”保存起来,用户登录时通过哈希函数计算密码的“指纹”再进行匹配来判断用户登录是否成功。
```Python
import hashlib
# 计算字符串"123456"的MD5摘要
print(hashlib.md5('123456'.encode()).hexdigest())
# 计算文件"Python-3.7.1.tar.xz"的MD5摘要
hasher = hashlib.md5()
with open('Python-3.7.1.tar.xz', 'rb') as file:
data = file.read(512)
while data:
hasher.update(data)
data = file.read(512)
print(hasher.hexdigest())
```
> **说明**:很多网站在下载链接的旁边都提供了哈希摘要,完成文件下载后,我们可以计算该文件的哈希摘要并检查它与网站上提供的哈希摘要是否一致(指纹比对)。如果计算出的哈希摘要与网站提供的并不一致,很有可能是下载出错或该文件在传输过程中已经被篡改,这时候就不应该直接使用这个文件。
### heapq - 堆排序模块
`heapq`模块实现了堆排序算法,如果希望使用堆排序,尤其是要解决**TopK问题**从序列中找到K个最大或最小元素直接使用该模块即可代码如下所示。
```Python
import heapq
list1 = [34, 25, 12, 99, 87, 63, 58, 78, 88, 92]
# 找出列表中最大的三个元素
print(heapq.nlargest(3, list1))
# 找出列表中最小的三个元素
print(heapq.nsmallest(3, list1))
list2 = [
{'name': 'IBM', 'shares': 100, 'price': 91.1},
{'name': 'AAPL', 'shares': 50, 'price': 543.22},
{'name': 'FB', 'shares': 200, 'price': 21.09},
{'name': 'HPQ', 'shares': 35, 'price': 31.75},
{'name': 'YHOO', 'shares': 45, 'price': 16.35},
{'name': 'ACME', 'shares': 75, 'price': 115.65}
]
# 找出价格最高的三只股票
print(heapq.nlargest(3, list2, key=lambda x: x['price']))
# 找出持有数量最高的三只股票
print(heapq.nlargest(3, list2, key=lambda x: x['shares']))
```
### itertools - 迭代工具模块
`itertools`可以帮助我们生成各种各样的迭代器,大家可以看看下面的例子。
```Python
import itertools
# 产生ABCD的全排列
for value in itertools.permutations('ABCD'):
print(value)
# 产生ABCDE的五选三组合
for value in itertools.combinations('ABCDE', 3):
print(value)
# 产生ABCD和123的笛卡尔积
for value in itertools.product('ABCD', '123'):
print(value)
# 产生ABC的无限循环序列
it = itertools.cycle(('A', 'B', 'C'))
print(next(it))
print(next(it))
print(next(it))
print(next(it))
```
### random - 随机数和随机抽样模块
这个模块我们之前已经用过很多次了,生成随机数、实现随机乱序和随机抽样,下面是常用函数的列表。
- `getrandbits(k)`:返回具有`k`个随机比特位的整数。
- `randrange(start, stop[, step])`:从`range(start, stop, step)` 返回一个随机选择的元素,但实际上并没有构建一个`range`对象。
- `randint(a, b)`:返回随机整数`N`满足`a <= N <= b`,相当于`randrange(a, b+1)`。
- `choice(seq)`:从非空序列`seq`返回一个随机元素。 如果`seq`为空,则引发`IndexError`。
- `choices(population, weight=None, *, cum_weights=None, k=1)`:从`population`中选择替换,返回大小为`k`的元素列表。 如果`population`为空,则引发`IndexError`。
- `shuffle(x[, random])`:将序列`x`随机打乱位置。
- `sample(population, k)`:返回从总体序列或集合中选择`k`个不重复元素构造的列表,用于无重复的随机抽样。
- `random()`:返回`[0.0, 1.0)`范围内的下一个随机浮点数。
- `expovariate(lambd)`:指数分布。
- `gammavariate(alpha, beta)`:伽玛分布。
- `gauss(mu, sigma)` / `normalvariate(mu, sigma)`:正态分布。
- `paretovariate(alpha)`:帕累托分布。
- `weibullvariate(alpha, beta)`:威布尔分布。
### os.path - 路径操作相关模块
`os.path`模块封装了操作路径的工具函数,如果程序中需要对文件路径做拼接、拆分、获取以及获取文件的存在性和其他属性,这个模块将会非常有帮助,下面为大家罗列一些常用的函数。
- `dirname(path)`:返回路径`path`的目录名称。
- `exists(path)`:如果`path`指向一个已存在的路径或已打开的文件描述符,返回 `True`
- `getatime(path)` / `getmtime(path)` / `getctime(path)`:返回`path`的最后访问时间/最后修改时间/创建时间。
- `getsize(path)`:返回`path`的大小,以字节为单位。如果该文件不存在或不可访问,则抛出`OSError`异常。
- `isfile(path)`:如果`path`是普通文件,则返回 `True`
- `isdir(path)`:如果`path`是目录(文件夹),则返回`True`。
- `join(path, *paths)`:合理地拼接一个或多个路径部分。返回值是`path`和`paths`所有值的连接,每个非空部分后面都紧跟一个目录分隔符 (`os.sep`),除了最后一部分。这意味着如果最后一部分为空,则结果将以分隔符结尾。如果参数中某个部分是绝对路径,则绝对路径前的路径都将被丢弃,并从绝对路径部分开始连接。
- `splitext(path)`:将路径`path`拆分为一对,即`(root, ext)`,使得`root + ext == path`,其中`ext`为空或以英文句点开头,且最多包含一个句点。
### uuid - UUID生成模块
`uuid`模块可以帮助我们生成全局唯一标识符Universal Unique IDentity。该模块提供了四个用于生成UUID的函数分别是
- `uuid1()`由MAC地址、当前时间戳、随机数生成可以保证全球范围内的唯一性。
- `uuid3(namespace, name)`通过计算命名空间和名字的MD5哈希摘要“指纹”值得到保证了同一命名空间中不同名字的唯一性和不同命名空间的唯一性但同一命名空间的同一名字会生成相同的UUID。
- `uuid4()`由伪随机数生成UUID有一定的重复概率该概率可以计算出来。
- `uuid5()`:算法与`uuid3`相同只不过哈希函数用SHA-1取代了MD5。
由于`uuid4`存在概率型重复,那么在真正需要全局唯一标识符的地方最好不用使用它。在分布式环境下,`uuid1`是很好的选择因为它能够保证生成ID的全局唯一性。下面是在**Python交互式环境中**使用`uuid1`函数生成全局唯一标识符的例子。
```Python
>>> import uuid
>>> uuid.uuid1().hex
'622a8334baab11eaaa9c60f81da8d840'
>>> uuid.uuid1().hex
'62b066debaab11eaaa9c60f81da8d840'
>>> uuid.uuid1().hex
'642c0db0baab11eaaa9c60f81da8d840'
```
### 简单的总结
Python标准库中有大量的模块日常开发中有很多常见的任务在Python标准库中都有封装好的函数或类可供使用这也是Python这门语言最可爱的地方。
>**温馨提示**:学习中如果遇到困难,可以加**QQ交流群**询问。
>
>付费群:**789050736**,群一直保留,供大家学习交流讨论问题。
>
>免费群:**151669801**,仅供入门新手提问,定期清理群成员。

View File

@ -0,0 +1,269 @@
## 第023课文件读写和异常处理
实际开发中常常会遇到对数据进行持久化的场景,所谓持久化是指将数据从无法长久保存数据的存储介质(通常是内存)转移到可以长久保存数据的存储介质(通常是硬盘)中。实现数据持久化最直接简单的方式就是通过**文件系统**将数据保存到**文件**中。
计算机的**文件系统**是一种存储和组织计算机数据的方法,它使得对数据的访问和查找变得容易,文件系统使用**文件**和**树形目录**的抽象逻辑概念代替了硬盘、光盘、闪存等物理设备的数据块概念,用户使用文件系统来保存数据时,不必关心数据实际保存在硬盘的哪个数据块上,只需要记住这个文件的路径和文件名。在写入新数据之前,用户不必关心硬盘上的哪个数据块没有被使用,硬盘上的存储空间管理(分配和释放)功能由文件系统自动完成,用户只需要记住数据被写入到了哪个文件中。
### 打开和关闭文件
有了文件系统我们可以非常方便的通过文件来读写数据在Python中要实现文件操作是非常简单的。我们可以使用Python内置的`open`函数来打开文件,在使用`open`函数时,我们可以通过函数的参数指定**文件名**、**操作模式**和**字符编码**等信息,接下来就可以对文件进行读写操作了。这里所说的操作模式是指要打开什么样的文件(字符文件或二进制文件)以及做什么样的操作(读、写或追加),具体如下表所示。
| 操作模式 | 具体含义 |
| -------- | -------------------------------- |
| `'r'` | 读取 (默认) |
| `'w'` | 写入(会先截断之前的内容) |
| `'x'` | 写入,如果文件已经存在会产生异常 |
| `'a'` | 追加,将内容写入到已有文件的末尾 |
| `'b'` | 二进制模式 |
| `'t'` | 文本模式(默认) |
| `'+'` | 更新(既可以读又可以写) |
下图展示了如何根据程序的需要来设置`open`函数的操作模式。
![](./res/file-open-mode.png)
在使用`open`函数时,如果打开的文件是字符文件(文本文件),可以通过`encoding`参数来指定读写文件使用的字符编码。如果对字符编码和字符集这些概念不了解,可以看看[《字符集和字符编码》](https://www.cnblogs.com/skynet/archive/2011/05/03/2035105.html)一文,此处不再进行赘述。如果没有指定该参数,则使用系统默认的编码作为读写文件的编码。当前系统默认的编码可以通过下面的代码获得。
```Python
import sys
print(sys.getdefaultencoding())
```
使用`open`函数打开文件成功后会返回一个文件对象,通过这个对象,我们就可以实现对文件的读写操作;如果打开文件失败,`open`函数会引发异常,稍后会对此加以说明。如果要关闭打开的文件,可以使用文件对象的`close`方法,这样可以在结束文件操作时释放掉这个文件。
### 读写文本文件
用`open`函数打开文本文件时,需要指定文件名并将文件的操作模式设置为`'r'`,如果不指定,默认值也是`'r'`;如果需要指定字符编码,可以传入`encoding`参数如果不指定默认值是None那么在读取文件时使用的是操作系统默认的编码。需要提醒大家如果不能保证保存文件时使用的编码方式与`encoding`参数指定的编码方式是一致的,那么就可能因无法解码字符而导致读取文件失败。
下面的例子演示了如何读取一个纯文本文件(一般指只有字符原生编码构成的文件,与富文本相比,纯文本不包含字符样式的控制元素,能够被最简单的文本编辑器直接读取)。
```Python
file = open('致橡树.txt', 'r', encoding='utf-8')
print(file.read())
file.close()
```
> **说明**[《致橡树》](http://www.china.org.cn/learning_english/2011-02/21/content_21967654.htm)是舒婷老师在1977年3月创建的爱情诗也是我最喜欢的现代诗之一。
除了使用文件对象的`read`方法读取文件之外,还可以使用`for-in`循环逐行读取或者用`readlines`方法将文件按行读取到一个列表容器中,代码如下所示。
```Python
import time
file = open('致橡树.txt', 'r', encoding='utf-8')
for line in file:
print(line, end='')
time.sleep(0.5)
file.close()
file = open('致橡树.txt', 'r', encoding='utf-8')
lines = file.readlines()
for line in lines:
print(line, end='')
time.sleep(0.5)
file.close()
```
如果要向文件中写入内容,可以在打开文件时使用`w`或者`a`作为操作模式,前者会截断之前的文本内容写入新的内容,后者是在原来内容的尾部追加新的内容。
```Python
file = open('致橡树.txt', 'a', encoding='utf-8')
file.write('\n标题《致橡树》')
file.write('\n作者舒婷')
file.write('\n时间1977年3月')
file.close()
```
也可以使用下面的代码来完成相同的操作。
```Python
lines = ['标题:《致橡树》', '作者:舒婷', '时间1977年3月']
file = open('致橡树.txt', 'a', encoding='utf-8')
for line in lines:
file.write(f'\n{line}')
file.close()
```
### 异常处理机制
请注意上面的代码,如果`open`函数指定的文件并不存在或者无法打开,那么将引发异常状况导致程序崩溃。为了让代码具有健壮性和容错性,我们可以**使用Python的异常机制对可能在运行时发生状况的代码进行适当的处理**。Python中和异常相关的关键字有五个分别是`try`、`except`、`else`、`finally`和`raise`,我们先看看下面的代码,再来为大家介绍这些关键字的用法。
```Python
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...`结构来捕获并处理异常。例如在函数中,当函数的执行条件不满足时,可以使用抛出异常的方式来告知调用者问题的所在,而调用者可以通过捕获处理异常来使得代码从异常中恢复,定义异常和抛出异常的代码如下所示。
```Python
class InputError(ValueError):
"""自定义异常类型"""
pass
def fac(num):
"""求阶乘"""
if type(num) != int or num < 0:
raise InputError('只能计算非负整数的阶乘!!!')
if num in (0, 1):
return 1
return num * fac(num - 1)
```
调用求阶乘的函数`fac`,通过`try...except...`结构捕获输入错误的异常并打印异常对象(显示异常信息),如果输入正确就计算阶乘并结束程序。
```Python
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`上下文语法的支持,后面再为大家进行讲解。
```Python
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`文件中的操作。
```Python
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`参数来指定每次读取的字节数,通过循环读取和写入的方式来完成上面的操作,代码如下所示。
```Python
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`要再次抛出,但是不建议捕获并抛出同一个异常;不建议在不清楚逻辑的情况下捕获所有异常,这可能会掩盖程序中严重的问题。最后强调一点,不要使用异常机制来处理正常业务逻辑或控制程序流程,简单的说就是不要滥用异常机制。
> **温馨提示**:学习中如果遇到困难,可以加**QQ交流群**询问。
>
> 付费群:**789050736**,群一直保留,供大家学习交流讨论问题。
>
> 免费群:**151669801**,仅供入门新手提问,定期清理群成员。

View File

@ -0,0 +1,206 @@
## 第024课对象的序列化和反序列化
###读写JSON格式的数据
通过上面的讲解我们已经知道如何将文本数据和二进制数据保存到文件中那么这里还有一个问题如果希望把一个列表或者一个字典中的数据保存到文件中又该怎么做呢在Python中我们可以将程序中的数据以JSON格式进行保存。JSON是“JavaScript Object Notation”的缩写它本来是JavaScript语言中创建对象的一种字面量语法现在已经被广泛的应用于跨语言跨平台的数据交换。使用JSON的原因非常简单因为它结构紧凑而且是纯文本任何操作系统和编程语言都能处理纯文本这就是实现跨语言跨平台数据交换的前提条件。目前JSON基本上已经取代了XML可扩展标记语言作为异构系统间交换数据的事实标准。可以在[JSON的官方网站](https://www.json.org/json-zh.html)找到更多关于JSON的知识这个网站还提供了每种语言处理JSON数据格式可以使用的工具或三方库。
下面是JSON格式的一个简单例子大家可能已经注意到了它跟Python中的字典非常类似而且支持嵌套结构就像Python字典中的值还可以是字典如果我们把下面的代码输入到浏览器控制台中它会创建出一个JavaScript中的对象。
```JSON
{
"name": "骆昊",
"age": 40,
"friends": ["王大锤", "白元芳"],
"cars": [
{"brand": "BMW", "max_speed": 240},
{"brand": "Benz", "max_speed": 280},
{"brand": "Audi", "max_speed": 280}
]
}
```
![json-in-console](res/json-in-console.png)
JSON格式的数据类型和Python中的数据类型也是很容易找到对应关系的正如下面的两张表所示。
| JSON | Python |
| ------------------- | ------------ |
| object | dict |
| array | list |
| string | str |
| number (int / real) | int / float |
| true / false | True / False |
| null | None |
| Python | JSON |
| -------------------------------------- | ------------ |
| dict | object |
| list, tuple | array |
| str | string |
| int, float, int- & float-derived Enums | number |
| True / False | true / false |
| None | null |
在Python中我们可以使用`json`模块将字典或列表以JSON格式写入到文件中代码如下所示。
```Python
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)
print('字典已经保存到data.json文件中')
```
执行上面的代码,会创建`data.json`文件文件的内容如下所示中文是用Unicode编码书写的。
```JSON
{"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`模块有四个比较重要的函数,分别是:
- `dump` - 将Python对象按照JSON格式序列化到文件中
- `dumps` - 将Python对象处理成JSON格式的字符串
- `load` - 将文件中的JSON数据反序列化成对象
- `loads` - 将字符串的内容反序列化成Python对象
这里出现了两个概念,一个叫序列化,一个叫反序列化,[维基百科](https://zh.wikipedia.org/)上的解释是“序列化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的三方库或工具macOS和Linux系统应该使用pip3。例如要安装替代`json`模块的`ujson`,可以使用下面的命令。
```Bash
pip install ujson
```
在默认情况下pip会访问`https://pypi.org/simple/`来获得三方库相关的数据,但是国内访问这个网站的速度并不是十分理想,因此国内用户可以使用豆瓣网提供的镜像来替代这个默认的下载源,操作如下所示。
```Bash
pip install ujson -i https://pypi.doubanio.com/simple
```
可以通过`pip search`命令根据名字查找需要的三方库,可以通过`pip list`命令来查看已经安装过的三方库。如果想更新某个三方库,可以使用`pip install -U`或`pip install --upgrade`;如果要删除某个三方库,可以使用`pip uninstall`命令。
搜索`ujson`三方库。
```Bash
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
```
查看已经安装的三方库。
```Bash
pip list
Package Version
----------------------------- ----------
aiohttp 3.5.4
alipay 0.7.4
altgraph 0.16.1
amqp 2.4.2
... ...
```
更新`ujson`三方库。
```Bash
pip install -U ujson -i https://pypi.doubanio.com/simple
```
如果要更新pip本身可以使用下面的命令。
macOS系统
```Bash
pip3 install -U pip
```
Windows系统
```Bash
python -m pip install -U pip
```
删除`ujson`三方库。
```Bash
pip uninstall -y ujson
```
### 使用网络API获取数据
如果想在我们自己的程序中显示天气、路况、航班等信息这些信息我们自己没有能力提供所以必须使用网络数据服务。目前绝大多数的网络数据服务或称之为网络API都是基于[HTTP](https://zh.wikipedia.org/wiki/%E8%B6%85%E6%96%87%E6%9C%AC%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AE)提供JSON格式的数据在Python程序中我们可以发送HTTP请求给指定的URL统一资源定位符这个URL就是所谓的网络API如果请求成功它会返回HTTP响应而HTTP响应的消息体中就有我们需要的JSON格式的数据。关于HTTP的相关知识可以看看阮一峰的[《HTTP协议入门》](http://www.ruanyifeng.com/blog/2016/08/http.html)一文。
国内有很多提供网络API接口的网站例如[聚合数据](https://www.juhe.cn/)、[阿凡达数据](http://www.avatardata.cn/)等,这些网站上有免费的和付费的数据接口,国外的[{API}Search](http://apis.io/)网站也提供了类似的功能,有兴趣的可以自行研究。下面的例子演示了如何使用[`requests`](http://docs.python-requests.org/zh_CN/latest/)库基于HTTP进行网络资源访问的三方库访问网络API获取国内新闻并显示新闻标题和链接这个例子使用了[天行数据](https://www.tianapi.com/)提供的国内新闻数据接口其中的APIKey需要自己到网站上注册申请。
安装`requests`库。
```Bash
pip install requests -i https://pypi.doubanio.com/simple/
```
获取国内新闻并显示新闻标题和链接。
```Python
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)
```
> **注意**上面代码中的APIKey需要换成自己在天行数据网站申请的APIKey同时还要申请开通国内新闻的接口才能获取到JSON格式的数据。这个网站上还有很多非常有意思的网络API接口例如垃圾分类、美女图片、周公解梦等等大家可以仿照上面的代码来调用这些接口。
### 简单的总结
Python中实现序列化和反序列化除了使用`json`模块之外,还可以使用`pickle`和`shelve`模块但是这两个模块是使用特有的序列化协议来序列化数据因此序列化后的数据只能被Python识别关于这两个模块的相关知识可以自己看看网络上的资料。处理JSON格式的数据很显然是程序员必须掌握的一项技能因为不管是访问网络API接口还是提供网络API接口给他人使用都需要具备处理JSON格式数据的相关知识。
> **温馨提示**:学习中如果遇到困难,可以加**QQ交流群**询问。
>
> 付费群:**789050736**,群一直保留,供大家学习交流讨论问题。
>
> 免费群:**151669801**,仅供入门新手提问,定期清理群成员。

View File

@ -0,0 +1,165 @@
## 第025课正则表达式的应用
### 正则表达式相关知识
在编写处理字符串的程时,经常会遇到在一段文本中查找符合某些规则的字符串的需求,正则表达式就是用于描述这些规则的工具,换句话说,我们可以使用正则表达式来定义字符串的匹配模式,即如何检查一个字符串是否有跟某种模式匹配的部分或者从一个字符串中将与模式匹配的部分提取出来或者替换掉。
举一个简单的例子如果你在Windows操作系统中使用过文件查找并且在指定文件名时使用过通配符`*`和`?`),那么正则表达式也是与之类似的用 来进行文本匹配的工具,只不过比起通配符正则表达式更强大,它能更精确地描述你的需求,当然你付出的代价是书写一个正则表达式比使用通配符要复杂得多,因为任何给你带来好处的东西都需要你付出对应的代价。
再举一个例子我们从某个地方可能是一个文本文件也可能是网络上的一则新闻获得了一个字符串希望在字符串中找出手机号和座机号。当然我们可以设定手机号是11位的数字注意并不是随机的11位数字因为你没有见过“25012345678”这样的手机号而座机号则是类似于“区号-号码”这样的模式如果不使用正则表达式要完成这个任务就会比较麻烦。最初计算机是为了做数学运算而诞生的处理的信息基本上都是数值而今天我们在日常工作中处理的信息基本上都是文本数据我们希望计算机能够识别和处理符合某些模式的文本正则表达式就显得非常重要了。今天几乎所有的编程语言都提供了对正则表达式操作的支持Python通过标准库中的`re`模块来支持正则表达式操作。
关于正则表达式的相关知识,大家可以阅读一篇非常有名的博文叫[《正则表达式30分钟入门教程》](https://deerchao.net/tutorials/regex/regex.htm),读完这篇文章后你就可以看懂下面的表格,这是我们对正则表达式中的一些基本符号进行的扼要总结。
| 符号 | 解释 | 示例 | 说明 |
| ------------------------ | -------------------------------- | ------------------- | ------------------------------------------------------------ |
| . | 匹配任意字符 | b.t | 可以匹配bat / but / b#t / b1t等 |
| \\w | 匹配字母/数字/下划线 | b\\wt | 可以匹配bat / b1t / b_t等<br>但不能匹配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等<br>但不能匹配but / b1t / b_t等 |
| \\S | 匹配非空白字符 | love\\Syou | 可以匹配love#you等<br>但不能匹配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并捕获到自动命名的组中 | | |
| (?&nbsp;&lt;name&gt;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<br>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`模块中的这些函数,实际开发中也可以用正则表达式对象的方法替代对这些函数的使用,如果一个正则表达式需要重复的使用,那么先通过`compile`函数编译正则表达式并创建出正则表达式对象无疑是更为明智的选择。
下面我们通过一系列的例子来告诉大家在Python中如何使用正则表达式。
#### 例子1验证输入用户名和QQ号是否有效并给出对应的提示信息。
```Python
"""
要求用户名必须由字母、数字或下划线构成且长度在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年底国内三家运营商推出的手机号段。
![](./res/tel-start-number.png)
```Python
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替换字符串中的不良内容
```Python
import re
sentence = '你丫是傻逼吗? 我操你大爷的. Fuck you.'
purified = re.sub('[操草艹]|fuck|shit|[傻煞沙][比屄逼叉缺吊屌碉雕]',
'*', sentence, flags=re.IGNORECASE)
print(purified) # 你丫是*吗? 我*你大爷的. * you.
```
> **说明:**` re`模块的正则表达式相关函数中都有一个`flags`参数它代表了正则表达式的匹配标记可以通过该标记来指定匹配时是否忽略大小写、是否进行多行匹配、是否显示调试信息等。如果需要为flags参数指定多个值可以使用[按位或运算符](http://www.runoob.com/python/python-operators.html#ysf5)进行叠加,如`flags=re.I | re.M`。
#### 例子4拆分长字符串
```Python
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)
```
### 简单的总结
正则表达式在字符串的处理和匹配上真的非常强大,通过上面的例子相信大家已经感受到了正则表达式的魅力,当然写一个正则表达式对新手来说并不是那么容易,但是很多事情都是熟能生巧,大胆的去尝试就行了,有一个在线的[正则表达式测试工具](https://c.runoob.com/front-end/854)相信能够在一定程度上帮到大家。
> **温馨提示**:学习中如果遇到困难,可以加**QQ交流群**询问。
>
> 付费群:**789050736**,群一直保留,供大家学习交流讨论问题。
>
> 免费群:**151669801**,仅供入门新手提问,定期清理群成员。