一次性搞懂JavaScript正则表达式之语法

本文是『horseshoe·Regex专题』系列文章之一,后续会有更多专题推出javascript

GitHub地址:github.com/veedrin/hor…前端

博客地址(文章排版真的很漂亮):matiji.cnjava

若是以为对你有帮助,欢迎来GitHub点Star或者来个人博客亲口告诉我git


名余曰正则兮,字余曰灵均。github

Regular Expressions翻译成中文叫正则表达式。也不知道是谁翻译过来的,听起来就很严肃。彷佛翻译成通用表达式更能传达其精髓,若是你不怕梦见屈原的话。正则表达式

为何叫通用表达式?由于它有一套和编程语言无关的文本匹配规则。不少语言都实现了正则表达式的文本匹配引擎,只不过在功能集合上略有不一样。express

咱们要记住的是三点:编程

其一,正则表达式是用来提取文本的。编程语言

其二,正则表达式的表达能力强大到使人发指。函数

其三,正则表达式的语法对初学者不友好。

另外,本专题只涉及JavaScript语言的正则表达式,其余语言的规则可能略有不一样。

我还为各位读者准备了一副宣传语,应该能让你心动(点赞)吧?

学一门前端工具,几年就过期了。学正则表达式,受用一生。

普通字符

什么叫普通字符?

当咱们写a的时候,咱们指的就是a;当咱们写的时候,咱们指的就是

'hello 😀 regex'.match(/😀/);
// ["😀", index: 6, input: "hello 😀 regex", groups: undefined]
复制代码

这就是普通字符,它在正则中的含义就是检索它自己。除了正则规定的部分字符外,其他的都是普通字符,包括各类人类语言,包括emoji,只要可以表达为字符串。

开始与结束

^字符的英文是caret,翻译成中文是脱字符。不要问我,又不是我翻译的。它在正则中属于元字符,一般表明的意义是文本的开始。说一般是由于当它在字符组中[^abc]另有含义。

什么叫文本的开始?就是若是它是正则主体的第一个符号,那紧跟着它的字符必须是被匹配文本的第一个字符。

'regex'.match(/^r/);
// ["r", index: 0, input: "regex", groups: undefined]
复制代码

问题来了,若是^不是正则的第一个符号呢?

'regex'.match(/a^r/);
// null
复制代码

因此呀,关于它有三点须要注意:

  • 做为匹配文本开始元字符的时候必须是正则主体的第一个符号,不然正则无效。
  • 它匹配的是一个位置,而不是具体的文本。
  • 它在其余规则中有另外的含义。

$字符与^正好相反。它表明文本的结束,而且没有其余含义(实际上是有的,但不是在正则主体内)。一样,它必须是正则主体的最后一个符号。

'regex'.match(/x$/);
// ["x", index: 4, input: "regex", groups: undefined]
复制代码

^$特殊的地方在于它匹配的是一个位置。位置不像字符,它看不见,因此更不容易理解。

转义

咱们如今已经知道$匹配文本的结束位置,它是元字符。可是若是我想匹配$自己呢?匹配一个美圆符号的需求再常见不过了吧。因此咱们得将它贬为庶民。

\反斜杠就是干这个的。

'price: $3.6'.match(/\$[0-9]+\.[0-9]+$/);
// ["$3.6", index: 7, input: "price: $3.6", groups: undefined]
复制代码

上面的例子有点超纲了,超纲的部分先无论。

你能够认为\也是一个元字符,它跟在另外一个元字符后面,就能还原它原本的含义。

若是有两个\呢?那就是转义自身了。若是有三个\呢?咱们得分红两段去理解。以此类推。

普通字符前面跟了一个\是什么效果?首先它们是一个总体,而后普通字符转义后仍是普通字符。

带反斜杠的元字符

通常来讲,普通字符前面带反斜杠仍是普通字符,可是有一些普通字符,带反斜杠后反而变成了元字符。

要怪只能怪计算机领域的经常使用符号太少了。

元字符 含义
\b 匹配一个单词边界(boundary)
\B 匹配一个非单词边界
\d 匹配一个数字字符(digit)
\D 匹配一个非数字字符
\s 匹配一个空白字符(space)
\S 匹配一个非空白字符
\w 匹配一个字母或者一个数字或者一个下划线(word)
\W 匹配一个字母、数字和下划线以外的字符

