这篇文章总结一下正则表达式的相关内容。
1. 什么是正则表达式
如果你有一个问题,想用正则表达式来解决,那么你就有两个问题了。:D
这句玩笑话道出了正则学习的不易。学习C语言时看到if
、else
这些还能从字面意思上猜出它是干啥的,但是看到((25[0-5]|2[0-4]\d|[0-1]\d{2}|[1-9]?\d)\.){3}((25[0-5]|2[0-4]\d|[0-1]\d{2}|[1-9]?\d))
这些东西的时候你知道它是干嘛的吗??恐怕大多数人的第一反应是懵逼吧。
虽然正则看起来简直像鬼符,但它的威力不容小觑。上面的这条正则表达式的作用是匹配任意的IP地址(别问我为什么不匹配邮箱,人生的一大错觉就是觉得自己能写出匹配有效邮箱的正则╮(╯▽╰)╭)。试想生活中有这样的场景,你需要在一个长达几百页的文档中找出所有的有效IP地址,你该怎么做?搜索?输入112.0.0.0这样的具体地址只能一个一个的找,整篇文档找完还不得累死。即使你的工具比较高级,可以匹配任意的数字和点号,那333.333.33.33这样的无效IP怎么排除?
正则表达式(Regular Expression)的出现就是为了解决类似于这样的问题的,它可以根据用户指定的规则来匹配某个符合句法的字符串。正则表达式通常用来检索、替换符合要求的文本。
2. 如何使用正则表达式
既然正则这么强大,我们当然要花时间去学习一下啦。别被上面那个长长的鬼符吓倒,再复杂的正则也是简单地句法拼凑起来的。”从战略上藐视敌人,战术上重视敌人”。
2.1 元字符
现在有这么一段文字:
我的IP地址是:225.6.6.6,我的电话是0755-1234567。My IP address is 225.6.6, my phone number is 0755-1234567. We are happy to see you. Welcome to China.
现在我们要把文中的所有数字找出来,怎么做?用正则很简单,\d
就代表了任意数字。用它就可以匹配出文中的所有数组。而类似于\d
这样匹配限定条件下任意字符的字符就是元字符。
常用的用于匹配字符的元字符:
元字符 | 含义 |
---|---|
\d | 匹配任意的数字 |
\w | 匹配任意的字母、数字、下划线、汉字 |
\s | 匹配任意的空白字符(空格、制表符、换行符) |
. | 匹配任意的字符(除换行符外) |
除了用于匹配字符,还有一类元字符可以匹配位置:
元字符 | 含义 |
---|---|
\b | 匹配单词的开头或结尾 |
^ | 匹配字符串的开头 |
$ | 匹配字符串的结尾 |
现在实践一下,我要匹配上面文本中的单词we,该怎么做?直接查找we就好,但是这样welcome中的we也会被包含进来。这时我们就可以用\bwe\b
来查找,\b
保证了匹配的一定是单词的开始或者结尾。
\w
可以匹配字母,那我如果要匹配任意的英文字母呢?\w
会把中文也匹配进去。这时我们可以用字符集合。
2.2 字符集合
用[ ]括起来的可以表示需要查找的字符集合。例如我们要匹配0-5的数字,大于5的舍弃,我们可以这么写:[012345],匹配元音字母:[aeiou]。当你要匹配的字符是连起来的时候可以写成[开始字符-结束字符],不必把每个字符都列出来,比如上边的[012345],可以写成[0-5]。
有了字符集合我们就可以匹配所有的英文字母~,只需要[a-zA-Z]就可以匹配所有的大小写英文字母。
可是问题又来了,如果我想要匹配特定长度,比如只匹配五个字母的单词呢,该怎么做?利用正则表达式的重复功能可以轻松加解决这个问题。
2.3 重复正则表达式
限定符 | 重复次数 |
---|---|
? | 重复零次或一次 |
* | 重复零次或多次 |
+ | 重复一次或多次 |
{m} | 重复m次 |
{m,n} | 重复m到n次 |
{m, } | 至少重复m次 |
有了重复我们就可以很轻易的匹配文本中的所有英文啦,只需要\b[a-zA-Z]{5}\b
就可以匹配五个字母长度的英文单词啦。注意这里我们在正则表达式的前后都加上了\b
元字符,保证只匹配长度为5的单词,而不是”长度大于5的单词”。
2.4 反义
上面的情况描述的都是符合情况下匹配,但有时我们还会遇到否定匹配的情况,例如我要匹配所有的非英文。利用反义可以解决这个问题。
反义字符 | 含义 |
---|---|
\W | 匹配任意非字母、数字、下划线、汉字的字符 |
\D | 匹配任意非数字的字符 |
\S | 匹配任意非空白符的字符 |
\B | 匹配任意非单词开头结尾的字符 |
其实就是把对应的元字符给大写,就是表示的相反的含义。除了现有的反义元字符,我们还可以自定义反义的情况。格式是: [^ 需要反义的字符]
。
比如我要匹配文本中的所有非英语单词,只需要这么写: [^a-zA-Z]+
就好啦。
2.5 分支和转义
如果我们要匹配元字符、限定字符等这些已经被正则表达式识别为特殊字符的字符,我们就需要用到转义。转义只需要在需要转义的字符前加上\
(backslash)就行,这点和大多数的编程语言是一样的。
例如我们要匹配”1+1”,需要写成1\+1
。
匹配的过程中有时会有多种情况,满足一种就要匹配的情况,这时就需要用分支。”|”表示分支,前后的两种情况满足其一就匹配。
例如匹配谷歌的网址,”http://google.com“、”https://google.com“、”http://www.google.com“和”https://www.google.com“都是合法的。我们可以这样匹配:\bhttp[s]?://(|w{3}\.)google.com\b
\b
保证我们的网址前后都有空格,不是某个句子的一部分;http[s]?
保证不管是http协议还是https都能正确识别,(|w{3}\.)
保证有无www都可以被识别。
注意这里用到了圆括号,圆括号可以将表达式分组。
2.6 分组
分组是正则表达式中很重要的一个概念。我们可以用重复限定符来重复单个字符,而分组允许我们重复符合条件的字符串。分组也叫自表达式,就像上边的例子中,我们把判定有无www.的条件与前后分割开,相当于创建了一个”隔离”的环境,这样匹配结果才能与前后的结果结合起来。
分组使用的方法就是圆括号: (exp)
。这样正则表达式会自动捕获分组。
分组的命名
分组被捕获后会有一个默认的名称,第一个被捕获的分组是1号,第二个是2号,以此类推。捕获分组编号为零的捕获是由整个正则表达式模式匹配的文本。
我们也可以自己为捕获的分组命名,格式为:(?<name>exp)
,也可以写成(?'name'exp)
。
引用分组
分组有了名称我们就可以通过名称来引用它。比如这样的表达式:((\d{1,33})\.){3}\d{1,3}
,里面出现了两个分组,我们可以将它改写为:((\d{1,3})\.){3}\2
,用转义后的2表示我们引用第二个分组。如果你的分组是自己命名过的,引用格式为\k<you_re_name>
。
2.7 贪婪模式与懒惰模式
正则表达式默认的是”贪婪”模式,也就是说它会尽可能多的匹配符合要求的字符串。比如对于”helloworld”,我想匹配出”hello”:h\w+o
。对么?不对,因为它的匹配结果是”hellowo”。这时想匹配出”hello”就需要启用正则表达式的”懒惰”模式,让它尽可能少的匹配字符。用?
来开启懒惰模式。h\w+?o
,这样就可以匹配出想要的字符串啦。
2.8 注释
正则表达式也可以进行注释说明,格式为:(?#comment)
。如:(?#这是一条注释)
。
2.9 进阶
// TODO 可选处理选项、零宽断言、平衡组、递归匹配……
3. 运用正则表达式
说了这么多好像一直没有提正则表达式在哪可以用……虽然每个人都可能会碰到需要用正则表达式的情况,但事实是往往只有程序员才会使用正则表达式。所以正则表达式的应用往往是和编程语言结合在一起的。类似于SQL,正则也可以独立使用或者嵌入的在编程语言中应用。
如果是简单地文本查找/替换,很多编辑器都为正则提供了支持,如Sublime Text、VS Code等等,可以将文本粘贴进去,按下’Ctrl + R’,选择正则模式就可以使用正则表达式进行查找/替换了。
如果是练习正则表达式的书写,可以使用在线或者本地的正则匹配工具。
在线:正则表达式在线匹配
本地:Regex Match Tracer,下载地址:
4. Python和正则表达式
最后说一下Python里正则表达式的运用。Python的正则表达式支持由re模块提供,在写代码前需要先import re
。
4.1 re.match(pattern, string[,flag])
这个函数有三个参数,其中flag参数可选的。第一个参数pattern指定匹配规则,第二个参数string是需要匹配的字符串。match函数从string的开头开始匹配,如果匹配到结果,立即返回,若到达string末尾仍未找到匹配则返回None。
to_match = "Hello, 2018!"
result = re.match("\w+", to_match)
print(result.group())
# Hello
注意re.match返回一个Match对象,获取值时需要用group()
方法取出来。
4.2 re.findall(pattern, string[,flags])
上边的匹配结果符合预期么?\w+
不应该把字母和数字都匹配了么?为什么数字没有匹配?那是因为re.match()
方法的特性导致的,re.match()
匹配到结果后立即返回,所以当它匹配到Hello后遇到逗号,发现没有匹配,于是立即返回了结果。如果我们需要找到字符串中的所有匹配就需要用re.findall()
方法。
该方法参数同re.match()
方法一样,但是会以列表形式返回所有的匹配对象。
to_match = "Hello, 2018!"
result = re.findall("\w+", to_match)
print(result)
# ['Hello', '2018']
4.3 re.finditer(pattern, string[, flags])
该方法类似于re.findall()
只不过findall方法返回的是列表,而该方法返回迭代器。示例如下:
to_match = "Hello, 2018!"
result = re.finditer("\w+", to_match)
print(result) # <callable_iterator object at 0x06D515D0>
for i in result:
print(i.group())
# Hello
# 2018
4.4 re.search(pattern, string[,flags])
该方法和re.match()
较为相似,不同的地方我们通过一个例子来展示。
to_match = "Hello, 2018"
match_result = re.match("\d+", to_match)
search_result = re.search("\d+", to_match)
print(search_result.group())
print(match_result.group())
# 2018
# AttributeError: 'NoneType' object has no attribute 'group'
用re.match()
没有匹配到任何对象,所以返回了None,尝试在None上边调用group()
方法当然会报错。结果的差别是两种方法匹配方式的不同导致的。re.match()
方法从字符串开始匹配,如果起始位置没有匹配成功的话,re.match()
就会返回None 。而re.search()
方法则会查找整个字符串寻求匹配。
4.5 re.split(pattern, string[,maxsplit])
这个方法和前边的有点不太一样,前边都是用来查找字符串,而这个方法用来切割字符串。
Python为str
对象内置了split()
方法,可以方便的进行字符串的分割。比如:
astr = "Hello World"
astr.split()
# ["Hello", "World"]
但是字符串的split()
方法功能有限,当我们想要自定义复杂的切割条件的时候就无能无力了,这时就应该使用re.split()
方法。该方法的前两个参数和上述几个方法完全相同,不再赘述。第三个参数指定了最大切割数量,若不指定则全部分割。
to_split = "H1e2l3l4o5"
result1 = re.split("\d+", to_split)
result2 = re.split("\d+", to_split, 2)
print(result1) # ['H', 'e', 'l', 'l', 'o', '']
print(result2) # ['H', 'e', 'l3l4o5']
4.6 re.compile(string, [,flags])
假设这样一种情况,我们需要进行许多次匹配,而匹配的模式都是相同的,难道需要每次都在pattern参数那里写一遍么?当然不是。Python提供了re.complie()
方法,可以将正则表达式字符串转化为pattern对象,这样以后每次调用只需要将转化后的对象当做参数即可。
pattern = re.compile("\d+")
result1 = re.split(pattern, to_split)
4.7 flags参数
前边几种方法除了re.split()
外都有一个可选的flags参数,那么这个参数有什么作用呢?
flags参数指定了”匹配模式”。有以下几个可选值:
- re.I:忽略大小写
- re.M:多行模式,改变”^”和”$”的行为
- re.S:点任意匹配模式,改变”.”的行为
- re.X:详细模式。正则可以是多行,忽略空白字符,并且可以插入注释
- re.L:使预定字符类\w\W\b\B\s\S取决于当前区域设定
- re.U:使预定字符类\w\W\b\B\s\S取决于Unicode定义的字符属性
既然参数名字叫”flags”而不是”flag”,它自然是可以多个模式一块使用的,多个模式利用”|”分开就好。例如”re.I | re.M”。
4.8 反斜杠问题
正则表达式利用”\”来转义字符使普通字符变为特殊字符,而Python和大多数编程语言也是使用的这样的转义模式。这就会导致出现这样的情况:当你想要匹配”\”本身时,需要写四个”\”。为了解决这个问题,Python提供了对原生字符串(raw string)的支持。只需要在普通字符串前加上’r’就可以将字符串转义为原生字符串,忽略转义的问题。所以匹配”\”时,直接写成r”\”就好。推荐在进行匹配时都加上”r”将转义字符串变为原生字符串。
4.9 使用re.sub()和re.subn()替换字符串
re.sub(pattern, repl, string[, count])
该方法用repl替换string中匹配到的值并返回替换后的字符串。可选参数count指定最多替换次数,默认值为全部替换。
s = "Hello, 2018, 2018"
p = re.compile(r"\d+") #前边说过了建议所有的匹配模式都加上"r"转化为原生字符串
result1 = re.sub(p, "2019", s)
result2 = re.sub(p, "2019", s, 1)
print(result1, result2)
# Hello, 2019, 2019 Hello, 2019, 2018
re.subn(pattern, repl, string[, count])
该方法用法和re.sub()
相同,但是返回元组形式的(替换后字符串,替换次数)。
s = "Hello, 2018, 2018"
p = re.compile(r"\d+")
result = re.subn(p, "2019", s)
print(type(result)) # tuple
print(result)
# ('Hello, 2019, 2019', 2)
4.10 Match对象
说一下Match对象。前边在re.match()
那里说该方法会返回一个Match对象,取值时要用Match对象的group()
方法取出来。其实所有用到了group()
方法取值的都是Match对象。而Match对象还有很多个属性和方法方便我们取值。还是拿代码来说明:
to_match = "Hello, 2018"
match_result = re.match("\d+", to_match) # 现在match_result是一个Match对象
print(match_result.re) # 匹配用的正则表达式值
print(match_result.string) # 匹配时用的文本
print(match_result.pos, match_result.endpos) # 匹配开始搜索时的索引和结束时的索引
print(match_result.group()) # 获得一个或多个分组截获的字符串,可以指定参数(group1, ...)参数为正则表达式分组的索引或者你为分组起的别名。传入多个参数时以元组形式返回。无参数时返回group(0),既所有匹配到的结果
print(match_result.groupdict()) # 返回值为字典,以有别名的组的名称为键,截获的字符串为值
最后提一点,如果我们提前用re.compile()
获取了pattern对象,那么也可以直接在pattern对象上调用这些方法,传入参数时不传pattern参数即可。如re.match(string[,flags])
。(我不喜欢这么用,所有放到最后才提了一下╮(╯▽╰)╭)
上边的内容包含了Python正则的大多数常用功能,但如果你想掌握更详细的内容的话,推荐阅读Python标准库re模块的说明:re模块。