web-dev-qa-db-fra.com

Fractionner une chaîne par des espaces - en préservant les chaînes citées - en Python

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.

223
Adam Pierce

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.

342
Jerub

Consultez le module shlex, en particulier shlex.split.

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

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.

31
Kate

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']
23
Ryan Ginstrom

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)
12
Daniel Dai

É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)
8
gooli

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

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

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.

2
hochl

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'))]
2
moschlar

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.

1
Ton van den Heuvel

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)

1
Tyris

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

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']
0
hussic