正则表达式

第 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 多选结构

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

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

相关推荐
_.Switch39 分钟前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光43 分钟前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   43 分钟前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   44 分钟前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web1 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常1 小时前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇2 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
Jiaberrr2 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
Tiffany_Ho3 小时前
【TypeScript】知识点梳理(三)
前端·typescript
安冬的码畜日常4 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js