it-swarm-es.tech

Dividir una cadena por espacios - preservando las subcadenas entre comillas - en Python

Tengo una cadena que es así:

this is "a test"

Estoy tratando de escribir algo en Python para dividirlo por espacio mientras ignoro los espacios entre comillas. El resultado que estoy buscando es:

['this','is','a test']

PD. Sé que va a preguntar "qué sucede si hay citas entre comillas, bueno, en mi solicitud, eso nunca sucederá.

232
Adam Pierce

Quieres dividir, desde el módulo shlex .

>>> import shlex
>>> shlex.split('this is "a test"')
['this', 'is', 'a test']

Esto debería hacer exactamente lo que quieres.

350
Jerub

Eche un vistazo al módulo shlex, particularmente a shlex.split.

>>> import shlex
>>> shlex.split('This is "a test"')
['This', 'is', 'a test']
54
Allen

Veo enfoques de expresiones regulares aquí que parecen complejos y/o incorrectos. Esto me sorprende, porque la sintaxis de expresiones regulares puede describir fácilmente "espacios en blanco o comillas entre comillas", y la mayoría de los motores de expresiones regulares (incluido el de Python) se pueden dividir en una expresión regular. Entonces, si vas a usar expresiones regulares, ¿por qué no simplemente decir exactamente lo que quieres decir?

test = 'this is "a test"'  # or "this is 'a test'"
# pieces = [p for p in re.split("( |[\\\"'].*[\\\"'])", test) if p.strip()]
# From comments, use this:
pieces = [p for p in re.split("( |\\\".*?\\\"|'.*?')", test) if p.strip()]

Explicación:

