Dividir uma string por espaços - preservando substrings entre aspas - em Python

269

Eu tenho uma string que é assim:

this is "a test"

Estou tentando escrever algo em Python para dividir por espaço, ignorando espaços entre aspas. O resultado que estou procurando é:

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

PS. Eu sei que você vai perguntar "o que acontece se houver aspas dentro das aspas, bem, no meu pedido, isso nunca acontecerá.

Adam Pierce
fonte
1
Obrigado por fazer esta pergunta. É exatamente o que eu precisava para consertar o módulo de compilação pypar.
Martlark

Respostas:

393

Você deseja split, a partir do shlexmódulo embutido .

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

Isso deve fazer exatamente o que você deseja.

Jerub
fonte
13
Use "posix = False" para preservar as cotações. shlex.split('this is "a test"', posix=False)retorna['this', 'is', '"a test"']
Boon
@MatthewG. A "correção" no Python 2.7.3 significa que a passagem de uma string unicode para shlex.split()acionará uma UnicodeEncodeErrorexceção.
Rockallite 1/11/19
57

Dê uma olhada no shlexmódulo, particularmente shlex.split.

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

Vejo abordagens regex aqui que parecem complexas e / ou erradas. Isso me surpreende, porque a sintaxe do regex pode descrever facilmente "espaço em branco ou entre aspas", e a maioria dos mecanismos de regex (incluindo o Python) podem dividir-se em um regex. Portanto, se você usará expressões regulares, por que não dizer exatamente o que você quer dizer ?:

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

Explicação:

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

O shlex provavelmente fornece mais recursos, no entanto.


fonte
1
Eu estava pensando da mesma forma, mas em vez disso sugeriria [t.strip ('"') para t em re.findall (r '[^ \ s"] + | "[^"] * "', 'este é" a test "')]
Darius Bacon
2
+1 Estou usando isso porque foi muito mais rápido que o shlex.
21119 hanleyp
3
Por que a barra invertida tripla? uma barra invertida simples não fará o mesmo?
Doppelganger
1
Na verdade, uma coisa que não gosto sobre isso é que qualquer coisa antes / depois das aspas não é dividida corretamente. Se eu tiver uma string como esta 'PARAMS val1 = "Thing" val2 = "Thing2"'. Espero que a string se divida em três partes, mas se divide em 5. Já faz um tempo desde que eu fiz o regex, então não sinto vontade de tentar resolvê-lo usando sua solução agora.
LeetNightshade
1
Você deve usar cadeias brutas ao usar expressões regulares.
asmeurer
29

Dependendo do seu caso de uso, você também pode conferir o csvmódulo:

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

Resultado:

['this', 'is', 'a string']
['and', 'more', 'stuff']
Ryan Ginstrom
fonte
2
útil, quando shlex tiras alguns caracteres necessários
scraplesh
1
Do CSV uso duas aspas duplas em uma linha (como em side-by-side, "") para representar um aspas duplas ", então vai virar duas aspas duplas em uma única citação 'this is "a string""'e 'this is "a string"""'será tanto mapa para['this', 'is', 'a string"']
Boris
15

Eu uso o shlex.split para processar 70.000.000 linhas de log do squid, é muito lento. Então eu mudei para re.

Por favor, tente isso, se você tiver problemas de desempenho com o shlex.

import re

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

Como esta pergunta está marcada com regex, decidi tentar uma abordagem regex. Primeiro substituo todos os espaços nas partes das aspas por \ x00, depois divido por espaços, depois substituo o \ x00 pelos espaços em cada parte.

Ambas as versões fazem a mesma coisa, mas o splitter é um pouco mais legível que o 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
fonte
Você deveria ter usado o re.Scanner. É mais confiável (e, de fato, implementei um shlex-like usando o re.Scanner).
Devin Jeanpierre
+1 Hm, essa é uma ideia bastante inteligente, dividindo o problema em várias etapas, para que a resposta não seja terrivelmente complexa. Shlex não fez exatamente o que eu precisava, mesmo tentando ajustá-lo. E as soluções regex de passagem única estavam ficando realmente estranhas e complicadas.
LeetNightshade
6

Parece que, por razões de desempenho, reé mais rápido. Aqui está minha solução usando um operador menos ganancioso que preserva as aspas externas:

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

Resultado:

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

Deixa construções como aaa"bla blub"bbbjuntas, pois esses tokens não são separados por espaços. Se a sequência contiver caracteres de escape, você poderá corresponder assim:

>>> 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.\""

Observe que isso também corresponde à string vazia ""por meio da \Sparte do padrão.

hochl
fonte
1
Outra vantagem importante desta solução é sua versatilidade em relação ao caráter delimitante (por exemplo, ,via '(?:".*?"|[^,])+'). O mesmo se aplica aos caracteres de citação (anexos).
a_guest
4

O principal problema com a shlexabordagem aceita é que ela não ignora caracteres de escape fora das substrings citadas e fornece resultados ligeiramente inesperados em alguns casos de canto.

Eu tenho o seguinte caso de uso, em que preciso de uma função de divisão que divida as seqüências de entrada, de forma que as subseqüências entre aspas simples ou duplas sejam preservadas, com a capacidade de escapar aspas dentro dessa subseqüência. As aspas dentro de uma sequência não citada não devem ser tratadas de maneira diferente de qualquer outro caractere. Alguns exemplos de casos de teste com a saída esperada:

