『正则表达式』《正则指引》学习笔记

1. 字符组

1.1 普通字符组

在正则表达式中,字符组表示在同一个位置可能出现的各种字符,其写法是在一对方括号[]之间列出所有可能出现的字符

>>> import re
>>> regex = r"[ab]"
>>> re.search(regex, "a")
<re.Match object; span=(0, 1), match='a'>
>>> re.search(regex, "a") != None
True
>>> re.search(regex, "b") != None
True
>>> re.search(regex, "c") != None
False

上面写法太感人,字符数量一多就非常麻烦,所以正则表达式提供了范围表示法

所谓范围表示法,就是用[x-y]的形式表示xy整个范围内的字符

>>> import re
>>> regex = r"[0-9]"
>>> re.search(regex, "5") != None
True
>>> re.search(regex, "a") != None
False

范围表示法本质上是按照码值来确定的,所以x的码值必须小于y的码值,如果大小颠倒,编译器会报错

另外,不要图省事用[0-z],虽然这么写可以,但是会有多余的字符也被匹配了

在不少语言中,还可以用\xhex来表示一个字符,它可以表示一些难输入或者难以显示的字符,比如所有ASCII字符对应的字符组就是[\x00-\x7F](码值0~127)

>>> import re
>>> regex = r"[\x00-\x7f]"
>>> re.search(regex,"c") != None
True
>>> re.search(regex,"I") != None
True
>>> re.search(regex,"0") != None
True
>>> re.search(regex,"<") != None
True

1.2 元字符与转义

上面的例子中,字符组中的横线-并不能匹配横线字符,而是用来表示范围,这类字符叫做元字符(meta-character)。字符组的开方括号[、闭方括号]以及^$都算元字符。

在匹配中,它们有着特殊的意义。但是,有时候并不需要表示这些特殊意义,只需要普通字符,此时就必须做特殊处理,也就是转义

>>> import re
>>> regex1 = r"[-09]"
>>> regex2 = r"[0-9]"
>>> regex3 = r"[-0-9]"
>>> regex4 = r"[0\-9]"
>>> re.search(regex1, "-") != None
True
>>> re.search(regex2, "-") != None
False
>>> re.search(regex3, "-") != None
True
>>> re.search(regex4, "-") != None
True
>>> re.search(regex4, "5") != None
False
  • 由于Python支持原生字符串,故直接用反斜杠就行,但是有些语言不支持原生字符串,如Java,应该写成regex = "[0\\-9]"
  • 有些字符成对出现时才是元字符,这种情况只要对开始的元字符转义即可

1.3 排除型字符组

非常类似普通字符组[……],只是在开方括号后紧跟一个脱字符^,写作[^……],表示“在当前位置,匹配一个没有列出的字符”。

>>> import re
>>> regex = r"[^0-9][0-9]"
>>> re.search(regex, "A8") != None
True
>>> re.search(regex, "x6") != None
True
>>> re.search(regex, "88") != None
False
>>> re.search(regex, "8") != None
False

