Разбить строку пробелами - сохраняя подстроки в кавычках - в Python

269

У меня есть строка, которая выглядит так:

this is "a test"

Я пытаюсь написать что-то на Python, чтобы разделить его на пробелы, игнорируя пробелы в кавычках. Результат, который я ищу:

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

PS. Я знаю, что вы спросите «что произойдет, если в кавычках есть кавычки, ну, в моем приложении, этого никогда не произойдет».

Адам Пирс
источник
1
Спасибо, что задали этот вопрос. Это именно то, что мне нужно для исправления модуля сборки Pypar.
Martlark

Ответы:

393

Вы хотите split, из встроенного shlexмодуля.

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

Это должно делать именно то, что вы хотите.

Jerub
источник
13
Используйте "posix = False", чтобы сохранить цитаты. shlex.split('this is "a test"', posix=False)возвращается['this', 'is', '"a test"']
Boon
@MatthewG. «Исправление» в Python 2.7.3 означает, что передача строки в shlex.split()юникоде вызовет UnicodeEncodeErrorисключение.
Rockallite
57

Посмотрите на shlexмодуль, в частности shlex.split.

>>> import shlex
>>> shlex.split('This is "a test"')
['This', 'is', 'a test']
Аллен
источник
40

Я вижу подходы регулярных выражений здесь, которые выглядят сложными и / или неправильными. Это удивляет меня, потому что синтаксис регулярных выражений может легко описать «пробел или вещь, заключенную в кавычки», и большинство движков регулярных выражений (включая Python) могут разделяться на регулярные выражения. Так что, если вы собираетесь использовать регулярные выражения, почему бы просто не сказать точно, что вы имеете в виду ?:

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

Объяснение:

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

Shlex, вероятно, предоставляет больше возможностей, хотя.


источник
1
Я думал примерно так же, но вместо этого предложил бы [t.strip ('"') для t в re.findall (r '[^ \ s"] + | "[^"] * "', 'this is" тест "')]
Дариус Бэкон
2
+1 Я использую это, потому что это было намного быстрее, чем шлекс.
Hanleyp
3
Почему тройной обратный слеш? простой обратный слеш не сделает то же самое?
Двойник
1
На самом деле, одна вещь, которая мне не нравится в этом, это то, что что-то до / после кавычек не разделяется должным образом. Если у меня есть такая строка: PARAMS val1 = "Thing" val2 = "Thing2" '. Я ожидаю, что строка разделится на три части, но она разделится на 5. Прошло много времени с тех пор, как я сделал регулярное выражение, поэтому я не чувствую, что пытаюсь решить его с помощью вашего решения прямо сейчас.
leetNightshade
1
Вы должны использовать необработанные строки при использовании регулярных выражений.
asmeurer
29

В зависимости от вашего варианта использования, вы также можете проверить csvмодуль:

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

Вывод:

['this', 'is', 'a string']
['and', 'more', 'stuff']
Райан Джинстрем
источник
2
полезно, когда шлекс лишает некоторых необходимых персонажей
'18
1
CSV используют две двойные кавычки подряд (как в бок о бок ""), чтобы представить одну двойную кавычку ", поэтому превратят две двойные кавычки в одну кавычку 'this is "a string""'и 'this is "a string"""'оба сопоставят с['this', 'is', 'a string"']
Борис
15

Я использую shlex.split для обработки 7000000 строк журнала Squid, это очень медленно. Поэтому я перешел на ре.

Пожалуйста, попробуйте это, если у вас есть проблемы с производительностью shlex.

import re

def line_split(line):
    return re.findall(r'[^"\s]\S*|".+?"', line)
Даниэль Дай
источник
8

Поскольку этот вопрос помечен регулярным выражением, я решил попробовать подход регулярного выражения. Сначала я заменяю все пробелы в частях кавычек на \ x00, затем делю на пробелы, а затем заменяю \ x00 на пробелы в каждой части.

Обе версии делают одно и то же, но разделитель немного читабельнее, чем разделитель2.

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)
elifiner
источник
Вы должны были использовать re.Scanner. Это более надежно (и я на самом деле реализовал shlex-like с помощью re.Scanner).
Девин Жанпьер
+1 Хм, это довольно умная идея, разбить проблему на несколько шагов, чтобы ответ не был слишком сложным. Шлекс не делал именно то, что мне было нужно, даже пытаясь настроить это. И решения regex за один проход становились действительно странными и сложными.
leetNightshade
6

Похоже, по соображениям производительности reэто быстрее. Вот мое решение с использованием наименее жадного оператора, который сохраняет внешние кавычки:

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

Результат:

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

Это оставляет конструкции как aaa"bla blub"bbbвместе, так как эти токены не разделены пробелами. Если строка содержит экранированные символы, вы можете сопоставить это так:

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

Обратите внимание, что это также соответствует пустой строке ""посредством \Sчасти шаблона.

hochl
источник
1
Другим важным преимуществом этого решения является его универсальность по отношению к символу-разделителю (например, ,через '(?:".*?"|[^,])+'). То же самое относится к кавычкам (вмещающим) символам.
a_guest
4

Основная проблема с принятым shlexподходом состоит в том, что он не игнорирует escape-символы вне указанных в кавычках подстрок и дает несколько неожиданные результаты в некоторых угловых случаях.

У меня есть следующий вариант использования, где мне нужна функция разбиения, которая разбивает входные строки так, чтобы подстроки с одинарными или двойными кавычками сохранялись, с возможностью экранирования кавычек внутри такой подстроки. Кавычки в строке без кавычек не должны трактоваться иначе, чем любой другой символ. Некоторые примеры тестовых случаев с ожидаемым выводом:

