2022年10月30日

前瞻断言与后瞻断言

有时我们只需要为一个模式找到那些在另一个模式之后或之前的匹配项。

有一种特殊的语法,称为“前瞻断言(lookahead)”和“后瞻断言(lookbehind)”。

首先,让我们从字符串中查找价格,例如 1 turkey costs 30€。即:一个数字,后跟符号。

前瞻断言

语法为:x(?=y),它表示“仅在后面是 Y 时匹配 X”。这里的 XY 可以是任何模式。

那么对于一个后面跟着 的整数,正则表达式应该为:\d+(?=€)

let str = "1 turkey costs 30€";

alert( str.match(/\d+(?=€)/) ); // 30,数字 1 被忽略了,因为它后面没有 €

请注意:前瞻断言只是一个测试,括号 (?=...) 中的内容不包含在匹配结果 30 中。

当我们查找 X(?=Y) 时,正则表达式引擎会找到 X,然后检查其后是否有 Y。如果没有,则跳过潜在匹配,并继续搜索。

更复杂的测试也是可能的,例如 X(?=Y)(?=Z) 表示:

  1. 寻找 X
  2. 检查 Y 是否紧跟在 X 之后(如果不是则跳过)。
  3. 检查 Z 是否也在 X 之后(如果不是则跳过)。
  4. 如果两个测试都通过了,那么 X 是匹配的,否则继续搜索。

换句话说,这样的模式意味着我们同时在寻找 X 后跟 YZ

这只有在模式 YZ 不是互斥的情况下才可行。

例如,\d+(?=\s)(?=.*30) 查找后跟着空格 (?=\s)\d+,并且有 30 在它之后的某个地方 (?=.*30)

let str = "1 turkey costs 30€";

alert( str.match(/\d+(?=\s)(?=.*30)/) ); // 1

在我们给出的字符串中,与数字 1 完全匹配。

否定的前瞻断言

假设我们想要一个数量,而不是来自同一字符串的价格。那是一个数字 \d+,后面不是

为此,我们可以使用否定的前瞻断言。

语法是:X(?!Y),意思是“搜索 X,但前提是后面没有 Y”。

let str = "2 turkeys cost 60€";

alert( str.match(/\d+\b(?!€)/g) ); // 2(价格不匹配)

后瞻断言

后瞻断言的浏览器兼容情况

请注意:非 V8 引擎的浏览器不支持后瞻断言,例如 Safari、Internet Explorer。

前瞻断言允许添加一个“后面要跟着什么”的条件判断。

后瞻断言也类似,只不过它是在相反的方向上进行条件判断。也就是说,它只允许匹配前面有特定字符串的模式。

语法为如下:

  • 肯定的后瞻断言:(?<=Y)X,匹配 X,仅在前面是 Y 的情况下。
  • 否定的后瞻断言:(?<!Y)X,匹配 X,仅在前面不是 Y 的情况下。

例如,让我们把价格换成美元。美元符号通常在数字前面,所以要查找 $30 我们将使用 (?<=\$)\d+ —— 一个前面带 $ 的数值:

let str = "1 turkey costs $30";

// 美元符号被转义 \$
alert( str.match(/(?<=\$)\d+/) ); // 30(跳过了仅仅是数字的值)

如果我们需要找到量词 —— 一个前面不带 $ 的数字,我们可以使用否定的后瞻断言:(?<!\$)\d+

let str = "2 turkeys cost $60";

alert( str.match(/(?<!\$)\b\d+/g) ); // 2(价格不匹配)

捕获组

一般来说,前瞻断言和后瞻断言括号中的内容不会成为结果的一部分。

例如,在模式 \d+(?!€) 中, 符号就不会出现在匹配结果中。这是很自然的事:我们寻找一个数字 \d+,而 (?=€) 只是一个测试,表示要匹配的数字后面应该紧跟着 字符。

但在某些情况下,我们可能还想捕获前瞻断言和后瞻断言所匹配的内容,或者部分内容。这也是可行的。只需要将该部分包装在额外的括号中。

在下面的示例中,货币符号 (€|kr) 和金额一起被捕获了:

let str = "1 turkey costs 30€";
let regexp = /\d+(?=(€|kr))/; // €|kr 两侧有额外的括号

alert( str.match(regexp) ); // 30, €

后瞻断言也一样:

let str = "1 turkey costs $30";
let regexp = /(?<=(\$|£))\d+/;

alert( str.match(regexp) ); // 30, $

总结

当我们想根据前面/后面的上下文匹配某些内容的时候,前瞻断言和后瞻断言(通常被称为“环视断言”)很有用。

