【整理】入门Python中的正则表达式

正则表达式(Regular Expression)是一种文本模式,包括普通字符(例如,a 到 z 之间的字母)和特殊字符(称为"元字符")。正则表达式使用单个字符串来描述、匹配一系列匹配某个句法规则的字符串。

  在碰到有关字符串的操作时,正则表达式将逐渐浮出水面,其在文本匹配(筛选出符合要求的子字符串或验证某字符串是否符合要求)和文本提取(如对爬虫获取的网页内容进行文本提取),都将带来方便的操作,尽管这种操作以其逻辑性作为要求。梳理的有关内容参考菜鸟教程胶囊编程等网页,并采用胶囊编程中的有关工具。


语法

  正则表达式作为一种文本规则的匹配,这种规则借助各种符号来体现,包括:对单个字符类型的约束,对字符个数的约束,对字符位置的约束,对字符间关系的约束。因此不同于一般教程,我将照我的这四点理解来完成这部分语法的梳理。


对单个字符类型的描述

  字符类型基本可以分为3类,一类是数字字符,一类是大小写字母,还有一类是特殊字符,这3类的字符组合往往也是我们设计一些密码时的要求。


区间[ ]的使用

   [ ]区间表示一个可选范围,只有当字符是区间内的字符集时才能完成匹配。 比如[0red]就表示在0,r,e,d中匹配相应的字符,彼此之间是或的关系;也可以用短横线 - 去描述一个连续范围,如[0-6a-f]就表示0-6的这7个数字和a-f的这6个字母,相当于此时区间内拥有13个元素。
在这里插入图片描述
   如果我们不用括号,这将与只有一个元素的[ ]达到一样的效果,表示字符串中必然要包含某字符。
在这里插入图片描述

特殊字符的快捷表示
字符描述
\s匹配任何空白字符,如空格、制表符、换页符
\S匹配任何非空白字符
\w匹配任何单词字符,即数字、字母和下划线
\W匹配任何非单位字符
\d匹配任何数字字符
\D匹配任何非数字字符
.匹配除换行符(\n、\r)之外的任何单个字符

  看得出来,快捷方式本质上是一系列字符区间[ ]简并的集合表示,取反^符号与快捷方式中的大小写转换起到一样的作用,本质都是集合中的补集运算。测试用例如下,分别匹配了任意两个非数字字符串 和 任意两个连续数字字符串。

在这里插入图片描述

  当然对于一些特殊字符,如[ ] ? { } 等, 在加入区间时要加上转义符号,对于一些非打印字符,也应用其转义形式,如换行符\n 和回车符 \r。看得出来,在测试中,文本里共有四个符合要求的换行符匹配。
在这里插入图片描述

对字符个数的约束

  上一部分讲了对单个字符的类型限制,这本质上是一系列待选元素的集合。对字符个数的约束通过限定符来实现,即正则表达式中的一个给定组件必须要出现多少次才能满足匹配,有 *+{n}{n,}{n,m} 共6种。

括号{ }的使用
字符描述
{n}n为非负整数,匹配确定的n次
{n,}n为非负整数,至少匹配n次
{n,m}m和n均为非负整数,最少匹配n次且最多匹配m次

  { }的使用将省去对重复类型元素的区间书写,并限定个数。如连续8个数字,就可以写成\d{8},个数为4-7之间的连续小写字母串,就可以写成[a-z]{4,7}。
在这里插入图片描述
  正则表达式默认贪婪,即总是先匹配能匹配到的更长字段,取消贪婪可在{ }后加?,就更改为先匹配能匹配到的更短字段。


限定符的快捷表示

  快捷表示能简化正则表达式的书写。

字符描述
*匹配前面的子表达式零次或多次,相当于{0,}
+匹配前面的子表达式一次或多次,相当于{1,}
?匹配前面的子表达式零次一次,相当于{0,1}

  到此为止,我们已经得到写一个正则表达式的基本模板,即“字符类型[ ]描述 字符数量{ }限制”。快捷键之间同样可以搭配使用,如.+将提取出输入的3个字符串,在取消贪婪后则将提取出输入的50个单个字符。但请谨慎单独使用.*的表达式,此时输出结果会产生警告。
