大多数pythonic方式打破高度分支的解析器

CTK*_*ein 5 python iterator loops python-2.7

我正在为一个特定类型的文件开发一个解析器,这个文件被一些header关键字分成几部分,后面跟着一堆异构数据.标题始终用空行分隔.以下内容:

Header_A

1 1.02345
2 2.97959
...

Header_B

1   5.1700   10.2500
2   5.0660   10.5000
...
Run Code Online (Sandbox Code Playgroud)

每个标头包含非常不同类型的数据,并且根据块内的某些关键字,数据必须存储在不同的位置.我采用的一般方法是使用一些正则表达式捕获所有可以定义标题的关键字,然后遍历文件中的行.一旦找到匹配项,我会弹出行直到我到达一个空行,将所有数据存储在相应位置的行中.

这是代码的基本结构,其中"使用current_line做东西"将涉及一堆分支,具体取决于该行包含的内容:

headers = re.compile(r"""
    ((?P<header_a>Header_A)
    |
    (?P<header_b>Header_B))
    """, re.VERBOSE)

i = 0
while i < len(data_lines):
    match = header.match(data_lines[i])
    if match:
        if match.group('header_a'):
            data_lines.pop(i)
            data_lines.pop(i)

            #     not end of file         not blank line
            while i < len(data_lines) and data_lines[i].strip():
                current_line = data_lines.pop(i)
                # do stuff with current_line

        elif match.group('header_b'):
            data_lines.pop(i)
            data_lines.pop(i)

            while i < len(data_lines) and data_lines[i].strip():
                current_line = data_lines.pop(i)
                # do stuff with current_line
        else:
            i += 1
    else:
        i += 1
Run Code Online (Sandbox Code Playgroud)

一切正常,但它相当于一个高度分支的结构,我觉得非常难以理解,很可能很难跟随任何不熟悉代码的人.这也使得将行保持在<79个字符变得更加困难,而且通常不会感觉非常pythonic.

我正在做的一件事是将每个标题的分支分成单独的函数.这有望提高可读性,但......

...是否有更简洁的方法来执行外循环/匹配结构?也许使用itertools?

此外,由于各种原因,此代码必须能够在2.7中运行.

unu*_*tbu 4

您可以使用itertools.groupby根据您希望执行的处理功能对行进行分组:

import itertools as IT

def process_a(lines):
    for line in lines:
        line = line.strip()
        if not line: continue        
        print('processing A: {}'.format(line))

def process_b(lines):
    for line in lines:
        line = line.strip()
        if not line: continue        
        print('processing B: {}'.format(line))

def header_func(line):
    if line.startswith('Header_A'):
        return process_a
    elif line.startswith('Header_B'):
        return process_b
    else: return None  # you could omit this, but it might be nice to be explicit

with open('data', 'r') as f:
    for key, lines in IT.groupby(f, key=header_func):
        if key is None:
            if func is not None:
                func(lines)
        else:
            func = key
Run Code Online (Sandbox Code Playgroud)

应用于您发布的数据,上面的代码打印

processing A: 1 1.02345
processing A: 2 2.97959
processing A: ...
processing B: 1   5.1700   10.2500
processing B: 2   5.0660   10.5000
processing B: ...
Run Code Online (Sandbox Code Playgroud)

上面代码中最复杂的一行是

for key, lines in IT.groupby(f, key=header_func):
Run Code Online (Sandbox Code Playgroud)

让我们尝试将其分解为几个组成部分:

In [31]: f = open('data')

In [32]: list(IT.groupby(f, key=header_func))
Out[32]: 
[(<function __main__.process_a>, <itertools._grouper at 0xa0efecc>),
 (None, <itertools._grouper at 0xa0ef7cc>),
 (<function __main__.process_b>, <itertools._grouper at 0xa0eff0c>),
 (None, <itertools._grouper at 0xa0ef84c>)]
Run Code Online (Sandbox Code Playgroud)

IT.groupby(f, key=header_func)返回一个迭代器。迭代器产生的项是 2 元组,例如

(<function __main__.process_a>, <itertools._grouper at 0xa0efecc>)
Run Code Online (Sandbox Code Playgroud)

二元组中的第一项是 的返回值header_func。二元组中的第二项是迭代器。该迭代器生成的行都f返回header_func(line)相同的值。

因此,根据 的返回值对行IT.groupby进行分组。当 in 中的行是标题行时 - 或者-然后返回or ,我们希望使用该函数来处理后续行。fheader_funcfHeader_AHeader_Bheader_funcprocess_aprocess_b

当 in 中的行是标题行时, (二元组中的第二项)f返回的行组很短且无趣——它只是标题行。IT.groupby

我们需要在下一组中寻找有趣的线条。对于这些行,header_func返回None

因此,我们需要查看两个 2 元组:由 生成的第一个 2 元组为IT.groupby我们提供了要使用的函数,第二个 2 元组给出了应应用标头函数的行。

一旦您拥有带有有趣行的函数和迭代器,您只需调用func(lines)即可完成!

请注意,将其扩展以处理其他类型的标头将非常容易。您只需要编写另一个process_*函数,并修改为在指示时header_func返回。process_*line


编辑:我删除了使用,izip(*[iterator]*2)因为它假设第一行是标题行。第一行可能是空白或非标题行,这会使一切都混乱。我用一些替换了它if-statements。它并不那么简洁,但结果更加稳健。