正则表达式是用于匹配字符串的特殊字符组合,本篇文章将会详细介绍正则表达式的字符含义与使用方法。
边界匹配
边界是指用来表明匹配内容的起始或终止部分,而边界匹配就是通过标识起始或终止的部分进行匹配。
匹配字符串的开头:^
"^"用于匹配字符串的开头,如果使用多行模式匹配,每行的开头都将会被匹配中。
const text = '1.亲手折断的玫瑰。\n2.又问为何枯萎。';
const regexp = /^\d/gi;
console.log(text.match(regexp));
// “\d”用来匹配一个数字字符
// 匹配 [ '1' ]
const regexp_m = /^\d/gim;
console.log(text.match(regexp_m));
// 多行匹配 [ '1', '2' ]
”regexp“表达式使用gi模式(全局不区分大小写),用于匹配字符串时,将只会匹配字符串其实的数字”1“。而”regexp_m“表达式使用gim模式(全局不区分大小写多行)则会进行多行匹配。
注意,当“^”出现在中括号里面时,则是表示匹配中括号范围之外的字符,例如"[^a-z]
"表示匹配小写字母之外的字符。
匹配字符串的结尾:$
”$“与”^“作用类似,但它用于匹配字符串的结尾。
(function () {
const text = '1.亲手折断的玫瑰。\n2.又问为何枯萎。';
const regexp = /.$/gi;
console.log(text.match(regexp));
// “.”用来匹配任意类型字符,“.$”就表示匹配字符串结尾的字符。
// 匹配 [ '。' ]
const regexp_m = /.$/gim;
console.log(text.match(regexp_m));
// 多行匹配 [ '。', '。' ]
})();
匹配英文单词的边界:\b
”\b“用于匹配一个单词的边界(“[\b]”用于匹配退格符“\b”),理解什么是单词的边界很重要。单词的边界可以是行首行尾、字符串的起始末尾,也可以是大小写字母数字下划线之外的字符,也就是正则表达式中字符类:"\W",用正则表达式的范围表示:[^A-Za-z0-9_]
。
const text = "Don't worry about failure;\nyou-only-have-to_be_right_once.";
const regexp = /\b\w+\b/gi;
console.log(text.match(regexp));
// ”\w“用来匹配字母类字符,量词“+”表示最少匹配一个字母类型的字符。
// [ 'Don', 't', 'worry', 'about', 'failure', 'you', 'only', 'have', 'to_be_right_once']
上述示例中的“Don't”中“'”以及”-“是单词的边界,而下划线”_“则不是,所以to_be_right_once匹配为一个单词了。
还有一点需要注意的是:“\b”本身不匹配中任何字符,因为它是用来匹配单词的边界。如果使用“\b”单独去匹配任意字符串,它永远只会返回一个空字符串。
匹配英文单词的边界:\B
”\B“用于匹配一个非单词的边界,但不意味着它与“\b”相反只匹配[A-Za-z0-9_]
范围内的字符,换句话说它匹配边界两侧都为同一类型的字符。
const text = "Don't worry. ";
const regexp = /\B[a-z]\B/gi;
// [a-z]表示a到z范围内的字母,也就是所有的小写字母
const regexp_any = /\B.\B/gi;
console.log(text.match(regexp));
// [ 'o', 'o', 'r', 'r' ]
console.log(text.match(regexp_any));
// [ 'o', 'o', 'r', 'r', ' ' ]
上述示例中的末尾存在一个空格,正则表达式regexp_any匹配结果中包含了空格,因为“.”匹配除换号符外所有的字符,所以“\b”将该空格的前面的”.“字符与后面的文本结束都视为同一类型的边界。
零宽断言
先行断言 a(?=b)
当字符"a"后面是字符“b”时,该正则匹配,匹配结果不含括号里面的字符“b”。
先行否定断言 a(?!b)
当字符"a"后面不是字符“b”时,该正则匹配。
后行断言 (?<=b)a
当字符"a"前面是字符“b”时,该正则匹配,匹配结果不含括号里面的字符“b”。
否定后行断言 (?<!b)a
当字符"a"前面不是字符“b”时,该正则匹配。
const text = 'Hello world! hello regexp! Hi Regexp!';
const regexps = [
/hello(?= world)/gi, // 先行断言,匹配字符串“Hello world”中的“Hello”
/hello(?! world)/gi, // 先行否定断言,匹配字符串“hello regexp”中的“hello”
/(?<= hello )regexp/gi, // 后行断言,匹配字符串“hello regexp”中的“regexp”
/(?<!hello )regexp/gi, // 后行否定断言,匹配字符串“Hi Regexp”中的"Regexp"
];
regexps.forEach((regItem) => console.log(text.match(regItem)));
// [ 'Hello' ] [ 'hello' ] [ 'regexp' ] [ 'Regexp' ]
字符类(元字符)
字符类是指含有特殊标识的字符,它们用来表示匹配某一类型的字符,如:数字、字母。
"."匹配任意字符
“.”匹配除行终止符之外的所有字符,但可以使用"s"标识使得它能够匹配包括行终止符在内的所有字符:
行终止符包括:
- U+000A 换行符("
\n
") - U+000D 回车符("
\r
") - U+2028 行分隔符
- U+2029 段分隔符
const text = ['\r', '\n', '\u2028', '\u2029'];
const regexp = /./;
const regexp_s = /./s;
text.forEach((item) => console.log(regexp.test(item)));
// false false false false
text.forEach((item) => console.log(regexp_s.test(item)));
// true true true true
const text = '你说的黑是什么黑\n你说的白是什么白';
const reg_1 = /.+/;
console.log(reg_1.exec(text)[0]);
// 你说的黑是什么黑
const reg_2 = /.+/s;
console.log(reg_2.exec(text)[0]);
// 你说的黑是什么黑
// 你说的白是什么白
如果“.”出现在中括号内时则只匹配这个字符本身,需要在非括号中匹配时,需要加上斜杠转义”\.“。
const reg = /\./g; // 匹配“.”字符
const reg_r = /[\d.]/g; // 匹配数字或“.“字符
若要使“.”与 astral 字符(大于 \uFFFF 的 Unicode 字符)匹配,额外使用"u"标志(即"/./su
"),".
"将会匹配所有的 Unicode 字符。(关于什么是 Unicode 可以查看这篇文章)
"\b"与"\B"
"\b"匹配一个英文单词的边界,"\B"匹配一个非英文单词的边界,上面已经仔细介绍过了,按照用法归为边界匹配更合适,因为这两个特殊字符的匹配结果并不是实体的字符。
"\d"与"\D"
”\d“匹配数字,与[0-9]
作用一致。”\D“则与之相反,匹配数字之外的字符,与[^0-9]
作用一致。
"\w"与"\W"
”\w“匹配字母、数字、_(下划线),与[A-Za-z0-9_]作用一致。”\W“匹配字母、数字、_(下划线)之外的字符,与[^A-Za-z0-9_]作用一致。
"\s"与"\S"
"\s"匹配空白字符,空白字符包括:ASCII码中的空格" "、制表符"\t"、换页符"\f"、换行符"\n"、回车符"\r"、垂直制表符"\v"以及Unicode中的空格字符。与之相反,"\S"匹配空白字符之外的字符。
"\c"
匹配ASCII码控制字符的脱字符表示。例如换行符“\n”的脱字符形式用“^J”表示,可以使用/\cJ/来匹配。
const reg = /\cJ/gi;
const text = '这是第一行。\n这是第二行';
console.log(text.match(reg)); // [ '\n' ]
"\x"
匹配十六进制的Unicode编码,用两个十六进制数字来表示(例如:\x32),Unicode中U+00 ~ U+FF范围中256个字符。
const reg_u = /\x30/gi; // 数字字符"0"的Unicode编码的十六进制为30
const text_n = '0';
console.log(reg_u.test(text_n)); // true
"\u"
匹配Unicode字符集的十六进制形式,它存在两种格式。
- 形如:
\u0000
、\uffff
,匹配两个字节的十六进制的UTF-16编码的字符,即使用四个十六进制的数字(UTF-16是Unicode的传输或存储的实现,它使用2个或4个字节来表示Unicode字符集,此篇文章有介绍) - 形如:
\u{0000}
、\u{FFFFF}
,匹配十六进制的Unicode编码的字符,即使用四个或五个十六进制的数字,但需要使用“u”标识。
const word = '梦';
const reg_utf16 = /\u68A6/gi;
const reg_unicode = /\u{68A6}/giu;
// 中文字符“梦”的UTF-16编码与Unicode编码的十六进制都为68A6
console.log(reg_utf16.test(word));
console.log(reg_unicode.test(word));
// 都打印 true
\p
匹配Unicode特定类型范围内的字符集,例如只匹配中文字符(/\p{Script=Han}+/gu
)、所有国家货币符号(/\p{Sc}/gu
)、大写字母(/\p{Lu}+/gu
)等特定类型的字符。
const text = '这是一句中文。This is an English sentence';
const regexpArr = [
/\p{Ideographic}+/gu,
/\p{Script=Han}+/gu,
/[\u4E00-\uFFFF]+/gu
];
regexpArr.forEach((regexpItem) => console.log(text.match(regexpItem)));
// 三个正则表达式都输出 [ '这是一句中文' ]
最后一个正则表达式使用Unicode的汉字编码范围来匹配字符,但实际上汉字在Unicode的编码范围不只是\u4E00
到\uFFFF
之间。
更多"\p"字符类的信息可以参考此处。
其他的特殊字符
\0
:匹配NUL 字符(不得紧随其他数字)\123
:匹配Unicode字符的八进制表示,必须是三位八进制的数,不足三位用“0”填充。\n
:当n是一个正整数(0不是正整数)时,表示对捕获组的引用(下面会详细介绍)。\t
:匹配水平制表符\r
:匹配回车符\n
:匹配换行符\v
:匹配垂直制表符\f
:匹配换页符[\b]
:匹配退格符号(必须带中括号)\\
、\/
、\.
、\*
、\?
等等这些有特殊含义的字符,都需要反斜杠来转译以实现匹配它们自身。
量词
量词是用来表示重复匹配的次数。
"*" 表示匹配任意次数,包括匹配0次,也就说不匹配任何内容。
"+"表示匹配至少1次。
"?"表示匹配1次或匹配0次。
"{3}"表示匹配3次。
"{2, 6}"表示匹配2到6次。
"{2,}"表示匹配至少2次。
const regexps = [/\d*/, /\d+/, /\d?/, /\d{3}/, /\d{2,6}/, /\d{2,}/];
const text = '演示字符串:1234';
regexps.forEach((regexp) => console.log(`${regexp} - ${regexp.exec(text)[0]}`));
// /\d*/ -
// /\d+/ - 1234
// /\d?/ -
// /\d{3}/ - 123
// /\d{2,6}/ - 1234
// /\d{2,}/ - 1234
注意上面这个演示,在非全局模式的匹配下,"\d*
"与"\d?
"不会有匹配结果,这两个正则表达式在匹配完第一个字符后就结束了,因为他俩可以匹配成功0次,也就说如果整个字符串都没有与之匹配的内容也会在正则表达式对象的test方法中通过返回true。而剩余的三种正则都会尽可能的向后匹配,以达到指定要求的匹配次数。
const regexp = /\d*/;
if (regexp.test('任意内容的字符串都会通过')) {
console.log('测试通过');
}
贪婪匹配与非贪婪匹配
在默认情况下,这些量词是执行贪婪匹配,换句话说这些量词都会尽可能多的去匹配。比如说:\d{2,6}
,这会尽可能的连续匹配6个数字,而不是2个。
但是我们可以通过在这些量词后面紧随一个"?"字符,使得它们执行非贪婪匹配,也就是尽可能的少匹配。
const diligent = /\d{2,6}/
const lazy = /\d{2,6}?/
const text = '12345678'
console.log(diligent.exec(text)[0], lazy.exec(text)[0]);
// 123456 12
范围和捕获组
范围是指需要匹配指定类型的范围,比如"[5-8]
"匹配5~8范围内的数字,又或是"[a-d]
"可以匹配a-d范围内的字母,也可以使用"|"来匹配它们两者的并集:"[5-8]|[a-d]
"。
"|"
形如:abc|123,表示匹配完整的一个“abc”字符或”123“字符。
const text = 'This dress is only $9.90!';
const regexp = /\d|\./gi;
console.log(text.match(regexp));
// [ '9', '.', '9', '0' ]
"[]"与"[^]"
"[]
"中括号用来表示匹配中括号内字符集合之中的一个。可以使用连字符表示连续性的集合,也可以枚举出需要匹配的集合。
const regexps = [
/[0-9]/g,
/[0123456789]/g,
/[\d]/g,
/[0-4|5-9]/g,
/[\u0030-\u0039]/gu
];
const text = 'No.9:这是第九个测试用例。';
regexps.forEach((regexpItem) => console.log(text.match(regexpItem)));
// [ '9' ] [ '9' ] [ '9' ] [ '9' ] [ '9' ]
/[0-9]/g
, 使用连号符表示0到9连续的数字字符集合。/[0123456789]/g
, 枚举0到9的每个数字字符集合。/[\d]/g
, 使用字符类"\d",也表示为数字字符的集合。/[0-4|5-9]/g
, 0到4与5-9的并集,也就是0-9连续的数字字符。/[\u0030-\u0039]/gu
, Unicode编码字符集中的U+0030到U+0039范围内的字符,这个范围对应的字符也是0-9。
上述示例中的五个正则表达式是等价的,最终都将匹配一个数字字符。
注意,如果将连字符"-"放在方括号中的第一个或最后一个位置时,则它将被视作为普通的一个字符用来匹配字符串中的连字符。
const regexp = /[0-]/;
const text = 'Hello-world';
console.log(regexp.test(text)); // true
"[^]
"与"[]
"作用相反,用于匹配中括号之外的字符,其他与"[]
"一致,但如果需要匹配"^"字符本身,将其放置在方括号第一个位置之外的地方即可。
const regexp = /[^\w-]/;
// 匹配数字、大小写字母、下划线、连字符之外的字符
捕获组"()"
捕获组是指正则表达式中使用小括号的部分。如果整个的正则表达式匹配成功,可以将捕获组捕获的内容通过标识进行引用。形如:/a(\w)a/
,匹配以"a"开头又以"a"结尾的字符串,而中间可以匹配任意一个大小写字母或数字字符,而这个字符可以通过特定的标识进行使用。
常规捕获组
一个正则表达式可以有多个捕获组,它们可以并列也可以嵌套。
const regexp = /a(\w(\w))/;
1. 正则捕获组捕获的内容可以从正则表达式对象的exec方法、字符串的match、matchAll方法返回的数组中获取。
const regexp = /((\d{2,4})年(\d{1,2})月)(\d{1,2})号/g;
const text = '今天是2023年10月20号!';
const matches = regexp.exec(text);
console.log(matches);
// ['2023年10月20号', '2023年10月', '2023', '10', '20']
exec方法返回的数组的第一项(下标为0)是整个正则表达式匹配结果,从数组第二项开始就是各个捕获组的捕获内容,顺序是按照在捕获组的左括号的前后位置决定的,即便是在捕获组的嵌套的情况下。
2. 正则捕获组捕获的内容也可以在字符串方法中进行引用。
const regexp = /(\d{3})\d*(\d{4})/;
const number = '13012345678';
console.log(number.replace(regexp, '$1 **** $2'));
// 130 **** 5678
在replace方法中的第二个参数内,"$1"与"$2"分别是正则中的两个捕获组捕获的内容的引用,如果存在嵌套的情况下,先后顺序规则也和上述一致。
3. 正则捕获组捕获的内容还可以在表达式自身中使用。
在正则中可以使用形如"\n
"的符号,其中 n 是一个正整数,它用于将前面的捕获组捕获的内容作为当前位置的正则表达式。
const text = '100,000,000,000';
const regexp = /100(,000)\1\1/;
console.log(regexp.exec(text));
// ['100,000,000,000', ',000',]
"\1
" 表示使用前面捕获组的捕获内容作为当前位置的正则表达式,"\1\1
"将会匹配text中的",000,000
"。请注意,它是使用捕获的内容作为正则表达式,而不是复用捕获组的正则表达式,这个特性在某些的情况下会非常有用。
const regexp = /((\d{4})年(\d{1,2})月)(\d{1,2})号.*(\1)/g;
const text_1 = '今天是2023年10月20号,明天就是2023年10月21号了。';
console.log(regexp.exec(text_1));
// ['2023年10月20号', '2023年10月', '2023', '10', '20']
const text_2 = '今天是2023年10月20号,下个月就是2023年11月21号了。';
console.log(regexp.exec(text_2));
// null
在text_2
上,\1
使用第一个捕获组捕获的'2023年10月
'去匹配后面的'2023年11月
'字符,结果就是不匹配。
具名捕获组
形如:(?<name>regexp)
的格式叫做具名捕获组,“name”是捕获组的名称,“regexp”是捕获组的正则。如果匹配,就会将捕获组捕获的内容以键值对的方式存储在返回的数组的 groups 属性中,键名为捕获组的名称,值为捕获组捕获的内容。
const text = 'First_Name: Evans, Last_Name: Ann.';
const regexp = /First_Name: (?<firstName>\w+), Last_Name: (?<lastName>\w+)/g;
const { firstName, lastName } = regexp.exec(text).groups;
console.log(firstName, lastName);
// Evans Ann
匿名捕获组
形如 (?:regexp)
的格式叫做匿名捕获组,它将不会返回捕获组捕获的内容。
常规和具名捕获组性能开销相较于常规捕获组来说更大,因此如果不需要使用捕获组捕获的内容,使用匿名捕获组会是一个更好的选择。
文章评论