Fractionner une chaîne par des espaces - en préservant les sous-chaînes entre guillemets - en Python


269

J'ai une chaîne qui ressemble à ceci:

this is "a test"

J'essaie d'écrire quelque chose en Python pour le diviser par espace 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 "ce qui se passe s'il y a des guillemets dans les guillemets, eh bien, dans mon application, cela n'arrivera jamais.


1
Merci d'avoir posé cette question. C'est exactement ce dont j'avais besoin pour réparer le module de construction pypar.
Martlark

Réponses:


393

Vous voulez split, à partir du shlexmodule intégré.

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

Cela devrait faire exactement ce que vous voulez.


13
Utilisez "posix = False" pour conserver les citations. shlex.split('this is "a test"', posix=False)retours['this', 'is', '"a test"']
Boon

@MatthewG. Le "correctif" dans Python 2.7.3 signifie que le passage d'une chaîne unicode à shlex.split()déclenchera une UnicodeEncodeErrorexception.
Rockallite

57

Jetez un œil au shlexmodule, en particulier shlex.split.

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

40

Je vois ici des approches regex qui semblent complexes et / ou erronées. Cela me surprend, car la syntaxe des regex peut facilement décrire "des espaces ou des choses entourées de guillemets", et la plupart des moteurs de regex (y compris Python) peuvent se diviser sur une regex. Donc, si vous allez utiliser des expressions rationnelles, 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.


1
Je pensais à peu près la même chose, mais je suggérerais plutôt [t.strip ('"') pour t dans re.findall (r '[^ \ s"] + | "[^"] * "', 'ceci est" un test "')]
Darius Bacon

2
+1 J'utilise ceci parce que c'était beaucoup plus rapide que shlex.
hanleyp

3
Pourquoi la triple barre oblique inverse? une simple barre oblique inverse ne fera-t-elle pas de même?
Doppelganger

1
En fait, une chose que je n'aime pas à ce sujet est que tout ce qui se trouve avant / après n'est pas divisé correctement. Si j'ai une chaîne comme celle-ci 'PARAMS val1 = "Thing" val2 = "Thing2"'. Je m'attends à ce que la chaîne se divise en trois morceaux, mais elle se divise en 5. Cela fait un moment que je n'ai pas fait d'expression régulière, donc je n'ai pas envie d'essayer de le résoudre en utilisant votre solution pour le moment.
leetNightshade

1
Vous devez utiliser des chaînes brutes lors de l'utilisation d'expressions régulières.
asmeurer

29

Selon votre cas d'utilisation, vous pouvez également consulter le csvmodule:

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

Production:

['this', 'is', 'a string']
['and', 'more', 'stuff']

2
utile, quand shlex dépouille certains personnages nécessaires
scraplesh

1
Les CSV utilisent deux guillemets consécutifs (comme côte à côte "") pour représenter un guillemet double ", donc transformeront deux guillemets doubles en un guillemet simple 'this is "a string""'et 'this is "a string"""'seront tous deux mappés à['this', 'is', 'a string"']
Boris

15

J'utilise shlex.split pour traiter 70 000 000 lignes de journal de calmar, c'est tellement lent. Je suis donc passé à re.

Veuillez essayer ceci, si vous avez un problème de performances avec shlex.

import re

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

8

Puisque cette question est étiquetée avec 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 en 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)

Vous auriez dû utiliser re.Scanner à la place. C'est plus fiable (et j'ai en fait implémenté un shlex-like en utilisant re.Scanner).
Devin Jeanpierre

+1 Hm, c'est une idée assez intelligente, décomposant le problème en plusieurs étapes, donc la réponse n'est pas terriblement complexe. Shlex n'a pas fait exactement ce dont j'avais besoin, même en essayant de le modifier. Et les solutions d'expression régulière en un seul passage devenaient vraiment étranges et compliquées.
leetNightshade

6

Il semble que pour des raisons de performances, rec'est plus rapide. Voici ma solution en utilisant un opérateur le moins gourmand qui préserve les guillemets externes:

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

Résultat:

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

Il laisse des constructions comme aaa"bla blub"bbbensemble 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 comme ça:

>>> 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 \Spartie du motif.


1
Un autre avantage important de cette solution est sa polyvalence par rapport au caractère délimitant (par exemple ,via '(?:".*?"|[^,])+'). Il en va de même pour le (s) caractère (s) (entre guillemets).
a_guest

4

Le principal problème avec l'acceptation shlex approche est qu'elle n'ignore pas les caractères d'échappement en dehors des sous-chaînes entre guillemets et donne des résultats légèrement inattendus dans certains cas de coin.

J'ai le cas d'utilisation suivant, où j'ai besoin d'une fonction de fractionnement qui fractionne les chaînes d'entrée de telle sorte que les sous-chaînes entre guillemets simples ou doubles sont préservées, avec la possibilité d'échapper les guillemets dans une telle sous-chaîne. Les guillemets dans une chaîne sans guillemets ne doivent pas être traités différemment de tout autre caractère. Quelques exemples de cas de test avec la sortie attendue:

