本文记录了正则表达式入门之后(不知道算不算得上进阶😅)的一些用法,主要用于个人随时查阅。
1. 防止过度匹配
正则表达式中 +
、*
和 {n,}
所能匹配到的字符长度是没有上限,通俗点说就是它们是’贪婪’的,举个例子(文中的例子以 JavaScript 的正则表达式实现为准)。
现在我们想要匹配一串字符里的 <a>
标签及其里面的内容,如果我们写的是这个表达式:
/
<a>.*<\/a>/g
如下所示,将会匹配到第一个 <a>
与最后一个 </a>
之间的所有字符,并不是我们想要的结果。
start <a>first</a> 夹在中间好尴尬😂 <a>second</a>
end.
解决方案是在 +
、*
和 {n,}
后面加一个 ?
,用来防止过度匹配,如:
/
<a>.*?
<\/a>/g
start<a>first</a>
这下不尴尬了😁<a>second</a>
end.
2. 回溯引用
现有这样一个需求,用一个正则表达式,找出字符串中所有的 HTML 标签及其包裹内容(不考虑属性及 <img> 等自关闭标签),如:
start<div>first</div>
吃🍉群众<span>second</span>
end.
第一反应我们可能会这样写:
/
<\w+>.*?<\/\w+>/g
这个表达式确实可以满足上面的例子,但是请看下面:
start<div>first</span>
吃🍉群众<span>second</div>
end.
可以看到当开闭标签对不上的时候,上面的正则仍然能匹配出来,这是不对的,如果我们能在表达式的后面使用前面匹配到的内容就好了,这时候回溯引用就派上用场了。
在 JavaScript 的实现中使用 \n
表示第 n 个捕获组(它们的序号以’(‘出现的位置为准)所匹配到的内容,使用回溯引用之后的正则及匹配结果如下:
/
<(\w+)
>.*?<\/\1
>/g
start <div>first</span> 吃🍉群众 <span>second</span>
end.
匹配结果终于正常了😊。
3. 前后查找
现在又有新的需求了,要求我们写一个新的正则表达式,能够匹配一个 HTML 文档中所有标题的内容,即 h1~h6 标签里的所有内容。在前面回溯引用的基础上,我们很容易写出:
/
<(h[1-6])
>.*?<\/\1
>/g
start<h1>heading1</h1>
继续吃🍉<h2>heading2</h2>
end.
好像看起来还不错,但是注意,我们要的只是标题的文本内容,是不包括标签的,也就是说我们要的是:
start <h1>heading1
</h1> 继续吃🍉 <h2>heading2
</h2> end.
由于目前大部分浏览器都不支持向后查找,我们先讨论向前查找(注意这里的’前后’指的是方向,而不是位置)。
3.1 向前查找
向前查找分为正向前查找(positive lookahead)和反向前查找(negative lookahead),正向前查找用 (?=)
表示,而反向前查找用 (?!)
表示,用一个简单的例子来描述就是:
正向前查找 - 匹配后面跟着字符 'b' 的字符 'a'
/
a(?=b)/g
- aaa
b ac反向前查找 - 匹配后面不是跟着字符 'b' 的字符 'a'
/
a(?!b)/g
-a
a
aba
c
再看一个具体的例子,比如匹配 url 地址中的协议名:
/
^\w+(?=:)/gm
http
://test.comhttps
://test.comftp
://test.com
3.2 向后查找
向后查找和向前查找一样,分为正向后查找(positive lookbehind)和反向后查找(negative lookbehind)。其中正向后查找用 (?<=)
表示,反向后查找用 (?<!)
表示。
正向后查找 - 匹配前面是字符 'a' 的字符 'b'
/
(?<=a)b/g
- ab
bb ba反向后查找 - 匹配前面不是字符 'a' 的字符 'b'
/
(?<!a)b/g
- abb
b
b
a
3.3 前后结合
把前后查找结合起来可以解决一开始获取标题内容的问题,如下所示:
/
(?<=<(h[1-6])>)
.*?(?=<\/\1>)
/g
start <h1>heading1
</h1> 继续吃🍉 <h2>heading2
</h2> end.
在不支持向后查找的环境中,可以用捕获组满足上面的需求。不过要注意的是,虽然前后查找的操作符也是用 ()
包裹起来的,但是它们并不是捕获组,实际上它们应该属于’非捕获组’,下面就开始介绍非捕获组。
4. 非捕获组与命名捕获组
4.1 非捕获组
有时候我们不需要获取某一组(子表达式)的内容,这时候可以用非捕获组(non-capturing group),非捕获组所匹配的内容不会出现在结果中,一般的非捕获组用 (?:)
表示。
举个例子,现在我想要获取一个 url 里面的域名,如果不使用非捕获组:
const re = /(https?|ftp):\/\/([^\/\s]+)/
'http://foo.com/bar'.match(re) // [http://foo.com, 'http', 'foo.com']
'https://foo.com/bar'.match(re) // [https://foo.com, 'https', 'foo.com']
'ftp://foo.com/bar'.match(re) // [ftp://foo.com, 'ftp', 'foo.com']
虽然看起来也满足了需求,但是我们要的只是域名,并不关心它是什么协议,所以可以把协议匹配的表达式放在非捕获组里:
const re = /(?:https?|ftp):\/\/([^\/\s]+)/
'http://foo.com/bar'.match(re) // [http://foo.com, 'foo.com']
'https://foo.com/bar'.match(re) // [https://foo.com, 'foo.com']
'ftp://foo.com/bar'.match(re) // [ftp://foo.com, 'foo.com']
4.2 命名捕获组
前面我们知道可以使用 \n
来代表第 n 个捕获组匹配的内容,然而对于一个复杂的、有很多嵌套捕获组和回溯引用的正则表达式,如果没有详细的说明的话,会变得难以分析和维护,这时候我们可以试试命名捕获组。
命名捕获组的语法为 (?<name>pattern)
, 还是上面关于 url 的例子,我们使用命名捕获组来获取 url 的协议名和主机名:
const re = /(?<protocol>https?|ftp):\/\/(?<host>[^\/\s]+)/
'https://foo.com/bar'.match(re)
// [https://foo.com, 'https', 'foo.com']
// { groups: { protocol: 'https', host: 'foo.com' }}
命名捕获组的回溯引用的语法为 \k<name>
。