注意,^只有紧跟在[之后才是排除型字符组的元字符

>>> regex = r"[0-9^]"
>>> re.search(regex, "^") != None
True

1.4 字符组简记法

字符组简记
等价字符组
含义
\d
[0-9]
数字(digit)
\w
[0-9a-zA-Z_]
单词字符(word)
\s
[ \t\r\n\v\f](第一个字符是空格)
空白字符(space)

相对于上面三个普通字符组简记法,正则表达式也提供了对应排除型字符组的简记法:\D\W\S——字母一样,只是变成大写

所以,要表示任意字符,可以这么写:[\s\S][\d\D][\w\W]

1.5 匹配范围

前面的例子没有对字符串的匹配范围做限定,只要子串有匹配的就可以,但有时候我们需要判断字符串整体是否满足一定要求,这就需要对正则表达式做一定限定,一般形如^[……]$,表示必须整体匹配

>>> import re
>>> regex1 = r"^[0-9][A-Z]$"
>>> regex2 = r"[0-9][A-Z]"
>>> re.search(regex1, "Q3Q") != None
False
>>> re.search(regex2, "Q3Q") != None
True

2. 量词

之前匹配的都是单个字符,现在用量词来实现多个字符的匹配

2.1 一般形式

量词
说明
{n}
之前的元素必须出现n次
{m,n}
之前的元素至少出现m次,至多出现n次
{m,}
之前的元素至少出现m次,出现次数无上限
{0,n}
之前的元素可以不出现,也可以出现,最多出现n次
>>> import re
>>> regex = r"^\d{4,6}$"
>>> re.search(regex, "123") != None
False
>>> re.search(regex, "1234") != None
True
>>> re.search(regex, "123456") != None
True
>>> re.search(regex, "1234567") != None
False

2.2 常用量词

常用量词
{m,n}的等价形式
说明
*
{0,}
可以出现,也可以不出现,出现次数无上限
+
{1,}
至少出现一次,出现次数无上限
?
{0,1}
至多出现一次,也可能不出现

2.2.1 量词?的应用

>>> import re
>>> re.search("travell?er", "traveler") != None
True
>>> re.search("travell?er", "traveller") != None
True

2.2.2 量词+的应用

>>> import re
>>> re.search(r"<[^>]+>", "<bold>") != None
True
>>> re.search(r"<[^>]+>", "</table>") != None
True
>>> re.search(r"<[^>]+>", "<>") != None
False

2.2.3 量词*的应用

>>> import re
>>> re.search("(dis)*like", "I like you") != None
True
>>> re.search("(dis)*like", "I dislike you") != None
True
>>> re.search("(dis)*like", "I hate you") != None
False

2.3 数据提取

re.search()如果匹配成功,返回一个MatchObject对象,可以通过MatchObjectgroup()方法返回匹配的文本。

这里再介绍一个方法:re.findall(pattern, string),这个方法能找出给定字符串的所有匹配正则表达式的子串,注意,正则表达式不要用^$

这个方法会返回一个list,其中的元素是在string中一次寻找的pattern能匹配的文本。

>>> import re
>>> re.findall(r"\d{6}", "zipcode1: 201203, zipcode2: 100859")
['201203', '100859']
>>> # 或者也可以这样输出
>>> for zipcode in re.findall(r"\d{6}", "zipcode1: 201203, zipcode2: 100859"):
	print(zipcode)

	
201203
100859

2.4 点号

点号是个特殊的元字符,可以匹配换行符\n之外的所有字符

所以要匹配任意字符不能用点号,而应该用[\s\S]或[\d\D][\w\W]

2.5 匹配优先量词和忽略优先量词

2.5.1 匹配优先量词

顾名思义,就是在拿不准是否要匹配的时候优先尝试匹配,并且记下这个状态,以备将来反悔。

例如:给定一个正则表达式regex = r"\d.*\d",简单说就是要匹配以数字开头结尾的字符串

>>> import re
>>> regex = r"\d.*\d"
>>> re.search(regex,"1abcd3").group()
'1abcd3'
>>> re.search(regex, "1abcd3efg5").group()
'1abcd3efg5'
>>> re.search(regex, "1abcd3efg").group()
'1abcd3'

这里以"1abcd3efg"当为例,匹配到 “3” 时,它既符合点号的匹配,也适合\d的匹配,因为优先匹配,所以会把 “3” 和点号匹配,就这样,直到点号不能匹配或字符串结束为止,最后在进行\d的匹配,从点号不能匹配点号的位置开始匹配\d,如果不满足则往前回溯一个字符,直到能匹配\d为止,如果回溯到第一个都不满足,则返回None,不存在匹配的子串。

2.5.2 忽略优先量词

如果不确定是否需要匹配,忽略优先量词会选择“不匹配”的状态,它会先尝试匹配该部分之后的表达式是否匹配,如果不匹配,再回溯回来判断是否匹配。

忽略优先量词的语法很简单,在一般的量词之后加上“?”即可

>>> import re
>>> regex = r"\d.*?\d"
>>> re.search(regex, "1abcd3").group()
'1abcd3'
>>> re.search(regex, "1abcd3efg5").group()
'1abcd3'
>>> re.search(regex, "1abcd3efg").group()
'1abcd3'

2.6 转义

量词
转义形式
*
\*
+
\+</center>
?
\?
{n}
\{n}
{m,n}
\{m,n}
{m,}
\{m,}
*?
\*\?
+?
\+\?
??</center>
\?\?
.
\.</center>

3. 括号

3.1 分组

以匹配身份证号码为例,按照之前说过的内容可以这样写:

要求
正则表达式
首位是数字,不能为0[1-9]
出去首位和末位,剩下13位或16位,切都是数字\d{13,16}
末尾可能是数字,也可能是x[\dx]

所以整个表达式是[1-9]\d{13,16}[\dx]

>>> import re
>>> # 最好限定整体匹配
>>> id_card_regex = r"^[1-9]\d{13,16}[\dx]$"
>>> re.search(id_card_regex, "110101198001017032") != None
True
>>> re.search(id_card_regex, "1101018001017016") != None
True
>>> re.search(id_card_regex, "11010119800101701x") != None
True

但是,这个表达式还有一些问题它会把不是身份证号码格式的字符串也匹配,就是16位和17位数字的字符串

>>> # 16位
>>> re.search(id_card_regex, "1101011980010171") != None
True
>>> # 17位
>>> re.search(id_card_regex, "1101011980010171x") != None
True

所以我们不能用量词{13,16},像这种可能出现或不出现的多个字符不能简单用量词限定,要先把这些组合成一个组,对它们整体限定,所谓分组,简单来说,就是把看成整体的字符组,在两端加圆括号就行了。

>>> import re
>>> id_card_regex = r"^[1-9]\d{14}(\d{2}[\dx])?$"
>>> # 16位
>>> re.search(id_card_regex, "1101011980010171") != None
False
>>> # 17位
>>> re.search(id_card_regex, "1101011980010171x") != None
False

3.2 多选结构

多选结构的形式是(...|...),在括号内以竖线”|“分隔开多个子表达式,这些子表达式也叫多选分支。在一个多选结构内,多选分支的数目没有限制。在匹配时,整个多选结构被视为单个元素,只要其中某个子表达式能够匹配,整个多选结构的匹配就成功;如果所有子表达式都不能匹配,则整个多选结构匹配失败。

>>> import re
>>> regex = r"(ab|cd)"
>>> re.search(regex, "ac") != None
False
>>> re.search(regex, "ab") != None
True
>>> re.search(regex, "bc") != None
False
>>> re.search(regex, "cd") != None
True
  • 多选结构最好加括号,因为竖线“|”的优先级很低,如果不加,那么可能会出现这种情况
  >>> regex = r"^ab|cd$"
  >>> re.search(regex, "ab") != None
False
  >>> re.search(regex, "^ab") != None
True
  • 多选分支不等于字符组,虽然可以替代,但是会很麻烦;
    排除型字符组可以表示”无法由某几个字符匹配的字符“,多选结构没有对应的结构表示“无法由某几个表达式匹配的字符串
  • 多选分支的排列是有讲究的,一般多选分支匹配都是从左往右,所以如果出现这种情况:湖南|湖南省,你要看实际需求,哪个在前哪个在后

3.3 引用分组

括号不仅仅能把有联系的元素归拢起来并分组,还有其他作用——使用括号之后,正则表达式会保存每个分组真正匹配的文本,等到匹配完成后,通过group(num)之类的方法“引用”分组在匹配时捕获的内容。其中,num表示对应括号的编号,括号分组的编号原则是从左向右计数,从1开始,因为捕获了文本,所以这种功能叫做捕获分组。对应的这种括号叫做捕获型括号。

举个例子,我们经常遇到诸如 2010-12-22、 2019-08-03 这类表示日期的字符串,希望从中提取出年、月、日之类的信息,就可借助分组来实现。正则表达式中,每个捕获分组都有一个编号,具体如下所示:

字  符  串:  2010  -  12  -  22
字  符  串:  2019  -  08  -  03
表  达  式:  (\d{4})-(\d{2})-(\d{2})
分组编号:       1         2        3

一般来说,正则表达式匹配完成之后,都会得到一个表示“匹配结果”的对象,对它调用获取分组的方法,传入分组编号num,就可以得到对应分组匹配的文本。re.search()返回的的是一个MatchObject对象,想要知道是否匹配,判断它是否为None即可,但如果想获得匹配结果的详细信息,可以使用MatchObject.group(num)就可以获得编号为num的分组匹配的文本。

>>> import re
>>> regex = r"(\d{4})-(\d{2})-(\d{2})"
>>> obj = re.search(regex, "2019-08-03")
>>> obj.group(1)
'2019'
>>> obj.group(2)
'08'
>>> obj.group(3)
'03'

注意,也有编号为0的分组,它默认存在,表示整个表达式匹配的文本

>>> obj.group()
'2019-08-03'

有些表达式会包含嵌套括号,者不影响括号的编号,括号的编号是以左开括号为准的,从左往右,从1开始一次编号计数

新手容易弄错的分组的结构

>>> import re
>>> re.search(r"(\d{4})-(\d{2})-(\d{2})", "2010-12-22").group(1)
'2010'
>>> re.search(r"(\d){4}-(\d){2}-(\d){2}", "2010-12-22").group(1)
'0'

注意第二个表达式,它的编号为1的分组为(\d),但是因为后面有量词{4},所以要重复出现4次,而且编号都是1,于是每出现一次就更新一次结果,所以值依次为2,0,1,0,所以最终一号编组值是0

正则表达式的替换

在Python语言中进行正则表达式替换的方法是re.sub(pattern, replacement, string),其中pattern是用来匹配被替换文本的表达式,replacement是要替换成的文本,string是要进行替换操作的字符串。

>>> import re
>>> re.sub(r"[a-z]", "*", "1a2b3c")
'1*2*3*'

replacement中也可以引用分组,形式是\num,其中的num是对应分组的编号,不过replacement并不是一个正则表达式,而是一个普通字符串,但是\1\2这些并不是合法转义,故replacement也必须是原生字符串。

>>> import re
>>> regex = r"(\d{4})-(\d{2})-(\d{2})"
>>> re.sub(regex, r"\2/\3/\1", "2019-08-25")
'08/25/2019'
>>> re.sub(regex, r"\1年\2月\3日", "2019-08-25")
'2019年08月25日'

值得注意的是,如果想在replacement中引用整个表达式匹配的文本,不能使用\0,即使是原生字符串也不行,因为\0xx表示八进制数字的转义,会有歧义冲突,而且\0本身也表示编码为0的字符,如果一定要用整个表达式,可以给正则表达式整体加括号,用\1引用

3.4 反向引用

前面我们看到了引用分组,能引用某个分组内的子表达式匹配的文本,但引用都是在匹配完成后进行的,能不能在正则表达式中引用呢?

答案是可以的,这种功能被称作反向引用,它允许在正则表达式内部引用之前的捕获分组匹配的文本(也就是说,只要你定义好一个捕获分组,后面可直接用编号来用),其形式也是\num,其中num表示所引用分组的编号,编号规则和之前介绍的相同。

>>> import re
>>> regex = r"([a-z])\1"
>>> re.search(regex, "ab") != None
False
>>> re.search(regex, "aa") != None
True

关于反向引用,还有一点需要强调:

        反向引用重复的是对应捕获分组匹配的文本,而不是之前的表达式;也就是说,反向引用的是由之前表达式决定的具体文本,而不是符合某种规则的未知文本

对分组的引用可能出现在三种场合:

  • 在匹配完成后,用group(num)之类的方法提取数据
  • 在进行正则表达式替换时,用\num引用
  • 在正则表达式内部,用\num引用

不过,这是Python中的规定,具体细微差别看具体语言

反向引用的二义性

\10这样的正则表达式,你不清楚他是想要\10还是\10,默认是查找\10,如果不存在则报错

为了消除二义性,Python提供了\g<num>表示法,如果不是想用第10组,那么应该这样写:\g<1>0

3.5 命名分组

为了杜绝上面的二义性,以及便于更加直观的引用捕获分组,而不是用不够直观的数字编号,一些语言提供了命名分组,可以将它看做另一种捕获分组,但是标识是容易记忆和辨别的名字,而不是数字编号。

在Python中,使用(?P<name>...)来分组,其中name是赋予这个分组名字,P必须大写举个例子:

  • 字符串:2019-08-28
  • 字符串:2020-01-01
  • 表达式:(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})
  • 分组名:      year              month            day
