关于regex:与不包含单词的行匹配的正则表达式

Regular expression to match a line that doesn't contain a word

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

输入:

1
2
3
4
hoho
hihi
haha
hede

代码:

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

期望输出:

1
2
3
hoho
hihi
haha


regex不支持反向匹配的观点并不完全正确。您可以使用负面环顾来模仿这种行为:

1
^((?!hede).)*$

上面的regex将匹配不包含(子)字符串"hede"的任何字符串或不带换行符的行。如前所述,这不是regex"擅长"(或应该做)的东西,但仍然有可能。

如果还需要匹配换行符字符,请使用dot-all修饰符(下面的模式中的尾随s)。

1
/^((?!hede).)*$/s

或者直接使用:

1
/(?s)^((?!hede).)*$/

(其中/.../是regex定界符,即不是模式的一部分)

如果dot-all修饰符不可用,则可以模拟与字符类[\s\S]相同的行为:

1
/^((?!hede)[\s\S])*$/

解释

字符串只是n字符的列表。每个字符前后都有一个空字符串。因此,一个n字符列表将有n+1空字符串。考虑字符串"ABhedeCD"

1
2
3
4
5
    ┌──┬───┬──┬───┬──┬───┬──┬───┬──┬───┬──┬───┬──┬───┬──┬───┬──┐
S = │e1│ A │e2│ B │e3│ h │e4│ e │e5│ d │e6│ e │e7│ C │e8│ D │e9│
    └──┴───┴──┴───┴──┴───┴──┴───┴──┴───┴──┴───┴──┴───┴──┴───┴──┘

index    0      1      2      3      4      5      6      7

其中e是空字符串。regex (?!hede).向前看,看是否没有子字符串"hede",如果是这种情况(因此可以看到其他情况),那么.将匹配除换行符以外的任何字符。环视也称为零宽度断言,因为它们不使用任何字符。它们只断言/验证某些内容。

因此,在我的示例中,首先验证每个空字符串,看前面是否没有"hede",然后.(点)使用字符。regex (?!hede).只做一次,所以它被包装成一组,重复零次或更多次:((?!hede).)*。最后,对输入的开始和结束进行锚定,以确保消耗整个输入:^((?!hede).)*$

如您所见,输入"ABhedeCD"将失败,因为在e3上,regex (?!hede)失败(前面有"hede").


请注意,解决方案不是从"hede"开始的:

1
^(?!hede).*$

通常比不包含"hede"的解决方案更有效:

1
^((?!hede).)*$

前者只在输入字符串的第一个位置检查"hede",而不是在每个位置检查。


如果您只是将它用于grep,那么可以使用grep -v hede获取不包含hede的所有行。

eta哦,重读这个问题,grep -v可能就是你所说的"工具选项"。


答:

1
^((?!hede).)*$

说明:

^字符串的开头,(组并捕获到1(0次或更多次(尽可能匹配最大数量)),(?!展望未来,看看是否有,

你的绳子,

)展望结束,.任何字符,除了,)*结束于1(注意:因为您在这个捕获上使用了一个量词,所以只有最后一个被捕获模式的重复将存储在1中)$,在可选的之前,以及字符串的结尾


给出的答案非常好,只是一个学术观点:

理论计算机科学意义上的正则表达式不能这样做。对他们来说,它必须看起来像这样:

1
^([^h].*$)|(h([^e].*$|$))|(he([^h].*$|$))|(heh([^e].*$|$))|(hehe.+$)

这只是完全匹配。在次比赛中这样做会更尴尬。


如果希望regex测试仅在整个字符串匹配时失败,则以下操作将起作用:

1
^(?!hede$).*

例如——如果您想允许除"foo"以外的所有值(即"foo foo"、"barfoo"和"foobar"将通过,但"foo"将失败),请使用:^(?!foo$).*

当然,如果您要检查是否完全相等,在这种情况下更好的通用解决方案是检查字符串是否相等,即。

1
myStr !== 'foo'

如果需要任何regex特性(这里是case-insensitive和range-matching),甚至可以将否定项放在测试之外:

