Pythonに `string.split()`のジェネレータバージョンはありますか?


113

string.split()リストインスタンスを返します。代わりにジェネレータを返すバージョンはありますか?ジェネレータバージョンを使用することに何らかの理由がありますか?


3
この質問は関連している可能性があります。
ビョルンポレックス

1
その理由は、それが役立つ場合を考えるのは非常に難しいからです。なぜこれが欲しいのですか?
Glenn Maynard

10
@Glenn:最近、長い文字列をnワードのチャンクに分割することについての質問を見ました。ソリューションsplitの1つは文字列であり、の結果を処理するジェネレータを返しましたsplit。そもそもsplit、ジェネレーターを元に戻す方法があるかどうかと思いました。
Manoj Govindan

5
Python Issueトラッカーに関する関連する議論があります:bugs.python.org/issue17343
saffsd

@GlennMaynardそれは本当に大きな裸の文字列/ファイルの解析に役立つ可能性がありますが、誰もが自作のDFAを使用してジェネレータパーサーを非常に簡単に記述できます
Dmitry Ponyatov

回答:


77

re.finditerメモリのオーバーヘッドをかなり最小限に抑える可能性が非常に高いです。

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

デモ:

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

編集:私のテスト方法論が正しいと仮定して、これはpython 3.2.1で一定のメモリを必要とすることを確認しました。私は非常に大きなサイズ(1 GB程度)の文字列を作成し、forループで反復可能オブジェクトを反復処理しました(リスト内包表記ではなく、余分なメモリが生成されていました)。これにより、メモリが著しく増加することはありませんでした(つまり、メモリが増加した場合、1GBの文字列よりもはるかに少なくなります)。


5
優れた!発見者のことを忘れていました。スプリットラインのようなことをすることに興味があるなら、このREを使用することをお勧めします: '(。* \ n |。+ $)' str.splitlinesは訓練中の改行を切り落とします(私が本当に好きではないもの...) ); 動作のその部分を複製したい場合は、グループ化を使用できます:(m.group(2)またはm.group(3)m for re.finditer( '((。*)\ n |(。+) $) '、s))。PS:REの外側の括弧は不要だと思います。使うのに不安を感じます|
かっこ

3
パフォーマンスはどうですか?再照合は通常の検索よりも遅くなるはずです。
anatoly techtonik 16

1
このsplit_iter関数を次のように書き換えるにはどうしますa_string.split("delimiter")か?
Moberg、2016年

とにかく分割は正規表現を受け入れるので、それほど速くはありません。戻り値を前の次の方法で使用する場合は、一番下の私の答えを見てください...
Veltzer Doron

str.split()正規表現を受け入れない、それはre.split()あなたが考えていることです...
アレクシス

17

メソッドのoffsetパラメーターを使用して記述する最も効率的なstr.find()方法です。これにより、大量のメモリ使用が回避され、不要な場合は正規表現のオーバーヘッドに依存します。

[編集2016-8-2:これを更新して、オプションで正規表現の区切り文字をサポートします]

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

これは好きなように使えます...

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

find()またはスライスが実行されるたびに文字列内をシークするコストは少しありますが、文字列はメモリ内の連続した配列として表されるため、これは最小限に抑える必要があります。


10

これはをsplit()介しre.search()て実装されたジェネレーターバージョンであり、あまりにも多くの部分文字列を割り当てる問題はありません。

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

編集:区切り文字が指定されていない場合、周囲の空白の処理が修正されました。


12
なんでこれより良いのre.finditer
エリックカプルン2013年

@ErikKaplunアイテムの正規表現ロジックは、セパレーターよりも複雑になる可能性があるためです。私の場合、各行を個別に処理したかったので、行が一致しなかった場合に報告できます。
rovyko

8

提案されたさまざまな方法のパフォーマンステストをいくつか行いました(ここでは繰り返しません)。いくつかの結果:

  • str.split (デフォルト= 0.3461570239996945
  • 手動検索(文字による)(Dave Webbの回答の1つ)= 0.8260340550004912
  • re.finditer (ninjageckoの答え)= 0.698872097000276
  • str.find (エリコリンズの回答の1つ)= 0.7230395330007013
  • itertools.takewhile (イグナシオバスケスアブラムスの答え)= 2.023023967998597
  • str.split(..., maxsplit=1) 再帰= N / A†

†再帰応答(string.splitを使用maxsplit = 1)は、適切な時間内に完了しないためstring.split、短い文字列では速度が向上する可能性がありますが、メモリが問題にならない短い文字列の使用例を確認できません。

を使用timeitしてテスト:

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

これはstring.split、メモリ使用量にもかかわらず、なぜそれほど高速であるかについて別の質問を投げかけます。


1
これは、メモリがcpuよりも遅いためです。この場合、リストはチャンクで読み込まれ、他のすべての要素は要素ごとに読み込まれます。同じように、多くの学者は、リンクされたリストの方が速く、複雑さが少ないと言っていますが、コンピュータは多くの場合、配列を使用すると高速になり、最適化が容易になります。オプションが他のオプションよりも速いと仮定することはできません。テストしてください!テスト用+1。
ブノワP

問題は、処理チェーンの次のステップで発生します。次に、特定のチャンクを見つけて、残りを見つけたいときに無視したい場合は、組み込みのソリューションの代わりにジェネレーターベースのスプリットを使用する正当な理由があります。
jgomo3

6

これが私の実装です。これは、他の答えよりもはるかに高速で完全です。ケースごとに4つのサブ機能があります。

main str_split関数のdocstringをコピーします。


str_split(s, *delims, empty=None)

s空の部分を省略して、残りの引数で文字列を分割します(emptyキーワード引数がその原因です)。これはジェネレーター関数です。

区切り文字が1つだけ指定された場合、文字列はそれによって単に分割されます。 emptyその場合True、デフォルトです。

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

複数の区切り文字が指定されている場合、文字列はデフォルトでそれらの区切り文字の可能な限り長いシーケンスで分割されます。またはにempty設定されている 場合True、区切り文字間の空の文字列も含まれます。この場合の区切り文字は単一の文字のみであることに注意してください。

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

区切り文字が指定されていない場合string.whitespaceはが使用されるため、str.split()この関数がジェネレータであることを除いて、効果はと同じです。

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)

