it-swarm-eu.dev

Dividere una stringa per spazi - preservando sottostringhe quotate - in Python

Ho una stringa che è così:

this is "a test"

Sto cercando di scrivere qualcosa in Python per suddividerlo dallo spazio ignorando gli spazi tra virgolette. Il risultato che sto cercando è:

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

PS. So che stai per chiedere "cosa succede se ci sono citazioni tra virgolette, beh, nella mia domanda, che non accadrà mai.

223
Adam Pierce

Vuoi dividere, dal modulo shlex .

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

Questo dovrebbe fare esattamente quello che vuoi.

342
Jerub

Dai un'occhiata al modulo shlex, in particolare shlex.split.

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

Vedo approcci regex qui che sembrano complessi e/o sbagliati. Questo mi sorprende, perché la sintassi regex può facilmente descrivere "spazi bianchi o cose circondate da virgolette", e la maggior parte dei motori regex (incluso Python) può essere divisa in un'espressione regolare. Quindi, se userai espressioni regex, perché non dire esattamente cosa intendi ?:

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

Spiegazione:

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

shlex probabilmente fornisce più funzionalità, però.

31
Kate

A seconda del tuo caso d'uso, potresti anche voler controllare il modulo csv:

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

Produzione: 

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

Io uso shlex.split per elaborare 70.000.000 di righe di calamaro, è così lento. Così sono passato a re.

Per favore prova questo, se hai problemi di prestazioni con shlex.

import re

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

Poiché questa domanda è contrassegnata con regex, ho deciso di provare un approccio regex. Prima rimpiazzo tutti gli spazi nelle parti delle citazioni con\x00, quindi diviso per spazi, quindi rimpiazziamo\x00 in spazi in ciascuna parte.

Entrambe le versioni fanno la stessa cosa, ma lo splitter è un po 'più leggibile rispetto allo 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

Per conservare le virgolette usa questa funzione:

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

Test di velocità di risposte diverse:

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

Sembra che per motivi di prestazioni re sia più veloce. Ecco la mia soluzione utilizzando un operatore meno avido che conserva le citazioni esterne:

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

Risultato:

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

Lascia i costrutti come aaa"bla blub"bbb insieme poiché questi token non sono separati da spazi. Se la stringa contiene caratteri di escape, puoi farlo in questo modo:

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

Si noti che questo corrisponde anche alla stringa vuota "" tramite la parte \S del pattern.

2
hochl

Per aggirare i problemi Unicode in alcune versioni di Python 2, suggerisco:

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

Il problema principale con l'approccio shlex accettato è che non ignora i caratteri di escape al di fuori delle sottostringhe quotate e fornisce risultati leggermente inaspettati in alcuni casi d'angolo.

Ho il seguente caso d'uso, in cui ho bisogno di una funzione di divisione che divida le stringhe di input in modo tale che vengano conservate sottostringhe con quotatura singola o con quotazione doppia, con la possibilità di evadere le virgolette all'interno di tale sottostringa. Le virgolette all'interno di una stringa non quotata non dovrebbero essere trattate in modo diverso da qualsiasi altro carattere. Alcuni esempi di casi di test con l'output previsto:

  stringa di input | output atteso 
 ======================================== == 
 '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'] 
 '"" test' | ['', 'test'] 
 "test" " ['', 'test'] 
 "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 $' "]

Ho finito con la seguente funzione per dividere una stringa in modo tale che i risultati di output previsti per tutte le stringhe di input:

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 seguente applicazione di test controlla i risultati di altri approcci (shlex e csv per ora) e l'implementazione della divisione personalizzata:

#!/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)')

Produzione:

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 -> eccezione: Nessuna quotazione di chiusura 
 [OK]' abc\s def 'ghi -> [' abc \\ s def ',' ghi '] 
 [OK] "abc\s def" ghi -> [' abc \\ s def ',' ghi '] 
 [ OK] "" test -> ['', 'test'] 
 [OK] '' test -> ['', 'test'] 
 [FAIL] abc'def -> eccezione: nessuna quotazione di chiusura 
 [FAIL] abc'def '-> [' abcdef '] 
 [FAIL] abc'def' ghi -> ['abcdef', 'ghi'] 
 [FAIL] abc'def'ghi -> ['abcdefghi'] 
 [FAIL] abc "def -> eccezione: nessuna quotazione finale 
 [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] "" test -> [' ',' test '] 
 [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 $ '"] 

 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 -> [" a B c D e F", ' ghi '] 
 [OK]' abc\s def 'ghi -> [' abc \\ s def ',' ghi '] 
 [OK] "abc\s def" ghi -> [' abc\\ s def ',' ghi '] 
 [OK] "" test -> [' ',' test '] 
 [OK]' '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 $ '"] 

 shlex: 0.281ms per iterazione 
 csv: 0.030ms per iterazione
re: 0.049ms per iterazione

Quindi le prestazioni sono molto meglio di shlex e possono essere ulteriormente migliorate precompilando l'espressione regolare, nel qual caso supererà l'approccio csv.

1

I problemi di unicode con shlex discussi sopra (top answer) sembrano essere risolti (indirettamente) in 2.7.2+ come per http://bugs.python.org/issue6988#msg146200

(risposta separata perché non posso commentare)

1
Tyris

Hmm, non riesco a trovare il pulsante "Rispondi" ... in ogni caso, questa risposta è basata sull'approccio di Kate, ma divide le stringhe correttamente con sottostringhe contenenti citazioni di escape e rimuove anche le virgolette di inizio e fine delle sottostringhe:

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

Funziona su stringhe come 'This is " a \\\"test\\\"\\\'s substring"' (il markup insane è purtroppo necessario per impedire a Python di rimuovere gli escape).

Se gli escape risultanti nelle stringhe nell'elenco restituito non sono desiderati, è possibile utilizzare questa versione leggermente modificata della funzione:

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

Suggerisco:

stringa di prova:

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

per catturare anche "" e "":

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

risultato:

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

ignorare vuoti "" e "":

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

risultato:

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