J'ai une chaîne qui ressemble à ceci:
this is "a test"
J'essaie d'écrire quelque chose en Python pour le scinder en espaces tout en ignorant les espaces entre guillemets. Le résultat que je recherche est:
['this','is','a test']
PS. Je sais que vous allez demander "qu'est-ce qui se passe s'il y a des citations dans les citations, eh bien, dans ma demande, cela n'arrivera jamais.
Vous voulez séparer, du module shlex .
>>> import shlex
>>> shlex.split('this is "a test"')
['this', 'is', 'a test']
Cela devrait faire exactement ce que vous voulez.
Consultez le module shlex
, en particulier shlex.split
.
>>> import shlex
>>> shlex.split('This is "a test"')
['This', 'is', 'a test']
Je vois des approches de regex ici qui semblent complexes et/ou fausses. Cela me surprend, car la syntaxe des expressions rationnelles peut facilement décrire «des espaces ou des guillemets entourés de choses», et la plupart des moteurs d’expression régulière (y compris ceux de Python) peuvent se scinder en regex. Donc, si vous allez utiliser des regex, pourquoi ne pas simplement dire exactement ce que vous voulez dire?
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()]
Explication:
[\\\"'] = double-quote or single-quote
.* = anything
( |X) = space or X
.strip() = remove space and empty-string separators
shlex fournit probablement plus de fonctionnalités, cependant.
Selon votre cas d'utilisation, vous pouvez également consulter le module csv:
import csv
lines = ['this is "a string"', 'and more "stuff"']
for row in csv.reader(lines, delimiter=" "):
print row
Sortie:
['this', 'is', 'a string']
['and', 'more', 'stuff']
J'utilise shlex.split pour traiter 70 000 000 lignes de bûches de calmar, c'est tellement lent. Alors je suis passé à re.
S'il vous plaît essayez ceci, si vous avez un problème de performance avec shlex.
import re
def line_split(line):
return re.findall(r'[^"\s]\S*|".+?"', line)
Étant donné que cette question porte la mention regex, j'ai décidé d'essayer une approche regex. Je remplace d'abord tous les espaces dans les parties de guillemets par\x00, puis je les divise par des espaces, puis je remplace le\x00 par des espaces dans chaque partie.
Les deux versions font la même chose, mais splitter est un peu plus lisible 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)
Pour conserver les guillemets, utilisez cette fonction:
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
Test de vitesse de différentes réponses:
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
Il semble que pour des raisons de performances, re
est plus rapide. Voici ma solution en utilisant un opérateur moins gourmand qui préserve les guillemets externes:
re.findall("(?:\".*?\"|\S)+", s)
Résultat:
['this', 'is', '"a test"']
Il laisse ensemble des constructions comme aaa"bla blub"bbb
car ces jetons ne sont pas séparés par des espaces. Si la chaîne contient des caractères d'échappement, vous pouvez faire correspondre ceci:
>>> 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.\""
Veuillez noter que cela correspond également à la chaîne vide ""
au moyen de la partie \S
du modèle.
Pour contourner les problèmes unicode dans certaines versions de Python 2, je suggère:
from shlex import split as _split
split = lambda a: [b.decode('utf-8') for b in _split(a.encode('utf-8'))]
Le principal problème de l’approche acceptée shlex
est qu’elle n’ignore pas les caractères d’échappement en dehors des sous-chaînes citées et donne des résultats légèrement inattendus dans certains cas extrêmes.
J'ai le cas d'utilisation suivant, pour lequel j'ai besoin d'une fonction de division qui scinde les chaînes d'entrée de sorte que les sous-chaînes entre guillemets simples ou les guillemets doubles soient préservées, avec la possibilité d'échapper des guillemets dans une telle sous-chaîne. Les guillemets dans une chaîne sans guillemets ne doivent pas être traités différemment des autres caractères. Quelques exemples de cas de test avec la sortie attendue:
chaîne d'entrée | sortie attendue ========================================== == '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 $ '"]
Je me suis retrouvé avec la fonction suivante pour diviser une chaîne de sorte que les résultats de sortie attendus pour toutes les chaînes d'entrée:
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)]
L'application de test suivante vérifie les résultats d'autres approches (shlex
et csv
pour l'instant) et l'implémentation personnalisée par division:
#!/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)')
Sortie:
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 -> exception: pas de citation de clôture [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 -> exception: pas de citation de clôture [FAIL] abc'def '-> [' abcdef '] [FAIL] abc'def' ghi -> ['abcdef', 'ghi'] [FAIL] abc'def'ghi -> ['abcdefghi'] [FAIL] abc "def -> exception: aucune citation de clôture [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 par itération csv: 0.030ms par itération re: 0.049ms par itération
Les performances sont donc bien meilleures que shlex
et peuvent être encore améliorées en précompilant l'expression régulière. Dans ce cas, elles seront plus performantes que l'approche csv
.
Les problèmes unicode avec shlex décrits ci-dessus (réponse principale) semblent être résolus (indirectement) dans la version 2.7.2+, conformément à http://bugs.python.org/issue6988#msg146200
(réponse séparée car je ne peux pas commenter)
Hmm, n'arrive pas à trouver le bouton "Répondre" ... de toute façon, cette réponse est basée sur l'approche de Kate, mais scinde correctement les chaînes avec des sous-chaînes contenant des guillemets d'échappement et supprime également les guillemets de début et de fin des sous-chaînes:
[i.strip('"').strip("'") for i in re.split(r'(\s+|(?<!\\)".*?(?<!\\)"|(?<!\\)\'.*?(?<!\\)\')', string) if i.strip()]
Cela fonctionne sur des chaînes comme 'This is " a \\\"test\\\"\\\'s substring"'
(le balisage insane est malheureusement nécessaire pour empêcher Python de supprimer les échappements).
Si les échappements résultant dans les chaînes de la liste renvoyée ne sont pas souhaités, vous pouvez utiliser cette version légèrement modifiée de la fonction:
[i.strip('"').strip("'").decode('string_escape') for i in re.split(r'(\s+|(?<!\\)".*?(?<!\\)"|(?<!\\)\'.*?(?<!\\)\')', string) if i.strip()]
Je suggère:
chaîne de test:
s = 'abc "ad" \'fg\' "kk\'rdt\'" zzz"34"zzz "" \'\''
capturer aussi "" et '':
import re
re.findall(r'"[^"]*"|\'[^\']*\'|[^"\'\s]+',s)
résultat:
['abc', '"ad"', "'fg'", '"kk\'rdt\'"', 'zzz', '"34"', 'zzz', '""', "''"]
ignorer les "" et '' vides:
import re
re.findall(r'"[^"]+"|\'[^\']+\'|[^"\'\s]+',s)
résultat:
['abc', '"ad"', "'fg'", '"kk\'rdt\'"', 'zzz', '"34"', 'zzz']