2013-12-15 5 views
9

Questa domanda è stata posta e ha risposto molte volte in precedenza. Alcuni esempi: [1], [2]. Ma non sembra essere qualcosa di più generale. Quello che sto cercando è un modo per dividere le stringhe in virgole che non siano tra virgolette o coppie di delimitatori. Per esempio:Suddivisione delle stringhe delimitate da virgola in python

s1 = 'obj<1, 2, 3>, x(4, 5), "msg, with comma"' 

dovrebbe essere diviso in una lista di tre elementi

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

Il problema ora è che questo può diventare più complicata dal momento che siamo in grado di guardare in coppie di <> e ().

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

che dovrebbe essere diviso in:

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

La soluzione ingenua senza l'utilizzo di espressioni regolari è quello di analizzare la stringa, cercando per i personaggi ,<(. Se vengono trovati < o (, iniziamo a contare la parità. Possiamo dividere solo una virgola se la parità è zero. Per esempio diciamo che vogliamo dividere s2, possiamo iniziare con parity = 0 e quando raggiungiamo s2[3] abbiamo incontrare < che aumenterà la parità di 1. La parità diminuisce solo quando incontra > o ) e aumenterà quando incontra < o ( . Mentre la parità non è 0, possiamo semplicemente ignorare le virgole e non fare alcuna suddivisione.

La domanda qui è, c'è un modo per questo rapidamente con regex? Stavo davvero esaminando questo solution ma questo non sembra che copra gli esempi che ho dato.

una funzione più generale sarebbe qualcosa di simile:

def split_at(text, delimiter, exceptions): 
    """Split text at the specified delimiter if the delimiter is not 
    within the exceptions""" 

Alcuni usi sarebbe come questo:

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

Sarebbe regex essere in grado di gestire questo o è necessario creare una specializzati parser?

+0

Le espressioni regolari non vi aiuterà in questo caso, poiché la lingua (es. un gruppo di stringhe) che stai cercando di analizzare non è regolare. Dato che si consente l'annidamento arbitrario dei tag, non esiste un modo semplice per regex l'uscita da questo. –

+1

Regex non può in effetti gestire questo problema e non lo si vorrebbe. La complessità è lineare al minimo, quindi è sempre necessario ottenere prestazioni migliori con il controllo di parità. Non devi costruirlo da solo però. Il modulo 'csv' di Python fa un sacco di legwork. –

+2

Argh, non dire che regex non può gestirlo! Forse il sapore del pitone non poteva, ma altri sapori come PCRE potevano farlo! Questo è [a prova] (http://regex101.com/r/wU7lC9), potremmo anche essere fantasiosi e usare pattern ricorsivi per tenere in considerazione nested '<>()' – HamZa

risposta

8

Anche se non è possibile usare un'espressione regolare, il seguente codice semplice sarà ottenere il risultato desiderato:

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 

L'esecuzione di questo nell'interprete:

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

'se char in closes: level - = 1 continua se char in opens:' Questo dovrebbe consentire di aggiungere delimitatori che si aprono e si chiudono, come la citazione letterale. quindi '' msg, con la virgola "' passa. Non è necessario un gestore seprate per questo caso. – kalhartt

4

Se si dispone di espressioni nidificate ricorsive , puoi dividere le virgole e convalidare che corrispondono facendo questo con 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) 

Stampe:

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

Il comportamento attuale è proprio come '(something does not split), b, "in quotes", c'.split',') compreso il mantenimento gli spazi iniziali e le virgolette. È banale smussare le virgolette e condurre gli spazi dai campi.

Modificare il else sotto try a:

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 
+0

Lato negativo di questo approccio è quindi necessario costruire condizionali per ricucire gli elementi che non dovrebbero essere divisi. – brandonscript

+1

Questo non è corretto poiché divide la stringa '" obj <1, 2, 3> "'. – jmlopez

+0

+1 per puntare a una libreria invece di ruotare la propria –

5

utilizzando iteratori e generatori:

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

e,

>>> 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"']