Główny problem z przyjętym shlex
podejściem polega na tym, że nie ignoruje znaków specjalnych poza podanymi podciągami i daje nieco nieoczekiwane wyniki w niektórych przypadkach narożnych.
Mam następujący przypadek użycia, w którym potrzebuję funkcji podziału, która dzieli ciągi wejściowe w taki sposób, że zachowane są zarówno jedno-, jak i podwójnie cudzysłowy, z możliwością ucieczki cudzysłowów w takim podciągu. Cytaty w ciągu niecytowanym nie powinny być traktowane inaczej niż jakikolwiek inny znak. Niektóre przykładowe przypadki testowe z oczekiwanym wynikiem:
ciąg wejściowy | oczekiwany wynik
===============================================
„abc def” | ['Alfabet']
"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” | ["Alfabet"]
„abc'def” ”| ["Alfabet'"]
"abc'def 'ghi" | [„abc'def” ”,„ ghi ”]
"abc'def'ghi" | [„abc'def'ghi”]
„abc” def ”| [„ abc ”def”]
„abc” def ”” ['Alfabet"']
„abc” def ”ghi” | [„abc” def ””, „ghi”]
„abc” def ”ghi” | ['abc "def" ghi']
„r'AA 'r”. * _ xyz $' ”| [„r'AA” ”,„ r ”. * _ xyz $ '”]
Skończyłem z następującą funkcją, aby podzielić ciąg tak, aby oczekiwane wyniki wyjściowe dla wszystkich ciągów wejściowych:
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)]
Następująca aplikacja testowa sprawdza wyniki innych podejść ( shlex
i csv
na razie) i implementacji niestandardowego podziału:
#!/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)')
Wynik:
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 -> wyjątek: brak cytatu końcowego
[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 -> wyjątek: brak cytatu końcowego
[FAIL] abc'def '-> [' abcdef ']
[FAIL] abc'def 'ghi -> [' abcdef ',' ghi ']
[FAIL] abc'def'ghi -> ['abcdefghi']
[FAIL] abc "def -> wyjątek: brak cytatu końcowego
[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 -> [„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 -> [”,„ 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,281 ms na iterację
csv: 0,030 ms na iterację
re: 0,049 ms na iterację
Wydajność jest więc znacznie lepsza shlex
i może być dalej poprawiana przez prekompilację wyrażenia regularnego, w którym to przypadku przewyższy to csv
podejście.