一、正则表达式的概念

正则表达式(Regular Expression,常简写为RegExp或RE),又称规则表达式,通常用来检索、替换那些符合某个模式(规则)的文本。

正则表达式是为字符串处理而生的,它使用特定的格式,来检测字符串中的某个特定模式,如以下的表达式用来检测字符串是否是三个数字构成的:

/^\d{3}$/

我们逐个来分析上面的每个部分。

首先,两侧的斜线/ ... /是正则表达式的特定标志,它表示中间的部分是一个正则表达式。^是匹配字符串的开头,但它并不会消耗字符。\d表示要匹配一个0-9的数字。{3}表示要将上述规则匹配三次。而$表示要匹配字符串的结尾。

这个正则表达式所要描述的规则是这样的:从字符串的开头到结尾,中间必须是三个数字构成。

现在我们拿这个正则表达式,来检测一下下面的几个字符串是否符合该模式吧:

/^\d{3}$/.test('369');   // true

/^\d{3}$/.test('023');   // true

/^\d{3}$/.test('1234');  // false

/^\d{3}$/.test('1d3');   // false

/^\d{3}$/.test('abcd');  // false

可以看到,只有被检测的字符串是由三个数字构成的时候,检测结果才是true,否则一律为false。有了这个正则表达式,我们就可以很容易检测任意一个字符串是否是三个数字构成的。

除了检测整个字符串是否符合某个模式外,正则表达式还可以从字符串中捕获出符合某个特定模式的子字符串。比如我们想从一个字符串中提取出任意相连的三个数字,上面的正则表达式可以进行如下改写:

/\d{3}/

这时我们不再关心这个字符串是否只包含三个数字,而是关心字符串中是否存在三个连续数字这样的模式。此时可以像下面一样使用上述正则表达式:

let reg = /\d{3}/;
reg.exec('abc543as');  
// ["543", index: 3, input: "abc543as", groups: undefined]

我们的正则表达式成功从字符串'abc543as'中检索出了'543'这个由三个连续数字构成的子串,并给出了它的索引index和原始字符串(groups输出的是捕获组,后面我们会介绍)。

除了这两种情况,字符串的replace方法、match方法等也会用到正则表达式。replace用于替换字符串中符合某个模式的子串,match的作用与exec类似,但是具体用法上有一些差别。

理解了正则表达式的基本概念后,我们就来看一下正则表达式为我们提供了哪些基础模式,它们是我们学习正则表达式最基础的知识。

二、正则表达式的模式单元

1. \

用来将下一个字符标记为特殊字符、原义字符、向后引用或转义字符。

比如我们是没办法直接在电脑屏幕上输出换行符的,因此正则表达式规定可以通过\n这样转义的方法来表达换行符。

再比如,上面我们讲到\d可以用来匹配数字。注意,只有d的前面带着一个反斜线组合使用时,它才可以匹配数字,否则它就只能匹配字母d。正则表达式中这种用法非常常见,下面我们会介绍很多使用\表达特定字符类型的模式单元。

2. ^

匹配字符串的开头位置。

注意,它本身并不匹配任何字符,只是一个位置标志。比如:

/^\d/.test('sd23a');   // false

/\d/.test('sd23a');   // true

可以看到,这两个模式是不一样的。加了^的表达式是要匹配一个数字,并且这个数字必须是在字符串开头的位置;没有^时则没有要求。

3. $

匹配字符串的结尾位置。

原理同上。如/\d$/这个表达式要求字符串必须是数字结尾的,而/\d/则允许数字出现在字符串的任意位置:

/\d$/.test('sd23a');   // false

/\d/.test('sd23a');   // true

4. *

匹配前面的子表达式零次或多次。

比如,下面的表达式可以匹配任意多个数字构成的字符串,包括0个:

let reg = /^\d*$/;

reg.test('');  // true
reg.test('23454');  // true

reg.test('34a34');  // false

除了匹配单个子表达式,还可以是一个较为复杂的表达式:

/^(\d{3})*$/

它会匹配零至多组由三个数字构成的字符串,也就是数字的数量必须是3的倍数,如:

/^(\d{3})*$/.test('');   // true,0个数字
/^(\d{3})*$/.test('324');  // true,3个数字
/^(\d{3})*$/.test('345756');  // true,6个数字

