正则浅谈
写在前面
最近工作中经常需要使用正则做字符串校验或片段提取。过去常常是百度一下,略作修改就用在项目中。近来尝试自己写正则表达式,发现正则语法已经忘记得七零八落,许多基础语法混淆不清,遇到复杂的正则更是抓瞎。不得已,将正则语法重新复习了一遍,又有新的收获。温故而知新,与君共勉。
什么是正则
回忆一下,通常情况下我们是如何表述一个字符串的:
以 “010 开头的电话号码”, “夹在HTML 的 和 中间的内容”, “含有 hello 的字符串”, “负数”, “IP地址”
显而易见,对于这些描述字符串的自然语言计算机是无法理解的。
而正则表达式是一种人和计算机都能轻松处理的约定, 用来描述一类具有某个性质的字符串。确切而言: 正则表达式是一种描述性的语言, 用来概括一类字符串 (或者说一个字符串集合)。当我们定义了某一个正则表达式,其实也就确定了这个正则表达式描述的所有字符串集合。
下列是几个最简单的正则表达式:
abc
\\d
\d[abc]
正则基础语法
正则基础语法简单而言就是符号匹配和位置匹配。符号匹配使得人可以以某几种简单的字符或字符组合描述一个字符串,位置匹配使得人可以以简单的语法描述字符串中的某个特定位置。
基本上掌握了字符匹配和位置匹配的语法,正则表达式也就八九不离十了。
下面是对正则语法的一些简单归纳:
符号匹配:
字符 | 含义 | 示例 | 结果 |
---|---|---|---|
普通字符 | 匹配单个字符 | /^abc$/.test(‘abc’) | True |
\d | 匹配一个数字字符。等价于 [0-9]。 | /^\d\d\d$/.test(‘123’) | True |
\D | 匹配一个非数字字符。等价于 [^0-9]。 | /^\D\D\D$/.test(‘abc’) | True |
\w | 匹配包括下划线的任何单词字符。等价于’[A-Za-z0-9_]’。 | /^\w\w\w$/.test(‘1a_’) | True |
\W | 匹配任何非单词字符。等价于 ‘[^A-Za-z0-9_]’。 | /^\W\W\W$/.test(‘!@#’) | True |
\s | 空白字符,包括空格、制表符、换页符等。等价于 [ \f\n\r\t\v]。 | /^\sabc$/.test(‘ abc’) | True |
\S | 匹配任何非空白字符。等价于 [^ \f\n\r\t\v]。 | True | |
\t\r\n\v\f | tab 回车 换行 垂直制表符 翻页符 | True | |
^ | 匹配单个位置时,表示取非 [^\w] | /^[^\d]abc$/.test(‘aabc’) | True |
量词:
字符 | 描述 | 示例 | 结果 |
---|---|---|---|
{n} | n 是一个非负整数。匹配确定的 n 次。 | /^\d{3}$/.test(‘123’) | True |
{n,} | n 是一个非负整数。至少匹配n 次。 | /^\d{3,}$/.test(‘123’) | True |
{n,m} | m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次。 | /^\d{1,3}$/.test(‘123’) | True |
* | 匹配前面的子表达式零次或多次。等价于{0,}。 | /^\d*$/.test(‘123’) | True |
+ | 匹配前面的子表达式一次或多次。等价于 {1,}。 | /^\d+$/.test(‘123’) | True |
? | 匹配前面的子表达式零次或一次。 等价于 {0,1}。 | /^\d?$/.test(‘123’) | False |
贪婪模式与非贪婪模式
说到量词就不得不提起正则表达式的贪婪模式以及非贪婪模式。贪婪模式在整个表达式匹配成功的情况下尽可能多的匹配字符;非贪婪模式在整个表达式匹配成功的前提下,尽可能少的匹配字符;
说到底: 正则贪婪与非贪婪模式的区别就是被 量词 修饰的子表达式的匹配行为;
贪婪模式量词:
{x,y} , {x,} , ? , * , +
非贪婪模式量词:
{x,y}?,{x,}?,??,*?, +?
下面举几个简单的例子:
贪婪模式下
'dxxxdxxxd'.match(/(d)(\w+)/)
//-> "dxxxdxxxd"
"\w+" 将匹配第一个 "d" 之后的所有字符 "xxxdxxxd"
'dxxxdxxxd'.match(/(d)(\w+)(d)/)
//->"dxxxdxxxd"
"\w+" 将匹配第一个 "d" 和最后一个 "d" 之间的所有字符 "xxxdxxx"。
虽然 "\w+" 也能够匹配上最后一个 "d",但是为了使整个表达式匹配成功,"\w+" 可以 "让出" 它本来能够匹配的最后一个 "d"
非贪婪模式下
'dxxxdxxxd'.match(/(d)(\w+?)/)
//-> "dx"
"\w+?" 将尽可能少的匹配第一个 "d" 之后的字符,结果是:"\w+?" 只匹配了一个 "x"
'dxxxdxxxd'.match(/(d)(\w+?)(d)/)
//-> "dxxxd"
为了让整个表达式匹配成功,"\w+?" 不得不匹配 "xxx" 才可以让后边的 "d" 匹配,从而使整个表达式匹配成功。
因此,结果是:"\w+?" 匹配 "xxx"
位置匹配:
字符 | 描述 | 示例 | 结果 |
---|---|---|---|
^ | 匹配开头,在多行匹配中匹配行开头 | “hello”.replace(/^|$/g, ‘#’) | “#hello#” |
$ | 匹配结尾,在多行匹配中匹配行结尾。 | “I\nlove\njavascript”.replace(/^|$/gm, ‘#’) | 该示例格式不利展示,请自行运行 |
\b | 匹配一个字边界,即字与空格间的位置。 | “[JS] Lesson_01.mp4”.replace(/\b/g, ‘#’) | [JS] Lesson_01.mp4.replace(/\b/g, ‘#’) |
\B | 匹配非字边界,即所有\b匹配不到的位置。 | “[JS] Lesson_01.mp4”.replace(/\B/g, ‘#’) | #[J#S]# L#e#s#s#o#n#_#0#1.m#p#4 |
(?=p) | p是子正则模式,匹配p前面的位置(正向先行断言) | “hello”.replace(/(?=l)/g, ‘#’) “hello”.match(/(?=ll)\w+?$/g) |
he#l#lo [“llo”] |
(?!p) | p是子正则模式,匹配所有非p前面的位置(负向先行断言) | “hello”.replace(/(?!l)/g, ‘#’) ‘hello’.match(/(?!l)\w+?/g, ‘#’) |
#h#ell#o# [“h”, “e”, “o”] |
以上是正则中用于位置匹配的符号。
其中(?=p)和(?!p)又被称为正向先行断言和负向先行断言,网络上对此也有许多解释和相关示例,可自行搜索了解。
正则里的括号
在正则表达式中,我们常常可以看到许多括号。而对于没有正则基础或正则基础薄弱的人来说,这些括号使得正则表达式看起来更加复杂,扑朔迷离。因此有必要对正则表达式中的括号做一个正确的认识。
那么正则中的括号有哪些作用呢?
分组和分支结构
分组和分支是正则中括号最显而易见也是最重要的作用。
1.分组
/(ab)+/
在这里括号提供分组功能,使量词“+”作用于“ab”这个整体。
2.分支
/(a|b)c/
这里的括号包含了多选分支结构(p1|p2)。
这两点比较简单,就不再赘述。
捕获分组
捕获分组,又称引用分组,是正则中括号的一个重要作用。捕获分组因为着正则表达式除了提取整体匹配的字符串,还可以提取正则中每一个被括号包围的子表达式匹配到字符串。
如下例:
"2017-06-12".match(/(\d{4})-(\d{2})-(\d{2})/)
\\-> ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12"]
match返回的一个数组,第一个元素是整体匹配结果,然后是各个分组(括号里)匹配的内容,然后是匹配下标,最后是输入的文本。(注意:如果正则有修饰符g,match返回的数组格式是不一样的)。
反向引用
反向引用实际和捕获分组是类似的作用,二者的区别在于捕获分组是在正则匹配完成后在外部程序中引用分组,而反向引用允许正则在匹配过程中就引用分组匹配到的字符。
如下例:
例如: 日期格式匹配 2016-06-12、2016/06/12
var regex = /\d{4}(-|\/)\d{2}(-|\/)\d{2}/;
var string1 = "2017-06-12";
var string2 = "2017/06/12";
var string3 = "2016-06/12";
console.log( regex.test(string1) ); // true
console.log( regex.test(string2) ); // true
console.log( regex.test(string3) ); // true
其中字符串3明显不符合要求,但是也通过了校验
利用反向引用
var regex = /\d{4}(-|\/)\d{2}\1\d{2}/;
var string3 = "2016-06/12";
console.log( regex.test(string3) ); // false
注意里面的\1,表示的引用之前的那个分组(-|\/)。不管它匹配到什么(比如-),\1都匹配那个同样的具体某个字符。
非捕获分组
之前文中出现的分组,都会捕获它们匹配到的数据,以便后续引用,因此也称他们是捕获型分组。
如果只想要括号最原始的功能,但不会引用它,即,既不在API里引用,也不在正则里反向引用。此时可以使用非捕获分组(?:p)
var regex = /(?:\d{4})-(?:\d{2})-(?:\d{2})/
var string = "2017-06-12";
console.log( string.match(regex) );
//-> ["2017-06-12", index: 0, input: "2017-06-12"]
括号嵌套怎么办?
看过以上这么多种括号的作用,难免产生一个疑问,一个正则表达式中如果出现了括号嵌套的情况,那么反向引用的顺序会是什么样的?
这个问题,通过观察下面这个例子可以解决:
var regex = /^((\d)(\d(\d)))\1\2\3\4$/;
var string = “1231231233”;
console.log( regex.test(string) ); // true
console.log( RegExp.$1 ); // 123
console.log( RegExp.$2 ); // 1
console.log( RegExp.$3 ); // 23
console.log( RegExp.$4 ); // 3
由此可见,分组的引用顺序是由该分组左括号出现的顺序决定的。
正则中几种括号的区别
()内的内容表示的是一个子表达式,()本身不匹配任何东西,也不限制匹配任何东西,只是把括号内的内容作为同一个表达式来处理。
[]表示匹配的字符在[]中,并且只能出现一次,并且特殊字符写在[]会被当成普通字符来匹配。例如[(a)],会匹配(、a、)、这三个字符。
{}一般用来表示匹配的长度,比如 \s{3} 表示匹配三个空格,\s[1,3]表示匹配一到三个空格。
结语
正则表达式看似复杂,其实理解透其中的规则之后就会发现,也就是那么回事儿。再复杂的正则表达式也是由最基础的元素符合构成的。当然,要做到熟练掌握正则表达式最重要的还是常用常练,常记常新。
推荐书籍:
精通正则表达式
javascript权威指南
更多资料:
https://zhuanlan.zhihu.com/p/27653434 正则表达式系列总结
http://www.cnblogs.com/tugenhua0707/p/5037811.html 深入浅出的javascript的正则表达式学习教程
http://www.jb51.net/tools/zhengze.html 正则表达式30分钟入门教程