2009-10-30 21 views
20

que tiene una cadena de tal formato:¿Cómo dividir una cadena por comas posicionadas fuera de paréntesis?

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

por lo que es básicamente la lista de los nombres de los actores (opcionalmente seguido por su papel en paréntesis). El rol en sí mismo puede contener coma (el nombre del actor no puede, realmente lo espero).

Mi objetivo es dividir esta cadena en una lista de pares - (actor name, actor role).

Una solución obvia sería la de ir a través de cada personaje, la verificación de ocurrencias de '(', ')' y ',', que se dividió cada vez que una coma occures exterior. Pero esto parece un poco pesado ...

Estaba pensando en spliting usando una expresión regular: en primer lugar dividir la cadena mediante paréntesis:

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

Los elementos impares aquí son nombres de actores, incluso son los papeles. Entonces podría dividir los nombres por comas y de alguna manera extraer los pares nombre-rol. Pero esto parece incluso peor que mi primer enfoque.

¿Hay alguna manera más fácil/más agradable de hacer esto, ya sea con una sola expresión regular o una buena pieza de código?

Respuesta

18

Una forma de hacerlo es utilizar findall con una regex que combina con avidez cosas que pueden ir entre separadores. por ejemplo:

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

la expresión regular anterior coincide con uno o más:

  • no comas, los caracteres no-open-paren
  • cadenas que comienzan con un paréntesis de apertura, contienen 0 o más no close-parens, y luego una paren cercana

Una peculiaridad de este enfoque es que los separadores adyacentes se tratan como un único separador. Es decir, no verá una cadena vacía. Eso puede ser un error o una función dependiendo de su caso de uso.

También tenga en cuenta que las expresiones regulares son no adecuadas para los casos en que la anidación es una posibilidad.Así, por ejemplo, esto sería dividir incorrectamente:

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

Si tiene que hacer frente a anidar la mejor opción sería la partición de la cadena en parens, comas, y todo lo demás (esencialmente tokenizar que - esta parte podría todavía hacerse con expresiones regulares) y luego caminar a través de esas fichas reensamblando los campos, haciendo un seguimiento de su nivel de anidación a medida que avanza (este seguimiento del nivel de anidación es lo que las expresiones regulares son incapaces de hacer por sí mismos).

+1

Se puede dividir en campos de inmediato, haciendo coincidir los registros en lugar de separadores: [(m.group ("nombre"), m.group ("papel")) para m de re.findall ("(? P . +?) (? \ ((? P [^ \)] +) \) (, \ s * | $)) ", x)] –

+0

+1 para la solución de token si la necesita. Aparece dentro y fuera de la pila mientras camina hacia arriba y hacia abajo ... una forma clásica de hacerlo. –

+2

cada vez que veo la expresión regular que es útil, como esta, empiezo a preguntarme: ¿deberían ser legibles por los humanos? O solo soy yo ... ¿quién no lo ve desde el primer vistazo? – kender

5

Creo que la mejor manera de abordar esto sería utilizar el módulo integrado csv de python.

Debido a que el módulo csv solamente un allows un carácter quotechar, se tendría que hacer un reemplazo en sus entradas para convertir () a algo así como | o ". Luego asegúrate de estar usando un dialecto apropiado y listo.

0

Ciertamente estoy de acuerdo con @Wogan anterior, que el uso del moudle CSV es un buen enfoque. Una vez dicho esto si todavía quiere probar una solución de expresiones regulares seguirlo, pero se tendrá que adaptarlo a Python dialecto

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

HTH

1

Mi respuesta no usará regex.

Creo que el escáner de caracteres simple con el estado "in_actor_name" debería funcionar. Recuerde que el estado "in_actor_name" termina con ')' o con una coma en este estado.

Mi intento:

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) 

Salida:

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

dividida por ")"

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

que puede hacer la comprobación adicional para conseguir esos nombres que no vienen con().

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

La búsqueda hacia delante coincide con todo hasta la siguiente paréntesis de apertura o al final de la cadena, si y sólo si no hay primer paréntesis en el medio. Eso asegura que la coma no está dentro de un paréntesis.

2

Un intento de expresiones regulares legible:

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) 

Salida:

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

Regex legible por humanos: ¿no es eso un oxímoron? – Amarghosh

0

Aquí está una técnica general que he utilizado en el pasado para estos casos:

Uso del sub función del módulo re con una función como argumento de reemplazo. La función realiza un seguimiento de apertura y cierre de parens, corchetes y llaves, así como comillas simples y dobles, y realiza un reemplazo solo fuera de dichas subcadenas entre corchetes y entre comillas. A continuación, puede reemplazar las comas sin corchetes/comillas con otro carácter que está seguro que no aparece en la cadena (uso el código ASCII/Unicode group-separator: chr (29)), luego haga una cadena simple. dividir en ese personaje. Aquí está el código:

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 

Si usted no tiene nonlocal en su versión de Python, simplemente cambiarlo a global y definir level y qtflags a nivel de módulo.

Así es como se usa:

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

Ninguna de las respuestas anteriores son correctas si hay algún error o ruido en los datos.

Es fácil encontrar una buena solución si sabe que los datos son correctos todo el tiempo. Pero, ¿qué sucede si hay errores de formato? ¿Qué quieres que pase?

Supongamos que hay paréntesis de anidación? Supongamos que hay paréntesis sin par? Supongamos que la cadena termina con o comienza con una coma, o tiene dos en una fila?

Todas las soluciones anteriores producirán más o menos basura y no se lo informarán.

Depende de mí, comenzaría con una restricción bastante estricta sobre qué datos "correctos" - sin paréntesis de anidamiento, sin paréntesis sin par, y sin segmentos vacíos antes, entre o después de los comentarios - validar como fui , y luego generar una excepción si no pude validar.

+1

Debemos asumir que la pregunta contiene toda la información que necesitamos para responderla. Por lo tanto, suponemos que la entrada ya se ha validado y que el formato se ha descrito por completo (p. Ej., No hay paréntesis anidados). Si cualquiera de esas suposiciones resulta ser incorrecta, se espera que el OP aprenda a hacer mejores preguntas en el futuro. ;) –

1

Esta publicación me ayudó mucho. Estaba buscando dividir una cadena por comas ubicadas fuera de las comillas. Lo usé como un titular. Mi última línea de código fue regEx = re.compile(r'(?:[^,"]|"[^"]*")+') Esto hizo el truco. Gracias una tonelada.

Cuestiones relacionadas