概念
正则表达式(regular expression)描述了一种字符串匹配的模式(pattern),可以用来检查一个串是否含有某种我们所需要的子串、将匹配的子串替换或者从某个串中取出符合某个条件的子串等。
相信做后台的一定经常会碰到,用正则表达式匹配某个字符是否符合规则
例如: 给定一个字符串 我学习过 PHP、Python、Java、Golang
,我如何提出我学过的所有语言
我们除了用语言自带的字符串处理的函数,但是很非常麻烦。
利用我们的正则就可以很好的解决这个问题
正则表达式: [a-zA-Z]+
我们在 python 中运行一下
import re
line = "我学习过 PHP、Python、Java、Golang"
reg = "[a-zA-Z]+"
match = re.findall(reg,line)
print(match)
# 输出 ['PHP', 'Python', 'Java', 'Golang']
相比我们用字符串处理函数来做无疑方便了很多
那个这个表达式是按照什么样的规则来实现的呢?
基本语法
分隔符
首先我们介绍一下'/'
的作用,他是一个分隔符,当然你可以选择任何非字母、数字、\
或空格的字符来作为分隔符,只是最常见的做法是'/'
。
如果你要在正则表达式里面用'/'
你可以在它前面加上转义字符'\'
。
例如:
http:\/\/
用来匹配我们这里匹配我们的http://
字符,我们这里用\
来转义我们的/
字符类和类型
使用我们的字符集合可以使得正则表达式的能力立即超过精确匹配表达式的能力。
例如:我们想查询 以at
结尾的三个字母组成的单词。我们可以编写正则表达式如下
.at
我们这里的 .
就代表匹配一个除了 \n
的通配符。 可以匹配 cat mat sat dat
等等字符
但这样的正则也能匹配注 #at @at
之类的字符。这时我们就要限制第一个字符为a-z或者A-Z
我们可以这样写 [a-z]at
任何包含在方括号中的内容都是一个字符类。只匹配一个字符(敲黑板 重点)
例如我们 [aeiou]
就表示匹配一个为元音的字符。
[a-zA-Z] //可以用代表大小写的任意字母
[^a-z] //注意当^在方括号里面时,表示的是除了a-z之外的字符.如在方括号外面则后面的规则在字符串开始匹配
重复
因为[]
只可以匹配一个字符,如果有的时候你希望指定字符或字符类多次出现的时候该怎么办呢?
在这里我们引入了重复的概念
*
表示这个模式可以重复0次或者多次(注意这个会匹配空字符)
+
表示这个模式可以重复1次或多次
?
表示这个模式可以出现1次或0次
例如 [a-zA-Z]+
表示至少有一个字母
子表达式
把一个表达式分成几个子表达式是有用的,例如,你可以用上面的重复来匹配一个或多个需要匹配的模式。
我们可以用**园括号()**来表示
例如:
(very )*large
因为*表示可以匹配0次或多次 可以匹配“large” "very large" "very very large"
等
子表达式计数
对于上面说的子表达式重复问题,我们还可以用另外一种方式来解决。、
(very ){1,3}
我们这里表示可以匹配“very ”,”very very “,”very very very ”
其中“{}”表示指定子表达式允许重复的次数
“{1}”代表重复一次
“{1,3}”代表重复一到三次
“{1,}”未指定最大范围的话 代表至少重复1次
定位到字符串的开始或末尾
定位到字符串开始的符号 ^
,前面讲到的^
不是说这个是取反的意思吗?
对的,当^
这个字符在我们的方括号里时,表达的时取反的意思
但当这个^
在方括号外面是,表达的是从字符串开始匹配
例如: ^bob
这将在字符串一开始匹配 bob这个字符
定位到字符串开始的符号”$” 和定位到结束符号是一样的。
例如: bob$
代表从字符串末尾开始匹配 bob这个字符
^[a-z]$
代表,至少两头是a-z的字符串或者字符。
分支
正则表达式可以用|
来表示模式选择。例如,我要匹配com、edu、net 你可以用如下表达式
com|edu|net
元字符一览
模式
再讲三种模式之前我们先来讲一讲 正则表达式的引擎
正则表达式引擎
正则引擎主要可以分为基本不同的两大类:一种是DFA(确定型有穷自动机),另一种是NFA(不确定型有穷自动机)。简单来讲,NFA 对应的是正则表达式主导的匹配,而 DFA 对应的是文本主导的匹配。
DFA从匹配文本入手,从左到右,每个字符不会匹配两次,它的时间复杂度是多项式的,所以通常情况下,它的速度更快,但支持的特性很少,不支持捕获组、各种引用等等;而NFA则是从正则表达式入手,不断读入字符,尝试是否匹配当前正则,不匹配则吐出字符重新尝试,通常它的速度比较慢,最优时间复杂度为多项式的,最差情况为指数级的。但NFA支持更多的特性,因而绝大多数编程场景下(包括java,js),我们面对的是NFA。以下面的表达式和文本为例,
text = ‘after tonight’ regex = ‘to(nite|nighta|night)’
在NFA匹配时候,是根据正则表达式来匹配文本的,从t开始匹配a,失败,继续,直到文本里面的第一个t,接着比较o和e,失败,正则回退到 t,继续,直到文本里面的第二个t,然后 o和文本里面的o也匹配,继续,正则表达式后面有三个可选条件,依次匹配,第一个失败,接着二、三,直到匹配。
而在DFA匹配时候,采用的是用文本来匹配正则表达式的方式,从a开始匹配t,直到第一个t跟正则的t匹配,但e跟o匹配失败,继续,直到文本里面的第二个 t 匹配正则的t,接着o与o匹配,n的时候发现正则里面有三个可选匹配,开始并行匹配,直到文本中的g使得第一个可选条件不匹配,继续,直到最后匹配。
可以看到,DFA匹配过程中文本中的字符每一个只比较了一次,没有吐出的操作,应该是快于NFA的。另外,不管正则表达式怎么写,对于DFA而言,文本的匹配过程是一致的,都是对文本的字符依次从左到右进行匹配,所以,DFA在匹配过程中是跟正则表达式无关的,而 NFA 对于不同但效果相同的正则表达式,匹配过程是完全不同的。
回溯陷阱
由于 NFA 引擎支持很多的匹配模式,他是所以编程语言中常用到的正则引擎,这就会引发我们经常遇到的回溯陷阱
回溯陷阱是怎么回事呢?
例如我写一个正则表达式 ab{1,3}c
,检测字符串abbbc
的过程中就不会出现回溯陷阱,因为我最多需要3个b
。假如我们检测abbc
字符串,那么检测流程是
1 a
2 a b
3 a b b
4 a b b c(这里想匹配我们的第三个 b ,把 c 和 b 做对比)
5 a b b (发现 c 和 b 不匹配 所以 c 出栈)
6 a b b c (正常匹配 c 字母)
这样就会多匹配几次,但你可能会说,其实这对我们性能来说也没什么大的影响,毕竟只是多执行了一行指令。
那么如果我换个例子呢?
ab{1,3}c
来检测 abbabc
同学们可以自己算一算。这样回溯位就比较多了
##### 回溯引用
回溯引用是通过一个反斜杠加上一个数字来表示。它用来匹配多次出现在一个字符串中的相同子表达式。
例如^([a-z]+) \1
可以匹配 “aab aab” 不可以匹配”aab abb”
([a-z]+)
表示一个或多个字母字符。
\1
也就是表示对前面匹配的子表达式的回溯引用,数字代表引用次数。
贪婪模式
顾名思义,在该模式下正则会竟可能的匹配尽可能多的内容
正则: [0]+
字符串: 10010000
例如:
- ?: 告诉引擎匹配前导字符0次或一次。事实上是表示前导字符是可选的。
- +: 告诉引擎匹配前导字符1次或多次。
- *: 告诉引擎匹配前导字符0次或多次。
- {min, max}: 告诉引擎匹配前导字符min次到max次。min和max都是非负整数。如果有逗号而max被省略了,则表示max没有限制;如果逗号和max都被省略了,则表示重复min次。
默认情况下,这个几个特殊字符都是贪婪的,也就是说,它会根据前导字符去匹配尽可能多的内容。这也就解释了为什么在第3部分的例子中,第3步以后的事情会发生了。
最后需要特别指出的是,正则匹配默认是贪婪匹配,也就是匹配尽可能多的字符。举例如下,匹配出数字后面的0:
re.match(r'^(\d+)(0*)$', '102300').groups()
('102300', '')
由于\d+采用贪婪匹配,直接把后面的0全部匹配了,结果0*只能匹配空字符串了。
必须让\d+采用非贪婪匹配(也就是尽可能少匹配),才能把后面的0匹配出来,加个?就可以让\d+采用非贪婪匹配:
re.match(r'^(\d+?)(0*)$', '102300').groups()
('1023', '00')
慵懒模式
匹配到最短就停止
独占模式
同贪婪模式一样,独占模式一样会匹配最长。不过在独占模式下,正则表达式尽可能长地去匹配字符串,一旦匹配不成功就会结束匹配而不会回溯
实战训练
邮箱的实战训练
邮箱的格式为
(字母、数字、下划线、点、连接符构成的字符)@(字母、数字、连字符组成的字符).(字母、数字、点、连接符构成的字符)
我们把邮箱的验证分成5个部分
那么正则表达式我们就该这样写
我们先验证第一个部分
正则:^[a-zA-Z0-9_\-\.]+
“+”代表匹配一次或多次。
第二部分
正则:@
第三部分
正则: [a-zA-Z0-9\-]+
第四部分
正则 .
第五部分
由于我们要在最后验证该规则,所以要用$符号
正则:[a-zA-Z0-9\-\.]+$
所以最后的结果为
^[a-zA-Z0-9_\-\.]+@[a-zA-Z0-9\-]+.[a-zA-Z0-9\-\.]+$
###码字不易 如果觉得有用,请点个赞