【问题标题】:Is there a generator version of `string.split()` in Python?Python中是否有`string.split()`的生成器版本?
【发布时间】:2011-04-21 04:37:39
【问题描述】:

string.split() 返回一个 list 实例。有没有返回generator 的版本?有没有理由反对使用生成器版本?

【问题讨论】:

  • This question 可能是相关的。
  • 原因是很难想出一个有用的案例。你为什么要这个?
  • @Glenn:最近我看到一个关于将长字符串拆分为 n 个单词的块的问题。解决方案之一是split 字符串,然后返回一个处理split 结果的生成器。这让我开始思考split 是否有办法返回一个生成器。
  • Python Issue tracker 上有相关讨论:bugs.python.org/issue17343
  • @GlennMaynard 它对于非常大的裸字符串/文件解析很有用,但是任何人都可以使用自酿的 DFA 和 yield 非常轻松地自己编写生成器解析器

标签: python string generator


【解决方案1】:

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)... 负向后视会将您限制为固定长度的定界符,因此我们不使用它。除了上述正则表达式之外,几乎所有东西似乎导致字符串开头和字符串结尾边缘情况的错误(例如,',,,a,,b,c' 上的r'(.*?)($|,)' 返回['', '', '', 'a', '', 'b', 'c', ''],末尾有一个无关的空字符串;可以查看编辑历史另一个看似正确的正则表达式,实际上有细微的错误。)

(如果你想自己实现它以获得更高的性能(尽管它们是重量级的,正则表达式最重要的是在 C 中运行),你会编写一些代码(使用 ctypes?不确定如何让生成器使用它?),使用以下固定长度定界符的伪代码:散列长度为 L 的定界符。在使用运行的散列算法扫描字符串时,保持长度为 L 的运行散列,O(1) 更新时间。每当散列可能等于你的定界符时,手动检查过去的几个字符是否是分隔符;如果是,则从上次 yield 开始产生子字符串。字符串开头和结尾的特殊情况。这将是教科书算法的生成器版本,用于执行 O(N) 文本搜索。多处理版本也是可能的。它们可能看起来有点矫枉过正,但问题意味着一个人正在处理非常大的字符串......此时您可能会考虑一些疯狂的事情,例如缓存字节偏移量(如果它们很少的话),或者使用一些磁盘从磁盘工作-支持的字节串视图对象,bu需要更多的内存等)

【讨论】:

  • 太棒了!我忘记了查找器。如果有人有兴趣做类似分割线的事情,我建议使用这个 RE: '(.*\n|.+$)' str.splitlines 砍掉训练换行符(我不太喜欢的东西...... );如果你想复制那部分行为,你可以使用分组: (m.group(2) or m.group(3) for m in re.finditer('((.*)\n|(.+) $)', s))。 PS:我猜RE中的外部括号是不需要的;我只是对使用感到不安|没有括号:P
  • 性能怎么样?重新匹配应该比普通搜索慢。
  • 你将如何重写这个 split_iter 函数以像 a_string.split("delimiter") 一样工作?
  • split 无论如何都接受正则表达式,所以它并不是真的更快,如果你想以 prev next 方式使用返回值,请查看底部的我的答案......
  • str.split() 不接受正则表达式,你想的就是re.split()...
【解决方案2】:

我能想到的最有效的方法是使用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() 或切片时都会在字符串中寻找一点成本,但这应该是最小的,因为字符串在内存中表示为连续数组。

