正则表达式用于匹配不包含某个单词的行

5321

我知道可以使用其他工具(例如 grep -v)来匹配一个词并反转匹配结果。然而,是否可能使用正则表达式来匹配不包含特定单词(例如 hede)的行?

输入:
hoho
hihi
haha
hede
代码:
grep "<Regex for 'doesn't contain hede'>" input
期望的输出:
hoho
hihi
haha

105
也许有几年的延迟,但是这个正则表达式 ([^h]*(h([^e]|$)|he([^d]|$)|hed([^e]|$)))* 有什么问题吗?它的思想很简单。继续匹配直到看到不想要的字符串的开头,然后只在字符串未完成的N-1种情况下进行匹配(其中N为字符串的长度)。这N-1种情况是“h之后非e”,“he之后非d”和“hed之后非e”。如果你成功通过了这些N-1种情况,那么你就成功地没有匹配上不想要的字符串,所以你可以开始再次寻找[^h]* - stevendesu
430
尝试输入“a-very-very-long-word”或更好的方式是半个句子。打字愉快。顺便说一句,这几乎无法阅读。不清楚性能影响如何。 - Peter Schuetze
14
@PeterSchuetze:当然,对于非常长的单词来说,这种方法可能不太美观,但它是可行且正确的解决方案。虽然我没有测试过其性能,但我认为它不会太慢,因为大多数后面的规则都被忽略了,直到你看到一个h(或单词、句子等的第一个字母)。而且你可以很容易地使用迭代连接生成长字符串的正则表达式字符串。如果它有效且能够快速生成,那么可读性重要吗?对此可以在评论中进行说明。 - stevendesu
66
@stevendesu: 我来晚了,但是那个答案几乎完全错误。首先,它要求主题包含“h”,而实际上不应该要求,因为任务是“匹配不包含特定单词的行”。我们假设你的意思是使内部组变为可选的,并且该模式已经被锚定:^([^h]*(h([^e]|$)|he([^d]|$)|hed([^e]|$))?)*$这个模式在 "hhede" 等包含部分"hede"的实例之前出现时失败了。 - jaytea
20
这个问题已经被添加到Stack Overflow正则表达式常见问题解答下的“高级Regex-Fu”部分。 - aliteralmind
35个回答

12

通过 PCRE 命令 (*SKIP)(*F)

^hede$(*SKIP)(*F)|^.*$

这将完全跳过包含字符串hede的行,并匹配其余所有行。

演示

部分执行:

我们将上述正则表达式分成两个部分。

  1. |符号之前的部分。该部分不应被匹配

    ^hede$(*SKIP)(*F)
    
  2. |符号后的部分。这部分应该被匹配

  3. ^.*$
    

第一部分

正则表达式引擎将从第一部分开始执行。

^hede$(*SKIP)(*F)

说明:

  • ^ 断言我们处于开头。
  • hede 匹配字符串 hede
  • $ 断言我们处于行尾。

因此,包含字符串 hede 的行将被匹配。一旦正则表达式引擎看到以下的 (*SKIP)(*F)注意:您可以将 (*F) 写成 (*FAIL))语句,它将跳过并使匹配失败。在 PCRE(Perl Compatible Regular Expressions)相关语句后添加逻辑或运算符 |,这将匹配除包含精确字符串 hede 的行之外的所有行中每个字符之间的所有边界。请参见演示此处。也就是说,它尝试从剩余的字符串中匹配字符。然后执行第二部分的正则表达式。

第二部分

^.*$

Explanation:

  • ^ 表示匹配字符串的开头。在这个例子中,它匹配除了hede行以外的所有行的开头。查看演示here
  • .* 在多行模式下,.会匹配除换行符和回车符之外的任何字符。 * 会重复前一个字符零次或多次。因此,.*会匹配整行内容。查看演示here

    为什么你要添加.*而不是.+?

    因为.*与空行匹配,但.+不会匹配空行。我们想要匹配除hede行以外的所有行,输入中可能还有空行。所以必须使用.*而不是.+.+会重复前一个字符一次或多次。在这里查看.*匹配空行的例子here

  • $ 表示字符串的结尾,但在这里不是必需的。


8

TXR语言支持正则表达式的否定。

$ txr -c '@(repeat)
@{nothede /~hede/}
@(do (put-line nothede))
@(end)'  Input

一个更复杂的例子:匹配所有以 a 开头并以 z 结尾,但不包含子字符串 hede 的行:
$ txr -c '@(repeat)
@{nothede /a.*z&~.*hede.*/}
@(do (put-line nothede))
@(end)' -
az         <- echoed
az
abcz       <- echoed
abcz
abhederz   <- not echoed; contains hede
ahedez     <- not echoed; contains hede
ace        <- not echoed; does not end in z
ahedz      <- echoed
ahedz

正则表达式的否定本身并不特别有用,但当您还拥有交集时,事情变得有趣了,因为您拥有完整的布尔集合操作:您可以表达“与此匹配的集合,除了与那个匹配的东西”。


请注意,这也是 ElasticSearch 基于 Lucene 的正则表达式的解决方案。 - Wiktor Stribiżew

