【问题标题】:Alternatives to `tell()` while iterating over lines of a file in Python3?在 Python3 中迭代文件行时替代“tell()”?
【发布时间】:2018-03-06 10:16:06
【问题描述】:

在 Python3 中迭代文件时如何找出文件光标的位置?

在 Python 2.7 中这很简单,使用 tell()。在 Python3 中,同样的调用会引发 OSError:

Traceback (most recent call last):
  File "foo.py", line 113, in check_file
    pos = infile.tell()
OSError: telling position disabled by next() call

我的用例是制作一个用于读取大型 CSV 文件的进度条。计算总行数太昂贵并且需要额外的通过。一个近似值很有用,我不关心缓冲区或其他噪声源,我想知道它需要 10 秒还是 10 分钟。

重现问题的简单代码。它在 Python 2.7 上按预期工作,但在 Python 3 上抛出:

file_size = os.stat(path).st_size
with open(path, "r") as infile:
    reader = csv.reader(infile)
    for row in reader:
        pos = infile.tell()  # OSError: telling position disabled by next() call
        print("At byte {} of {}".format(pos, file_size))

这个答案https://stackoverflow.com/a/29641787/321772 表明问题在于next() 方法在迭代期间禁用了tell()。替代方法是手动逐行读取,但该代码位于 CSV 模块内,因此我无法理解。我也无法理解 Python 3 通过禁用 tell() 会获得什么。

那么在 Python 3 中迭代文件行时找出字节偏移量的首选方法是什么?

【问题讨论】:

  • 您可以使用enumerate 并返回行号。这样你就可以给用户一些有用的东西,而不必遍历文件两次
  • @MaartenFabré 当然,打印行号很有用,如果只是为了显示脚本没有卡住,而且如果你不知道长度(即从标准输入)。但是打印“完成 55%,还剩 2 分钟”比“读取 10,543,000 行”要好得多。

标签: python python-3.x csv


【解决方案1】:

如果您对不使用 csv 模块感到满意。您可以执行以下操作:

import os, csv

file_size = os.path.getsize('SampleCSV.csv')
pos = 0

with open('SampleCSV.csv', "r") as infile:
    for line in infile:
        pos += len(line) + 1    # 1 for newline character
        row = line.rstrip().split(',')
        print("At byte {} of {}".format(pos, file_size))

但这在字段本身包含 \" 的情况下可能不起作用。

例如:1,"Hey, you..",22:04 虽然这些也可以使用正则表达式来处理。

【讨论】:

    【解决方案2】:

    csv 模块只期望reader 调用的第一个参数是一个迭代器,它在每个next 调用上返回一行。因此,您可以只使用迭代器包装器而不是计算字符。如果您希望计数准确,则必须以二进制模式打开文件。但事实上,这很好,因为您不会进行 csv 模块所期望的行尾转换。

    所以一个可能的包装是:

    class SizedReader:
        def __init__(self, fd, encoding='utf-8'):
            self.fd = fd
            self.size = 0
            self.encoding = encoding   # specify encoding in constructor, with utf8 as default
        def __next__(self):
            line = next(self.fd)
            self.size += len(line)
            return line.decode(self.encoding)   # returns a decoded line (a true Python 3 string)
        def __iter__(self):
            return self
    

    你的代码会变成:

    file_size = os.stat(path).st_size
    with open(path, "rb") as infile:
        szrdr = SizedReader(infile)
        reader = csv.reader(szrdr)
        for row in reader:
            pos = szrdr.size  # gives position at end of current line
            print("At byte {} of {}".format(pos, file_size))
    

    这里的好消息是您保留了 csv 模块的所有功能,包括引用字段中的换行符...

    【讨论】:

    • 这行得通。尽管您不必担心编码;只需取给你的东西,找到它的长度,然后返回它。这样你就不会改变解码行为。另请注意,您需要def next(self): return self.__next__(),因此相同的代码适用于 Python 2 和 3。
    • @Adam:这个问题是专门关于 Python 3 的。如果你不解码以二进制模式读取的内容,你将得到字节而不是字符串。 csv 模块在 Python2 和 Python3 中的行为完全不同,这就是我没有尝试提供兼容代码的原因。这确实是可能的,但会更复杂。
    • 是的,但问题没有以二进制模式打开文件。
    • @Adam:... 我的回答解释了为什么文件应该以二进制模式打开。如果您不这样做并且文件不是纯 ASCII 格式,则大小将不准确。
    • 嗯,这很好 - 但与使用 tell() 相比,它似乎会减慢读取文件的速度。
    【解决方案3】:

    由于你的csv文件太大,根据你提到的the page还有另一种解决方案:

    使用offset += len(line) 代替file.tell()。例如,

    offset = 0
    with open(path, mode) as file:
        for line in file:
            offset += len(line)
    

    【讨论】:

    • 该问题提出了这种替代方案,并解释了为什么它不适用于 CSV 模块。接受的答案是使其与 CSV 一起使用的一种方法。
    猜你喜欢
    • 1970-01-01
    • 2021-06-23
    • 1970-01-01
    • 2011-02-19
    • 2019-10-07
    • 1970-01-01
    • 2016-11-21
    • 2018-07-27
    • 2019-02-23
    相关资源
    最近更新 更多