编程入门14:Python模式匹配

上一篇:编程入门13:Python文本处理

我们有时需要判断一段文本是否符合特定的“模式”(Pattern),这称为文本模式匹配——例如手机号的模式可以描述为“1再加上任意10个数字”,你可以写一个实现此功能的函数:如果字符串长度为11,首个字符为1,其他字符均为数字,就返回真值,否则返回假值;而如果需要从一大段文本中找出所有的手机号,你就得从第一个字符开始循环截取长度为11的字符串进行判断——这显然十分笨拙。

更为灵活高效的做法是使用“正则表达式”(Regular Expression),这是一种专门描述文本模式规则的字符串,例如“1\d{10}”就表示“1再加上任意10个数字”。以下Python代码使用标准库提供的re模块实现正则表达式匹配,从来自“选号网”的一段文本中找出所有的手机号——这显然优雅多了:

In [1]: s = "1881001118835400188101888992360018801392999236001881010005523600"

In [2]: import re

In [3]: re.findall(r"1\d{10}", s)
Out[3]: ['18810011188', '18810188899', '18801392999', '18810100055']

findall()函数的两个参数分别指定正则表达式和目标文本,因为正则表达式里经常包含反斜杠,所以推荐使用加r前缀的原始字符串来表示——手机号的首个字符一定是1,正则表达式里直接用1来匹配;之后的“\d”表示匹配任一数字,以下是常用的正则转义码:

代码转义说明
\d数字类字符,默认也包括全角数字
\D非数字类字符
\w单词类字符,默认也包括汉字等
\W非单词类字符
\s空白类字符,即空格/制表/换行等
\S非空白类字符
\b单词边界,用于精确匹配单词
\B非单词边界

“\d”后用花括号指定匹配10次——这类特殊功能符号如下所示(单纯作为文本来匹配时就要加反斜杠):

符号功能说明正则示例目标示例
.匹配任意字符,换行符\n除外a.cabc acc
\转义a\.ca.c
*匹配前一字符0至任意次abc*ab abccc
+匹配前一字符1至任意次abc+abc abccc
?匹配前一字符0至1次abc?ab abc
{m,n}匹配前一字符m至n次,省略n则无上限ab{1,2}cabc abbc
^匹配字符串开头^abcabc
$匹配字符串末尾abc$abc
|abc|defabc def
[]指定字符集如[abc]a[bc]eabe ace
()分组(ab){2}a(12|34)cababa34c

指定字符集的方括号之内还有更多写法,例如[a-z]表示从a到z即任意小写字母,[^abc]表示除abc外的任意字符。

re模块的常用函数如下:

  • compile() 编译正则表达式,返回模式对象——如果某个正则表达式要在程序中多次使用,就应先编译并使用模式对象的相应方法来匹配文本以提高运行效率(以下函数去掉正则参数就是模式对象的方法)
  • findall() 在字符串内查找所有匹配文本,返回字符串列表
  • split() 用匹配文本拆分字符串,返回字符串列表
  • sub() 将字符串内匹配文本替换为指定文本,返回替换后的字符串
  • subn() 将字符串内匹配文本替换为指定文本,返回替换的次数
  • match() 从字符串开头匹配文本,返回匹配对象
  • search() 在字符串内查找匹配文本,返回匹配对象
  • finditer() 在字符串内查找所有匹配文本,返回匹配对象迭代器

注意match()、search()和finditer()返回的是匹配对象,匹配对象有下列方法:

  • group() 返回匹配的字符串,如定义了多个分组可以指定分组号
  • start() 返回匹配开始位置
  • end() 返回匹配结束位置
  • span() 返回匹配开始和结束位置(元组类型)
  • groups() 返回匹配的所有分组字符串(元组类型)

以下代码将文本拆分为单词(对于中文则是句子),注意findall()返回列表而finditer()返回生成迭代器——每次迭代返回一个单词,这样更省内存。

In [4]: po = re.compile(r"\w+")

In [5]: po.findall("Life is short, you need Python.")
Out[5]: ['Life', 'is', 'short', 'you', 'need', 'Python']

In [6]: mo = po.finditer("道可道,非常道;名可名,非常名。")

In [7]: for i in mo:
   ...:     print(i.group(), end=" ")
   ...:     
道可道 非常道 名可名 非常名 

添加圆括号可以在正则表达式中创建分组,假如你在匹配手机号的同时还想分别提取其中的“号段”和“地区码”,就可以使用分组功能:

In [8]: po = re.compile(r"(1\d{2})(\d{4})(\d{4})")

In [9]: mo = po.search("手机号码:13366669999")

In [10]: mo.group(0)  # 参数为0与无参数都返回整个匹配
Out[10]: '13366669999'

In [11]: mo.group(1)  # 参数为1返回第一个分组,以下依次类推
Out[11]: '133'

In [12]: mo.group(2)
Out[12]: '6666'

In [13]: mo.group(3)
Out[13]: '9999'

In [14]: mo.groups()  # 此方法返回所有分组
Out[14]: ('133', '6666', '9999')

替换类方法如果需要将匹配文本的一部分放入替换文本中,也是通过添加分组,在替换文本中用反斜杠加组号表示即可:

In [15]: mo = re.compile(r"特工(\w)\w")

In [15]: mo.sub(r"特工\1某", "特工赵大告诉特工钱二:特工孙三将与特工李四接头。")
Out[15]: '特工赵某告诉特工钱某:特工孙某将与特工李某接头。'

正则表达式默认采用最长匹配(也叫“贪婪”匹配),只要规则允许就匹配尽可能多的字符;有时我们需要采用最短匹配,那就在多次匹配符号(*、+、})后再加一个?号:

In [16]: s = "子曰:“君子坦荡荡”。子曰:“见贤思齐焉”。"

In [17]: re.findall(r"“(.*)”", s)
Out[17]: ['君子坦荡荡”。子曰:“见贤思齐焉']

In [18]: re.findall(r"“(.*?)”", s)
Out[18]: ['君子坦荡荡', '见贤思齐焉']

以下网络爬虫程序使用正则表达式找出网页中的图片链接并批量下载:

"""webcrawler.py 百度图片搜索并批量下载
"""
from urllib.request import urlopen, urlretrieve
from urllib.parse import quote
import re
url = "https://image.baidu.com/search/flip?tn=baiduimage&word="
keyword = "高清动漫"
path = "D:/Test/img/"


def main():
    try:
        html = urlopen(url + quote(keyword)).read().decode()
        links = re.findall(r'"objURL":"(.+?)"', html)
        for i in links:
            urlretrieve(i, path + i.split("/")[-1])  # 原文件名保存
    except Exception as e:
        print(repr(e))


if __name__ == "__main__":
    main()

《编程入门14:Python模式匹配》 14_img.jpg

学会正则表达式能让你省下许多宝贵的时间。

——编程原来是这样……

编程小提示:在线正则表达式工具

下面是一些在线工具,可以方便地测试正则表达式:

下一篇:编程入门15:Python迭代机制

    原文作者:python
    原文地址: https://www.jianshu.com/p/4784ecca936e
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