входная строка | ожидаемый результат
===============================================
 'abc def' | ['abc', 'def']
 "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']
 "" "тест" | ['', 'тест']
 "тест" | ['', 'тест']
 "abc'def" | [ "Abc'def"]
 "abc'def" »| [ "Abc'def"]
 "abc'def 'ghi" | ["abc'def", "ghi"]
 "abc'def'ghi" | [ "abc'def'ghi"]
 'abc "def' | ['abc" def']
 'abc "def"' | [ 'А "DEF"']
 'abc "def" ghi' | ['abc "def"', 'ghi']
 'abc "def" ghi' | [ 'А "DEF" ГХИ']
 "r'AA 'r'. * _ xyz $ '" | ["r'AA", "r '. * _ xyz $'"]

В итоге я получил следующую функцию, чтобы разбить строку таким образом, чтобы ожидаемые выходные результаты были получены для всех входных строк:

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

Следующее тестовое приложение проверяет результаты других подходов ( shlexи csvпока) и пользовательскую реализацию разделения:

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

Вывод:

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 -> исключение: закрывающая кавычка отсутствует
[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 -> исключение: закрывающая кавычка отсутствует
[FAIL] abc'def '-> [' abcdef ']
[FAIL] abc'def 'ghi -> [' abcdef ',' ghi ']
[FAIL] abc'def'ghi -> ['abcdefghi']
[FAIL] abc "def -> исключение: закрывающая кавычка отсутствует
[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 $'"]

ре

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

шлекс: 0,281 мс на итерацию
CSV: 0,030 мс на итерацию
Re: 0,049 мс на итерацию

Таким образом, производительность намного выше shlexи может быть улучшена путем предварительной компиляции регулярного выражения, и в этом случае он превзойдет csvподход.

Тон ван ден Хеувел
источник
Не уверен, о чем вы говорите: `` `>>> shlex.split ('это" тест ") [' this ',' is ',' тест '] >>> shlex.split (' это \\ "тест \\" ') [' this ',' is ',' 'a', 'test' '] >>> shlex.split (' это "a \\" test \\ " "') [' this ',' is ',' a" test "']` ``
morsik
@morsik, в чем твоя точка зрения? Может быть, ваш вариант использования не совпадает с моим? Когда вы посмотрите на контрольные примеры, вы увидите все случаи, когда shlexповедение не соответствует ожидаемым для моих сценариев использования.
Тон ван ден
3

Чтобы сохранить кавычки используйте эту функцию:

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
THE_MAD_KING
источник
При сравнении со строкой большего размера ваша функция слишком медленная
Faran2007
3

Тест скорости разных ответов:

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
har777
источник
1

Хм, похоже, не могу найти кнопку «Ответить» ... во всяком случае, этот ответ основан на подходе Кейт, но правильно разбивает строки на подстроки, содержащие экранированные кавычки, а также удаляет начальные и конечные кавычки подстрок:

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

Это работает с такими строками 'This is " a \\\"test\\\"\\\'s substring"'(безумная разметка, к сожалению, необходима, чтобы Python не удалял экранированные символы).

Если результирующие экранированные строки в возвращаемом списке не нужны, вы можете использовать эту слегка измененную версию функции:

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

источник
1

Чтобы обойти проблемы с юникодом в некоторых версиях Python 2, я предлагаю:

from shlex import split as _split
split = lambda a: [b.decode('utf-8') for b in _split(a.encode('utf-8'))]
moschlar
источник
Для python 2.7.5 это должно быть: в split = lambda a: [b.decode('utf-8') for b in _split(a)]противном случае вы получите:UnicodeDecodeError: 'ascii' codec can't decode byte ... in position ...: ordinal not in range(128)
Peter Varo
1

Как вариант попробуйте tssplit:

In [1]: from tssplit import tssplit
In [2]: tssplit('this is "a test"', quote='"', delimiter='')
Out[2]: ['this', 'is', 'a test']
Михаил Захаров
источник
0

Я предлагаю:

тестовая строка:

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

захватить также «» и «»:

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

результат:

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

игнорировать пустые "" и "':

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

результат:

['abc', '"ad"', "'fg'", '"kk\'rdt\'"', 'zzz', '"34"', 'zzz']
hussic
источник
Могут быть написаны re.findall("(?:\".*?\"|'.*?'|[^\s'\"]+)", s)также.
hochl
-3

Если вам не нужны подстроки, чем простая

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

Производительность:

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

Или строковый модуль

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

Производительность: модуль 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

Или вы можете использовать двигатель RE

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

Производительность

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

Для очень длинных строк не следует загружать всю строку в память и вместо этого либо разбивать строки, либо использовать итеративный цикл

Грегори
источник
11
Вы, кажется, упустили всю суть вопроса. В строке есть разделы в кавычках, которые не нужно разбивать.
rjmunro
-3

Попробуй это:

  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

Некоторые тестовые строки:

'This is "a test"' -> ['This', 'is', 'a test']
'"This is \'a test\'"' -> ["This is 'a test'"]
pjz
источник
Пожалуйста, укажите repr строки, которая, по вашему мнению, потерпит неудачу.
16:04
Думаешь ? adamsplit("This is 'a test'")['This', 'is', "'a", "test'"]
Мэтью Шинкель
ОП только говорит "в кавычках" и имеет только пример с двойными кавычками
pjz