
小郑那天写了一个日志分析工具,要统计应用日志里每种错误出现的次数。日志文件不大,几百兆,他想着一次性读进来处理最方便。代码大概长这样:
with open("app.log", "r", encoding="utf-8") as f:
content = f.read()
for line in content:
if "ERROR" in line:
count += 1跑完他傻眼了:统计结果大得离谱,几乎每一行都被算成了错误。他打印了一下line,发现每次循环拿到的都是一个字符,不是一行。
“我明明读的是日志,怎么变成逐个字符遍历了?”
他改成readlines():
with open("app.log", "r", encoding="utf-8") as f:
lines = f.readlines()
for line in lines:
if "ERROR" in line:
count += 1这次对了。但他又写了一个场景:需要把整个日志当做一个字符串,用正则做跨行匹配。他用了readlines()拿到列表,然后"\n".join(lines)拼回去。同事路过看了一眼,说:“你直接用read()不就行了,干嘛绕这一圈?”
小郑愣了一下:read()读出来是字符串,readlines()读出来是列表,那到底什么时候用哪个?两者看起来都是“把文件读进来”,区别真的有那么大吗?
区别不但大,而且用错了不是报错,是静默地做出错误的结果。就像他第一次那样,程序不崩,只是统计数字全错。
准备一个简单的文本文件demo.txt:
第一行
第二行
第三行用两种方式读:
with open("demo.txt", "r", encoding="utf-8") as f:
content = f.read()
print(type(content)) # <class 'str'>
print(repr(content)) # '第一行\n第二行\n第三行\n'
print(len(content)) # 12,字符数
with open("demo.txt", "r", encoding="utf-8") as f:
lines = f.readlines()
print(type(lines)) # <class 'list'>
print(lines) # ['第一行\n', '第二行\n', '第三行\n']
print(len(lines)) # 3,行数区别非常清楚:
read()把整个文件读成一个字符串,换行符\n保留在字符串里。你拿到的是连续的一大段文本。
readlines()把整个文件读成一个列表,列表的每个元素是一行,行尾的换行符\n也保留着。你拿到的是按行切好的若干段文本。
用一个生活场景来理解。你有一本记事本,上面记着三行字。
read()的做法是:把整本记事本的内容复印到一张长纸上,三行字连在一起,中间用换行隔开。你拿到的是“一整张纸”。
readlines()的做法是:把记事本拆成一页一页,每页一行,装进一个文件夹。你拿到的是“一叠纸”。
区别一目了然:一个是一整块文本,一个是按行切好的列表。
read()的签名是file.read(size=-1)。不传参数时,读取从当前位置到文件末尾的所有内容,返回一个字符串。
with open("demo.txt", "r", encoding="utf-8") as f:
content = f.read()
print(content)
# 第一行
# 第二行
# 第三行read()适合“需要把整个文件当做一个整体来处理”的场景。比如:
read()也可以传一个size参数,读取指定数量的字符:
with open("demo.txt", "r", encoding="utf-8") as f:
first_5 = f.read(5)
print(repr(first_5)) # '第一行\n第'注意:在文本模式下,size是字符数,不是字节数。一个中文字符算一个字符。
readlines()的签名是file.readlines(hint=-1)。不传参数时,读取所有行,返回一个字符串列表。每个元素是一行,包含行尾的换行符。
with open("demo.txt", "r", encoding="utf-8") as f:
lines = f.readlines()
print(lines) # ['第一行\n', '第二行\n', '第三行\n']readlines()适合“需要按行逐个处理,而且文件不大”的场景。比如:
readlines()的hint参数是一个“提示”,告诉Python大概读多少字节。但Python不保证精确按这个数量读,只是用它来优化内存分配。实际中很少用到这个参数。
这就是小郑踩的第一个坑。
content = f.read()
for line in content:
print(line) # 每次输出一个字符read()返回的是字符串。对字符串用for循环,迭代的是字符,不是行。
如果想按行遍历,要么用content.splitlines()先切成列表:
for line in content.splitlines():
print(line)要么一开始就用readlines():
for line in f.readlines():
print(line)要么更直接:文件对象本身就是可迭代的,直接for line in f就是逐行读取。
readlines()保留行尾的\n。如果你直接拿去比较或者拼接,很容易出问题。
lines = f.readlines()
if lines[0] == "第一行": # False,实际是 "第一行\n"
...需要手动去掉换行符:
lines = [line.rstrip("\n") for line in f.readlines()]或者用splitlines():
lines = f.read().splitlines()splitlines()会去掉行尾的换行符,而且能处理\r\n、\r、\n等多种换行符。这通常比readlines()更符合直觉。
readlines()会把整个文件读进内存,变成一个列表。文件几百兆、几个G的时候,内存直接爆掉。
# 文件 5GB,这行会直接 OOM
lines = f.readlines()而且列表里每个元素都是一个独立的字符串对象,额外还有列表本身的开销。一个5GB的文件,读成列表后占用的内存可能超过6GB。
大文件应该逐行迭代,而不是一次性读进列表:
with open("big.log", "r", encoding="utf-8") as f:
for line in f:
process(line)文件对象本身就是可迭代的,每次只读一行,内存占用恒定。这是处理大文件的标准做法。
文件对象内部维护一个“当前位置”指针。read()读完之后,指针到了文件末尾。再调readlines(),读到的是空列表。
with open("demo.txt", "r", encoding="utf-8") as f:
content = f.read() # 指针到末尾
lines = f.readlines() # []
print(lines) # []反过来也一样:
with open("demo.txt", "r", encoding="utf-8") as f:
lines = f.readlines() # 指针到末尾
content = f.read() # ''
print(repr(content)) # ''如果需要重新读,必须f.seek(0)把指针移回开头:
f.seek(0)
content = f.read()还有一个readline(),每次读一行,返回字符串。它和read()名字很像,但行为完全不同。
with open("demo.txt", "r", encoding="utf-8") as f:
line1 = f.readline() # '第一行\n'
line2 = f.readline() # '第二行\n'
line3 = f.readline() # '第三行\n'
line4 = f.readline() # '',到末尾了readline()适合“一次处理一行,但需要手动控制读取节奏”的场景。大多数时候,直接for line in f更简洁,效果也一样。
readline()在文件末尾返回空字符串'',可以用它来判断是否读完:
while True:
line = f.readline()
if not line:
break
process(line)但这段代码完全可以用for line in f替代:
for line in f:
process(line)后者更简洁,也更 Pythonic。
Python 读文本文件主要有四种方式:
方法 | 返回类型 | 内存占用 | 适用场景 |
|---|---|---|---|
read() | 字符串 | 整个文件 | 小文件,需要整体处理 |
readlines() | 字符串列表 | 整个文件 | 小文件,需要按行列表处理 |
readline() | 字符串 | 一行 | 手动控制读取节奏 |
for line in f | 字符串 | 一行 | 大文件逐行处理,首选 |
内存占用上,read()和readlines()都是O(文件大小),readline()和for line in f都是O(一行大小)。
判断标准很清晰:
需要把整个文件当做一个字符串处理:用read()。比如正则跨行匹配、整体哈希、传给只接受字符串的解析器。
需要按行处理,而且文件不大:用readlines(),或者f.read().splitlines()。
需要按行处理,而且文件很大:用for line in f,逐行迭代,不要一次性读进内存。
需要手动控制读取节奏:用readline()。
需要读取固定大小的块:用read(size),配合循环,适合二进制或超大文本的分块处理。
readlines()保留行尾换行符,splitlines()不保留。大多数按行处理的场景,你其实不想要换行符。
# readlines():行尾带 \n
lines = f.readlines()
print(lines) # ['第一行\n', '第二行\n']
# splitlines():行尾不带 \n
lines = f.read().splitlines()
print(lines) # ['第一行', '第二行']splitlines()还能正确处理\r\n(Windows换行)、\r(老Mac换行)、\n(Unix换行)以及一些特殊换行符(如\v、\f)。跨平台处理文本时,splitlines()比readlines()更可靠。
但splitlines()需要先read()整个文件,所以只适合小文件。大文件还是用for line in f,需要去换行符就line.rstrip("\n")。
对于小文件,read()、readlines()、for line in f的差别不大。对于大文件,差别就出来了。
import time
# 假设有一个 100MB 的文本文件
# read()
start = time.time()
with open("big.txt", "r", encoding="utf-8") as f:
content = f.read()
print(f"read(): {time.time() - start:.2f}s")
# readlines()
start = time.time()
with open("big.txt", "r", encoding="utf-8") as f:
lines = f.readlines()
print(f"readlines(): {time.time() - start:.2f}s")
# for line in f
start = time.time()
with open("big.txt", "r", encoding="utf-8") as f:
for line in f:
pass
print(f"for line in f: {time.time() - start:.2f}s")时间上,三者可能差不多,因为瓶颈在磁盘I/O。但内存占用完全不同:read()和readlines()要一次性加载整个文件,for line in f只加载一行。文件越大,内存差距越明显。
小郑最终的代码是这样的。
统计错误次数,用逐行迭代:
count = 0
with open("app.log", "r", encoding="utf-8") as f:
for line in f:
if "ERROR" in line:
count += 1跨行正则匹配,用read():
with open("app.log", "r", encoding="utf-8") as f:
content = f.read()
matches = re.findall(r"ERROR.*?\n.*?stack", content, re.DOTALL)需要按行列表处理的小文件,用splitlines():
with open("config.txt", "r", encoding="utf-8") as f:
lines = f.read().splitlines()他后来在团队文档里写了一条:“read()读成字符串,readlines()读成列表。字符串用for循环拿到的是字符,列表用for循环拿到的是行。大文件用for line in f,别一次性读进内存。需要去换行符用splitlines(),别用readlines()再手动处理。”
这个区分看起来简单,但真到写代码的时候,很多人会随手写f.read()然后for循环,发现拿到的是字符,还以为是编码问题。或者用readlines()读一个大日志,把内存撑爆。
记住那个复印记事本的比喻:read()是把整本记事本复印到一张长纸上,readlines()是拆成一页一页装进文件夹。你要整体处理,就用长纸;你要逐页处理,就用文件夹;文件太大,就一页一页翻,别一次性全拆开。
文本IO本身不难,难的是想清楚“我要的是整块文本还是按行列表,文件有多大,内存够不够”。想清楚了,read和readlines就不再是坑,而是两个各司其职的工具。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。