首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Python的文本IO把我坑惨了,原来read和readlines的区别这么大

Python的文本IO把我坑惨了,原来read和readlines的区别这么大

原创
作者头像
风一样的男子
发布于 2026-10-23 15:33:11
发布于 2026-10-23 15:33:11
120
举报
文章被收录于专栏:编程教程编程教程

小郑那天写了一个日志分析工具,要统计应用日志里每种错误出现的次数。日志文件不大,几百兆,他想着一次性读进来处理最方便。代码大概长这样:

代码语言:javascript
复制
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():

代码语言:javascript
复制
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:

代码语言:javascript
复制
第一行
第二行
第三行

用两种方式读:

代码语言:javascript
复制
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():整个文件读成一个字符串

read()的签名是file.read(size=-1)。不传参数时,读取从当前位置到文件末尾的所有内容,返回一个字符串。

代码语言:javascript
复制
with open("demo.txt", "r", encoding="utf-8") as f:
    content = f.read()

print(content)
# 第一行
# 第二行
# 第三行

read()适合“需要把整个文件当做一个整体来处理”的场景。比如:

  • 用正则做跨行匹配
  • 把整个文件内容传给某个只接受字符串的解析器
  • 统计整个文件的字符数、单词数
  • 文件很小,一次性读完最省事

read()也可以传一个size参数,读取指定数量的字符:

代码语言:javascript
复制
with open("demo.txt", "r", encoding="utf-8") as f:
    first_5 = f.read(5)
    print(repr(first_5))  # '第一行\n第'

注意:在文本模式下,size是字符数,不是字节数。一个中文字符算一个字符。

readlines():整个文件读成一个字符串列表

readlines()的签名是file.readlines(hint=-1)。不传参数时,读取所有行,返回一个字符串列表。每个元素是一行,包含行尾的换行符。

代码语言:javascript
复制
with open("demo.txt", "r", encoding="utf-8") as f:
    lines = f.readlines()

print(lines)  # ['第一行\n', '第二行\n', '第三行\n']

readlines()适合“需要按行逐个处理,而且文件不大”的场景。比如:

  • 逐行解析配置
  • 逐行过滤日志
  • 对每一行做独立处理

readlines()的hint参数是一个“提示”,告诉Python大概读多少字节。但Python不保证精确按这个数量读,只是用它来优化内存分配。实际中很少用到这个参数。

坑一:对 read() 的结果用 for 循环,拿到的是字符

这就是小郑踩的第一个坑。

代码语言:javascript
复制
content = f.read()
for line in content:
    print(line)  # 每次输出一个字符

read()返回的是字符串。对字符串用for循环,迭代的是字符,不是行。

如果想按行遍历,要么用content.splitlines()先切成列表:

代码语言:javascript
复制
for line in content.splitlines():
    print(line)

要么一开始就用readlines():

代码语言:javascript
复制
for line in f.readlines():
    print(line)

要么更直接:文件对象本身就是可迭代的,直接for line in f就是逐行读取。

坑二:readlines() 返回的行带换行符

readlines()保留行尾的\n。如果你直接拿去比较或者拼接,很容易出问题。

代码语言:javascript
复制
lines = f.readlines()
if lines[0] == "第一行":  # False,实际是 "第一行\n"
    ...

需要手动去掉换行符:

代码语言:javascript
复制
lines = [line.rstrip("\n") for line in f.readlines()]

或者用splitlines():

代码语言:javascript
复制
lines = f.read().splitlines()

splitlines()会去掉行尾的换行符,而且能处理\r\n、\r、\n等多种换行符。这通常比readlines()更符合直觉。

坑三:大文件用 readlines() 会撑爆内存

readlines()会把整个文件读进内存,变成一个列表。文件几百兆、几个G的时候,内存直接爆掉。

代码语言:javascript
复制
# 文件 5GB,这行会直接 OOM
lines = f.readlines()

而且列表里每个元素都是一个独立的字符串对象,额外还有列表本身的开销。一个5GB的文件,读成列表后占用的内存可能超过6GB。

大文件应该逐行迭代,而不是一次性读进列表:

代码语言:javascript
复制
with open("big.log", "r", encoding="utf-8") as f:
    for line in f:
        process(line)

文件对象本身就是可迭代的,每次只读一行,内存占用恒定。这是处理大文件的标准做法。

坑四:以为 read() 和 readlines() 可以混用

文件对象内部维护一个“当前位置”指针。read()读完之后,指针到了文件末尾。再调readlines(),读到的是空列表。

代码语言:javascript
复制
with open("demo.txt", "r", encoding="utf-8") as f:
    content = f.read()       # 指针到末尾
    lines = f.readlines()    # []
    print(lines)             # []

反过来也一样:

代码语言:javascript
复制
with open("demo.txt", "r", encoding="utf-8") as f:
    lines = f.readlines()    # 指针到末尾
    content = f.read()       # ''
    print(repr(content))     # ''

如果需要重新读,必须f.seek(0)把指针移回开头:

代码语言:javascript
复制
f.seek(0)
content = f.read()

坑五:readline() 和 read() 搞混

还有一个readline(),每次读一行,返回字符串。它和read()名字很像,但行为完全不同。

代码语言:javascript
复制
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()在文件末尾返回空字符串'',可以用它来判断是否读完:

代码语言:javascript
复制
while True:
    line = f.readline()
    if not line:
        break
    process(line)

但这段代码完全可以用for line in f替代:

代码语言:javascript
复制
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()?

判断标准很清晰:

需要把整个文件当做一个字符串处理:用read()。比如正则跨行匹配、整体哈希、传给只接受字符串的解析器。

需要按行处理,而且文件不大:用readlines(),或者f.read().splitlines()。

需要按行处理,而且文件很大:用for line in f,逐行迭代,不要一次性读进内存。

需要手动控制读取节奏:用readline()。

需要读取固定大小的块:用read(size),配合循环,适合二进制或超大文本的分块处理。

一个容易忽略的细节:splitlines() 比 readlines() 更干净

readlines()保留行尾换行符,splitlines()不保留。大多数按行处理的场景,你其实不想要换行符。

代码语言:javascript
复制
# 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的差别不大。对于大文件,差别就出来了。

代码语言:javascript
复制
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只加载一行。文件越大,内存差距越明显。

回到小郑的故事

小郑最终的代码是这样的。

统计错误次数,用逐行迭代:

代码语言:javascript
复制
count = 0
with open("app.log", "r", encoding="utf-8") as f:
    for line in f:
        if "ERROR" in line:
            count += 1

跨行正则匹配,用read():

代码语言:javascript
复制
with open("app.log", "r", encoding="utf-8") as f:
    content = f.read()
matches = re.findall(r"ERROR.*?\n.*?stack", content, re.DOTALL)

需要按行列表处理的小文件,用splitlines():

代码语言:javascript
复制
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 删除。

目录
  • 先看一个最直观的对比
  • read():整个文件读成一个字符串
  • readlines():整个文件读成一个字符串列表
  • 坑一:对 read() 的结果用 for 循环,拿到的是字符
  • 坑二:readlines() 返回的行带换行符
  • 坑三:大文件用 readlines() 会撑爆内存
  • 坑四:以为 read() 和 readlines() 可以混用
  • 坑五:readline() 和 read() 搞混
  • 四种读取方式的对比
  • 什么时候用 read(),什么时候用 readlines()?
  • 一个容易忽略的细节:splitlines() 比 readlines() 更干净
  • 性能差异
  • 回到小郑的故事
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档