2022年3月10日

前瞻断言与后瞻断言

有时候我们需要匹配后面跟着特定模式的一段模式。比如,我们要从 1 turkey costs 30€ 这段字符中匹配价格数值。

我们需要获取 符号前面的数值(假设价格是整数)。

那就是前瞻断言要做的事情。

前瞻断言

语法为:x(?=y),它表示“仅在后面是 y 的情况匹配 x”。

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

let str = "1 turkey costs 30€";

alert( str.match(/\d+(?=€)/) ); // 30 (正确地跳过了单个的数字 1)

让我们来看另一种情况:这次我们想要一个数量,它是一个不被 跟着的数值。

这里就要用到前瞻否定断言了。

语法为:x(?!y),意思是 “查找 x, 但是仅在不被 y 跟随的情况下匹配成功”。

let str = "2 turkeys cost 60€";

alert( str.match(/\d+(?!€)/) ); // 2(正确地跳过了价格)

后瞻断言

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

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

语法为:

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

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

let str = "1 turkey costs $30";

alert( str.match(/(?<=\$)\d+/) ); // 30 (跳过了单个的数字 1)

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

let str = "2 turkeys cost $60";

alert( str.match(/(?<!\$)\d+/) ); // 2 (跳过了价格)

捕获组

一般来说,环视断言括号中(前瞻和后瞻的通用名称)的内容不会成为匹配到的一部分结果。

例如:在模式 \d+(?!€) 中, 符号就不会出现在匹配结果中。

但是如果我们想要捕捉整个环视表达式或其中的一部分,那也是有可能的。只需要将其包裹在另加的括号中。

例如,这里货币符号 (€|kr) 和金额一起被捕获了:

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

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

后瞻断言也一样:

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

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

总结

当我们想根据前面/后面的上下文筛选出一些东西的时候,前瞻断言和后瞻断言(通常被称为“环视断言”)对于简单的正则表达式就很有用。

有时我们可以手动处理来得到相同的结果,即:匹配所有,然后在循环中按上下文进行筛选。请记住,str.matchAllreg.exec 返回的匹配结果有 .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. 正则表达式中修饰符(flag)例如 si 也很有用:/<body.*?>/si。这里修饰符(flag)s 使得 . 可以匹配换行符,而修饰符 i 使 <body> 大小写不敏感,可以匹配 <BODY>

教程路线图

评论

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