Podziel ciąg według spacji - zachowując cytowane podciągi - w Pythonie


269

Mam ciąg, który jest taki:

this is "a test"

Próbuję napisać coś w Pythonie, aby podzielić to na spację, ignorując spacje w cudzysłowie. Wynik, którego szukam to:

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

PS. Wiem, że zapytasz „co się stanie, jeśli w cytatach są cytaty, cóż, w mojej aplikacji, to się nigdy nie wydarzy.


1
Dzięki, że zadałeś to pytanie. Właśnie tego potrzebowałem do naprawy modułu kompilacji pypar.
Martlark

Odpowiedzi:


392

Chcesz splitz wbudowanego shlexmodułu.

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

To powinno zrobić dokładnie to, co chcesz.


13
Użyj „posix = False”, aby zachować cytaty. shlex.split('this is "a test"', posix=False)zwraca['this', 'is', '"a test"']
Boon

@MatthewG. „Poprawka” w Pythonie 2.7.3 oznacza, że ​​przekazanie ciągu znaków Unicode shlex.split()spowoduje wyzwolenie UnicodeEncodeErrorwyjątku.
Rockallite,

57

W szczególności spójrz na shlexmoduł shlex.split.

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

40

Widzę tutaj wyrażenia regularne, które wyglądają na złożone i / lub niepoprawne. Zaskakuje mnie to, ponieważ składnia wyrażeń regularnych może łatwo opisać „białe znaki lub cytaty otoczone rzeczami”, a większość silników wyrażeń regularnych (w tym Pythona) może się dzielić na wyrażenia regularne. Jeśli więc zamierzasz używać wyrażeń regularnych, dlaczego nie powiesz dokładnie, co masz na myśli ?:

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

Wyjaśnienie:

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

shlex prawdopodobnie zapewnia jednak więcej funkcji.


