正则表达式
- 在线编写和检验正则的网站:https://regex101.com 支持多语言切换
- freeCodeCamp: Introduction to the Regular Expression Challenges
基础概念
创建方式
构造函数
const regex = new RegExp('abc', 'i');
字面量
const regex = /abc/i;
修饰符
g
:global,全文搜索,默认搜索到第一个结果停止搜索。i
:insensitive,不区分大小写,默认大小写敏感。m
:multiple line,多行搜索,更改^
和$
的含义,使它们分别在每行的行首和行尾匹配,默认是在整个字符串的开头和结尾匹配。s
:single line,单行模式,此模式下.
能匹配任意字符,包括换行符,默认是不包括的。
TODO
y
:sticky,必须从上一次匹配成功的下一个位置开始(lastIndex),作用是达到^
在全局匹配中都有效。u
:unicode,用来正确处理大于\uFFFF
的 Unicode 字符(对于大于\uFFFF
的字,把 4 字节的字符当做单独 1 个字来解析,否则当做 2 个 2 字节的字),把\u{}
当做 Unicode(否则当做正则内容)。默认按照 ES5 来处理字符,1 个字仅有 2 个字节。
元字符
大部分字符在正则表达式中,就是字面的含义,比如/a/
匹配 a,/b/
匹配 b。如果在正则表达式之中,某个字符只表示它字面的含义(就像前面的 a 和 b),那么它们就叫做“字面量字符”(literal characters)。
除了字面量字符以外,还有一部分字符有特殊含义,不代表字面的意思。它们叫做“元字符”(meta characters),先介绍以下几个:
元字符 | 符号 | 说明 |
---|---|---|
选择类 | | | 或匹配,如 /x|y/ 正则可匹配 x 或 y 两个字符 |
选择类 | [] | 或匹配,如 [abcd] 代表一个字符,这个字符可以是 abcd 四个字符中的任意一个 |
范围类 | [0-9] | 0-9 中的任意一个字符 |
范围类 | [a-z] | a-z 中的任意一个字符 |
范围类 | [a-zA-Z0-9] | 大写字母、小写字母、数字中的任意一个字符 |
位置类 | ^ | 匹配字符串的开始 |
位置类 | \$ | 匹配字符串的结束 |
预定义 | . | 匹配除回车(\r )、换行(\n ) 、行分隔符(\u2028 )和段分隔符(\u2029 )以外的任意字符 |
预定义 | \w | 匹配字母数字下划线,等同于 [a-zA-Z0-9_] |
预定义 | \s | 匹配任意空白符,等同于 [\r\n\t\f\v ] |
预定义 | \d | 匹配数字,等同于 [0-9] |
预定义 | \b | 匹配单词(字母、数字、下划线)边界,注意:- 也被认为是单词边界 |
预定义 | [\b] | 匹配退格符(backspace) |
预定义 | \t | 匹配水平制表符(tab) |
预定义 | \r | 匹配回车符(carriage return) |
预定义 | \n | 匹配换行符(linefeed) |
预定义 | \0 | 匹配空字符(NUL)不要在此后面跟小数点 |
预定义 | \v | 匹配垂直制表符(vertical tab) |
预定义 | \f | 匹配换页符(form-feed) |
其它元字符将会在后面进行介绍。
转义
需要使用 \
进行转义的元字符:
字符 | 说明 |
---|---|
\ | 因为已在转义中使用 |
( | 因为已在捕获中使用 |
) | 因为已在捕获中使用 |
[ | 因为已在范围类和反义中使用 |
. | 因为已在预定义字符中使用 |
^ | 因为已在位置类字符中使用 |
\$ | 因为已在位置类字符中使用 |
| | 因为已在选择类字符中使用 |
* | 因为已在量词类中使用 |
+ | 因为已在量词类中使用 |
? | 因为已在量词类中使用 |
反义
反义字符 | 说明 |
---|---|
[^x] | 匹配除“x”之外的所有字符,其中“x”可以为任意字符 |
[^xyz] | 同上,匹配除“x、y、z”之外的任意字符 |
\W | 匹配除了字母、数字、下划线之外的所有字符,等同于 [^\w] |
\S | 匹配除空白符之外的任意字符,等同于 [^\s] |
\B | 匹配不是单词边界的字符,等同于 [^\b] |
\D | 匹配不是数字的所有字符,等同于 [^\d] |
贪婪模式重复匹配
贪婪模式:即匹配直到下一个字符不满足匹配规则为止,这是一种最大可能匹配。
元字符 | 符号 | 重复出现次数 |
---|---|---|
量词符 | * | ≥ 0 |
量词符 | + | ≥ 1 |
量词符 | ? | 0 或 1 |
精确匹配 | {n} | n |
精确匹配 | {n,} | ≥ n |
精确匹配 | {m,n} | m ≤ count ≤ n |
惰性模式重复匹配
惰性模式:即一旦条件满足,就不再往下匹配,这是一种最小可能匹配。
元字符 | 符号 | 重复出现次数,但尽可能少的重复 |
---|---|---|
量词符 | *? | ≥ 0 |
量词符 | +? | ≥ 1 |
量词符 | ?? | 0 或 1 |
精确匹配 | {n}? | n |
精确匹配 | {n,}? | ≥ n |
精确匹配 | {m,n}? | m ≤ count ≤ n |
常用正则
- http(s) url:
/^(https?:)?\/\/.+/
- Phone:
/^(0|86|17951)?(13[0-9]|15[012356789]|166|17[3678]|18[0-9]|14[57])[0-9]{8}$/
- Email:
/\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/
- National ID:
/^(^[1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$)|(^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])((\d{4})|\d{3}[Xx])$)$/
- Post Code:
/^[1-9]\d{5}(?!\d)$/
更多参考: https://www.baidufe.com/fehelper/regexp/index.html
RegExp 实例
属性
.ignoreCase
:返回一个布尔值,是否大小写敏感,默认是false
。.global
: 是否全局搜索,默认是false
。.multiline
: 多行搜索,默认值是false
。.lastIndex
: 下一次开始搜索的位置,每次正则表达式成功匹配时,lastIndex
属性值都会随之改变,可读写,只要手动设置了lastIndex
的值,就会从指定位置开始匹配。.source
: 返回正则表达式的字符串形式(不包括反斜杠),该属性只读。
RegExp.prototype.test(str)
测试字符串参数中是否存正则表达式模式,如果存在则返回true
,否则返回false
。
const r = /x/g;
const s = '_x_x';
r.lastIndex; // 0
r.test(s); // true
如果正则表达式带有g
修饰符,则每一次test
方法都从上一次结束的位置开始向后匹配。
r.lastIndex; // 2
r.test(s); // true
如果正则模式是一个空字符串,则匹配所有字符串。
new RegExp('').test('abc'); // true
RegExp.prototype.exec(str)
该方法用来返回匹配结果。如果发现匹配,就返回一个数组,成员是匹配成功的子字符串,否则返回 null
。
const s = '_x_x';
/x/.exec(s) // ["x"]
/y/.exec(s) // null
如果表达式中有正则捕获,则返回的数组会包括多个成员。第一个成员是整个匹配成功的结果,后面的成员就是圆括号对应的匹配成功的组:
/_(x)/.exec('_x_x'); // ["_x", "x"]
exec 方法的返回数组还包含以下两个属性:
input
:整个原字符串。index
:整个模式匹配成功的开始位置(从0
开始计数)。
如果正则表达式加上 g 修饰符,则可以使用多次exec
方法,下一次搜索的位置从上一次匹配成功结束的位置开始。
利用g
修饰符允许多次匹配的特点,可以用一个循环遍历到所有匹配的字符:
const reg = /a/g;
const str = 'abc_abc_abc';
while (true) {
const match = reg.exec(str);
if (!match) break;
console.log(match[0]);
}
// a
// a
// a
以上代码每轮循环也是基于不断更新 lastIndex
属性工作的,lastIndex
只对同一个正则表达式有效,所以下面这样写是错误的,会造成死循环:
while (true) {
const match = /a/.exec('abc_abc_abc');
if (!match) break;
console.log(match[0]); // 0 代表整个匹配的,如果有分组则从 1 开始
}
因为 while
循环的每次匹配条件都是一个新的正则表达式,导致 lastIndex
属性总是等于 0
。
String 实例方法(正则相关)
String.prototype.match(regex)
'_x_x'.match(/x/); // ["x"]
'_x_x'.match(/y/); // null
字符串的match
方法与正则对象的exec
方法非常类似,但正则表达式带有g
修饰符,则该方法与正则对象的exec
方法返回值不同,会一次性返回所有匹配成功的结果:
'_x_x'.match(/x/g); // ["x", "x"]
/x/g.exec('_x_x'); // ["x"]
String.prototype.search(regex)
返回第一个满足条件的匹配结果在整个字符串中的位置。如果没有任何匹配,则返回-1
:
'_x_x'.search(/x/); // 1
'_x_x'.search(/y/); // -1
String.prototype.replace(regex, str | func)
正则表达式如果不加g
修饰符,就替换第一个匹配成功的值,否则替换所有匹配成功的值。
'_x_x'.replace(/x/, 'y'); // _y_x
'_x_x'.replace(/x/g, 'y'); // _y_y
第二个参数是字符串时,其中可以使用美元符号$
,用来指代所替换的内容。
$&
:匹配的字符串。- \$`:匹配的字符串前面的文本。
$'
:匹配字符串后面的文本。$n
:匹配成功的第 n 组内容,n 是从 1 开始的自然数。$$
:指代美元符号$
。
'hello world'.replace(/(\w+)\s(\w+)/, '$2 $1'); // "world hello"
'abc'.replace('b', "[$`-$&-$']"); // "a[a-b-c]c"
replace
方法也可以用来消除字符串首尾两端的空格,实现 trim
函数的效果,也可以根据不同场景变得更加灵活:
var str = ' #id div.class ';
str.replace(/^\s+|\s+$/g, ''); // "#id div.class"
第二个参数既可以是字符串也可以是函数:
'_x_x'.replace(/x/, 'y'); // _y_x
'_x_x'.replace(/x/, function (match) {
return match.toUpperCase();
}); // _X_x
其中函数第二个参数是捕捉到的组匹配(有多少个组匹配,就有多少个对应的参数,通常用$number
表示),此外,最后还可以添加两个参数,倒数第二个参数是捕捉到的内容在整个字符串中的位置,最后一个参数是原字符串:
'_x_y_z'.replace(/(x).*(y).*(z).*/g, function (match, $1, $2, $3, index, str) {
console.log(match, $1, $2, $3); // x_y_z x y z
console.log(index, str); // 1 _x_y_z
return $1 + $2 + $3; // _xyz
});
如果要全局遍历匹配的字符串,除了用上文提到的 while
+ exec
方式,还可以用 replace
方法,不要返回任何值,虽然最后会返回undefined
,但并不会更改原字符串:
const str = 'abc_abc_abc';
str.replace(/a/g, function (match) {
console.log(match);
});
// a
// a
// a
str; // abc_abc_abc
String.prototype.split(regex[, num])
这里主要介绍该方法在正则中的应用。
// 非正则分隔
'a, b,c, d'.split(','); // [ 'a', ' b', 'c', ' d' ]
// 正则分隔,去除多余的空格
'a, b,c, d'.split(/,\s*/); // [ 'a', 'b', 'c', 'd' ]
// 指定返回数组的最大成员
'a, b,c, d'.split(/,\s*/, 2); // [ 'a', 'b' ]
捕获
正则表达式中用 ()
来表示分组,例如:/([0-9])/
,()
会把每个分组里匹配的值保存起来。
捕获型 ()
捕获与引用
被正则表达式匹配到的字符串会被暂存起来。
分组捕获的串从 1 开始编号,$1
表示第一个被捕获的串, $2
是第二个,以此类推,我们可以通过 $1,$2...
引用这些串。
let reg = /(\d{4})-(\d{2})-(\d{2})/;
let data = '2017-10-24';
reg.test(data);
RegExp.$1; //2017
RegExp.$2; //10
RegExp.$3; //24
与 replace 配合
String.prototype.replace
方法的传参中可以直接引用被捕获的串。比如我们想将日期 10.24/2017
改为 2017-10-24
:
let reg = /(\d{2})\.(\d{2})\/(\d{4})/;
let data = '10.24/2017';
data = data.replace(reg, '$3-$1-$2');
console.log(data); //2017-10-24
给 replace
传递迭代函数可以优雅地解决一些问题:
将违禁词转换为等字数的星号是一个常见的需求,比如文本是 dot is a doubi
,其中 dot
和 doubi
是违禁词,转换后应为 *** is a *****
。
let reg = /(dot|doubi)/g;
let str = 'dot is a doubi';
str = str.replace(reg, function (word) {
return word.replace(/./g, '*');
});
console.log(str); //*** is a *****
replace
与正则捕获组匹配还有一个常见用法,将浮点数左边的数从右向左每三位添加一个逗号,匹配全局中,数字后面跟随的是(以 .
结尾的、三个数字的分组至少有一组)的串。
function commafy(num) {
return num.toString().replace(/(\d)(?=(\d{3})+\.)/g, function ($2) {
return $2 + ',';
});
}
console.log(commafy(1200000000.11)); //1,200,000,000.11
console.log(commafy(123246723749.213769283)); //123,246,723,749.21378
嵌套分组的捕获
如果碰到类似 /((dot) is (a (doubi)))/
这种嵌套分组,规则是以左括号出现的顺序进行捕获。
let reg = /((dot) is (a (doubi)))/;
let str = 'dot is a doubi';
reg.test(str); //true
console.log(RegExp.$1); //dot is a doubi
console.log(RegExp.$2); //dot
console.log(RegExp.$3); //a doubi
console.log(RegExp.$4); //doubi
反向引用
let reg = /(\w{3}) is \1/;
console.log(reg.test('dot is dot')); //true
console.log(reg.test('dolby is dolby')); //false
console.log(reg.test('dot is tod')); //false
console.log(reg.test('dolby is dlboy')); //false
\1
引用了第一个被分组所捕获的串,本例中即 (\w{3})
,表达式是动态决定的,如果编号越界了会被当成普通的表达式。
let reg = /(\w{3}) is \3/;
console.log(reg.test('dot is \3')); //true
console.log(reg.test('dolby is dolby')); //false
尝试用反向引用解决外观数列问题。
非捕获型 (?: )
有时我们只是想分个组,并没有捕获的需求,这种情况下可以使用非捕获性分组,语法为 (?:)
。
let reg = /(?:\d{4})-(\d{2})-(\d{2})/;
let date = '2017-10-24';
console.log(reg.test(date)); //true
console.log(RegExp.$1); //10
console.log(RegExp.$2); //24
这个例子中,(?:\d{4})
分组不会捕获任何串,所以 $1
为 (\d{2})
捕获的串。
断言 Assertion
断言虽然包裹在 ()
,但并不会捕获值。
以下所说的前
和后
指的要匹配的内容相对于断言的前后位置。
正向前瞻型 (?= )
Lookahead assertion x(?=y)
: Matches "x" only if "x" is followed by "y". For example:
let reg = /dot is a (?=doubi)/;
console.log(reg.test('dot is a doubi')); //true
console.log(reg.test('dot is a shadou')); //false
这个正则要求 dot is a
后面要是 doubi
才匹配成功。
反向前瞻型 (?! )
Negative lookahead assertion x(?!y)
: Matches "x" only if "x" is not followed by "y". For example:
let reg = /dot is a (?!doubi)/;
console.log(reg.test('dot is a doubi')); //false
console.log(reg.test('dot is a shadou')); //true
这个正则要求 dot is a
后面除了 doubi
,都能匹配成功。
正向后瞻型 (?<= )
Lookbehind assertion (?<=y)x
: Matches "x" only if "x" is preceded by "y". For example:
let reg = /(?<=dot) is a doubi/;
console.log(reg.test('dot is a doubi')); //true
console.log(reg.test('pot is a doubi')); //false
这个正则要求 is a doubi
前面要是 dot
才匹配成功。
反向后瞻型 (?<! )
Negative lookbehind assertion (?<!y)x
: Matches "x" only if "x" is not preceded by "y". For example:
let reg = /(?<!dot) is a doubi/;
console.log(reg.test('dot is a doubi')); //false
console.log(reg.test('pot is a doubi')); //true
这个正则要求 is a doubi
前面除了 dot
,都能匹配成功。
前瞻型分组与非捕获型都不会捕获值,那么它们的区别是什么?
A: 非捕获型分组匹配到的串仍会被外层的捕获型分组捕获到,但前瞻型却不会,当你需要参考后面的值,又不想连它一起捕获时,前瞻型分组就派上用场了:
let str = 'dot is a doubi';
let reg;
相同点:
reg = /dot is a (?:doubi)/;
console.log(reg.test(str)); //true
console.log(RegExp.$1); //无结果
reg = /dot is a (?=doubi)/;
console.log(reg.test(str)); //true
console.log(RegExp.$1); //无结果
不同点:
reg = /(dot is a (?:doubi))/;
console.log(reg.test(str)); //true
console.log(RegExp.$1); //dot is a doubi
reg = /(dot is a (?=doubi))/;
console.log(reg.test(str)); //true
console.log(RegExp.$1); //dot is a