/^(\d{3})*$/.test('56');   // false,2个数字
/^(\d{3})*$/.test('a34');  // false,含有非数字字符

5. +

匹配前面的子表达式一次或多次。

它的用法与*很像,只是它要求子表达式至少要匹配一次。如:

let reg = /^\d+$/;

reg.test('23');  // true

reg.test('');  // false

可以看到,当字符串为空时,使用+就不能匹配了。

6. ?

匹配前面的子表达式0次或1次。

*+类似,只是它要求前面的子表达式要么不出现,要么只出现一次。如下面的表达式只能匹配0或1个数字:

let reg = /^\d?$/;

reg.test('');  // true
reg.test('6');  // true

reg.test('23');  // false

7. {n}、{n,}、{n, m}

匹配子表达式n次、至少n次、n至m次。

{n}匹配表达式n次,如/o{2}/可以匹配food中的字符串oo但不能匹配Bob中的o

{n,}匹配表达式至少n次,如/o{2,}/可以匹配foodoooh中的ooooo,但是不能匹配Bob中的o,因为这里o出现的次数少于2次。

{n, m}匹配表达式至少n次,至多m次。如/o{2, 3}/可以匹配foodoooh中的ooooo,但是不能匹配Bob中的o,因为这里o出现的次数不在2-3次之间。不过当o出现的次数大于3次的时候不会导致检测失败,只是超过3个的字符o会被截断。

这些修饰单元也可以修饰括号包含起来的更复杂的子表达式。

8. 限制字符后面的’?’

?位于限制字符后面时,它将以非贪婪模式匹配字符串。

上面我们介绍的*、+、?、{n}、{n,}、{n,m}都属于限制字符,它们的作用是限制子表达式的数量。默认地,这些限制字符会以贪婪模式去匹配字符串,也就是尽量多地匹配字符串,如:

/\d+/.exec('2342');

// ["2342", index: 0, input: "2342", groups: undefined]

可以看到,我们的正则表达式希望匹配数字至少一次。但是这里有4个数字,到底要匹配几次呢?显然,1、2、3、4次都是可以的。

由于表达式默认匹配尽量多的字符串,因此它会匹配所有4个数字。

但是如果在+的后面加上一个?,表达式将启用非贪婪模式,即匹配尽量少的字符:

/\d+?/.exec('2342');

// ["2", index: 0, input: "2342", groups: undefined]

可以看到,这时表达式只匹配了一个2。其他的限制字符也是同样的道理。

9. .

该字符就是英文的句号,它匹配除\n之外的任意字符。

如:

/./.test('2');  // true
/./.test('s');  // true
/./.test('\t');  // true,这里的\t是制表符,不是两个字符

/./.test('\n');  // false,不能匹配换行符

可以看到,.可以匹配除换行符\n以外的任意字符。如果想要匹配任意字符,可以使用[.\n],关于中括号的用法后面会介绍。

10. (patten)

匹配某个模式并获取匹配结果。

该语法用于从原字符串中捕获符合某个模式的子字符串。假如我们希望捕获某字符串中的多个连续数字组合,可以把我们要匹配的模式包含在括号内,然后从原生对象RegExp的静态属性中获取。如:

let reg = /(\d{3}).*(\d{3})/g;
let string = 'as123bs456';

string.match(reg);

RegExp.$1;  // '123'
RegExp.$2;  // '456'

这里定义了两个需要捕获的模式,即两组三个连续的数字。第一个模式(\d{3})捕获到了字符串'123',第二个模式(\d{3})捕获到了字符串'456'

注意,RegExp的静态属性最多只能存储9个捕获组(即RegExp.$1 - $9),并且一旦进行了下次匹配,上次的捕获结果就会被覆盖。如果想要在正则表达式中使用上次的捕获结果,需要使用\n的格式,注意,这里的n代指数字,而不是字母n。如:

let reg = /(\d{3}).*\1/g;

这里的\1代指对前面第一个捕获到的字符串的引用,也就是(\d{3})所捕获到的字符串。因此该表达式要匹配的是类似于'123sd123'这样的字符串,即至少存在两组相同的三数字组。如:

reg.test('s345v345');   // true
reg.test('345345');   // true
reg.test('345v345w345');   // true

reg.test('ads345vv346');   // false
reg.test('ads34vv34');   // false

11. (?:patten)

匹配但不捕获该模式。

