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

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个回答

37

既然没有其他人直接回答了问题(提问者提出的问题),那我来回答吧。

答案是,在 POSIX grep 中,无法实现文字上的满足这个请求:

grep "<Regex for 'doesn't contain hede'>" input

没有使用标志的情况下,POSIX grep 只需要使用基本正则表达式 (BREs) 工作,这些表达式不足以完成该任务,因为子表达式中缺少交替。它仅支持一种交替方式,即提供多个用新行分隔的正则表达式,而这并不能覆盖所有正则语言,例如没有有限集合的 BREs 能够匹配与 扩展正则表达式 (ERE) ^(ab|cd)*$ 相同的正则语言。 然而,GNU grep 实现了允许这样做的扩展。特别地,\| 是 GNU BREs 实现中的交替运算符。如果您的正则表达式引擎支持交替、括号和 Kleene 星号,并且能够锚定到字符串的开头和结尾,那么这就是你需要的所有内容。但请注意,负集 [^ ... ] 对于此方法非常方便,因为否则,您需要用形如 (a|b|c| ... ) 的表达式来替换它们,该表达式列出不在集合中的每个字符,这非常繁琐和冗长,尤其是如果整个字符集是 Unicode。

通过形式语言理论,我们可以看到这样的表达式是什么样子的。对于 GNU grep,答案可能会是这样:

grep "^\([^h]\|h\(h\|eh\|edh\)*\([^eh]\|e[^dh]\|ed[^eh]\)\)*\(\|h\(h\|eh\|edh\)*\(\|e\|ed\)\)$" input

使用Grail找到并进行手动优化。

你还可以使用实现ERE的工具,如egrep,以摆脱反斜杠,或者等效地将-E标志传递给POSIX grep(尽管我认为该问题要求完全避免对grep使用任何标志):

egrep "^([^h]|h(h|eh|edh)*([^eh]|e[^dh]|ed[^eh]))*(|h(h|eh|edh)*(|e|ed))$" input

这是一个用于测试的脚本(请注意,它会在当前目录生成一个名为testinput.txt的文件)。其他答案中提供的一些表达式未通过此测试。
#!/bin/bash
REGEX="^\([^h]\|h\(h\|eh\|edh\)*\([^eh]\|e[^dh]\|ed[^eh]\)\)*\(\|h\(h\|eh\|edh\)*\(\|e\|ed\)\)$"

# First four lines as in OP's testcase.
cat > testinput.txt <<EOF
hoho
hihi
haha
hede

h
he
ah
head
ahead
ahed
aheda
ahede
hhede
hehede
hedhede
hehehehehehedehehe
hedecidedthat
EOF
diff -s -u <(grep -v hede testinput.txt) <(grep "$REGEX" testinput.txt)

在我的系统中,它打印:
Files /dev/fd/63 and /dev/fd/62 are identical

如预期所述。

对于那些对细节感兴趣的人,采用的技术是将匹配单词的正则表达式转换为有限状态自动机,然后通过将每个接受状态更改为非接受状态,反转自动机,然后将结果FA转换回正则表达式。

正如每个人都指出的那样,如果您的正则表达式引擎支持负向先行断言,则正则表达式会简单得多。例如,使用GNU grep:

grep -P '^((?!hede).)*$' input

然而,这种方法的缺点是需要回溯正则表达式引擎。这使得它不适用于使用安全正则表达式引擎(如RE2)的安装,这也是在某些情况下更喜欢生成方法的原因之一。
使用Kendall Hopkins出色的FormalTheory库(用PHP编写),它提供了类似于Grail的功能,以及我自己编写的简化程序,我已经能够编写一个在线负正则表达式生成器,给定输入短语(目前仅支持字母数字和空格字符,并且长度有限):http://www.formauri.es/personal/pgimeno/misc/non-match-regex/ 对于hede,它输出:
^([^h]|h(h|e(h|dh))*([^eh]|e([^dh]|d[^eh])))*(h(h|e(h|dh))*(ed?)?)?$

