匹配包含两个名称且顺序任意的字符串的正则表达式

301

我需要在正则表达式中使用逻辑AND。

类似于

jack AND james

匹配以下字符串

  • 'hi jack 这里是 james'

  • 'hi james 这里是 jack'


1
可能是重复问题:使用正则表达式以任意顺序匹配多个单词 - Anderson Green
@AndersonGreen,这个问题被过早地锁定了。答案严重缺乏,因为大多数正则表达式不识别lookaroundmode quantifier。我相信在提问时quantifier是存在的。 - XPMai
10个回答

384
您可以使用正向前瞻进行检查。以下是来自不可或缺的regular-expressions.info的概要:

前瞻和后顾,统称为“环视”,是零长度断言... 环视实际上匹配字符, 但然后放弃匹配,仅返回结果:匹配或不匹配。这就是它们被称为“断言”的原因。 它们不会消耗字符串中的字符,而只是断言是否可能存在匹配。

它接着解释了正向前瞻用于断言后面的内容与某个表达式匹配,而不会占用该匹配表达式中的字符。
下面是一个使用两个连续的正向前瞻来断言短语匹配jackjames的表达式:
^(?=.*\bjack\b)(?=.*\bjames\b).*$

点击测试。

括号内以 ?= 开头的表达式是正向前瞻。下面我将分解该模式:

  1. ^ 表示被匹配的字符串的开始位置。
  2. (?=.*\bjack\b) 是第一个正向前瞻,表示接下来的内容必须与 .*\bjack\b 匹配。
  3. .* 表示任意字符出现零次或多次。
  4. \b 表示任意单词边界(空格、开头、结尾等)。
  5. jack 就是这四个字符连写(下一个正向前瞻中的 james 同理)。
  6. $ 表示被匹配的字符串的结束位置。

所以,第一个正向前瞻表示“后面紧跟(并且不是前后瞻)的表达式必须以零个或多个任意字符、单词边界和 jack,再以单词边界结尾”,第二个正向前瞻表示“后面紧跟的表达式必须以零个或多个任意字符、单词边界和 james,再以单词边界结尾”。两个正向前瞻后是 .*,它只是零次或多次匹配任意字符,然后是 $,它匹配字符串结束位置。

因此,“从任意字符开始,然后是 jack 或 james,最后以任意字符结尾”满足第一个正向前瞻,因为有一些字符紧随其后,然后是单词 jack,它也满足第二个正向前瞻,因为有一些字符(其中恰好包括 jack,但这并不是满足第二个正向前瞻的必要条件)紧随其后,然后是单词 james。两个正向前瞻都没有断言字符串结束位置,所以接下来的 .* 可以匹配超出两个前瞻匹配范围的内容,例如“最后以任何字符结尾”。

我想你明白了,但为了更加清楚,以下是反转了 jackjames 的情况,即“从任意字符开始,然后是 james 或 jack,最后以任意字符结尾”;它满足第一个正向前瞻,因为有一些字符紧随其后,然后是单词 james,它也满足第二个正向前瞻,因为有一些字符(其中恰好包括 james,但这并不是满足第二个正向前瞻的必要条件)紧随其后,然后是单词 jack。与前面相同,两个正向前瞻都没有断言字符串结束位置,所以接下来的 .* 可以匹配超出两个前瞻匹配范围的内容,例如“最后以任何字符结尾”。

这种方法的优点是可以轻松指定多个条件。

^(?=.*\bjack\b)(?=.*\bjames\b)(?=.*\bjason\b)(?=.*\bjules\b).*$

35
有人介意详细解释一下这个例子是如何工作的吗? - bjmc
5
vim 语法:^\(.*\<jack\>\)\@=\(.*\<james\>\@=\).*$ 或者 \v^(.*<jack>)@=(.*<james>)@=.*$这是一个用于在 vim 编辑器中匹配文本的正则表达式。它的含义是匹配包含“jack”但不包含“james”的任何行。其中,\(\) 用于分组,\@= 表示只有当组内的内容存在时才匹配该组,\<\> 表示单词的开头和结尾,\v 则表示使用“非规范模式”,使得正则表达式更加简洁。 - mykhal
3
\b在这里是什么意思? - user2286243
1
如何改进它以匹配例如一行中的“Jack”和下一行中的“James”? - Kfcaio
4
@bjmc似乎作者不打算解释,所以我解释了一下。 - rory.ap
显示剩余9条评论

173

尝试:

