【问题标题】:Python regular expressions with overlap具有重叠的 Python 正则表达式
【发布时间】:2022-01-06 18:18:10
【问题描述】:

给定一个字符串,如:

text = 'jack betty "donald jake rita" lorie katie danny'

还有一个关键字列表,例如:

keywords = ["jack", "donald", "rita", "katie"]

如何只返回在双引号中找到的关键字?

预期回报:

["donald", "rita"]

我做到了:

import re
import string

keywords = ["jack", "donald", "rita", "katie"]
text = 'jack betty "donald jake rita" lorie katie danny'

pattern_template = string.Template(r'(?:^[^"]*["][^"]*)($keywords)')
search_pattern = pattern_template.safe_substitute(keywords="|".join(keywords))

# This gives me a search_pattern of: (?:^[^"]*["][^"]*)(jack|donald|rita|katie)

matches = [match.group(1) for match in re.finditer(search_pattern, text)]

print(matches)

我的搜索模式从字符串的开头开始,跳过所有非引号字符,匹配一个引号,跳过 0 个或多个额外的非引号字符,然后查找其中一个关键字。

这仅返回:

['rita']

如果我从关键字中去掉“rita”,它只会返回:

['donald']

上面,我只使用match.group(1) 检查第一个捕获组,但如果我检查.groups() 以获取所有捕获组的元组,我仍然只能看到[('rita',)]

我认为问题在于重叠匹配。 有没有办法返回所有重叠的匹配项?

【问题讨论】:

  • 分两步完成:首先获取双引号内的字符串,然后搜索关键字。
  • 引号之间是否总是只有一个子字符串?此外,如果您对匹配 whole 单词不感兴趣,则不需要正则表达式。

标签: python regex


【解决方案1】:

这里有两种方法可以一次性完成。

匹配你不想要的,捕捉你想要的

使用这种方法,通过匹配以下正则表达式,我们可以获得捕获组 1 中感兴趣的单词,而不注意不会导致捕获的匹配项。

