构建正则表达式以查找彼此附近的文本

Building a regular expression to find text near each other

提问人:PracticingPython 提问时间:8/23/2021 最后编辑:mkrieger1PracticingPython 更新时间:8/27/2021 访问量:281

问:

我无法使此搜索正常工作:

import re

word1 = 'this'
word2 = 'that'
sentence = 'this and that'

print(re.search('(?:\b(word1)\b(?: +[^ \n]*){0,5} *\b(word2)\b)|(?:\b(word2)\b(?: +[^ \n]*){0,5} *\b(word1)\b)',sentence))

我需要构建一个正则表达式搜索,以查找一个字符串是否在一定数量的其他单词中具有多达 5 个不同的子字符串(因此两个字符串可能相距 3 个单词,三个字符串总共相距 6 个单词,依此类推)。

我发现了很多类似的问题,例如正则表达式让 3 个单词彼此靠近。如何获取他们的上下文?或者如何在Python中检查两个单词是否相邻?,但它们都没有完全做到这一点。

因此,如果搜索词是“this”、“that”、“these”和“those”,并且它们以任何顺序出现在彼此的 9 个单词以内,则脚本将输出 True。

似乎使用各种不同的正则表达式语句编写一个 if/else 块来适应不同的排列会相当麻烦,所以我希望有一种更有效的方法来在 Python 中对此进行编码。

Python 正则表达 式文本

评论

0赞 tripleee 8/23/2021
使用复杂的正则表达式可能既不比一次超过 9 个单词的滑动窗口中单独检查每个单独的组件正则表达式更快,也更易读。

答:

1赞 Alain T. 8/24/2021 #1

答案已更改,因为我找到了一种仅使用正则表达式即可完成此操作的方法。方法是从前瞻开始,要求所有目标词都出现在接下来的 N 个词中。然后查找目标词的模式(按任何顺序),由 0 个或多个其他词分隔(最多允许的最大中间词)

单词跨度 (N) 是允许所有目标单词处于最大允许距离的最大单词数。

例如,如果我们有 3 个目标词,并且我们最多允许它们之间有 4 个其他词,那么最大词跨度将为 11。所以 3 个目标词加上 2 个中间系列,最多 4 个其他词 3+4+4=11。

搜索模式是通过组合依赖于单词和允许的最大中间单词数的部分而形成的。

模式:\bALL((ANY)(\W+\w+\W*){0,INTER}){COUNT,COUNT}

故障:

  • \b从单词边界开始
  • ALL将被多个前瞻替换,以确保在接下来的 N 个单词中找到每个目标单词。
  • 每个 lookahead 都将具有形式,其中 WORD 是目标词,SPAN 是可能最长的词序列中的其他词的数量。每个目标词都会有一个这样的前瞻。从而确保 N 个单词的序列包含所有目标单词。(?=(\w+\W*){0,SPAN}WORD\b)
  • (\b(ANY)(\W+\w+\W*){0,INTER})匹配任何目标词,后跟 0 到 maxInter 中间词。其中,将被与任何目标词(即用管道分隔的词)匹配的模式替换。并将替换为允许的中间词数。ANYINTER
  • {COUNT,COUNT}确保上述内容的重复次数与目标词的重复次数一样多。这对应于以下模式:targetWord+intermediates+targetWord+intermediates...+targetWord
  • 将“展望”放在重复模式之前,我们可以保证在单词序列中拥有所有目标单词,这些单词恰好包含目标单词的数量,中间单词不会超过允许的数量。

...

import re

words    = {"this","that","other"}
maxInter = 3 # maximum intermediate words between the target words

wordSpan = len(words)+maxInter*(len(words)-1)

anyWord  = "|".join(words)
allWords = "".join(r"(?=(\w+\W*){0,SPAN}WORD\b)".replace("WORD",w) 
                    for w in words)
allWords = allWords.replace("SPAN",str(wordSpan-1))
                    
pattern = r"\bALL(\b(ANY)(\W+\w+\W*){0,INTER}){COUNT,COUNT}"
pattern = pattern.replace("COUNT",str(len(words)))
pattern = pattern.replace("INTER",str(maxInter))
pattern = pattern.replace("ALL",allWords)
pattern = pattern.replace("ANY",anyWord)


textList = [
   "looking for this and that and some other thing", # YES
   "that rod is longer than this other one",         # NO: 4 words apart
   "other than this, I have nothing",                # NO: missing "that"
   "ignore multiple words not before this and that or other", # YES
   "this and that or other, followed by a bunch of words",    # YES
           ] 

输出:

print(pattern)

