it-swarm-pt.tech

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

Eu tenho uma string que é assim:

this is "a test"

Eu estou tentando escrever algo em Python para dividi-lo pelo 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 citações dentro das aspas, bem, na minha aplicação, isso nunca acontecerá.

223
Adam Pierce

Você quer dividir, a partir do módulo shlex .

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

Isso deve fazer exatamente o que você quer.

342
Jerub

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

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

Eu vejo regex abordagens aqui que parecem complexas e/ou erradas. Isso me surpreende, porque a sintaxe de regex pode facilmente descrever "espaço em branco ou coisa cercada por aspas", e a maioria dos mecanismos de regex (incluindo o Python) pode dividir em um regex. Então, se você usar regexes, 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.

31
Kate

Dependendo do seu caso de uso, você também pode querer verificar o módulo csv:

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

Saída: 

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

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

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

import re

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

Como essa questão é marcada com regex, decidi tentar uma abordagem de regex. Primeiro eu substituo todos os espaços nas partes de cotas com\x00, depois divido por espaços, depois substituo o\x00 de volta aos 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)
8
gooli

Para preservar as cotações, 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
3
THE_MAD_KING

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

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

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

Resultado:

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

Ele deixa construções como aaa"bla blub"bbb juntas, pois esses tokens não são separados por espaços. Se a string contiver caracteres de escape, você pode combinar 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 parte \S do padrão.

2
hochl

Para contornar os problemas de 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'))]
2
moschlar

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

Eu tenho o seguinte caso de uso, onde eu preciso de uma função de divisão que divide as strings de entrada de forma que as substrings de aspas simples ou aspas duplas sejam preservadas, com a capacidade de escapar de aspas dentro de uma substring desse tipo. As citações dentro de uma sequência sem aspas não devem ser tratadas de maneira diferente de qualquer outro caractere. Alguns exemplos de casos de teste com a saída esperada:

  cadeia de entrada | saída 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'] 
 '"" teste' | ['', 'test'] 
 "'' teste" | ['', 'test'] 
 "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 $ '"]

Acabei com a seguinte função para dividir uma string de forma 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 (shlex e csv por agora) e a implementação de 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 __== '__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)')

Saída:

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 : Nenhuma citaçã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 -> [' ',' test '] 
 [FAIL] abc'def -> exceção: Nenhuma cotação de fechamento 
 [FAIL] abc'def' -> ['abcdef'] 
 [FAIL abc'def 'ghi -> [' abcdef ',' ghi '] 
 [FALHA] abc'def'ghi -> [' abcdefghi '] 
 [FAIL] abc "def -> exceção: Nenhuma citação de fechamento 
 [FAIL] abc "def" -> ['abcdef'] 
 [FALHA] abc "def" ghi -> ['abcdef', 'ghi'] 
 [FAIL] abc "def" ghi -> ['abcdefghi'] 
 [FALHA] 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'] 
 [FALHA] '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] '' 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 $ '"] 
 
 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] "" teste -> ['', 'teste'] 
 [OK] '' teste -> [ '', '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 $'"] 
 
 shlex: 0.281ms por iteração [. csv: 0,030ms por iteração 
 re: 0,049ms por iteração

Portanto, o desempenho é muito melhor do que shlex, e pode ser aprimorado ainda mais pré-compilando a expressão regular, caso em que ela superará a abordagem csv.

1
Ton van den Heuvel

Os problemas unicode com shlex discutidos acima (resposta principal) parecem ser resolvidos (indiretamente) em 2.7.2+ conforme http://bugs.python.org/issue6988#msg146200

(resposta separada porque não posso comentar)

1
Tyris

Hmm, parece que não consegue encontrar o botão "Responder" ... de qualquer forma, essa resposta é baseada na abordagem do Kate, mas divide corretamente as strings com substrings contendo aspas escapadas e também remove as aspas inicial e final 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 strings 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()]
1
user261478

Eu sugiro:

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

para ignorar "" e "" vazio:

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

resultado:

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