你这么聪明,确定一眼就看出来,大写表明反义。对,就是这么好记。

\b元字符

\b匹配的也是一个位置,而不是一个字符。单词和空格之间的位置,就是所谓单词边界。

'hello regex'.match(/\bregex$/);
// ["regex", index: 6, input: "hello regex", groups: undefined]
'hello regex'.match(/\Bregex$/);
// null
复制代码

所谓单词边界,对中文等其余语言是无效的。

'jiangshuying gaoyuanyuan huosiyan'.match(/\bgaoyuanyuan\b/);
// ["gaoyuanyuan", index: 13, input: "jiangshuying gaoyuanyuan huosiyan", groups: undefined]
'江疏影 高圆圆 霍思燕'.match(/\b高圆圆\b/);
// null
复制代码

因此\b翻译一下就是^\w|\w$|\W\w|\w\W

\d元字符

\d匹配一个数字,注意,这里的数字不是指JavaScript中的数字类型,由于文本全是字符串。它指的是表明数字的字符。

'123'.match(/\d/);
// ["1", index: 0, input: "123", groups: undefined]
复制代码

\s元字符

\s匹配一个空白字符。

这里须要解释一下什么是空白字符。

空白字符不是空格,它是空格的超集。不少人说它是\f\n\r\t\v的总和,其中\f是换页符,\n是换行符,\r是回车符,\t是水平制表符,\v是垂直制表符。是这样么?

'a b'.match(/\w\s\w/);
// ["a b", index: 0, input: "a b", groups: undefined]
'a b'.match(/\w\f\w/);
// null
'a b'.match(/\w\n\w/);
// null
'a b'.match(/\w\r\w/);
// null
'a b'.match(/\w\t\w/);
// null
'a b'.match(/\w\v\w/);
// null
'a b'.match(/\w \w/);
// ["a b", index: 0, input: "a b", groups: undefined]
复制代码

这样说的人,明显是没有作过实验。其实正确的写法是空格\f\n\r\t\v的总和,集合里面包含一个空格,可千万别忽略了。诶,难道空格在正则中的写法就是空一格么,是的,就是这样随意。

这个集合中不少都是不可打印字符,估计只有\n是咱们的老朋友。因此,若是不须要区分空格和换行的话,那就大胆的用\s吧。

\w元字符

\w匹配一个字母或者一个数字或者一个下划线。为何要将它们放一块儿?想想JavaScript中的变量规则,包括不少应用的用户名都只能是这三样,因此把它们放一块儿挺方便的。

不过要注意,字母指的是26个英文字母,其余的不行。

'正则'.match(/\w/);
// null
复制代码

负阴抱阳

若是咱们将大写和小写的带反斜杠的元字符组合在一块儿,就能匹配任何字符。是的,不针对任何人。

'@regex'.match(/[\s\S]/);
// ["@", index: 0, input: "@regex", groups: undefined]
复制代码

方括号的含义咱们先按下不表。

道生一

.在正则中的含义仙风道骨,它匹配换行符以外的任意单个字符。

若是文本不存在换行符,那么.[\b\B][\d\D][\s\S][\w\W]是等价的。

若是文本存在换行符,那么(.|\n)[\b\B][\d\D][\s\S][\w\W]是等价的。

'@regex'.match(/./);
// ["@", index: 0, input: "@regex", groups: undefined]
复制代码

量词

前面咱们一直在强调,一个元字符只匹配一个字符。即使强大如.它也只能匹配一个。

那匹配gooooogle的正则是否是得写成/gooooogle/呢?

正则冷笑,并向你发射一个蔑视。

若是匹配的模式有重复,咱们能够声明它重复的次数。

量词 含义
? 重复零次或者一次
+ 重复一次或者屡次,也就是至少一次
* 重复零次或者屡次,也就是任意次数
{n} 重复n次
{n,} 重复n次或者更屡次
{n,m} 重复n次到m次之间的次数,包含n次和m次

有三点须要注意:

  • ?在诸如匹配http协议的时候很是有用,就像这样:/http(s)?/。它在正则中除了是量词还有别的含义,后面会提到。

  • 咱们习惯用/.*/来匹配若干对咱们没有价值的文本,它的含义是若干除换行符以外的字符。好比咱们须要文本两头的格式化信息,中间是什么无所谓,它就派上用场了。不过它的性能可很差。

  • {n,m}之间不能有空格,空格在正则中是有含义的。

