遍历字符串


119

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

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

我们用作我正在编写的解析器的测试输入的字符串。解析器功能接收file-object作为输入并对其进行迭代。它还确实next()直接调用该方法以跳过行,因此我确实需要一个迭代器作为输入,而不是可迭代的。我需要一个迭代器,它可以在字符串的各个行之间进行迭代,就像file-object可以在文本文件的行之间进行迭代一样。我当然可以这样:

lineiterator = iter(foo.splitlines())

是否有更直接的方法?在这种情况下,字符串必须遍历一次才能进行拆分,然后再由解析器再次遍历。在我的测试用例中,这无关紧要,因为那里的字符串很短,我只是出于好奇而问。Python有很多有用且高效的内置程序,但是我找不到适合此需求的东西。


12
您知道可以反复执行foo.splitlines()吗?
SilentGhost

“再次解析器”是什么意思?
danben 2010年

4
@SilentGhost:我认为关键是不要将字符串重复两次。一次迭代splitlines()一次,然后遍历此方法的结果。
Felix Kling 2010年

2
是否有特定的原因为什么splitlines()默认情况下不返回迭代器?我认为趋势是通常针对可迭代对象这样做。还是仅对dict.keys()等特定函数适用?
塞诺

Answers:


144

这是三种可能性:

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(并使用* 100for 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基于方法的方法好-仍然要牢记,因为它可能不大可能出现小的一次性错误(如f3上面所述,任何出现+1和-1的循环都应该自动触发一个个的怀疑-许多循环应该缺少这些调整并且应该进行调整-尽管我相信我的代码也是正确的,因为我能够使用其他函数检查其输出')。

但是基于拆分的方法仍然占主导地位。

顺便说一句:可能更好的样式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不幸的是,需要去除尾随s禁止使用来更清楚,更快速地替换while循环return iter(stri)iter在现代版本的Python中,多余的部分是多余的,我相信从2.3或2.4开始,但它也是无害的)。也许也值得尝试:

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

或其变体-但我在这里停止,因为这几乎是strip基础,最简单和最快的一项理论练习。


而且,(line[:-1] for line in cStringIO.StringIO(foo))速度很快;几乎与幼稚的实现速度一样快,但不完全一样。
Matt Anderson 2010年

感谢您的出色回答。我想这里的主要课程(因为我是python新手)是timeit养成使用习惯。
比约恩·波莱克斯(BjörnPollex)2010年

@Space,是的,timeit很好,任何时候您都在关注性能(请务必谨慎使用,例如,在这种情况下,请参阅我的笔记,其中需要list致电以实际计时所有相关部分!-)。
Alex Martelli 2010年

6
那内存消耗呢?split()显然用内存换取性能,除了保留列表结构之外,还保留所有部分的副本。
ivan_pozdeev 2014年

3
首先,您的发言让我很困惑,因为您以与实现和编号相反的顺序列出了时序结果。= P
jamesdlin

53

我不确定您的意思是“然后再由解析器”。拆分完成后,将不再遍历字符串,而仅遍历拆分字符串列表。只要您的字符串的大小不是绝对很大,这实际上可能是最快的方法。python使用不可变字符串的事实意味着您必须始终创建一个新字符串,因此无论如何都必须这样做。

如果字符串很大,则不利之处在于内存使用情况:您将同时在内存中拥有原始字符串和拆分字符串列表,从而使所需的内存增加了一倍。迭代器方法可以节省您的开销,可以根据需要构建字符串,尽管它仍然要付出“分割”的代价。但是,如果您的字符串太大,则通常甚至要避免将未拆分的字符串存储在内存中。最好只从文件中读取字符串,该文件已经允许您以行形式遍历该字符串。

但是,如果您确实已经在内存中存储了一个巨大的字符串,则一种方法是使用StringIO,它为字符串提供了一个类似于文件的接口,包括允许逐行迭代(内部使用.find查找下一个换行符)。您将得到:

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

5
注意:对于python 3,您必须为此使用io软件包,例如,使用io.StringIO而不是StringIO.StringIO。参见docs.python.org/3/library/io.html
Attila123 '18

使用StringIO也是获得高性能通用换行符处理的好方法。
martineau,

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

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

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

2
这个问题是关于一个特定的场景的,因此有助于显示一个简单的基准,就像得分最高的答案一样。
比约恩博动

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

我不确定此实现的效率如何,但这只会在您的字符串上迭代一次。

嗯,发电机。

编辑:

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


对于较长的行,效率很低(该+=部件的O(N squared)性能最差,不过有几种实现方法会在可行的情况下降低该性能)。
Alex Martelli 2010年

是的-我最近才刚刚了解这一点。追加到chars列表,然后对它们进行''.join(chars)”会更快吗?还是我应该自己做一个实验?;)
Wayne Werner 2010年

请认真衡量一下,这很有启发性-一定要尝试同时尝试OP中的短线和长线!-)
Alex Martelli 2010年

对于短字符串(<〜40个字符),+ =实际上更快,但最坏的情况很快。对于更长的字符串,该.join方法实际上看起来像O(N)复杂度。由于我仍无法找到在SO上进行的特定比较,因此我提出了一个问题stackoverflow.com/questions/3055477/…(出人意料的是,这不仅仅是我自己的回答!)
Wayne Werner 2010年

0

您可以遍历“文件”,该文件将产生包括尾随换行符在内的行。要使用字符串制作“虚拟文件”,可以使用StringIO

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

for line in io.StringIO(foo):
    print(repr(line))
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.