我知道可以使用其他工具(例如 grep -v
)来匹配一个词并反转匹配结果。然而,是否可能使用正则表达式来匹配不包含特定单词(例如 hede
)的行?
输入:
hoho
hihi
haha
hede
代码:
grep "<Regex for 'doesn't contain hede'>" input
期望的输出:
hoho
hihi
haha
我知道可以使用其他工具(例如 grep -v
)来匹配一个词并反转匹配结果。然而,是否可能使用正则表达式来匹配不包含特定单词(例如 hede
)的行?
hoho
hihi
haha
hede
grep "<Regex for 'doesn't contain hede'>" input
hoho
hihi
haha
既然没有其他人直接回答了问题(提问者提出的问题),那我来回答吧。
答案是,在 POSIX grep
中,无法实现文字上的满足这个请求:
grep "<Regex for 'doesn't contain hede'>" input
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
你还可以使用实现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
hede
,它输出:^([^h]|h(h|e(h|dh))*([^eh]|e([^dh]|d[^eh])))*(h(h|e(h|dh))*(ed?)?)?$
这与上述内容等价。
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 -v
的正则表达式版本。 - Angel.King.47good_stuff #comment_stuff
这样的行。 - Xavi Montero使用这种方法,您可以避免在每个位置上进行前瞻测试:
/^(?:[^h]+|h++(?!ede))*+$/
等同于(针对 .net):
^(?>(?:[^h]+|h+(?!ede))*)$
旧回答:
/^(?>[^h]+|h+(?!ede))*$/
/^[^h]*(?:h+(?!ede)[^h]*)*$/
。 - Alan Moore上述(?:(?!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
在我看来,更易读的顶部答案变体:
^(?!.*hede)
基本上,如果一行开头没有“hede”,则“匹配该行开头” - 因此该要求几乎可以直接转换为正则表达式。
当然,可能有多个失败要求:
^(?!.*(hede|hodo|hada))
详情: ^锚点确保正则表达式引擎不会在字符串的每个位置重试匹配,否则会匹配每个字符串。
开头的^锚点表示行的开头。grep工具逐行匹配,如果您处理的是多行字符串,则可以使用“m”标志:
/^(?!.*hede)/m # JavaScript syntax
或者(?m)^(?!.*hede) # Inline flag
.*
来轻松更改:^(?!.*hede).*
,匹配将包含所有文本。 - Falco这是我会怎么做:
^[^h]*(h(?!ede)[^h]*)*$
比其他答案更准确和高效。它实现了弗里德尔的"展开循环"效率技术,需要更少的回溯。
hhede
或 hedhe
,该怎么办? - Jon Grah另一个选择是添加一个正向先行断言,检查输入行中是否有hede
,然后我们将其否定,使用类似以下表达式:
另一种选项是添加正向前瞻并检查输入行中是否存在hede
,然后我们会使用类似下面的表达式对其取反:
^(?!(?=.*\bhede\b)).*$
带有单词边界。
该表达式在regex101.com的右上方面板中解释,如果您想要探索/简化/修改它,以及在此链接中,您可以观看它如何匹配一些示例输入,如果您喜欢的话。
jex.im可视化正则表达式:
^(?!.*\bhede\b).*$
。 - Wiktor Stribiżew如果你想匹配一个字符以否定一个类似于否定字符类的单词:
例如,给定一个字符串:
<?
$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"
(?!
。正向前瞻的前缀为(?=
,而对应的后顾前缀分别为(?<!
和(?<=
。前瞻表示读取下一个字符(因此“向前”),但不会消耗它们。后顾表示检查已经被消耗的字符。 - Didier L(?!abc)abcde
有任何意义。 - Scratte这个帖子的发帖者没有指定或标记(编程语言、编辑器、工具)正则表达式将在其中使用的上下文。
对我来说,有时我需要在使用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
([^h]*(h([^e]|$)|he([^d]|$)|hed([^e]|$)))*
有什么问题吗?它的思想很简单。继续匹配直到看到不想要的字符串的开头,然后只在字符串未完成的N-1种情况下进行匹配(其中N为字符串的长度)。这N-1种情况是“h之后非e”,“he之后非d”和“hed之后非e”。如果你成功通过了这些N-1种情况,那么你就成功地没有匹配上不想要的字符串,所以你可以开始再次寻找[^h]*
。 - stevendesu^([^h]*(h([^e]|$)|he([^d]|$)|hed([^e]|$))?)*$
这个模式在 "hhede" 等包含部分"hede"的实例之前出现时失败了。 - jaytea