Pisahkan string dengan spasi - melestarikan substring yang dikutip - dengan Python

269

Saya memiliki string seperti ini:

this is "a test"

Saya mencoba menulis sesuatu dengan Python untuk membaginya dengan spasi sambil mengabaikan spasi dalam tanda kutip. Hasil yang saya cari adalah:

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

PS. Saya tahu Anda akan bertanya "apa yang terjadi jika ada tanda kutip di dalam tanda kutip, yah, dalam aplikasi saya, itu tidak akan pernah terjadi.

Adam Pierce
sumber
1
Terima kasih telah mengajukan pertanyaan ini. Persis seperti yang saya butuhkan untuk memperbaiki modul build pypar.
Martlark

Jawaban:

393

Anda ingin split, dari shlexmodul bawaan.

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

Ini harus melakukan apa yang Anda inginkan.

Jerub
sumber
13
Gunakan "posix = False" untuk menyimpan kutipan. shlex.split('this is "a test"', posix=False)kembali['this', 'is', '"a test"']
Boon
@ MatthewG. "Memperbaiki" dalam Python 2.7.3 berarti bahwa meneruskan string unicode ke shlex.split()akan memicu UnicodeEncodeErrorpengecualian.
Rockallite
57

Lihatlah shlexmodul, khususnya shlex.split.

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

Saya melihat pendekatan regex di sini yang terlihat rumit dan / atau salah. Ini mengejutkan saya, karena sintaksis regex dapat dengan mudah menggambarkan "ruang kosong atau hal-dikelilingi-oleh-kutipan", dan kebanyakan mesin regex (termasuk Python) dapat dibagi pada sebuah regex. Jadi jika Anda akan menggunakan regex, mengapa tidak mengatakan dengan tepat apa yang Anda maksud ?:

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

Penjelasan:

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

shlex mungkin menyediakan lebih banyak fitur.


sumber
1
Saya berpikir hampir sama, tetapi sebaliknya akan menyarankan [t.strip ('"') untuk t di re.findall (r '[^ \" "+ +" "[^"] * "', 'ini adalah" a test "')]
Darius Bacon
2
+1 Saya menggunakan ini karena itu jauh lebih cepat daripada shlex.
hanleyp
3
Mengapa triple backslash? tidakkah backslash sederhana akan melakukan hal yang sama?
Doppelganger
1
Sebenarnya, satu hal yang saya tidak suka tentang ini adalah bahwa apapun sebelum / sesudah kutipan tidak dibagi dengan benar. Jika saya memiliki string seperti ini 'PARAMS val1 = "Thing" val2 = "Thing2"'. Saya berharap string akan terbagi menjadi tiga bagian, tetapi terbagi menjadi 5. Sudah lama sejak saya melakukan regex, jadi saya tidak merasa ingin mencoba menyelesaikannya menggunakan solusi Anda sekarang.
leetNightshade
1
Anda harus menggunakan string mentah saat menggunakan ekspresi reguler.
penanggung jawab
29

Tergantung pada kasus penggunaan Anda, Anda mungkin juga ingin memeriksa csvmodul:

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

Keluaran:

['this', 'is', 'a string']
['and', 'more', 'stuff']
Ryan Ginstrom
sumber
2
berguna, ketika shlex menghapus beberapa karakter yang diperlukan
scraplesh
1
CSV menggunakan dua tanda kutip ganda berturut-turut (seperti pada sisi-sisinya, "") untuk mewakili satu tanda kutip ganda ", sehingga akan mengubah dua tanda kutip ganda menjadi tanda kutip tunggal 'this is "a string""'dan 'this is "a string"""'keduanya akan dipetakan ke['this', 'is', 'a string"']
Boris
15

Saya menggunakan shlex.split untuk memproses 70.000.000 baris squid log, ini sangat lambat. Jadi saya beralih ke kembali.

Silakan coba ini, jika Anda memiliki masalah kinerja dengan shlex.

import re

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

