簡體   English   中英

Python 中是否有 `string.split()` 的生成器版本?

[英]Is there a generator version of `string.split()` in Python?

string.split()返回一個列表實例。 是否有返回生成器的版本? 是否有任何理由反對使用生成器版本?

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 中需要恆定內存,假設我的測試方法是正確的。 我創建了一個非常大的字符串(1GB 左右),然后用for循環遍歷可迭代對象(不是列表理解,它會產生額外的內存)。 這並沒有導致顯着的內存增長(也就是說,如果內存有增長,遠遠小於 1GB 字符串)。

更通用的版本:

在回復“我看不到與str.split ”的評論時,這里有一個更通用的版本:

def splitStr(string, sep="\s+"):
    # warning: does not yet work if sep is a lookahead like `(?=b)`
    if sep=='':
        return (c for c in string)
    else:
        return (_.group(1) for _ in re.finditer(f'(?:^|{sep})((?:(?!{sep}).)*)', string))
    # alternatively, more verbosely:
    regex = f'(?:^|{sep})((?:(?!{sep}).)*)'
    for match in re.finditer(regex, string):
        fragment = match.group(1)
        yield fragment

這個想法是((?!pat).)*通過確保它貪婪地匹配直到模式開始匹配來'否定'一個組(前瞻不消耗正則表達式有限狀態機中的字符串)。 在偽代碼中:重復使用 ( begin-of-string xor {sep} ) + as much as possible until we would be able to begin again (or hit end of string)

演示:

>>> splitStr('.......A...b...c....', sep='...')
<generator object splitStr.<locals>.<genexpr> at 0x7fe8530fb5e8>

>>> list(splitStr('A,b,c.', sep=','))
['A', 'b', 'c.']

>>> list(splitStr(',,A,b,c.,', sep=','))
['', '', 'A', 'b', 'c.', '']

>>> list(splitStr('.......A...b...c....', '\.\.\.'))
['', '', '.A', 'b', 'c', '.']

>>> list(splitStr('   A  b  c. '))
['', 'A', 'b', 'c.', '']

(應該注意str.split有一個丑陋的行為:它的特殊情況是sep=None作為第一次執行str.strip以刪除前導和尾隨空格。上面故意沒有這樣做;請參閱最后一個示例,其中 sep= "\\s+" .)

(我在嘗試實現這個時遇到了各種錯誤(包括內部 re.error)......負后視將限制你使用固定長度的分隔符,所以我們不使用它。除了上面的正則表達式之外,幾乎任何東西似乎都會導致字符串開頭和字符串結尾邊緣情況的錯誤(例如r'(.*?)($|,)' on ',,,a,,b,c'返回['', '', '', 'a', '', 'b', 'c', '']末尾有一個無關的空字符串;你可以查看另一個看似正確的正則表達式的編輯歷史,但實際上有細微的錯誤.)

(如果你想自己實現它以獲得更高的性能(盡管它們是重量級的,最重要的是在 C 中運行的正則表達式),你會寫一些代碼(使用 ctypes?不確定如何讓生成器使用它?),如下固定長度分隔符的偽代碼:散列長度為 L 的分隔符。在使用運行散列算法掃描字符串時保持長度為 L 的運行散列,更新時間為 O(1)。每當散列可能等於您的分隔符時,手動檢查是否過去的幾個字符是分隔符;如果是這樣,則自上次產生以來產生子字符串。字符串開頭和結尾的特殊情況。這將是進行 O(N) 文本搜索的教科書算法的生成器版本。多處理版本也是可能。他們可能看起來有點矯枉過正,但這個問題意味着一個人正在處理非常大的字符串......那時你可能會考慮一些瘋狂的事情,比如緩存字節偏移量,如果它們很少,或者從磁盤使用一些磁盤支持的字節串視圖反對,買進 g 更多 RAM 等)

我能想到的最有效的方法是使用str.find()方法的offset參數編寫一個。 這避免了大量內存使用,並在不需要時依賴正則表達式的開銷。

[編輯 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() 或切片時在字符串中都會有一些成本搜索,但這應該是最小的,因為字符串在內存中表示為連續數組。

對提出的各種方法進行了一些性能測試(我不會在這里重復)。 一些結果:

  • str.split (默認值 = 0.3461570239996945
  • 手動搜索(按字符)(戴夫韋伯的答案之一)= 0.8260340550004912
  • re.finditer (忍者的回答)= 0.698872097000276
  • str.find (Eli Collins 的答案之一)= 0.7230395330007013
  • itertools.takewhile (Ignacio Vazquez-Abrams 的回答) = 2.023023967998597
  • str.split(..., maxsplit=1)遞歸 = N/A†

†遞歸答案( string.splitmaxsplit = 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使用了內存,但它的速度卻如此之快。

這是通過re.search() split()實現的split()生成器版本,它沒有分配太多子字符串的問題。

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

編輯:如果沒有給出分隔符,則更正了對周圍空白的處理。

這是我的實現,它比這里的其他答案要快得多,也更完整。 對於不同的情況,它有 4 個獨立的子功能。

我將復制主str_split函數的文檔字符串:


str_split(s, *delims, empty=None)

用其余的參數分割字符串s ,可能省略空部分( empty關鍵字參數負責)。 這是一個生成器函數。

當只提供一個分隔符時,字符串會被它簡單地分割。 默認情況下, emptyTrue

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

我寫了一個@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 的正則表達式模塊說它對 unicode 空格做了“正確的事情” ,但我還沒有真正測試過它。

也可作為要點

如果您還希望能夠讀取迭代器(以及返回一個),請嘗試以下操作:

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!']

不,但是使用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()

我認為 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)

more_itertools.split_at為迭代器提供了一個類似於str.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是第三方包。

我想展示如何使用 find_iter 解決方案為給定的分隔符返回一個生成器,然后使用 itertools 中的成對配方來構建上一個下一次迭代,這將獲得原始 split 方法中的實際單詞。


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. 我使用 prev & curr 而不是 prev & next 因為在 python 中覆蓋 next 是一個非常糟糕的主意
  2. 這是相當有效的

最愚蠢的方法,沒有正則表達式 / 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:]

很老的問題,但這是我對高效算法的謙虛貢獻:

def str_split(text: str, separator: str) -> Iterable[str]:
    i = 0
    n = len(text)
    while i <= n:
        j = text.find(separator, i)
        if j == -1:
            j = n
        yield text[i:j]
        i = j + 1
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

這是一個簡單的回復

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:]
def isplit(text, sep=None, maxsplit=-1):
    if not isinstance(text, (str, bytes)):
        raise TypeError(f"requires 'str' or 'bytes' but received a '{type(text).__name__}'")
    if sep in ('', b''):
        raise ValueError('empty separator')

    if maxsplit == 0 or not text:
        yield text
        return

    regex = (
        re.escape(sep) if sep is not None
        else [br'\s+', r'\s+'][isinstance(text, str)]
    )
    yield from re.split(regex, text, maxsplit=max(0, maxsplit))

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM