Розбийте рядок на пробіли - зберігаючи цитовані підрядки - в 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.