james.*jack
如果你想同时拥有两者,那么使用or运算符:
james.*jack|jack.*james

1
被接受的答案有效。这对我也完美地起作用了。在Visual Studio中搜索代码,请使用“查找结果”。 - Yogurt The Wise
7
这个对我很有用,比被接受的答案更为简洁易懂! - Kumar Manish
2
我需要一个只有两个名称匹配的解决方案,因此对于这种情况,这个答案更加简洁。但是,当超过2个名称时,被接受的答案变得更加简洁,因为“或”的数量呈阶乘增加。对于3个名称,将有6个“或”,4个名称将有24个“或”等等。 - WileCau
2
我建议将其设置为惰性匹配 james.*?jack|jack.*?james。这对于大文本有所帮助。 - Jekis
1
请注意,这也将匹配“jacky”和“jameson”等名称。 - Gershom Maes
唯一的问题是,如果不需要n²个组,就无法使用适当的捕获组。 - Nixinova

59
我将要编写的命令的解释:

.表示任何字符或数字可以出现在它的位置。

*表示前一个字符或数字可以出现零次或多次。

|表示“或”。

因此,

james.*jack

要搜索 james,然后是任意数量的字符,直到出现jack

因为你要么想要jack.*james,要么想要james.*jack

因此命令

jack.*james|james.*jack

11
顺便说一下,你也可以编辑@icyrock的答案(与你的答案相同,只是早了6年),你的解释本身非常有用。 - WoJ
2
谢谢您的回答,但我觉得需要指出,在VSCode搜索中,您的答案** jack.*james | james.*jack **会在搜索过程中考虑'|'(或)符号之间的空格。 **jack.james|james.jack 有效且不会查找空格。 - jgritten
2
如果 $_explanation === "awesome",那么返回 $THUMBS_UP。ENDIF; - Syed Aqeel
1
你不需要2000个声望才能获得编辑特权吗? - Chris Strickland

37

简短而精炼

(?=.*jack)(?=.*james)

测试用例:

[
  "xxx james xxx jack xxx",
  "jack xxx james ",
  "jack xxx jam ",
  "  jam and jack",
  "jack",
  "james",
]
.forEach(s => console.log(/(?=.*james)(?=.*jack)/.test(s)) )


你能说一下它是如何工作的吗?Lookahead 需要前面的单词,但这里没有。在这种情况下,element (?=.*jack) 的结果将是 element,而对于 (?=.*jack) 将没有结果。也在此处尝试了示例字符串:https://regex101.com。 - sygneto

9

您可以做:

\bjack\b.*\bjames\b|\bjames\b.*\bjack\b

7
本回答中的表达式 可以实现对任意顺序下的一个 jack 和一个 james 的匹配。
在这里,我们将探索其他场景。

方法1:一个 jack 和一个 james

假设只允许有一个 jack 和一个 james,不允许有两个 jack 或两个 james,我们可以设计类似于以下的表达式:
^(?!.*\bjack\b.*\bjack\b)(?!.*\bjames\b.*\bjames\b)(?=.*\bjames\b)(?=.*\bjack\b).*$

在这里,我们将使用以下语句来排除这些实例:
(?!.*\bjack\b.*\bjack\b)

and,

(?!.*\bjames\b.*\bjames\b)

正则表达式演示 1

我们还可以简化为:

^(?!.*\bjack\b.*\bjack\b|.*\bjames\b.*\bjames\b)(?=.*\bjames\b|.*\bjack\b).*$

正则表达式演示2


如果您想简化/更新/探索表达式,可以在regex101.com的右上角面板上找到详细说明。您可以观看匹配步骤或在此调试器链接中修改它们,如果您感兴趣的话。该调试器演示了正则表达式引擎如何逐步消耗一些示例输入字符串并执行匹配过程。


正则表达式电路

jex.im将正则表达式可视化:

enter image description here

测试

const regex = /^(?!.*\bjack\b.*\bjack\b|.*\bjames\b.*\bjames\b)(?=.*\bjames\b|.*\bjack\b).*$/gm;
const str = `hi jack here is james
hi james here is jack
hi james jack here is jack james
hi jack james here is james jack
hi jack jack here is jack james
hi james james here is james jack
hi jack jack jack here is james
`;
let m;

while ((m = regex.exec(str)) !== null) {
    // This is necessary to avoid infinite loops with zero-width matches
    if (m.index === regex.lastIndex) {
        regex.lastIndex++;
    }
    
    // The result can be accessed through the `m`-variable.
    m.forEach((match, groupIndex) => {
        console.log(`Found match, group ${groupIndex}: ${match}`);
    });
}