chaîne d'entrée | production 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 ( shlexet csvpour l'instant) et l'implémentation du fractionnement personnalisé:

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

Production:

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: aucune 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: aucune 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 $'"]

ré

[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 par itération
csv: 0,030 ms par itération
re: 0,049 ms par itération

Ainsi, les performances sont bien meilleures que shlex, et peuvent être encore améliorées en précompilant l'expression régulière, auquel cas elle surpassera l' csvapproche.


Je ne sais pas de quoi vous parlez: `` `>>> shlex.split ('ceci est" un test "') ['ceci', 'est', 'un test'] >>> shlex.split (' c'est \\ "un test \\" ') [' ceci ',' est ',' "un ',' test" '] >>> shlex.split (' c'est "un \\" test \\ " "') [' ceci ',' est ',' un" test "']` `
morsik

@morsik, quel est votre point? Peut-être que votre cas d'utilisation ne correspond pas au mien? Lorsque vous regardez les cas de test, vous verrez tous les cas où shlexne se comporte pas comme prévu pour mes cas d'utilisation.
Ton van den Heuvel

3

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

Lorsque vous comparez avec une chaîne plus grande, votre fonction est si lente
Faran2007

3

Test de vitesse des 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

1

Hmm, ne semble pas trouver le bouton "Répondre" ... de toute façon, cette réponse est basée sur l'approche de Kate, mais divise correctement les chaînes avec des sous-chaînes contenant des guillemets échappés 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 fou est malheureusement nécessaire pour empêcher Python de supprimer les échappements).

Si les échappements résultants dans les chaînes de la liste retourné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

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

Pour python 2.7.5, cela devrait être: split = lambda a: [b.decode('utf-8') for b in _split(a)]sinon vous obtenez:UnicodeDecodeError: 'ascii' codec can't decode byte ... in position ...: ordinal not in range(128)
Peter Varo

1

En option, essayez tssplit:

In [1]: from tssplit import tssplit
In [2]: tssplit('this is "a test"', quote='"', delimiter='')
Out[2]: ['this', 'is', 'a test']

0

Je suggère:

chaîne de test:

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

pour capturer aussi "" et '':

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

résultat:

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

ignorer "" et '' vides:

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

résultat:

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

Pourrait aussi s'écrire re.findall("(?:\".*?\"|'.*?'|[^\s'\"]+)", s).
hochl

-3

Si vous ne vous souciez pas des sous-chaînes qu'un simple

>>> 'a short sized string with spaces '.split()

Performance:

>>> s = " ('a short sized string with spaces '*100).split() "
>>> t = timeit.Timer(stmt=s)
>>> print "%.2f usec/pass" % (1000000 * t.timeit(number=100000)/100000)
171.39 usec/pass

Ou module de chaîne

>>> from string import split as stringsplit; 
>>> stringsplit('a short sized string with spaces '*100)

Performances: le module String semble fonctionner mieux que les méthodes String

>>> s = "stringsplit('a short sized string with spaces '*100)"
>>> t = timeit.Timer(s, "from string import split as stringsplit")
>>> print "%.2f usec/pass" % (1000000 * t.timeit(number=100000)/100000)
154.88 usec/pass

Ou vous pouvez utiliser le moteur RE

>>> from re import split as resplit
>>> regex = '\s+'
>>> medstring = 'a short sized string with spaces '*100
>>> resplit(regex, medstring)

Performance

>>> s = "resplit(regex, medstring)"
>>> t = timeit.Timer(s, "from re import split as resplit; regex='\s+'; medstring='a short sized string with spaces '*100")
>>> print "%.2f usec/pass" % (1000000 * t.timeit(number=100000)/100000)
540.21 usec/pass

Pour les chaînes très longues, vous ne devez pas charger la chaîne entière en mémoire et au lieu de cela diviser les lignes ou utiliser une boucle itérative


11
Vous semblez avoir raté tout le point de la question. Il existe des sections entre guillemets qui ne doivent pas être divisées.
rjmunro

-3

Essaye ça:

  def adamsplit(s):
    result = []
    inquotes = False
    for substring in s.split('"'):
      if not inquotes:
        result.extend(substring.split())
      else:
        result.append(substring)
      inquotes = not inquotes
    return result

Quelques chaînes de test:

'This is "a test"' -> ['This', 'is', 'a test']
'"This is \'a test\'"' -> ["This is 'a test'"]

Veuillez fournir la reproduction d'une chaîne qui, selon vous, échouera.
pjz

Tu penses ? adamsplit("This is 'a test'")['This', 'is', "'a", "test'"]
Matthew Schinckel

OP ne dit que "entre guillemets" et n'a qu'un exemple avec des guillemets doubles.
pjz
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.