>>> import re
>>> regex = r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})"
>>> result = re.search(regex, "2020-12-12")
>>> result.group("year")
'2020'
>>> result.group("month")
'12'
>>> result.group("day")
'12'

虽然分组有了名字,但是数字计数的名字仍然可以使用

如果使用了命名分组,在表达式中反向引用时,必须使用(?P=name)的写法,而要进行正则表达式的替换,则需要写作\g<name>,其中的name是分组的名字。

>>> import re
>>> re.search(r"^(?P<char>[a-z])(?P=char)$", "aa") != None
True
>>> re.sub(r"(?P<digit>\d)", r"\g<digit>0", "123")
'102030'

3.6 非捕获分组

目前为止,介绍了括号的三种用途:

  • 分组:将相关元素归拢到一起,构成单个元素
  • 多选构造:规定可能出现的多个子表达式
  • 引用分组:将子表达式匹配的文本存储起来,供之后引用

这些功能是耦合的,有时候不需要引用,括号也会进行引用分组,浪费了性能,所以正则表达式提供了非捕获分组

它和普通的捕获分组很类似,只是在开括号后紧跟一个问号和冒号(?:...),这样的括号叫做非捕获型括号。这样引用分组会按照从左到右计数编号,不过,会略过这种非捕获型括号

>>> import re
>>> re.search(r"(\d{4})-(\d{2})-(\d{2})", "2010-12-22").group(2)
'12'
>>> re.search(r"(?:\d{4})-(\d{2})-(\d{2})", "2010-12-22").group(2)
'22'

