在Python中拆分逗号分隔的字符串

9
这个问题以前已经被问过和回答过很多次。一些例子: [1], [2]。但是好像没有更加通用的方法。我想要的是一种在不在引号或者分隔符对中的逗号分割字符串的方式。例如:
s1 = 'obj<1, 2, 3>, x(4, 5), "msg, with comma"'

应该将其拆分为三个元素的列表。
['obj<1, 2, 3>', 'x(4, 5)', '"msg, with comma"']

现在的问题是,由于我们可以查看一对<>(),所以这可能变得更加复杂。
s2 = 'obj<1, sub<6, 7>, 3>, x(4, y(8, 9), 5), "msg, with comma"'

应该拆分为:

['obj<1, sub<6, 7>, 3>', 'x(4, y(8, 9), 5)', '"msg, with comma"']

没有使用正则表达式的朴素解决方案是通过查找字符,<(来解析字符串。如果找到<(中的任何一个,则开始计算奇偶性。只有当奇偶性为零时,我们才能在逗号处分割。例如,假设我们想要拆分s2,我们可以从parity = 0开始,并且当我们到达s2 [3]时遇到<,这将增加1个奇偶校验位。当它遇到>)时,奇偶校验位才会减少,当它遇到<(时,奇偶校验位才会增加。只要奇偶性不为0,我们就可以简单地忽略逗号而不进行任何分割。
问题是,有没有一种使用正则表达式快速解决此问题的方法?我真的很看重这个解决方案,但似乎它并不涵盖我提供的例子。
更通用的函数可能如下所示:
def split_at(text, delimiter, exceptions):
    """Split text at the specified delimiter if the delimiter is not
    within the exceptions"""

一些用法如下所示:

split_at('obj<1, 2, 3>, x(4, 5), "msg, with comma"', ',', [('<', '>'), ('(', ')'), ('"', '"')]

正则表达式能够处理这个问题吗,还是需要创建一个专门的解析器?

正则表达式在这种情况下无法帮助您,因为您尝试解析的语言(即一组字符串)不是正则的。鉴于您允许标签的任意嵌套,没有简单的方法可以通过正则表达式来解决这个问题。 - Yuval Adam
1
正则表达式实际上无法处理这种情况,也不应该使用它。其复杂度至少是线性的,因此您始终可以通过奇偶校验器获得更好的性能。然而,您并不需要亲自构建。Python 的 csv 模块会为您完成大部分工作。 - Slater Victoroff
2
啊,不要说正则表达式处理不了它!也许 Python 的风格不能,但像 PCRE 这样的其他风格可以做到!这是一个证明,我们甚至可以使用递归模式来考虑嵌套的 <>() - HamZa
如果您知道括号元素的最大嵌套递归深度,使用正则表达式也是可能的。但在Python中,由于缺乏递归正则表达式支持,建议使用更易于维护的解析器函数。 - Dean Taylor
1
完成了,现在的问题是为什么我会这样做 O_o? - HamZa
3个回答

8
虽然不能使用正则表达式,但以下简单的代码能够实现所需的结果:
def split_at(text, delimiter, opens='<([', closes='>)]', quotes='"\''):
    result = []
    buff = ""
    level = 0
    is_quoted = False

    for char in text:
        if char in delimiter and level == 0 and not is_quoted:
            result.append(buff)
            buff = ""
        else:
            buff += char

            if char in opens:
                level += 1
            if char in closes:
                level -= 1
            if char in quotes:
                is_quoted = not is_quoted

    if not buff == "":
        result.append(buff)

    return result

在解释器中运行以下代码:
>>> split_at('obj<1, 2, 3>, x(4, 5), "msg, with comma"', ',')                                                                                                                                 
#=>['obj<1, 2, 3>', ' x(4, 5)', ' "msg with comma"']

如果字符在closes中,那么level减1并继续;如果字符在opens中,则可以添加同时开启和关闭的分隔符,例如文字引号。这样,"msg, with comma"就可以通过。不需要为此情况单独处理程序。 - kalhartt

5

使用迭代器和生成器:

def tokenize(txt, delim=',', pairs={'"':'"', '<':'>', '(':')'}):
    fst, snd = set(pairs.keys()), set(pairs.values())
    it = txt.__iter__()

    def loop():
        from collections import defaultdict
        cnt = defaultdict(int)

        while True:
            ch = it.__next__()
            if ch == delim and not any (cnt[x] for x in snd):
                return
            elif ch in fst:
                cnt[pairs[ch]] += 1
            elif ch in snd:
                cnt[ch] -= 1
            yield ch

    while it.__length_hint__():
        yield ''.join(loop())

而且,

>>> txt = 'obj<1, sub<6, 7>, 3>,x(4, y(8, 9), 5),"msg, with comma"'
>>> [x for x in tokenize(txt)]
['obj<1, sub<6, 7>, 3>', 'x(4, y(8, 9), 5)', '"msg, with comma"']

4

如果您有递归嵌套的表达式,您可以按逗号拆分并使用pyparsing验证它们是否匹配:

import pyparsing as pp

def CommaSplit(txt):
    ''' Replicate the function of str.split(',') but do not split on nested expressions or in quoted strings'''
    com_lok=[]
    comma = pp.Suppress(',')
    # note the location of each comma outside an ignored expression:
    comma.setParseAction(lambda s, lok, toks: com_lok.append(lok))
    ident = pp.Word(pp.alphas+"_", pp.alphanums+"_")  # python identifier
    ex1=(ident+pp.nestedExpr(opener='<', closer='>'))   # Ignore everthing inside nested '< >'
    ex2=(ident+pp.nestedExpr())                       # Ignore everthing inside nested '( )'
    ex3=pp.Regex(r'("|\').*?\1')                      # Ignore everything inside "'" or '"'
    atom = ex1 | ex2 | ex3 | comma
    expr = pp.OneOrMore(atom) + pp.ZeroOrMore(comma  + atom )
    try:
        result=expr.parseString(txt)
    except pp.ParseException:
        return [txt]
    else:    
        return [txt[st:end] for st,end in zip([0]+[e+1 for e in com_lok],com_lok+[len(txt)])]             


tests='''\
obj<1, 2, 3>, x(4, 5), "msg, with comma"
nesteobj<1, sub<6, 7>, 3>, nestedx(4, y(8, 9), 5), "msg, with comma"
nestedobj<1, sub<6, 7>, 3>, nestedx(4, y(8, 9), 5), 'msg, with comma', additional<1, sub<6, 7>, 3>
bare_comma<1, sub(6, 7), 3>, x(4, y(8, 9), 5),  , 'msg, with comma', obj<1, sub<6, 7>, 3>
bad_close<1, sub<6, 7>, 3), x(4, y(8, 9), 5), 'msg, with comma', obj<1, sub<6, 7>, 3)
'''

for te in tests.splitlines():
    result=CommaSplit(te)
    print(te,'==>\n\t',result)

输出:

obj<1, 2, 3>, x(4, 5), "msg, with comma" ==>
     ['obj<1, 2, 3>', ' x(4, 5)', ' "msg, with comma"']
nesteobj<1, sub<6, 7>, 3>, nestedx(4, y(8, 9), 5), "msg, with comma" ==>
     ['nesteobj<1, sub<6, 7>, 3>', ' nestedx(4, y(8, 9), 5)', ' "msg, with comma"']
nestedobj<1, sub<6, 7>, 3>, nestedx(4, y(8, 9), 5), 'msg, with comma', additional<1, sub<6, 7>, 3> ==>
     ['nestedobj<1, sub<6, 7>, 3>', ' nestedx(4, y(8, 9), 5)', " 'msg, with comma'", ' additional<1, sub<6, 7>, 3>']
bare_comma<1, sub(6, 7), 3>, x(4, y(8, 9), 5),  , 'msg, with comma', obj<1, sub<6, 7>, 3> ==>
     ['bare_comma<1, sub(6, 7), 3>', ' x(4, y(8, 9), 5)', '  ', " 'msg, with comma'", ' obj<1, sub<6, 7>, 3>']
bad_close<1, sub<6, 7>, 3), x(4, y(8, 9), 5), 'msg, with comma', obj<1, sub<6, 7>, 3) ==>
     ["bad_close<1, sub<6, 7>, 3), x(4, y(8, 9), 5), 'msg, with comma', obj<1, sub<6, 7>, 3)"]

当前行为类似于'(某些内容不分割),b,“带引号的内容”,c'.split('),',包括保留前导空格和引号。从字段中删除引号和前导空格非常容易。

将try下面的else更改为:

else:
    rtr = [txt[st:end] for st,end in zip([0]+[e+1 for e in com_lok],com_lok+[len(txt)])]
    if strip_fields:
        rtr=[e.strip().strip('\'"') for e in rtr]
    return rtr  

这种方法的缺点是你需要构建条件语句来重新拼接那些本不应该被分割的项目。 - brandonscript
1
这不正确,因为它将字符串“obj<1, 2, 3>”分割了。 - jmlopez
我同意使用库是一个明智的解决方案,但这并没有正确回答问题。 - Aaron Cronin
我修复了结果。谢谢。 - dawg
可能需要考虑另一种修复方法,因为以下代码无法正常工作:result=expr.parseString('obj<1, sub<6, 7>, 3>,x(4, y(8, 9), 5),"msg, with comma"') - jmlopez
@jmlopez:好的,我又修复了一下,并在此过程中学到了一些pyparsing。这是一个非常好的问题! - dawg

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接