对于简单的正则表达式,我们可以手动执行类似的操作。即:不管上下文,匹配所有可匹配的内容,然后在循环中根据上下文进行过滤。

请记住,str.match(没有修饰符 g)和 str.matchAll(总是)将匹配项作为具有 index 属性的数组返回,因此我们知道它在文本中的确切位置,并且可以检查上下文。

但通常环视断言更方便。

环视断言类型:

模式 类型 匹配
X(?=Y) 肯定的前瞻断言 X 后紧跟着 Y
X(?!Y) 否定的前瞻断言 X 后没紧跟着 Y
(?<=Y)X 肯定的后瞻断言 X 紧跟在 Y 后面
(?<!Y)X 否定的后瞻断言 X 没紧跟在 Y 后面

任务

这里有一个由整数组成的字符串。

创建一个正则表达式来找出所有的非负整数(包括 0)。

使用示例:

let regexp = /你的正则表达式/g;

let str = "0 12 -5 123 -18";

alert( str.match(regexp) ); // 0, 12, 123

整数的正则表达式是 \d+

我们可以通过在它前面加上否定的后瞻断言来排除负数:(?<!-)\d+

尽管如此,如果我们现在尝试使用上面的正则表达式,会发现有一个“例外”情况:

let regexp = /(?<!-)\d+/g;

let str = "0 12 -5 123 -18";

console.log( str.match(regexp) ); // 0, 12, 123, 8

正如我们所看到的,它从 -18 中配到了 8。要排除这种情况,我们需要确保正则表达式要从一个数的开头开始匹配数字,而不是从另一个(不匹配的)数字的中间开始进行匹配。

我们可以通过指定另一个否定的后瞻断言来实现这一点:(?<!-)(?<!\d)\d+。现在 (?<!\d) 确保匹配不会从另一个数字之后开始进行匹配了,这正是我们所需要的。

我们也可以把它们合并成一个后瞻断言:

let regexp = /(?<![-\d])\d+/g;

let str = "0 12 -5 123 -18";

alert( str.match(regexp) ); // 0, 12, 123

我们有一个带有 HTML 文档的字符串。

编写一个正则表达式,在 <body> 标签之后立即插入 <h1>Hello</h1>。标签可能具有特性(attribute)。

例如:

let regexp = /你的正则表达式/;

let str = `
<html>
  <body style="height: 200px">
  ...
  </body>
</html>
`;

str = str.replace(regexp, `<h1>Hello</h1>`);

之后,str 的值应该为:

<html>
  <body style="height: 200px"><h1>Hello</h1>
  ...
  </body>
</html>

为了在 <body> 标签后面插入内容,我们必须先找到它。我们可以使用正则表达式 <body.*?> 来实现。

在本任务中,我们不需要修改 <body> 标签。我们只需要在它后面添加文本。

我们可以这样做:

let str = '...<body style="...">...';
str = str.replace(/<body.*?>/, '$&<h1>Hello</h1>');

alert(str); // ...<body style="..."><h1>Hello</h1>...

在替换字符串中,$& 表示匹配本身,即源文本中与 <body.*?> 相对应的部分。它会被它自身加上 <h1>Hello</h1> 替换。

另一种方法是使用后瞻断言:

let str = '...<body style="...">...';
str = str.replace(/(?<=<body.*?>)/, `<h1>Hello</h1>`);

alert(str); // ...<body style="..."><h1>Hello</h1>...

正如你所看到的,这个正则表达式中只有后瞻断言部分。

它的工作原理如下:

  • 在文本的每个位置。
  • 检查它前面是否有 <body.*?>
  • 如果有,就匹配该位置。

标签 <body.*?> 不会被作为结果返回。这个正则表达式的结果实际上是一个空字符串,但它只匹配前面紧挨着 <body.*?> 的位置。

因此,它将紧挨着 <body.*?> 的“空位置”替换为了 <h1>Hello</h1>。这样就在 <body> 之后插入了内容。

P.S. 正则表达式中的修饰符,例如 si 也很有用:/<body.*?>/si。这里修饰符 s 使得 . 可以匹配换行符,而修饰符 i 使 <body> 大小写不敏感,可以匹配 <BODY>

教程路线图

评论

在评论之前先阅读本内容…
  • 如果你发现教程有错误,或者有其他需要修改和提升的地方 — 请 提交一个 GitHub issue 或 pull request,而不是在这评论。
  • 如果你对教程的内容有不理解的地方 — 请详细说明。
  • 使用 <code> 标签插入只有几个词的代码,插入多行代码可以使用 <pre> 标签,对于超过 10 行的代码,建议你使用沙箱(plnkrJSBincodepen…)