代码之家  ›  专栏  ›  技术社区  ›  Björn Pollex

迭代字符串的行

  •  95
  • Björn Pollex  · 技术社区  · 15 年前

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

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

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

    lineiterator = iter(foo.splitlines())
    

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

    5 回复  |  直到 8 年前
        1
  •  117
  •   Alex Martelli    15 年前

    有三种可能性:

    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() 调用以确保遍历迭代器,而不仅仅是生成迭代器。

    噢,天真的实现速度太快了,甚至都不好笑:比我尝试的速度快6倍 find 调用,这反过来比低级方法快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 很遗憾,S禁止更清晰和更快地更换 while 循环带 return iter(stri) (the iter 这部分内容在现代版本的python中是多余的,我相信从2.3或2.4开始,但它也是无害的)。也许值得一试,还有:

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

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

        2
  •  44
  •   Brian    15 年前

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

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

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

    import StringIO
    s = StringIO.StringIO(myString)
    for line in s:
        do_something_with(line)
    
        3
  •  3
  •   Jacob Oscarson    15 年前

    如果我读 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
    
        4
  •  3
  •   socketpair    8 年前

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

    RRR = re.compile(r'(.*)\n')
    def f4(arg):
        return (i.group(1) for i in RRR.finditer(arg))
    
        5
  •  1
  •   Wayne Werner    15 年前

    我想你可以自己滚:

    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
    

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

    嗯,发电机。

    编辑:

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