Karena pertanyaan ini ditandai dengan regex, saya memutuskan untuk mencoba pendekatan regex. Saya pertama-tama mengganti semua spasi di bagian kutipan dengan \ x00, lalu pisah dengan spasi, lalu ganti \ x00 kembali ke spasi di setiap bagian.

Kedua versi melakukan hal yang sama, tetapi splitter sedikit lebih mudah dibaca daripada 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)
Elifiner
sumber
Anda seharusnya menggunakan re.Scanner sebagai gantinya. Ini lebih dapat diandalkan (dan saya sebenarnya telah menerapkan shlex-like using re.Scanner).
Devin Jeanpierre
+1 Hm, ini adalah ide yang cukup cerdas, memecah masalah menjadi beberapa langkah sehingga jawabannya tidak terlalu rumit. Shlex tidak melakukan persis apa yang saya butuhkan, bahkan dengan mencoba men-tweak itu. Dan solusi single pass regex menjadi sangat aneh dan rumit.
leetNightshade
6

Tampaknya karena alasan kinerja relebih cepat. Inilah solusi saya menggunakan operator serakah yang menjaga kutipan luar:

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

Hasil:

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

Ini meninggalkan konstruksi seperti aaa"bla blub"bbbbersama karena token ini tidak dipisahkan oleh spasi. Jika string berisi karakter yang lolos, Anda dapat mencocokkan seperti itu:

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

Harap dicatat bahwa ini juga cocok dengan string kosong ""melalui \Sbagian dari pola.

hochl
sumber
1
Keuntungan penting lain dari solusi ini adalah keserbagunaannya sehubungan dengan karakter pembatas (misalnya ,via '(?:".*?"|[^,])+'). Hal yang sama berlaku untuk karakter kutipan (terlampir).
a_guest
4

Masalah utama dengan shlexpendekatan yang diterima adalah bahwa ia tidak mengabaikan karakter melarikan diri di luar substring yang dikutip, dan memberikan hasil yang sedikit tak terduga dalam beberapa kasus sudut.

Saya memiliki use case berikut, di mana saya membutuhkan fungsi split yang membagi string input sedemikian rupa sehingga baik substring yang dikutip tunggal atau ganda dikutip dipertahankan, dengan kemampuan untuk keluar dari kutipan dalam substring tersebut. Kutipan dalam string yang tidak dikutip tidak boleh diperlakukan secara berbeda dari karakter lain. Beberapa contoh uji kasus dengan output yang diharapkan:

masukan string | output yang diharapkan
===============================================
 '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']
 '"" tes' | ['', 'uji']
 "'' test" | ['', 'uji']
 "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 $ '"]

Saya berakhir dengan fungsi berikut untuk membagi string sehingga hasil output yang diharapkan untuk semua string input:

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

Aplikasi pengujian berikut memeriksa hasil pendekatan lain ( shlexdan csvuntuk saat ini) dan implementasi pemisahan kustom:

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

