在两个符号之间提取多行数据 - 正则表达式与Python3
我有一个很大的文件,我需要从中提取特定条目的数据。这个文件的结构是:
>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 个回答
我不太确定你在问什么。这样说能帮到你吗?这个方法会把你所有的条目当作字典的键,并把所有的条目放在一个列表里。前提是它的格式和我想的一样。里面有重复的条目吗?这是我写的代码:
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)
我对正则表达式不太在行,所以我尽量寻找不使用正则表达式的解决方案。在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,你可能已经被输入输出速度限制了。
使用正则表达式
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
。