一般只使用多选结构的功能时,最好使用非捕获型括号

3.7 括号的转义

这与前面的元字符不同,与括号有关的三个元字符()|都必须转义

4. 断言

正则表达式中的大多数结构匹配的文本会出现在最终的匹配结果中(一般用group(0)可以得到),但是也有些结构并不真正匹配文本,而只负责判断在某个位置左/右侧的文本是否符合要求,这种结构被称为断言。常见的断言有三类:单词边界、行起始/结束位置、环视。

4.1 单词边界

符号是\b,顾名思义,它匹配的是“单词边界”的位置,而不是字符。也就是说,\b能匹配这样的位置:一边是单词字符,另一边不是单词字符。

下面的表格更详细的说明了\b的用法:

字符串
\brow\b
\brow
row\b
tomorrow
brown
row
rowdy
表达式
说明
只能是单词row\b的右侧是单词字符,所以左侧不能是单词字符\b的左侧是单词字符,所以右侧不能是单词字符

①单词字符只能出现在一侧,具体左边还是右边无所谓

②规则要求一边是单词字符,另一边不是单词字符,故另一边可以是非单词字符,也可以什么都没有

③一般而言,单词字符\w只能匹配[0-9a-zA-Z_],所以用\b\w+\b就能准确匹配英文单词了

