Dividir una cadena por espacios, preservando las subcadenas citadas, en Python

269

Tengo una cadena que es así:

this is "a test"

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

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

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

Adam Pierce
fuente
1
Gracias por hacer esta pregunta. Es exactamente lo que necesitaba para arreglar el módulo de compilación pypar.
Martlark

Respuestas:

393

Usted quiere split, desde el shlexmódulo incorporado .

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

Esto debería hacer exactamente lo que quieres.

Jerub
fuente
13
Use "posix = False" para preservar las citas. shlex.split('this is "a test"', posix=False)vuelve['this', 'is', '"a test"']
Boon
@MatthewG. El "arreglo" en Python 2.7.3 significa que pasar una cadena Unicode shlex.split()desencadenará una UnicodeEncodeErrorexcepción.
Rockallite
57

Echa un vistazo al shlexmódulo, en particular shlex.split.

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

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 cosas entre comillas", y la mayoría de los motores de expresiones regulares (incluido Python) pueden dividirse en una expresión regular. Entonces, si vas a usar expresiones regulares, ¿por qué no 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 funciones, sin embargo.


fuente
1
Estaba pensando lo mismo, pero sugeriría en su lugar [t.strip ('"') para t en re.findall (r '[^ \ s"] + | "[^"] * "', 'esto es" una prueba "')]
Darius Bacon
2
+1 Estoy usando esto porque fue muchísimo más rápido que shlex.
hanleyp
3
¿Por qué la triple barra invertida? ¿una simple barra invertida no hará lo mismo?
Doppelganger
1
En realidad, una cosa que no me gusta de esto es que las citas antes / después no se dividen correctamente. Si tengo una cadena como esta 'PARAMS val1 = "Thing" val2 = "Thing2"'. Espero que la cadena se divida en tres partes, pero se divide en 5. Ha pasado un tiempo desde que hice regex, por lo que no tengo ganas de tratar de resolverla usando su solución en este momento.
leetNightshade
1
Debe usar cadenas sin procesar cuando use expresiones regulares.
asmeurer
29

Dependiendo de su caso de uso, también puede consultar el csvmódulo:

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']
Ryan Ginstrom
fuente
2
útil, cuando shlex elimina algunos caracteres necesarios
scraplesh
1
Los CSV usan dos comillas dobles seguidas (como una al lado de la otra "") para representar una comilla doble ", por lo que convertirá dos comillas dobles en una comilla simple 'this is "a string""'y 'this is "a string"""'ambas se asignarán a['this', 'is', 'a string"']
Boris el
15

Uso shlex.split para procesar 70,000,000 líneas de registro de calamar, es muy lento. Entonces cambié a re.

Intente esto si tiene problemas de rendimiento con shlex.

import re

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

Como esta pregunta está etiquetada con expresiones regulares, decidí probar un enfoque de expresiones regulares. Primero reemplazo todos los espacios en las partes de comillas con \ x00, luego los separo por espacios, luego reemplazo los \ x00 por 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)
elifiner
fuente
Deberías haber usado re.Scanner en su lugar. Es más confiable (y de hecho he implementado un estilo shlex usando re.Scanner).
Devin Jeanpierre
+1 Hm, esta es una idea bastante inteligente, dividiendo el problema en varios pasos para que la respuesta no sea terriblemente compleja. Shlex no hizo exactamente lo que necesitaba, incluso al tratar de modificarlo. Y las soluciones de expresiones regulares de un solo paso se estaban volviendo realmente extrañas y complicadas.
leetNightshade
6

Parece que por razones de rendimiento rees más rápido. Aquí está mi solución usando un operador menos codicioso que conserva las comillas externas:

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

Resultado:

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

Deja construcciones como aaa"bla blub"bbbjuntas, ya que estos tokens no están separados por espacios. Si la cadena contiene caracteres escapados, puede 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 \Sparte del patrón.

hochl
fuente
1
Otra ventaja importante de esta solución es su versatilidad con respecto al carácter delimitador (p . Ej., ,Vía '(?:".*?"|[^,])+'). Lo mismo se aplica a los caracteres entre comillas.
a_guest
4

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

Tengo el siguiente caso de uso, donde necesito una función dividida que divida las cadenas de entrada de modo que se mantengan las subcadenas entre comillas simples o dobles, con la capacidad de escapar de las comillas dentro de dicha subcadena. Las comillas dentro de una cadena sin comillas no deben tratarse de manera diferente a cualquier otro carácter. Algunos ejemplos de casos de prueba con la salida esperada:

cadena de entrada | Rendimiento esperado
===============================================
 'abc def' | ['a B C D e F']
 "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']
 '"" prueba' | ['', 'prueba']
 "'' prueba" | ['', 'prueba']
 "abc'def" | ["a B C D e F"]
 "abc'def '" | ["a B C D e F'"]
 "abc'def 'ghi" | ["abc'def '",' ghi ']
 "abc'def'ghi" | ["abc'def'ghi"]
 'abc "def' | ['abc" def']
 'abc "def"' | ['a B C D e F"']
 '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 modo que los resultados de salida esperados 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 ( shlexy csvpor ahora) y la implementación de división 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 __name__ == '__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']
[FALLO] 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']
[FALLO] 'abc \' def 'ghi -> excepción: sin presupuesto de cierre
[OK] 'abc \ s def' ghi -> ['abc \\ s def', 'ghi']
[OK] "abc \ s def" ghi -> ['abc \\ s def', 'ghi']
[OK] "" prueba -> ['', 'prueba']
[OK] '' prueba -> ['', 'prueba']
[FAIL] abc'def -> excepción: sin presupuesto de cierre
[FALLO] abc'def '-> [' abcdef ']
[FALLO] abc'def 'ghi -> [' abcdef ',' ghi ']
[FALLO] abc'def'ghi -> ['abcdefghi']
[FAIL] abc "def -> excepción: sin presupuesto de cierre
[FALLO] abc "def" -> ['abcdef']
[FALLO] abc "def" ghi -> ['abcdef', 'ghi']
[FALLO] abc "def" ghi -> ['abcdefghi']
[FALLO] r'AA 'r'. * _ Xyz $ '-> [' rAA ',' r. * _ Xyz $ ']