方法二:按照特定顺序使用一个jack和一个james

这个表达式也可以设计成先使用一个james再使用一个jack,类似于下面的表达式:

^(?!.*\bjack\b.*\bjack\b|.*\bjames\b.*\bjames\b)(?=.*\bjames\b.*\bjack\b).*$

正则表达式演示 3

反之亦然:

^(?!.*\bjack\b.*\bjack\b|.*\bjames\b.*\bjames\b)(?=.*\bjack\b.*\bjames\b).*$

正则表达式演示 4


1
很好的解释。如果您的方法1可以匹配任意顺序的“james”和“jack”,那就更好了。测试后,我发现您的正则表达式只能匹配单个“james”或“jack”。 - Kfcaio

7

不需要两个预测, 一个子字符串可以正常地匹配。

^(?=.*?\bjack\b).*?\bjames\b.*

在regex101上查看此演示

Lookarounds是零长度断言(条件)。此处的前瞻在^开始处检查字符串中是否稍后出现了jack,并在成功时匹配到james.*其余部分(可以删除)。在单词(用\b单词边界括起来)之前使用惰性 点号。使用i标志忽略大小写


1
非常好的回答,感谢分享。一个问题:在最后一个\b之后我们需要加上.*吗?还是没有也可以工作? - RavinderSingh13
1
@RavinderSingh13 感谢您的评论,非常好的观点!仅仅验证末尾的 .* 确实是没有用的,只有在需要完全匹配时才需要它。 - bobble bubble

5

Vim有一个分支运算符\&,在搜索包含一组单词的行时非常有用,而且扩展所需单词集合也很简单。

例如,

/.*jack\&.*james

将匹配任意顺序包含jackjames的行。

有关使用的更多信息,请参见这个答案。我不知道其他正则表达式风格实现分支,该运算符甚至没有在正则表达式维基百科条目中记录。


5

由于不是所有时候都支持 lookaround,因此您可以利用正则表达式的量词功能。

(\bjames\b){1,}.*(\bjack\b){1,}|(\bjack\b){1,}.*(\bjames\b){1,}

为什么没有人尝试这个?0票答案可能是最好的。谢谢啊,伙计。 - captain_majid
@captain_majid,我道歉。经过深入的研究并基于误报的数据,我意识到我的原始答案是错误的。我已经修正了正则表达式代码。这个正确的正则表达式将按预期完美地工作。 - XPMai
你的第一个示例对我来说很好用,甚至像这样更简单的一个也可以: \b(word1|word2|word3|word4|etc)\b我在这里测试过了:https://rubular.com/r/Pgn2d6dXXXHoh7 - captain_majid

0
到目前为止,所有的答案都适用于找到匹配项,但并非所有答案都适用于突出显示该匹配项。例如:如果您想使用grep的"--only-matching"或"--color"选项,那么到目前为止只有一种答案适用:james.*jack|jack.*james。我将其称为"排列组合技术",而其他的则是"环视技术"和"vim分支技术"。
"环视技术"不会进行任何突出显示,因为它总是匹配一个零长度的字符串,这是根据定义的。也就是说,对于这个输入文本:
hi jack here is james
hi james here is jack

一个(Perl)正则表达式(?=.*jack)(?=.*james)不会突出显示任何内容。您可以通过在大多数Unix shell中运行此命令进行测试:
printf 'hi jack here is james\nhi james here is jack\n' | grep --color --perl '(?=.*jack)(?=.*james)'

一些答案在开头和结尾添加了.*。这样会突出显示一些东西 - 整行 - 但如果我们的目标是突出显示我们正在寻找的单词以及这些单词之间的内容,那就没有帮助了。 Vim分支技术(也称为\&)会突出显示一些看起来可能有用的内容,但这可能不是你想要的。对于相同的输入文本,Vim搜索/.*james\&.*jack会突出显示hi jackhi james here is jack。要从shell中测试,请运行以下命令:
printf 'hi jack here is james\nhi james here is jack\n' | vim -R - '+/.*james\&.*jack'

只有排列组合技术才能突出最有用的内容:这里的杰克是詹姆斯这里的詹姆斯是杰克。要从命令行测试此功能:
printf 'hi jack here is james\nhi james here is jack\n' | grep --color --perl 'james.*jack|jack.*james'

我在这里写的一切都是基于你想要一个可以适用于三个或更多单词的技巧。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接