1
!/^[a-f]oo$/i.test(myStr)

然而,在需要进行积极的regex测试的情况下(可能是由API),这个答案顶部的regex解决方案可能会有所帮助。


fwiw,由于正则语言(又称Rational语言)是在互补的情况下关闭的,所以总是可以找到一个否定另一个表达式的正则表达式(又称Rational表达式)。但实现这一点的工具并不多。

VCSN支持此运算符(表示{c},postfix)。

首先定义表达式的类型:标签是从az中选择的字母(lal_char),例如(在使用补码时定义字母表当然非常重要),为每个单词计算的"值"只是一个布尔值:true这个词被接受,false被拒绝。

在Python中:

1
2
3
4
In [5]: import vcsn
        c = vcsn.context('lal_char(a-z), b')
        c
Out[5]: {a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z} → ??

然后输入表达式:

1
2
In [6]: e = c.expression('(hede){c}'); e
Out[6]: (hede)^c

将此表达式转换为自动机:

1
In [7]: a = e.automaton(); a

The corresponding automaton

最后,将这个自动机转换回一个简单的表达式。

1
2
In [8]: print(a.expression())
        \e+h(\e+e(\e+d))+([^h]+h([^e]+e([^d]+d([^e]+e[^]))))[^]*

其中+通常表示|\e表示空字,[^]通常表示.或(任何字符)。所以,稍微重写一下()|h(ed?)?|([^h]|h([^e]|e([^d]|d([^e]|e.)))).*

您可以在这里看到这个例子,并在那里在线尝试VCSN。


这里有一个很好的解释为什么不容易否定一个任意的正则表达式。不过,我必须同意其他答案:如果这不是一个假设性问题,那么regex在这里不是正确的选择。


基准点

我决定评估一些呈现的选项,比较它们的性能,并使用一些新特性。.NET Regex引擎的基准测试:http://regexhero.net/tester/

基准文本:

前7行不应该匹配,因为它们包含搜索表达式,而下面7行应该匹配!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Regex Hero is a real-time online Silverlight Regular Expression Tester.
XRegex Hero is a real-time online Silverlight Regular Expression Tester.
Regex HeroRegex HeroRegex HeroRegex HeroRegex Hero is a real-time online Silverlight Regular Expression Tester.
Regex Her Regex Her Regex Her Regex Her Regex Her Regex Her Regex Hero is a real-time online Silverlight Regular Expression Tester.
Regex Her is a real-time online Silverlight Regular Expression Tester.Regex Hero
egex Hero egex Hero egex Hero egex Hero egex Hero egex Hero Regex Hero is a real-time online Silverlight Regular Expression Tester.
RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRegex Hero is a real-time online Silverlight Regular Expression Tester.

Regex Her
egex Hero
egex Hero is a real-time online Silverlight Regular Expression Tester.
Regex Her is a real-time online Silverlight Regular Expression Tester.
Regex Her Regex Her Regex Her Regex Her Regex Her Regex Her is a real-time online Silverlight Regular Expression Tester.
Nobody is a real-time online Silverlight Regular Expression Tester.
Regex Her o egex Hero Regex  Hero Reg ex Hero is a real-time online Silverlight Regular Expression Tester.

结果:

结果是每秒迭代3次运行的中位数-较大的数字=较好

