Розбийте рядок на пробіли - зберігаючи цитовані підрядки - в Python


269

У мене є такий рядок:

this is "a test"

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

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

PS. Я знаю, що ви запитаєте "що станеться, якщо в цитатах є цитати, ну, в моїй заяві, це ніколи не відбудеться.


1
Дякуємо, що задали це запитання. Це саме те, що мені було потрібно для закріплення модуля збірки pypar.
Martlark

Відповіді:


392

Ви хочете split, від вбудованого shlexмодуля.

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

Це має робити саме те, що ви хочете.


13
Використовуйте "posix = False", щоб зберегти цитати. shlex.split('this is "a test"', posix=False)повернення['this', 'is', '"a test"']
Бун

@MatthewG. "Виправлення" в Python 2.7.3 означає, що передача рядка Unicode до 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"] + | "[^"] * "'," це " тест "')]
Дарій Бекон

2
+1 Я використовую це, тому що це було біса набагато швидше, ніж shlex.
hanleyp

3
Чому потрійний нахил? чи не простий зворотний нахил зробить те саме?
Doppelganger

1
Насправді, одне, що мені не подобається в цьому, - це те, що що-небудь перед / після лапок не розбивається належним чином. Якщо у мене є такий рядок, як "PARAMS val1 =" Thing "val2 =" Thing2 "'. Я очікую, що струна розділиться на три частини, але вона розділиться на 5. Минув час, коли я зробив регулярний вираз, тому я не відчуваю, що намагаюся вирішити його за допомогою вашого рішення прямо зараз.
leetNightshade

1
Ви повинні використовувати необроблені рядки при використанні регулярних виразів.
асмеурер

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
корисно, коли shlex знімає потрібні символи
scraplesh

1
CSV використовує дві подвійні лапки підряд (як набік, ""щоб представити одну подвійну цитату) ", тому перетворить дві подвійні лапки в одну цитату 'this is "a string""'і 'this is "a string"""'обидва будуть відображатись['this', 'is', 'a string"']
Борис

15

Я використовую shlex.split для обробки 70 000 000 рядків журналу кальмарів, це так повільно. Тому я перейшов на повторне.

Спробуйте це, якщо у вас є проблеми з продуктивністю 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)

Натомість ви повинні були використовувати re.Scanner. Це надійніше (і я насправді реалізував шлекс-подібний за допомогою re.Scanner).
Devin Jeanpierre

+1 Гм, це досить розумна ідея, яка розбиває проблему на кілька кроків, тому відповідь не є надзвичайно складним. Шлекс не робив саме того, що мені потрібно, навіть намагаючись налаштувати його. І рішення з одноразовим виродженням ставали справді дивними та складними.
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частини шаблону.


1
Ще одна важлива перевага цього рішення - його універсальність щодо обмежувального характеру (наприклад, ,через '(?:".*?"|[^,])+'). Те саме стосується і котируючих (додаючих) символів.
a_guest

4

Основна проблема з прийнятим shlex підходу полягає в тому, що він не ігнорує втечі символів поза цитованими підрядками та дає дещо несподівані результати в деяких кутових випадках.

У мене є наступний випадок використання, коли мені потрібна функція розбиття, яка розбиває рядки введення таким чином, щоб збереглися або одноцитати, або подвійні котирування, з можливістю вийти з лапок у такій підрядку. Цитати в рядку, що не котирується, не повинні трактуватися інакше, ніж будь-який інший символ. Деякі приклади тестових випадків із очікуваним результатом:

вхідний рядок | очікуваний вихід
=================================================
 '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"' | ['abc "def"']
 'abc "def" ghi "| ['abc "def"', 'ghi']
 'abc "def" ghi "| ['abc "def" ghi "]
 "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)')

Вихід:

шлекс

[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']
[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']
[FAIL] '' test -> ["''", 'test']
[ОК] abc'def -> ["abc'def"]
[OK] abc'def '-> ["abc'def'"]
[Добре] abc'def 'ghi -> ["abc'def'", "ghi"]
[Добре] 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 "]
[Гаразд] 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']
[OK] '' test -> ['', 'test']
[ОК] abc'def -> ["abc'def"]
[OK] abc'def '-> ["abc'def'"]
[Добре] abc'def 'ghi -> ["abc'def'", "ghi"]
[Добре] 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 "]
[Гаразд] r'AA 'r'. * _ Xyz $ '-> ["r'AA" "," r'. * _ Xyz $ '"]

shlex: 0,281ms за ітерацію
csv: 0,030ms за ітерацію
re: 0,049 мс за ітерацію

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


Не впевнений, про що ви говорите: `` >>> shlex.split ('це' тест '') ['це', 'є', 'тест'] >>> shlex.split (' це \\ "тест \\" ') [' цей ',' є ',' 'a', 'тест' '] >>> shlex.split (' це "a \\" тест \\ " "') [' це ',' є ',' a 'тест' ']` `
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

Якщо порівнювати великі струни, ваша функція настільки повільна
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

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

Щоб подолати проблеми Unicode в деяких версіях Python 2, я пропоную:

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

Для 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']

Можна записати 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)

Продуктивність: Струнний модуль, здається, працює краще, ніж струнні методи

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

Думаєте ? adamsplit("This is 'a test'")['This', 'is', "'a", "test'"]
Меттью Шинкель

ОП говорить лише "в межах лапок" і має лише приклад з подвійними лапками.
pjz
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.