这篇文章主要是我个人学习正则表达式的回顾与总结,另外还会写一点自己使用过程中的小经验与小技巧,作为自己学习正则表达式的学习笔记。
虽然我在实验室的方向是前端开发,在表单验证中使用正则表达式很频繁,但是我接触并喜欢上正则表达式是在大二暑假参加全国电赛的时候,那个时候我们这一队属于测量方向,经常使用 DA 生成特定波形的电信号,在编写 DA 数据查找表的时候,需要选中很多特定格式的数据并对其进行一些操作,如果一个一个地改,工作量虽然不是特别巨大,但也是够烦的,然后去请教学长有没有什么简单快捷的方式,才知道了有正则表达式这么个工具,但是当时自己也不会用,所以就赖着脸皮请学长帮忙演示了一下,看完以后顿时感觉,WTF!还可以这样?这简直太神奇了!于是下定决心一定要学好正则表达式。从那时到现在差不多一年半的时间,感觉自己学的还不是很到家,但是至少一些简单正则表达式的应用已经可以胜任了,现在就在这里总结一下自己的学习成果。
不同的环境对正则表达式的解释略有不同,我这里主要是应用于 JavaScript 环境,测试一般就直接使用 Sublime Text 的正则条件搜索查看匹配结果与预想的是否相同。另外,我有时也使用可视化工具来帮助理解一些比较繁琐的正则表达式,顺便给大家推荐一个该类应用的网站,可视化效果挺不错的。
Regulex: JavaScript Regular Expression Visualizer
下面进入正题。
元字符
正则表达式中元字符是最常用的,它们在正则表达式中经常被用于匹配各种特殊的字符,或者标识匹配方式,了解并记住它们的作用是很有必要的。
\
用法很多,罗列如下。
1.将紧随其后的下一个字符标记为转义字符,例如n
匹配普通的字母 n,而\n
则匹配一个换行符。
2.将紧随其后的下一个字符标记为原义字符,也就是将正则表达式规定的元字符转换为普通字符,例如匹配反斜杠字符\
,则需要使用\\
,匹配*
,则需要使用\*
,因为\
和*
都是元字符,在正则表达式中具有特定的含义,所以需要用\
将这些转义字符标记为原义字符。
3.向后引用一个捕获分组(捕获分组的解释在后面,这里可以简单的理解为用括号括起来的子表达式),用法为\num
,表示对第 num 个捕获分组向后引用,这个用法较难理解,我们借助几个示例来给大家演示一下。
(1).(a)\1
,即向后引用正则表达式中的第一个捕获分组,也就是(a)(a)
,匹配形如 aabbccdd。
(2).(a)(b)(c)\2
,即向后引用正则表达式中的第二个捕获分组,也就是(a)(b)(c)(b)
,匹配形如 abcdabcbabcd。
(3).(1)(\d)\2
,即向后引用正则表达式中的第二个捕获分组,也就是(1)(\d)(\d)
,但需要注意的是,因为这里是向后引用第二个捕获分组,所以只能匹配形如 123212232211223,也就是说后两个数字必须相同,而不是只需要三个数字的组合,因为这里是对捕获分组的引用。
(4).(a)(\w)\2
,与上个示例基本相同,只不过变成了匹配字母,这里也需要注意是向后引用捕获分组,即要求第二个字母与第三个字母相同,也就是匹配形如 abcabbcbbaabbbcacc。
4.将紧随其后的下一个字符标记为八进制转义符,这个用法我基本上没怎么用过,有兴趣的同学可以自行了解一下。
^
最常见的用法是匹配字符串的开始位置,例如匹配每一行最开始的字母 a,就可以使用^a
;除此之外还有一种用法是反向匹配,也就是排除某些字符串,一般与[]
一起使用,例如[^a]
的含义就是匹配除了字母 a 之外的所有字符,包括显示字符与非显示字符等。
$
匹配字符串的结束位置,例如匹配每一行最后面的字母 a,就可以使用a$
,这个用法与^
的第一个用法效果恰好相反。
*
匹配前面的子表达式零次或多次,例如使用ab*
就可以匹配形如 abbcc 或 acc,前面匹配了两次,后面匹配了零次。
+
匹配前面的子表达式一次或多次,例如使用ab+
就可以匹配形如 abbcc,但是不能匹配形如 acc,因为要求前面的子表达式至少匹配一次。
?
该元字符同样有两种用法,最常用的是匹配前面的子表达式零次或一次,例如使用ab?
就可以匹配形如 abbcc 或 acc;除此之外,当该元字符紧跟在任何一个限制符(具体有*
、+
、?
、{n}
、{n,}
、{n,m}
)后面时,则表示当前匹配模式是非贪婪的,它会尽可能少地匹配符合条件的字符串,而默认情况下以上限制符的匹配模式均是贪婪的,它会尽可能多地匹配符合条件的字符串,例如ab+
将会匹配形如 abbbbbcabbbc,而使用ab+?
则只会匹配形如 abbbbbcabbbc,也就是说贪婪模式追求匹配字符数量的上限,能够匹配多少就匹配多少,非贪婪模式追求匹配字符数量的下限,匹配的字符数量满足要求的最低值便不再向后匹配。
{n}
匹配前面的子表达式确定的 n 次,例如使用ab{2}
就可以匹配形如 aaaabbbbcccc。
{n,}
匹配前面的子表达式至少 n 次,例如使用ab{2}
就可以匹配形如 aaaabbbbcccc,但不能匹配 aaaabcccc,因为这里只能匹配到一次 b。
{n,m}
匹配前面的子表达式至少 n 次,至多 m 次,例如使用ab{2,3}
就可以匹配形如 aaaabbbbcccc,因为至多匹配三次,所以最后的 b 不能被匹配到。需要注意的是,两个数字与逗号之间没有空格,大家不要写代码写习惯了,自己加一个空格进去……
.
匹配除了换行符\n
之外的任何单个字符,例如使用.+
就可以匹配一整行字符。
(string)
匹配 string 并获取该匹配作为一个捕获分组,这里的捕获分组可以供元字符\
的向后引用用法使用。
(?:string)
匹配 string 但不获取该匹配,也就是说该匹配不可以供元字符\
的向后引用用法使用。例如ab(c)(d)\1
,即向后引用第一个捕获分组,也就是ab(c)(d)(c)
,而ab(?:c)(d)\1
则为ab(c)(d)(d)
,也就是说这里的第一个捕获分组是(d)
而不是(c)
,因为(?:c)
表示不获取该匹配作为捕获分组。
(?=string)
正向预查,也就是从匹配 string 的地方开始向前查找匹配字符串,但并不把 string 也添加到匹配到的字符串中,例如使用windows(?=2000|XP)
就可以匹配形如 windows2000 windows7 windowsNT windowsXP,注意到 windows 后面的 2000 和 XP 没有被添加进匹配的字符串。
(?<=string)
反向预查,也就是从匹配 string 的地方开始向后查找匹配字符串,但同样不把 string 添加到匹配到的字符串中,例如使用(?<=windows)10
就可以匹配形如 windows7 linux10 ubuntu10 windows10。此用法与上面的正向预查用法一起使用,可以精确定位并选择想要选择的内容,例如使用(?<=string)xxx(?=number)
就可以精确地选择出字符串 string 和 number 中间所夹的字符。
注意:在JavaScript的正则对象中,不支持反向预查功能,比较好的替代方法是在想要匹配的字符串前面该换用 (?:string) 来替代 (?<=string),最后的效果相同。
(?!=string)
反向预查,也就是从任何不匹配 string 的地方开始向前查找匹配字符串,这个用法我没怎么用过,不敢多说话……
x|y
匹配 x 或者 y,例如使用abc|123
就可以匹配形如 abcdefghi123456789。
[xyz]
字符集合。匹配该字符集合中的任意一个字符(字母或数字),例如使用[abc]
就可以匹配形如 a1b2c3d4e5f6g7。
[^xyz]
负值字符集合。不匹配该字符集合中的任意一个字符(字母或数字),例如使用[^abc]
就可以匹配形如 a1b2c3d4e5f6g7。
[a-z]
字符范围。匹配指定范围内的任意一个字符(字母或数字),例如使用[a-d]
就可以匹配形如 abcdefg。
[^a-z]
负值字符范围。不匹配指定范围内的任意一个字符(字母或数字),例如使用[^a-d]
就可以匹配形如 abcdefg。
\b
匹配一个单词边界,但不将该边界也选择到字符串,也就是说该元字符的作用是标识匹配位置(其实,上面的 (?=string) 和 (?<=string) 的作用也是标识匹配位置)。例如使用\bab|ab\b
就可以匹配形如 ababababababab,可以发现表达式只匹配了字符串两头的 ab,而中间的 ab 则没有被匹配,因为中间的 ab 左右两边都没有单词边界。另外,该元字符也可以匹配数字的边界。
\B
匹配一个非单词边界,与上面的\b
作用恰好相反。
\d
匹配一个数字字符,等价于[0-9]
。
\D
匹配一个非数字字符,等价于[^0-9]
,与上面的\B
作用恰好相反。
\f
匹配一个换页符。这个我基本没用过。
\n
匹配一个换行符。
\r
匹配一个回车符。
\t
匹配一个制表符。
\v
匹配一个垂直制表符。
\s
匹配任何空白字符,等价于[\f\n\r\t\v]
。
注意:严格来说,\s
并不完全等价于[\f\n\r\t\v]
,因为除此之外还有一些不能显示的字符也相当于空白字符,这些字符可以被\s
匹配,但不能被[\f\n\r\t\v]
匹配,所以实际使用的时候需要注意这一点。
\S
匹配任何非空白字符,等价于[^\f\n\r\t\v]
,与上面的\s
作用恰好相反。
\w
匹配包括下划线在内的任何单词字符,等价于[A-Za-z0-9_]
。
\W
不匹配包括下划线在内的任何单词字符,等价于[^A-Za-z0-9_]
。
正则表达式中的所有元字符基本上就是这么多了,前面的元字符使用频率很高,后面的相对而言比较低,基本上掌握了元字符的使用方法与技巧,正则表达式就已经掌握了一大半,剩下的就是不断寻找机会慢慢练习了。
$1
、$&
、$`
前面我们说过正则表达式在匹配过程中会捕获分组,这些捕获的分组一方面可以通过\num
这种用法来继续匹配字符串后面与该捕获分组相同的子字符串,另一方面还可以直接通过$1
、$&&
、$`
这种方式获取到。例如在 JavaScript 中,字符串有 replace 方法,该方法接收的第二个参数,即用于替换的字符串便可以接收通过$1
、$&&
、$`
这种方式获取到的子字符串。这种用法说起来比较抽象,下面我们举个例子来理解这种用法。
假设我们现在有这样一段字符串 111222333444555,我们想把其中的 333 这个子字符串的两侧各添加一些字符,如 AAA,即最后期望得到的字符串是 111222AAA333AAA444555,则其对应的正则表达式可以写成(333)
,相对应的替换字符串为AAA$1AAA
,这样会先寻找与正则表达式匹配的子字符串 333,找到以后因为是捕获分组,所以会将 333 存入到$1
中,之后进行替换时相对应的替换字符串就变为了 AAA333AAA,所以最后将得到我们期望的字符串。
由上面的例子我们便可以知道,$1
中存入的是第一个捕获分组,那么$2
中存入的便是第二个捕获分组,以此类推,在 JavaScript 中最大可以一直到$99
,即存储99个捕获分组。
除此以外,还有三种替换方式可供选择,$&
存储的是与正则表达式匹配的字符串;$`
存储的是与正则表达式匹配的字符串的左侧文本;$'
存储的是与正则表达式匹配的字符串的右侧文本,了解了这些以后,使用正则表达式替换某些特定文本的时候就能够更加游刃有余。
这里是参考网址,这上面说的应该更加清楚一些。
优先级
正则表达式是存在优先级的,不同的元字符,其对应的优先级也不同,掌握各个元字符的优先级,可以简化正则表达式,降低出错概率,当然,如果你对优先级不了解,你也可以使用()
将正则表达式的每个子表达式包裹起来,这可以达到同样的效果。
第一优先级 转义符 \
转义符\
的优先级是最高的,其后面跟随的字符几乎全部会首先被看做是转义字符。
第二优先级 圆括号与方括号
具体来说有()
,(?:)
,(?=)
,(?<=)
,[]
。
第三优先级 限制符
具体来说有 *
,+
,?
,{n}
,{n,}
,{n,m}
。
第四优先级 或运算
|
具有最低的优先级,字符优先级都比|
高,例如使用ab|cd
就可以匹配形如 abdacd,如果你的本意是向匹配 abd 或 acd,请按照我上面所说的方法,使用a(b|c)d
来改变优先级,或者更简单地使用a[bc]d
。
常用的正则技巧
下面这些是我在学习工作过程中遇到的一些比较有用的正则技巧,现在在这里记下来,以备后用。
匹配汉字
在 JavaScript 的正则对象中,元字符\w
仅匹配字母和数字,不匹配中文字符,这里需要使用[\u4e00-\u9fa5]
来匹配中文,其原理是在 Unicode 编码表中,汉字的编码是从 4e00 到 9fa5,所以在此范围内的所有编码字符都是中文字符,使用字符集合即可匹配这些中文字符。
关于正则表达式,就暂时写这么多吧,这种东西最注重实践,用得多了,自然就熟练了,关于技巧嘛,我暂时还没有什么特别的技巧,等以后在实践中遇到了,我会到这里来补充的,就这样吧~