这里在patten的前面加了字符?:,表示只匹配括号内的模式,但不计入捕获组。换句话说,此时的括号只是用作普通的括号,不再具有捕获模式的能力。如:

/industr(?:y|ies)/.exec('industries')

RegExp.$1;   // ''

如果上述括号内没有?:,则这里捕获到的ies应当被计入RegExp.$1,加了该修饰符后它没有被捕获。该模式可以防止由括号的使用导致意外捕获。此时,下面的两种写法是完全等价的:

/industr(?:y|ies)/  <=> /industry|industries/

12. (?=patten)

对后续字符串进行预查,只有后面的字符串符合预查中的模式,当前字符串才算匹配上,不获取匹配结果,也不消耗字符串。

举个例子:

/win(?=10)/

该表达式要匹配的字符串是win,但我们要捕获的必须是后面紧跟着10的字符串win,否则不予匹配。下次匹配时仍然会继续从10开始匹配,也就是说上次的匹配并没有把字符串10算进去。如:

/win(?=10)(\d+)/.test('win10');  //true
RegExp.$1;   // '10'

可以看到,尽管在匹配win(?=10)已经对字符串10进行了预查,但是(\d+)仍然能匹配到字符串10,这是因为上次的检查只是预检查,并没有消耗掉这两个数字。如果是下面的写法,则无法匹配:

/win10(\d+)/.test('win10');  // fasle

这里(\d+)前面的部分已经匹配完了所有的字符(win10),于是(\d+)没有匹配到任何字符,导致最终匹配失败。

13. (?!patten)

该模式与上一个正好相反,只有当后续字符串不符合某个模式时才算配成功,同样不消耗字符串,也不进行捕获。

/win(?!10)/只会匹配后面没有紧跟着字符串10的字符串win,但是不影响字符串10参与后续的匹配过程。

14. x|y

逻辑‘或’,表示匹配左侧模式,或者匹配右侧的模式。

如:

/industr(?:y|ies)/

表示字符串industr后面跟着字符y,或者ies。因此industryindustries都符合该表达式。

15. []

匹配列出的字符中的任意字符。

[abc123]可以匹配a、b、c、1、2、3这六个字符中的任意一个(注意,它只匹配一个字符,中括号列出的是字符的范围)。

中括号内可以使用范围字符,如[a-zA-Z0-9]匹配的是所有的小写、大写字母和数字字符。a-z的含义是匹配ASCII码在字符a和字符z之间的所有字符,由于所有的小写字母的ASCII码值是连续的,因此这里可以匹配到所有的小写字母。

中括号内可以出现特殊字符,如[.\n]。由于.可以匹配除\n之外的所有字符,在该范围上再加入字符\n,就可以匹配所有的字符了。

16. [^]

作用与上述模式相反,匹配不在列出的字符内的任意字符。

[^a-z]匹配的是不在a-z范围内的任意字符。

17. \b、\B

分别匹配单词边界和非单词边界。

\b匹配单词边界。如:

let reg = /(as)\b/;

reg.test('was');  // true
reg.test('was you');  // true

reg.test('aswww');  // false
reg.test('wasw');  // false

这里\b用来匹配单词的边界,因此只有as的后面是单词边界时,它才会被匹配。

\B只在该字符串不在单词边界时才会被匹配,与\b刚好相反。如:

reg = /(as)\B/;

reg.test('wasw');  // true

reg.test('was');   // false

第一个字符串中as不位于单词边界,匹配成功,第二个位于单词边界,则匹配失败。

18. \d、\D

分别匹配数字、非数字。

\d用于匹配一个数字,这个我们上面已经见到了。

\D相反,用于匹配一个非数字的字符。如:

/\d/.test('1');   // true
/\d/.test('a');   // false

/\D/.test('1');   // false
/\D/.test('a');   // true

19. \f、\n、\r、\t、\v

这几个都是特殊字符,分别是换页符、换行符、回车符、制表符和垂直制表符,在进行文件字符串匹配时可能会用到。这里不再详述。

20. \s、\S

匹配空白字符、非空白字符。

上述五个特殊字符都属于空白字符,另外,空格也属于空白字符,因此\s等价于[ \f\n\r\t\v]

\S匹配非空白字符,不属于上面6个字符的所有字符,都可以被\S匹配到。

21. \w、\W

匹配包括下划线在内的单词字符、非单词字符。

