我有一个像这样定义的多行字符串:
foo = """
this is
a multi-line string.
"""
这个字符串我们用作我正在编写的解析器的测试输入。解析器函数接收一个file
- 对象作为输入并迭代它。它也直接调用next()
方法来跳过行,所以我真的需要一个迭代器作为输入,而不是迭代。
我需要一个迭代器,迭代遍历该字符串的各个行,就像file
- 对象将遍历文本文件的行。我当然可以这样做:
lineiterator = iter(foo.splitlines())
有更直接的方法吗?在这种情况下,字符串必须遍历一次以进行拆分,然后再由解析器遍历。在我的测试用例中没关系,因为那里的字符串很短,我只是出于好奇而问。 Python为这些东西提供了许多有用且高效的内置插件,但我找不到任何适合这种需求的东西。
答案 0 :(得分:120)
以下是三种可能性:
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')
至少,它有点不那么冗长。不幸的是,删除尾随\n
的需要禁止用while
更清楚和更快地替换return iter(stri)
循环(iter
部分在现代版本的Python中是多余的,我相信自2.3或2.4,但它也是无害的)。也许值得一试:
return itertools.imap(lambda s: s.strip('\n'), stri)
或其变体 - 但我停在这里,因为它几乎是strip
基于最简单,最快速的理论练习。
答案 1 :(得分:47)
我不确定你的意思是“然后再由解析器”。分割完成后,字符串没有进一步遍历,只有遍历分割字符串的列表。这可能实际上是实现这一目标的最快方法,只要字符串的大小不是很大。 python使用不可变字符串的事实意味着你必须总是创建一个新字符串,所以这必须在某个时候完成。
如果您的字符串非常大,则缺点在于内存使用:您将同时拥有原始字符串和内存中的拆分字符串列表,从而使所需的内存增加一倍。迭代器方法可以为您节省这一点,根据需要构建一个字符串,尽管它仍然会支付“分裂”惩罚。但是,如果您的字符串很大,您通常希望避免连接 unsplit 字符串在内存中。最好只读取文件中的字符串,它已经允许您以行的形式迭代它。
但是如果你的内存中确实有一个巨大的字符串,一种方法是使用StringIO,它为字符串提供类似文件的接口,包括允许逐行迭代(内部使用.find查找下一个换行符) 。然后你得到:
import StringIO
s = StringIO.StringIO(myString)
for line in s:
do_something_with(line)
答案 2 :(得分: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
答案 3 :(得分:3)
基于正则表达式的搜索有时比生成器方法更快:
RRR = re.compile(r'(.*)\n')
def f4(arg):
return (i.group(1) for i in RRR.finditer(arg))
答案 4 :(得分: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
我不确定这个实现的效率如何,但这只会迭代你的字符串一次。
嗯,发电机。编辑:
当然,你也想要添加你想要的任何类型的解析动作,但这很简单。
答案 5 :(得分:0)
您可以遍历“文件”,该文件将产生包括尾随换行符在内的行。要使用字符串制作“虚拟文件”,可以使用StringIO
:
import io # for Py2.7 that would be import cStringIO as io
for line in io.StringIO(foo):
print(repr(line))