【问题标题】:Iterate over the lines of a string遍历字符串的行
【发布时间】:2011-03-04 12:48:08
【问题描述】:

我有一个这样定义的多行字符串:

foo = """
this is 
a multi-line string.
"""

我们将这个字符串用作我正在编写的解析器的测试输入。解析器函数接收file-object 作为输入并对其进行迭代。它还直接调用next() 方法来跳过行,所以我真的需要一个迭代器作为输入,而不是一个可迭代的。 我需要一个迭代器来遍历该字符串的各个行,就像 file-object 将遍历文本文件的行一样。我当然可以这样做:

lineiterator = iter(foo.splitlines())

有没有更直接的方法?在这种情况下,字符串必须遍历一次以进行拆分,然后再由解析器遍历。在我的测试用例中没关系,因为那里的字符串很短,我只是出于好奇而问。 Python 为这些东西提供了很多有用且高效的内置函数,但我找不到适合这种需要的东西。

【问题讨论】:

  • 你知道你可以迭代foo.splitlines() 对吗?
  • “再次被解析器”是什么意思?
  • @SilentGhost:我认为关键是不要重复字符串两次。一次通过splitlines() 迭代,第二次通过迭代此方法的结果。
  • 默认情况下 splitlines() 不返回迭代器是否有特殊原因?我认为趋势通常是对可迭代对象这样做。还是这仅适用于 dict.keys() 等特定函数?

标签: python string iterator


【解决方案1】:

我想你可以自己动手:

def parse(string):
    retval = ''
    for char in string:
        retval += char if not char == '\n' else ''
        if char == '\n':
            yield retval
            retval = ''
    if retval:
        yield retval

我不确定这个实现的效率如何,但它只会遍历你的字符串一次。

嗯,发电机。

编辑:

当然,您还需要添加您想要执行的任何类型的解析操作,但这很简单。