Keluaran:

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']
[GAGAL] pengecualian 'abc \' def 'ghi ->: Tidak ada kutipan penutup
[OK] 'abc \ s def' ghi -> ['abc \\ s def', 'ghi']
[OK] "abc \ s def" ghi -> ['abc \\ s def', 'ghi']
[OK] "" tes -> ['', 'tes']
[OK] '' tes -> ['', 'tes']
[GAGAL] abc'def -> pengecualian: Tidak ada kutipan penutup
[FAIL] abc'def '-> [' abcdef ']
[FAIL] abc'def 'ghi -> [' abcdef ',' ghi ']
[FAIL] abc'def'ghi -> ['abcdefghi']
[FAIL] abc "def -> exception: Tidak ada kutipan penutup
[FAIL] abc "def" -> ['abcdef']
[FAIL] abc "def" ghi -> ['abcdef', 'ghi']
[FAIL] abc "def" ghi -> ['abcdefghi']
[GAGAL] 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] "" tes -> ['', 'tes']
[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 $'"]

kembali

[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] "" tes -> ['', 'tes']
[OK] '' tes -> ['', 'tes']
[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,281ms per iterasi
csv: 0,030 ms per iterasi
re: 0,049ms per iterasi

Jadi kinerja jauh lebih baik daripada shlex, dan dapat ditingkatkan lebih lanjut dengan mengkompilasi ekspresi reguler, dalam hal ini akan mengungguli csvpendekatan.

Ton van den Heuvel
sumber
Tidak yakin apa yang Anda bicarakan: `` `>>> shlex.split ('ini adalah" tes "') ['ini', 'adalah', 'tes'] >>> shlex.split (' ini \\ "tes \\" ') [' ini ',' adalah ',' "a ',' tes" '] >>> shlex.split (' ini "a \\" test \\ " "') [' ini ',' adalah ',' sebuah" ujian "']` ``
morsik
@Morsik, apa maksudmu? Mungkin kotak penggunaan Anda tidak cocok dengan milikku? Ketika Anda melihat kasus uji Anda akan melihat semua kasus di mana shlextidak berperilaku seperti yang diharapkan untuk kasus penggunaan saya.
Ton van den Heuvel
3

Untuk menyimpan kutipan, gunakan fungsi ini:

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
sumber
Ketika membandingkan dengan string yang lebih besar, fungsi Anda sangat lambat
Faran2007
3

Tes kecepatan untuk jawaban yang berbeda:

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

Hmm, sepertinya tidak bisa menemukan tombol "Balas" ... lagi pula, jawaban ini didasarkan pada pendekatan oleh Kate, tetapi dengan benar membagi string dengan substring yang berisi tanda kutip yang lolos dan juga menghilangkan tanda kutip mulai dan akhir dari substring:

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

Ini berfungsi pada string seperti 'This is " a \\\"test\\\"\\\'s substring"'(sayangnya, markup gila diperlukan untuk menjaga Python dari menghapus lolos).

Jika hasil yang keluar dalam string dalam daftar yang dikembalikan tidak diinginkan, Anda dapat menggunakan versi fungsi yang sedikit diubah ini:

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

sumber
1

Untuk mengatasi masalah unicode di beberapa versi Python 2, saya sarankan:

from shlex import split as _split
split = lambda a: [b.decode('utf-8') for b in _split(a.encode('utf-8'))]
moschlar
sumber
Untuk python 2.7.5 ini harus: split = lambda a: [b.decode('utf-8') for b in _split(a)]jika tidak, Anda akan mendapatkan:UnicodeDecodeError: 'ascii' codec can't decode byte ... in position ...: ordinal not in range(128)
Peter Varo
1

Sebagai opsi coba tssplit:

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

Saya menyarankan:

string uji:

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

untuk menangkap juga "" dan '':

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

hasil:

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

untuk mengabaikan "" dan '' kosong:

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

hasil:

['abc', '"ad"', "'fg'", '"kk\'rdt\'"', 'zzz', '"34"', 'zzz']
hussic
sumber
Bisa juga ditulis re.findall("(?:\".*?\"|'.*?'|[^\s'\"]+)", s).
Hochl
-3

Jika Anda tidak peduli tentang sub string daripada yang sederhana

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

Kinerja:

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

Atau modul string

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

Kinerja: Modul string tampaknya berkinerja lebih baik daripada metode 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

Atau Anda bisa menggunakan mesin RE

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

Performa

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

Untuk string yang sangat panjang Anda tidak harus memuat seluruh string ke dalam memori dan sebaliknya membagi garis atau menggunakan loop berulang

Gregory
sumber
11
Anda sepertinya telah melewatkan seluruh inti pertanyaan. Ada bagian yang dikutip dalam string yang tidak perlu dibagi.
rjmunro
-3

Coba ini:

  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

Beberapa string uji:

'This is "a test"' -> ['This', 'is', 'a test']
'"This is \'a test\'"' -> ["This is 'a test'"]
pjz
sumber
Harap berikan repr string yang menurut Anda akan gagal.
pjz
Pikirkan ? adamsplit("This is 'a test'")['This', 'is', "'a", "test'"]
Matthew Schinckel
OP hanya mengatakan "dalam tanda kutip" dan hanya memiliki contoh dengan tanda kutip ganda.
pjz