在这里插入图片描述
在这里插入图片描述

对字符位置的约束

  实际上,字符往往具有一定的位置特征,比如限制在开头,限制在结尾,限制在空格之间。这类位置特征的描述采用定位符来实现,即定位符用来描述字符串或单词的边界^$ 分别指字符串的开始与结束,\b 描述单词的前或后边界,\B 表示非单词边界。
  要谨慎将限定符与定位符一起使用的情况,如并不允许诸如 ^* 之类的表达式。进行边界匹配时,也请注意边界前后的位置务必放置正确。下面展示了非单词边界和单词边界匹配的区别。
在这里插入图片描述

例子:匹配手机号码

  现在你可以练手基本的正则表达式了,已知手机号码的匹配必须是11位的数字;第一位数字必须以1开头,第二位数字可以是[3,4,5,7,8]中的任意一个,后面9个数是[0-9]中的任意一个数字。那么可以写出严格的表达式如下 :

^1[3,4,5,7,8]\d{9}$

在这里插入图片描述
  如果为了让换行符存在的情况也能进行匹配,则可以修改如下:

^1[3,4,5,7,8]\d{9}\n??$

在这里插入图片描述
  强调一下两个问号的作用,第一个问号是量词,是对换行符数量的限定,而第二个问号是慵懒,即取消贪婪模式。
在这里插入图片描述

对字符间关系的约束

  我们最后来讲分组和断言。在分组中既需要匹配左边的数据,也需要通过分组将左侧的关键数据提取到右侧,相当于对数据的特定提取断言也称预搜索,是对字符串左右包含关系的一种确认


分组

  分组通过( )来实现,当正则比较复杂时,可用( )来进行分组,比如组合字符串作为一个整体进行匹配
在这里插入图片描述
  当需要整体中的部分内容时,也可以用( )来分组,比如提取html标签中的内容:
在这里插入图片描述
  值得留意的是分组的回溯引用,即用\N可以引用编号为N的分组,这有助于一些标签的彼此照应和字符重复组合的验证。如下面的用例将提取出满足abba形式的回文字符串。
在这里插入图片描述

断言
字符描述
(?=表达式)正向先行断言,在某个位置向右看,表示所在位置右侧必须能匹配表达式
(?!表达式)反向先行断言,在某个位置向右看,表示所在位置右侧不能出现表达式
(?<=表达式)正向后行断言,在某个位置向左看,表示所在位置左侧必须能匹配表达式
(?<!表达式)反向后行断言,在某个位置向左看,表示所在位置左侧不能匹配表达式

  下面展现了正向和反向的差别,看得出来彼此同样是互为补集的关系。在这里插入图片描述
    这是“喜欢”前要有“我”,“喜欢”后要有“你”。
在这里插入图片描述
    这是“喜欢”前不能有“我”,“喜欢”后要有“你”。


例子:密码强度验证

  已知密码规则要求:至少一个大写字母,至少一个小写字母,至少一个数字,至少8个字符。请编写正则表达式进行密码强度的验证。结果和测试如下:

(?=.*?[a-z])(?=.*?[A-Z])(?=.*?[0-9]).{8,}



运算符优先级

  正则表达式从左到右进行计算,并遵循优先级顺序,这与算术表达式类似。

运算符描述
\转义符
(), (? :), (?=), [ ]圆括号和方括号
*, +, ?, {n}, {n,}, {n,m}限定符
^, $, \任何元字符、任何字符定位点和序列(即:位置和顺序)
|替换,"或"操作

  注意字符具有高于替换运算符的优先级,“f|mood"匹配的将是"f"或"mood”。若要匹配"food"或"mood",请用括号创建子表达式,从而产生"(f|m)ood"。
在这里插入图片描述

Re模块

  正则表达式的基本语法已经说明完毕,让我们从定义入手,看下Python提供的Re模块中与之搭配的一些常用函数。

匹配函数match

def match(pattern, string, flags=0):
    """Try to apply the pattern at the start of the string, returning
    a Match object, or None if no match was found."""
    return _compile(pattern, flags).match(string)

  使用 group(num) 或 groups() 匹配对象函数来获取匹配表达式。先来执行一个案例:

