关于python:迭代字符串的行

Iterate over the lines of a string

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

1
2
3
4
foo ="""
this is
a multi-line string.
"""

这个字符串用作我正在编写的解析器的测试输入。parser函数接收一个file对象作为输入,并对其进行迭代。它也直接调用next()方法来跳过行,所以我确实需要一个迭代器作为输入,而不是一个iterable。我需要一个迭代器,它像file对象一样,在字符串的各个行上迭代器会覆盖文本文件的行。我当然可以这样做:

1
lineiterator = iter(foo.splitlines())

有更直接的方法吗?在这个场景中,字符串必须遍历一次以进行拆分,然后由解析器再次遍历。在我的测试用例中,这并不重要,因为字符串在那里非常短,我只是出于好奇而问。python有这么多有用和高效的内置组件,但是我找不到适合这种需求的东西。


有三种可能性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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 == '
'
else ''
        if char == '
'
:
            yield retval
            retval = ''
    if retval:
        yield retval

def f3(foo=foo):
    prevnl = -1
    while True:
      nextnl = foo.find('
'
, 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以获得更精确测量的大量字符串):

1
2
3
4
5
6
$ 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的建议,稍作修改,以获得与其他建议相同的结果(保留行尾空格),即:

1
2
3
4
5
6
7
8
9
10
11
from cStringIO import StringIO

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

测量给出:

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

虽然不如基于.find的方法好,但值得记住的是,它可能不太容易被一个错误所破坏(任何出现+1和-1的循环,如我上面的f3的循环,都应该由一个怀疑自动触发,并且许多没有这种调整的循环也应该有这种调整——这是因为虽然我相信我的代码也是正确的,因为我可以用其他函数检查它的输出)。

但是基于分裂的方法仍然是规则。

旁白:对于f4来说,可能更好的样式是:

1
2
3
4
5
6
7
8
9
from cStringIO import StringIO

def f4(foo=foo):
    stri = StringIO(foo)
    while True:
        nl = stri.readline()
        if nl == '': break
        yield nl.strip('
'
)

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

1
2
    return itertools.imap(lambda s: s.strip('
'
), stri)

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


我不知道你所说的"然后又是解析器"是什么意思。拆分完成后,不再遍历字符串,只遍历拆分字符串列表。这可能是实现这一点的最快方法,只要字符串的大小不是绝对巨大的。事实上,python使用不可变的字符串意味着您必须始终创建一个新的字符串,所以无论如何,这必须在某个时刻完成。

如果字符串非常大,缺点是内存使用:您将在内存中同时拥有原始字符串和拆分字符串列表,从而使所需内存翻倍。迭代器方法可以节省您的开销,根据需要构建一个字符串,尽管它仍然要支付"拆分"的代价。但是,如果字符串太大,则通常希望避免内存中甚至存在未拆分的字符串。最好只是从一个文件中读取字符串,它已经允许您以行的形式进行迭代。

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

1
2
3
4
import StringIO
s = StringIO.StringIO(myString)
for line in s:
    do_something_with(line)


基于regex的搜索有时比生成器方法更快:

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


如果我正确地阅读了Modules/cStringIO.c,这应该是相当有效的(尽管有些冗长):

1
2
3
4
5
6
7
8
9
10
from cStringIO import StringIO

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


我想你可以自己滚:

1
2
3
4
5
6
7
8
9
10
11
def parse(string):
    retval = ''
    for char in string:
        retval += char if not char == '
'
else ''
        if char == '
'
:
            yield retval
            retval = ''
    if retval:
        yield retval

我不确定这个实现有多有效,但它只会在您的字符串上迭代一次。

嗯,发电机。

编辑:

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