(?:^|\").*?(?:$|\")|(\b(?:jake|donald|rita|katie)\b)

这个表达式当然必须通过数组keywords以编程方式构造出来,这并不难。

Demo

这利用了格式良好的字符串包含偶数个双引号这一事实。

字符串可以分解如下。

(?:^|\")  # match the beginning of the string or a double-quote
.*?       # match zero or more characters lazily
(?:$|\")  # match the end of the string or a double-quote
|         # or
(         # begin capture group 1
  \b      # match word boundary
  (?:jake|donald|rita|katie) # match one of the four words 
  \b      # match word boundary
)         # end capture group 1

单词边界是为了避免匹配单词,例如'heritage'(其中包含一个可爱的仪表女仆的名字)。


计算尾随双引号以确定单词是否在带引号的字符串中

观察一个单词在双引号字符串中当且仅当该单词后面的字符串部分包含奇数个双引号。例如,假设字符串是:

'jack betty "donald jake rita" lorie katie danny "katie"'

             ^^^^^^ ^^^^ ^^^^                     ^^^^^

"donald"、"jake" 和 "rita" 后跟三个双引号,因此我们推断它们在双引号字符串中。 "katie" 相同,后跟一个双引号。所有剩余的单词后跟偶数个双引号,因此我们推断它们不在双引号字符串中。当然,这是假设整个字符串格式正确,即包含偶数个双引号。

您可以使用re.findall 和以下正则表达式来识别感兴趣的单词。

\b(?:jake|donald|rita|katie)\b(?=[^\"]*(?:(?:\"[^\"]*){2})*\"[^\"]*$)

Demo.

正则表达式可以分解如下。

\b             # match a word boundary
(?:jake|donald|rita|katie) # match one of four words in a non-capture group
\b             # match a word boundary
(?=            # begin a positive lookhead 
  [^\"]*       # match zero or more chars other than dq's (double-quotes)
  (?:          # begin a non-capture group                     
    (?:        # begin a non-capture group
      \"[^\"]* # match a dq followed by zero or more chars other than a dq
    ){2}       # end non-capture group and execute twice
  )*           # end non-capture group and execute zero or more times
  \"[^\"]*     # match a dq followed by zero or more chars other than a dq 
  $            # match end of string
)              # end positive lookahead

使用此表达式可能需要正则表达式引擎进行大量回溯。如链接所示,正则表达式引擎需要 261 个步骤来解析上面给出的字符串。

【讨论】:

  • 感谢您抽出宝贵的时间来写这篇文章。我正在努力遵循您的第一个示例的逻辑。我可以看到它在演示网站上有效,但我不知道如何。我知道你说过我们忽略了比赛本身,但显然它们很重要。为什么我们要匹配字符串的开头,以及引用文本之外的所有内容?为什么有(?:$|\") | (\b(?:jake|...)\b),而不仅仅是(\b(?:jake|...)\b)?为什么捕获组中有非捕获组?我以为我对正则表达式的基础知识掌握得很好,但也许我没有。
  • 将正则表达式视为A1|A2。考虑我上面的示例字符串。 'jack betty "' 匹配 A1,因此将正则表达式引擎的内部字符串指针移动到 'donald' 之前的右侧。 'donald' 与 A1 不匹配,因此我们尝试匹配 A2 并找到匹配项并将 'donald' 保存到捕获组 1。字符串指针移动到 'donald''d' 和以下空格之间. A1 或 A2 不匹配,因此指针向前移动一个字符。没有匹配 A1 但 A2 匹配 'jake' 被捕获...
  • ...如果示例是 'rufus' 而不是 'jake' 则不会匹配,并且字符串指针将一次向前移动一个字符,A1 和 A2 都不匹配,直到它就在 'rita' 之前,它将匹配 A2 并被捕获。然后字符串指针将位于双引号之前,因此 A1 将匹配'" lorie katie danny "'。然后'katie' 将被 A2 匹配并被捕获。最后,最后一个双引号将与 A1 匹配。通过限制对捕获的考虑,我们获得了预期的结果。明白了吗?
  • 捕获组内不需要有非捕获组;它比(\bjake\b|\bdonald\b|\brita\b|\bkatie\b) 更简洁。顺便说一句,我的意思是忽略不会导致捕获的匹配项。我已经编辑了我的答案以澄清这一点。
【解决方案2】:

注意:我不知道 python 但这是我的尝试

代码

import re
import string

keywords = ["jack", "donald", "rita", "katie"]
text = 'jack betty "donald jake rita" lorie katie danny'

# keep only the keywords in quotes
result = re.sub('[^"]*"([^"]+)"[^"]*', r"\1 ", text)

# split the unquoted word list into separate keywords
result = re.split(r"\s+", result, 0)

# Get intersection of two keyword lists
result = list(set(keywords) & set(result))

print(result)

输出

['donald', 'rita']

【讨论】:

    【解决方案3】:

    假设双引号对整个文本进行配对,则可以获取捕获组中双引号之间的所有匹配项。

    然后在 1 个或多个空白字符上拆分匹配,并获得 2 个集合的交集。

    该模式还匹配双引号之间的转义双引号。

    import re
    
    pattern = r'"([^"\\]*(\\.[^"\\]*)*)"'
    text = 'jack betty "donald jake rita" lorie katie danny, "donald and brad and katie"'
    keywords = ["jack", "donald", "rita", "katie"]
    
    for matchNum, match in enumerate(re.finditer(pattern, text), start=1):
        print(list(set(keywords) & set(re.split(r"\s+", match.group(1))))) 
    

    输出

    ['donald', 'rita']
    ['katie', 'donald']
    

    查看Python demoregex demo

    【讨论】:

    • @CarySwoveland 谢谢! (再次:-))
    【解决方案4】:

    如果每个匹配项都必须从字符串的特定索引开始(在您的情况下是开始 ^),则您无法获得多个匹配项,因为每次都会找到相同的匹配项。只能从上一场比赛结束后开始的位置找到后续比赛。

    这是一种使用正向预测的方法,它是一个零宽度断言,不会消耗它匹配的字符,因此下一个匹配不必从匹配的末尾开始:

    import re
    import string
    
    keywords = ["jack", "donald", "rita", "katie"]
    text = 'jack betty "donald jake rita" lorie katie danny'
    
    pattern_template = string.Template(r'\b($keywords)\b(?=[^"]*"[^"]*$)')
    
    search_pattern = pattern_template.safe_substitute(keywords="|".join(keywords))
    
    matches = [match.group(1) for match in re.finditer(search_pattern, text)]
    
    print(search_pattern)
    print()
    print(matches)
    

    如果文本可以有多个引用部分,则可以使用替代的正向前瞻性,例如

    (?=(?:[^"]*"[^"]*")*[^"]*"[^"]*$)
    

    可能已经足够好了。它断言字符串中前面有奇数个"

    我们必须向前看,因为 Python 不允许可变长度的look-behinds。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-08-11
      • 1970-01-01
      • 2017-11-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多