Python 系列(四十七) 学会读写文本文件,让程序保存运行结果
1 | 作者:李晓辉 |
- Python 系列(一) 从零开始认识 Python,入门其实没那么难
- Python 系列(二) 学 Python 的第一步,把开发环境搭建好
- Python 系列(三) 基础语法避坑指南,缩进、大小写、续行与注释
- Python 系列(四) Python 输入输出详解,print()、input() 与常见避坑指南
- Python 系列(五) 数值类型与运算符详解,整除、浮点精度与进制转换实战
- Python 系列(六) 字符串处理实战,查找、替换、切片与日志分析
- Python 系列(七) 类型转换详解,int()、float()、str() 与金额计算避坑
- Python 系列(八) Python 流程控制入门,掌握 if、elif 和 else
- Python 系列(九) Python 逻辑运算符详解,and、or、not 一篇搞懂
- Python 系列(十) while、for 循环实战,彻底搞懂 range、break 与 continue
- Python 系列(十一) list、tuple、set、dict,到底该怎么选?
- Python 系列(十二) List 列表实战,搞定数据增删改查与批量处理
- Python 系列(十三) Tuple 元组实战,搞懂不可变特性与 enumerate() 遍历
- Python 系列(十四) Set 集合实战,搞定数据去重与集合运算
- Python 系列(十五) Dict 字典详解,掌握快速查找与键值操作
- Python 系列(十六) sort() 和 sorted() 详解,掌握数据排序技巧
- Python 系列(十七) 自定义函数入门,让重复代码一次编写多次使用
- Python 系列(十八) 函数参数详解,搞懂位置参数、关键字参数和默认参数
- Python 系列(十九) 从 *args 到 **kwargs,让函数调用更灵活
- Python 系列(二十) 变量作用域是什么?一文搞懂 global 和 nonlocal
- Python 系列(二十一) 搞懂变量、对象和引用,理解函数参数传递机制
- Python 系列(二十二) map() 和 filter() 详解,轻松实现批量数据处理与筛选
- Python 系列(二十三) 嵌套函数与 lambda 表达式详解,搞懂作用域与闭包
- Python 系列(二十四) 搞懂递归函数,从基线条件到实际应用
- Python 系列(二十五) 聊聊模块与 import,让代码告别重复编写
- Python 系列(二十六) Python 模块入门,从 import 到模块复用
- Python 系列(二十七) Python dir() 函数详解,快速查看模块中的函数与属性
- Python 系列(二十八) 不会用 Python 标准库?教你快速查找模块和函数
- Python 系列(二十九) 搞懂 Python sys 模块,让脚本更适合自动化运维
- Python 系列(三十) 为什么 0.1 + 0.2 不等于 0.3?彻底搞懂 Decimal
- Python 系列(三十一) 搞懂 datetime 日期时间,轻松处理时区与时间计算
- Python 系列(三十二) 面向对象编程入门,从零理解类、对象、封装与继承
- Python 系列(三十三) 什么是类和对象?零基础理解 Python class
- Python 系列(三十四) Python 属性详解,搞懂类属性、实例属性和 setter
- Python 系列(三十五) 学会特殊方法,让对象打印出你想要的内容
- Python 系列(三十六) 多个对象如何共享数据?彻底搞懂类变量
- Python 系列(三十七) 搞懂父类、子类和方法重写,真正理解继承与多态
- Python 系列(三十八) 如何判断数据类型?三个内置函数一次讲明白
- Python 系列(三十九) 搞懂异常模型与常见异常类型,读懂程序报错
- Python 系列(四十) 从零学会异常处理,掌握 try、except、else 和 finally
- Python 系列(四十一) ValueError、TypeError 到底是什么?一文搞懂常见异常
- Python 系列(四十二) ValueError、TypeError 到底是什么?一文搞懂常见异常
- Python 系列(四十三) 别只会处理异常!学会自定义异常,让程序更容易维护
- Python 系列(四十四) 从零认识 assert 断言,让程序学会检查自己
- Python 系列(四十五) 使用 sys 模块处理标准输入与输出
- Python 系列(四十六) 使用 open() 读取和写入文件
- Python 系列(四十七) 学会读写文本文件,让程序保存运行结果
- Python 系列(四十八) 搞懂 bytes、bytearray 和二进制读写
- Python 系列(四十九) 使用 seek() 和 tell() 灵活控制文件读取位置
- Python 系列(五十) 掌握 os 和 os.path,实现文件与目录自动化管理
- Python 系列(五十一) 掌握列表推导式,轻松实现列表生成与数据筛选
- Python 系列(五十二) 掌握字典推导式,让字典创建更简单
- Python 系列(五十三) 从零理解生成器,轻松处理大量数据
- Python 系列(五十五) 使用 zip() 同时遍历多个集合
前面我们学习了如何使用 print() 在屏幕上显示信息,以及如何使用 input() 接收用户输入。
但这两种方式都有一个明显的局限:程序运行时产生的数据,如果没有保存到文件或其他持久化存储中,程序结束后就无法通过程序变量继续使用这些数据。
比如,我们编写了一个 Python 程序,用于检查 Linux 服务器的磁盘使用率。当程序发现某台服务器的磁盘使用率超过 80% 时,我们希望把检查结果保存下来,方便后续查看。
如果每次都只能在屏幕上查看结果,那么程序关闭后,再想查找之前的检查记录就不方便了。
这时,我们就需要让 Python 具备读取和写入文件的能力。
在 Linux 运维工作中,文件无处不在:服务器配置文件、应用程序日志、自动化巡检结果、备份记录,都可以通过 Python 进行读取和处理。
这篇文章将从最基本的文本文件开始,带你一步步掌握 Python 文件读写的常用方法。
认识文本文件
我们平时接触的很多文件,都是以文本形式保存数据的,例如:
server.txt:记录服务器的 IP 地址。config.txt:保存程序使用的配置信息。app.log:记录应用程序的运行日志。report.txt:保存自动化巡检的结果。
文本文件可以保存人类能够阅读的字符,例如中文、英文、数字和标点符号。
例如,创建一个名为 server.txt 的文件,内容如下:
1 | web01 192.168.8.128 |
使用文本编辑器打开这个文件,就可以直接看到里面保存的内容。
Python 也可以完成同样的操作:读取文件中的内容,将其交给程序处理,或者把新的内容写入文件。
整个过程可以理解为:
flowchart TD
A[文本文件] --> B[Python 程序]
B --> C[读取内容]
C --> D[分析或处理数据]
D --> E[生成处理结果]
E --> F[写入文本文件]接下来,我们先学习如何读取一个已经存在的文本文件。
使用 open() 打开文件
在 Python 中,open() 是打开文件的内置函数。
它会根据我们指定的文件路径和打开模式,打开文件并返回一个文件对象。我们可以把文件对象理解为 Python 与文件进行交互的入口。
先看一个简单的例子:
1 | f = open("server.txt", "r", encoding="utf-8") |
这行代码暂时不需要死记硬背,我们逐个理解其中的内容。
open():告诉 Python 打开一个文件。"server.txt":指定要打开的文件名。"r":表示以读取模式打开文件。encoding="utf-8":指定文本的字符编码,避免读取中文时出现乱码。f:用于保存open()返回的文件对象,后续可以通过它读取文件内容。
这里的 r 是英文单词 read 的首字母,表示读取。
需要注意,"server.txt" 是相对路径。Python 会以当前工作目录为基准寻找这个文件。如果文件不存在,读取模式通常会抛出 FileNotFoundError 异常。
如果不清楚程序当前在哪个目录运行,可以使用 os.getcwd() 查看:
1 | import os |
运行后,程序会输出当前工作目录。例如:
1 | /home/python |
如果输出的是 /home/python,那么 Python 就会尝试读取 /home/python/server.txt。
如果文件放在其他目录,也可以使用绝对路径,例如:
1 | f = open("/home/python/server.txt", "r", encoding="utf-8") |
这样就不需要依赖当前工作目录来确定文件的位置。
使用 read() 读取文件内容
打开文件后,我们就可以使用文件对象提供的方法读取内容。
最常见的方法之一是 read()。
例如:
1 | f = open("server.txt", "r", encoding="utf-8") |
假设 server.txt 的内容如下:
1 | web01 192.168.8.128 |
运行程序后,会输出:
1 | web01 192.168.8.128 |
这段程序完成了几个步骤。
首先,open() 打开文件。
然后,f.read() 读取文件中的内容,并将读取到的文本作为字符串返回。
接着,data = f.read() 将这个字符串保存到变量 data 中。
最后,print(data) 将变量中的内容显示在屏幕上。
这里需要特别注意:read() 读取的是文件内容,print() 才负责把内容显示出来。
如果只写下面这行代码:
1 | data = f.read() |
程序已经读取了文件,但不会自动将读取结果显示在屏幕上。
另外,read() 默认会尝试读取从当前位置到文件末尾的全部剩余内容。如果文件非常大,一次性读取可能会占用较多内存,因此处理大型日志文件时,还需要学习逐行读取等方法。
为什么推荐使用 with open()
前面的例子最后使用了:
1 | f.close() |
它的作用是关闭文件。
为什么需要关闭文件?
因为程序打开文件后,操作系统需要为文件操作分配相应的资源。文件使用完毕后,应该及时释放这些资源。
如果程序在打开文件后发生异常,导致 f.close() 没有执行,文件就可能没有按照预期及时关闭。
为了解决这个问题,Python 提供了一种更方便的写法:with open()。
例如:
1 | with open("server.txt", "r", encoding="utf-8") as f: |
这段代码的执行结果与前面的示例相同。
不过,写法有所不同。
with open(...) as f: 表示打开文件,并将文件对象交给变量 f 使用。
冒号后面的缩进代码,就是需要在文件打开期间执行的操作。
当程序离开 with 代码块时,Python 会自动关闭文件,即使代码块执行过程中发生异常,也会进行相应的清理。
因此,在实际开发中,我们通常推荐使用 with open(),而不是每次手动调用 close()。
可以把它理解为:
flowchart TD
A[进入 with 代码块] --> B[打开文件]
B --> C[读取或写入内容]
C --> D[离开 with 代码块]
D --> E[自动关闭文件]这里还有一个需要注意的地方:with 代码块中的内容必须按照 Python 的缩进规则编写,通常使用四个空格缩进。
例如:
1 | with open("server.txt", "r", encoding="utf-8") as f: |
data = f.read() 属于 with 代码块,而 print(data) 位于代码块之外。
由于文件内容已经保存到了变量 data 中,因此即使文件关闭后,我们仍然可以使用 data。
使用 read() 读取部分内容
如果文件内容比较多,我们不一定需要一次读取全部内容。
read() 还可以接收一个数字参数,用于指定最多读取多少个字符。
例如:
1 | with open("server.txt", "r", encoding="utf-8") as f: |
假设文件开头的内容是:
1 | web01 192.168.8.128 |
运行后输出:
1 | web0 |
为什么只输出了 web0?
因为 f.read(4) 表示从当前读取位置开始,最多读取四个字符。
第一次读取之后,文件对象的读取位置也会向后移动。
例如:
1 | with open("server.txt", "r", encoding="utf-8") as f: |
输出:
1 | web0 |
第一次读取了 web0,第二次就从上一次结束的位置继续读取,而不是重新从文件开头开始。
这种行为对于理解文件读取非常重要。
使用 readline() 逐行读取
除了 read(),Python 还提供了 readline(),用于读取一行文本。
例如:
1 | with open("server.txt", "r", encoding="utf-8") as f: |
假设文件内容如下:
1 | web01 192.168.8.128 |
运行后只会输出第一行:
1 | web01 192.168.8.128 |
这是因为 readline() 每次只读取一行,通常会保留这一行末尾的换行符。
这里的 end="" 是 print() 的参数,用于取消它默认添加的结尾换行符。
如果不写 end="",由于读取到的文本可能已经带有换行符,输出时就可能出现额外的空行。
如果希望连续读取多行,可以多次调用 readline():
1 | with open("server.txt", "r", encoding="utf-8") as f: |
这样就会输出文件的前两行。
不过,如果不知道文件一共有多少行,就不适合一直手动调用 readline()。
对于日志分析、服务器清单处理等场景,我们通常会使用 for 循环逐行读取文件,后续学习循环时会更容易理解这种写法。
使用 write() 写入文本文件
学会读取文件后,我们再来看如何将数据保存到文件中。
例如,我们想让 Python 自动生成一份服务器巡检记录,而不是每次都在屏幕上查看结果。
这时可以使用文件对象的 write() 方法。
先看一个简单的示例:
1 | with open("report.txt", "w", encoding="utf-8") as f: |
运行后,程序会在当前工作目录下创建 report.txt,并写入以下内容:
1 | 服务器巡检开始 |
如果文件原本不存在,Python 会创建这个文件。
如果文件已经存在,使用 "w" 模式打开时,原来的文件内容通常会被清空,再写入新的内容。
这是一个非常重要的注意事项:使用 "w" 写入已有文件之前,一定要确认是否允许覆盖原有数据。
再看代码中的另一个细节:
1 | f.write("服务器巡检开始\n") |
这里的 \n 表示换行符。
write() 本身不会自动在每次写入的文本末尾添加换行符。如果希望下一次写入的内容出现在新的一行,就需要根据实际需要添加 \n。
例如:
1 | with open("test.txt", "w", encoding="utf-8") as f: |
文件内容将会是:
1 | 第一行第二行 |
因为两次写入的字符串之间没有换行符。
如果改成:
1 | with open("test.txt", "w", encoding="utf-8") as f: |
文件内容才会分成两行。
使用 a 模式追加内容
在运维工作中,我们经常需要不断记录新的巡检结果或程序运行日志。
例如,每次执行巡检时,都希望在原有记录的末尾增加一条新记录,而不是清空以前的内容。
这种情况下,就应该使用 "a" 模式。
a 是英文单词 append 的首字母,表示追加。
示例代码如下:
1 | with open("report.txt", "a", encoding="utf-8") as f: |
如果 report.txt 原本的内容是:
1 | 服务器巡检开始 |
执行追加操作后,文件内容将变成:
1 | 服务器巡检开始 |
原有内容得到了保留,新内容被追加到了文件末尾。
如果文件原本不存在,使用 "a" 模式通常也会创建文件。
这里可以对比一下 "w" 和 "a":
| 打开模式 | 文件不存在时 | 文件已经存在时 |
|---|---|---|
"r" | 通常报错 | 从已有文件读取 |
"w" | 创建文件 | 清空原内容,再写入 |
"a" | 创建文件 | 保留原内容,在末尾追加 |
对于保存巡检历史、持续记录日志等场景,通常更适合使用 "a"。
一个完整的实战案例:生成服务器巡检报告
现在,我们把前面学到的内容结合起来,编写一个完整的小程序。
假设我们需要检查三台服务器,并把检查结果写入文件。
为了让初学者可以直接运行,这里先使用模拟数据,不会真正连接服务器。
创建一个名为 server_check.py 的文件,在其中写入以下代码:
1 | # 模拟服务器巡检结果 |
这段代码包含了几个之前学习过的知识点。
servers保存模拟的服务器信息。with open()打开报告文件。"w"表示写入模式。encoding="utf-8"用于指定文本编码。for循环逐台处理服务器信息。f.write()将每台服务器的检查结果写入文件。print()在屏幕上提示报告已经生成。
如果还不熟悉列表、循环或 f"..." 这样的字符串写法,也不用着急。这里的重点是理解:程序如何把已经准备好的数据保存到文件中。
在终端中运行:
1 | python server_check.py |
在 Linux 环境中,也可以根据实际安装情况使用:
1 | python3 server_check.py |
程序运行后,屏幕会显示:
1 | 巡检报告已生成,请查看 report.txt |
接着,我们查看生成的文件:
1 | cat report.txt |
预期输出:
1 | 服务器巡检报告 |
这样,我们就完成了从准备数据到生成巡检报告的整个过程。
在实际工作中,数据来源可以进一步替换成 Linux 命令的执行结果、日志分析结果,或者通过 API 获取的服务器状态。
需要注意,这个案例目前只是将模拟数据写入文件,并没有真正执行服务器检查命令。
文本文件为什么需要指定编码
在前面的示例中,我们反复使用了:
1 | encoding="utf-8" |
为什么需要它?
因为计算机保存文件时,最终保存的是字节,而文本需要按照一定的字符编码规则,才能在读取时正确还原成文字。
可以简单理解为:写入文件时,Python 需要知道怎样把文字转换成字节;读取文件时,则需要知道怎样把字节还原成文字。
如果写入和读取时使用的编码不匹配,就可能出现乱码,甚至在某些情况下直接报错。
例如:
1 | with open("hello.txt", "w", encoding="utf-8") as f: |
这段代码使用 UTF-8 编码写入中文。
读取时也指定 UTF-8:
1 | with open("hello.txt", "r", encoding="utf-8") as f: |
输出:
1 | 你好,Python! |
对于自己创建的文本文件,建议明确指定 encoding="utf-8"。
但在实际运维工作中,我们还可能遇到来自 Windows、旧版应用程序或其他系统的文件。它们使用的编码不一定是 UTF-8,因此读取外部文件时,需要根据文件的实际编码选择正确的参数。
读取文件时常见的错误
初学者在操作文件时,经常会遇到以下问题。
文件不存在
例如:
1 | with open("missing.txt", "r", encoding="utf-8") as f: |
如果当前工作目录下不存在 missing.txt,程序就会抛出 FileNotFoundError。
排查时,可以先确认文件是否存在,以及程序当前的工作目录是否正确。
中文乱码或编码错误
如果文件使用的实际编码与 encoding 指定的编码不一致,就可能出现乱码或 UnicodeDecodeError。
解决方法是确认文件的实际编码,而不是随意更换编码参数。
不小心覆盖了原有内容
如果希望保留原有内容,却使用了 "w" 模式,原来的文件内容就可能被清空。
如果目标是增加新记录,应考虑使用 "a" 模式。
忘记添加换行符
write() 不会自动添加换行符。如果连续写入多条文本而没有添加 \n,它们就可能连在一起。
一次性读取过大的文件
read() 默认读取全部剩余内容。如果文件达到数 GB,一次性读取可能占用大量内存。
对于大型日志文件,通常更适合逐行处理,避免将整个文件一次性加载到内存中。
写在最后
这篇文章,我们从最基本的文本文件开始,学习了如何让 Python 读取和保存数据。
需要重点掌握的内容包括:
open():打开文件。read():读取文件的全部剩余内容,或者指定数量的字符。readline():读取一行文本。write():向文件写入文本。with open():在代码块结束时自动关闭文件。"r"、"w"、"a":分别表示读取、写入和追加模式。encoding="utf-8":明确指定文本编码。\n:在文本中表示换行。
文件读写是自动化运维中非常基础、也非常实用的能力。掌握它之后,我们就可以开始编写能够保存巡检结果、整理配置文件、分析日志的 Python 程序。
不过,并不是所有文件都能像普通文本一样读取。图片、压缩包、音频和许多其他文件,都包含不能简单当作普通文本处理的数据。
下一篇,我们将继续学习二进制文件的读取与写入,了解 Python 如何处理这些不同于普通文本的文件。