1
2
3
4
5
6
7
8
01: ^((?!Regex Hero).)*$                    3.914   // Accepted Answer
02: ^(?:(?!Regex Hero).)*$                  5.034   // With Non-Capturing group
03: ^(?>[^R]+|R(?!egex Hero))*$             6.137   // Lookahead only on the right first letter
04: ^(?>(?:.*?Regex Hero)?)^.*$             7.426   // Match the word and check if you're still at linestart
05: ^(?(?=.*?Regex Hero)(?#fail)|.*)$       7.371   // Logic Branch: Find Regex Hero? match nothing, else anything

P1: ^(?(?=.*?Regex Hero)(*FAIL)|(*ACCEPT))  ?????   // Logic Branch in Perl - Quick FAIL
P2: .*?Regex Hero(*COMMIT)(*FAIL)|(*ACCEPT) ?????   // Direct COMMIT & FAIL in Perl

因为.NET不支持动作动词(*fail等),所以我无法测试解决方案p1和p2。

总结:

我尝试测试大多数建议的解决方案,一些优化对于某些词是可能的。例如,如果搜索字符串的前两个字母不相同,则可以将答案03扩展为^(?>[^R]+|R+(?!egex Hero))*$导致较小的性能增益。

但从总体上看,最具可读性和性能方面最快的解决方案似乎是05使用条件语句或04与可能的量词。我认为Perl解决方案应该更快、更容易阅读。


对于负的lookahead,正则表达式可以匹配不包含特定模式的内容。这是巴特·基尔斯的回答和解释。很好的解释!

然而,有了巴特·基尔斯的答案,lookahead部分将提前测试1到4个字符,同时匹配任何单个字符。我们可以避免这种情况,让先行部分检查整个文本,确保没有"hede",然后正常部分(.%)可以一次吃掉整个文本。

下面是改进的regex:

1
/^(?!.*?hede).*$/

注意(*?)负向前部分中的惰性量词是可选的,您可以使用(*)贪婪量词代替,这取决于您的数据:如果"hede"确实存在,并且在文本的前半部分中,惰性量词可以更快;否则,贪婪量词会更快。但是,如果"hede"不存在,两个都会变慢。

这是演示代码。

有关lookahead的更多信息,请阅读这篇伟大的文章:掌握lookahead和lookback。

另外,请查看regexgen.js,它是一个有助于构造复杂正则表达式的javascript正则表达式生成器。使用regexgen.js,可以以更易读的方式构造regex:

1
2
3
4
5
6
7
8
9
10
var _ = regexGen;

var regex = _(
    _.startOfLine(),            
    _.anything().notContains(       // match anything that not contains:
        _.anything().lazy(), 'hede' //   zero or more chars that followed by 'hede',
                                    //   i.e., anything contains 'hede'
    ),
    _.endOfLine()
);


不是regex,但我发现使用带有管道的串行greps来消除噪音是合乎逻辑和有用的。

例如,搜索一个apache配置文件而不搜索所有注释-

1
grep -v '\#' /opt/lampp/etc/httpd.conf      # this gives all the non-comment lines

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

串行grep的逻辑是(不是注释)和(匹配dir)


这样,您就可以避免对每个位置进行前瞻性测试:

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

等价于(对于.NET):

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

老回答:

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


前面提到的(?:(?!hede).)*非常好,因为它可以锚定。

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

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

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

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

这种简化可以添加"和"条款:

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

我会这样做:

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

比其他答案更准确、更有效。它实现了Friedl的"展开循环"效率技术,并且需要更少的回溯。


如果要匹配一个字符来否定一个类似于negate character类的单词:

例如,字符串:

1
2
3
<?
$str="aaa        bbb4      aaa     bbb7";
?>

不要使用:

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

用途:

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

注意,"(?!bbb)."既不是lookback也不是lookahead,它是lookcurrent,例如:

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


操作没有指定或tag post来指示regex将在其中使用的上下文(编程语言、编辑器、工具)。

对我来说,有时在使用Textpad编辑文件时需要这样做。

Textpad支持一些regex,但不支持lookahead或lookbehind,因此需要采取一些步骤。

如果我希望保留不包含字符串hede的所有行,我将这样做:

1. Search/replace the entire file to add a unique"Tag" to the beginning of each line containing any text.

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

2. Delete all lines that contain the string hede (replacement string is empty):

1
2
3
4
    Search string:<@#-unique-#@>.*hede.*
 
    Replace string:<nothing>  
    Replace-all

3. At this point, all remaining lines Do NOT contain the string hede. Remove the unique"Tag" from all lines (replacement string is empty):

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

现在已经删除了包含字符串hede的所有行的原始文本。

如果我只想对不包含字符串hede的行执行其他操作,我会这样做:

1. Search/replace the entire file to add a unique"Tag" to the beginning of each line containing any text.

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

2. For all lines that contain the string hede, remove the unique"Tag":

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

3. At this point, all lines that begin with the unique"Tag", Do NOT contain the string hede. I can now do my Something Else to only those lines.

4. When I am done, I remove the unique"Tag" from all lines (replacement string is empty):

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


由于ruby-2.4.1的引入,我们可以在ruby的正则表达式中使用新的不存在的操作符。

从官方文件

1
2
(?~abc) matches:"","ab","aab","cccc", etc.
It doesn't match:"abc","aabc","ccccabc", etc.

因此,在您的情况下,^(?~hede)$为您做这项工作

1
2
2.4.1 :016 > ["hoho","hihi","haha","hede"].select{|s| /^(?~hede)$/.match(s)}
 => ["hoho","hihi","haha"]

既然没有人直接回答所问的问题,我就去做。

答案是,有了posix grep,就不可能真正满足这个要求:

1
grep"Regex for doesn't contain hede" Input

原因是posix grep只需要与基本的正则表达式一起工作,这些表达式的功能不足以完成该任务(由于缺乏交替和分组,它们无法解析正则语言)。

然而,gnu grep实现了允许它的扩展。具体来说,\|是GNU实施BRES的交替运算符,\(\)是分组运算符。如果正则表达式引擎支持交替、负括号表达式、分组和Kleene星,并且能够锚定到字符串的开头和结尾,那么这就是这种方法所需要的全部内容。

对于GNU grep,它将类似于:

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

(在Grail和一些手工进行的进一步优化中发现)。

您还可以使用实现扩展正则表达式的工具,如egrep来消除反斜杠:

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

下面是一个测试它的脚本(注意它在当前目录中生成一个文件testinput.txt):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/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)

在我的系统中,它打印:

1
Files /dev/fd/63 and /dev/fd/62 are identical

果不其然。

对于那些对细节感兴趣的人,所采用的技术是将匹配单词的正则表达式转换为有限自动机,然后通过将每个接受状态更改为不接受(反之亦然)来反转自动机,然后将生成的fa转换回正则表达式。

最后,正如所有人所指出的,如果正则表达式引擎支持负向前看,那么可以大大简化任务。例如,使用gnu grep:

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

更新:我最近发现了用PHP编写的Kendall Hopkins优秀的形式库,它提供了类似于Grail的功能。通过使用它和我自己编写的一个简化程序,我可以编写一个给定输入短语的负正则表达式的在线生成器(目前只支持字母数字和空格字符):http://www.formuri.es/personal/pgimeno/misc/non-match-regex/

对于hede输出:

1
^([^h]|h(h|e(h|dh))*([^eh]|e([^dh]|d[^eh])))*(h(h|e(h|dh))*(ed?)?)?$

相当于上述。


通过pcre动词(*SKIP)(*F)

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

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

演示

零件的执行:

让我们把上面的正则表达式分成两部分来考虑。

  • |符号前的部分。零件不应匹配。

    1
    ^hede$(*SKIP)(*F)
  • |符号后的部分。零件应匹配。

    1
    ^.*$
  • 第1部分

    regex引擎将从第一部分开始执行。

    1
    ^hede$(*SKIP)(*F)

    说明:

    • ^断言我们已经开始了。
    • hede与字符串hede匹配
    • $断言我们在生产线上。

    因此,包含字符串hede的行将匹配。一旦regex引擎看到下面的(*SKIP)(*F)(注意:你可以把(*F)写成(*FAIL)动词,它就会跳过并使匹配失败。|被称为涂改或逻辑或运算符,加在pcre动词旁边,该动词与所有行上的每个字符之间的所有边界都匹配,除了行包含精确的字符串hede。请看这里的演示。也就是说,它尝试匹配剩余字符串中的字符。现在将执行第二部分中的regex。

    第2部分

    1
    ^.*$

    说明:

    • ^断言我们已经开始了。也就是说,它匹配除hede行中的行外的所有行开始。请看这里的演示。
    • .*在多行模式下,.将匹配除换行符或回车符以外的任何字符。而*将重复前面的字符零次或更多次。因此,.*将与整条线路匹配。请看这里的演示。

      嘿,你为什么加上.*而不是.+?

      因为.*与空行匹配,而.+与空行不匹配。我们要匹配除hede以外的所有行,输入中可能也有空白行。所以你必须使用.*,而不是.+.+将重复前面的字符一次或多次。见.*匹配一个空行。

    • 此处不需要$端锚点。


    代码中的两个regex可能更易于维护,一个用于进行第一次匹配,如果匹配,则运行第二个regex以检查希望阻止的异常情况,例如^.*(hede).*,然后在代码中具有适当的逻辑。

    好吧,我承认这不是一个真正的答案张贴的问题,它也可能使用比一个单一的regex稍微多一点的处理。但是对于来这里为异常情况寻找快速紧急解决方案的开发人员来说,这个解决方案不应该被忽视。


    txr语言支持regex否定。

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

    一个更复杂的例子:匹配所有以a开头,以z结尾,但不包含子字符串hede的行:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    $ 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

    regex否定本身并不是特别有用,但是当你也有交集时,事情会变得有趣,因为你有一套完整的布尔集操作:你可以表示"匹配这个的集合,除了匹配那个的东西"。


    在我看来,最上面的答案有一个更易读的变体:

    1
    ^(?!.*hede)

    基本上,"如果并且仅当行的开头没有‘hede’时才匹配"——所以需求几乎直接转换为regex。

    当然,可能有多种故障要求:

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

    详细信息:^锚确保regex引擎不会在字符串中的每个位置重试匹配,这些位置将匹配每个字符串。

    开头的^锚表示行的开头。grep工具一次匹配每一行,在使用多行字符串的上下文中,可以使用"m"标志:

    1
    /^(?!.*hede)/m # JavaScript syntax

    1
    (?m)^(?!.*hede) # Inline flag

    下面的函数将帮助您获得所需的输出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <?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;
        }


    ?>

    如何使用PCRE的回溯控制动词匹配不包含单词的行

    以下是我以前从未使用过的方法:

    1
    /.*hede(*COMMIT)^|/

    它是如何工作的

    首先,它试图在一行中的某个地方找到"hede"。如果成功,此时,(*COMMIT)通知发动机,不仅要在发生故障时回溯,而且在这种情况下不要尝试任何进一步的匹配。然后,我们尝试匹配一些不可能匹配的东西(在本例中,是^)。

    如果一行不包含"hede",则第二个备选方案(空子模式)成功匹配主题字符串。

    这个方法并不比负向前看更有效,但我想我还是把它放在这里,以防有人发现它很漂亮,并将其用于其他更有趣的应用程序。


    也许你可以在谷歌上找到它,同时尝试编写一个regex,它能够匹配不包含子字符串的行的段(而不是整行)。让我想一想,我会分享:

    给定字符串:
    barfoobaz

    我想匹配不包含子字符串"bad"的标记。

    /将与匹配。

    请注意,有两组(层)圆括号:

    • 最里面的一个用于负向前看(它不是一个捕获组)
    • Ruby将最外层解释为捕获组,但我们不希望它是捕获组,所以我补充道?:在开始时,它不再被解释为捕获组。

    露比演示:

    1
    2
    3
    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">"]

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

    您的if语句将需要匹配"contains"而不匹配"excludes"。

    1
    2
    3
    4
    var contains = /abc/;
    var excludes =/hede/;

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

    我相信regex的设计者预期会使用not操作符。


    ^(?)!hede)。)*$是一个很好的解决方案,除非它使用字符,否则您将无法将其与其他条件组合在一起。例如,假设您想检查"hede"是否不存在以及"haha"是否存在。此解决方案会起作用,因为它不会消耗字符:

    ^?!\bHeD)(?= bHaa)


    使用conyedit,可以使用命令行cc.gl !/hede/获取不包含regex匹配的行,或者使用命令行cc.dl /hede/删除包含regex匹配的行。他们有同样的结果。