正则表达式二——总结 各种字符代表什么意思

640 阅读9分钟

字符类

字符类是一种特殊的符号,用于匹配特定集合中的符号

字符意义
\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
  • 数字(Number)N

    • 十进制数字(decimal digit)Nd
    • 字母数字(letter number)Nl
    • 其它(other)No
  • 标点符号(Punctuation)P

    • 连接符(connector)Pc
    • 横杠(dash)Pd
    • 起始引号(initial quote)Pi
    • 结束引号(final quote)Pf
    • 开(open)Ps
    • 闭(close)Pe
    • 其它(other)Po
  • 标记(Mark)M(accents etc):

    • 间隔合并(spacing combining)Mc
    • 封闭(enclosing)Me
    • 非间隔(non-spacing)Mn
  • 符号(Symbol)S

    • 货币(currency)Sc
    • 修饰(modifier)Sk
    • 数学(math)Sm
    • 其它(other)So
  • 分隔符(Separator)Z

    • 行(line)Zl
    • 段落(paragraph)Zp
    • 空格(space)Zs
  • 其它(Other)C

    • 控制符(control)Cc
    • 格式(format)Cf
    • 未分配(not assigned)Cn
    • 私有(private use)Co
    • 代理伪字符(surrogate)Cs
    • 中文字符 sc=Han
    • 西里尔字符 sc=Cyrillic
    • 十六进制 Hex_Digit

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,将查找第一个匹配项,并将它作为数组返回:

  1. 在索引 0 处:完整的匹配项。
  2. 在索引 1 处:第一个括号的内容。
  3. 在索引 2 处:第二个括号的内容。
  4. ……等等……
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 ' ]

嵌套组

括号可以嵌套。在这种情况下,编号也从左到右

image.png

可选组

即使组是可选的,并且在匹配中不存在(例如,具有量词 `(...)?`),也存在相应的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
}

image.png

替换中的捕获组

让我们能够替换 `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表示表达式AB或C其一均可
符号匹配
graey匹配gra或 ey
gr(ae)y匹配gray或grey
gr[ae]y等同于 gr(ae)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这个正则表达式,但它不精确,比如3099也会满足要求
如何构建更好的模式呢?
首先,对于时针:
  • 如果第一位数字是01,那么下一位数可以是任何数值:[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,而不是从它开始搜索