string de entrada | resultado esperado
=================================================
 '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 \ def s ghi" | ['abc \\ s def', 'ghi']
 '"abc \\ s def" ghi' | ['abc \\ s def', 'ghi']
 '"" teste' | ['', 'teste']
 "'' teste" | ['', 'teste']
 "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 $ '"]

Eu terminei com a seguinte função para dividir uma string de modo que a saída esperada resulte em todas as strings 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)]

O aplicativo de teste a seguir verifica os resultados de outras abordagens ( shlexe csvpor enquanto) e a implementação da divisão 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)')

Resultado:

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 -> exceção: sem cotação de fechamento
[OK] 'abc \ s def' ghi -> ['abc \ s def', 'ghi']
[OK] "abc \ s def" ghi -> ['abc \\ s def', 'ghi']
[OK] "" teste -> ['', 'teste']
[OK] '' teste -> ['', 'teste']
[FAIL] abc'def -> exceção: sem cotação de fechamento
[FAIL] abc'def '-> [' abcdef ']
[FAIL] abc'def 'ghi -> [' abcdef ',' ghi ']
[FAIL] abc'def'ghi -> ['abcdefghi']
[FAIL] abc "def -> exceção: sem cotação de fechamento
[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', 'def']
[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] "" teste -> ['', 'teste']
[FAIL] '' test -> ["''", 'test']
[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 $'"]

ré

[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] "" teste -> ['', 'teste']
[OK] '' teste -> ['', 'teste']
[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 iteração
csv: 0,030ms por iteração
re: 0,049ms por iteração

Portanto, o desempenho é muito melhor shlexe pode ser aprimorado ainda mais pré-compilando a expressão regular, caso em que superará a csvabordagem.

Ton van den Heuvel
fonte
Não sei ao certo o que você está falando: `` >>> shlex.split ('este é "um teste"') ['this', 'is', 'a test'] >>> shlex.split (' este é \\ "um teste \\" ') [' este ',' é ',' "um ',' teste" '] >>> shlex.split (' este é "um \\" teste \\ " "') [' this ',' is ',' a" test "']` ``
morsik 15/05/19
@morsik, qual é o seu ponto? Talvez o seu caso de uso não corresponda ao meu? Quando você olha para os casos de teste, verá todos os casos em shlexque não se comporta conforme o esperado para meus casos de uso.
Ton van den Heuvel
3

Para preservar aspas, use esta função:

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
fonte
Ao comparar com uma string maior, sua função é muito lenta.
Faran2007
3

Teste de velocidade de respostas diferentes:

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

Hmm, parece que não consigo encontrar o botão "Responder" ... de qualquer maneira, esta resposta é baseada na abordagem de Kate, mas divide corretamente as seqüências de caracteres com substrings contendo aspas escapadas e também remove as aspas de início e fim das substrings:

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

Isso funciona em strings como 'This is " a \\\"test\\\"\\\'s substring"'(a marcação insana é infelizmente necessária para impedir que o Python remova as fugas).

Se as fugas resultantes nas seqüências de caracteres na lista retornada não forem desejadas, você poderá usar esta versão ligeiramente alterada da função:

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

fonte
1

Para contornar os problemas unicode em algumas versões do Python 2, sugiro:

from shlex import split as _split
split = lambda a: [b.decode('utf-8') for b in _split(a.encode('utf-8'))]
Moschlar
fonte
Para o python 2.7.5, deve ser: split = lambda a: [b.decode('utf-8') for b in _split(a)]caso contrário, você obtém:UnicodeDecodeError: 'ascii' codec can't decode byte ... in position ...: ordinal not in range(128)
Peter Varo
1

Como opção, tente o tssplit:

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

Eu sugiro:

cadeia de teste:

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

para capturar também "" e '':

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

resultado:

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

ignorar vazio "" e '':

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

resultado:

['abc', '"ad"', "'fg'", '"kk\'rdt\'"', 'zzz', '"34"', 'zzz']
hussic
fonte
Pode ser escrito como re.findall("(?:\".*?\"|'.*?'|[^\s'\"]+)", s)também.
hochl
-3

Se você não se importa com sub-strings, basta

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

Atuação:

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

Ou módulo de string

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

Desempenho: o módulo String parece ter um desempenho melhor que os métodos String

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

Ou você pode usar o mecanismo RE

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

atuação

>>> 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 cadeias muito longas, você não deve carregar a cadeia inteira na memória e, em vez disso, dividir as linhas ou usar um loop iterativo

Gregory
fonte
11
Você parece ter esquecido o ponto principal da questão. Existem seções citadas na string que não precisam ser divididas.
Rjmunro 31/10/08
-3

Tente o seguinte:

  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

Algumas seqüências de teste:

'This is "a test"' -> ['This', 'is', 'a test']
'"This is \'a test\'"' -> ["This is 'a test'"]
pjz
fonte
Forneça o repr de uma string que você acha que falhará.
pjz
Pense ? adamsplit("This is 'a test'")['This', 'is', "'a", "test'"]
Matthew Schinckel
O OP diz apenas "entre aspas" e só possui um exemplo com aspas duplas.
pjz