この関数はPython 3で機能し、2つのバージョンと3つのバージョンの両方で機能するように、簡単ではありますがかなり醜い修正を適用できます。関数の最初の行を次のように変更する必要があります。

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

3

いいえ。ただし、を使用して簡単に作成できitertools.takewhile()ます。

編集:

非常にシンプルで壊れた実装:

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:docsの例では、整数のリストを使用しての使用を説明していますtakeWhilepredicate文字列を単語(デフォルトsplit)に分割するために何が良いでしょうtakeWhile()か?
Manoj Govindan

での存在を探してくださいstring.whitespace
Ignacio Vazquez-Abrams

区切り文字には複数の文字を'abc<def<>ghi<><>lmn'.split('<>') == ['abc<def', 'ghi', '', 'lmn']
含める

@Ignacio:回答に例を追加できますか?
Manoj Govindan

1
書きやすいが、多くの遅く桁違い。これは、ネイティブコードで実際に実装する必要がある操作です。
Glenn Maynard

3

のジェネレーターバージョンに明らかな利点はありませんsplit()。ジェネレーターオブジェクトには、反復する文字列全体を含める必要があるため、ジェネレーターを使用してメモリを節約する必要はありません。

あなたがそれを書きたいなら、それはかなり簡単ですが:

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)

3
結果の各部分に文字列の2番目のコピーを格納する必要がないため、使用されるメモリが半分になり、さらに配列とオブジェクトのオーバーヘッド(通常は文字列自体よりも多くなります)になります。ただし、これは一般的に問題ではありません(これが問題になるほど文字列を分割している場合は、おそらく何かがおかしいでしょう)。ネイティブのCジェネレーターの実装でさえ、一度にすべてを行うよりも常にかなり遅くなります。
Glenn Maynard

@Glenn Maynard-気づいた。何らかの理由で、元々ジェネレーターは参照ではなく文字列のコピーを保存していました。簡単に確認しid()てください。また、文字列は不変であるため、繰り返し処理中に誰かが元の文字列を変更することを心配する必要はありません。
Dave Webb

6
メモリ使用量ではなくジェネレータを使用することの主なポイントではありませんが、早期に終了したい場合は、文字列全体を分割する手間を省くことができますか?(それはあなたの特定の解決策についてのコメントではありません。私は記憶についての議論に驚いただけです)。
スコットグリフィス

@スコット:それが本当に勝利である場合を考えるのは難しいです。1:途中で分割を停止したい、2:事前に分割している単語の数がわからない、3:あなたはそれが問題になるのに十分な大きさの文字列、4:str.splitに対して大きな勝利を収めるために、十分に早く停止します。これは非常に狭い条件のセットです。
Glenn Maynard

4
文字列も遅延して生成された場合(ネットワークトラフィックやファイルの読み取りなど)は、はるかに大きなメリットがあります
Lie Ryan

3

@ninjageckoの回答のバージョンを作成しました。これはstring.splitのように動作します(つまり、デフォルトで空白が区切られており、区切り文字を指定できます)。

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

ここに私が使ったテストがあります(python 3と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

Pythonのregexモジュールは、Unicodeの空白に対して「正しいこと」行うと言っていますが、実際にはテストしていません。

要旨としてもご利用いただけます。


3

イテレータを読み取ることができるようにしたい場合(そしてイテレータを返す場合も)、次のようにしてください。

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)

使用法

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

3

more_itertools.split_atstr.splitイテレータのアナログを提供します。

>>> 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 サードパーティのパッケージです。


1
more_itertools.split_at()は、各呼び出しで新しく割り当てられたリストを引き続き使用しているため、これは反復子を返しますが、一定のメモリ要件を達成していないことに注意してください。したがって、イテレータを最初に使用する理由に応じて、これが役立つ場合と役に立たない場合があります。
jcater

@jcater良い点。実装に応じて、中間値はイテレータ内のサブリストとして実際にバッファリングされます。リストをイテレータで置き換え、ソースを追加しitertools.chain、リスト内包表記を使用して結果を追加および評価できます。必要や要望に応じて、例を掲載することができます。
pylang

2

find_iterソリューションを使用して特定の区切り文字のジェネレーターを返し、次にitertoolsのペアワイズレシピを使用して、元の分割メソッドのように実際の単語を取得する前の次の反復を構築する方法を示したいと思いました。


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

注意:

  1. Pythonでの次のオーバーライドは非常に悪い考えであるため、私はprev&nextではなくprev&currを使用します。
  2. これはかなり効率的です

1

正規表現/ 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:]

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

なぜあなたは降伏し[f[j:i]]、そうではないのf[j:i]ですか?
Moberg、2014

0

ここに簡単な応答があります

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:]
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.