import re

line = "Cats are smarter than dogs"

matchObj = re.match(r'(.*) are (.*?) .*', line, re.M | re.I)

if matchObj:
    print("matchObj.group() : ", matchObj.group())
    print("matchObj.group(1) : ", matchObj.group(1))
    print("matchObj.group(2) : ", matchObj.group(2))
else:
    print("No match!!")

结果:

matchObj.group() :  Cats are smarter than dogs
matchObj.group(1) :  Cats
matchObj.group(2) :  smarter

搜索函数search

def search(pattern, string, flags=0):
    """Scan through string looking for a match to the pattern, returning
    a Match object, or None if no match was found."""
    return _compile(pattern, flags).search(string)

继续测试同样的案例。

import re

line = "Cats are smarter than dogs"

searchObj = re.search(r'(.*) are (.*?) .*', line, re.M | re.I)

if searchObj:
    print("searchObj.group() : ", searchObj.group())
    print("searchObj.group(1) : ", searchObj.group(1))
    print("searchObj.group(2) : ", searchObj.group(2))
else:
    print("Nothing found!!")

结果是一致的 (但其实它俩肯定是有差异的)

searchObj.group() :  Cats are smarter than dogs
searchObj.group(1) :  Cats
searchObj.group(2) :  smarter

match对象

  match对象作为match和search函数的返回对象,具有以下几个参数:

  • group([group1, …]) 方法用于获得一个或多个分组匹配的字符串,当要获得整个匹配的子串时,可直接使用 group() 或 group(0);
  • start([group]) 方法用于获取分组匹配的子串在整个字符串中的起始位置(子串第一个字符的索引),参数默认值为 0;
  • end([group]) 方法用于获取分组匹配的子串在整个字符串中的结束位置(子串最后一个字符的索引+1),参数默认值为 0;
  • span([group]) 方法返回 (start(group), end(group))

  值得注意的是,re.match只匹配字符串的开始,如果字符串开始不符合正则表达式,则匹配失败,函数返回None;而re.search匹配整个字符串,直到找到一个匹配。我们来看一个案例:

import re

line = "Cats are smarter than dogs"

matchObj = re.match(r'dogs', line, re.M | re.I)
if matchObj:
    print("match --> matchObj.group() : ", matchObj.group())
else:
    print("No match!!")

searchObj = re.search(r'dogs', line, re.M | re.I)
if searchObj:
    print("search --> searchObj.group() : ", searchObj.group())
    print("Search --> searchObj.span()",searchObj.span())
else:
    print("No match!!")

结果:

No match!!
search --> searchObj.group() :  dogs
Search --> searchObj.span() (22, 26)

编译函数compile

def compile(pattern, flags=0):
    "Compile a regular expression pattern, returning a Pattern object."
    return _compile(pattern, flags)

  生成的正则表达式( Pattern )对象,供其他函数使用,比如可后接findall函数。


替换函数sub

def sub(pattern, repl, string, count=0, flags=0):
    """Return the string obtained by replacing the leftmost
    non-overlapping occurrences of the pattern in string by the
    replacement repl.  repl can be either a string or a callable;
    if a string, backslash escapes in it are processed.  If it is
    a callable, it's passed the Match object and must return
    a replacement string to be used."""
    return _compile(pattern, flags).sub(repl, string, count)

看如下的测试案例:

import re

phone = "2004-959-559 # 这是一个国外电话号码"

# 删除字符串中的 Python注释
num = re.sub(r'#.*$', "", phone)
print("电话号码是: ", num)

# 删除非数字(-)的字符串
num = re.sub(r'\D', "", phone)
print("电话号码是 : ", num)

输出为:

电话号码是:  2004-959-559 
电话号码是 :  2004959559

查询函数findall

