使用Python解析基于块的程序输出

1 投票
5 回答
554 浏览
提问于 2025-04-16 03:05

我正在尝试用Python解析一个统计程序(Mplus)的输出结果。

这个输出的格式(这里有个例子)是分成多个块、子块和列的,空格和换行非常重要。根据你请求的选项,可能会在某些地方多出一个(子)块或列。

用正则表达式来处理这个问题让我感到非常痛苦,而且完全无法维护。我在考虑使用解析器作为更稳妥的解决方案,但

  1. 我对所有可能的工具和方法感到有点不知所措
  2. 我觉得这些工具不太适合处理这种类型的输出。

例如,LEPL有一种叫做行感知解析的功能,似乎朝着正确的方向发展(处理空格、块等),但它还是更适合解析编程语法,而不是输出结果。

如果能给我一些建议,告诉我该往哪个方向寻找,我会很感激。

5 个回答

1

根据你的例子,你现在面对的是一堆不同的、嵌套的子格式。单独来看,这些格式都很容易解析,但问题在于格式的数量太多,而且它们可以以不同的方式嵌套在一起,这就让人觉得有些复杂。

在最基本的层面上,你会看到一行行用空格分开的值。这些行组合成块,而这些块是如何组合和嵌套在一起的,才是复杂的地方。这种输出格式是为了人类阅读设计的,并不是为了被“抓取”回机器可读的形式。

首先,我建议你联系软件的作者,看看是否有其他输出格式,比如XML或CSV。如果做得好(也就是说,不是简单地把打印格式包裹在笨重的XML里,或者用逗号替换空格),那么处理起来会容易得多。如果没有这样的选项,我会尝试列出一个格式的层级列表,看看它们是如何嵌套的。例如:

  1. ESTIMATED SAMPLE STATISTICS 开始一个块
  2. 在这个块内,MEANS/INTERCEPTS/THRESHOLDS 开始一个嵌套块
  3. 接下来的两行是一些列标题
  4. 然后是一行(或多行?)数据,包含行标题和数据值

依此类推。如果你把每个问题单独处理,你会发现这虽然繁琐,但并不复杂。可以把上面的每一步看作是模块,测试输入是否匹配,如果匹配,就调用其他模块进一步测试块内可能出现的内容,如果遇到不匹配的情况,就回溯(这被称为“递归下降”)。

请注意,你无论如何都需要做类似的事情,以便构建一个内存中的数据版本(即“数据模型”),这样你才能进行操作。

1

我的建议是对这些数据进行一些简单的处理,让它们变得更有用。下面是我对你的数据做的一些实验:

from __future__ import print_function
from itertools import groupby
import string
counter = 0

statslist = [ statsblocks.split('\n')
            for statsblocks in  open('mlab.txt').read().split('\n\n')
            ]
print(len(statslist), 'blocks')

def blockcounter(line):
    global counter
    if not line[0]:
        counter += 1
    return counter

blocklist = [ [block, list(stats)] for block, stats in groupby(statslist, blockcounter)]

for blockno,block in enumerate(blocklist):
    print(120 * '=')
    for itemno,line in enumerate(block[1:][0]):
        if len(line)<4 and any(line[-1].endswith(c) for c in string.letters) :
            print('\n** DATA %i, HEADER (%r)**' % (blockno,line[-1]))
        else:
            print('\n** DATA %i, item %i, length %i **' % (blockno, itemno, len(line)))
        for ind,subdata in enumerate(line):
            if '___' in subdata:
                print(' *** Numeric data starts: ***')
            else:
                if 6 < len(subdata)<16:
                    print( '** TYPE: %s **' % subdata)
                print('%3i : %s' %( ind, subdata))
1

是的,解析这个东西确实挺麻烦的。不过,你其实不需要用很多复杂的正则表达式。普通的 split 方法就可能足够把这个文档分成更容易处理的字符串序列。

这里面有很多我称之为“头-体”结构的文本块。你会看到标题、一行“--”的分隔线,然后是数据。

你想做的就是把这种“头-体”结构压缩成一个生成器函数,这个函数会逐个输出字典。

def get_means_intecepts_thresholds( source_iter ):
    """Precondition: Current line is a "MEANS/INTERCEPTS/THRESHOLDS" line"""
    head= source_iter.next().strip().split()
    junk= source_iter.next().strip()
    assert set( junk ) == set( [' ','-'] )
    for line in source_iter:
        if len(line.strip()) == 0: continue
        if line.strip() == "SLOPES": break
        raw_data= line.strip().split()
        data = dict( zip( head, map( float, raw_data[1:] ) ) )
        yield int(raw_data[0]), data 

def get_slopes( source_iter ):
    """Precondition: Current line is a "SLOPES" line"""
    head= source_iter.next().strip().split()
    junk= source_iter.next().strip()
    assert set( junk ) == set( [' ','-'] )
    for line in source_iter:
        if len(line.strip()) == 0: continue
        if line.strip() == "SLOPES": break
        raw_data= line.strip().split() )
        data = dict( zip( head, map( float, raw_data[1:] ) ) )
        yield raw_data[0], data

关键是用一套操作来处理头部和一些杂乱的内容。

然后用另一套操作来处理后面的数据行。

因为这些是生成器,你可以把它们和其他操作结合起来使用。

def get_estimated_sample_statistics( source_iter ):
    """Precondition: at the ESTIMATED SAMPLE STATISTICS line"""
    for line in source_iter:
        if len(line.strip()) == 0: continue
    assert line.strip() == "MEANS/INTERCEPTS/THRESHOLDS"
    for data in get_means_intercepts_thresholds( source_iter ):
        yield data
    while True:
        if len(line.strip()) == 0: continue
        if line.strip() != "SLOPES": break
        for data in get_slopes( source_iter ): 
            yield data

这样做可能比用正则表达式要好。

撰写回答