菜单

Administrator
发布于 2026-10-17 / 2 阅读
0
0

Python P6:文件读写——让程序把结果存下来

Python P6:文件读写——让程序把结果存下来

系列:Python 从零开始 · P6 这一课:open() 开门、with 自动关门、读写的四种姿势,附 SSH 日志分析实战

P5 我们造了函数这台机器:parse_log(log) 吃进一行日志,吐出结构化数据,漂亮。但有个扎心的问题——程序一退出,算出来的东西全没了。

变量、字典、函数的返回值,都住在内存里。内存是"临时工宿舍":程序活着,它在;程序一关门,人去楼空。想让数据活下去,得把它写进文件——硬盘才是"档案室"。

这篇解决三件事:怎么读文件、怎么写文件、怎么写得安全不丢数据。最后串起 P3、P4、P5 的全部本事,做一个 SSH 登录日报脚本。

一、open():开门的三个要素

Python 里跟文件打交道,只认一个入口:open()。把它想象成"开门"——开哪扇门、进去干什么、用什么语言交流,三个要素一次说清:

log_file = open("auth.log", "r", encoding="utf-8")
  • "auth.log":文件名,开哪扇门

  • "r":模式,进去干什么(r = read,只读)

  • encoding="utf-8":编码,用哪种语言交流——中文不乱码就靠它

门开了,open() 会递给你一个文件对象(这里叫 log_file),之后读和写都找它。

二、四种模式:r、w、a

模式决定了你进门之后能干什么,先看最常用的三个:

模式

名字

干什么

文件不存在时

r

只读

只能读,不能写

报错 FileNotFoundError

w

只写

只能写,会清空原文件

自动新建

a

追加

只能写,从文件末尾接着写

自动新建

r 是默认模式,不写也行,但写上更清楚。

演示 1:r 模式读一个不存在的文件

python3 -c "open('no_such_file.log', 'r')"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
FileNotFoundError: [Errno 2] No such file or directory: 'no_such_file.log'

只读模式很"老实":文件不在,它不替你新建,直接报错。(番外篇讲过怎么读报错:从下往上看,最后一行就是病因。)

演示 2:w 模式会清空原文件——这是个坑

echo "原来的内容" > demo.txt
cat demo.txt
原来的内容
python3 -c "
with open('demo.txt', 'w', encoding='utf-8') as f:
    f.write('新的内容\n')
"
cat demo.txt
新的内容

看见没?"原来的内容"没了。w 一开门,先把屋子清空。 想保留原内容、往后接着写,用 a(append,追加):

python3 -c "
with open('demo.txt', 'a', encoding='utf-8') as f:
    f.write('追加的一行\n')
"
cat demo.txt
新的内容
追加的一行

记住这个手感:w 是"推倒重写",a 是"续写"。 日志、日报这种越积越多的东西,永远用 a;配置文件整体重写,才用 w。

还有第四种 +(如 r+、w+),意思是"又读又写",新手期几乎用不上,先知道有它就行。

三、读文件:四种姿势

门开了(r 模式),怎么把内容拿出来?四种姿势:

log_file = open("auth.log", "r", encoding="utf-8")

all_text = log_file.read()        # 姿势 1:一次全读进来,变成一个大字符串
log_file.seek(0)                  # 把"读指针"拨回开头(不然下面读到的是空的)

first_line = log_file.readline()  # 姿势 2:只读一行
rest_lines = log_file.readlines() # 姿势 3:剩下的一次全读,装进一个列表

log_file.close()

多说一句 seek(0):文件对象里有个"读指针",读到哪它跟到哪。read() 一口气读到文件尾,指针就停在末尾了——后面再 readline() 只能读到空气。seek(0) 就是把指针拨回开头。

真实跑一下姿势 2,看它到底拿回来什么:

python3 -c "
with open('auth_demo.log', encoding='utf-8') as f:
    first_line = f.readline()
print(repr(first_line))
"
'Oct 16 09:12:01 web sshd[1234]: Failed password for root from 203.0.113.7 port 51234\n'

注意末尾那个 \n:文件里的每一行,读进来都自带一个换行符。 print() 默认又会加一个换行,所以直接 print(first_line) 会多出一个空行——处理文本时记得 .strip() 去掉它(P2 讲过 strip())。

姿势 4(推荐):for 循环逐行读

with open("auth_demo.log", "r", encoding="utf-8") as log_file:
    for log_line in log_file:
        print(log_line.strip())

这是服务器脚本里最常用的读法,两个理由:

  1. 省内存:read() 是把整个文件一口气吞进内存,10GB 的日志能把小 VPS 直接吃爆;for 循环一次只读一行,文件再大也不怕。

  2. 顺手:每一行天然就是一个 log_line,P3 的 for 循环 + if "Failed" in log_line 直接就能接上。

四、写文件:write() 只干一件事

report_file = open("daily_report.txt", "w", encoding="utf-8")
report_file.write("SSH 登录日报\n")
report_file.close()

write() 只干一件事:把字符串原样写进去,一个字节都不多给。想要换行,自己加 \n——忘了加,下一笔就糊在同一行末尾。这是新手最常踩的坑之一。

五、with:自动关门,数据不丢

你可能注意到了,上面的例子里我一会儿 log_file.close(),一会儿又 with open(...) as f:。with 是 Python 给文件准备的"自动门":

# 不用 with:门要自己关,忘了关就出事
f = open("report.txt", "w", encoding="utf-8")
f.write("重要数据")
# 万一这里报错崩了,close() 没执行——门没关!

# 用 with:代码块一结束,自动关门,天塌下来也关
with open("report.txt", "w", encoding="utf-8") as f:
    f.write("重要数据")