这与上述内容等价。


2
这是唯一一个试图回答问题的答案。 - questionto42

36
不是正则表达式,但我发现使用串行grep和管道来消除噪音是合乎逻辑且有用的。
例如,搜索一个Apache配置文件时可以排除所有注释-
grep -v '\#' /opt/lampp/etc/httpd.conf      # this gives all the non-comment lines

并且

grep -v '\#' /opt/lampp/etc/httpd.conf |  grep -i dir

串行grep的逻辑是(不是注释)并且(匹配目录)。

2
我认为他正在寻找grep -v的正则表达式版本。 - Angel.King.47
9
这很危险。同时,也会错过像 good_stuff #comment_stuff 这样的行。 - Xavi Montero

31

使用这种方法,您可以避免在每个位置上进行前瞻测试:

/^(?:[^h]+|h++(?!ede))*+$/

等同于(针对 .net):

^(?>(?:[^h]+|h+(?!ede))*)$

旧回答:

/^(?>[^h]+|h+(?!ede))*$/

8
不错的观点,我很惊讶之前没有人提到过这种方法。然而,当应用于不匹配文本时,那个特定的正则表达式容易导致灾难性回溯。以下是我会使用的正则表达式:/^[^h]*(?:h+(?!ede)[^h]*)*$/ - Alan Moore
或者你可以让所有的量词都变成占有格。 ;) - Alan Moore
@Alan Moore - 我也很惊讶。我在下面的答案中发布了相同的模式,只有在看到你的评论(和最佳正则表达式)后才发现它。 - ridgerunner
@ridgerunner,不一定要是最好的。我看过基准测试,排名第一的答案表现更好。(尽管我对此感到惊讶。) - Qtax

30

上述(?:(?!hede).)*是很棒的,因为它可以被锚定。

^(?:(?!hede).)*$               # A line without hede

foo(?:(?!hede).)*bar           # foo followed by bar, without hede between them

但在这种情况下,以下内容就足够了:

^(?!.*hede)                    # A line without hede

这个简化版本已经可以添加“AND”从句:

^(?!.*hede)(?=.*foo)(?=.*bar)   # A line with foo and bar, but without hede
^(?!.*hede)(?=.*foo).*bar       # Same

28

在我看来,更易读的顶部答案变体:

^(?!.*hede)

基本上,如果一行开头没有“hede”,则“匹配该行开头” - 因此该要求几乎可以直接转换为正则表达式。

当然,可能有多个失败要求:

^(?!.*(hede|hodo|hada))

详情: ^锚点确保正则表达式引擎不会在字符串的每个位置重试匹配,否则会匹配每个字符串。

开头的^锚点表示行的开头。grep工具逐行匹配,如果您处理的是多行字符串,则可以使用“m”标志:

/^(?!.*hede)/m # JavaScript syntax
或者
(?m)^(?!.*hede) # Inline flag

与顶部答案的一个区别是,这个不匹配任何东西,并且如果没有“hede”,它匹配整行。 - Bernardo Dal Corno
1
@BernardoDalCorno 这可以通过向表达式添加.*来轻松更改:^(?!.*hede).*,匹配将包含所有文本。 - Falco
这个答案似乎是JavaScript中最有效的,因为所有其他答案在处理非常大的输入时都会遇到“max call stack size exceeded”的问题。这个答案不使用任何分组,只是一个简单的前瞻。 - Falco

24

这是我会怎么做:

^[^h]*(h(?!ede)[^h]*)*$

比其他答案更准确和高效。它实现了弗里德尔的"展开循环"效率技术,需要更少的回溯。


如果搜索词包含两个或更多相同的首字母,例如 hhedehedhe,该怎么办? - Jon Grah

21

另一个选择是添加一个正向先行断言,检查输入行中是否有hede,然后我们将其否定,使用类似以下表达式:

另一种选项是添加正向前瞻并检查输入行中是否存在hede,然后我们会使用类似下面的表达式对其取反:

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

带有单词边界。


该表达式在regex101.com的右上方面板中解释,如果您想要探索/简化/修改它,以及在此链接中,您可以观看它如何匹配一些示例输入,如果您喜欢的话。


正则表达式电路图

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

enter image description here


5
我不理解"内部"正向先行断言有什么用。 - Scratte
4
这是一个伪装的 ^(?!.*\bhede\b).*$ - Wiktor Stribiżew

20

如果你想匹配一个字符以否定一个类似于否定字符类的单词:

例如,给定一个字符串:

<?
$str="aaa        bbb4      aaa     bbb7";
?>

不要使用:

<?
preg_match('/aaa[^bbb]+?bbb7/s', $str, $matches);
?>

使用:

<?
preg_match('/aaa(?:(?!bbb).)+?bbb7/s', $str, $matches);
?>

注意 "(?!bbb)." 既不是顺序断言也不是反向顺序断言,它是当前位置匹配模式,例如:

"(?=abc)abcde", "(?!abc)abcde"

3
Perl正则表达式中没有"lookcurrent"这个概念。真正的负向前瞻使用前缀(?!。正向前瞻的前缀为(?=,而对应的后顾前缀分别为(?<!(?<=。前瞻表示读取下一个字符(因此“向前”),但不会消耗它们。后顾表示检查已经被消耗的字符。 - Didier L
不确定 (?!abc)abcde 有任何意义。 - Scratte

15

这个帖子的发帖者没有指定或标记(编程语言、编辑器、工具)正则表达式将在其中使用的上下文。

对我来说,有时我需要在使用Textpad编辑文件时执行此操作。

Textpad支持一些正则表达式,但不支持前瞻或后顾,因此需要进行一些步骤。

如果我想保留所有不包含字符串hede的行,则可以像这样执行:

1. 搜索/替换整个文件,为包含任何文本的每一行开头添加一个唯一的“标记”。

    Search string:^(.)  
    Replace string:<@#-unique-#@>\1  
    Replace-all  

2. 删除所有包含字符串hede的行(替换字符串为空):

    Search string:<@#-unique-#@>.*hede.*\n  
    Replace string:<nothing>  
    Replace-all  

3. 此时,所有剩余的行都不包含字符串hede。从所有行中移除唯一的“Tag”(替换字符串为空):

    Search string:<@#-unique-#@>
    Replace string:<nothing>  
    Replace-all  

现在您有原始文本,其中包含字符串hede的所有行已被删除。


如果我想要对仅不包含字符串hede的行执行其他操作,则可以按照以下步骤进行:

1. 搜索/替换整个文件,在包含任何文本的每行开头添加一个唯一的“标记”。

    Search string:^(.)  
    Replace string:<@#-unique-#@>\1  
    Replace-all  

2. 对于所有包含字符串hede的行,删除唯一的“Tag”:

    Search string:<@#-unique-#@>(.*hede)
    Replace string:\1  
    Replace-all  

3. 此时,所有以独特的“标签”开头的行,不包含字符串hede。我现在可以对这些行执行其他操作

4. 完成后,我会从所有行中删除唯一的“标签”(替换字符串为空):

    Search string:<@#-unique-#@>
    Replace string:<nothing>  
    Replace-all  

13
自从ruby-2.4.1推出以来,我们可以在Ruby的正则表达式中使用新的缺失运算符
来自官方文档
(?~abc) matches: "", "ab", "aab", "cccc", etc.
It doesn't match: "abc", "aabc", "ccccabc", etc.

因此,在您的情况下,^(?~hede)$ 可以胜任。
2.4.1 :016 > ["hoho", "hihi", "haha", "hede"].select{|s| /^(?~hede)$/.match(s)}
 => ["hoho", "hihi", "haha"]

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