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

时间:2014-02-28 22:10:48

标签: python loops python-2.7 iterator

我正在研究一种特定类型文件的解析器,该文件被一些标题关键字分成几部分,后面跟着一堆异构数据。标题始终用空行分隔。以下内容:

Header_A

1 1.02345
2 2.97959
...

Header_B

1   5.1700   10.2500
2   5.0660   10.5000
...

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

这是代码的基本结构,其中“使用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

一切正常但它相当于一个高度分支的结构,我发现它非常难以辨认,并且对于不熟悉代码的人来说可能难以理解。这也使得将行保持在<79个字符变得更加困难,而且通常不会感觉非常pythonic。

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

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

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

3 个答案:

答案 0 :(得分:3)

您可以使用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

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

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: ...

上面代码中的一行复杂是

for key, lines in IT.groupby(f, key=header_func):

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

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>)]

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

(<function __main__.process_a>, <itertools._grouper at 0xa0efecc>)

2元组中的第一项是header_func返回的值。 2元组中的第二项是迭代器。这个迭代器产生来自f的行,header_func(line)都返回相同的值。

因此,IT.groupby根据f的返回值对header_func中的行进行分组。当f中的行是标题行 - Header_AHeader_B - 然后header_func返回process_aprocess_b时,函数我们希望用来处理后续行。

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

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

所以我们需要看两个2元组:由IT.groupby产生的第一个2元组给我们使用的函数,第二个2元组给出了应该应用头函数的行

一旦你将函数和迭代器都包含在有趣的行中,你只需调用func(lines)就可以了!

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


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

答案 1 :(得分:2)

如何将用于解析不同标头数据类型的逻辑拆分为单独的函数,然后使用字典从给定标头映射到正确的标题:

def parse_data_a(iterator):
    next(iterator) # throw away the blank line after the header
    for line in iterator:
        if not line.strip():
            break  # bale out if we find a blank line, another header is about to start
        # do stuff with each line here

# define similar functions to parse other blocks of data, e.g. parse_data_b()

# define a mapping from header strings to the functions that parse the following data
parser_for_header = {"Header_A": parse_data_a} # put other parsers in here too!

def parse(lines):
    iterator = iter(lines)
    for line in iterator:
        header = line.strip()
        if header in parser_for_header:
            parser_for_header[header](iterator)

此代码使用迭代,而不是索引来处理行。这样做的一个优点是除了在行列表上之外,您还可以直接在文件上运行它,因为文件是可迭代的。它还使边界检查变得非常容易,因为当迭代中没有任何内容时,以及当for语句被命中时,break循环将自动结束。

根据您正在对正在解析的数据执行的操作,您可能需要让各个解析器返回一些内容,而不是仅仅关闭并执行自己的操作。在这种情况下,您需要在顶级parse函数中使用一些逻辑来获取结​​果并将其组合成一些有用的格式。也许字典最有意义,最后一行成为:

results_dict[header] = parser_for_header[header](iterator)

答案 2 :(得分:2)

您也可以使用生成器的send功能执行此操作:)

data_lines = [
    'Header_A   ',
    '',
    '',
    '1 1.02345',
    '2 2.97959',
    '',
]

def process_header_a(line):
    while True:
        line = yield line
        # process line
        print 'A', line

header_processors = {
    'Header_A': process_header_a(None),
}

current_processer = None
for line in data_lines:
    line = line.strip()
    if line in header_processors:
        current_processor = header_processors[line]
        current_processor.send(None)
    elif line:
        current_processor.send(line)    

for processor in header_processors.values():
    processor.close()

如果替换

,您可以从主循环中删除所有if条件
current_processer = None
for line in data_lines:
    line = line.strip()
    if line in header_processors:
        current_processor = header_processors[line]
        current_processor.send(None)
    elif line:
        current_processor.send(line)    

map(next, header_processors.values())
current_processor = header_processors['Header_A']
for line in data_lines:
    line = line.strip()
    current_processor = header_processors.get(line, current_processor)
    line and line not in header_processors and current_processor.send(line)