# 到这里,门已经自动关了

为什么关门这么重要? 写文件不是"一写就落盘"——Python 为了快,会先把数据攒在内存的缓冲区里,攒够一批再一次性写进硬盘。close()(也就是 with 结束时自动做的事)就是"结账"的那一下:把缓冲区里剩下的数据全部刷进文件。

门没关 = 账没结 = 最后那点数据可能根本没写进文件。程序正常结束时 Python 会尽力收尾,但中途报错崩溃的话,没有 with 保护的那部分数据就丢了。

规矩:以后凡是 open(),一律配 with。 这是 Python 社区的铁律,照做就行。

六、实战:SSH 登录日报脚本

把 P3(循环+判断)、P4(字典)、P5(函数)的本事全用上。场景:每天看一眼 /var/log/auth.log,到底谁在爆破我的 SSH?

先准备一份模拟日志 auth_demo.log(8 行,混合了爆破和正常登录):

Oct 16 09:12:01 web sshd[1234]: Failed password for root from 203.0.113.7 port 51234
Oct 16 09:12:04 web sshd[1235]: Failed password for root from 203.0.113.7 port 51235
Oct 16 09:12:09 web sshd[1236]: Accepted password for chris from 198.51.100.9 port 2222
Oct 16 09:12:15 web sshd[1237]: Failed password for admin from 203.0.113.7 port 51236
Oct 16 09:12:20 web sshd[1238]: Failed password for root from 192.0.2.44 port 3333
Oct 16 09:12:31 web sshd[1239]: Failed password for root from 203.0.113.7 port 51237
Oct 16 09:12:40 web sshd[1240]: Accepted publickey for chris from 198.51.100.9 port 2223
Oct 16 09:12:55 web sshd[1241]: Failed password for test from 192.0.2.44 port 3334

脚本 parse_auth.py(变量名按规矩来:小写+下划线、见名知意):

log_path = "auth_demo.log"
report_path = "daily_report.txt"

failed_by_ip = {}  # 字典:IP -> 失败次数(P4 的本事)
accepted_count = 0

# 读:逐行扫描日志
with open(log_path, "r", encoding="utf-8") as log_file:
    for log_line in log_file:
        if "Failed password" in log_line:
            ip_address = log_line.split("from")[1].split()[0]
            failed_by_ip[ip_address] = failed_by_ip.get(ip_address, 0) + 1
        elif "Accepted" in log_line:
            accepted_count += 1

# 写:把统计结果存进报告文件
with open(report_path, "w", encoding="utf-8") as report_file:
    report_file.write("SSH 登录日报\n")
    report_file.write("=" * 30 + "\n")
    report_file.write(f"成功登录: {accepted_count} 次\n")
    report_file.write(f"失败尝试: {sum(failed_by_ip.values())} 次\n")
    report_file.write("失败来源 IP:\n")
    for ip_address, fail_count in failed_by_ip.items():
        report_file.write(f"  {ip_address}: {fail_count} 次\n")

print(f"分析完成,报告已写入 {report_path}")

拆解几个关键点:

  • log_line.split("from")[1].split()[0]:先按 "from" 切开取后半段,再按空白切开取第一个词——就是 IP 地址。P2 的 split() 组合拳。

  • failed_by_ip.get(ip_address, 0) + 1:字典的 get(),key 不在就按 0 算,省掉一次 if 判断——P4 的字典在这里当了计数器。

  • 读和写各用一个 with,门都自动关,数据丢不了。

真实运行(Python 3.12.3):

python3 parse_auth.py
分析完成,报告已写入 daily_report.txt

生成的报告文件 daily_report.txt 内容:

SSH 登录日报
==============================
成功登录: 2 次
失败尝试: 6 次
失败来源 IP:
  203.0.113.7: 4 次
  192.0.2.44: 2 次
ls -l
-rw-rw---- 1 root root 684 Oct 16 17:46 auth_demo.log
-rw-rw---- 1 root root 146 Oct 16 17:46 daily_report.txt
-rw-rw---- 1 root root 956 Oct 16 17:46 parse_auth.py

一眼结论:203.0.113.7 在 30 秒里爆破了 4 次——这就是 Fail2Ban 该封的 IP(还记得那篇《Fail2Ban 防护》吗,同一个战场)。以前这种统计靠肉眼翻日志,现在一个脚本每天跑一遍,报告自动躺在文件里。

七、三个小坑

  1. 相对路径坑:open("auth.log") 找的是"当前目录"下的文件,不是脚本所在的目录。换个目录运行脚本就 FileNotFoundError。稳妥做法:用绝对路径,或者先 cd 到日志目录再跑。

  2. 编码坑:encoding="utf-8" 每次都写上。Windows 上默认是 GBK,中文一写就乱码、一读就报错。养成肌肉记忆。

  3. w 误用坑:想追加却用了 w,原文件内容一次清空,找不回来。拿不准就先备份:cp report.txt report.txt.bak,这是 L9 学过的保命习惯。

小结

模式

含义

文件不存在

典型用途

r

只读

报错

读日志、读配置

w

只写(清空原文件)

自动新建

生成报告、重写配置

a

追加(末尾续写)

自动新建

写日志、攒日报

方法

作用

注意

read()

一次读全部

大文件别用,内存扛不住

readline()

读一行

行尾自带 \n,记得 strip()

readlines()

全读进列表

同 read(),大文件慎用

for line in f

逐行读(推荐)

省内存,服务器脚本首选

write(s)

写字符串

换行要自己加 \n

with open(...) as f

自动关门

以后 open 一律配 with

下一课 P7:异常处理——文件不存在、权限不够,程序不再直接崩溃,try/except 让脚本优雅地兜住错误。


评论