正则表达式

第 1 章 字符组

字符组 就是"一组"字符。在正则表达式中,它表示"在同一个位置可能出现的各种字符"。
写法 :在一对方括号[]之间,列出所有可能出现的字符。例如:[abc]、[123]、[#?.]等

1.1 普通字符组

例如:检测字符串中是否存在数字

js 复制代码
/[0123456789]/.test('hello123world');

字符组中字符的排列顺序 并不影响字符组的功能,出现重复字符 也不会影响。例如:[0123456789]完全等价于[9876543210][998876543210]

为了代码更容易编写、方便阅读,不推荐在字符组中出现重复字符,而且还应该让字符组中的字符排列更符合认知习惯。为此,正则表达式提供了**-范围表示法(range)**,它更直观,能进一步简化字符组。

所谓"-范围表示法 ",就是用[x-y]的形式,表示 x 到 y 整个范围内的字符。[0123456789]就可以表示为[0-9]

1.1.1 范围表示法

"-范围表示法 "的范围是如何确定的?为什么要写作[0-9],而不写作[9-0]?

在字符组中,-表示的范围,一般是根据字符对应的码值(字符在对应编码表中的编码数值)来确定的。码值小的字符在前,码值大的字符在后。在ASCII编码中,字符 0 的码值是48(十进制),字符9的码值是57(十进制)。所以[0-9]等价于[0123456789]

js 复制代码
/[0-9]/.test('2')

在字符组中可以同时并列多个"-范围表示法" 。例如:[0-9a-fA-F]可以匹配数字,大小写形式的a ~ f,它可以用来验证十六进制字符。

1.1.2 转义序列\xnum表示字符

可以使用转义序列\xnum来表示一个字符。其中\x是固定前缀,表示转义序列的开头,num是字符对应的码值,是一个两位的十六进制数值。例如:字符A的码值是41(十进制则为65),所以也可以用\x41表示。

字符组中有时会出现这种表示法,它可以表示一些难以输入或者难以显示的字符,比如\x7f。也可以用来方便地表示某个范围,例如:

  • 所有标准ASCII字符对应的字符组:[\x00-\x7f],匹配码值范围[0,127]
  • 所有扩展ASCII字符对应的字符组:[\x00-\xff],匹配码值范围[0,255],可以用来匹配非中文字符。

1.2 元字符与转义

字符组中的横线-并不能匹配横线字符,而是用来表示范围,这类字符叫元字符。

字符组的开方括号[、闭方括号]、横线-、尖角号^都算元字符,在匹配中,有着特殊的意义。但有时候并不需要表示特殊的意义,只需要表示普通字符,此时就必须做特殊处理。

字符组中的-,如果它紧邻着字符组中的开方括号[或闭方括号],那么它就是普通字符,其他情况下都是元字符。

js 复制代码
/[-9]/.test('-'); // true
/[9-]/.test('-'); // true

而对于其他元字符,取消特殊含义的做法都是转义,也就是在正则表达式中的元字符前加上反斜线字符\

使用RegExp构造器创建正则,转义字符组中的横线-

js 复制代码
new RegExp('[0\\-2]');

仔细观察会发现,在正文里说"在正则表达式中的元字符之前加上反斜线字符\",而在代码里写的却不是[0\-2],而是[0\\-2]。因为在这段程序里,正则表达式是以字符串的方式提供的,而字符串本身也有关于转义的规定。上面说的"正则表达式",是经过"字符串转义处理"之后的字符串的值。因为处理字符串时,反斜线和它之后的字符会被认为是转义序列。因此需要\\ 转义成\

1.3 排除型字符组

在方括号[...]中列出希望匹配的所有字符,这种字符组可以叫做"普通字符组"。它的确非常方便,但也有些问题是普通字符组不能解决的。比如匹配字符串中是否存在非数字字符,不是数字的字符太多了,全部列出几乎不可能,这时就应当使用排除型字符组

排除型字符组 非常类似普通字符串[...],只是在开方括号[之后紧跟一个尖角号^,写作[^...],表示"在当前位置,匹配一个没有列出的字符"。所以[^0-9]就表示"0 ~ 9之外的字符",也就是"非数字字符"。

注意:排除型字符组必须匹配一个字符。

js 复制代码
// 匹配一个除-、0、9之外的字符
/[^-09]/.test('-');  // false

// 匹配一个除0~9之外的字符
/[^0-9]/.test('a');  // true

在排除型字符组中,^是一个元字符,但只有它紧跟在[之后时才是元字符。如果想表示"这个字符组中可以出现^字符",不要让它紧挨着[即可,否则就要转义。

js 复制代码
/[0^9]/.test('^');   // true

1.4 字符组简记法

[0-9][a-z]等字符组,可以很方便地表示数字字符和小写字母字符。对于这类常用的字符组,正则表达式提供了更简单的记法,这就是字符组简记法

常见的字符组简记法有\d\w\s

  • \d等价于[0-9],其中的 d 代表"数字(digit)"。
  • \w等价于[0-9a-zA-Z_],其中的 w 代表"单词字符(word)"。
  • \s等价于[ \n\r\t\v\f](第一个字符是空格),其中的 s 代表"空白字符(space)"。空白字符,可以是空格字符、新行换行符\n、回车换行符\r、水平制表符\t、垂直制表符\v、换页符\f
js 复制代码
/\d/.test('2');   // true
/\w/.test('a');   // true
/\w/.test('_');   // true
/\w/.test('2');   // true
/\s/.test(' ');   // true

字符组简记法 可以单独出现,也可以使用在字符组 中。比如[0-9a-zA-Z]可以写成[\da-zA-Z]字符组简记法 也可以用在排除型字符组 中,比如[^0-9]可以写成[^\d]

相对于\d\w\s这三个普通字符组简记法,正则表达式也提供了对应排除型字符组的简记法:\D\W\S。字母完全一样,只是改为大写。

  • \D等价于[^\d][^0-9]
  • \W等价于[^\w][^0-9a-zA-Z_]
  • \S等价于[^\s][^ \n\r\t\v\f]
js 复制代码
// \d 和 \D
/\d/.test('8');   // true
/\d/.test('a');   // false
/\D/.test('8');   // false
/\D/.test('a');   // true

// \w 和 \W
/\w/.test('c');   // true
/\w/.test('!');   // false
/\W/.test('c');   // false
/\W/.test('!');   // true

// \s 和 \S
/\s/.test('\t');   // true
/\s/.test('0');   // false
/\S/.test('\t');   // false
/\S/.test('0');   // true

注意[\s\S][\w\W][\s\S]能匹配任意字符。

关于字符组简记法,最后需要补充三点:

  1. 如果字符组中出现了字符组简记法,最好不要出现单独的-,否则可能会引起错误,比如[\d-a]就很让人困惑。
  2. 以上说的\d\w\s的匹配规则,都是针对ASCII编码而言的,也叫ASCII匹配规则。而在一些语言中的正则表达式已经支持了Unicode字符。那么数字字符、单词字符、空白字符的范围,已经不仅限于ASCII编码中的字符。

第 2 章 量词

2.1 一般形式

验证中国大陆地区的邮政编码(6位数字构成的字符串),比如201203

只有同时满足以下两个条件,匹配才成功:

  1. 长度是6个字符。
  2. 每个字符都是数字。
js 复制代码
/^\d\d\d\d\d\d$/.test('201203');   // true

^表示字符串的开头位置,$表示字符的结尾位置。

\d重复了6次,读写都不方便。为此,正则表达式提供了量词 。比如上面匹配邮政编码的表达式,就可以简写为\d{6},它使用阿拉伯数字,更简洁直观。

js 复制代码
/^\d{6}$/.test('201203');   // true
量词 说明
{n} 之前的元素必须出现 n 次
{n,m} 之前的元素最少出现 n 次,最多出现 m 次
{n,} 之前的元素最少出现 n 次,最多无上限(隐式上限 65536)
{0,n} 之前的元素可以不出现,也可以出现,最多出现 n 次

注意 :量词中的逗号之后不能有空格。 术语

  • 结构:一般指的是正则表达式所提供功能的记法。比如字符组就是一种结构。
  • 元素 :指的是具体的正则表达式中的某个部分,比如某个具体表达式中的字符组[a-z],可以算作一个元素元素 ,也叫"子表达式"。

2.2 常用量词

{n,m}是通用形式的量词,正则表达式还有3个常用量词,分别是+*?。它们的形态虽然不同于{n,m},功能却是相同的(也可以把它们理解为"量词简记法")。

常用量词 {n,m}等价形式 说明
* {0,} 可能不出现,也可能出现,出现次数没有上限
+ {1,} 至少出现 1 次,出现次数没有上限
? {0,1} 至多出现 1 次,也可能不出现
js 复制代码
/^https?$/.test('http'); // true
/^https?$/.test('https'); // true

HTML标签匹配,分为开始标签、结束标签、自闭合单标签。例如:<div>就是开始标签,</div>就是结束标签,<br/>就是自闭合单标签。

  1. 开始标签:/^<[a-zA-Z]+\d?[^<>]*(?<![/\s])>$/
    • <开始,>结束。
    • <后紧跟一个或多个大小写字母,之后可以是0个或1个数字,之后可以是0个或多个除<、>外字符。
    • >前不能是/、空白字符。
  2. 结束标签:/^<\/[a-zA-Z]+\d?>$/
    • </开始,以>结束。
    • </后紧跟一个或多个大小写字母,之后可以是0个或1个数字。
  3. 自闭合单标签:/^<[a-zA-Z]+\d?[^<>]*(?<!\/)\/?>$/
    • <开始,以>/>结束
    • <后紧跟一个或多个大小写字母,之后可以是0个或1个数字,之后可以是0个或多个除<、>外字符。

2.3 .点号

.点号:能匹配除换行符外的任意单个字符。等价于:[^\n\r]

2.3.1 滥用点号的问题

因为点号能匹配几乎所有的字符,所以实际应用中许多人图省事,随意使用.*.+,结果却事与愿违,下面以双引号字符串为例来说明。

之前我们使用正则表达式/"[^"]"/来匹配双引号字符串,而"图省事"的做法是/".*"/。通常这么用是没有问题的,但也可能有意外。

js 复制代码
'\"quoted string\"'.match(/".*"/g)[0]; // "quoted string"
'"quoted string" and another"'.match(/".*?"/g)[0];  // "quoted string" and another"

/".*"/匹配双引号字符串,不但可以匹配正常的双引号字符串"quoted string",还可以匹配格式错误的字符串"quoted string" and another"。这是为什么呢?

在正则表达式/".*"/中,点号.能匹配除换行符外的任意字符,*表示可以匹配的字符串长度没有限制。所以.*在匹配过程结束之前,每遇到一个字符(除换行符外),.*都可以匹配。但是到底是匹配这个字符,还是忽略它,将其交给之后的"来匹配呢?

具体选择取决于所使用的量词。正则表达式中的量词分为几类,之前介绍的量词都可以归到一类,叫做匹配优先量词 (贪婪量词)。匹配优先量词,就是在拿不准是否要匹配的时候,优先尝试匹配,并且记下这个状态,以备将来"反悔"。

来看表达式/".*"/对字符串"quoted string"的匹配过程。

  • 一开始,"匹配",然后轮到字符q,.*可以匹配它,也可以不匹配。因为使用了匹配优先量词,所以.*先匹配q,并且记录下这个状态"q也可能是.*不应该匹配的"。
  • 接下来是字符u,.*可以匹配它,也可以不匹配。因为使用了匹配优先量词,所以.*先匹配u,并且记录下这个状态"u也可能是.*不应该匹配的"。

......

  • 现在轮到字符g,.*可以匹配它,也可以不匹配。因为使用了匹配优先量词,所以.*先匹配g,并且记录下这个状态"g也可能是.*不应该匹配的"。
  • 最后是末尾的",.*可以匹配它,也可以不匹配。因为使用了匹配优先量词,所以.*先匹配",并且记录下状态""也可能是.*不应该匹配的"。

这时候,字符串之后已经没有字符了,但正则表达式中还有"没有匹配。所以只能查询之前保存备用的状态,看看能不能退回几步,照顾"的匹配。查询到最近保存的状态是:""也可能是.*不应该匹配的"。于是让.*"反悔"对"的匹配,把"交给",测试发现正好能匹配,所以整个匹配宣告成功。这个"反悔"的过程,专业术语叫做回溯

如果我们把字符串换成"quoted string" and another".*会首先匹配第一个双引号之后的所有字符,再进行回溯,表达式中的"匹配了字符串结尾的字符",整个匹配宣告完成。

如果要准确匹配双引号字符串,就不能图省事使用/".*"/,而要使用"[^"]*"

2.4 忽略优先量词

有些时候,确实需要用到.*(或者[\s\S]*),比如匹配HTML代码中的Javascript示例就是如此。

js 复制代码
// 因为点号.不能匹配换行符,所以必须使用[\s\S](或者[\d\D]、[\w\W])
/<script[\s>][\s\S]*<\/script>/

如果遇到更复杂的情况会出错,比如针对下面这段字符串。

js 复制代码
// 假设上面的Javascript字符串保存在变量htmlSource中
htmlsource.match(/<script[\s>][\s\S]*<\/script>/g);

/<script[\s>][\s\S]*<\/script>/来匹配,会一次性匹配两段Javascript代码,甚至包含之间不是Javascript的代码。

按照匹配原理,[\s\S]*先匹配所有的文本,回溯时交还最后的</script>,整个表达式匹配就成功了,逻辑就是如此,无可改进。这个问题也不能模仿之前双引号字符串匹配,用[^"]匹配<script...></script>之间的代码,因为排除型字符组只能排除单个字符,[^</script>]不能表示"不是<script>的字符串"。

换个角度,通过改变[\s\S]*的匹配策略来解决问题:在不确定是否要匹配的场合,先尝试不匹配的选择,测试正则表达式中后面的元素。如果失败,再退回来尝试[\s\S]*匹配,如此就没有问题了。

循着这个思路,正则表达式中还提供了忽略优先量词(或懒惰量词)。如果不确定是否要匹配,忽略优先量词会选择"不匹配"的状态,再尝试表达式中之后的元素。如果尝试失败,再回溯,选择之前保存的"不匹配"的状态。

对于[\s\S]*来说,把*改为*?就是使用了忽略优先量词,*?限定的元素出现次数范围与*完全一样,都表示"可能不出现,也可能出现,出现次数没有上限"。区别在于,在实际匹配过程中,遇到[\s\S]能匹配的字符,先尝试"忽略",如果后面的元素不能匹配,再尝试"匹配",这样就保证了结果的正确性。

js 复制代码
htmlsource.match(/<script[>\s][\s\S]*?<\/script>/g);

匹配优先量词与忽略优先量词

匹配优先量词 忽略优先量词 限定次数
* *? 可能不出现,也可能出现,出现次数没有上限
+ +? 至少出现 1 次,出现次数没有上限
? ?? 至多出现 1 次,也可能不出现
{n,m} {n,m}? 出现次数最少为 n 次,至多为 m 次
{n,} {n,}? 出现次数最少为 n 次,没有上限
{0,n} {0,n}? 可能不出现,也可能出现,最多出现 n 次

2.4.1 应用:提取代码中C语言注释

C语言的注释有两种:

  1. 单行注释,以//开头。
  2. 多行注释,以/*开头,以*/结尾。
js 复制代码
// 匹配单行注释
/\/\/.*/

// 匹配多行注释
/\/\*[\s\S]*?\*\//

2.4.2 应用:提取网页中的超链接

常见的超链接形似<a href="http://somehost/somepath">text</a>。它是以<a开头,以</a>结束,href属性是超链接的地址。

js 复制代码
/<a\s[\s\S]+?<\/a>/

2.4.3 应用:拆解Linux/Unix/MacOS/Windows的路径

Linux/Unix/MacOS 下的文件名类似/usr/local/bin/python这样,它包含两个部分:路径/usr/local/bin/,文件名python

js 复制代码
// 提取路径
/^.*\//

// 提取文件名
/[^/]+$/

Windows 下路径的分隔符是\,比如C:\Program Files\Python 2.7.1\python.exe

js 复制代码
// 提取路径
/^.*\\/

// 提取文件名
/[^\\]+$/

2.5 量词转义

在正则表达式中,+*?等作为量词的字符具有特殊意义。但有些情况下,我们需要的就是这些字符本身,,此时就必须使用转义,也就是在它们之前添加反斜线\

常用量词 所使用的字符+*?。如果希望表示这三个字符本身,直接添加反斜线,变为\+\*\?即可。

一般形式的量词{n,m} 字符串中,虽然具有特殊含义的字符不止一个,转义时却只需要给第一个{添加反斜线即可。如果希望匹配字符串{n,m},正则表达式必须写成\{n,m}

忽略优先量词 字符串中也包含不止一个特殊含义的字符,在转义时却不像一般形式的量词那样,,只转义第一个字符即可,而需要将两个量词全部转义。例如:如果要匹配字符串*?,则正则表达式就必须写成\*\?

各种量词字符串的转义

量词字符/字符串 转义形式
{n} {n}
{n,m} {n,m}
{n,} {n,}
{0,n} {0,n}
+ +
* *
? ?
+? +?
*? *?
?? ??
{n}? {n}?
{n,m}? {n,m}?
{n,}? {n,}?
{0,n}? {0,n}?

点号.也是一个元字符,它可以匹配除换行符外的任意字符。如果要匹配点号本身,必须将它转义为\.

js 复制代码
/^\+$/.test('+');    // true
/^\*$/.test('*');    // true
/^\?$/.test('?');    // true
/^\{6}$/.test('{6}');    // true
/^\{6,8}$/.test('{6,8}');    // true
/^\{0,8}$/.test('{0,8}');    // true

/^\{0,8}\?$/.test('{0,8}?');    // true
/^\{6,8}\?$/.test('{6,8}?');    // true
/^\{6}\?$/.test('{6}?');    // true
/^\+\?$/.test('+?');    // true
/^\*\?$/.test('*?');    // true
/^\?\?$/.test('??');    // true
/^\.$/.test('.');    // true

第 3 章 括号

3.1 分组

用正则表达式匹配身份证号码,依靠字符组和量词能不能做到呢?

身份证号码是一个长度值为 15 或 18 个字符的字符串。

  • 如果是 15 位,则全部由数字组成,首位不能为0。
  • 如果是 18 位,则前 17 位全部是数字,首位同样不能是0,末位可能是数字,也可能是字母x。
js 复制代码
// 15 位身份证号码
/^[1-9]\d{14}$/

// 18 位身份证号码
/^[1-9]\d{14}\d{2}[\dx]$/

只要以 15 位身份证号码的匹配为基础,末尾加上可能出现的\d{2}[\dx]即可。最后的\d{2}[\dx]必须作为一个整体,或许不出现(15位号码),或许出现(18位号码)。量词?可以表示"不出现,或者出现 1 次"。

正则表达式\d{2}[\dx]?是不行的,因为量词?只能限定[\dx]的出现,而正则表达式\d{2}?[\dx]?同样不行。

使用括号(),把正则表达式改写为/^[1-9]\d{14}(\d{2}[\dx])?$/

量词限定之前元素的出现,这个元素可能是一个字符,也可能是一个字符组,还可能是一个表达式。如果把一个表达式用括号包裹起来,这个元素就是括号里的表达式,括号里的表达式通常称为"子表达式"。

括号的这种功能叫做分组 。如果用量词限定出现次数的元素不是字符或字符组,而是连续的几个字符甚至子表达式,就应该用括号将它们"编为一组"。比如,希望字符串ab重复出现一次以上,就应该写作(ab)+,此时(ab)成为一个整体。

有了分组,就可以准确表示"长度只能是m 或 n"。

关于括号的分组功能,最后来看E-mail地址的匹配:E-mail地址以@分隔成两段,@之前的是用户名,之后的是主机名。用户名@主机名

用户名的匹配非常简单,其中能出现的字符主要有大写字母[A-Z]、小写字母[a-z]、阿拉伯数字[0-9]、下划线_、点号.,所以总的字符组就是[A-Za-z0-9_.],又可以简化为[\w.]。用户名的最大长度是 64 个字符,所以匹配用户名的正则表达式就是[\w.]{1,64}

主机名匹配的情况则要麻烦一些,简单的情况比如somehost.net,复杂的情况则还包括子域名,比如mail.somehost.net,而且子域名可能不止一级,比如mail.sub.somehost.net。查阅规范可知,主机名被点号分隔成为若干段,叫做域名字段,每个域名字段中能出现的字符是字母字符、数字字符和横线字符,长度必须在1~63之间。

最后的域名字段是顶级域名,之前的部分可以看做某种模式的重复:该模式由域名字段和点号组成,域名字段在前,点号在后。匹配域名字段的表达式是[A-Za-z0-9-]{1,63},匹配点号的表达式是\.。使用括号的分组功能,把两个表达式分为一组,用量词*限定表示"不出现,或出现多次",就得到匹配主机名的表达式([A-Za-z0-9-]{1,63}\.)*[A-Za-z0-9-]{1,63}。(顶级域名也是一个域名字段,所以即便主机名是localhost,也可以由最后那个匹配域名字段的表达式匹配)

将匹配用户名的表达式、@符号、匹配主机名的表达式组合起来,就得到了完整的匹配E-mail地址的表达式:

js 复制代码
/^[\w.]{1,64}@([A-Za-z0-9-]{1,63}\.)*[A-Za-z0-9-]{1,63}$/

3.2 多选结构

专注于原创短更,便于碎片化涉猎知识。希望我走过的路,留下的痕迹,能对你有所启发和帮助。

转发请注明原处,平台投稿请私信。

相关推荐
慧一居士21 分钟前
flex 布局完整功能介绍和示例演示
前端
DoraBigHead23 分钟前
小哆啦解题记——两数失踪事件
前端·算法·面试
一斤代码6 小时前
vue3 下载图片(标签内容可转图)
前端·javascript·vue
中微子6 小时前
React Router 源码深度剖析解决面试中的深层次问题
前端·react.js
光影少年6 小时前
从前端转go开发的学习路线
前端·学习·golang
中微子6 小时前
React Router 面试指南:从基础到实战
前端·react.js·前端框架
3Katrina6 小时前
深入理解 useLayoutEffect:解决 UI "闪烁"问题的利器
前端·javascript·面试
前端_学习之路7 小时前
React--Fiber 架构
前端·react.js·架构
coderlin_7 小时前
BI布局拖拽 (1) 深入react-gird-layout源码
android·javascript·react.js
伍哥的传说8 小时前
React 实现五子棋人机对战小游戏
前端·javascript·react.js·前端框架·node.js·ecmascript·js