[\\\"'] = double-quote or single-quote
.* = anything
( |X) = space or X
.strip() = remove space and empty-string separators

shlex probablemente proporciona más características, sin embargo.

33
Kate

Dependiendo de su caso de uso, es posible que también desee revisar el módulo csv:

import csv
lines = ['this is "a string"', 'and more "stuff"']
for row in csv.reader(lines, delimiter=" "):
    print row

Salida:

['this', 'is', 'a string']
['and', 'more', 'stuff']
25
Ryan Ginstrom

Utilizo shlex.split para procesar 70,000,000 de líneas de registro de calamar, es muy lento. Así que me cambié a re.

Por favor intente esto, si tiene problemas de rendimiento con shlex.

import re

def line_split(line):
    return re.findall(r'[^"\s]\S*|".+?"', line)
12
Daniel Dai

Como esta pregunta está etiquetada con expresiones regulares, decidí probar un enfoque de expresiones regulares. Primero reemplacé todos los espacios en las partes de comillas con\x00, luego dividí por espacios, luego reemplacé\x00 de nuevo a espacios en cada parte.

Ambas versiones hacen lo mismo, pero splitter es un poco más legible que splitter2.

import re

s = 'this is "a test" some text "another test"'

def splitter(s):
    def replacer(m):
        return m.group(0).replace(" ", "\x00")
    parts = re.sub('".+?"', replacer, s).split()
    parts = [p.replace("\x00", " ") for p in parts]
    return parts

def splitter2(s):
    return [p.replace("\x00", " ") for p in re.sub('".+?"', lambda m: m.group(0).replace(" ", "\x00"), s).split()]

print splitter2(s)
8
elifiner

Parece que por razones de rendimiento re es más rápido. Aquí está mi solución utilizando un operador menos codicioso que conserva las citas externas:

re.findall("(?:\".*?\"|\S)+", s)

Resultado:

['this', 'is', '"a test"']

Deja construcciones como aaa"bla blub"bbb juntas ya que estos tokens no están separados por espacios. Si la cadena contiene caracteres escapados, puedes hacer coincidir así:

>>> a = "She said \"He said, \\\"My name is Mark.\\\"\""
>>> a
'She said "He said, \\"My name is Mark.\\""'
>>> for i in re.findall("(?:\".*?[^\\\\]\"|\S)+", a): print(i)
...
She
said
"He said, \"My name is Mark.\""

Tenga en cuenta que esto también coincide con la cadena vacía "" por medio de la parte \S del patrón.

5
hochl

Para preservar las cotizaciones use esta función:

def getArgs(s):
    args = []
    cur = ''
    inQuotes = 0
    for char in s.strip():
        if char == ' ' and not inQuotes:
            args.append(cur)
            cur = ''
        Elif char == '"' and not inQuotes:
            inQuotes = 1
            cur += char
        Elif char == '"' and inQuotes:
            inQuotes = 0
            cur += char
        else:
            cur += char
    args.append(cur)
    return args
3
THE_MAD_KING

Prueba de velocidad de diferentes respuestas:

import re
import shlex
import csv

line = 'this is "a test"'

%timeit [p for p in re.split("( |\\\".*?\\\"|'.*?')", line) if p.strip()]
100000 loops, best of 3: 5.17 µs per loop

%timeit re.findall(r'[^"\s]\S*|".+?"', line)
100000 loops, best of 3: 2.88 µs per loop

%timeit list(csv.reader([line], delimiter=" "))
The slowest run took 9.62 times longer than the fastest. This could mean that an intermediate result is being cached.
100000 loops, best of 3: 2.4 µs per loop

%timeit shlex.split(line)
10000 loops, best of 3: 50.2 µs per loop
2
har777

El problema principal con el enfoque aceptado shlex es que no ignora los caracteres de escape que se encuentran fuera de las subcadenas citadas, y da resultados ligeramente inesperados en algunos casos.

Tengo el siguiente caso de uso, en el que necesito una función de división que divide las cadenas de entrada de manera que se conserven las subcadenas entre comillas simples o entre comillas dobles, con la capacidad de evitar las comillas dentro de dicha subcadena. Las citas dentro de una cadena sin comillas no deben tratarse de manera diferente a cualquier otro carácter. Algunos ejemplos de casos de prueba con el resultado esperado:

 cadena de entrada | salida esperada 
 =========================================== ==== 
 'abc def' | ['abc', 'def'] 
 "abc \\ s def" | ['abc', '\\ s', 'def'] 
 '"abc def" ghi' | ['abc def', 'ghi'] 
 "'abc def' ghi" | ['abc def', 'ghi'] 
 '"abc \\" def "ghi' | ['abc" def', 'ghi'] 
 "'abc \\' def ' ghi "| ["abc 'def",' ghi '] 
 "' abc \\ s def 'ghi" | ['abc \\ s def', 'ghi'] 
 '"abc \\ s def" ghi' | ['abc \\ s def', 'ghi'] 
 '"" test' | ['', 'prueba'] 
 "'' prueba" | ['', 'prueba'] 
 "abc'def" | ["abc'def"] 
 "abc'def '" | ["abc'def '"] 
 "abc'def' ghi" | ["abc'def '",' ghi '] 
 "abc'def'ghi" | ["abc'def'ghi"] 
 'abc "def' | ['abc" def'] 
 'abc "def"' | ['abc "def"'] 
 'abc "def" ghi' | ['abc "def"', 'ghi'] 
 'abc "def" ghi' | ['abc "def" ghi'] 
 "r'AA 'r'. * _ xyz $ '" | ["r'AA '", "r'. * _ xyz $ '"]

Terminé con la siguiente función para dividir una cadena de manera que los resultados esperados resulten para todas las cadenas de entrada:

import re

def quoted_split(s):
    def strip_quotes(s):
        if s and (s[0] == '"' or s[0] == "'") and s[0] == s[-1]:
            return s[1:-1]
        return s
    return [strip_quotes(p).replace('\\"', '"').replace("\\'", "'") \
            for p in re.findall(r'"(?:\\.|[^"])*"|\'(?:\\.|[^\'])*\'|[^\s]+', s)]

La siguiente aplicación de prueba verifica los resultados de otros enfoques (shlex y csv por ahora) y la implementación dividida personalizada:

#!/bin/python2.7

import csv
import re
import shlex

from timeit import timeit

def test_case(fn, s, expected):
    try:
        if fn(s) == expected:
            print '[ OK ] %s -> %s' % (s, fn(s))
        else:
            print '[FAIL] %s -> %s' % (s, fn(s))
    except Exception as e:
        print '[FAIL] %s -> exception: %s' % (s, e)

def test_case_no_output(fn, s, expected):
    try:
        fn(s)
    except:
        pass

def test_split(fn, test_case_fn=test_case):
    test_case_fn(fn, 'abc def', ['abc', 'def'])
    test_case_fn(fn, "abc \\s def", ['abc', '\\s', 'def'])
    test_case_fn(fn, '"abc def" ghi', ['abc def', 'ghi'])
    test_case_fn(fn, "'abc def' ghi", ['abc def', 'ghi'])
    test_case_fn(fn, '"abc \\" def" ghi', ['abc " def', 'ghi'])
    test_case_fn(fn, "'abc \\' def' ghi", ["abc ' def", 'ghi'])
    test_case_fn(fn, "'abc \\s def' ghi", ['abc \\s def', 'ghi'])
    test_case_fn(fn, '"abc \\s def" ghi', ['abc \\s def', 'ghi'])
    test_case_fn(fn, '"" test', ['', 'test'])
    test_case_fn(fn, "'' test", ['', 'test'])
    test_case_fn(fn, "abc'def", ["abc'def"])
    test_case_fn(fn, "abc'def'", ["abc'def'"])
    test_case_fn(fn, "abc'def' ghi", ["abc'def'", 'ghi'])
    test_case_fn(fn, "abc'def'ghi", ["abc'def'ghi"])
    test_case_fn(fn, 'abc"def', ['abc"def'])
    test_case_fn(fn, 'abc"def"', ['abc"def"'])
    test_case_fn(fn, 'abc"def" ghi', ['abc"def"', 'ghi'])
    test_case_fn(fn, 'abc"def"ghi', ['abc"def"ghi'])
    test_case_fn(fn, "r'AA' r'.*_xyz$'", ["r'AA'", "r'.*_xyz$'"])

def csv_split(s):
    return list(csv.reader([s], delimiter=' '))[0]

def re_split(s):
    def strip_quotes(s):
        if s and (s[0] == '"' or s[0] == "'") and s[0] == s[-1]:
            return s[1:-1]
        return s
    return [strip_quotes(p).replace('\\"', '"').replace("\\'", "'") for p in re.findall(r'"(?:\\.|[^"])*"|\'(?:\\.|[^\'])*\'|[^\s]+', s)]

if __== '__main__':
    print 'shlex\n'
    test_split(shlex.split)
    print

    print 'csv\n'
    test_split(csv_split)
    print

    print 're\n'
    test_split(re_split)
    print

    iterations = 100
    setup = 'from __main__ import test_split, test_case_no_output, csv_split, re_split\nimport shlex, re'
    def benchmark(method, code):
        print '%s: %.3fms per iteration' % (method, (1000 * timeit(code, setup=setup, number=iterations) / iterations))
    benchmark('shlex', 'test_split(shlex.split, test_case_no_output)')
    benchmark('csv', 'test_split(csv_split, test_case_no_output)')
    benchmark('re', 'test_split(re_split, test_case_no_output)')

Salida:

shlex 
 
 [OK] abc def -> ['abc', 'def'] 
 [FAIL] abc\s def -> ['abc', 's', 'def'] 
 [OK] "abc def" ghi -> ['abc def', 'ghi'] 
 [OK] 'abc def' ghi -> ['abc def', 'ghi'] 
 [OK] "abc \" def "ghi -> ['abc" def', 'ghi'] 
 [FAIL] 'abc \' def 'ghi -> excepción : Sin comillas de cierre 
 [OK] 'abc\s def' ghi -> ['abc \\ s def', 'ghi'] 
 [OK] "abc\s def" ghi - > ['abc \\ s def', 'ghi'] 
 [OK] "" test -> ['', 'test'] 
 [OK] '' test -> [' ',' prueba '] 
 [FAIL] abc'def -> excepción: No hay cita de cierre 
 [FAIL] abc'def' -> ['abcdef'] 
 [FAIL ] abc'def 'ghi -> [' abcdef ',' ghi '] 
 [FAIL] abc'def'ghi -> [' abcdefghi '] 
 [FAIL] abc "def -> excepción: no hay una cita de cierre 
 [FAIL] abc "def" -> ['abcdef'] 
 [FAIL] abc "def" ghi -> ['abcdef', 'ghi'] 
 [FAIL] abc "def" ghi -> ['abcdefghi'] 
 [FAIL] r'AA 'r'. * _ Xyz $ '-> [' rAA ',' r. * _ Xyz $ '] 
 
 csv 
 
 [OK] abc def -> [' abc ',' de f '] 
 [OK] abc\s def -> [' abc ',' \\ s ',' def '] 
 [OK] "abc def" ghi -> [' abc def ',' ghi '] 
 [FAIL]' abc def 'ghi -> ["' abc", "def '",' ghi '] 
 [FAIL] "abc \" def "ghi -> ['abc \\', 'def"', 'ghi'] 
 [FAIL] 'abc \' def 'ghi -> ["' abc", "\\ '", " def '",' ghi '] 
 [FAIL]' abc\s def 'ghi -> ["' abc ", '\\ s'," def '",' ghi '] 
 [OK] "abc\s def" ghi -> ['abc \\ s def', 'ghi'] 
 [OK] "" test -> ['', 'test'] 
 [FAIL] '' prueba -> ["''", 'prueba'] 
 [OK] abc'def -> ["abc'def"] 
 [OK] abc ' def '-> ["abc'def'"] 
 [OK] abc'def 'ghi -> ["abc'def'", 'ghi'] 
 [OK] abc'def 'ghi -> ["abc'def'ghi"] 
 [OK] abc "def -> [' abc" def '] 
 [OK] abc "def" -> [' abc "def" '] 
 [OK] abc "def" ghi -> [' abc "def" ',' ghi '] 
 [OK] abc "def" ghi -> [' abc "def" ghi '] 
 [OK] r'AA' r '. * _ xyz $' -> ["r'AA '", "r'. * _ xyz $ '"] 
 
 re 
 
 [OK] abc def -> ['abc', 'def'] 
 [OK] abc\s def -> ['abc' , '\\ s', 'def'] 
 [OK] "abc def" ghi -> ['abc def', 'ghi'] 
 [OK] 'abc def' ghi -> ['abc def', 'ghi'] 
 [OK] "abc \" def "ghi -> ['abc" def', 'ghi'] 
 [OK] 'abc \' def 'ghi -> ["abc' def" , 'ghi'] 
 [OK] 'abc\s def' ghi -> ['abc \\ s def', 'ghi'] 
 [OK] "abc\s def" ghi -> ['abc \\ s def', 'ghi'] 
 [OK] "" test -> ['', 'test'] 
 [OK] '' test -> [ '', 'prueba'] 
 [OK] abc'def -> ["abc'def"] 
 [OK] abc'def '-> ["abc'def'"] 
 [OK] abc'def 'ghi -> ["abc'def'", 'ghi'] 
 [OK] abc'def'ghi -> ["abc'def'ghi"] 
 [OK] abc "def -> ['abc" def'] 
 [OK] abc "def" -> ['abc "def"'] 
 [OK] abc "def" ghi -> ['abc "def"', 'ghi'] 
 [OK] abc "def" ghi -> ['abc "def" ghi'] 
 [OK ] r'AA 'r'. * _ xyz $ '-> ["r'AA'", "r '. * _ xyz $'"] 
 
 shlex: 0.281 ms por iteración 
 csv: 0.030ms por iteración 
 re: 0.049ms por iteración

Por lo tanto, el rendimiento es mucho mejor que shlex, y puede mejorarse aún más al precompilar la expresión regular, en cuyo caso superará el enfoque csv.

2
Ton van den Heuvel

Para solucionar los problemas de Unicode en algunas versiones de Python 2, sugiero:

from shlex import split as _split
split = lambda a: [b.decode('utf-8') for b in _split(a.encode('utf-8'))]
2
moschlar

Los problemas de Unicode con shlex discutidos anteriormente (respuesta principal) parecen resolverse (indirectamente) en 2.7.2+ según http://bugs.python.org/issue6988#msg146200

(respuesta separada porque no puedo comentar)

1
Tyris

Hmm, parece que no puede encontrar el botón "Responder" ... de todos modos, esta respuesta se basa en el enfoque de Kate, pero divide correctamente las cadenas con subcadenas que contienen comillas escapadas y también elimina las comillas de inicio y final de las subcadenas:

  [i.strip('"').strip("'") for i in re.split(r'(\s+|(?<!\\)".*?(?<!\\)"|(?<!\\)\'.*?(?<!\\)\')', string) if i.strip()]

Esto funciona en cadenas como 'This is " a \\\"test\\\"\\\'s substring"' (el marcado demente es desafortunadamente necesario para evitar que Python elimine los escapes).

Si no se desean los escapes resultantes en las cadenas en la lista devuelta, puede usar esta versión ligeramente alterada de la función:

[i.strip('"').strip("'").decode('string_escape') for i in re.split(r'(\s+|(?<!\\)".*?(?<!\\)"|(?<!\\)\'.*?(?<!\\)\')', string) if i.strip()]
1
user261478

Yo sugiero:

cadena de prueba:

s = 'abc "ad" \'fg\' "kk\'rdt\'" zzz"34"zzz "" \'\''

para capturar también "" y '':

import re
re.findall(r'"[^"]*"|\'[^\']*\'|[^"\'\s]+',s)

resultado:

['abc', '"ad"', "'fg'", '"kk\'rdt\'"', 'zzz', '"34"', 'zzz', '""', "''"]

para ignorar el vacío "" y '':

import re
re.findall(r'"[^"]+"|\'[^\']+\'|[^"\'\s]+',s)

resultado:

['abc', '"ad"', "'fg'", '"kk\'rdt\'"', 'zzz', '"34"', 'zzz']
0
hussic