Python文件读取技巧

Last Updated: 2023-05-18 09:32:16 Thursday

-- TOC --

本文从一个调用文件对象的readline函数接口导致死循环的故障开始,总结Python文件读取技巧。

相关主题:用Python安全读取UTF-8文件

readline导致死循环

出现死循环的问题代码,大概的逻辑如下,看看你是否能够一眼看出问题所在:

with open(filenameencoding='utf-8') as f:
    while True:
        line = f.readline().strip()
        if line is None:
            break
        ...  # do something with line here

再一次测试中,发现这段代码陷入了死循环!

以上代码没有考虑filename可能不是utf-8编码的情况,具体可参考:Python安全读取utf-8文件

问题原因

陷入死循环,一定是没有 break,而没有break,一定是 line is None 这个条件没有满足。难道文件读取结束时,不是返回None吗?

是的,文件读取结束,Python返回空串。我们来做个测试:

假设有一个文本文件,内容如下:

$ cat test.txt
1111111

3333333

5555555

共6行,中间有3行空白。然后,我们用该文件对象的readline函数去读取这个文件:

>>> f = open('test.txt')
>>> print(f.readline(), end='')
1111111
>>> print(f.readline(), end='')  # only \n

>>> print(f.readline(), end='')
3333333
>>> print(f.readline(), end='')  # only \n

>>> print(f.readline(), end='')
5555555
>>> print(f.readline(), end='')  # only \n

>>> print(f.readline(), end='')  # empty string object
>>> print(f.readline(), end='')  # empty string object
>>>

看到问题的原因了吗?当文件已经读取完毕后,再使用readline,返回的内容不是None,而是一个空的string对象''。这就是上面那段代码出现死循环的原因。

另一个值得关注的细节,中间的空白行,用readline读取,得到的是\n

如何解决

Python读取stream有三个主要的接口,read,readline和readlines。解决方法很多,关键是要根据你的业务场景来分析哪一种最合适。

readline接口

正确地使用readline接口应该是这样的:

with open(filename,encoding='utf-8') as f:
    while line:=f.readline():
        line = line.strip()
        ... # do something here

readline遇到中间的空白行,会得到\n,不会是'',因此上面的代码是安全的,只有读到最后,while判断''为False,才退出循环。strip必须要新起一行调用。

readline接口如果遇到line超长的情况,比如一个size很大的文件,从头到尾没有换行,会出现内存占用过大的问题。因此,给readline传入一个参数,来限制最大读取长度,是更安全的做法!

readlines接口

下面是readlines接口的示例代码:

with open(filename) as f:
    lines = f.readlines()
    for line in lines:
        line = line.strip()
        ... # do something with line here

使用 readlines 函数,一口气将所有line读出到内存,然后在for循环中,一行行地处理。这种机制决定了,使用 readlines 一定要注意业务场景,对于一般的文件OK,但是对于那种可能出现size超大的文件的场景,readlines函数接口又会引入新的问题,内存占用过多。readlines有一个hint参数,可以控制最大读取的行数。

read接口

read接口如果不指定读取长度,一口气读取文件所有内容,直到EOF。与readline一样,read不管是否指定读取长度,如果已经到文件末尾,返回的也是空字符串对象

用enumerate读取文件

一个小技巧,TextIOWrapper对象也是可迭代的,每次迭代返回一行。在使用enumerate遍历的时候,可以同时获得行号与行内容

$ echo -e '123\nabc\n789\njkl' > test.txt
$ cat test.txt
123
abc
789
jkl
$ python -q
>>> with open('test.txt',encoding='utf-8') as f:
...   for line_no,line in enumerate(f,1):
...     print(line_no, line)
...
1 123

2 abc

3 789

4 jkl

>>>

enumerate接口的第2个参数,starter,表示从哪个数字开始编号。

分块读取超大文件

下面代码的stopper,在rb模式下,是b''

from functools import partial


def read_blocks(pathname, mode='r', block_size=1024**2):
    """Yield each block_size bytes at most while reading pathname in mode."""
    if mode not in ('r', 'rb'):
        raise ValueError('mode parameter error')
    stopper = '' if mode=='r' else b''
    with open(pathname, mode) as f:
        for chunk in iter(partial(f.read,block_size), stopper):
            yield chunk


readb_blocks = partial(read_blocks, mode='rb')

C语言标准库中的fgets接口

C语言标准库中的fgets接口,与Python的readline接口很类似,或者这句话应该反过来说,Python的很多接口,都是在尽量与C标准库中的接口保持良好的对应关系。

readline在遇到文件末尾时返回'',C的fgets接口在遇到文件末尾时返回NULL。对于中间的空白行,读取到\n都是一样的。Python中没有指针的概念,因此返回'',而C语言可以返回NULL,表示end of file或error occurs。

关于C语言标准库的fgets接口,给出一段参考代码:

#include <stdio.h>


int main(void) {
    FILE *fin = fopen("aa.txt", "r");

    char line[80] = {0};
    while (fgets(line, 80, fin) != NULL)
        printf("%s", line);
    return 0;
}

代码中80的含义,表示fgets最多读取79个字符到line所指向的空间,它会确保这个空间最后的符号时\0。上面这段代码的作用,就是将文件aa.txt的内容读出并显示在stdout上。

本文链接:https://cs.pynote.net/sf/python/202110045/

-- EOF --

-- MORE --