def findall(pattern, string, flags=0):
    """Return a list of all non-overlapping matches in the string.

    If one or more capturing groups are present in the pattern, return
    a list of groups; this will be a list of tuples if the pattern
    has more than one group.

    Empty matches are included in the result."""
    return _compile(pattern, flags).findall(string)

   findall在字符串中找到正则表达式所匹配的所有子串,并返回一个列表,不同于 match 和 search 仅匹配一次 ,findall 匹配所有。 如果有多个匹配模式,则返回元组列表,如果没有找到匹配的,则返回空列表。finditer和 findall 类似,在字符串中找到正则表达式所匹配的所有子串,但把它们作为一个迭代器返回。也看一个用例:

import re

pattern = re.compile(r'\d+')  # 查找数字
result1 = pattern.findall('runoob 123 google 456')
result2 = pattern.finditer('run88oob123google456', 0, 10)

print(result1)
print(result2)

for out in result2:
    print(out.group())

结果为:

['123', '456']
<callable_iterator object at 0x000001D15B6053C0>
88
12

分割函数split

def split(pattern, string, maxsplit=0, flags=0):
    """Split the source string by the occurrences of the pattern,
    returning a list containing the resulting substrings.  If
    capturing parentheses are used in pattern, then the text of all
    groups in the pattern are also returned as part of the resulting
    list.  If maxsplit is nonzero, at most maxsplit splits occur,
    and the remainder of the string is returned as the final element
    of the list."""
    return _compile(pattern, flags).split(string, maxsplit)

直接看一个用例:

import re
# 按非字符分割字符串
result1 = re.split('\W+', 'runoob, runoob, runoob.')

# 提取分割符
result2 = re.split('(\W+)', ' runoob, runoob, runoob.')

# 仅分割一次
result3 = re.split('\W+', ' runoob, runoob, runoob.', 1)

print("按非字符分割字符串:",result1)
print("提取分割符:",result2)
print("仅分割一次:",result3)

输出结果如下:

按非字符分割字符串: ['runoob', 'runoob', 'runoob', '']
提取分割符: ['', ' ', 'runoob', ', ', 'runoob', ', ', 'runoob', '.', '']
仅分割一次: ['', 'runoob, runoob, runoob.']

应该指出,这比string自带的split()函数的分割功能更强。


应用

   让我们回到写作本文的初衷,即探讨正则表达式在文本匹配和文本提取方面的应用。


文本匹配:连续最长数字串

   这其实是牛客网上华为题库里的一道算法题,要求为:输入一个字符串,返回其最长的数字子串,以及其长度。若有多个最长的数字子串,则将它们全部输出(按原字符串的相对位置)。

import re

test = 'abcd12345ed125ss123058789'
# test = 'a8a72a6a5yy98y65ee1r2'

num = re.compile(r'\d+')
# 找到各连续数字串
result = num.findall(test)
# 计算最大子字符串的长度
maxlens = max([len(substr) for substr in result])
# 排列子字符串,打印对应结果
out = ''
for substr in result:
    if maxlens == len(substr):
        out += substr

print(out+','+str(maxlens))

测试结果:

123058789,9

  对于许多连续性的字符集合型条件,我们都能用正则表达式学习找到更简洁的解决方案。


文本提取:爬虫采个名

  最后让我们来做一个有趣的测试,利用网页的开发者工具打开我CSDN博客主页的html文档 (你也可以尝试你自己的) ,可以看到 title 为 月亮鱼与十四行的博客_CSDN博客-课程领域博主
在这里插入图片描述


我们利用爬虫和re模块将这条标题信息提取出来:

from urllib.request import urlopen
from bs4 import BeautifulSoup
import re
# 打开我的主页
html = urlopen("https://blog.csdn.net/weixin_47305073?spm=1011.2423.3001.5343") #博客主页地址
bsObj = BeautifulSoup(html,'html.parser')

# 查找标题栏,name为tag对象
name = bsObj.find("title")
# 应用正则表达式中的分组提取
get = re.compile(r'<(title)>(.+)</\1>')
# 提取出我的标题,先将name转换为str对象,注意gruop中num为2
print(get.match(str(name)).group(2))

运行结果如下:

月亮鱼与十四行的博客_CSDN博客-课程领域博主

  以此类推,对于采集到的浩繁的文本信息,可采用正则表达式方便数据的有效提取。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值