正则表达式介绍
前言
在日常的开发中,我们常常需要对一些字符串进行验证,例如验证玩家创角的密码是否包含字母、数字、特殊符号,输入的手机号、身份证号码是否正确等等。如果利用遍历字符串的每个字符,来判断是否符合我们想要的条件话,可能需要写一长串繁琐的逻辑代码。但是若使用正则表达式则可以很简单的通过一串特殊定义的字符串来实现我们对字符串的逻辑判断。
假设我们要验证用户注册的密码,有下列条件:
- 长度6-9位
- 必须包含数字,字母,特殊符号
- 特殊符号只包含@_&三种
如果用传统的方法判断,可能就是先验证长度,然后遍历每个字符,找到数字,字母,特殊符号的时候分别做一个标记,若有字符不匹配则直接返回false。遍历结束对应的标记都有的话就返回true。
接下来我们看看用正则表达式的话,怎么来解决这个问题:
Regex pwdRegex = new Regex("^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[@_&])[a-zA-Z0-9@_&]{6,9}$");
string pwdString1 = "Q1234567"; //False
string pwdString2 = "qw23@"; //False
string pwdString3 = "qw1REs@2"; //True
string pwdString4 = "qWE@123@33"; //False
string pwdString5 = "qWE#3@3"; //False
Debug.Log($"pwdString1:{pwdString1} isMatch:{pwdRegex.IsMatch(pwdString1)}");
......
只需要一个表达式"^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[@_&])[a-zA-Z0-9@_&]{6,9}$"以及Regex类便实现了我们需要的验证。除此以外,我们还可以通过正则表达式来实现替换文本,或者提取子字符串的功能。
这个表达式看着挺复杂的?那就让我们先来了解下正则表达式最基础的构造吧。
菜鸟教程:https://www.runoob.com/regexp/regexp-syntax.html
在线工具:http://c.runoob.com/front-end/854
正则表达式
正则表达式是由普通字符(例如字符 abcd 0123等)以及特殊字符(称为"元字符",例如 [] () 等)组成的文字模式,描述在搜索文本时要匹配的一个或多个字符串。正则表达式作为一个模板,将某个字符模式与所搜索的字符串进行匹配。一般的格式如下:
/pattern/flags
其中pattern即代表我们的正则表达式,左右由两个 / 包围,而后面的flags我们称之为修饰符,可以定义一些匹配策略,例如,是否忽略大小写,是否全局查找等。
普通字符
普通字符包括除了元字符外的所有可打印和不可打印字符。这包括所有大写和小写字母、所有数字、所有标点符号和一些其他符号。
我们先从一些简单的例子来了解正则表达式,假设我们有一段字符串如下:
<div>apple: iphone8 iphoneX iphone12</div>
如果我们要从中找出所有iphone的单词,那么我们的正则表达式可以如下:
/iphone/g
其中iphone就是我们的普通字符组成的正则表达式,代表着从字符串中搜索和其相同的子串。而g则是我们的修饰符,代表搜索所有的匹配项。因此这个通过这个正则表达式,我们可搜索到字符串中的三个iphone子串。
非打印字符
例如我们所知道的换行符\n,制表符\t都属于非打印字符串,具体种类如下:
\cx | 匹配由x指明的控制字符。例如, \cM 匹配一个 Control-M 或回车符。x 的值必须为 A-Z 或 a-z 之一。否则,将 c 视为一个原义的 'c' 字符 |
\f | 匹配一个换页符。等价于 \x0c 和 \cL |
\n | 匹配一个换行符。等价于 \x0a 和 \cJ |
\r | 匹配一个回车符。等价于 \x0d 和 \cM |
\t | 匹配一个制表符。等价于 \x09 和 \cI |
\v | 匹配一个垂直制表符。等价于 \x0b 和 \cK |
\s | 匹配任何非打印(空白)字符,包括空格、制表符、换页符等等。等价于 [\f\n\r\t\v]。注意 Unicode 正则表达式会匹配全角空格符 |
\S | 匹配任何打印(非空白)字符。等价于 [^ \f\n\r\t\v] |
例如我们有下列字符串:
qwe
123
12e
rre
当我们使用下面正则的时候,一共可以搜索到到2个匹配项,因为前三行的行末都有我们看不见的回车符,也就是非打印字符\n。
/e\n/g
元字符
除了普通字符外,还有很多具有特殊意义的字符,我们称之为元字符。具体类别如下:
. | 匹配除换行符 \n 和 \r 之外的任何单个字符。若要匹配包括 \n 在内的任何字符,可以使用 (.|\n) |
| | 匹配 x 或 y。例如,z|food 能匹配 z 或 food。(z|f)ood 则匹配 zood 或 food |
[] | 字符集合。匹配所包含的任意一个字符。例如 [abc] 可以匹配 app 中的 a |
^ | 负值字符集合,搭配 [] 使用,代表除...之外(否则为定位符,后续会介绍到)。例如 [^abc] 可以匹配 app 中的 p,即除 abc 之外的任意字符。 |
- | 字符范围。匹配任何在指定范围内的任意字符。例如 [a-z] 可以匹配任何在 a 到 z 范围内的任意字符 |
() | 标记一个子表达式的开始和介绍位置 |
\w | 匹配字母、数字、下划线。等价于 [A-Za-z0-9_] |
\W | 匹配非字母、数字、下划线。等价于 [^A-Za-z0-9_] |
\xn | 匹配 n,其中 n 为十六进制转义值。十六进制转义值必须为确定的两个数字长。例如 \x41 匹配 A(ASCII码中十六进制41对应着字符 A)。\x041 则等价于 \x04 和 1。正则表达式中可以使用 ASCII 编码 |
\num | 即 \ 后面跟着阿拉伯数字,例如 \1 \11 \111等。共有两种含义: 1.反向引用:num是十进制正整数,并且 \num 之前至少 num 个匹配到的子字符串。例如 (.)\1 可以匹配两个连续的相同字符。(后续会有更详细的介绍) 2.如若上面条件不满足,且每位阿拉伯数字都是0-7,那么将作为标识一个八进制转义值,类似之前的十六进制转义, \101 匹配 A |
\un | 匹配 n,其中 n 是一个用四个十六进制数字表示的 Unicode 字符。例如, \u00A9 匹配版权符号 ? |
除了以上这些,还有限定符,定位符等,会在后续介绍到,它们也都属于元字符。
[ ]
中括号算是最常见的元字符了,可以匹配所包含的任意一个字符。例如 [abc],可以匹配字符串中任何一位是a或者b或者c的字符。
看下面这个例子,依旧是之前的字符串:
<div>apple: iphone8 iphoneX iphone12</div>
我们这次想匹配除了iphone以外,把后面跟着的型号也带上去。此时 [ ] 就可以很好的帮助我们。
/iphone[8X]/g
这样我们就可以匹配到iphone8和iphoneX两个子串了,但是要同时匹配iphone12就毕竟麻烦了,因为12是两个字符,一个 [] 只能匹配一个字符,后续我们可以通过限定符还轻松的解决这个问题。
-
上面的正则只能匹配8和X,那如果我们还想匹配4567这些型号的话,虽然可以通过 [45678X]来实现,但是这样过于的繁琐。利用 - 可以帮我们来优化表达式,它代表一个字符范围(范围参照ASCII码),例如45678,我们就可以使用4-8来代替。例如 [0-9] 可以匹配任何阿拉伯数字, [a-z] 可以匹配任何小写字母等。
/iphone[4-8X]/g
上述正则就可以匹配iphone4到8与X的所有型号子串。
^
该符号配合 [] ,代表着除...之外,例如 [^0-9QW] 即除 0到9和Q和W 之外的所有字符。
/iphone[^X]/g
上述正则可以匹配到iphone6
()
()代表着一个子表达式,它就类似于我们的算术运算,如 a * (b + c)。例如
/iphone8|X/g
上述正则匹配到是iphone8和X两个子串。
/iphone(8|X)/g
这样才会匹配到iphone8和iphoneX两个子串。
如果我们有多个(),他们之间的关系是串行的,即有前后顺序,而并非并列关系。
例如下列正则
/iphone(8)(X)/g
它等价于
/iphone8X/g
因此一个子字符串都无法匹配到。我们可以稍加修改,例如
/iphone(8|)(X)/g
或者
/iphone(8?)(X)/g
这样我们可以匹配到iphoneX这个子串,因为 (8|) 可以匹配8或者空字符,(8?) 同样如此。有关 | 与 ? 会在后面进行介绍。
除此之外()搭配一下别的符合还有更多的含义,例如 (?=),将在后续进行更详细的介绍。
|
该符合类似于我们代码中的或运算: if(a | b) ,例如
/apple|iphone/g
可以匹配到apple以及三个iphone的子字符串。
/iphone(8|X|12)/g
可以匹配到iphone8,iphoneX,iphone12三个子字符串。
转义
在平时敲代码时,若我们字符串中要显示的 " ,我们需要通过如下转义来实现
string s = "this is \"xxx\"";
同样的我们的元字符,若想使其代表字符本身的含义,同样需要进行转义,转义符为 \
例如我们有下列字符串
qwe[qwe]
若我们想匹配[qwe]字符串,若使用下面正则
/[qwe]/g
[]会被当做元字符的含义来处理,匹配出来的是字符串中q,w,e,q,w,e六个子字符。因此我们需要进行转义,如下
/\[qwe\]/g
这样就可以匹配出 [qwe] 的子串。
限定符
前面的例子中,我们都是通过 [] 来匹配一个字符,但是如果我们想匹配多个字符,(例如前面遗留下来的问题:如何同时匹配iphone8与iphone12),有什么好的解决方法呢?
限定符可以帮我们解决这类的问题,其符合和含义如下:
* | 匹配前面的子表达式零次或多次。例如,zo* 能匹配 z 以及 zoo。* 等价于{0,} |
+ | 匹配前面的子表达式一次或多次。例如,zo+ 能匹配 zo 以及 zoo,但不能匹配 z。+ 等价于 {1,} |
? | 匹配前面的子表达式零次或一次。例如,do(es)? 可以匹配 do 或 does 。? 等价于 {0,1} |
{n} | n 是一个非负整数。匹配确定的 n 次。例如,o{2} 不能匹配 Bob 中的 o,但是能匹配 food 中的 oo |
{n,} | n 是一个非负整数。至少匹配n 次。例如,o{2,} 不能匹配 Bob 中的 o,但能匹配 foooood 中的所有 o。o{1,} 等价于 o+。o{0,} 则等价于 o* |
{n,m} | m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次。例如,o{1,3} 将匹配 fooooood 中的前三个 o。o{0,1} 等价于 o?。请注意在逗号和两个数之间不能有空格。 |
看了表格,应该就能够理解的差不多了,我们可以通过下面的正则
/iphone[0-9X]+/g
上述正则还好有一个问题,例如,它可以匹配到iphone123,iphoneXX,iphone1X这类的子串,我们可以进行一个简单的优化,即只匹配一位或两位数字: [0-9]{1,2} 和只匹配一个X:[|] 这两者的关系是或的关系,因此我们需要用 | 来关联。
/iphone([0-9]{1,2}|[X])/g
这样就可以避免匹配到上述的字符串,但是还有个问题:虽然iphone123不会匹配到了,但是却还会匹配到iphone123中的iphone12这个子串。后续我们可以通过定位符来解决。
贪婪与非贪婪
在语文中贪婪的意思就是什么都想要,要的很多。而我们的限定符在默认情况下是贪婪的,也就是匹配尽可能的多。
我们来观察下前面例子中的字符串
<div>apple: iphone8 iphoneX iphone12</div>
可以看出整个字符串是包含在<>中的,但是其中的div同样也是包含在<>中的,假设我们有如下正则
/<.*>/
它会匹配到我们整个字符串,因为前面我们说到限定符是贪婪的。那么如果我们想要只匹配<div>,也就是让其变得非贪婪,需要怎么处理呢?
在正则中,我们可以通过在任何一个限定符 (*,+,?,{n},{n,},{n,m}) 后面添加 ? 来使匹配模式是非贪婪的,也就是匹配尽可能的少。例如贪婪模式的 .? 对应非贪婪模式即为 .??
因此我们要匹配<div>的话只需使用如下正则即可
/<.*?>/
注:需要注意的是,在非贪婪的模式后添加字符时,会影响导致非贪婪无效。猜测可能是因为正则默认从左到右的匹配规则导致的(使用 RegexOptions.RightToLeft 则正常)。
例如我们有foood单词,当正则为 fo+? 时,匹配到的时候 fo 。但是当我们的正则为 o+?d 时,匹配到的是 oood 而非 od 。
定位符
在前面我们遗留了一个问题,就是如何修改我们下列的正则
/iphone([0-9]{1,2}|[X])/g
使其在下列字符串中
iphone12 iphone123 iphoneX
只匹配第一个iphone12和iphoneX,而不会匹配到iphone123中的iphone12子串。而iphone123和iphone12的区别在于,前者后面跟着的是字符3,而后者跟着的是空格。
在正则中,定位符可以帮我们来对此进行区分。它能够将正则表达式固定到行首或行尾,或者是在一个单词的开头或者一个单词的结尾。具体有如下几类
^ | 匹配输入字符串的开始位置。若支持多行模式,^ 也匹配 \n 或 \r 之后的位置。 |
$ | 匹配输入字符串的结束位置。若支持多行模式,$ 也匹配 \n 或 \r 之前的位置。 |
\b | 匹配一个单词边界,也就是指单词和空格间的位置。例如, er\b 可以匹配 never to do 中的 er,但不能匹配 verb 中的 er。 |
\B | 匹配非单词边界。er\B 能匹配 verb 中的 er,但不能匹配 never to do 中的 er。 |
因此我们如下修改即可实现我们的要求,在结尾处匹配一个空格
/iphone([0-9]{1,2}|[X])\b/g
^与$
上述两个元字符分别定位在字符串的开始和结束位置。例如在最开始的字符串中,我们想要匹配第一个<div>,可以使用下面正则
/^<.*?>/
若想匹配最后面的</div>使用下列正则即可
/</.*?>$/
需要注意的是,假设如下字符串
<div>a</div>
<div>b</div>
<div>c</div>
当我们使用下面正则的时候
/^<.*?>/g
只能匹配到第一行的<div>,若想要匹配每行的开头,我们需要开启正则的多行支持,我们可以使用修饰符m 来开启,如下
/^<.*?>/gm
这样就可以匹配到三行中,每行行首的<div>了。
捕获与非捕获
在前面我们提到过元字符 () ,代表着一段子表达式。此外它还有另外一层功能:会把子表达式中匹配到的值保存起来,我们称之为捕获分组。
为了更方便的理解,我们来看一段代码:
Regex regex = new Regex("iphone8");
string origin = "<div>apple: iphone8 iphoneX iphone12</div>";
Match match = regex.Match(origin);
if (match.Success)
{
for (int i = 0; i < match.Groups.Count; i++)
Debug.Log($"group[{i}]:{match.Groups[i]}");
}
代码中用到了C#的Regex类(后续会更详细介绍),初始化类时传递的字符串即为我们的正则表达式,origin即为要被匹配的字符串,match.Success代表匹配成功,而我们打印的group的值,即是我们现在要讨论的分组。
此时的打印结果如下:
group[0]:iphone8
当我们的正则匹配到一个结果时,便会将其放入分组当中。
此时我们稍作修改,如下:
Regex regex = new Regex("iphone(8)");
打印的结果为
group[0]:iphone8
group[1]:8
再修改下:
Regex regex = new Regex("ip(.+)(8)");
打印的结果为
group[0]:iphone8
group[1]:hone
group[2]:8
即可发现,每次使用 () 时,匹配到的结果都会被依次写入分组当中。
但是大部分情况下我们并不需要它们,因为这些缓存会造成额外的内存开销,我们可以在每个子串的开始位置添加非捕获元字符 ?: 来消除这种副作用,如下
Regex regex = new Regex("ip(?:.+)(?:8)");
反向引用
前面提到捕获状态下,我们匹配到的值会被存放到缓冲区中,缓冲区编号从 1 开始,最多可存储 99 个。我们可以使用元字符 \ 加上100以内的十进制数来访问这些子表达式。例如:
/(.)\1/g
可以匹配两个相同且连续的字符,需要注意的是,缓存区是针对每次我们正则匹配到的值,因此若我们使用全局匹配,查找到N个相关的子串时,是拥有N个100以内的缓冲区,而并不是所有匹配到的值加起来只有100以内个缓存区。
/([a-z])([0-9])\1\2/g
可以匹配w2w2,q5q5这类的子串,但是不能匹配到q2w1,q2q3这类的。因为当例如前面的正则 ([a-z])([0-9]) 匹配到w2时,\1\2 分别代表的是缓冲区中的 w 和 2,而并非是 ([a-z]) 和 ([0-9])
预查找
我们先来假设下以下一种情况,假如哪天苹果公司突(nao)发(zi)奇(huai)想(le),要把所有iphone4-iphone8系列的手机名称改名为ophone4-ophone8。而iphoneX,11,12这些不变。那么我们应该如何在下列字符串中找出我们需要修改的子串呢?
iphone4 iphone4s iphone12 iphoneX iphone8 iphone7p
匹配所有的iphone?( /iphone/g)明显不行,因为这样会把iphone12这类不需要修改的iphone也修改了。
匹配所有的后面是4-8型号的子串?(/iphone[4-8][sp]?/g)这样也存在着问题,虽然型号找对了,但是找到的都是带型号的子串,例如iphone4,iphone4s,不方便替换。
那么如何才能在型号找对的情况下,只提取iphone子串不带后面的型号呢?预查找,就可以帮我们解决这一类的问题。
预查找属于一个子表达式,因此写在 () 内,预查找会参与匹配,但是预查找的匹配结果不会出现在最后的结果当中。正则中一共有四个预查找的元字符,如下
元字符 | 例句 | 含义 |
?= | pattern1(?=pattern2) | 正向肯定预查,在任何匹配pattern2的字符串开始处匹配pattern1,例如 iphone(?=4) 可以匹配到iphone4中的iphone,但是不能匹配到iphone8中的iphone |
?<= | (?<=pattern1)pattern2 | 反向肯定预查,在任何匹配pattern1的字符串结尾处匹配pattern2,例如 (?<=ios)5 可以匹配到ios5中的5,但是不能匹配到android5中的5 |
?! | pattern1(?!pattern2) | 正向否定预查,在任何不匹配pattern2的字符串开始处匹配pattern1,例如 iphone(?!4) 可以匹配到iphone8中的iphone,但是不能匹配到iphone4中的iphone |
?<! | (?<!pattern1)pattern2 | 反向否定预查,在任何不匹配pattern1的字符串结尾处匹配pattern2,例如 (?<!ios)5 可以匹配到android5中的5,但是不能匹配到ios5中的5 |
因此要实现我们上述说的情况,就可以使用我们的正向预查,正则如下(关于如何替换,可以使用后面将会介绍到的Regex类)
/iphone(?=[4-8][sp]?)/g
同时需要注意的是,上述元字符同样属于非捕获元字符,也就是预查找的值不会给存储到组中。同时预查不消耗字符,例如下面正则
/iphone(?=4)4/
他会匹配的是iphone4子串,而并不是iphone44
pattern1(?=pattern2)pattern3
因此以上正则的匹配顺序即为:匹配pattern2前面的pattern1,然后匹配pattern1后面的pattern3。
这也导致了当多个预查连续使用的情况下,它们都是并行的关系,而非串行,例如
/iphone(?:4)(?:s)/
/iphone(?=4)(?=s)/
前者为串行,会匹配到iphone4s子串,而后者并不会匹配到iphone4s的iphone。若要匹配iphone4s的iphone,我们可以使用如下方法
/iphone(?=4s)/
/iphone(?=4)(?=.s)/
读到这里,再看看文章最开头的有关密码的正则,基本也就可以很好的理解了。
^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[@_&])[a-zA-Z0-9@_&]{6,9}$
前面三个正向查找 ^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[@_&]) 即可理解为,我们的开头后面先要跟着数字,然后要跟着字母,最后要跟着特殊符号。满足之后便是剩下的匹配规则: ^[a-zA-Z0-9@_&]{6,9}$ 开头和结尾中包含6-9位符合要求的字符。
优先级
正则表达式从左到右进行计算,并遵循优先级顺序,与我们的算术运算非常类似。元字符的优先级从高到低如下:
- 转义符:\
- 圆括号,方括号:(), (?:), (?=), []
- 限定符:*, +, ?, {n}, {n,}, {n,m}
- 定位符和普通字符:^, $, \任何元字符、任何字符
- 或操作:|
常用正则
- 汉字:^[\u4e00-\u9fa5]{0,}$
- 英文和数字:^[A-Za-z0-9]+$ 或 ^[A-Za-z0-9]{4,40}$
- Email地址:^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$
- 域名:[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+\.?
- InternetURL:[a-zA-z]+://[^\s]* 或 ^http://([\w-]+\.)+[\w-]+(/[\w-./?%&=]*)?$
- 手机号码:^(13[0-9]|14[5|7]|15[0|1|2|3|4|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$
- 电话号码("XXX-XXXXXXX"、"XXXX-XXXXXXXX"、"XXX-XXXXXXX"、"XXX-XXXXXXXX"、"XXXXXXX"和"XXXXXXXX):^(\(\d{3,4}-)|\d{3.4}-)?\d{7,8}$
- 国内电话号码(0511-4405222、021-87888822):\d{3}-\d{8}|\d{4}-\d{7}
- 电话号码正则表达式(支持手机号码,3-4位区号,7-8位直播号码,1-4位分机号): ((\d{11})|^((\d{7,8})|(\d{4}|\d{3})-(\d{7,8})|(\d{4}|\d{3})-(\d{7,8})-(\d{4}|\d{3}|\d{2}|\d{1})|(\d{7,8})-(\d{4}|\d{3}|\d{2}|\d{1}))$)
- 身份证号(15位、18位数字),最后一位是校验位,可能为数字或字符X:(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)
- 日期格式:^\d{4}-\d{1,2}-\d{1,2}
- 中国邮政编码:[1-9]\d{5}(?!\d) (中国邮政编码为6位数字)
- IPv4地址:((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(\.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3}
PS:由于篇幅较长,有关C#的Regex的相关介绍放到下篇介绍。
本文地址:https://blog.csdn.net/wangjiangrong/article/details/109820029