Apakah ada versi generator dari `string.split ()` dengan Python?

113

string.split()mengembalikan contoh daftar . Apakah ada versi yang mengembalikan generator ? Apakah ada alasan untuk tidak memiliki versi generator?

Manoj Govindan
sumber
3
Pertanyaan ini mungkin terkait.
Björn Pollex
1
Alasannya adalah sangat sulit untuk memikirkan kasus yang berguna. Kenapa kamu menginginkan ini?
Glenn Maynard
10
@ Glenn: Baru-baru ini saya melihat pertanyaan tentang membagi string panjang menjadi beberapa n kata. Salah satu solusi splitstring dan kemudian generator kembali bekerja pada hasil split. Itu membuat saya berpikir apakah ada cara untuk splitmengembalikan generator untuk memulai.
Manoj Govindan
5
Ada diskusi yang relevan tentang pelacak Masalah Python: bugs.python.org/issue17343
saffsd
@GlennMaynard dapat berguna untuk penguraian string / file yang sangat besar, tetapi siapa pun dapat menulis parser generator sendiri dengan sangat mudah menggunakan DFA dan hasil yang dibuat sendiri
Dmitry Ponyatov

Jawaban:

77

Sangat mungkin bahwa re.finditermenggunakan overhead memori yang cukup minimal.

def split_iter(string):
    return (x.group(0) for x in re.finditer(r"[A-Za-z']+", string))

Demo:

>>> list( split_iter("A programmer's RegEx test.") )
['A', "programmer's", 'RegEx', 'test']

edit: Saya baru saja mengonfirmasi bahwa ini membutuhkan memori konstan di python 3.2.1, dengan asumsi metodologi pengujian saya benar. Saya membuat string dengan ukuran yang sangat besar (1GB atau lebih), kemudian mengulang melalui iterable dengan forloop (BUKAN pemahaman daftar, yang akan menghasilkan memori tambahan). Ini tidak menghasilkan pertumbuhan memori yang nyata (yaitu, jika ada pertumbuhan memori, itu jauh lebih kecil daripada string 1GB).

ninjagecko
sumber
5
Luar biasa! Saya sudah lupa tentang finditer. Jika seseorang tertarik untuk melakukan sesuatu seperti splitlines, saya akan menyarankan menggunakan RE ini: '(. * \ N |. + $)' Str.splitlines memotong baris baru pelatihan (sesuatu yang saya tidak terlalu suka ... ); jika Anda ingin mereplikasi bagian perilaku tersebut, Anda dapat menggunakan pengelompokan: (m.group (2) atau m.group (3) untuk m di re.finditer ('((. *) \ n | (. +) $) ', s)). PS: Saya kira kurung luar di RE tidak diperlukan; Aku hanya merasa tidak nyaman menggunakan | tanpa tanda kurung: P
allyourcode
3
Bagaimana dengan kinerja? pencocokan ulang harus lebih lambat dari pencarian biasa.
anatoly techtonik
1
Bagaimana Anda menulis ulang fungsi split_iter ini agar berfungsi a_string.split("delimiter")?
Moberg
split menerima ekspresi reguler jadi tidak terlalu cepat, jika Anda ingin menggunakan nilai yang dikembalikan dengan cara selanjutnya, lihat jawaban saya di bagian bawah ...
Veltzer Doron
str.split()tidak menerima ekspresi reguler, itulah yang re.split()Anda pikirkan ...
alexis
17

Cara paling efisien yang dapat saya pikirkan untuk menulis satu menggunakan offsetparameter str.find()metode. Hal ini menghindari banyak penggunaan memori, dan mengandalkan overhead regexp saat tidak diperlukan.

[edit 2016-8-2: memperbarui ini untuk mendukung pemisah regex secara opsional]

