遍历字符串的每一行
我有一个多行字符串,定义如下:
foo = """
this is
a multi-line string.
"""
这个字符串是我正在编写的解析器的测试输入。解析器函数接收一个file
对象作为输入,并对其进行逐行处理。它还会直接调用next()
方法来跳过某些行,所以我实际上需要一个迭代器作为输入,而不是一个可迭代对象。
我需要一个迭代器,能够像file
对象处理文本文件的行那样,逐行遍历这个字符串。当然,我可以这样做:
lineiterator = iter(foo.splitlines())
有没有更直接的方法来实现这个呢?在这种情况下,字符串需要被遍历一次来进行分割,然后解析器又要遍历一次。对于我的测试案例来说,这并不重要,因为字符串非常短,我只是出于好奇在问。Python有很多有用且高效的内置功能来处理这些事情,但我找不到适合这个需求的。
6 个回答
你可以遍历“一个文件”,这样会逐行读取内容,包括每行末尾的换行符。想要把一个字符串当作“虚拟文件”来使用,可以用 StringIO
。
import io # for Py2.7 that would be import cStringIO as io
for line in io.StringIO(foo):
print(repr(line))
我不太明白你说的“然后再由解析器处理”是什么意思。在字符串被分割后,就没有再对这个字符串进行遍历了,只有对分割后的字符串列表进行遍历。只要你的字符串不是特别大,这种方法可能是最快的。因为Python使用的是不可变字符串,这意味着你必须始终创建一个新的字符串,所以这在某个时候是必须的。
如果你的字符串非常大,缺点就是内存使用:你会同时在内存中保留原始字符串和分割后的字符串列表,这样会使所需的内存翻倍。使用迭代器的方法可以节省这部分内存,按需构建字符串,尽管仍然需要付出“分割”的代价。不过,如果你的字符串真的那么大,通常你会想避免在内存中保留未分割的字符串。更好的方法是直接从文件中读取字符串,这样你可以逐行遍历。
但是如果你已经在内存中有一个巨大的字符串,一种方法是使用StringIO,它为字符串提供了类似文件的接口,包括允许按行遍历(内部使用.find来找到下一个换行符)。这样你就可以得到:
import StringIO
s = StringIO.StringIO(myString)
for line in s:
do_something_with(line)
这里有三种可能性:
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
的,最简单和最快的方法。