【问题标题】:Multiline regex matching多行正则表达式匹配
【发布时间】:2013-04-13 19:01:51
【问题描述】:

我有一个如下所示的文件:

useless stuff

fruit: apple
fruit: banana

useless stuff

fruit: kiwi
fruit: orange
fruit: pear

useless stuff

这个想法是按照它们出现的顺序和分组来捕获所有水果名称。对于上面的例子,输出必须是这样的:

[['apple', 'banana'], ['kiwi', 'orange', 'pear']]

我通过遍历多行正则表达式 '^fruit: (.+)$' 的所有匹配项来成功地做到这一点,如果发现水果名称的行似乎彼此跟随,则将水果名称添加到同一个给定列表中。

但是,这对于在水果名称上进行替换是不切实际的(跟踪匹配开始和结束索引成为强制性的),所以我更愿意在单个正则表达式中执行此操作。

我试过了:

re.findall(r'(?:^fruit: (.+)$\n)+', thetext, re.M)

但它只返回一行。

我哪里错了?

【问题讨论】:

  • 你绝对需要使用正则表达式吗?
  • @jamylak:我认为如果没有正则表达式,这将变得非常痛苦,匹配模式在现实世界中非常复杂。
  • 它找到的一行是什么?

标签: python regex


【解决方案1】:

您不能在正则表达式中以这种方式进行“分组”,因为通常一个组只捕获其最新匹配项。一种解决方法是逐字重复一个组:

matches = re.findall(r'(?m)(?:^fruit: (.+)\n)(?:^fruit: (.+)\n)?(?:^fruit: (.+)\n)?', text)
# [('apple', 'banana', ''), ('kiwi', 'orange', 'pear')]

如果这适合您的任务(例如,不超过 5-6 个组),您可以轻松地即时生成此类表达式。如果没有,唯一的选择是两次匹配(我想这与您已经拥有的类似):

matches = [re.findall(': (.+)', x) 
    for x in re.findall(r'(?m)((?:^fruit: .+\n)+)', text)]
# [['apple', 'banana'], ['kiwi', 'orange', 'pear']]

一个非标准(尚未)regex 模块提供了一个有趣的方法,称为“捕获”。 m.captures(n) 返回一个组的所有匹配项,而不仅仅是最新的匹配项,就像 m.group(n) 所做的那样:

import regex
matches = [x.captures(2) for x in regex.finditer(r'(?m)((?:^fruit: (.+)\n)+)', text)]
# [['apple', 'banana'], ['kiwi', 'orange', 'pear']]

【讨论】:

  • 感谢您提到regex 模块,我一定会使用它。不过,在我的情况下,两次比赛似乎是不可避免的。
【解决方案2】:

我认为如果你像这样使内部组不被捕获,你会看到问题:

re.findall(r'(?:^fruit: (?:.+)$\n)+', thetext, re.M)
# result:
['fruit: apple\nfruit: banana\n', 'fruit: kiwi\nfruit: orange\nfruit: pear\n']

问题是每个匹配都匹配一整串fruit: 行,但捕获组(在您的原始解决方案中)捕获多次。由于捕获组只能有一个与之关联的值,因此它以最后一个捕获的子字符串结束(我认为 last 的选择是任意的;我不会指望这种行为)。

【讨论】:

  • 感谢您的示例,它使问题易于理解。
【解决方案3】:

这可以让你保留你的正则表达式,正如你所说的你以后可能需要更复杂的表达式:

>>> import re
>>> from itertools import groupby
>>> with open('test.txt') as fin:
        groups = groupby((re.match(r'(?:fruit: )(.+)', line) for line in fin),
                         key=bool) # groups based on whether each line matched
        print [[m.group(1) for m in g] for k, g in groups if k]
        # prints each matching group


[['apple', 'banana'], ['kiwi', 'orange', 'pear']]

没有正则表达式:

>>> with open('test.txt') as f:
        print [[x.split()[1] for x in g]
               for k, g in groupby(f, key=lambda s: s.startswith('fruit'))
               if k]


[['apple', 'banana'], ['kiwi', 'orange', 'pear']]

【讨论】:

    【解决方案4】:

    另一种方式:

    import re
    with open('input') as file:
        lines = "".join(file.readlines())
        fruits = [[]]
        for fruit in re.findall(r'(?:fruit: ([^\n]*))|(?:\n\n)', lines, re.S):
            if fruit == '': 
                if len(fruits[-1]) > 0:
                    fruits.append([])
            else:
                fruits[-1].append(fruit)
        del fruits[-1]
        print fruits
    

    输出

    [['apple', 'banana'], ['kiwi', 'orange', 'pear']]
    

    【讨论】:

      【解决方案5】:

      除非绝对必要,否则我不太喜欢使用正则表达式。退后一步,看看你的情况,我的第一个倾向是考虑你是否实际上不应该在将输入文件输入 python 之前使用 awk 等专用工具将输入文件按摩成 csv 之类的东西。

      话虽如此,您当然仍然可以使用清晰的无正则表达式 python 完成您想要做的事情。一个例子(我确信可以在不牺牲透明度的情况下减少它):

      # newlst keeps track of whether you should start a new sublist
      newlst=False
      # result is the end result list of lists
      result = []
      # lst is the sublist which gets reset every time a grouping concludes
      lst = []
      
      with open('input.txt') as f:
          for line in f.readlines():
              # is the first token NOT a fruit?
              if line.split(':')[0] != 'fruit':
                  # if so, start a new sublist
                  newlst=True
                  # just so we don't append needless empty sublists
                  if len(lst) > 0: result.append(lst)
                  # initialise a new sublist, since last line wasn't a fruit and
                  # this implies a new group is starting
                  lst = []
              else:
                  # first token IS a fruit. So append it to the sublist
                  lst.append(line.split()[1])
      
      print result
      

      【讨论】:

        【解决方案6】:

        怎么样:

        re.findall(r'fruit: ([\w]+)\n|[^\n]*\n', str, re.M);
        

        结果:

        ['', '', 'apple', 'banana', '', '', '', 'kiwi', 'orange', 'pear', '']
        

        这可以很容易地转换为 [['apple', 'banana'], ['kiwi', 'orange', 'pear']]]

        example in ideone

        【讨论】:

        • 如果文件末尾有一个水果没有后跟换行符,这不起作用。 ideone.com/DTpEAx
        猜你喜欢
        • 2017-07-26
        • 1970-01-01
        • 2021-12-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多