Разделение строк с разделителями-запятыми в python

Этот вопрос уже задавался и на него уже много раз давался ответ. Некоторые примеры: [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"', ',', [('<', '>'), ('(', ')'), ('"', '"')]

Сможет ли регулярное выражение справиться с этим или необходимо создать специализированный парсер?


person jmlopez    schedule 15.12.2013    source источник
comment
Регулярные выражения в этом случае вам не помогут, так как язык (т.е. группа строк), который вы пытаетесь разобрать, не является регулярным. Учитывая, что вы допускаете произвольное вложение тегов, нет простого способа регулярного выражения выйти из этого.   -  person Yuval Adam    schedule 16.12.2013
comment
На самом деле Regex не может справиться с этим, и вы бы этого не хотели. Сложность, как минимум, линейна, поэтому вы обязательно всегда получите лучшую производительность с проверкой четности. При этом вам не нужно строить его самостоятельно. Модуль Python csv выполняет большую часть работы.   -  person Slater Victoroff    schedule 16.12.2013
comment
Argh, не говорите, что регулярное выражение не может справиться с этим! Возможно, вариант Python не мог, но другие варианты, такие как PCRE, могли бы это сделать! Это доказательство, мы могли бы даже придумать и использовать рекурсивные шаблоны для учета вложенных <>()   -  person HamZa    schedule 16.12.2013
comment
Это также возможно с регулярным выражением, если вы знаете максимальную глубину вложенной рекурсии элементов в квадратных скобках. Но в Python, где у вас нет поддержки рекурсивных регулярных выражений, используйте более удобный парсер функция.   -  person Dean Taylor    schedule 16.12.2013
comment
Аааа и я это сделал, теперь вопрос, почему я сделал О_о ?   -  person HamZa    schedule 16.12.2013


Ответы (3)


Хотя использовать регулярное выражение невозможно, следующий простой код позволит достичь желаемого результата:

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"']
person Aaron Cronin    schedule 15.12.2013
comment
if char in closes: level -= 1 continue if char in opens: Это должно позволить вам добавить разделители, которые одновременно открываются и закрываются, как буквальная цитата. так что "msg, with comma" проходит. В этом случае нет необходимости в отдельном обработчике. - person kalhartt; 16.12.2013

используя итераторы и генераторы:

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"']
person behzad.nouri    schedule 15.12.2013

Если у вас есть рекурсивные вложенные выражения, вы можете разделить их запятыми и проверить, что они совпадают, сделав это с помощью 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)"]

Текущее поведение такое же, как '(something does not split), b, "in quotes", c'.split','), включая сохранение начальных пробелов и кавычек. Тривиально удалить кавычки и начальные пробелы из полей.

Измените else под try на:

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  
person dawg    schedule 15.12.2013
comment
Недостатком этого подхода является то, что вам придется создавать условные операторы, чтобы повторно сшивать элементы, которые не должны были быть разделены. - person brandonscript; 16.12.2013
comment
Это неверно, так как разделяет строку "obj<1, 2, 3>". - person jmlopez; 16.12.2013
comment
Я согласен с тем, что библиотеки являются разумным решением, но это не дает правильного ответа на вопрос. - person Aaron Cronin; 16.12.2013
comment
Я зафиксировал результат. Спасибо. - person dawg; 16.12.2013
comment
Может потребоваться рассмотреть другое исправление, поскольку следующее не работает: result=expr.parseString('obj<1, sub<6, 7>, 3>,x(4, y(8, 9), 5),"msg, with comma"') - person jmlopez; 16.12.2013
comment
@jmlopez: ОК -- я исправил это снова и в процессе немного научился парсингу. Это очень хороший вопрос! - person dawg; 20.12.2013