2009-10-30 13 views
20

ho avuto una serie di tale formato:Come dividere una stringa con virgole posizionate al di fuori della parentesi?

"Wilbur Smith (Billy, son of John), Eddie Murphy (John), Elvis Presley, Jane Doe (Jane Doe)" 

quindi fondamentalmente è l'elenco dei nomi di attori (eventualmente seguita da loro ruolo tra parentesi). Il ruolo stesso può contenere una virgola (il nome dell'attore non può, lo spero fortemente).

Il mio obiettivo è dividere questa stringa in un elenco di coppie - (actor name, actor role).

Una soluzione ovvia sarebbe quella di passare attraverso ogni carattere, controllare le occorrenze di '(', ')' e ',' e dividere ogni volta una virgola occures fuori. Ma questo sembra un po 'pesante ...

stavo pensando spliting utilizzando un regexp: prima dividere la stringa da parentesi:

import re 
x = "Wilbur Smith (Billy, son of John), Eddie Murphy (John), Elvis Presley, Jane Doe (Jane Doe)" 
s = re.split(r'[()]', x) 
# ['Wilbur Smith ', 'Billy, son of John', ', Eddie Murphy ', 'John', ', Elvis Presley, Jane Doe ', 'Jane Doe', ''] 

Gli elementi dispari qui sono i nomi di attori, anche sono i ruoli. Quindi potrei dividere i nomi con delle virgole e in qualche modo estrarre le coppie nome-ruolo. Ma questo sembra ancora peggio del mio primo approccio.

Esistono modi più semplici/più belli per eseguire questa operazione, con una singola espressione regolare o un bel pezzo di codice?

risposta

18

Un modo per farlo è quello di utilizzare findall con un regex che combina avidamente le cose che possono andare tra i separatori. ad esempio:

>>> s = "Wilbur Smith (Billy, son of John), Eddie Murphy (John), Elvis Presley, Jane Doe (Jane Doe)" 
>>> r = re.compile(r'(?:[^,(]|\([^)]*\))+') 
>>> r.findall(s) 
['Wilbur Smith (Billy, son of John)', ' Eddie Murphy (John)', ' Elvis Presley', ' Jane Doe (Jane Doe)'] 

L'espressione regolare sopra corrisponde a uno o più:

  • non virgola, caratteri non-open-paren
  • stringhe che iniziano con una parentesi aperta, contengono 0 o più non- close-parens e quindi close paren

Una stranezza su questo approccio è che i separatori adiacenti sono trattati come un singolo separatore. Cioè, non vedrai una stringa vuota. Potrebbe trattarsi di un bug o di una funzionalità a seconda del caso d'uso.

Si noti inoltre che le espressioni regolari sono non adatto per i casi in cui è possibile la nidificazione.Così, per esempio, questo potrebbe dividere in modo non corretto:

"Wilbur Smith (son of John (Johnny, son of James), aka Billy), Eddie Murphy (John)" 

Se è necessario affrontare con la nidificazione la soluzione migliore sarebbe quella di dividere la stringa in parens, virgole, e everthing altro (essenzialmente creazione di token è - questa parte potrebbe ancora esegui regex) e poi cammina attraverso quei token riassemblando i campi, tenendo traccia del tuo livello di nidificazione mentre procedi (questo tenere traccia del livello di nidificazione è ciò che le regex non sono in grado di fare da soli).

+1

È possibile dividere in campi subito abbinando record invece che i separatori: [(m.group ("nome"), m.group ("ruolo")) per m di re.findall ("(? P . +?) (? \ ((? P [^ \)] +) \) (, \ s * | $)) ", x)] –

+0

+1 per la soluzione token se ne ha bisogno. Scendi e scendi dalla pila mentre cammini su e giù ... un modo classico per farlo. –

+2

ogni volta che vedo l'espressione regolare che è utile, come questa, comincio a chiedermi - dovrebbero essere leggibili? O sono solo io ... chi non lo vede dal primo sguardo? – kender