8
在你的代码中使用两个正则表达式可能更易于维护,一个用于第一次匹配,如果匹配成功,则运行第二个正则表达式以检查您希望阻止的异常情况,例如^.*(hede).*,然后在代码中使用适当的逻辑。
好吧,我承认这不是对发布的问题的真正答案,并且它可能比单个正则表达式稍微多一些处理。但是对于寻找快速应急解决方案的开发人员来说,这个解决方案不应被忽视。

6
我想再举一个例子,如果您想匹配包含字符串 X 的整行,但不包含字符串 Y
例如,假设我们想检查 URL / 字符串是否包含 "tasty-treats",只要它不在任何位置也包含 "chocolate",那么这个正则表达式模式将起作用(在 JavaScript 中也适用)。
^(?=.*?tasty-treats)((?!chocolate).)*$

(全局匹配、多行匹配)

交互式示例:https://regexr.com/53gv4

匹配结果

(这些网址包含“tasty-treats”,但不包含“chocolate”)

  • example.com/tasty-treats/strawberry-ice-cream
  • example.com/desserts/tasty-treats/banana-pudding
  • example.com/tasty-treats-overview

不匹配结果

(这些网址中某处包含“chocolate”,因此它们不会匹配,即使它们包含“tasty-treats”)

  • example.com/tasty-treats/chocolate-cake
  • example.com/home-cooking/oven-roasted-chicken
  • example.com/tasty-treats/banana-chocolate-fudge
  • example.com/desserts/chocolate/tasty-treats
  • example.com/chocolate/tasty-treats/desserts

6
下面的函数将帮助您获得所需的输出。
<?PHP
      function removePrepositions($text){

            $propositions=array('/\bfor\b/i','/\bthe\b/i'); 

            if( count($propositions) > 0 ) {
                foreach($propositions as $exceptionPhrase) {
                    $text = preg_replace($exceptionPhrase, '', trim($text));

                }
            $retval = trim($text);

            }
        return $retval;
    }


?>

6
只要你处理的是“行”,就将负匹配标记并针对其余部分进行目标处理。
实际上,我在使用sed时也使用了这个技巧,因为^((?!hede).)*$似乎不被其支持。
对于所需的输出:
  1. Mark the negative match: (e.g. lines with hede), using a character not included in the whole text at all. An emoji could probably be a good choice for this purpose.

    s/(.*hede)/\1/g
    
  2. Target the rest (the unmarked strings: e.g. lines without hede). Suppose you want to keep only the target and delete the rest (as you want):

    s/^.*//g
    

为更好的理解

假设您想要删除目标

  1. Mark the negative match: (e.g. lines with hede), using a character not included in the whole text at all. An emoji could probably be a good choice for this purpose.

    s/(.*hede)/\1/g
    
  2. Target the rest (the unmarked strings: e.g. lines without hede). Suppose you want to delete the target:

    s/^[^].*//g
    
  3. Remove the mark:

    s///g
    

5

^((?!hede).)*$ 是一种优雅的解决方案,但由于它会消耗字符,您将无法与其他条件组合使用。例如,假设您想检查“hede”不存在且“haha”存在,这个解决方案可以工作,因为它不会消耗字符:

^(?!.*\bhede\b)(?=.*\bhaha\b) 

3

如何使用PCRE的回溯控制字符来匹配不含某个单词的行

这里介绍一种我以前没有见过的方法:

/.*hede(*COMMIT)^|/

它是如何工作的

首先,它尝试在行中找到"hede"。如果成功,此时(*COMMIT)告诉引擎,在失败的情况下不仅不回溯,而且也不会尝试任何进一步的匹配。然后,我们尝试匹配一个不可能匹配的东西(在本例中,是^)。

如果一行不包含"hede",则第二个选择,一个空的子模式,可以成功匹配主题字符串。

这种方法并不比负向先行断言更有效率,但我认为我应该将其放在这里,以防有人发现它很棒,并且能够在其他更有趣的应用程序中使用它。


3

一个更简单的解决方案是使用非运算符!

你的if语句需要匹配"包含"而不是匹配"排除"。

var contains = /abc/;
var excludes =/hede/;

if(string.match(contains) && !(string.match(excludes))){  //proceed...

我相信正则表达式的设计者们预料到了非运算符的使用。


2
也许在尝试编写能够匹配行段(而不是整行)中不包含子字符串的正则表达式时,你会在Google上找到这篇文章。我花了一些时间才弄清楚,所以我想分享一下:
给定一个字符串:<span class="good">bar</span><span class="bad">foo</span><span class="ugly">baz</span> 我想匹配不包含子字符串“bad”的<span>标签。 /<span(?:(?!bad).)*?>将匹配<span class=\"good\"><span class=\"ugly\">
请注意,有两组(层)括号:
- 最内部的是负向先行断言(它不是捕获组) - 最外层被 Ruby 解释为捕获组,但我们不希望它成为捕获组,所以我在开头添加了 ?:,它不再被解释为捕获组。
Ruby 中的演示:
s = '<span class="good">bar</span><span class="bad">foo</span><span class="ugly">baz</span>'
s.scan(/<span(?:(?!bad).)*?>/)
# => ["<span class=\"good\">", "<span class=\"ugly\">"]

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