关于正则表达式

这篇文章总结一下正则表达式的相关内容。

1. 什么是正则表达式

如果你有一个问题,想用正则表达式来解决,那么你就有两个问题了。:D

这句玩笑话道出了正则学习的不易。学习C语言时看到ifelse这些还能从字面意思上猜出它是干啥的,但是看到((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模块

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值