【讨论】:

    【解决方案3】:

    对提出的各种方法进行了一些性能测试(我不会在这里重复)。一些结果:

    • str.split(默认 = 0.3461570239996945
    • 手动搜索(按字符)(Dave Webb 的答案之一)= 0.8260340550004912
    • re.finditer(忍者的回答)= 0.698872097000276
    • str.find(Eli Collins 的答案之一)= 0.7230395330007013
    • itertools.takewhile(伊格纳西奥·巴斯克斯-艾布拉姆斯的回答)= 2.023023967998597
    • str.split(..., maxsplit=1) 递归 = N/A†

    †鉴于string.splits 的速度,递归答案(string.splitmaxsplit = 1)无法在合理的时间内完成,它们在较短的字符串上可能效果更好,但是我看不到使用- 短字符串的情况,无论如何内存都不是问题。

    使用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 尽管占用了内存却要快得多。

    【讨论】:

    • 这是因为内存比 cpu 慢,在这种情况下,列表是按块加载的,而所有其他列表都是按元素加载的。同样,许多学者会告诉您链表速度更快且复杂性更低,而您的计算机通常会更快地使用数组,它发现更容易优化。 你不能假设一个选项比另一个更快,测试它! +1 进行测试。
    • 问题出现在处理链的后续步骤中。如果您想找到一个特定的块并在找到它时忽略其余部分,那么您有理由使用基于生成器的拆分而不是内置解决方案。
    【解决方案4】:

    这是通过re.search() 实现的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('][')
    

    编辑:如果没有给出分隔符,则更正对周围空格的处理。

    【讨论】:

    • 为什么这比re.finditer更好?
    • @ErikKaplun 因为项目的正则表达式逻辑可能比它们的分隔符更复杂。在我的情况下,我想单独处理每一行,所以如果一行不匹配,我可以报告。
    【解决方案5】:

    这是我的实现,它比这里的其他答案更快、更完整。对于不同的情况,它有 4 个独立的子功能。

    我将复制主 str_split 函数的文档字符串:


    str_split(s, *delims, empty=None)
    

    将字符串 s 拆分为其余参数,可能会省略 空部分(empty 关键字参数负责)。 这是一个生成器函数。

    当只提供一个分隔符时,字符串被它简单地分割。 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')
    

    【讨论】:

      【解决方案6】:

      不,但使用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:文档中的示例使用整数列表来说明takeWhile 的使用。使用takeWhile() 将字符串拆分为单词(默认split)会有什么好的predicate
      • string.whitespace中寻找存在。
      • 分隔符可以有多个字符,'abc&lt;def&lt;&gt;ghi&lt;&gt;&lt;&gt;lmn'.split('&lt;&gt;') == ['abc&lt;def', 'ghi', '', 'lmn']
      • @Ignacio:你能在你的答案中添加一个例子吗?
      • 易于编写,但要慢 许多 个数量级。这是一个真正应该在本机代码中实现的操作。
      【解决方案7】:

      我看不出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)
      

      【讨论】:

      • 您将使用的内存减半,因为不必在每个结果部分中存储字符串的第二个副本,加上数组和对象开销(通常比字符串本身更多)。不过,这通常无关紧要(如果您将字符串拆分得如此之大以至于这很重要,那么您可能做错了什么),即使是本机 C 生成器实现也总是比一次执行所有操作要慢得多。
      • @Glenn Maynard - 我刚刚意识到这一点。由于某种原因,我最初的生成器会存储字符串的副本而不是引用。快速检查id() 让我正确。显然,由于字符串是不可变的,因此您无需担心在迭代原始字符串时有人会更改它。
      • 使用生成器的重点不是内存使用,而是如果您想提前退出,您可以省去拆分整个字符串的麻烦? (这不是对您的特定解决方案的评论,我只是对关于内存的讨论感到惊讶)。
      • @Scott:很难想象一个真正成功的案例——其中 1:你想中途停止拆分,2:你不知道要拆分多少字提前,3:你有一个足够大的字符串,它很重要,4:你总是足够早地停下来让它成为对 str.split 的重大胜利。这是一组非常狭窄的条件。
      • 如果您的字符串也是延迟生成的(例如来自网络流量或文件读取),您可以获得更高的收益
      【解决方案8】:

      我写了一个版本的@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 的正则表达式模块说它 does "the right thing" 用于 unicode 空白,但我还没有实际测试过。

      也可作为gist 使用。

      【讨论】:

        【解决方案9】:

        如果您还希望能够读取一个迭代器(以及返回一个),试试这个:

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

        【讨论】:

          【解决方案10】:

          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 是第三方包。

          【讨论】:

          • 请注意,more_itertools.split_at() 在每次调用时仍然使用新分配的列表,因此虽然这确实返回了一个迭代器,但它并没有达到恒定的内存要求。因此,这取决于您希望迭代器开始的原因,这可能有帮助,也可能没有帮助。
          • @jcater 好点。根据其implementation,中间值确实被缓冲为迭代器中的子列表。可以调整源以用迭代器替换列表,附加itertools.chain 并使用列表推导评估结果。根据需要和要求,我可以发布一个示例。
          【解决方案11】:

          我想展示如何使用 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. 我使用 prev & curr 而不是 prev & next 因为在 python 中覆盖 next 是一个非常糟糕的主意
          2. 这个效率很高

          【讨论】:

            【解决方案12】:

            最笨的方法,没有正则表达式/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:]
            

            【讨论】:

              【解决方案13】:

              非常老的问题,但这是我对高效算法的谦虚贡献:

              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
              

              【讨论】:

                【解决方案14】:
                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]
                【解决方案15】:

                这是一个简单的回应

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

                【讨论】:

                  【解决方案16】:
                  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))
                  

                  【讨论】:

                    【解决方案17】:

                    这是一个基于 split 和 maxsplit 的答案。这不使用递归。

                    def gsplit(todo):
                        chunk= 100
                        while todo:
                            splits = todo.split(maxsplit=chunk)
                            if len(splits) == chunk:
                                todo = splits.pop()
                            else:
                                todo=None
                            for item in splits:
                                yield item
                    

                    【讨论】:

                      猜你喜欢
                      • 2015-03-29
                      • 2014-02-02
                      • 2012-07-23
                      • 2010-11-21
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 2016-01-02
                      相关资源
                      最近更新 更多