\w等价于[A-Za-z0-9_],而\W则匹配这些字符以外的所有字符。

22. \cx

匹配由x指定的控制字符。

这里的x代指a-z或A-Z中的一个字母,如\cM可以匹配Control-M或回车符。如果\c后面不是一个字母,那么\c等价于字符c

23. \num

斜线后紧跟一个数字,表示对前面第num个被捕获字符串的引用。当num是一个0-7之间的数值时,它还可以匹配一个八进制数字。

如下面的表达式匹配的是两个挨着的相同字符:

let reg = /(.)\1/;

reg.test('aa');  // true
reg.test('sraav');  // true

reg.test('aba');  // false

这里的\1代指前面(.)捕获到的字符,而(.)可以匹配\n以外的任意字符,因此这个表达式就可以匹配所有除\n以外的任意连着的相同字符。

该语法匹配八进制数字的用法如下:

/\2/.test('\02');  // true

注意,这里的\02只代表一个八进制的2\0是八进制数的标志。

24. \un

匹配一个十六进制的Unicode字符,其中n是由四个数字或字母组成的。

\u00A9 匹配版权符号 (©),00A9就是版权符号对应的Unicode值。该方法可以匹配任意的中文字符,因为每个中文字符都有对应的Unicode值。如:

/\u6770/.test('杰');  // true

各个字符的Unicode值请自行查阅Unicode字符表。

25. (?<=patten)

对前置字符串进行预查,只有前面的字符串符合预查中的模式,当前字符串才算匹配上,不获取匹配结果,也不消耗字符串。它与(?=patten)是对应的,一个是向前预查,一个是向后预查。

如:

var reg = /(?<=95)windows/

该表达式要求只有前面是字符串95的字符串windows才能被匹配:

reg.test('95windows'); // true

reg.test('2000windows');  // false

第一个字符串的windows前面是字符串95,因此它匹配成功,第二个则失败了。

25. (?<!patten)

与上述模式相反,只有前置字符不符合预查的模式时,当前字符才算被匹配上。如:

var reg = /(?<!95)windows/

上面的表达式只能匹配前面没有字符串95的字符串windows

reg.test('2000windows'); // true

reg.test('95windows');  // false

第一个字符串的windows前面不是字符串95,因此它匹配成功,第二个前面是95,因此检测失败。

26. 限制字符后面的’+’

在限制字符*、+、?、{n}、{n,}、{n,m}后面加+将启用独占模式,它是与贪婪模式和非贪婪模式并列的一种匹配模式。

独占模式与贪婪模式一样,会尽量多地匹配字符串,但是在后续的匹配失败时,它不会像贪婪模式一样发生回溯。举个例子:

var reg = /ab{1,3}+bc/

reg.test('abbc');  // false

{1,3}后面加上+表示启用独占模式,此时它会尽可能多地匹配字符b,因此会消耗这里的两个b。接下来引擎尝试用剩下的字符c与正则表达式匹配,发现匹配失败。如果在贪婪模式下,引擎会进行回溯,也就是尝试吐出之前匹配的一个字符b进行重新比对,这样就可以匹配成功。但是在独占模式下不会释放之前已经匹配到的字符,因此最终匹配失败。

三、模式修饰符

模式修饰符定义了该正则表达式在匹配时采用何种方式,如/i表示在匹配时要忽略大小写。模式修饰符写法如下:

/\w/i   // 单个修饰符

/\w/igm  // 多个修饰符,顺序无关

下面是所有的模式修饰符。

1. /g

全局匹配模式。启用了全局模式后,正则表达式的匹配是有状态的,即它会记录上次的匹配位置,以便继续下次匹配。

默认情况下,正则表达式在完成一次匹配后就会结束匹配,如:

'123a456'.match(/\d{3}/);
// ["123", index: 0, input: "123a456", groups: undefined]

上面的正则表达式只匹配到了第一个符合条件的字符串123,却没有去匹配后面符合条件的字符串456。而加了/g修饰符后,匹配结果如下:

'123a456'.match(/\d{3}/g);
// ["123", "456"]

可以看到,所有符合条件的字符串都被捕获到了。

2. /i

不区分大小写。

即不对英文字符区分大小写,如:

/[a-z]/.test('A');   // false

/[a-z]/i.test('A');   // true

3. /m

多行匹配。当需要匹配的字符串是个多行文本时,/m可以改变^和$的行为。

默认情况下,即使一个字符串是由多行文本组成的,整个字符串也只有一个开头和一个结束位置。但是使用/m开启多行模式后,每行都有一个开头和一个结尾位置。如:

/^a[.\n]*c$/.test('abc\n123');  // false

字符串'abc\n123'看起来大致是下面的结构:

^abc
123$

/^a.*c$/要匹配的是以a开头,c结尾的任意字符串。由于没有开启多行匹配,这里的结尾指的是整个字符串的结尾,即字符3后面的位置。因此这里匹配失败了,因为整个字符串的尾部是字符3

但是开启多行匹配后,字符串'abc\n123'则相当于下面的结构:

^abc$
^123$

即每行都相当于有了一个开始符合结束符,此时上面的检查就会返回true:

/^a[.\n]*c$/m.test('abc\n123');  // true

此时的$匹配到的是字符c后面的结束符,而不是整个字符串最后的结束符。

4. /s

单行匹配模式。

单行匹配模式下,字符串只有一个开始符和一个结束符。有人会问,那不是跟没有任何修饰符一样的吗?

其实还是有一点差别的。/s会改变模式单元.的行为,使得它可以匹配\n,如:

'ab\ncd'.match(/.*/);  
// ["ab", index: 0, input: "ab", groups: undefined]

'ab\ncd'.match(/.*/s);  
// ["ab↵cd", index: 0, input: "ab↵cd", groups: undefined]

此时.对换行符\n也是有效的。

5. /y

是否启用粘性模式。

启用了粘性模式后,该正则表达式必须从lastIndex指定的位置开始匹配,lastIndex的值可以手动修改,默认是0,并且会在匹配失败时重置为0。如:

var str = '#foo#';
var reg = /foo/y;

reg.lastIndex = 0;
reg.test(str);   // false,字符串的0位置是#,与f不匹配

reg.lastIndex = 1;
reg.test(str);   // true

reg.lastIndex = 3;
reg.test(str);   // false

reg.lastIndex;  // 0,上次的匹配失败导致lastIndex被重置

此时只有把lastIndex置为1才可匹配成功,请他情况均会匹配失败。即此时由lastIndex人为指定了匹配位置,该位置匹配失败则立即判定为失败,不再继续匹配。

此外还有两个修饰符/x和/e,它们在JavaScript中是无效的,因此这里不再介绍。

四、正则表达式的用法

这里我们只介绍最常见的用法。

1. 正则表达式的原型方法

正则表达式主要有两个用于匹配的原型方法,testexec

(1). test

用于检测传入的字符串是不是符合正则表达式所规定的模式,只要检测到一个符合要求的子串,就返回true,否则返回false。如:

let reg = /\d+/;

reg.test('12sd');  // true
reg.test('af');  // false

(2). exec

exec用于执行正则表达式,输出匹配结果及捕获组。如:

let reg = /(\d+)a/;

reg.exec('12a34'); 
// ["12a", "12", index: 0, input: "12a34", groups: undefined]
// '12a'是匹配到的完整字符串,'12'是捕获组捕获的结果

exec方法返回的结果是一个数组,第0项是匹配到的符合正则表达式的完整字符串;从第一项往后,都是由捕获组捕获到的字符串。

在没有设置匹配模式为/g或/y的情况下,正则表达式的匹配是无状态的,也就是说它不会记录上次的匹配位置。如:

let reg = /\d+/;
let string = '12a34';

reg.exec(string);
// ["12", index: 0, input: "12a34", groups: undefined]

reg.exec(string);
// ["12", index: 0, input: "12a34", groups: undefined]

可以看到,由于没有设置/g或/y,因此第二次的匹配没有接着上次的位置继续匹配,而是再次匹配到了字符串12

但是在设置了/g或/y后,正则表达式就会变成有状态的,它上次的匹配位置会被记录在lastIndex属性中,此时,再次执行exec时,便可以从上次匹配到的位置继续向后匹配:

let reg = /\d+/g;
let string = '12a34';

reg.lastIndex;   // 0
reg.exec(string);
// ["12", index: 0, input: "12a34", groups: undefined]

reg.lastIndex;   // 2,本次的初始匹配位置
reg.exec(string);
// ["34", index: 3, input: "12a34", groups: undefined]

可以看到,设置了/g后,正则表达式在执行了一次exec后,lastIndex的值变成了2,这意味着下次执行exec时,将从索引位置2开始继续匹配,因此这次匹配到了字符串34

因此,一旦设置了/g,那么连续多次执行exec,就可以将字符串中所有符合条件的子串匹配出来。如:

let reg = /\d+/g;
let string = '12a34f56h';

cosnole.log(reg.exec(string));
// ["12", index: 0, input: "12a34f56h", groups: undefined]
while(reg.lastIndex !== 0){
	console.log(reg.exec(string));
}
// ["34", index: 3, input: "12a34f56h", groups: undefined]
// ["56", index: 6, input: "12a34f56h", groups: undefined]
// null

由于在匹配失败时,lastIndex会被自动置为0,因此判断lastIndex是否为0可以判定匹配是否结束。如果没有结束将一直向后匹配,最终输出所有匹配结果。

2. 字符串的正则表达式方法

字符串有两个常用的正则表达式方法:match和replace。

(1). match

该方法非常类似于正则表达式的原型方法exec,但是它并没有exec那么灵活。

两者的关键差异在于,当设置了/g时,exec每次只捕获一个子串,并且会输出完整的捕获组;而match一次会匹配所有符合条件的子串,这样的子串只有一个时,它的输出结果与exec完全一致,但是当符合条件的子串超过1个时,它只会返回匹配结果,不会返回捕获组。如:

let reg = /(\d+)[a-z]/g;
let string = '12a34f56g';

reg.exec(string);  // ["12a", "12", index: 0, input: "12a34f56", groups: undefined]
reg.exec(string);  // ["34f", "34", index: 3, input: "12a34f56", groups: undefined]
reg.exec(string);  // ["56g", "56", index: 6, input: "12a34f56", groups: undefined]

string.match(reg);
// ["12a", "34f", "56g"]
// 假如这里只能匹配到一个结果,那么它也会像exec一样输出捕获组

可以看到,每次执行exec都会执行一次匹配,输出匹配结果和捕获组。但是使用match方法却只输出了匹配结果,捕获组并没有输出出来。因此,如果捕获组的值对你很重要,请不要选择match方法。

(2). replace

这是非常常用的子串替换方法。我们最简单的用法一般就是直接把一个字符串替换成另一个字符串,如:

'abc'.replace('b', 'm'); // amc

但是当我们把replace方法与正则表达式结合时,它的作用可能会有点超出想象。

replace的第一个参数可以传入一个正则表达式,此时第二个参数除了可以是一个字符串外,还可以是一个函数。函数接受的参数如下表:

变量名代表的值
match匹配的子串
p1,p2, …第n个括号匹配的字符串,即第n个捕获组
offset匹配到的子字符串在原字符串中的偏移量。(比如,如果原字符串是 ‘abcd’,匹配到的子字符串是 ‘bc’,那么这个参数将会是 1)
string被匹配的原字符串
举个例子,下面的正则表达式匹配a=b这种结构的子串,并且分别捕获a和b对应的值,我们把a和b对应的值分别记为key和value:
let reg = /([^=&]+)=([^&]*)/g;

现在我们执行如下的替换:

var result = {};
"foo=1&bar=2&foo=3".replace(/([^=&]+)=([^&]*)/g,
	 function(match, key, value){
		result[key] = ( result[key] ? result[key] + "," : "") + value;
		return match;
	})

由于设置了/g,因此这里的回调函数会被多次执行,每次传入的是一组匹配结果(与exec方法的行为一致,由于匹配位置和原始字符串我们用不到,因此这里没有传)。

第一次,正则表达式匹配到了子串foo=1,并且分别捕获了foo1,因此我们的函数前三个值分别是'foo=1'、'foo'、'1',分别记为match、key、value。在回调函数中,我们先检查result[key]是否存在,如果存在,就把value前加个逗号拼在之前的值后面,否则直接把value赋给该key。随后该回调函数会继续匹配到bar=2foo=3,并重复上述过程。

猜猜最终result的值是什么样的?

result = { 
	foo: "1,3", 
	bar: "2" 
}

看起来非常巧妙对吧?当你熟练掌握正则表达式后,它可能会远比你现在以为的强大得多!

总结

如果你读了本文,觉得已经完全掌握了正则表达式,那你可太低估正则表达式了。。。

友情链接:正则表达式(应用篇)

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