\b(?=(\w*\b\W+){0,8}this\b)(?=(\w*\b\W+){0,8}other\b)(?=(\w*\b\W+){0,8}that\b)(\b(other|this|that)\b(\w*\b\W+){0,3}){3,3}

for text in textList:
    found = bool(re.search(pattern,text))
    print(found,"\t:",text)

True    : looking for this and that and some other thing
False   : that rod is longer than this other one
False   : other than this, I have nothing
True    : ignore multiple words not before this and that or other
True    : this and that or other, followed by a bunch of words

评论

0赞 PracticingPython 8/26/2021
这太棒了!非常彻底,解释也很好。谢谢!
0赞 sln 8/27/2021
问题不在于找到一个集合中的所有单词是否都包含在接下来的 N 个单词中,而在于找到集合的第一个和最后一个跨越集合的位置。因此,任何像这样的计划或其他任何事情都永远不会奏效。展望是正确的,但仅靠它并不能真正找到集合的跨度。(\b(other|this|that)\b(\w*\b\W+){0,3}){3,3}
0赞 Alain T. 8/27/2021
@sln,您有没有我的解决方案不起作用的情况示例?
0赞 sln 8/27/2021
@AlainT不说行不通,只是凭直觉,数字数与匹配字数是不同的。对于单词范围内的每个单词项,单独进行展望是可以的,但是要匹配它们实际跨越和跨越的位置,需要更多的逻辑,而 Python 引擎无法做到这一点。我已将您的正则表达式修改为在多行模式下工作,以测试您的所有采样行。这里 regex101.com/r/2UhbNy/1。使用具有 PCRE/Perl 和 Python 支持的构造的相同示例行,等效项如下 regex101.com/r/VORo3m/1reimport regex
2赞 sln 8/25/2021 #2

这可以使用支持条件、原子组和捕获组
状态的引擎来完成,如标记、标记或 .其中 null 未定义。
EMPTYNULL

所以这几乎是所有现代引擎。有些是不完整的,比如 JS。
Python 可以使用其替换引擎 import regex 来支持这一点。

基本上,这将支持无序,并且可以限制在总共 4 到 9 个单词的最短
范围内。
底部断言已找到所有必需的项目。
在没有原子组的情况下使用它可能会导致回溯问题,但由于它
在那里,这个正则表达式非常快。
(?= \1 \2 \3 \4 )

更新:添加了 LookAhead,因此它开始匹配一个特殊的单词。(?= this | that | these | those )

Python 代码

>>> import regex
>>>
>>> targ = 'this  sdgbsesfrgnh these meat ball those  nhwsgfr that sfdng  sfgnsefn sfgnndfsng'
>>> pat = r'(?=this|that|these|those)(?>\s*(?:(?(1)(?!))\bthis\b()|(?(2)(?!))\bthat\b()|(?(3)(?!))\bthese\b()|(?(4)(?!))\bthose\b()|(?(5)(?!))\b(.+?)\b|(?(6)(?!))\b(.+?)\b|(?(7)(?!))\b(.+?)\b|(?(8)(?!))\b(.+?)\b|(?(9)(?!))\b(.+?)\b)\s*){4,9
}?(?=\1\2\3\4)'
>>>
>>> regex.search(pat, targ).group()
'this  sdgbsesfrgnh these meat ball those  nhwsgfr that '

General PCRE / Perl et all (same regex)

(?=this|that|these|those)(?>\s*(?:(?(1)(?!))\bthis\b()|(?(2)(?!))\bthat\b()|(?(3)(?!))\bthese\b()|(?(4)(?!))\bthose\b()|(?(5)(?!))\b(.+?)\b|(?(6)(?!))\b(.+?)\b|(?(7)(?!))\b(.+?)\b|(?(8)(?!))\b(.+?)\b|(?(9)(?!))\b(.+?)\b)\s*){4,9}?(?=\1\2\3\4)

https://regex101.com/r/zhSa64/1

 (?= this | that | these | those )
 (?>
    \s* 
    (?:
       (?(1)(?!))
       \b this \b ( )                # (1)
     |
       (?(2)(?!))
       \b that \b ( )                # (2)
     | 
       (?(3)(?!))
       \b these \b ( )               # (3)
     | 
       (?(4)(?!))
       \b those \b ( )               # (4)
     |
       (?(5)(?!))
       \b ( .+? ) \b                 # (5)
     |
       (?(6)(?!))
       \b ( .+? ) \b                 # (6)
     |
       (?(7)(?!))
       \b ( .+? ) \b                 # (7)
     |
       (?(8)(?!))
       \b ( .+? ) \b                 # (8)
     |
       (?(9)(?!))
       \b ( .+? ) \b                 # (9)
    )
    \s* 
 ){4,9}?
 (?= \1 \2 \3 \4 )