字符类
字符类是一种特殊的符号,用于匹配特定集合中的符号
| 字符 | 意义 | |
|---|---|---|
| \d(digit) | 数字 0-9 | |
| \s (space) | 空格 | |
| \w(word) | 字母、数字或下划线_ | |
| \D(NOT digit) | 除数字以外的任何字符 | |
| \S(NOT space) | 非空格符号 | |
| \W(NOT word) | 除了\w以外的任何字符,例如非拉丁字母或空格 | |
| .(点号) | 匹配除换行符以外的所有字符 | |
| /./s | .匹配所有字符 | |
| {n} | 精确的n次 | |
| {3,5} | 匹配3-5次 | |
| + | 一个或多个,相当于{1,} | |
| ? | 0个或1个 | |
| * | 0个及以上,字符可以出现任何次数或不出现 | |
| 量词后面加问号?如*? | 惰性搜索,重复尽可能少的次数。惰性模式仅对后面带有?的量词启用 | |
| [ \ ^ $ . | ? * + ( ) | 前面加\,才能被正则表达式识别 |
unicode:修饰符u和类\p{...}
在js中字符一般使用两个字节编码,最多可用编码65535个字符。 Unicode使用4个字节编码字符,我们可用添加一个修饰符u启用正则表达式对unicode的支持,去查找给定语言中的词,特殊字符(引用、货币)等等 unicode属性可用使用\p{...}进行查找 以下是主要的字符类别和它们对应的子类别:
-
字母(Letter)
L:- 小写(lowercase)
Ll, - 修饰(modifier)
Lm, - 首字母大写(titlecase)
Lt, - 大写(uppercase)
Lu, - 其它(other)
Lo。
- 小写(lowercase)
-
数字(Number)
N:- 十进制数字(decimal digit)
Nd, - 字母数字(letter number)
Nl, - 其它(other)
No。
- 十进制数字(decimal digit)
-
标点符号(Punctuation)
P:- 连接符(connector)
Pc, - 横杠(dash)
Pd, - 起始引号(initial quote)
Pi, - 结束引号(final quote)
Pf, - 开(open)
Ps, - 闭(close)
Pe, - 其它(other)
Po。
- 连接符(connector)
-
标记(Mark)
M(accents etc):- 间隔合并(spacing combining)
Mc, - 封闭(enclosing)
Me, - 非间隔(non-spacing)
Mn。
- 间隔合并(spacing combining)
-
符号(Symbol)
S:- 货币(currency)
Sc, - 修饰(modifier)
Sk, - 数学(math)
Sm, - 其它(other)
So。
- 货币(currency)
-
分隔符(Separator)
Z:- 行(line)
Zl, - 段落(paragraph)
Zp, - 空格(space)
Zs。
- 行(line)
-
其它(Other)
C:- 控制符(control)
Cc, - 格式(format)
Cf, - 未分配(not assigned)
Cn, - 私有(private use)
Co, - 代理伪字符(surrogate)
Cs。 - 中文字符 sc=Han
- 西里尔字符 sc=Cyrillic
- 十六进制 Hex_Digit
- 控制符(control)
let str='A ბ ㄱ 123 45 你好 ,-) m $%@ number: xAF';
console.log(str.match(/\p{L}/ug));//[ 'A', 'ბ', 'ㄱ' ]
console.log(str.match(/\p{L}/g));//null 没有匹配项,因为没使用修饰符u
// 匹配数字
console.log(str.match(/\p{N}/gu));//[ '1', '2', '3', '4', '5' ]
// 匹配标点符号
console.log(str.match(/\p{P}/gu));//[ ',', '-', ')', '%', '@' ]
//匹配标记
console.log(str.match(/\p{M}/gu));
//匹配符号
console.log(str.match(/\p{S}/gu));//[ '$' ]
//匹配分隔符 行z段落空格
console.log(str.match(/\p{Z}/gu));//匹配了7个空格
//十六进制数
console.log(str.match(/x\p{Hex_Digit}\p{Hex_Digit}/u));//xAF
// 中文字符
console.log(str.match(/\p{sc=Han}/gu));
// \p{sc=Cyrillic} 西里尔字符
锚点 字符串开头^和结尾$
插入符号 ^ 匹配文本开头,而美元符号 $ 则匹配文本末尾。
匹配开头或结尾
注:锚点只测试条件,不测试^$本身
用于在行的开头和结尾查找某些内容
let str='Mary had a little lamb snow';
// 测试str是否以Mary开头
console.log(/^Mary/.test(str));//true
//测试str是否以snow结尾
console.log(/snow$/.test(str));//true
^...$ 通常放在一起 完全匹配
测试一个字符串是否匹配12:34格式的事件
let str='12:34';
let str2='7:26';
console.log(/^\d\d:\d\d$/.test(str));//true
console.log(/^\d\d:\d\d$/.test(str2));//false
什么字符串可用完全匹配^$?
空字符串 这个示例再次证明了锚点不是字符串,而是测试。
console.log(/^$/.test(''));//true
对于空字符串 "",正则表达式引擎将会首先匹配 ^(输入开始),匹配成功,然后立即匹配结束 $,也匹配成功。所空字符串是匹配项。
锚点的多行形式,修饰符"m'
多行模式由m开启,它只影响^和$的行为 在多行模式下,他们不仅仅他匹配文本的开始和结尾,还匹配每一行的开始和结尾
匹配多行的开头
let str = `1st place: Winnie
2nd place: Piglet
3rd place: Eeyore`;
console.log(str.match(/^\d/g));//[ '1' ]
console.log(str.match(/^\d/gm));//[ '1', '2', '3' ]
行的开头表示在换行符之后:多行模式下测试^匹配是以换行符\n开头的位置,以及文本最开始的位置
多行的结尾
let str = `Winnie: 4
Piglet: 5
Eeyore: 6`;
console.log(str.match(/\d$/g));//[ '6' ]
console.log(str.match(/\d$/gm));//[ '4', '5', '6' ]
行的末尾--在换行符之前,多行模式下$匹配是以换行符\n结尾的位置。以及文本末尾的位置
搜索\n而不是^$
换行符和锚点^$的区别
let str = `Winnie: 4
Piglet: 5
Eeyore: 6`;
console.log(str.match(/\d\n/g));//[ '4\n', '5\n' ]
let str = `1st place: Winnie
2nd place: Piglet
3rd place: Eeyore`;
console.log(str.match(/\n\d/g));//[ '\n2', '\n3' ]
这里只匹配了两个,因为6之后没有换行符。1之前没有换行符。同时换行符\n自己本身就是就是条件的一部分,因此它成为了结果的一部分,而锚点只测试条件
词边界\b
当遇到\b时,它会检查字符串中的位置是否是词边界
使用场景:可以在 Hello, Java! 中找到 \bJava\b 的匹配项,其中 Java 是一个独立的单词,而在 Hello, JavaScript! 中则不行。
console.log("Hello, Java!".match(/\bJava\b/));//匹配
console.log("Hello, JavaScript!".match(/\bJava\b/));//null
//结果
[ 'Java', index: 7, input: 'Hello, Java!', groups: undefined ]
null
匹配独立的两位数
\b\d\d\b
console.log('1 23 45 456 78'.match(/\b\d\d\b/g));//[ '23', '45', '78' ]
console.log('1,23,45,789'.match(/\b\d\d\b/g));//[ '23', '45' ]
转义、特殊字符
要将特殊字符用作常规字符,就需要在字符前加上一个反斜杠\。这个反斜杠就是转义字符 例子 匹配字符串中的一个斜杠
let str='/';
//字面量需要转义字符\,\/是查找单斜杠/
console.log(str.match(/\//));
// 使用构造函数的时候不需要使用转义字符
console.log(str.match(new RegExp("/")));
//[ '/', index: 0, input: '/', groups: undefined ]
//构造函数需要使用\\
console.log('chapter 12.3d'.match(new RegExp("d")));//匹配d
console.log('chapter 12.3d'.match(new RegExp("\d")));//null
console.log('chapter 12.3d'.match(new RegExp("\\d\\.\\d")));//匹配2.3
需要在前面加上\才能被正确识别的字符:
[ \ ^ $ . | ? * + ( )
集合和范围[...]
放括号中的几个字符或字符类表示"搜索给定字符中的任意一个"
集合,只匹配一个
集合中有多个字符,但它们只会对应其中的一个
let str='Voila';
console.log(str.match(/V[oi]la/));//null
这个模式会搜索
- V
- 然后匹配其中一个字符[oi]
- 最后匹配la 所有这个正则表达式匹配的是Vola,Vila
范围
方括号也可以匹配字符范围 [a-z]表示从a-z范围内的字符,[0-5]表示从0-5的数字
let str='Exception 0xAF16';
console.log(str.match(/x[A-F0-9][A-F0-9]/g));//xAF
[0-9A-Z]:的意思是这个字符要么是0-9范围内的数字,要么是从A-Z的字母
排除范围[^...]
在[]开头插入符号^来表示匹配所有除了给定字符以外的任意字符
- [^aeyo]——匹除了
'a'、'e'、'y'或'o'之外的任何字符 - [^0-9]——匹配除了数字以外的任何字符
- [^\s]——匹配任何非空格字符,与/s作用相同
我们有一个正则表达式/Java[^script]/。
它会和字符串Java中的任何一部分匹配吗?JavaScript 呢?
不匹配,匹配
regexp=/Java[^script]/;//匹配Java,以及除了script以外的其他字符
console.log("Java".match(regexp));//null
console.log("JavaScript".match(regexp));// 'JavaS'
[...]中的转义
在方括号中绝大多数特殊字符都无需转义
let reg = /[-().^+]/g;
console.log( "1 + 2 - 3".match(reg) ); // 匹配 +,-
[-().^+]查找 -().^+ 中的任何字符
量词+* ? {n}
| 量词名称 | 描述 |
|---|---|
| {n} | 精确的n次 |
| {3,5} | 匹配3-5次 |
| + | 一个或多个,相当于{1,} |
| ? | 0个或1个 |
| * | 0个及以上,字符可以出现任何次数或不出现 |
| 量词后面加问号?如*? | 惰性搜索,重复尽可能少的次数。惰性模式仅对后面带有?的量词启用 |
贪婪变量和惰性变量
在贪婪模式下,量词都会尽可能多的重复字符,然后在模式的其余部分不再匹配时再将其逐一缩短
惰性变量:,表示重复尽可能少的次数。在量词后面加一个?量词后面的问号意味着——它将匹配模式从贪婪转化为惰性
let str='"boom" my "switch" and "book" is my name';
console.log(str.match(/".+"/g));//[ '"boom" my "switch" and "book"' ]
console.log(str.match(/".+?"/g));//[ '"boom"', '"switch"', '"book"' ]
console.log(str.match(/"[^"]+"/g));//[ '"boom"', '"switch"', '"book"' ]
console.log("123 4567".match(/\d+ \d+?/g));//[ '123 4' ]
练习
查找HTML注释
str=`... <!-- My -- comment
test --> .. <!----> ..
`;
regexp=/<!--(.*?)-->/gs;//匹配Java,以及除了script以外的其他字符
console.log(str.match(regexp));//'<!-- My -- comment\ntest -->', '<!---->'
查找HTML标签
str= '<> <a href="/"> <input type="radio" checked> <b>';
regexp=/<[^<>]+>/gs;//匹配Java,以及除了script以外的其他字符
console.log(str.match(regexp));//<a href="/">', '<input type="radio" checked>', '<b>'
查找HTML标签的一个错误表达
str= '<> <a href="/"> <input type="radio" checked> <b> ';
regexp=/<.+?>/s;//匹配Java,以及除了script以外的其他字符
console.log(str.match(regexp));//'<> <a href="/">'
因为第一个>刚好和.+匹配成功,字符串往下走,下一个是空格,空格不匹配">",而匹配>字符串继续往下走,一直到a标签最后的一个字符>才匹配成功
捕获组
模式的一部分可以用括号括起来作为一个整体,以便量词可用整体使用,括号括起来的部分被称为捕获组 这有两个影响: 它允许将匹配的一部分作为结果数组中的单独项 如果我们将量词放在括号后,则它将括号视为一个整体
let str='Gogoogo now';
console.log(str.match(/go+/ig));//'Go', 'goo', 'go'
console.log(str.match(/(go)+/ig));//'Gogo', 'go'
不带括号:模式go+表示g字符,其后的o重复一次或多次,例如goooooo goooooooooo 带括号:括号将字符组合,(go)+匹配go gogo gogogo等
匹配括号中的内容
括号被从左到右编号。正则引擎会记住它们各自匹配的内容,并允许在结果中获取它们
方法 str.match(regexp),如果 regexp 没有修饰符 g,将查找第一个匹配项,并将它作为数组返回:
- 在索引
0处:完整的匹配项。 - 在索引
1处:第一个括号的内容。 - 在索引
2处:第二个括号的内容。 - ……等等……
let str='<h1>Hello, world! gogo</h1>';
let tag=str.match(/<(.*?)>/);
console.log(tag);
console.log(tag[0]);//<h1>
console.log(tag[1]);//h1
解析:方法str.match(regexp),如果regexp没有修饰符g,将查找第一个匹配项,并将它作为数组返回:
tag的输出: "[ <h1>', 'h1', index: 0, input: '<h1>Hello, world! gogo', groups: undefined ]"
let str='<h1s 23>Hello, world! gogo</h1s>';
console.log(str.match(/(\d)(\w)(\s)/));//['1s ','1','s', ' ',]
str='<h1s 23>2d Hello, world! gogo</h1s>';
console.log(str.match(/(\d)(\w)(\s)/g));//全局匹配下 [ '1s ', '2d ' ]
嵌套组
括号可以嵌套。在这种情况下,编号也从左到右
可选组
即使组是可选的,并且在匹配中不存在(例如,具有量词 `(...)?`),也存在相应的result数组项,并且等于undefined
对正则表达式a(z)?(c)?,查找a,后面是可选的z,在后面是可选的c
let match = 'a'.match(/a(z)?(c)?/);
console.log(match);//[ 'ac', undefined, undefined]
console.log( match.length ); // 3
console.log( match[0] ); // a(完整的匹配项)
console.log( match[1] ); // undefined
console.log( match[2] ); // undefined
对字符串ac的匹配
let match = 'ac'.match(/a(z)?(c)?/);
console.log(match);//[ 'ac', undefined, 'c']
console.log( match.length ); // 3
console.log( match[0] ); // a(完整的匹配项)
console.log( match[1] ); // undefined
console.log( match[2] ); // c(完整的匹配项)
带有组搜索的所有匹配项 matchAll
当正则表达式使用修饰符g时会搜索所有匹配项,match方法会返回由匹配项组成的结果数组,但是不会返回组的内容。
let str='<h1s 23>2d Hello, world! gogo</h1s>';
console.log(str.match(/(\d)(\w)(\s)/g));//全局匹配下 [ '1s ', '2d ' ]
let str='<h1s 23>2d Hello, world! gogo</h1s>';
console.log(str.match(/(\d)(\w)(\s)/g));//全局匹配下 [ '1s ', '2d ' ]
console.log(str.matchAll(/(\d)(\w)(\s)/g));//Object [RegExp String Iterator] {}
console.log(Array.from(str.matchAll(/(\d)(\w)(\s)/g)));
结果:[
[
'1s ','1','s',' ',
index: 2,
input: '<h1s 23>2d Hello, world! gogo</h1s>',
groups: undefined
],['2d ','2','d',' ',index: 8,
input: '<h1s 23>2d Hello, world! gogo</h1s>',
groups: undefined
]
] 如果想要返回组的内容,应该使用str.matchAll(regexp)进行搜索
命名括号
在左括号后紧跟着放置?<name>,即可完成对括号的命名
let dateRegexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/g;
let str = "2019-10-30 2020-01-01";
console.log(Array.from(str.matchAll(dateRegexp)));
let results = str.matchAll(dateRegexp);
for (let result of results) {
let { day,year, month } = result.groups;
console.log(`${day}.${month}.${year}`);
//第一个 30.10.2019
//第二个01.01.2020
}
替换中的捕获组
让我们能够替换 `str` 中 `regexp` 的所有匹配项的方法 `str.replace(regexp, replacement)` 允许我们在 `replacement` 字符串中使用括号中的内容。这使用 `$n` 来完成,其中 `n` 是组号。
let str = "John Bull"; let regexp = /(\w+) (\w+)/; alert( str.replace(regexp, '$2, $1') ); // Bull, John
对于命名括号,引用为$<name>
let regexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/g; let str = "2019-10-30, 2020-01-01"; alert( str.replace(regexp, '$<day>.$<month>.$<year>') ); // 30.10.2019, 01.01.2020
非捕获组?:
使用?:来排除组--当我们希望匹配,但不希望捕获组不出现在结果中时候 例如:当我们需要查找(go)+,但是不希望括号内容(go)作为一个单独的数组项,则可以编写(?:go)+
let str = "Gogogo John!";
// ?: 从捕获组中排除 'go'
let result = str.match(/(?:go)+ (\w+)/i);
console.log(result);// 'Gogogo John','John',
console.log( result[0] ); // Gogogo John(完整的匹配项)
console.log( result[1] ); // John
console.log( result.length ); // 2(在数组中没有其他数组项)
console.log(str.match(/(\w+)/i));// 'Gogogo', 'Gogogo'
练习:
编写一个正则表达式,找出所有十进制数字,包括整数、浮点数和负数。
let str="-1.5 0 2 -123.4.";
let regexp=/-?\d+(\.\d+)?/gi;
console.log(str.match(regexp));
模式中的反向引用:\N和\K
我们不仅可以在结果或替换字符串中使用捕获组{...}的内容,还可以在模式本身中使用它们。
## 按编号反向引用 \N
我们可以在\N在模式中引用一个组,其中N是组号
任务:我们需要找到带引号的字符串,单引号'...'或双引号"..."——应匹配这两种变体。为了确保单引号和单引号配对,双引号和双引号配对,我们可以将其包装到捕获组中并对其进行反向引用(['"])(.*?)\1
let str = `He said: "She's the one!".`;
let regexp = /(['"])(.*?)\1/g;
console.log( str.match(regexp) ); // "She's the one!"
正则表达式会找到第一个引号['"]并记住其中内容。那是第一个捕获组
在模式中,\1表示找到与第一组相同的文本,在我们的实例中为完全相同的引号。
于此类推,\2表示与第二组相同的内容,、3——第三分组,以此类推
在模式中用\1,在替换项中用$1
## 按命名进行反向引用:\k<name>
如果一个正则表达式中有很多括号,给它们取个名字会便于引用
要引用命名的捕获组,我们可以使用:\k<name>
在下面的示例中,带引号的组被命名为?<quote>,因此反向引用为\k<quote>
let str = `He said: "She's the one!".`;
let regexp = /(?<quote>['"])(.*?)\k<quote>/g;
console.log( str.match(regexp) ); // "She's the one!"
# 选择OR|
方括号只允许字符或字符类,选择允许任何任何表达式。
正则表达式A|B|C表示表达式A、B或C其一均可
| 符号 | 匹配 | ||
|---|---|---|---|
| gra | ey | 匹配gra或 ey | |
| gr(a | e)y | 匹配gray或grey | |
| gr[a | e]y | 等同于 gr(a | e)y |
- `I love HTML|CSS` 匹配 `I love HTML` 或 `CSS`。
- `I love (HTML|CSS)` 匹配 `I love HTML` 或 `I love CSS`。
- `I love [HTML|CSS]` 等同于`I love (HTML|CSS)`。
## 示例:用于时间匹配的正则表达式
之前构建过对时间hh:mm的字符串,我们使用的是/d/d:/d/d这个正则表达式,但它不精确,比如30:99也会满足要求
如何构建更好的模式呢?
首先,对于时针:
-
如果第一位数字是0或1,那么下一位数可以是任何数值:[01]\d -
如果第一位数字是2,那么下一位数字必须是[0-3],即2[0-3] -
时针的正则表达式为[01]\d|2[0-3]不允许其他的首位数
对于分针必须是00-59的数字,写成正则表达式则为[0-5]\d 将时针和分针组合到一起可以得到:[01]\d|2[0-3]:[0-5]\d
[01]\d | 2[0-3]:[0-5]\d
但是问题来了,选择|恰好位于[01]\d和2[0-3]:[0-5]\d之间,它只会匹配符号左边或符号右边的其中一个表达式,只能匹配左边的[01]\d或右边的 2[0-3]:[0-5]\d。这是错误的,应该只在正则表达式的小时部分选择使用,以允许[01]\d或2[0-3]其中一个,([01]\d|2[0-3]):[0-5]\d
最终解决方案:
let regexp = /([01]\d|2[0-3]):[0-5]\d/g;
console.log("00:00 10:10 23:59 25:99 1:2".match(regexp)); // 00:00,10:10,23:59
#前瞻断言与后瞻断言 需要根据前后上下文匹配某些内容的时候,及一个模式在另一个模式之前或之后的才匹配这个模式。这种特殊的语法称之为“前瞻断言”“后瞻断言”。 注意断言只是一个测试,括号中的内容不包含在匹配结果中,既满足条件时,才匹配x,y不匹配
| || 标题 | | | --- | --- | --- | | 前瞻断言|x(?=y)| x后面是y时,才匹配x | | 否定的前瞻断言|x(?!y)| x后面不是y时,才匹配x | |后瞻断言 |(?<=y)x| x前面是y时,才匹配x | |否达的后瞻断言 |(?<!y)x| x前面不是y时,才匹配x | 示例:后面跟着一个€的整数,正则表达式应该是\d(?=€)
## 捕获组
断言中括号内的元素并不会称为结果的一部分。如果想要使用括号中的内容,需要使用捕获组,将许雅的部分包装在额外的括号里
let str="1 turkey costs 30€";
let regexp=/\d+(?=€)/;
console.log(str.match(regexp));//30
想要使得,前瞻性/后瞻性断言成为结果的一部分——加括号,把先要保留的部分括起来
let str="1 turkey costs 30€";
regexp=/\d+(?=(€|kr))/;
console.log(str.match(regexp));// '30','€',
# 灾难性回溯
执行起来耗时非常长,甚至导致js引擎“挂起”。在这种情况下浏览器会终止脚本并重新加载页面,挂起服务器进程
## 是什么造成了灾难性回溯
举个例子,我们使用正则表达式^(\d+)*$,匹配(123456789)z,在模式中$匹配字符串的结尾,但是我们的例子里有z,所以匹配失败
由于没有匹配结果,贪婪量词+的重复匹配次数会减一,并回溯一个字符,\d+匹配12345678.
然后引擎从9的位置搜索,(\d+)*匹配成功,引擎再次去尝试匹配结尾$,但又失败了,因为遇到了z
没有匹配结果,所以引擎继续回溯,减少重复匹配次数。回溯通常是这样工作的:最后一个贪婪量词逐渐减少重复次数,直到达到最小值。然后前一个贪婪量词再减少重复次数,以此类推。
引擎会尝试所有的排列组合,123456789可以有很多种拆分方式。准确的说有`2n-1` 种,其中 `n` 是序列的长度。
搜索会花这么长时间就是因为在一个一个的尝试这么多种组合
## 解决方案
占有型量词,在常规量词后面添加+,也就是说,我们可以使用 `\d++` 替代 `\d+` 来阻止 `+` 回溯。占有型量词可以实现尽可能的多的匹配,并且没有任何回溯
重写正则表达式,降低可能的组合数量
防止回溯
#粘性修饰符'y'
修饰符y使正则表达式精确所搜位置lastIndex,而不是从它开始搜索