>>> import re
>>> re.findall(r"\b\w+\b", "a sentence\tcontains\na lot of words")
['a', 'sentence', 'contains', 'a', 'lot', 'of', 'words']

与单词边界\b对应的还有非单词边界\B,两者的关系类似\s\S\w\W\d\D。在同一种语言中,不论\b是如何规定的,\b能匹配的位置,\B就不能匹配;\B能匹配的位置,\b也不能匹配。

4.2 行起始/结束位置

单词边界匹配的是某个位置而不是文本,在正则表达式中,这类匹配位置的元素叫做锚点,它用来定位到某个位置。常用的锚点有^$,它们分别匹配字符串的开始位置和结束位置,所以可以用来判断“整个字符串能否由表达式匹配”。

4.2.1 行终止符

在编辑本文中,敲下回车键就代表换行,那么在字符串中什么代表一行终止呢?

不同平台下的行终止符
平台行终止符
UNIX/Linux\n
Windows\r\n
Mac OS\n

每一行的起始位置就是行终止符之后的那个位置,后面用*NL*表示行终止符

4.2.2 多行模式

设定多行模式最简单的方法就是在正则表达式之前加上(?m),这里虽然出现了括号,但因为是专用于指定品牌模式,所以不会作为捕获分组。

>>> import re
>>> regex = r"(?m)^\w+"
>>> string = "first line\nsecond line\nlast line"
>>> re.findall(regex, string)
['first', 'second', 'last']

