Last Updated: 2024-05-13 01:40:47 Monday
-- TOC --
本文总结几个常见的用Python读取文件的技巧。
相关主题:用Python安全读取UTF-8文件
出现死循环的问题代码,大概的逻辑如下,看看你是否能够一眼看出问题所在:
with open(filename,encoding='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接口应该是这样的:
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接口的示例代码:
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接口如果不指定读取长度,一口气读取文件所有内容,直到EOF。与readline一样,read不管是否指定读取长度,如果已经到文件末尾,返回的也是空字符串对象!
一个小技巧,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接口,与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 --