关于量词最使人困惑的是:它重复什么?

它重复紧贴在它前面的某个集合。第一点,必须是紧贴在它前面;第二点,重复一个集合。最多见的集合就是一个字符,固然正则中有一些元字符可以将若干字符变成一个集合,后面会讲到。

'gooooogle'.match(/go{2,5}gle/);
// ["gooooogle", index: 0, input: "gooooogle", groups: undefined]
复制代码

若是一个量词紧贴在另外一个量词后面会怎样?

'gooooogle'.match(/go{2,5}+gle/);
// Uncaught SyntaxError: Invalid regular expression: /go{2,5}+gle/: Nothing to repeat
复制代码

贪婪模式与非贪婪模式

前面提到量词不能紧跟在另外一个量词后面,立刻要👋👋打脸了。

'https'.match(/http(s)?/);
// ["https", "s", index: 0, input: "https", groups: undefined]
'https'.match(/http(s)??/);
// ["http", undefined, index: 0, input: "https", groups: undefined]
复制代码

然而,个人脸是这么好打的?

紧跟在?后面的?它不是一个量词,而是一个模式切换符,从贪婪模式切换到非贪婪模式。

贪婪模式在正则中是默认的模式,就是在既定规则之下匹配尽量多的文本。由于正则中有量词,它的重复次数多是一个区间,这就有了取舍。

紧跟在量词以后加上?就能够开启非贪婪模式。怎么省事怎么来。

这里的要点是,?必须紧跟着量词,不然的话它本身就变成量词了。

字符组

正则中的普通字符只能匹配它本身。若是我要匹配一个普通字符,可是我不肯定它是什么,怎么办?

'grey or gray'.match(/gr[ae]y/);
// ["grey", index: 0, input: "grey or gray", groups: undefined]
复制代码

方括号在正则中表示一个区间,咱们称它为字符组。

首先,字符组中的字符集合只是全部的可选项,最终它只能匹配一个字符。

而后,字符组是一个独立的世界,元字符不须要转义。

'$'.match(/[$&@]/);
// ["$", index: 0, input: "$", groups: undefined]
复制代码

最后,有两个字符在字符组中有特殊含义。

^在字符组中表示取反,再也不是文本开始的位置了。

'regex'.match(/[^abc]/);
// ["r", index: 0, input: "regex", groups: undefined]
复制代码

若是我就要^呢?前面已经讲过了,转义。

-原本是一个普通字符,在字符组中摇身一变成为连字符。

'13'.match(/[1-9]3/);
// ["13", index: 0, input: "13", groups: undefined]
复制代码

连字符的意思是匹配范围在它的左边字符和右边字符之间。

若是我这样呢?

'abc-3'.match(/[0-z]/);
// ["a", index: 0, input: "abc-3", groups: undefined]
复制代码
'xyz-3'.match(/[0-c]/);
// ["3", index: 4, input: "xyz-3", groups: undefined]
复制代码
'xyz-3'.match(/[0-$]/);
// Uncaught SyntaxError: Invalid regular expression: /[0-$]/: Range out of order in character class
复制代码

发现什么了没有?只有两种字符是能够用连字符的:英文字母和数字。并且英文字母能够和数字连起来,英文字母的顺序在后面。这和扑克牌1 2 3 4 5 6 7 8 9 10 J Q K是一个道理。

捕获组与非捕获组

咱们已经知道量词是怎么回事了,咱们也知道量词只能重复紧贴在它前面的字符。

若是我要重复的是一串字符呢?

'i love you very very very much'.match(/i love you very +much/);
// null
'i love you very very very much'.match(/i love you v+e+r+y+ +much/);
// null
复制代码

这样确定是不行的。是时候请圆括号出山了。

'i love you very very very much'.match(/i love you (very )+much/);
// ["i love you very very very much", "very ", index: 0, input: "i love you very very very much", groups: undefined]
复制代码

圆括号的意思是将它其中的字符集合打包成一个总体,而后量词就能够操做这个总体了。这和方括号的效果是彻底不同的。

并且默认的,圆括号的匹配结果是能够捕获的。

正则内捕获

如今咱们有一个需求,匹配<div>标签。

'<div>hello regex</div>'.match(/<div>.*<\/div>/);
// ["<div>hello regex</div>", index: 0, input: "<div>hello regex</div>", groups: undefined]
复制代码