4.2.3 多行匹配

4.2.3.1 ^$

^能匹配字符串开头,以及行终止符的下一个位置

\$能匹配行终止符之前的位置,以及字符串最后一个字符之后的位置

行终止符之前之后的位置只的是字符之间的空间位置,不是字符的索引位置

>>> import re
>>> regex1 = r"(?m)^\w+"
>>> regex2 = r"(?m)\w+$"
>>> string = "first line\nsecond line\n\rlast line\r\n"
>>> re.findall(regex1, string)
['first', 'second']
>>> re.findall(regex2, string)
['line', 'line']

这里注意,\r\n是分开算2个行终止符,不是一个!!

4.2.3.2 \A\Z

多行模式下,用来匹配字符串的开头和结尾的锚点,

>>> import re
>>> string = "first line\nsecond line\rlast line"
>>> regex1 = r"(?m)\A\w+"
>>> regex2 = r"(?m)\w+\Z"
>>> re.findall(regex1, string)
['first']
>>> re.findall(regex2, string)
['line']

4.3 环视

什么是环视?简单而言,在某个位置向左或向右看必须出现或不能出现某类字符

环视的分类
名字记法判断方向结构内表达式匹配成功的返回值
肯定顺序环视(?=...)向右True
否定顺序环视(?!...)向右False
肯定逆序环视(?<=...)向左True
否定逆序环视(?<!...)向左False

True表示出现要求的字符组才匹配,False表示不是给定的字符组才匹配

>>> import re
>>> regex = r"^<(?!/)[^>]+(?<!/)>$"
>>> re.search(regex, "</>") != None
False
>>> re.search(regex, "<u>") != None
True
>>> re.search(regex, "<br/>") != None
False
>>> re.search(regex, "<font color=blue>") != None
True

环视有一个很重要的用途,就是避免编写正则表达式时“牵一发而动全身”的尴尬——既可以集中关注某个部分,添加全局性的限制,又不会干扰其他部分的分配。

环视的另一点价值在于,提取数据时杜绝匹配错误,比如提取邮政编码,手机号的子串也满足,这时候用环视限定一下就可以杜绝这种错误。

4.4 环视的组合

环视因为不改变位置,要对同一位置有多个要求时,可以连着写几个环视,相当于and操作,如果想做相当于or操作,则相当于表达式多选结构的语法

4.5 断言和反向引用

断言不匹配任何字符,只匹配位置;而反向引用只引用之前的捕获分组匹配的文本,之前捕获分组中锚点表示的位置信息,在反向引用时不会保留下来。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值