1
Myślałem w podobny sposób, ale zamiast tego sugerowałbym [t.strip ('"') dla t in re.findall (r '[^ \ s"] + | "[^"] * "',„ to jest ” test "')]
Darius Bacon

2
+1 Korzystam z tego, ponieważ był o wiele szybszy niż shlex.
hanleyp

3
Dlaczego potrójny ukośnik? czy prosty odwrotny ukośnik nie robi tego samego?
Doppelganger,

1
W rzeczywistości jedną rzeczą, która mi się nie podoba, jest to, że wszystko przed / po cudzysłowach nie jest poprawnie podzielone. Jeśli mam taki ciąg „PARAMS val1 =„ Thing ”val2 =„ Thing2 ”. Oczekuję, że ciąg podzieli się na trzy części, ale dzieli się na 5. Minęło trochę czasu, odkąd zrobiłem regex, więc nie mam ochoty próbować go rozwiązać za pomocą twojego rozwiązania.
leetNightshade

1
Powinieneś używać nieprzetworzonych ciągów znaków podczas używania wyrażeń regularnych.
asmeurer,

29

W zależności od przypadku użycia możesz również sprawdzić csvmoduł:

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

Wynik:

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

2
przydatne, gdy shlex usuwa potrzebne postacie
scraplesh

1
CSV używa dwóch podwójnych cytatów z rzędu (jak obok siebie ""), aby przedstawić jeden podwójny cytat ", więc zamieni dwa podwójne cudzysłowy w pojedynczy cytat 'this is "a string""'i 'this is "a string"""'oba ['this', 'is', 'a string"']
Boris

15

Używam shlex.split do przetworzenia 70 000 000 linii dziennika kałamarnic, to jest tak wolne. Więc przełączyłem się na re.

Spróbuj tego, jeśli masz problem z wydajnością shlex.

import re

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

8

Ponieważ to pytanie jest oznaczone wyrażeniem regularnym, postanowiłem wypróbować podejście wyrażenia regularnego. Najpierw zamieniam wszystkie spacje w częściach cudzysłowów na \ x00, następnie dzielę spacje, a następnie zamieniam \ x00 z powrotem na spacje w każdej części.

Obie wersje robią to samo, ale splitter jest nieco bardziej czytelny niż 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)

Zamiast tego powinieneś użyć re.Scanner. Jest bardziej niezawodny (i faktycznie zaimplementowałem shlex-podobny za pomocą re.Scanner).
Devin Jeanpierre

+1 Hm, to całkiem sprytny pomysł, dzieląc problem na kilka kroków, więc odpowiedź nie jest strasznie skomplikowana. Shlex nie zrobił dokładnie tego, czego potrzebowałem, nawet próbując go ulepszyć. A rozwiązania regex jednoprzebiegowe stały się naprawdę dziwne i skomplikowane.
leetNightshade

6

Wydaje się, że ze względu na wydajność rejest szybszy. Oto moje rozwiązanie wykorzystujące najmniej chciwy operator, który zachowuje zewnętrzne cytaty:

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

Wynik:

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

Pozostawia konstrukty jak aaa"bla blub"bbbrazem, ponieważ te tokeny nie są oddzielone spacjami. Jeśli ciąg zawiera znaki specjalne, możesz dopasować w następujący sposób:

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

Należy pamiętać, że to również pasuje do pustego łańcucha ""za pomocą \Sczęści wzoru.


1
Kolejną ważną zaletą tego rozwiązania jest jego wszechstronność w odniesieniu do charakteru ograniczającego (np. ,Via '(?:".*?"|[^,])+'). To samo dotyczy cudzysłowu (otaczającego) znaku (znaków).
a_guest

4

Główny problem z przyjętym shlexpodejś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ść ( shlexi csvna 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 shlexi może być dalej poprawiana przez prekompilację wyrażenia regularnego, w którym to przypadku przewyższy to csvpodejście.


Nie jestem pewien, o czym mówisz: `` >>> shlex.split ('is "a test"') ['this', 'is', 'a test'] >>> shlex.split (' to jest \\ "test \\" ') [„this”, „is”, „„ a ”,„ test ””] >>> shlex.split („to jest„ a \\ ”test \\” „”) [„to”, „jest”, „test” ”]„ „
morsik

@morsik, o co ci chodzi? Może twój przypadek użycia nie pasuje do mojego? Gdy spojrzysz na przypadki testowe, zobaczysz wszystkie przypadki, w których shlexnie zachowuję się zgodnie z oczekiwaniami dla moich przypadków użycia.
Ton van den Heuvel

3

Aby zachować cytaty, użyj tej funkcji:

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

W porównaniu z większym ciągiem twoja funkcja jest tak wolna
Faran2007

3

Test szybkości różnych odpowiedzi:

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, nie mogę znaleźć przycisku „Odpowiedz”… w każdym razie ta odpowiedź jest oparta na podejściu Kate, ale poprawnie dzieli ciągi znaków na podciągi zawierające cytaty, a także usuwa początkowe i końcowe cytowania podciągów:

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

Działa to na ciągach takich jak 'This is " a \\\"test\\\"\\\'s substring"' (szalone znaczniki są niestety konieczne, aby powstrzymać Pythona przed usunięciem ucieczki).

Jeśli wynikowe znaki ucieczki w łańcuchach na zwróconej liście nie są potrzebne, możesz użyć tej nieco zmienionej wersji funkcji:

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

1

Aby obejść problemy z Unicode w niektórych wersjach Python 2, sugeruję:

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

W przypadku Pythona 2.7.5 powinno to być: w split = lambda a: [b.decode('utf-8') for b in _split(a)]przeciwnym razie otrzymasz:UnicodeDecodeError: 'ascii' codec can't decode byte ... in position ...: ordinal not in range(128)
Peter Varo

1

Opcjonalnie wypróbuj tssplit:

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

0

Sugeruję:

ciąg testowy:

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

przechwytywać także „” i „”:

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

wynik:

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

aby zignorować puste „” i „”:

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

wynik:

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

Może być re.findall("(?:\".*?\"|'.*?'|[^\s'\"]+)", s)również napisane .
hochl

-3

Jeśli nie zależy ci na napisach niż na prostym

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

Występ:

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

Lub moduł ciągów

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

Wydajność: moduł ciągów wydaje się działać lepiej niż metody ciągów

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

Lub możesz użyć silnika RE

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

Występ

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

W przypadku bardzo długich łańcuchów nie należy ładować całego łańcucha do pamięci, a zamiast tego albo podzielić linie, albo użyć pętli iteracyjnej


11
Wygląda na to, że przegapiłeś cały punkt pytania. W ciągu są cytowane sekcje, których nie trzeba dzielić.
rjmunro

-3

Spróbuj tego:

  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

Niektóre ciągi testowe:

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

Podaj repr ciągu, który Twoim zdaniem się nie powiedzie.
pjz

Myśleć ? adamsplit("This is 'a test'")['This', 'is', "'a", "test'"]
Matthew Schinckel

OP mówi tylko „w cudzysłowie” i ma tylko przykład z podwójnymi cudzysłowami.
pjz
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.