日志文件一大,readlines() 这东西就开始露馅。
本地跑着没事,线上一拉 2GB 的 access.log,进程内存直接往上顶。代码看着还挺无辜:
with open("access.log", "r", encoding="utf-8") as f:
lines = f.readlines()
for line in lines:
if"ERROR"in line:
print(line)
这段我一般第一眼就不太信。
不是因为它语法有问题,而是它把整个文件一次性塞进了内存。小文件无所谓,配置文件、几十行 CSV、测试数据都能这么干。日志、导出文件、用户上传的大文本,就别这么写了。
Python 里读文件常见就这三个:read()、readline()、readlines()。
名字长得像,脾气完全不一样。
read() 是一次读一坨。
with open("order_2026_05.log", "r", encoding="utf-8") as f:
content = f.read()
print(content[:200])
不传参数,它会把文件剩余内容全部读出来,返回一个字符串。
这个适合读小文件,比如一份模板、一段 SQL、一份 JSON 配置。
with open("sql/check_user.sql", "r", encoding="utf-8") as f:
sql = f.read()
sql = sql.replace("${day}", "2026-05-10")
print(sql)
但 read() 不是只能全读,也可以指定大小。
with open("upload_packet.bin", "rb") as f:
head = f.read(16)
body = f.read(1024)
print(head.hex())
print(len(body))
这里要注意,文本模式下 read(10) 读的是 10 个字符,二进制模式下 read(10) 读的是 10 个字节。排查文件头、协议包、图片头这些东西,我一般都用 rb,少一点编码干扰。
readline() 是一次读一行。
with open("access.log", "r", encoding="utf-8") as f:
first = f.readline()
second = f.readline()
print(first.rstrip())
print(second.rstrip())
它每次只往后挪一行,返回的字符串通常带着结尾的 \n。所以很多代码会写 rstrip(),不然打印时容易多空一行。
readline() 的好处是稳。文件再大,它也不是一下子吞进去。
比如线上查某个订单号,我更愿意这样写:
deffind_order_log(path, order_no):
with open(path, "r", encoding="utf-8", errors="replace") as f:
line_no = 0
whileTrue:
line = f.readline()
if line == "":
break
line_no += 1
if order_no in line:
return line_no, line.rstrip()
returnNone, None
line_no, text = find_order_log("pay.log", "ORDER_930021")
if text:
print(f"hit line={line_no}, log={text}")
else:
print("not found")
这里有个小细节:判断结束用 line == "",不要直接写 if not line 来糊弄。
空行读出来是 "\n",不是空字符串。文件真的读到末尾,才是 ""。
当然,平时更常见的写法不是手写 readline(),而是直接迭代文件对象:
with open("pay.log", "r", encoding="utf-8", errors="replace") as f:
for line_no, line in enumerate(f, start=1):
if"timeout"in line or"Traceback"in line:
print(line_no, line.rstrip())
这个底层也是按流式方式往后读,内存压力小,代码还干净。处理大日志,我基本优先写这种。
readlines() 是一次把所有行读成列表。
with open("users.txt", "r", encoding="utf-8") as f:
rows = f.readlines()
print(type(rows))
print(rows[:3])
返回结果大概这样:
[
"1001,alice\n",
"1002,bob\n",
"1003,cindy\n",
]
它不是返回一个大字符串,而是返回一个列表,列表里每个元素是一行。
这东西适合小文件,而且适合你确实需要“所有行一起处理”的场景。比如读取一份白名单,再做清洗:
defload_user_whitelist(path):
with open(path, "r", encoding="utf-8") as f:
rows = f.readlines()
users = set()
for row in rows:
row = row.strip()
ifnot row or row.startswith("#"):
continue
users.add(row)
return users
white_users = load_user_whitelist("white_users.conf")
print("U10086"in white_users)
这种配置文件就几百行,readlines() 没啥毛病。别把它拿去读大文件就行。
我见过一个比较典型的慢代码,导入供应商给的商品文件,几十万行,代码这么写:
with open("goods.csv", "r", encoding="utf-8") as f:
rows = f.readlines()
for row in rows:
save_goods(row)
这段不只是内存难看,还有一个问题:前面读完整个文件,后面才开始处理。文件越大,等待越明显。
我一般会改成边读边处理:
defimport_goods(path):
ok_count = 0
bad_count = 0
with open(path, "r", encoding="utf-8", errors="replace") as f:
for line_no, line in enumerate(f, start=1):
line = line.strip()
ifnot line:
continue
parts = line.split(",")
if len(parts) < 4:
bad_count += 1
print(f"bad row line={line_no}, data={line}")
continue
sku, name, price, stock = parts[:4]
try:
price = int(price)
stock = int(stock)
except ValueError:
bad_count += 1
print(f"bad number line={line_no}, data={line}")
continue
# 这里换成真实入库逻辑
# goods_repo.upsert(sku, name, price, stock)
ok_count += 1
return ok_count, bad_count
ok, bad = import_goods("goods.csv")
print(f"import done, ok={ok}, bad={bad}")
这段代码不花哨,但线上更扛事。哪一行坏了能打出来,文件再大也不会先吃一大口内存。
还有个容易误会的点:read()、readline()、readlines() 都会受“文件指针”影响。
with open("demo.txt", "r", encoding="utf-8") as f:
print(f.readline())
print(f.read())
第一行被 readline() 读走了,后面的 read() 只能读剩下的内容。
想重新读,要么重新打开文件,要么 seek(0):
with open("demo.txt", "r", encoding="utf-8") as f:
first = f.readline()
f.seek(0)
all_text = f.read()
print(first.rstrip())
print(all_text[:100])
这几个方法的选择,我通常就按文件大小和处理方式来判断。
读小配置、小模板、小 JSON:read()。
只看第一行,或者手动控制一行一行读:readline()。
小文件,并且后面确实要拿到所有行列表:readlines()。
大日志、大 CSV、大导入文件:不要上来就 readlines(),直接 for line in f。
很多 Python 文件处理问题,不是语法问题,是读法太贪。
小文件怎么写都行,大文件就别赌内存。文件越大,代码越应该一口一口吃。