这很简单。但若是我要匹配的是任意标签,包括自定义的标签呢?

'<App>hello regex</App>'.match(/<([a-zA-Z]+)>.*<\/\1>/);
// ["<App>hello regex</App>", "App", index: 0, input: "<App>hello regex</App>", groups: undefined]
复制代码

这时候就要用到正则的捕获特性。正则内捕获使用\数字的形式,分别对应前面的圆括号捕获的内容。这种捕获的引用也叫反向引用

咱们来看一个更复杂的状况:

'<App>hello regex</App><p>A</p><p>hello regex</p>'.match(/<((A|a)pp)>(hello regex)+<\/\1><p>\2<\/p><p>\3<\/p>/);
// ["<App>hello regex</App><p>A</p><p>hello regex</p>", "App", "A", "hello regex", index: 0, input: "<App>hello regex</App><p>A</p><p>hello regex</p>", groups: undefined]
复制代码

若是有嵌套的圆括号,那么捕获的引用是先递归的,而后才是下一个顶级捕获。

正则外捕获

'@abc'.match(/@(abc)/);
// ["@abc", "abc", index: 0, input: "@abc", groups: undefined]
RegExp.$1;
// "abc"
复制代码

没错,RegExp就是构造正则的构造函数。若是有捕获组,它的实例属性$数字会显示对应的引用。

若是有多个正则呢?

'@abc'.match(/@(abc)/);
// ["@abc", "abc", index: 0, input: "@abc", groups: undefined]
'@xyz'.match(/@(xyz)/);
// ["@xyz", "xyz", index: 0, input: "@xyz", groups: undefined]
RegExp.$1;
// "xyz"
复制代码

RegExp构造函数的引用只显示最后一个正则的捕获。

另外还有一个字符串实例方法也支持正则捕获的引用,它就是replace方法。

'hello **regex**'.replace(/\*{2}(.*)\*{2}/, '<strong>$1</strong>');
// "hello <strong>regex</strong>"
复制代码

实际上它才是最经常使用的引用捕获的方式。

捕获命名

这是ES2018的新特性。

使用\数字引用捕获必须保证捕获组的顺序不变。如今开发者能够给捕获组命名了,有了名字之后,引用起来更加肯定。

'<App>hello regex</App>'.match(/<(?<tag>[a-zA-Z]+)>.*<\/\k<tag>>/);
// ["<App>hello regex</App>", "App", index: 0, input: "<App>hello regex</App>", groups: {tag: "App"}]
复制代码

在捕获组内部最前面加上?<key>,它就被命名了。使用\k<key>语法就能够引用已经命名的捕获组。

是否是很简单?

一般状况下,开发者只是想在正则中将某些字符当成一个总体看待。捕获组很棒,可是它作了额外的事情,确定须要额外的内存占用和计算资源。因而正则又有了非捕获组的概念。

'@abc'.match(/@(abc)/);
// ["@abc", "abc", index: 0, input: "@abc", groups: undefined]
'@abc'.match(/@(?:abc)/);
// ["@abc", index: 0, input: "@abc", groups: undefined]
复制代码

只要在圆括号内最前面加上?:标识,就是告诉正则引擎:我只要这个总体,不须要它的引用,你就别费劲了。从上面的例子也能够看出来,match方法返回的结果有些许不同。

我的观点:我以为正则的捕获设计应该反过来,默认不捕获,加上?:标识后才捕获。由于大多数时候开发者是不须要捕获的,可是它又懒得加?:标识,会有些许性能浪费。

分支

有时候开发者须要在正则中使用或者

'高圆圆'.match(/陈乔恩|高圆圆/);
// ["高圆圆", index: 0, input: "高圆圆", groups: undefined]
复制代码

|就表明或者。字符组其实也是一个多选结构,可是它们俩有本质区别。字符组最终只能匹配一个字符,而分支匹配的是左边全部的字符或者右边全部的字符。

咱们来看一个例子:

'我喜欢高圆圆'.match(/我喜欢陈乔恩|高圆圆/);
// ["高圆圆", index: 3, input: "我喜欢高圆圆", groups: undefined]
复制代码

由于|是将左右两边一切两半,而后匹配左边或者右边。因此上面的正则显然达不到咱们想要的效果。这个时候就须要一个东西来缩小分支的范围。诶,你可能已经想到了:

'我喜欢高圆圆'.match(/我喜欢(?:陈乔恩|高圆圆)/);
// ["我喜欢高圆圆", index: 0, input: "我喜欢高圆圆", groups: undefined]
复制代码

没错,就是圆括号。

零宽断言

正则中有一些元字符,它不匹配字符,而是匹配一个位置。好比以前提到的^$^的意思是说这个位置应该是文本开始的位置。

正则还有一些比较高级的匹配位置的语法,它匹配的是:在这个位置以前或以后应该有什么内容。

零宽(zero-width)是什么意思?指的就是它匹配一个位置,自己没有宽度。

断言(assertion)是什么意思?指的是一种判断,断言以前或以后应该有什么或应该没有什么。

零宽确定先行断言

所谓的确定就是判断有什么,而不是判断没有什么。

而先行指的是向前看(lookahead),断言的这个位置是为前面的规则服务的。

语法很简单:圆括号内最左边加上?=标识。

'CoffeeScript JavaScript javascript'.match(/\b\w{4}(?=Script\b)/);
// ["Java", index: 13, input: "CoffeeScript JavaScript javascript", groups: undefined]
复制代码

上面匹配的是四个字母,这四个字母要知足如下条件:紧跟着的应该是Script字符串,并且Script字符串应该是单词的结尾部分。

因此,零宽确定先行断言的意思是:如今有一段正则语法,用这段语法去匹配给定的文本。可是,知足条件的文本不只要匹配这段语法,紧跟着它的必须是一个位置,这个位置又必须知足一段正则语法。

说的再直白点,我要匹配一段文本,可是这段文本后面必须紧跟着另外一段特定的文本。零宽确定先行断言就是一个界碑,我要知足前面和后面全部的条件,可是我只要前面的文本。

咱们来看另外一种状况:

'CoffeeScript JavaScript javascript'.match(/\b\w{4}(?=Script\b)\w+/);
// ["JavaScript", index: 13, input: "CoffeeScript JavaScript javascript", groups: undefined]
复制代码

上面的例子更加直观,零宽确定先行断言已经匹配过Script一次了,后面的\w+却仍是能匹配Script成功,足以说明它的零宽特性。它为紧贴在它前面的规则服务,而且不影响后面的匹配规则。

零宽确定后行断言

先行是向前看,那后行就是向后看(lookbehind)咯。

语法是圆括号内最左边加上?<=标识。

'演员高圆圆 将军霍去病 演员霍思燕'.match(/(?<=演员)霍\S+/);
// ["霍思燕", index: 14, input: "演员高圆圆 将军霍去病 演员霍思燕", groups: undefined]
复制代码

一个正则能够有多个断言:

'演员高圆圆 将军霍去病 演员霍思燕'.match(/(?<=演员)霍.+?(?=\s|$)/);
// ["霍思燕", index: 14, input: "演员高圆圆 将军霍去病 演员霍思燕", groups: undefined]
复制代码

零宽否认先行断言

确定是判断有什么,否认就是判断没有什么咯。

语法是圆括号内最左边加上?!标识。

'TypeScript Perl JavaScript'.match(/\b\w{4}(?!Script\b)/);
// ["Perl", index: 11, input: "TypeScript Perl JavaScript", groups: undefined]
复制代码

零宽否认后行断言

语法是圆括号最左边加上?<!标识。

'演员高圆圆 将军霍去病 演员霍思燕'.match(/(?<!演员)霍\S+/);
// ["霍去病", index: 8, input: "演员高圆圆 将军霍去病 演员霍思燕", groups: undefined]
复制代码

修饰符

正则表达式除了主体语法,还有若干可选的模式修饰符。

写法就是将修饰符安插在正则主体的尾巴上。好比这样:/abc/gi

g修饰符

gglobal的缩写。默认状况下,正则从左向右匹配,只要匹配到告终果就会收工。g修饰符会开启全局匹配模式,找到全部匹配的结果。

'演员高圆圆 将军霍去病 演员霍思燕'.match(/(?<=演员)\S+/);
// ["高圆圆", index: 2, input: "演员高圆圆 将军霍去病 演员霍思燕", groups: undefined]
'演员高圆圆 将军霍去病 演员霍思燕'.match(/(?<=演员)\S+/g);
// ["高圆圆", "霍思燕"]
复制代码

i修饰符

iignoreCase的缩写。默认状况下,/z/是没法匹配Z的,因此咱们有时候不得不这样写:/[a-zA-Z]/i修饰符能够全局忽略大小写。

