遍历字符串的每一行

144 投票
6 回答
187601 浏览
提问于 2025-04-16 00:03

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

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

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

我需要一个迭代器,能够像file对象处理文本文件的行那样,逐行遍历这个字符串。当然,我可以这样做:

lineiterator = iter(foo.splitlines())

有没有更直接的方法来实现这个呢?在这种情况下,字符串需要被遍历一次来进行分割,然后解析器又要遍历一次。对于我的测试案例来说,这并不重要,因为字符串非常短,我只是出于好奇在问。Python有很多有用且高效的内置功能来处理这些事情,但我找不到适合这个需求的。

6 个回答

9

你可以遍历“一个文件”,这样会逐行读取内容,包括每行末尾的换行符。想要把一个字符串当作“虚拟文件”来使用,可以用 StringIO

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

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

我不太明白你说的“然后再由解析器处理”是什么意思。在字符串被分割后,就没有再对这个字符串进行遍历了,只有对分割后的字符串列表进行遍历。只要你的字符串不是特别大,这种方法可能是最快的。因为Python使用的是不可变字符串,这意味着你必须始终创建一个新的字符串,所以这在某个时候是必须的。

如果你的字符串非常大,缺点就是内存使用:你会同时在内存中保留原始字符串和分割后的字符串列表,这样会使所需的内存翻倍。使用迭代器的方法可以节省这部分内存,按需构建字符串,尽管仍然需要付出“分割”的代价。不过,如果你的字符串真的那么大,通常你会想避免在内存中保留未分割的字符串。更好的方法是直接从文件中读取字符串,这样你可以逐行遍历。

但是如果你已经在内存中有一个巨大的字符串,一种方法是使用StringIO,它为字符串提供了类似文件的接口,包括允许按行遍历(内部使用.find来找到下一个换行符)。这样你就可以得到:

import StringIO
s = StringIO.StringIO(myString)
for line in s:
    do_something_with(line)
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 (并且对 foo 乘以 * 100,这样可以得到更大的字符串,便于更精确的测量):

$ 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 不幸地使得无法用 return iter(stri) 更清晰和更快地替代 while 循环(我认为 iter 在现代 Python 版本中是多余的,自 2.3 或 2.4 以来就是这样,但它也无伤大雅)。也许值得尝试一下:

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

或者类似的变体——但我在这里就停下来了,因为这基本上是一个理论上的练习,关于基于 strip 的,最简单和最快的方法。

撰写回答