def isplit(source, sep=None, regex=False):
    """
    generator version of str.split()

    :param source:
        source string (unicode or bytes)

    :param sep:
        separator to split on.

    :param regex:
        if True, will treat sep as regular expression.

    :returns:
        generator yielding elements of string.
    """
    if sep is None:
        # mimic default python behavior
        source = source.strip()
        sep = "\\s+"
        if isinstance(source, bytes):
            sep = sep.encode("ascii")
        regex = True
    if regex:
        # version using re.finditer()
        if not hasattr(sep, "finditer"):
            sep = re.compile(sep)
        start = 0
        for m in sep.finditer(source):
            idx = m.start()
            assert idx >= start
            yield source[start:idx]
            start = m.end()
        yield source[start:]
    else:
        # version using str.find(), less overhead than re.finditer()
        sepsize = len(sep)
        start = 0
        while True:
            idx = source.find(sep, start)
            if idx == -1:
                yield source[start:]
                return
            yield source[start:idx]
            start = idx + sepsize

Ini dapat digunakan seperti yang Anda inginkan ...

>>> print list(isplit("abcb","b"))
['a','c','']

Meskipun ada sedikit biaya pencarian dalam string setiap kali find () atau slicing dilakukan, ini harus minimal karena string direpresentasikan sebagai array yang bersebelahan dalam memori.

Eli Collins
sumber
10

Ini adalah versi generator yang split()diimplementasikan melalui re.search()yang tidak memiliki masalah dalam mengalokasikan terlalu banyak substring.

import re

def itersplit(s, sep=None):
    exp = re.compile(r'\s+' if sep is None else re.escape(sep))
    pos = 0
    while True:
        m = exp.search(s, pos)
        if not m:
            if pos < len(s) or sep is not None:
                yield s[pos:]
            break
        if pos < m.start() or sep is not None:
            yield s[pos:m.start()]
        pos = m.end()


sample1 = "Good evening, world!"
sample2 = " Good evening, world! "
sample3 = "brackets][all][][over][here"
sample4 = "][brackets][all][][over][here]["

assert list(itersplit(sample1)) == sample1.split()
assert list(itersplit(sample2)) == sample2.split()
assert list(itersplit(sample3, '][')) == sample3.split('][')
assert list(itersplit(sample4, '][')) == sample4.split('][')

EDIT: Perbaikan penanganan spasi di sekitar jika tidak ada karakter pemisah yang diberikan.

Bernd Petersohn
sumber
12
mengapa ini lebih baik dari re.finditer?
Erik Kaplun
@ErikKaplun Karena logika regex untuk item bisa lebih kompleks daripada pemisahnya. Dalam kasus saya, saya ingin memproses setiap baris satu per satu, jadi saya dapat melaporkan kembali jika satu baris gagal untuk mencocokkan.
rovyko
9