csv

[OK] abc def -> ['abc', 'def']
[OK] abc \ s def -> ['abc', '\\ s', 'def']
[OK] "abc def" ghi -> ['abc def', 'ghi']
[FALLO] 'abc def' ghi -> ["'abc", "def'", 'ghi']
[FALLO] "abc \" def "ghi -> ['abc \\', 'def"', 'ghi']
[FALLO] 'abc \' def 'ghi -> ["' abc", "\\ '", "def'", 'ghi']
[FALLO] 'abc \ s def' ghi -> ["'abc",' \\ s ', "def'", 'ghi']
[OK] "abc \ s def" ghi -> ['abc \\ s def', 'ghi']
[OK] "" prueba -> ['', 'prueba']
[FALLO] '' 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] "" prueba -> ['', 'prueba']
[OK] '' 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 $'"]

shlex: 0.281ms por iteración
csv: 0.030ms por iteración
re: 0.049ms por iteración

Por lo tanto, el rendimiento es mucho mejor shlexy se puede mejorar aún más mediante la precompilación de la expresión regular, en cuyo caso superará el csvenfoque.

Ton van den Heuvel
fuente
No estoy seguro de lo que estás hablando: `` `>>> shlex.split ('this is" a test "') ['this', 'is', 'a test'] >>> shlex.split (' this is \\ "a test \\" ') [' this ',' is ',' "a ',' test" '] >>> shlex.split (' this is "a \\" test \\ " "') [' this ',' is ',' a" test "']` ``
morsik
@morsik, ¿cuál es tu punto? ¿Quizás su caso de uso no coincide con el mío? Cuando observe los casos de prueba, verá todos los casos en los shlexque no se comporta como se esperaba para mis casos de uso.
Ton van den Heuvel
3

Para preservar las comillas 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
THE_MAD_KING
fuente
Al comparar con una cadena más grande, su función es muy lenta
Faran2007
3

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
har777
fuente
1

Hmm, parece que no puedo 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 finalización 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"'(desafortunadamente, el marcado loco es necesario para evitar que Python elimine los escapes).

Si no se desean los escapes resultantes en las cadenas de 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()]

fuente
1

Para evitar 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'))]
moschlar
fuente
Para Python 2.7.5 esto debería ser: de lo split = lambda a: [b.decode('utf-8') for b in _split(a)]contrario, obtendrá:UnicodeDecodeError: 'ascii' codec can't decode byte ... in position ...: ordinal not in range(128)
Peter Varo
1

Como opción, prueba tssplit:

In [1]: from tssplit import tssplit
In [2]: tssplit('this is "a test"', quote='"', delimiter='')
Out[2]: ['this', 'is', 'a test']
Mikhail Zakharov
fuente
0

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 "" y '' vacíos:

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

resultado:

['abc', '"ad"', "'fg'", '"kk\'rdt\'"', 'zzz', '"34"', 'zzz']
hussic
fuente
Podría escribirse como re.findall("(?:\".*?\"|'.*?'|[^\s'\"]+)", s)también.
hochl
-3

Si no te importan las subcadenas que un simple

>>> 'a short sized string with spaces '.split()

Actuación:

>>> s = " ('a short sized string with spaces '*100).split() "
>>> t = timeit.Timer(stmt=s)
>>> print "%.2f usec/pass" % (1000000 * t.timeit(number=100000)/100000)
171.39 usec/pass

O módulo de cadena

>>> from string import split as stringsplit; 
>>> stringsplit('a short sized string with spaces '*100)

Rendimiento: el módulo de cadena parece funcionar mejor que los métodos de cadena

>>> s = "stringsplit('a short sized string with spaces '*100)"
>>> t = timeit.Timer(s, "from string import split as stringsplit")
>>> print "%.2f usec/pass" % (1000000 * t.timeit(number=100000)/100000)
154.88 usec/pass

O puedes usar el motor RE

>>> from re import split as resplit
>>> regex = '\s+'
>>> medstring = 'a short sized string with spaces '*100
>>> resplit(regex, medstring)

Actuación

>>> s = "resplit(regex, medstring)"
>>> t = timeit.Timer(s, "from re import split as resplit; regex='\s+'; medstring='a short sized string with spaces '*100")
>>> print "%.2f usec/pass" % (1000000 * t.timeit(number=100000)/100000)
540.21 usec/pass

Para cadenas muy largas, no debe cargar toda la cadena en la memoria y, en su lugar, dividir las líneas o usar un bucle iterativo

Gregory
fuente
11
Parece que has perdido todo el punto de la pregunta. Hay secciones citadas en la cadena que no deben dividirse.
rjmunro
-3

Prueba esto:

  def adamsplit(s):
    result = []
    inquotes = False
    for substring in s.split('"'):
      if not inquotes:
        result.extend(substring.split())
      else:
        result.append(substring)
      inquotes = not inquotes
    return result

Algunas cadenas de prueba:

'This is "a test"' -> ['This', 'is', 'a test']
'"This is \'a test\'"' -> ["This is 'a test'"]
pjz
fuente
Proporcione la repr de una cadena que cree que fallará.
pjz
Pensar ? adamsplit("This is 'a test'")['This', 'is', "'a", "test'"]
Matthew Schinckel
OP solo dice "entre comillas" y solo tiene un ejemplo con comillas dobles.
pjz