正则表达式学习笔记(一)基本用法

这篇文章主要是我个人学习正则表达式的回顾与总结,另外还会写一点自己使用过程中的小经验与小技巧,作为自己学习正则表达式的学习笔记。

虽然我在实验室的方向是前端开发,在表单验证中使用正则表达式很频繁,但是我接触并喜欢上正则表达式是在大二暑假参加全国电赛的时候,那个时候我们这一队属于测量方向,经常使用 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,所以在此范围内的所有编码字符都是中文字符,使用字符集合即可匹配这些中文字符。

关于正则表达式,就暂时写这么多吧,这种东西最注重实践,用得多了,自然就熟练了,关于技巧嘛,我暂时还没有什么特别的技巧,等以后在实践中遇到了,我会到这里来补充的,就这样吧~