2022年6月7日

前瞻和后顾

有时我们需要找到仅满足以下条件的模式匹配:该模式后面或前面跟着另一个模式。

有一种特殊的语法用于此,称为“前瞻”和“后顾”,统称为“环顾”。

首先,让我们从类似 1 turkey costs 30€ 的字符串中找到价格。即:一个数字,后面跟着 符号。

前瞻

语法为:X(?=Y),表示“查找 X,但仅在后面跟着 Y 时匹配”。XY 可以是任何模式。

对于一个整数后面跟着,正则表达式将是\d+(?=€)

let str = "1 turkey costs 30€";

alert( str.match(/\d+(?=€)/) ); // 30, the number 1 is ignored, as it's not followed by €

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

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

更复杂的测试是可能的,例如X(?=Y)(?=Z)意味着

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

换句话说,这种模式意味着我们正在寻找X,后面紧跟着YZ

只有当模式YZ不是相互排斥的时候才有可能。

例如,\d+(?=\s)(?=.*30)查找\d+,后面跟着一个空格(?=\s),并且在它之后某个地方有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 (the price is not matched)

后行断言

后行断言浏览器兼容性

请注意:后行断言在非 V8 浏览器(如 Safari、Internet Explorer)中不受支持。

先行断言允许为“后面是什么”添加条件。

后行断言类似,但它向后看。也就是说,它允许仅在它前面有某些内容的情况下匹配模式。

语法是

  • 正向后查找:(?<=Y)X,匹配X,但仅当它之前有Y
  • 负向后查找:(?<!Y)X,匹配X,但仅当它之前没有Y

例如,让我们将价格更改为美元。美元符号通常在数字之前,因此要查找$30,我们将使用(?<=\$)\d+ - 一个以$开头的金额。

let str = "1 turkey costs $30";

// the dollar sign is escaped \$
alert( str.match(/(?<=\$)\d+/) ); // 30 (skipped the sole number)

并且,如果我们需要数量 - 一个数字,而不是以$开头,那么我们可以使用负向后查找(?<!\$)\d+

let str = "2 turkeys cost $60";

alert( str.match(/(?<!\$)\b\d+/g) ); // 2 (the price is not matched)

捕获组

通常,后视括号内的内容不会成为结果的一部分。

例如,在模式\d+(?=€)中,符号不会作为匹配的一部分被捕获。这是自然的:我们寻找一个数字\d+,而(?=€)只是一个测试,它应该以结尾。

但在某些情况下,我们可能还想捕获后视表达式,或其一部分。这是可能的。只需将该部分包装在额外的括号中。

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

let str = "1 turkey costs 30€";
let regexp = /\d+(?=(€|kr))/; // extra parentheses around €|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 之后

任务

有一串整数。

创建一个正则表达式,只查找非负整数(允许零)。

使用示例

let regexp = /your 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

如您所见,它匹配 8,来自 -18。为了排除它,我们需要确保正则表达式从另一个(不匹配)数字的中间开始匹配数字。

我们可以通过指定另一个负向后顾来做到这一点:(?<!-)(?<!\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>。该标签可能具有属性。

例如

let regexp = /your regular expression/;

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.*?> 开头的那些位置。

因此,它用 <h1>Hello</h1> 替换以 <body.*?> 开头的“空行”。这就是在 <body> 之后插入。

P.S. 正则表达式标志,例如 si 也很有用:/<body.*?>/sis 标志使点 . 匹配换行符,i 标志使 <body> 也匹配 <BODY>,不区分大小写。

教程地图

评论

在评论之前请阅读…
  • 如果您有改进建议,请提交 GitHub 问题或拉取请求,而不是评论。
  • 如果您不理解文章中的某些内容,请详细说明。
  • 要插入几行代码,请使用 <code> 标签,对于多行代码,请将其包装在 <pre> 标签中,对于超过 10 行的代码,请使用沙箱 (plnkrjsbincodepen…)