有时候我们需要匹配后面跟着特定模式的一段模式。比如,我们要从 1 turkey costs 30€
这段字符中匹配价格数值。
我们需要获取 €
符号前面的数值(假设价格是整数)。
那就是前瞻断言要做的事情。
前瞻断言
语法为:x(?=y)
,它表示 “匹配 x
, 仅在后面是 y
的情况””
那么对于一个后面跟着 €
的整数金额,它的正则表达式应该为:\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, $
请注意,对于后瞻断言,顺序保持不变,尽管前瞻括号在主模式之前。
通常括号是从左到右编号,但是后瞻断言是一个例外,它总是在主模式之后被捕获。所以 \d+
的匹配会首先进入结果数组,然后是 (\$|£)
。
总结
当我们想根据前面/后面的上下文筛选出一些东西的时候,前瞻断言和后瞻断言(通常被称为“环视断言”)对于简单的正则表达式就很有用。
有时我们可以手动处理来得到相同的结果,即:匹配所有,然后在循环中按上下文进行筛选。请记住,str.matchAll
和reg.exec
返回的匹配结果有 .index
属性,因此我们能知道它在文本中的确切位置。但通常正则表达式可以做得更好。
环视断言类型:
模式 | 类型 | 匹配 |
---|---|---|
x(?=y) | 前瞻肯定断言 | x ,仅当后面跟着 y |
x(?!y) | 前瞻否定断言 | x ,仅当后面不跟 y |
(?<=y)x | 后瞻肯定断言 | x ,仅当跟在 y 后面 |
(?<!y)x | 后瞻否定断言 | x ,仅当不跟在 y 后面 |
前瞻断言也可用于禁用回溯。为什么它是需要的 – 请看下一章。
任务
Find non-negative integers
There’s a string of integer numbers.
Create a regexp that looks for only non-negative ones (zero is allowed).
An example of use:
let regexp = /your regexp/g;
let str = "0 12 -5 123 -18";
alert( str.match(regexp) ); // 0, 12, 123
解决方案
The regexp for an integer number is \d+
.
We can exclude negatives by prepending it with the negative lookahead: (?<!-)\d+
.
Although, if we try it now, we may notice one more “extra” result:
let regexp = /(?<!-)\d+/g;
let str = "0 12 -5 123 -18";
console.log( str.match(regexp) ); // 0, 12, 123, 8
As you can see, it matches 8
, from -18
. To exclude it, we need to ensure that the regexp starts matching a number not from the middle of another (non-matching) number.
We can do it by specifying another negative lookbehind: (?<!-)(?<!\d)\d+
. Now (?<!\d)
ensures that a match does not start after another digit, just what we need.
We can also join them into a single lookbehind here:
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 = /ваше регулярное выражение/;
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>
.
P.S. Этому регулярному выражению не помешают флаги: /<body.*>/si
, чтобы в “точку” входил перевод строки (тег может занимать несколько строк), а также чтобы теги в другом регистре типа <BODY>
тоже находились.