5

Penso che il modo migliore per avvicinarsi a questo sarebbe utilizzare il modulo integrato csv di python.

Poiché il modulo csv unico allows un un carattere quotechar, si avrebbe bisogno di fare una sostituzione su ingressi per convertire () a qualcosa come | o ". Quindi assicurati di utilizzare un dialetto appropriato e di partire.

0

Sono certamente d'accordo con @Wogan sopra, che l'utilizzo del moudle CSV è un buon approccio. Detto questo, se si vuole ancora provare una soluzione regex dare una prova, ma si dovrà adattarlo alle Python dialetto

string.split(/,(?=(?:[^\"]*\"[^\"]*\")*(?![^\"]*\"))/) 

HTH

1

La mia risposta non utilizzerà regex.

Penso che lo scanner di caratteri semplice con stato "in_actor_name" dovrebbe funzionare. Ricorda quindi che lo stato "in_actor_name" è terminato con ")" o con una virgola in questo stato.

La mia prova:

s = 'Wilbur Smith (Billy, son of John), Eddie Murphy (John), Elvis Presley, Jane Doe (Jane Doe)' 

in_actor_name = 1 
role = '' 
name = '' 
for c in s: 
    if c == ')' or (c == ',' and in_actor_name): 
     in_actor_name = 1 
     name = name.strip() 
     if name: 
      print "%s: %s" % (name, role) 
     name = '' 
     role = '' 
    elif c == '(': 
     in_actor_name = 0 
    else: 
     if in_actor_name: 
      name += c 
     else: 
      role += c 
if name: 
    print "%s: %s" % (name, role) 

uscita:

Wilbur Smith: Billy, son of John 
Eddie Murphy: John 
Elvis Presley: 
Jane Doe: Jane Doe 
0

divisa da ")"

>>> s="Wilbur Smith (Billy, son of John), Eddie Murphy (John), Elvis Presley, Jane Doe (Jane Doe)" 
>>> s.split(")") 
['Wilbur Smith (Billy, son of John', ', Eddie Murphy (John', ', Elvis Presley, Jane Doe (Jane Doe', ''] 
>>> for i in s.split(")"): 
... print i.split("(") 
... 
['Wilbur Smith ', 'Billy, son of John'] 
[', Eddie Murphy ', 'John'] 
[', Elvis Presley, Jane Doe ', 'Jane Doe'] 
[''] 

si può fare un'ulteriore verifica per ottenere quei nomi che non vengono con().

4
s = re.split(r',\s*(?=[^)]*(?:\(|$))', x) 

Il lookahead corrisponde tutto fino al prossimo open-parentesi o alla fine della stringa, se e solo se non c'è primo parentesi in mezzo. Ciò garantisce che la virgola non si trovi all'interno di una serie di parentesi.

2

Un tentativo su regex leggibile:

import re 

regex = re.compile(r""" 
    # name starts and ends on word boundary 
    # no '(' or commas in the name 
    (?P<name>\b[^(,]+\b) 
    \s* 
    # everything inside parentheses is a role 
    (?:\(
     (?P<role>[^)]+) 
    \))? # role is optional 
    """, re.VERBOSE) 

s = ("Wilbur Smith (Billy, son of John), Eddie Murphy (John), Elvis Presley," 
    "Jane Doe (Jane Doe)") 
print re.findall(regex, s) 

uscita:

[('Wilbur Smith', 'Billy, son of John'), ('Eddie Murphy', 'John'), 
('Elvis Presley', ''), ('Jane Doe', 'Jane Doe')] 
+1

regex leggibile dall'uomo - non è un ossimoro? – Amarghosh

0

Ecco una tecnica generale che ho usato in passato per questi casi:

Utilizzare il sub funzione del modulo re con una funzione come argomento di sostituzione. La funzione tiene traccia dell'apertura e della chiusura di parentesi, parentesi graffe e parentesi graffe, nonché virgolette singole e doppie, ed esegue una sostituzione solo al di fuori di tali sottostringhe tra parentesi e quotate. È quindi possibile sostituire le virgole non tra parentesi/quotate con un altro carattere che si è certi non compaia nella stringa (io uso il separatore di gruppo ASCII/Unicode: chr (29)), quindi eseguo una stringa semplice. diviso su quel personaggio. Ecco il codice:

import re 
def srchrepl(srch, repl, string): 
    """Replace non-bracketed/quoted occurrences of srch with repl in string""" 

    resrchrepl = re.compile(r"""(?P<lbrkt>[([{])|(?P<quote>['"])|(?P<sep>[""" 
          + srch + """])|(?P<rbrkt>[)\]}])""") 
    return resrchrepl.sub(_subfact(repl), string) 

def _subfact(repl): 
    """Replacement function factory for regex sub method in srchrepl.""" 
    level = 0 
    qtflags = 0 
    def subf(mo): 
     nonlocal level, qtflags 
     sepfound = mo.group('sep') 
     if sepfound: 
      if level == 0 and qtflags == 0: 
       return repl 
      else: 
       return mo.group(0) 
     elif mo.group('lbrkt'): 
      level += 1 
      return mo.group(0) 
     elif mo.group('quote') == "'": 
      qtflags ^= 1   # toggle bit 1 
      return "'" 
     elif mo.group('quote') == '"': 
      qtflags ^= 2   # toggle bit 2 
      return '"' 
     elif mo.group('rbrkt'): 
      level -= 1 
      return mo.group(0) 
    return subf 

Se non si dispone di nonlocal nella versione di Python, basta cambiare a global e definire level e qtflags a livello di modulo.

Ecco come viene utilizzato:

>>> GRPSEP = chr(29) 
>>> string = "Wilbur Smith (Billy, son of John), Eddie Murphy (John), Elvis Presley, Jane Doe (Jane Doe)" 
>>> lst = srchrepl(',', GRPSEP, string).split(GRPSEP) 
>>> lst 
['Wilbur Smith (Billy, son of John)', ' Eddie Murphy (John)', ' Elvis Presley', ' Jane Doe (Jane Doe)'] 
-1

Nessuna delle risposte di cui sopra sono corrette se ci sono errori o rumore nei dati.

È facile trovare una buona soluzione se si sa che i dati sono corretti ogni volta. Ma cosa succede se ci sono errori di formattazione? Cosa vuoi che accada?

Supponiamo che ci siano parentesi di annidamento? Supponiamo che ci siano parentesi non corrispondenti? Supponiamo che la stringa termini con o inizi con una virgola o due in fila?

Tutte le soluzioni di cui sopra produrrà più o meno spazzatura e non segnalarlo a voi.

Se dipendesse da me, inizierei con una restrizione piuttosto rigorosa su quali dati "corretti" erano - nessuna parentesi di nidificazione, nessuna parentesi non abbinata e nessun segmento vuoto prima, tra o dopo i commenti - convalidare come sono andato e quindi genera un'eccezione se non ero in grado di convalidare.

+1

Dobbiamo supporre che la domanda contenga tutte le informazioni di cui abbiamo bisogno per rispondere. Pertanto assumiamo che l'input sia già stato convalidato e che il formato sia stato descritto completamente (ad esempio, nessuna parentesi annidata). Se qualcuno di questi presupposti risulta errato, si spera che il PO impari a porre domande migliori in futuro. ;) –

1

Questo post mi ha aiutato molto. Stavo cercando di dividere una stringa con virgole posizionate fuori dalle virgolette. L'ho usato come antipasto. La mia ultima riga di codice era regEx = re.compile(r'(?:[^,"]|"[^"]*")+') Questo ha fatto il trucco. Grazie mille.

Problemi correlati