不少时候咱们不在意文本是大写、小写仍是大小写混写,这个修饰符仍是颇有用的。

'javascript is great'.match(/JavaScript/);
// null
'javascript is great'.match(/JavaScript/i);
// ["javascript", index: 0, input: "javascript is great", groups: undefined]
复制代码

m修饰符

mmultiline的缩写。这个修饰符有特定起做用的场景:它要和^$搭配起来使用。默认状况下,^$匹配的是文本的开始和结束,加上m修饰符,它们的含义就变成了行的开始和结束。

` abc xyz `.match(/xyz/);
// ["xyz", index: 5, input: "↵abc↵xyz↵", groups: undefined]
` abc xyz `.match(/^xyz$/);
// null
` abc xyz `.match(/^xyz$/m);
// ["xyz", index: 5, input: "↵abc↵xyz↵", groups: undefined]
复制代码

y修饰符

这是ES2015的新特性。

ysticky的缩写。y修饰符有和g修饰符重合的功能,它们都是全局匹配。因此重点在sticky上,怎么理解这个粘连呢?

g修饰符不挑食,匹配完一个接着匹配下一个,对于文本的位置没有要求。可是y修饰符要求必须从文本的开始实施匹配,由于它会开启全局匹配,匹配到的文本的下一个字符就是下一次文本的开始。这就是所谓的粘连。

'a bag with a tag has a mag'.match(/\wag/g);
// ["bag", "tag", "mag"]
'a bag with a tag has a mag'.match(/\wag/y);
// null
'bagtagmag'.match(/\wag/y);
// ["bag", index: 0, input: "bagtagmag", groups: undefined]
'bagtagmag'.match(/\wag/gy);
// ["bag", "tag", "mag"]
复制代码

有人确定发现了猫腻:你不是说y修饰符是全局匹配么?看上面的例子,单独一个y修饰符用match方法怎么并非全局匹配呢?

诶,这里说来就话长了。

长话短说呢,就涉及到y修饰符的本质是什么。它的本质有二:

  • 全局匹配(先别着急打我)。
  • 从文本的lastIndex位置开始新的匹配。lastIndex是什么?它是正则表达式的一个属性,若是是全局匹配,它用来标注下一次匹配的起始点。这才是粘连的本质所在。

不知道大家发现什么了没有:lastIndex是正则表达式的一个属性。而上面例子中的match方法是做用在字符串上的,都没有lastIndex属性,休怪人家工做不上心。

const reg = /\wag/y;
reg.exec('bagtagmag');
// ["bag", index: 0, input: "bagtagmag", groups: undefined]
reg.exec('bagtagmag');
// ["tag", index: 3, input: "bagtagmag", groups: undefined]
reg.exec('bagtagmag');
// ["mag", index: 6, input: "bagtagmag", groups: undefined]
复制代码

我们换成正则方法exec,屡次执行,正则的lastIndex在变,匹配的结果也在变。全局匹配无疑了吧。

s修饰符

这是ES2018的新特性。

s不是dotAll的缩写。s修饰符要和.搭配使用,默认状况下,.匹配除了换行符以外的任意单个字符,然而它尚未强大到无所不能的地步,因此正则索性给它开个挂。

s修饰符的做用就是让.能够匹配任意单个字符。

ssingleline的缩写。

` abc xyz `.match(/c.x/);
// null
` abc xyz `.match(/c.x/s);
// ["c↵x", index: 3, input: "↵abc↵xyz↵", groups: undefined]
复制代码

u修饰符

这是ES2015的新特性。

uunicode的缩写。有一些Unicode字符超过一个字节,正则就没法正确的识别它们。u修饰符就是用来处理这些不常见的状况的。

'𠮷'.match(/^.$/);
// null
'𠮷'.match(/^.$/u);
// ["𠮷", index: 0, input: "𠮷", groups: undefined]
复制代码

𠮷,与同义。

笔者对Unicode认识尚浅,这里不过多展开。


本文是『horseshoe·Regex专题』系列文章之一,后续会有更多专题推出

GitHub地址:github.com/veedrin/hor…

博客地址(文章排版真的很漂亮):matiji.cn

若是以为对你有帮助,欢迎来GitHub点Star或者来个人博客亲口告诉我

Regex专题一览

👉 语法

👉 方法

👉 引擎

相关文章
相关标签/搜索