6.3 字符串边界
单词边界可以用来进行与单词有关的位置匹配(单词的开头、单词的结束、整个单词,等等)。字符串边界有着类似的用途,只不过是用来进行与字符串有关的位置匹配而已(字符串的开头、字符串的结束、整个字符串,等等)。
用来定义字符串边界的元字符有两个:一个是用来定义字符串开头的 ^
,另一个是用来定义字符串结尾的 $
。
我们在第 3 章里已经见识过元字符 ^
了,但那时的它是一个用来对字符集合进行“求非”操作的元字符。那它还怎么用来表明一个字符串的开头呢?
^
是几个有着多种用途的元字符之一。只有当它出现在一个字符集合里(被放在 [
和 ]
之间)并紧跟在左方括号[的后面时,它才能发挥“求非”作用。如果是在一个字符集合的外面并位于一个模式的开头, ^
将匹配字符串的开头。
为了演示字符串边界的用法,我们在下面准备了一个例子。合法的 XML 文档都必须以"<?xml>"标签开头并有一些其他属性(比如一个版本号,如<?xml version="1.0" ?>)。下面这个简单的测试可以检查一段文本是否是一篇 XML 文档:
<\?xml.*\?>
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>何方的个人小站 Blog</title>
<link>https://iamhefang.cn/code</link>
<description>何方的个人小站 Blog</description>
<lastBuildDate>Thu, 24 Mar 2022 04:02:51 GMT</lastBuildDate>
<docs>https://validator.w3.org/feed/docs/rss2.html</docs>
<generator>https://github.com/jpmonette/feed</generator>
<language>zh-hans</language>
</channel>
</rss>
这个模式似乎能够解决问题: <\? xml
匹配 "<? xml", .*
匹配随后的任意文本(.的零次或多次重复出现), \?>
匹配 "?>"。
这是一个非常不准确的测试。在下面的例子里,上例中的模式虽然匹配到了一个 XML 文档的开头部分,但位置却完全不对。它匹配到的语句位于文档的第 2 行而不是第 1 行。
<\?xml.*\?>
This is bad, real bad!
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>何方的个人小站 Blog</title>
<link>https://iamhefang.cn/code</link>
<description>何方的个人小站 Blog</description>
<lastBuildDate>Thu, 24 Mar 2022 04:02:51 GMT</lastBuildDate>
<docs>https://validator.w3.org/feed/docs/rss2.html</docs>
<generator>https://github.com/jpmonette/feed</generator>
<language>zh-hans</language>
</channel>
</rss>
模式 <\?xml.*\? >
匹配到的是整个文本的第 2 行。虽然它也是 XML 文档的开始标签,但因为出现在文本的第 2 行,所以这份文档肯定不是一份合法的 XML 文档,把它当做一份 XML 文档来处理会导致种种问题。
6.2.1 匹配左边界
这里需要的是一个能够确保被匹配到的"<?xml>"标签出现在字符串最开始处的测试,而这正是 ^
元字符大显身手的地方;如下所示:
^\s*<\?xml.*\?>
This is bad, real bad! <?xml version="1.0" encoding="utf-8"?> <rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"> <channel> <title>何方的个人小站 Blog</title> <link>https://iamhefang.cn/code</link> <description>何方的个人小站 Blog</description> <lastBuildDate>Thu, 24 Mar 2022 04:02:51 GMT</lastBuildDate> <docs>https://validator.w3.org/feed/docs/rss2.html</docs> <generator>https://github.com/jpmonette/feed</generator> <language>zh-hans</language> </channel> </rss>
^\s*<\?xml.*\?>
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>何方的个人小站 Blog</title>
<link>https://iamhefang.cn/code</link>
<description>何方的个人小站 Blog</description>
<lastBuildDate>Thu, 24 Mar 2022 04:02:51 GMT</lastBuildDate>
<docs>https://validator.w3.org/feed/docs/rss2.html</docs>
<generator>https://github.com/jpmonette/feed</generator>
<language>zh-hans</language>
</channel>
</rss>
^
匹配一个字符串的开头位置,所以 ^\s*
将匹配一个字符串的开头位置和随后的零个或多个空白字符(这解决了"<?xml>"标签前允许有空格、制表符、换行符等空白字符的问题)。作为一个整体,模式 ^\s*<\?xml.*\?>
不仅能正确地匹配一个位置正确的"<?xml>"标签,还能对合法的空白字符做出妥善处理。
6.2.2 匹配右边界
除了位置上的差异, $
的用法与 ^
完全一样。比如说,在一份 Web 页面里,</html>标签的后面不应该再有任何实际内容,而这一点可以用下面这个模式来检查:
</html>\s*$
<html>
<head>
<title>何方的个人小站</title>
</head>
<body>
<h1>何方的个人小站</h1>
</body>
</html>
我们用 \s*$
匹配一个字符串结尾处的零个或多个空白字符。
模式 ^.*$
是一个在语法上完全正确的正则表达式;它几乎总能找到一个匹配,但没有任何实际用途。你能分析出这个模式将匹配什么以及它在什么情况下会找不到任何匹配吗?
6.2.3 分行匹配模式
我们刚刚讲过, ^
匹配一个字符串的开头, $
匹配一个字符串的结尾。但这一结论并非绝对正确,它还有一个例外或者说有一种改变这种行为的办法。
有许多正则表达式都支持使用一些特殊的元字符去改变另外一些元字符行为的做法,用来启用分行匹配模式(multiline mode)的 (?m)
记号就是一个能够改变其他元字符行为的元字符序列。分行匹配模式将使得正则表达式引擎把行分隔符当做一个字符串分隔符来对待。在分行匹配模式下, ^
不仅匹配正常的字符串开头,还将匹配行分隔符(换行符)后面的开始位置(这个位置是不可见的);类似地, $
不仅匹配正常的字符串结尾,还将匹配行分隔符(换行符)后面的结束位置。
在使用时, (?m)
必须出现在整个模式的最前面,就像下面这个例子里那样。在这个例子里,我们将使用一个正则表达式把一段 JavaScript 代码里的单行注释全部查找出来:
^\s*//.*$
// Hash计算工具 export const Hash = { /** * 计算文本的SM3 * @param input 要计算SM3的文本 * @returns 16进制SM3值文本 */ async sm3(input: string): Promise<string> { // 动态加载sm3计算库,在不使用时不加载 const func = (await import("sm3")).default; return func(input); }, };
^\s*//.*$
将匹配一个字符串的开始,然后是任意多个空白字符,再后面是//(JavaScript 代码里的注释标签),再往后是任意文本,最后是一个字符串的结束。不过,这个模式只能找出第一条注释(并认为这条注释将一直延续到文件的末尾,因为 *
是一个“贪婪型”元字符)。加上 (?m)
前缀之后, (?m)^\s*//.*$
将把换行符视为一个字符串分隔符,这样就可以把每一行注释都匹配出来了。
用 ^\s*//.*$
来匹配单行注释并不是最优方案,在这里是为了演示分行模式的使用,如果需要匹配注释可以使用我们在第一章提到过的那个匹配注释的模式。
有些正则表达式实现还支持使用 \A
来定义一个字符串的开始,以 \B
来定义一个字符串的结束的做法。此时, \A
和 \B
的作用将基本等价于 ^
和 $
,但请注意, \A
和 \B
不会因为加上了 (?m)
前缀而改变行为。换句话说,在跨行匹配模式下使用 \A
和 \B
的做法不会收到在分行匹配模式下使用 ^
和 $
的效果。