Python3中在迭代文件行时替代 `tell()` 的方法有哪些?

5

当使用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://dev59.com/IF0b5IYBdhLWcg3wGN0s#29641787表明问题在于next()方法在迭代期间禁用了tell()。替代方法是手动逐行读取,但该代码位于CSV模块中,因此我无法使用它。我也不明白Python 3禁用tell()的好处在哪里。

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


1
您可以使用 enumerate 并返回行号。这样,您可以向用户提供有用的信息,而无需对文件进行两次遍历。 - Maarten Fabré
当然打印行号是有用的,即使只是为了显示脚本没有卡住,而且如果你不知道长度(即从stdin读取),那么这是你能做的全部。但是打印“55%完成,剩余2分钟”比“读取10,543,000行”要好得多得多。 - Adam
3个回答

6

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
@Adam:这个问题特别涉及到Python 3。如果你不在二进制模式下解码读取的内容,你会得到字节而不是字符串。csv模块在Python2和Python3中的行为非常不同,这就是我没有尝试提供兼容代码的原因。虽然这是可能的,但会更加复杂。 - Serge Ballesta
没错,但这个问题没有以二进制模式打开文件。 - Adam
@Adam:我的回答解释了为什么应该以二进制模式打开文件。如果你不这样做,而且文件不是纯ASCII格式,那么文件大小将不准确。 - Serge Ballesta
好的,但是与使用tell()相比,它似乎会大大减慢读取文件的速度。 - xorsyst

0

如果您不需要特定的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,“嘿,你..”,22:04 虽然这些也可以使用正则表达式来处理。


0

由于您的csv文件太大,根据您提到的页面,还有另一种解决方案:

使用offset += len(line)而不是file.tell()。例如:

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

问题提出了这个替代方案,并解释了为什么它不能与CSV模块一起使用。被接受的答案是使其能够与CSV一起使用的方法。 - Adam

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接