迭代一个字符串的每一行

144

我有一个多行字符串,定义如下:

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

这个字符串是我用来测试正在编写的解析器的输入。解析器函数接收一个file对象作为输入并迭代它。它也会直接调用next()方法跳过行,所以我确实需要一个迭代器作为输入,而不是可迭代对象。

我需要一个能够像file对象按行迭代文本文件的迭代器,迭代字符串的各个行。当然我也可以像这样做:

lineiterator = iter(foo.splitlines())
有没有更直接的方法?在这种情况下,字符串必须先进行一次分割,然后再由解析器进行处理。虽然在我的测试案例中这并不重要,因为字符串很短,但我只是出于好奇而问。Python有许多有用且高效的内置函数来处理这种情况,但我找不到适合这种需求的东西。

22
你知道可以使用 foo.splitlines() 进行迭代吗? - SilentGhost
“again by the parser” 是什么意思? - danben
7
我觉得重点是不要对字符串进行两次迭代。一旦使用splitlines()方法进行迭代,就不要再迭代这个方法的结果。 - Felix Kling
6
splitlines() 默认不返回迭代器的特定原因是什么?我认为通常趋势是针对可迭代对象进行迭代处理。或者这只适用于像dict.keys()这样的特定函数吗? - Cerno
6个回答

167

以下是三种可能性:

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()函数以确保迭代器被遍历,而不仅仅是构建。
换言之,简单实现方式快得让人难以置信:比我使用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')

至少,这样会少些冗长。不幸的是,需要去除结尾的\n会妨碍使用更清晰和更快速的while循环替换为return iter(stri)(在Python的现代版本中,我相信从2.3或2.4开始就不再需要iter部分,但也是无害的)。也许也值得尝试:

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

或者类似的变体 - 但是我在这里停止,因为这基本上是关于strip最简单和最快的理论练习。


1
谢谢您的出色回答。我想这里的主要教训(因为我是Python新手)是养成使用timeit的习惯。 - Björn Pollex
您能否尝试使用更大的示例进行测量?比如一百万行或类似的数据。 - Thomas Ahle
8
内存消耗怎么样?split()显然将内存作为性能的代价,除了列表结构之外,还要保存所有部分的副本。 - ivan_pozdeev
1
需要注意的是,splitlines() 不仅会在 \n 上进行分割,还会在许多被视为换行符的字符上进行分割:https://docs.python.org/3.5/library/stdtypes.html#str.splitlines - Matt Boehm
7
一开始我非常困惑你的言论,因为你按相反的顺序列出了时间结果,与它们的实施和编号不符。 =P - jamesdlin
显示剩余8条评论

60

“then again by the parser”是什么意思我不确定。在分割完成之后,就没有对字符串进行进一步的遍历,只有对分割后的字符串列表进行遍历。这可能实际上是最快的完成此操作的方法,只要你的字符串大小不是绝对巨大。Python使用不变字符串的事实意味着你必须始终创建一个新字符串,因此无论如何都必须这样做。

如果您的字符串非常大,则劣势在于内存使用:您将同时在内存中拥有原始字符串和分割后的字符串列表,双倍增加了所需的内存。迭代器方法可以节省您的内存,根据需要构建字符串,但它仍然会支付“分割”的惩罚。但是,如果您的字符串非常大,通常要避免甚至未分割的字符串存在于内存中。最好只从文件中读取字符串,这已允许您按行迭代它。

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

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

11
注意:对于Python 3,您需要使用io包,例如使用io.StringIO而不是StringIO.StringIO。请参见https://docs.python.org/3/library/io.html。 - Attila123
1
使用 StringIO 也是一种很好的方法,可以实现高性能的通用换行符处理。 - martineau

8

你可以迭代“一个文件”,它会产生包括结尾换行符的行。如果要将字符串创建为“虚拟文件”,可以使用StringIO

import io  # for Py2.7 that would be import cStringIO as io

for line in io.StringIO(foo):
    print(repr(line))

3

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

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

2
这个问题涉及到一个具体的场景,因此展示一个简单的基准测试会很有帮助,就像最高得分的答案所做的那样。 - Björn Pollex

3
如果我正确地阅读了 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

0

我想你可以自己编写:

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),尽管有几种实现技巧可以在可行的情况下降低它)。 - Alex Martelli
是的 - 我最近刚学习了这个。将字符附加到字符列表中,然后再使用''.join(chars)会更快吗?还是我应该自己尝试一下这个实验?;) - Wayne Werner
请务必进行测量,这是有益的 - 一定要尝试像OP示例中那样的短行和长行!-) - Alex Martelli
对于短字符串(<〜40个字符),+=实际上更快,但会很快达到最坏情况。对于较长的字符串,.join方法实际上看起来像O(N)复杂度。由于我还没有在SO上找到特定的比较,所以我发了一个问题https://dev59.com/3XA75IYBdhLWcg3w4tN7(出乎意料地收到了比我自己更多的答案!) - Wayne Werner

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接