第 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]
能匹配任意字符。
关于字符组简记法,最后需要补充三点:
- 如果字符组中出现了字符组简记法,最好不要出现单独的
-
,否则可能会引起错误,比如[\d-a]
就很让人困惑。 - 以上说的
\d
、\w
、\s
的匹配规则,都是针对ASCII编码而言的,也叫ASCII匹配规则。而在一些语言中的正则表达式已经支持了Unicode字符。那么数字字符、单词字符、空白字符的范围,已经不仅限于ASCII编码中的字符。
第 2 章 量词
2.1 一般形式
验证中国大陆地区的邮政编码(6位数字构成的字符串),比如
201203
。
只有同时满足以下两个条件,匹配才成功:
- 长度是6个字符。
- 每个字符都是数字。
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/>
就是自闭合单标签。
- 开始标签:
/^<[a-zA-Z]+\d?[^<>]*(?<![/\s])>$/
- 以
<
开始,>
结束。 <
后紧跟一个或多个大小写字母,之后可以是0个或1个数字,之后可以是0个或多个除<、>外字符。>
前不能是/、空白字符。
- 以
- 结束标签:
/^<\/[a-zA-Z]+\d?>$/
- 以
</
开始,以>
结束。 </
后紧跟一个或多个大小写字母,之后可以是0个或1个数字。
- 以
- 自闭合单标签:
/^<[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语言的注释有两种:
- 单行注释,以
//
开头。 - 多行注释,以
/*
开头,以*/
结尾。
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 多选结构
专注于原创短更,便于碎片化涉猎知识。希望我走过的路,留下的痕迹,能对你有所启发和帮助。
转发请注明原处,平台投稿请私信。