该笔记摘记了《Python 语言及其应用》、《Python 核心编程》等几本书的内容,从中你可以了解到关于 Python 语言的基本使用,同时也会深入讨论一些编码上的细节问题。此外,该笔记也穿插记录了关于 Python 语言的诸多面试问题
8 正则表达式
8.1 特殊符号和字符
正则表达式为高级的文本模式匹配、抽取、与/或文本形式的搜索和替换功能提供了基础。简单地说,正则表达式是一些由字符和特殊符号组成的字符串,它们描述了模式的重复或者表述多个字符,于是正则表达式能按照某种模式匹配一系列有相似特征的字符串
注意『匹配』和『搜索』两个术语:
- 匹配:指模式匹配,判断一个字符串能否从起始处全部或者部分地匹配某个模式,调用
match()
函数 - 搜索:即在字符串任意部分中搜索匹配的模式,调用
search()
函数
匹配对个字符串
使用择一匹配符号匹配多个正则表达式
表示择一匹配的管道符号,也就是键盘上的竖线( | ),表示从一个 “ 从多个模式中选择其一 ” 的操作
匹配任意单个字符
点号( . )匹配除了换行符 \n 以外的任何字符
Python 正则表达式有有一个编译标价 [S 或者 DOTALL],该标记能够推翻这个限制,使点号能够匹配换行符
有个问题,如果我们就是要匹配点号呢?使用转义符( \. )
从字符串起始或者结尾或者单词边界匹配
还有些符号和相关的特殊字符用于在字符串的起始和结尾部分指定用于搜索的模式
特殊字符 \b 和 \B 可以用来匹配字符边界,区别在于
- \b 将用于匹配一个单词的边界,这意味着如果一个模式必须位于单词的起始部分,就不管该单词前面是否有任何字符
- \B 将匹配出现在一个单词中间的模式,即不是单词边界
创建字符集
尽管点号可以用于匹配任意符号,但某些时候,可能想要匹配某些特定字符。因此,就发明了方括号。该正则表达式能够匹配一对方括号中包含的任何字符
限定范围和否定
除了单字符以外,字符集还支持匹配指定的字符范围
方括号中两个符号中间用连字符( - )连接,用于指定一个字符的范围。如果,脱字符( ^ )紧跟在左方括号后面,表示不匹配给定字符集中的任何一个字符
使用闭包操作符实现存在性和频数匹配
- 星号( * )将匹配其左边的正则表达式出现零次或者多次的情况(在编译原理中,称为 Kleene 闭包)
- 加号( + )将匹配一次或者多次出现的正则表达式(称为正闭包)
- 问号( ?)将匹配零次或者一次出现的正则表达式,如果问号紧跟在任何使用闭合操作符的匹配后面,它将直接要求正则表达式引擎匹配尽可能少的次数
- 大括号( {} )里面或者是单个值后者是一对由逗号分隔的值,{N} 表示精确匹配前面的正则表达式 N 次,{M, N} 将匹配 M ~ N 次出现
表示字符集的特殊字符
- \d 表示匹配任何十进制数字
- \w 能够用于表示全部字母数字的字符集,相当于 [A-Za-z0-9]
- \s 可以用来表示空格字符
这些特殊字符的大写版本表示不匹配,例如,\D 表示任何非十进制数
我们考虑简单电子邮件地址的正则表达式,\w+@\w.com,如果我们想要在此基础上添加可选的主机名时,比如 nobody@www.xxx.com,那么正则表达式即需修改为 \w+@(\w+.)?\w+.com,?表示该模式出现零次或者一次
我们可以对该模式进行一些扩展,允许任意数量的中间子域名存在,将 ?变成 *,\w+@(\w+.)*\w+.com
使用圆括号指定分组
当使用正则表达式时,一对圆括号可以实现以下任意一个(或者两个)功能:
- 对正则表达式进行分组
- 匹配子组
对正则表达式进行分组很好理解,我们重点来看一下什么叫做匹配子组?
在很多时候除了进行匹配操作以外,我们还想要提取所匹配的模式。例如,如果决定匹配模式 \w±\d+,但是想要分别保存第一部分的字母和第二部分的数字,该如何实现呢?
如果为两个子模式都加上圆括号,例如 (\w+)-(\d+),然后就能够分别访问每一个匹配子组
扩展表示法
正则表达式的最后一个方面是扩展表示法,它们以问号开始( ?.. )
我们不会为此花费太多的时间,因为它们通常用于在判断匹配之前提供标记,实现一个前视(或者后视)匹配,或者条件检查
通过使用 (?..) ,可以对部分正则表达式进行分组,但是不会保存该分组用于后续的检索或应用。换句话说,当不想保存今后永远不会使用的多余匹配时,这个符号就非常有用
我们可以同时一起使用 (?P<name>) 符号,通过使用一个名称标识符而不是使用从 1 开始增加到 N 的增量数字来保存匹配。同时,可以使用一个类似风格的 \g<name> 来检索他们
(?P=name) 可以在一个相同的正则表达式中重用模式,而不必稍后再次在相同正则表达式中指定相同的模式
(?=…) 和 (?!..) 符号在目标字符串中实现一个前视匹配,而不必实际上使用这些字符串。前者是正向前视断言,后者是负向前视断
8.2 正则表达式和 Python 语言
re 模块支持更强大而且更通用的 Perl 风格的正则表达式,该模块允许多个线程共享同一个已编译的正则表达式对象,也支持命名子组
re 模块:核心函数和方法
【编译正则表达式】
在模式匹配发生之前,正则表达式模式必须编译成正则表达式对象。由于正则表达式在执行过程中将进行多次比较操作,因此前列建议使用预编译
而且,既然正则表达式的编译时必须的,那么使用预编译来提升执行性能无疑是明智之举,re.compile() 能够提供此功能
匹配对象以及 group() 和 groups() 方法
当处理正则表达式时,除了正则表达式对象(re.RegexObject)之外,还有另外一个对象类型:匹配对象(re.MatchObject)
group()
要么返回整个匹配对象,要么根据要求返回特定子组groups()
则仅返回一个包含唯一或者全部子组的元组- 如果没有子组要求,
group()
返回整个匹配,groups
返回一个空元组
使用 match() 方法匹配字符串
match()
函数试图从字符串的起始部分对模式进行匹配,如果匹配成功,就返回一个匹配对象;如果失败,就返回 None
接着,我们使用 group(num)
或 groups()
匹配对象函数来获取匹配表达式
使用 search() 在一个字符串中查找模式
match()
只匹配字符串的开始,如果字符串开始不符合正则表达式,则匹配失败,例如下面的代码中字符串的字母为 C,而模式中的首字母为 f,故匹配失败
search()
匹配整个字符串,并返回第一个成功的匹配
使用 findall() 和 finditer() 查找每一出现的位置
findall() 在字符串中找到正则表达式所匹配的所有子串,并返回一个列表,如果没有找到匹配的,则返回空列表
finditer() 和 findall() 类似,在字符串中找到正则表达式所匹配的所有子串,并把它们作为一个迭代器返回
使用 sub() 和 subn() 搜索与替换
sub() 和 subn() 都是将某字符串中所有匹配正则表达式的部分进行某种形式的替换,subn() 返回一个表示替换的总数
使用 \N 能够取出匹配分组编号,N 是在替换字符串中使用的分组编号
例如,将美式日期表示法 MM/DD/YY 转化为其他国家常用的格式 DD/MM/YY
在限定模式上使用 split() 分隔字符串
re 模块和正则表达式的对象方法 split() 和相应字符串的工作方式类似,不同的是,在 re 模块和正则表达式中,它们基于正则表达式的模式分隔字符串
如果你不想为每次模式的出现都分隔字符串,就可以通过为 max 参数设定一个值来指定最大分隔次数
【使用 Python 原始字符串】
ASCII 字符串和正则表达式的特殊字符之间会存在冲突,比如,\b 表示 ASCII 字符的退格符,但是 \b 同时也是一个正则表达式的特殊符,表示匹配一个单词的边界。对于正则表达式编译器而言,若要把 \b 视为字符串内容而不是单个退格符,就需要在字符串中再使用一个反斜线转义反斜线,如 \\b
这是一个很麻烦的事情,于是提出了原始字符串的概念。原始字符串是 Python 中一类比较特殊的字符串,以大写字母 R 或者小写字母 r 开始,在原始字符串中,字符 \ 不再表示转义字符的含义
贪婪匹配
考虑下面的例子,我们希望的是通过 .+ 匹配数字前面的所有内容,而子组得到的是 1171590364-6-8,但是为什么输出却是 4-6-8?
问题在于正则表达式本质上实现贪婪匹配 ,这就意味着对于该通配符模式,将对正则表达式从左至右按顺序求值,而且试图获取匹配该模式的尽可能多的字符
于是,就出现了下图所示的情况
要解决这个问题,可以使用非贪婪操作符(?),可以在 *、+、?之后使用该操作符,它要求正则表达式引擎匹配尽可能少的字符
9 网络编程
9.1 Python 中的网络编程
socket()模块函数
要创建套接字,必须使用 socket.socket()
函数,其语法如下
socket(socket_family, socket_type, protocol=0)
其中,
- socket_family 是 AF_UNIX(基于文件的套接字家族) 或 AF_INET(基于网络的套接字家族)
- socket_type 是 SOCK_STREAM(TCP)或 SOCK_DGRAM(UDP)
所以,为了创建 TCP/IP 套接字,可以通过下面的方式
tcpsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
为了创建 UCP/IP 套接字,可以通过下面的方式
udpsocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
套接字对象(内置方法)
创建 TCP 服务器
我们先给出创建通用 TCP 服务器的伪代码,然后对这些代码的含义进行讲解
ss = socket() # 创建服务器套接字
ss.bind() # 套接字与地址绑定
inf_loop: # 服务器无限循环
cs = ss.accept() # 接受客户端连接
comm_loop: # 通信循环
cs.recv()/cs.send() # 对话
cs.close() # 关闭客户端套接字
ss.close() # 关闭服务器套接字(可选)
调用 accept() 函数之后,就开启了一个简单的单线程服务器,它会等待客户端的连接。一旦服务器接受了一个连接,就会利用 accept() 返回一个独立的客户端套接字,用来与即将到来的消息进行交换。这将能够空出原始服务器套接字,以便可以继续等待新的客户请求
一旦创建了临时套接字,通信就可以开始,通过使用这个新的套接字,客户端与服务器就可以开始参与发送和接收的对话中,直到连接终止。当一方关闭连接或者向对方发送一个空字符串时,通常就会关闭连接
我们给出一个 TCP 服务器程序,它接受客户端发送的数据字符串,并告知客户端成功接收数据
from socket import *
HOST = ''
PORT = 21567
BUFSIZ = 1024
ADDR = (HOST, PORT)
tcpSerSock = socket(AF_INET, SOCK_STREAM)
tcpSerSock.bind(ADDR)
tcpSerSock.listen(5)
while True:
print("waiting for connection...")
tcpCliSock, addr = tcpSerSock.accept()
print("... connected from:", addr)
while True:
data = tcpCliSock.recv(BUFSIZ)
if not data: break
tcpCliSock.send(b'Successfully receive: ' + data)
tcpCliSock.close()
tcpSerSock.close()
HOST 变量是空白的,这是对 bind() 方法的标识,表示它可以使用任何可用的地址。我们选择了一个随机的端口号,并且该端口号似乎没有被使用或系统保留。另外,对于该程序,将设置 1KB 的缓冲区,可以根据网络性能和程序需要改变这个容量。listen() 方法在连接被转接或拒绝之前,传入连接请求的最大数
创建 TCP 客户端
cs = socket() # 创建客户端套接字
cs.connect() # 尝试连接服务器
comm_loop: # 通信循环
cs.send() / cs.recv() # 对话
cs.close() # 关闭客户端套接字
我们给出一个脚本连接到服务器,并以逐行数据的形式提示用户
from socket import *
HOST = 'localhost'
PORT = 21567
BUFSIZE = 1024
ADDR = (HOST, PORT)
tcpCliSock = socket(AF_INET, SOCK_STREAM)
tcpCliSock.connect(ADDR)
while True:
data = input('>')
if not data: break
tcpCliSock.send(data.encode('utf-8'))
data = tcpCliSock.recv(BUFSIZE)
if not data: break
print(data.decode('utf-8'))
tcpCliSock.close()
因为是在通过一台主机上运行测试,所以 HOST 包含本地主机名。端口号 PORT 应该与服务器的端口号完全相同,否则,无法进行通信
执行 TCP 服务器和客户端
当你开始运行两个脚本时,你会发现,对于客户端如果我们什么都不输入,直接按下回车键的话,客户端将正常关闭。此时,服务器继续运行,等待新的客户端连接。但是,从服务器退出时,却好像不是那么容易
因此,我们可以将服务器的 while 循环放在一个 try-except 子句中,并监控 EOFError 或 KeyboardInterrupt 异常,这样就可以在 except 或 finally 字句中关闭服务器的套接字
创建 UDP 服务器
ss = socket() # 创建服务器套接字
ss.bind() # 绑定服务器套接字
inf_loop: # 服务器无限循环
cs = ss.recvfrom / ss.sendto()
ss.close() # 关闭服务器套接字
我们还是给出一个 UDP 服务器程序,它接受客户端发送的数据字符串,并告知客户端成功接收数据
from socket import *
from time import ctime
HOST = ''
PORT = 21567
BUFSIZ = 1024
ADDR = (HOST, PORT)
udpSerSock = socket(AF_INET, SOCK_DGRAM)
udpSerSock.bind(ADDR)
while True:
print("waiting for message...")
data, addr = udpSerSock.recvfrom(BUFSIZ)
udpSerSock.sendto(b'Successfully receive: '+data, addr)
print('...received from and return to: ', addr)
udpSerSock.close()
创建 UDP 客户端
cs = socket() # 创建客户端套接字
comm_loop: # 通信循环
cs.sendto() / cs.recvfrom()
cs.close() # 关闭客户端套接字
我们还是给出一个脚本连接到服务器,并以逐行数据的形式提示用户
from socket import *
HOST = 'localhost'
PORT = 21567
BUFSIZ = 1024
ADDR = (HOST, PORT)
udpCliSock = socket(AF_INET, SOCK_DGRAM)
while True:
data = input('>')
if not data: break
udpCliSock.sendto(data.encode('utf-8'), ADDR)
data, ADDR = udpCliSock.recvfrom(BUFSIZ)
if not data: break
print(data.decode('utf-8'))
udpCliSock.close()
socket 模块属性
除了现在熟悉的 socket.socket() 函数之外,socket 模块还提供了更多用于网络应用开发的属性,下面列出了一些最受欢迎的属性