在两个符号之间提取多行数据 - 正则表达式与Python3

4 投票
3 回答
3157 浏览
提问于 2025-04-17 16:24

我有一个很大的文件,我需要从中提取特定条目的数据。这个文件的结构是:

>Entry1.1
#size=1688
704 1   1   1   4
979 2   2   2   0
1220    1   1   1   4
1309    1   1   1   4
1316    1   1   1   4
1372    1   1   1   4
1374    1   1   1   4
1576    1   1   1   4
>Entry2.1
#size=6251
6110    3   1.5 0   2
6129    2   2   2   2
6136    1   1   1   4
6142    3   3   3   2
6143    4   4   4   1
6150    1   1   1   4
6152    1   1   1   4
>Entry3.2
#size=1777
AND SO ON-----------

我想要做的是提取所有与某些条目相关的行(完整记录)。比如说,我需要Entry1.1的记录,那么我可以用条目的名字'>Entry1.1'到下一个'>'作为标记,利用正则表达式(REGEX)来提取中间的行。但是我不知道怎么写这么复杂的正则表达式。一旦我有了这样的表达式,我就会把它放进一个循环里:

For entry in entrylist:
    GET record from big_file
    DO some processing
    WRITE in result file

那么,应该用什么正则表达式来提取特定条目的记录呢?有没有更简单的Python方法来实现这个?我会很感激你们的帮助。

AK

3 个回答

0

我不太确定你在问什么。这样说能帮到你吗?这个方法会把你所有的条目当作字典的键,并把所有的条目放在一个列表里。前提是它的格式和我想的一样。里面有重复的条目吗?这是我写的代码:

entries = {}
key = ''
for entry in open('entries.txt'):
    if entry.startswith('>Entry'):
       key = entry[1:].strip() # removes > and newline
       entries[key] = []
    else:
       entries[key].append(entry)
1

我对正则表达式不太在行,所以我尽量寻找不使用正则表达式的解决方案。在Python中,处理循环逻辑的自然选择是生成器,所以我会用类似下面的方式(不需要itertools的版本):

def group_by_marker(seq, marker):
    group = []
    # advance past negatives at start
    for line in seq:
        if marker(line):
            group = [line]
            break
    for line in seq:
        # found a new group start; yield what we've got
        # and start over
        if marker(line) and group:
            yield group
            group = []
        group.append(line)
    # might have extra bits left..
    if group:
        yield group

在你的例子中,我们得到:

>>> with open("entry0.dat") as fp:
...     marker = lambda line: line.startswith(">Entry")
...     for group in group_by_marker(fp, marker):
...         print(repr(group[0]), len(group))
...         
'>Entry1.1\n' 10
'>Entry2.1\n' 9
'>Entry3.2\n' 4

这种方法的一个好处是,我们不需要在内存中保留多个组,所以对于非常大的文件来说,这种方法很方便。虽然它的速度没有正则表达式快,但如果文件有1GB,你可能已经被输入输出速度限制了。

4

使用正则表达式

import re

ss = '''
>Entry1.1
#size=1688
704 1   1   1   4
979 2   2   2   0
1220    1   1   1   4
1309    1   1   1   4
1316    1   1   1   4
1372    1   1   1   4
1374    1   1   1   4
1576    1   1   1   4
>Entry2.1
#size=6251
6110    3   1.5 0   2
6129    2   2   2   2
6136    1   1   1   4
6142    3   3   3   2
6143    4   4   4   1
6150    1   1   1   4
6152    1   1   1   4
>Entry3.2
#size=1777
AND SO ON-----------
'''

patbase = '(>Entry *%s(?![^\n]+?\d).+?)(?=>|(?:\s*\Z))'


while True:
    x = raw_input('What entry do you want ? : ')
    found = re.findall(patbase % x, ss, re.DOTALL)
    if found:
        print 'found ==',found
        for each_entry in found:
            print '\n%s\n' % each_entry
    else:
        print '\n ** There is no such an entry **\n'

'(>Entry *%s(?![^\n]+?\d).+?)(?=>|(?:\s*\Z))' 的解释:

1)

%s 是用来接收条目的引用,比如 1.1、2、2.1 等等。

2)

其中 (?![^\n]+?\d) 是用来做验证的。

(?![^\n]+?\d) 是一种负向前瞻断言,意思是 %s 后面不能跟着 [^\n]+?\d,也就是说在数字 \d 前面不能有任何字符 [^\n]+?

我写 [^\n] 是为了表示“除了换行符 \n 之外的任何字符”。
我必须这样写,而不能简单用 .+?,因为我设置了 re.DOTALL 这个标志,而 .+? 这个模式会一直匹配到条目的结尾。
但我只想验证在输入的引用(用 %s 表示)后面,不会有额外的数字出现在行的末尾,这是因为用户可能输入错误。

这样做是因为如果存在 Entry2.1 但没有 Entry2,而用户只输入 2,因为他只想要 Entry2,而不是其他的,正则表达式会检测到 Entry2.1 的存在,并返回它,尽管用户实际上想要的是 Entry2。

3)

'(>Entry *%s(?![^\n]+?\d).+?) 的末尾,.+? 部分会捕获整个条目的块,因为点号代表任何字符,包括换行符 \n
我设置 re.DOTALL 的目的是让后面的模式 .+? 能够跨越换行符,一直到条目的结尾。

4)

我希望匹配在所需条目的末尾停止,而不是在下一个条目内部,这样用括号定义的组 (>Entry *%s(?![^\n]+?\d).+?) 就能准确捕获我们想要的内容。
因此,我在末尾加了一个正向前瞻断言 (?=>|(?:\s*\Z)),这表示运行中的非贪婪 .+? 必须在 >(下一个条目的开始)或字符串的结尾 \Z 停止匹配。
由于最后一个条目的结束可能并不正好是整个字符串的结束,我加了 \s*,表示“在最后结束之前可能有空白字符”。
所以 \s*\Z 的意思是“在遇到字符串的结尾之前可能有空白字符”。
空白字符包括 空格\f\n\r\t\v

撰写回答