【讨论】:

  • 长线效率相当低(+= 部分的性能最差 O(N squared),尽管一些实现技巧会在可行的情况下尝试降低该性能)。
  • 是的 - 我最近才刚刚了解这一点。追加到字符列表然后 ''.join(chars) 会更快吗?或者那是我应该自己进行的实验? ;)
  • 请自己衡量一下,这很有指导意义——一定要尝试像 OP 示例中那样的短线和长线!-)
  • 对于短字符串(.join 方法实际上看起来像 O(N) 复杂度。由于我还没有找到关于 SO 的特定比较,所以我提出了一个问题 stackoverflow.com/questions/3055477/…(令人惊讶的是,得到的答案比我自己的还多!)
【解决方案2】:

如果我正确阅读了Modules/cStringIO.c,这应该是非常有效的(虽然有些冗长):

from cStringIO import StringIO

def iterbuf(buf):
    stri = StringIO(buf)
    while True:
        nl = stri.readline()
        if nl != '':
            yield nl.strip()
        else:
            raise StopIteration

【讨论】:

    【解决方案3】:

    这里有三种可能性:

    foo = """
    this is 
    a multi-line string.
    """
    
    def f1(foo=foo): return iter(foo.splitlines())
    
    def f2(foo=foo):
        retval = ''
        for char in foo:
            retval += char if not char == '\n' else ''
            if char == '\n':
                yield retval
                retval = ''
        if retval:
            yield retval
    
    def f3(foo=foo):
        prevnl = -1
        while True:
          nextnl = foo.find('\n', prevnl + 1)
          if nextnl < 0: break
          yield foo[prevnl + 1:nextnl]
          prevnl = nextnl
    
    if __name__ == '__main__':
      for f in f1, f2, f3:
        print list(f())
    

    将其作为主脚本运行可确认这三个功能是等效的。使用timeit(以及* 100 用于foo 以获得大量字符串以进行更精确的测量):

    $ python -mtimeit -s'import asp' 'list(asp.f3())'
    1000 loops, best of 3: 370 usec per loop
    $ python -mtimeit -s'import asp' 'list(asp.f2())'
    1000 loops, best of 3: 1.36 msec per loop
    $ python -mtimeit -s'import asp' 'list(asp.f1())'
    10000 loops, best of 3: 61.5 usec per loop
    

    请注意,我们需要调用 list() 来确保遍历迭代器,而不仅仅是构建迭代器。

    IOW,天真的实现要快得多,甚至都不好笑:比我尝试使用 find 调用的速度快 6 倍,而后者又比较低级别的方法快 4 倍。

    要记住的教训:测量总是一件好事(但必须准确);像splitlines 这样的字符串方法以非常快的方式实现;通过在非常低的级别进行编程(尤其是通过 += 的非常小的片段的循环)将字符串放在一起可能会非常慢。

    编辑:添加了@Jacob 的提案,稍作修改以提供与其他提案相同的结果(保留一行尾随空格),即:

    from cStringIO import StringIO
    
    def f4(foo=foo):
        stri = StringIO(foo)
        while True:
            nl = stri.readline()
            if nl != '':
                yield nl.strip('\n')
            else:
                raise StopIteration
    

    测量给出:

    $ python -mtimeit -s'import asp' 'list(asp.f4())'
    1000 loops, best of 3: 406 usec per loop
    

    不如基于.find 的方法好——但值得牢记,因为它可能不太容易出现小错误(任何出现+1和-1的循环,就像我上面的f3 一样,应该会自动触发一对一的怀疑——许多缺乏这种调整并且应该有调整的循环也应该如此——尽管我相信我的代码也是正确的,因为我能够检查它的输出其他功能)。

    但基于拆分的方法仍然适用。

    顺便说一句:f4 可能更好的风格是:

    from cStringIO import StringIO
    
    def f4(foo=foo):
        stri = StringIO(foo)
        while True:
            nl = stri.readline()
            if nl == '': break
            yield nl.strip('\n')
    

    至少,它不那么冗长了。不幸的是,需要去除尾随 \ns 禁止更清晰、更快速地用 while 替换 while 循环(iter 部分在现代版本的 Python 中是多余的,我相信从 2.3 或 2.4 开始,但它是也无害)。也许也值得一试:

        return itertools.imap(lambda s: s.strip('\n'), stri)
    

    或其变体——但我在这里停下来,因为它几乎是一个基于strip、最简单、最快的理论练习。

    【讨论】:

    • 另外,(line[:-1] for line in cStringIO.StringIO(foo)) 非常快;几乎和天真的实现一样快,但还不够。
    • 感谢您的出色回答。我想这里的主要教训(因为我是 python 新手)是养成使用timeit 的习惯。
    • @Space,是的,timeit 很好,任何时候你关心性能(一定要小心使用它,例如在这种情况下,请参阅我关于需要list 调用来实际计时的注释相关部分!-)。
    • 内存消耗呢? split() 显然以内存换取性能,除了列表结构之外,还保存所有部分的副本。
    • 一开始我真的被你的评论弄糊涂了,因为你列出的时序结果与它们的实现和编号顺序相反。 =P
    【解决方案4】:

    我不确定您所说的“然后再由解析器”是什么意思。拆分完成后,不再遍历 string,只遍历拆分字符串的 list。这实际上可能是完成此任务的最快方法,只要您的字符串的大小不是绝对巨大。 python 使用不可变字符串这一事实意味着您必须始终创建一个新字符串,因此无论如何都必须在某个时候完成。

    如果您的字符串非常大,则缺点是内存使用量:您将同时在内存中拥有原始字符串和拆分字符串列表,从而使所需内存增加一倍。迭代器方法可以为您节省这一点,根据需要构建一个字符串,尽管它仍然会付出“拆分”的代价。但是,如果您的字符串那么大,您通常希望避免 unsplit 字符串在内存中。最好只从文件中读取字符串,这已经允许您将其作为行进行迭代。

    但是,如果您在内存中已经有一个巨大的字符串,一种方法是使用 StringIO,它为字符串提供了一个类似文件的接口,包括允许逐行迭代(内部使用 .find 查找下一个换行符) .然后你得到:

    import StringIO
    s = StringIO.StringIO(myString)
    for line in s:
        do_something_with(line)
    

    【讨论】:

    • 注意:对于 python 3,您必须为此使用 io 包,例如使用io.StringIO 而不是StringIO.StringIO。见docs.python.org/3/library/io.html
    • 使用StringIO 也是获得高性能通用换行符处理的好方法。
    【解决方案5】:

    基于正则表达式的搜索有时比生成器方法更快:

    RRR = re.compile(r'(.*)\n')
    def f4(arg):
        return (i.group(1) for i in RRR.finditer(arg))
    

    【讨论】:

    • 这个问题是关于一个特定场景的,所以展示一个简单的基准会很有帮助,就像得分最高的答案所做的那样。
    【解决方案6】:

    您可以遍历“文件”,它会产生行,包括尾随的换行符。要从字符串中创建“虚拟文件”,您可以使用StringIO

    import io  # for Py2.7 that would be import cStringIO as io
    
    for line in io.StringIO(foo):
        print(repr(line))
    

    【讨论】:

      猜你喜欢
      • 2011-05-13
      • 2013-01-05
      • 2012-09-12
      • 2018-05-15
      • 2014-12-30
      相关资源
      最近更新 更多