Melakukan beberapa pengujian kinerja pada berbagai metode yang diusulkan (saya tidak akan mengulanginya di sini). Beberapa hasil:

  • str.split (default = 0,3461570239996945
  • pencarian manual (menurut karakter) (salah satu jawaban Dave Webb) = 0.8260340550004912
  • re.finditer (jawaban ninjagecko) = 0.698872097000276
  • str.find (salah satu jawaban Eli Collins) = 0.7230395330007013
  • itertools.takewhile (Jawaban Ignacio Vazquez-Abrams) = 2.023023967998597
  • str.split(..., maxsplit=1) rekursi = N / A †

† Jawaban rekursi ( string.splitdengan maxsplit = 1) gagal diselesaikan dalam waktu yang wajar, mengingat string.splitkecepatannya, jawaban tersebut dapat berfungsi lebih baik pada string yang lebih pendek, tetapi saya tidak dapat melihat kasus penggunaan untuk string pendek di mana memori bukanlah masalah.

Diuji menggunakan timeitpada:

the_text = "100 " * 9999 + "100"

def test_function( method ):
    def fn( ):
        total = 0

        for x in method( the_text ):
            total += int( x )

        return total

    return fn

Ini menimbulkan pertanyaan lain mengapa string.splitjauh lebih cepat meskipun menggunakan memori.

cz
sumber
2
Ini karena memori lebih lambat daripada cpu dan dalam kasus ini, daftar dimuat oleh potongan di mana semua yang lain dimuat elemen demi elemen. Pada catatan yang sama, banyak akademisi akan memberi tahu Anda daftar tertaut lebih cepat dan memiliki lebih sedikit kerumitan sementara komputer Anda akan sering lebih cepat dengan array, yang menurutnya lebih mudah untuk dioptimalkan. Anda tidak dapat berasumsi bahwa suatu opsi lebih cepat dari yang lain, ujilah! 1 untuk pengujian.
Benoît P
Masalah muncul pada langkah selanjutnya dari rantai pemrosesan. Jika Anda kemudian ingin mencari potongan tertentu dan mengabaikan sisanya saat Anda menemukannya, Anda memiliki alasan untuk menggunakan pemisahan berbasis generator dan bukan solusi bawaan.
jgomo3
6

Inilah implementasi saya, yang jauh, jauh lebih cepat dan lebih lengkap daripada jawaban lain di sini. Ini memiliki 4 subfungsi terpisah untuk kasus yang berbeda.

Saya hanya akan menyalin docstring dari str_splitfungsi utama :


str_split(s, *delims, empty=None)

Pisahkan string sdengan sisa argumen, mungkin menghilangkan bagian kosong (empty argumen kata kunci bertanggung jawab untuk itu). Ini adalah fungsi generator.

Jika hanya satu pembatas yang diberikan, string akan dipisahkan dengannya. emptykemudian Truesecara default.

str_split('[]aaa[][]bb[c', '[]')
    -> '', 'aaa', '', 'bb[c'
str_split('[]aaa[][]bb[c', '[]', empty=False)
    -> 'aaa', 'bb[c'

Ketika beberapa pembatas disediakan, string dipisahkan dengan urutan terpanjang dari pembatas tersebut secara default, atau, jika emptydiatur ke True, string kosong antara pembatas juga disertakan. Perhatikan bahwa pembatas dalam kasus ini hanya boleh satu karakter.

str_split('aaa, bb : c;', ' ', ',', ':', ';')
    -> 'aaa', 'bb', 'c'
str_split('aaa, bb : c;', *' ,:;', empty=True)
    -> 'aaa', '', 'bb', '', '', 'c', ''

Jika tidak ada pembatas yang disuplai, string.whitespacedigunakan, sehingga efeknya sama str.split(), kecuali fungsi ini adalah generator.

str_split('aaa\\t  bb c \\n')
    -> 'aaa', 'bb', 'c'

import string

def _str_split_chars(s, delims):
    "Split the string `s` by characters contained in `delims`, including the \
    empty parts between two consecutive delimiters"
    start = 0
    for i, c in enumerate(s):
        if c in delims:
            yield s[start:i]
            start = i+1
    yield s[start:]

def _str_split_chars_ne(s, delims):
    "Split the string `s` by longest possible sequences of characters \
    contained in `delims`"
    start = 0
    in_s = False
    for i, c in enumerate(s):
        if c in delims:
            if in_s:
                yield s[start:i]
                in_s = False
        else:
            if not in_s:
                in_s = True
                start = i
    if in_s:
        yield s[start:]


def _str_split_word(s, delim):
    "Split the string `s` by the string `delim`"
    dlen = len(delim)
    start = 0
    try:
        while True:
            i = s.index(delim, start)
            yield s[start:i]
            start = i+dlen
    except ValueError:
        pass
    yield s[start:]

def _str_split_word_ne(s, delim):
    "Split the string `s` by the string `delim`, not including empty parts \
    between two consecutive delimiters"
    dlen = len(delim)
    start = 0
    try:
        while True:
            i = s.index(delim, start)
            if start!=i:
                yield s[start:i]
            start = i+dlen
    except ValueError:
        pass
    if start<len(s):
        yield s[start:]


def str_split(s, *delims, empty=None):
    """\
Split the string `s` by the rest of the arguments, possibly omitting
empty parts (`empty` keyword argument is responsible for that).
This is a generator function.

When only one delimiter is supplied, the string is simply split by it.
`empty` is then `True` by default.
    str_split('[]aaa[][]bb[c', '[]')
        -> '', 'aaa', '', 'bb[c'
    str_split('[]aaa[][]bb[c', '[]', empty=False)
        -> 'aaa', 'bb[c'

When multiple delimiters are supplied, the string is split by longest
possible sequences of those delimiters by default, or, if `empty` is set to
`True`, empty strings between the delimiters are also included. Note that
the delimiters in this case may only be single characters.
    str_split('aaa, bb : c;', ' ', ',', ':', ';')
        -> 'aaa', 'bb', 'c'
    str_split('aaa, bb : c;', *' ,:;', empty=True)
        -> 'aaa', '', 'bb', '', '', 'c', ''

When no delimiters are supplied, `string.whitespace` is used, so the effect
is the same as `str.split()`, except this function is a generator.
    str_split('aaa\\t  bb c \\n')
        -> 'aaa', 'bb', 'c'
"""
    if len(delims)==1:
        f = _str_split_word if empty is None or empty else _str_split_word_ne
        return f(s, delims[0])
    if len(delims)==0:
        delims = string.whitespace
    delims = set(delims) if len(delims)>=4 else ''.join(delims)
    if any(len(d)>1 for d in delims):
        raise ValueError("Only 1-character multiple delimiters are supported")
    f = _str_split_chars if empty else _str_split_chars_ne
    return f(s, delims)

Fungsi ini bekerja di Python 3, dan perbaikan yang mudah, meskipun cukup jelek, dapat diterapkan untuk membuatnya berfungsi di versi 2 dan 3. Baris pertama dari fungsi tersebut harus diubah menjadi:

def str_split(s, *delims, **kwargs):
    """...docstring..."""
    empty = kwargs.get('empty')
Oleh Prypin
sumber
3

Tidak, tetapi seharusnya cukup mudah untuk menulis satu menggunakan itertools.takewhile() .

EDIT:

Penerapan yang sangat sederhana dan setengah rusak:

import itertools
import string

def isplitwords(s):
  i = iter(s)
  while True:
    r = []
    for c in itertools.takewhile(lambda x: not x in string.whitespace, i):
      r.append(c)
    else:
      if r:
        yield ''.join(r)
        continue
      else:
        raise StopIteration()
Ignacio Vazquez-Abrams
sumber
@Ignacio: Contoh dalam dokumen menggunakan daftar bilangan bulat untuk menggambarkan penggunaan takeWhile. Apa yang bagus predicateuntuk memisahkan string menjadi kata-kata (default split) menggunakan takeWhile()?
Manoj Govindan
Cari keberadaan di string.whitespace.
Ignacio Vazquez-Abrams
Pemisah dapat memiliki banyak karakter,'abc<def<>ghi<><>lmn'.split('<>') == ['abc<def', 'ghi', '', 'lmn']
kennytm
@Ignacio: Bisakah Anda menambahkan contoh pada jawaban Anda?
Manoj Govindan
1
Mudah untuk menulis, tetapi banyak lipat lebih lambat. Ini adalah operasi yang benar-benar harus diterapkan dalam kode asli.
Glenn Maynard
3

Saya tidak melihat manfaat yang jelas pada versi generator split() . Objek generator harus berisi seluruh string untuk mengulang sehingga Anda tidak akan menghemat memori dengan memiliki generator.

Jika Anda ingin menulisnya, itu akan cukup mudah:

import string

def gsplit(s,sep=string.whitespace):
    word = []

    for c in s:
        if c in sep:
            if word:
                yield "".join(word)
                word = []
        else:
            word.append(c)

    if word:
        yield "".join(word)
Dave Webb
sumber
3
Anda akan membagi separuh memori yang digunakan, dengan tidak harus menyimpan salinan kedua dari string di setiap bagian yang dihasilkan, ditambah overhead array dan objek (yang biasanya lebih dari string itu sendiri). Itu umumnya tidak masalah, meskipun (jika Anda membagi string begitu besar sehingga ini penting, Anda mungkin melakukan sesuatu yang salah), dan bahkan implementasi generator C asli akan selalu jauh lebih lambat daripada melakukannya sekaligus.
Glenn Maynard
@ Glenn Maynard - Saya baru menyadarinya. Saya untuk beberapa alasan saya awalnya generator akan menyimpan salinan string daripada referensi. Pemeriksaan cepat dengan id()membuat saya benar. Dan jelas karena string tidak dapat diubah, Anda tidak perlu khawatir tentang seseorang yang mengubah string asli saat Anda mengulanginya.
Dave Webb
6
Bukankah poin utama dalam menggunakan generator bukanlah penggunaan memori, tetapi Anda dapat menyelamatkan diri Anda sendiri karena harus membagi seluruh string jika Anda ingin keluar lebih awal? (Itu bukan komentar tentang solusi khusus Anda, saya hanya terkejut dengan diskusi tentang memori).
Scott Griffiths
@Scott: Sulit untuk memikirkan kasus di mana itu benar-benar menang - di mana 1: Anda ingin berhenti membelah di tengah jalan, 2: Anda tidak tahu berapa banyak kata yang Anda pisahkan sebelumnya, 3: Anda memiliki string yang cukup besar untuk menjadi masalah, dan 4: Anda secara konsisten berhenti cukup awal agar itu menjadi kemenangan yang signifikan atas str.split. Itu adalah serangkaian kondisi yang sangat sempit.
Glenn Maynard
4
Anda bisa mendapatkan keuntungan yang jauh lebih tinggi jika string Anda dibuat dengan malas juga (misalnya dari lalu lintas jaringan atau file dibaca)
Lie Ryan
3

Saya menulis versi jawaban @ ninjagecko yang berperilaku lebih seperti string.split (yaitu dipisahkan spasi putih secara default dan Anda dapat menentukan pembatas).

def isplit(string, delimiter = None):
    """Like string.split but returns an iterator (lazy)

    Multiple character delimters are not handled.
    """

    if delimiter is None:
        # Whitespace delimited by default
        delim = r"\s"

    elif len(delimiter) != 1:
        raise ValueError("Can only handle single character delimiters",
                        delimiter)

    else:
        # Escape, incase it's "\", "*" etc.
        delim = re.escape(delimiter)

    return (x.group(0) for x in re.finditer(r"[^{}]+".format(delim), string))

Berikut adalah tes yang saya gunakan (di python 3 dan python 2):

# Wrapper to make it a list
def helper(*args,  **kwargs):
    return list(isplit(*args, **kwargs))

# Normal delimiters
assert helper("1,2,3", ",") == ["1", "2", "3"]
assert helper("1;2;3,", ";") == ["1", "2", "3,"]
assert helper("1;2 ;3,  ", ";") == ["1", "2 ", "3,  "]

# Whitespace
assert helper("1 2 3") == ["1", "2", "3"]
assert helper("1\t2\t3") == ["1", "2", "3"]
assert helper("1\t2 \t3") == ["1", "2", "3"]
assert helper("1\n2\n3") == ["1", "2", "3"]

# Surrounding whitespace dropped
assert helper(" 1 2  3  ") == ["1", "2", "3"]

# Regex special characters
assert helper(r"1\2\3", "\\") == ["1", "2", "3"]
assert helper(r"1*2*3", "*") == ["1", "2", "3"]

# No multi-char delimiters allowed
try:
    helper(r"1,.2,.3", ",.")
    assert False
except ValueError:
    pass

Modul regex python mengatakan bahwa ia melakukan "hal yang benar" untuk whitespace unicode, tetapi saya belum benar-benar mengujinya.

Juga tersedia sebagai intinya .

dshepherd
sumber
3

Jika Anda juga ingin bisa membaca iterator (serta mengembalikannya ) coba ini:

import itertools as it

def iter_split(string, sep=None):
    sep = sep or ' '
    groups = it.groupby(string, lambda s: s != sep)
    return (''.join(g) for k, g in groups if k)

Pemakaian

>>> list(iter_split(iter("Good evening, world!")))
['Good', 'evening,', 'world!']
reubano
sumber
3

more_itertools.split_atmenawarkan analog ke str.splituntuk iterator.

>>> import more_itertools as mit


>>> list(mit.split_at("abcdcba", lambda x: x == "b"))
[['a'], ['c', 'd', 'c'], ['a']]

>>> "abcdcba".split("b")
['a', 'cdc', 'a']

more_itertools adalah paket pihak ketiga.

pylang
sumber
1
Perhatikan bahwa more_itertools.split_at () masih menggunakan daftar yang baru dialokasikan pada setiap panggilan, jadi meskipun ini mengembalikan iterator, itu tidak mencapai persyaratan memori konstan. Jadi tergantung pada mengapa Anda menginginkan iterator untuk memulai, ini mungkin atau mungkin tidak membantu.
jcater
@jater Poin yang bagus. Nilai antara memang di-buffer sebagai sub list di dalam iterator, menurut implementasinya . Seseorang dapat menyesuaikan sumber untuk mengganti daftar dengan iterator, menambahkan itertools.chaindan mengevaluasi hasil menggunakan pemahaman daftar. Bergantung pada kebutuhan dan permintaan, saya dapat memposting contoh.
pylang
2

Saya ingin menunjukkan bagaimana menggunakan solusi find_iter untuk mengembalikan generator untuk pembatas yang diberikan dan kemudian menggunakan resep berpasangan dari itertools untuk membangun iterasi berikutnya sebelumnya yang akan mendapatkan kata-kata yang sebenarnya seperti pada metode split asli.


from more_itertools import pairwise
import re

string = "dasdha hasud hasuid hsuia dhsuai dhasiu dhaui d"
delimiter = " "
# split according to the given delimiter including segments beginning at the beginning and ending at the end
for prev, curr in pairwise(re.finditer("^|[{0}]+|$".format(delimiter), string)):
    print(string[prev.end(): curr.start()])

catatan:

  1. Saya menggunakan prev & curr daripada prev & next karena mengganti next di python adalah ide yang sangat buruk
  2. Ini cukup efisien
Veltzer Doron
sumber
1

Metode terbodoh, tanpa regex / itertools:

def isplit(text, split='\n'):
    while text != '':
        end = text.find(split)

        if end == -1:
            yield text
            text = ''
        else:
            yield text[:end]
            text = text[end + 1:]
Tavy
sumber
0
def split_generator(f,s):
    """
    f is a string, s is the substring we split on.
    This produces a generator rather than a possibly
    memory intensive list. 
    """
    i=0
    j=0
    while j<len(f):
        if i>=len(f):
            yield f[j:]
            j=i
        elif f[i] != s:
            i=i+1
        else:
            yield [f[j:i]]
            j=i+1
            i=i+1
travelingbones
sumber
mengapa Anda mengalah [f[j:i]]dan tidak f[j:i]?
Moberg
0

berikut adalah tanggapan sederhana

def gen_str(some_string, sep):
    j=0
    guard = len(some_string)-1
    for i,s in enumerate(some_string):
        if s == sep:
           yield some_string[j:i]
           j=i+1
        elif i!=guard:
           continue
        else:
           yield some_string[j:]
pengguna1438644
sumber