12文件操作

12文件操作

_

注:学习资料来源<尚硅谷>

视频:点击跳转

代码仓库:pythonStudy

在 Python 学习与开发过程中,文件操作是一项绕不开的基础能力。无论是读取配置文件、保存日志、处理文本数据,还是复制图片、音频、视频等资源,本质上都离不开对文件系统的访问与管理。熟练掌握文件操作,不仅能帮助我们编写更实用的程序,也能为后续学习数据处理、自动化办公、爬虫、后端开发等方向打下坚实基础。

本文将系统梳理 Python 文件操作的核心知识,包括文件分类、路径表示、open() 函数、读取与写入方式、with 上下文管理器、缓冲区与 flush()、组合模式、目录操作,以及两个典型练习案例。整篇内容尽量采用连贯自然的叙述方式,既适合入门阶段建立整体认知,也适合已有基础的读者进行查漏补缺。


一、文件的本质与分类

从操作系统的角度看,文件本质上是存储在外部介质中的一段有组织的数据。程序运行时,往往需要把内存中的数据持久化到磁盘,或者把磁盘中的数据重新加载到内存,这就是文件操作的核心意义。

在日常开发中,文件通常可以分为两大类:纯文本文件二进制文件。理解这两类文件的差异,是正确使用文件读写模式的前提。

纯文本文件在读取和存储时,需要遵循某种字符编码规范,例如 UTF-8、GBK、ASCII 等。也就是说,文本内容在保存之前会先根据编码规则转换成二进制字节,而在读取时则会按照相同或兼容的编码规则解码为字符。正因为存在“编码—解码”的过程,纯文本文件通常可以直接被人阅读,其内容表现为文字信息,例如 .txt.py.md.html 等文件都属于这一类。

与之相对,二进制文件并不以字符编码为核心,而是按照某种特定的文件格式规范来组织字节数据。比如图片、音频、视频、文档、演示文稿等,都属于典型的二进制文件。它们无法直接通过普通文本编辑器正确阅读,需要由能够识别其格式的软件进行解析。例如 .jpg.png.mp3.mp4.doc.ppt 等,都是常见的二进制文件。

因此,我们可以用一句话概括两者的区别:文本文件强调字符语义与编码规则,二进制文件强调数据结构与格式规范。这个区别会直接影响我们后续使用 open() 时选择文本模式还是二进制模式。


二、绝对路径与相对路径

在操作文件之前,程序首先要知道文件在哪里,这就涉及路径的概念。路径可以理解为文件或目录在文件系统中的“地址”。Python 中最常见的路径表示方式有两种:绝对路径相对路径

绝对路径是从文件系统的根位置开始,完整描述目标文件或目录所在的位置。比如在 Windows 中,D:/demo/test/a.txt 就是一个绝对路径;在 Linux 或 macOS 中,类似 /home/user/project/data.txt 这样的写法也属于绝对路径。它的特点是清晰、唯一、明确,不依赖程序当前所在位置。

相对路径则是以当前工作目录为参照来描述目标文件的位置。比如 a.txt 表示当前目录下的 a.txt 文件,../a.txt 表示上一级目录中的 a.txt,而 ../../data/info.txt 则表示向上返回两级目录后,再进入 data 目录查找 info.txt。其中,. 表示当前目录,.. 表示上一级目录。

很多文件找不到的问题,本质上不是文件不存在,而是开发者误判了当前工作目录。尤其是在 Jupyter Notebook、IDE、命令行、脚本运行器等不同环境中,工作目录可能并不完全相同。因此,在编写依赖相对路径的程序时,最好明确当前工作目录,必要时可以使用 os.getcwd() 进行检查。

路径问题看似简单,但在真实项目中非常关键。很多“读取失败”“写入失败”“权限异常”等问题,最终都可能追溯到路径配置不正确。因此,建议在初学阶段就养成清晰区分绝对路径与相对路径的习惯。


三、Python 文件操作的标准流程

Python 中文件操作通常遵循一个非常经典的流程:打开文件、操作文件、关闭文件。这三步看似朴素,却构成了几乎所有文件处理逻辑的基础。

第一步是创建文件对象,也就是通过 open() 函数与目标文件建立连接。第二步是调用文件对象的方法执行读取、写入、追加等具体操作。第三步是关闭文件对象,释放底层系统资源。如果文件未及时关闭,可能导致资源泄露、文件内容未完整写入、其他程序无法访问该文件等问题。

因此,文件操作的核心入口就是 open() 函数。它的基本形式如下:

 open(file, mode='r', encoding=None)

其中,file 表示文件路径,mode 表示打开模式,encoding 表示字符编码。对于文本文件而言,encoding 尤其重要;对于二进制文件,则一般不需要设置编码。


四、open() 函数与常见打开模式

理解 open() 的关键,在于理解不同的打开模式。模式决定了文件是以只读、只写、追加、二进制还是可更新的方式被打开。

r 是最常见的读取模式,也是默认模式。以 r 模式打开文件时,要求目标文件必须已经存在,否则会抛出 FileNotFoundError。如果配合文本模式使用,也就是 rt,那么读取到的是字符串内容。

w 表示写入模式。它的一个非常重要的特点是:如果文件已存在,会先截断文件内容,也就是先清空原来的数据,再从头写入。如果文件不存在,通常会自动创建新文件。因此,w 模式适合“覆盖式写入”,但不适合保留旧数据的场景。

x 表示排它性创建模式。它要求目标文件必须不存在,否则就会直接报错。这种模式非常适合需要避免误覆盖已有文件的业务场景,例如生成一次性报告、导出唯一结果文件等。

a 表示追加模式。如果文件已存在,那么写入操作会默认发生在文件末尾;如果文件不存在,通常也会创建新文件。它非常适合日志记录、操作历史保存等“保留既有内容并持续增加”的需求。

b 表示二进制模式,用于处理图片、音频、视频、压缩包等非文本数据。使用 b 模式时,读写的对象是字节流,而不是字符串。

t 表示文本模式,它其实是默认值。文本模式会涉及编码与解码过程,因此对于纯文本文件来说最常见。

+ 表示更新模式,也就是文件既可以读,也可以写。它不会单独使用,而是通常与 rwxa 组合形成如 r+w+a+ 等模式。

这些模式之间的差异,看似细微,实际影响很大。开发中一旦模式选错,轻则读不到数据,重则误清空原文件。因此,建议在每次写文件前都先确认业务意图:到底是读取、覆盖、创建还是追加。


五、读取文件的几种常见方式

文件读取是最常见的操作之一。Python 为文件对象提供了多种读取方法,以适应不同数据规模和处理需求。常用方式包括 read()readline()readlines() 以及直接使用 for 循环遍历文件对象。

1. read():按整体或按指定大小读取

read() 方法用于从文件中读取内容。如果不传参数,它会一次性读取整个文件的所有内容;如果传入 size 参数,则表示读取指定数量的字符或字节。

例如:

 file = open('a.txt', 'rt', encoding='utf-8')
 content = file.read()
 print(content)
 file.close()

这类写法适合处理较小的文本文件,因为一次性读取完整内容在逻辑上非常直接。但如果目标文件很大,这种方式可能带来较高的内存占用。

read(size) 的另一个重要特点是:它会从上一次读取结束的位置继续向后读。也就是说,文件对象内部维护着一个“文件指针”,多次调用 read() 时,读指针会不断向后移动。当读取到文件末尾后,再继续读取就会返回空字符串 ''(文本模式)或空字节串 b''(二进制模式)。

如果要处理大文件,通常更推荐循环分块读取,例如每次读取固定大小的数据,这样可以显著降低内存压力。这种方式在处理大文本文件、日志文件、音视频文件时非常常见。

2. readline():按行读取

readline() 用于读取当前的一行内容。它既可以不传参数,也可以传入一个上限参数,表示最多读取多少字符或字节。需要特别注意的是,这个参数不是“读取多少行”,而是“在当前行中最多读取多少内容”。

按行读取的优势在于,它非常适合处理结构化文本,比如配置文件、日志文件、CSV 原始文本等。很多业务场景中,我们并不关心整个文件的完整字符串,而是希望一行一行地分析、过滤、统计,这时 readline() 就很自然。

同样地,readline() 也是基于当前文件指针继续读取的,读到文件末尾后会返回空字符串。因此,经常可以把它与 while True 配合使用,通过判断返回值是否为空来结束循环。

3. for 循环遍历文件对象

在 Python 中,文件对象本身是一个可迭代对象,因此可以直接使用 for line in file 的方式逐行遍历文件内容。这是一种非常 Pythonic 的写法,也是最推荐的文本文件读取方式之一。

例如:

 with open('a.txt', 'rt', encoding='utf-8') as file:
     for line in file:
         print(line, end='')

这种方式的优点很明显:语法简洁、可读性强、逐行处理、对大文件友好。对于绝大多数文本处理任务,使用 with + for 的组合通常都是优先选择。

4. readlines():一次性读取所有行

readlines() 会把文件内容一次性按行读取出来,并返回一个列表,列表中的每个元素通常对应文件中的一行。

例如:

 with open('a.txt', 'rt', encoding='utf-8') as file:
     lines = file.readlines()
     print(lines)

这种方式的好处是便于后续做列表级别的处理,比如切片、排序、批量修改等。但它的缺点也很明显:通常会把所有内容一次性加载到内存中,所以并不适合体积较大的文件。

因此,从工程实践的角度看,readlines() 更适用于小规模文本的便捷处理,而不适合大数据量的文件读取任务。


六、为什么推荐使用 with

在传统写法中,文件打开后需要手动调用 close() 进行关闭。但如果程序在操作过程中发生异常,close() 可能无法顺利执行,最终造成资源未释放的问题。

为了解决这个问题,Python 提供了 with 上下文管理机制。它的本质是一种资源托管语法,适合处理那些“需要成对出现”的操作,例如打开/关闭、加锁/解锁、连接/断开等。

使用 with 打开文件时,程序进入代码块前会自动执行资源准备逻辑,离开代码块后无论是否发生异常,都会自动进行资源释放。因此,它能显著提高代码的健壮性与可维护性。

例如:

 with open('a.txt', 'rt', encoding='utf-8') as file:
     data = file.read()
     print(data)

这段代码在语义上非常清晰:文件只在 with 管理的代码块中有效,代码块结束后自动关闭文件,无需再额外调用 close()

从底层机制来看,with 依赖的是上下文管理器协议,即对象实现 __enter__()__exit__() 方法。__enter__() 在进入代码块之前执行,其返回值会赋给 as 后的变量;__exit__() 在离开代码块时执行,无论是正常结束还是发生异常都会被调用。

这就是为什么文件对象可以天然与 with 配合使用,因为文件对象本身就是一个符合上下文管理器协议的资源对象。

从编码规范角度讲,只要是文件操作,优先使用 with,已经可以视为一种最佳实践。


七、文件写入操作

除了读取,写入同样是文件操作的核心场景。Python 中文件写入主要依赖 write() 方法,而实际行为则由打开模式决定。

如果使用 w 模式,程序在写入之前会先截断文件内容,这意味着原文件中的数据会被全部清空。因此,w 模式适合那些“新结果完全覆盖旧结果”的任务,例如重新生成报表、导出最新配置等。

如果使用 x 模式,则要求目标文件不存在。程序会尝试创建新文件,一旦文件已存在就报错。这个模式更强调“安全创建”,避免误覆盖已有结果。

如果使用 a 模式,则写入内容会自动添加到文件末尾。这是日志、运行记录、消息缓存等场景中最常用的方式,因为它能够保留历史内容,并持续追加新的数据。

举个简单的例子:

 with open('b.txt', 'wt', encoding='utf-8') as file:
     file.write('你好')

这段代码会以文本写入模式打开 b.txt,然后写入“你好”。如果文件存在,原内容会被覆盖;如果文件不存在,则会新建文件。

写入时还应注意一个细节:write() 方法返回的是成功写入的字符数或字节数,但它并不会自动换行。如果需要多行文本,通常需要手动添加 \n


八、缓冲区与 flush() 方法

很多初学者在写文件时会误以为 write() 一调用,数据就立即保存到了磁盘。实际上,大多数情况下并不是这样。为了提高 I/O 性能,Python 在写文件时通常会先把数据写入缓冲区,当缓冲区满、文件关闭、程序正常结束,或者显式触发刷新时,数据才真正写入磁盘。

这种机制的优点是减少频繁磁盘访问带来的性能损耗,但也意味着:如果程序尚未结束、文件尚未关闭,某些数据可能仍停留在内存缓冲区中。

这时,flush() 方法就非常有用了。它的作用是将缓冲区中的数据立即刷新到文件中。例如在写日志、写临时结果、长时间运行任务的实时输出中,及时调用 flush() 可以避免程序中途异常退出导致部分信息尚未来得及落盘。

例如:

 with open('demo.txt', 'at', encoding='utf-8') as file:
     file.write('你好1')
     file.write('你好2')
     file.flush()

在这个例子中,调用 flush() 后,前面写入的内容会尽快写入文件,而不必等到 with 代码块结束。

需要注意的是,flush() 主要解决的是 Python 层面的缓冲刷新问题。在更底层的操作系统缓存层面,仍然可能存在额外的延迟机制。但在大多数应用开发场景中,flush() 已经足够满足“及时写入”的需求。


九、组合模式:读写一体化操作

某些业务场景并不满足于单纯读取或单纯写入,而是希望在同一个文件对象上同时完成读写操作。这时就可以使用带 + 的组合模式。

1. r+

r+ 表示在文件已存在的前提下,以可读可写的方式打开文件。它不会清空文件,文件指针初始位于开头。因此,如果直接写入,很可能覆盖原有内容的一部分。

2. w+

w+ 表示可读可写,但会先清空文件内容。也就是说,它兼具写入和读取能力,但代价是文件会被重置。适合那些需要“先生成新内容,再重新读回来验证或处理”的场景。

3. x+

x+ 表示排它性创建并支持读写,要求文件必须不存在。这种模式在需要安全创建新文件且马上读取验证时很有用。

4. a+

a+ 表示可读可写,并且写入行为默认发生在文件末尾。它常用于日志类任务:既希望继续追加新日志,又希望偶尔读取历史日志内容。

组合模式下,一个非常重要的辅助方法是 seek()。该方法用于调整文件指针的位置,从而决定下一次读写从哪里开始。它的基本形式是:

 file.seek(offset, whence)

其中 offset 表示偏移量,whence 表示参考点。whence=0 代表从文件开头计算,whence=1 代表从当前位置计算,whence=2 代表从文件末尾计算。

例如,在 w+ 模式下写入内容后,文件指针通常位于末尾。如果想立即读取刚写入的内容,就需要先使用 seek(0, 0) 把指针移回开头,再执行 read()

不过,在文本模式下对文件位置进行复杂跳转时,应保持谨慎,尤其是包含中文字符的文本文件。因为文本模式涉及编码与解码,字符与字节的边界并不总是可以简单对应。若需要精确字节级定位,通常更适合使用二进制模式。


十、目录操作:不仅能处理文件,也能管理文件夹

文件系统中的操作对象不只是文件,还包括目录。很多时候,程序在写文件前需要先检查目标目录是否存在;批量处理文件时,还需要遍历某个目录下的所有子文件与子目录。因此,目录操作也是文件处理体系的重要组成部分。

Python 中常见的目录操作依赖 osshutil 模块。

os.mkdir(path) 用于创建单级目录。如果目录已经存在,通常会抛出异常。它适用于路径层级简单且父目录已经存在的场景。

os.makedirs(path) 用于创建多级目录。如果某些中间层级不存在,它会一并创建,因此在工程开发中更常用。

os.rmdir(path) 用于删除空目录。它有两个限制:目录必须存在,而且必须为空,否则会报错。

os.removedirs(path) 则可以递归删除空目录。当最深层目录删除成功后,它会继续尝试向上删除父级目录,直到某个父目录不为空为止。

os.path.exists(path) 用于判断路径是否存在,不管这个路径指向的是文件还是目录。

os.path.isdir(path) 用于判断路径是否存在且是否为目录;而 os.path.isfile(path) 用于判断路径是否存在且是否为文件。它们对于路径分流处理非常实用。

os.scandir(path) 可以扫描指定目录下的直接子项,并返回可迭代对象。相比单纯获取名称,它能更方便地判断当前子项是文件还是目录。

os.walk(path) 则更强大,它会按层级递归遍历指定目录下的所有子目录和文件。对于批量处理、自动整理、构建索引、统计文件数量等任务来说,os.walk() 是极其实用的工具。

最后,shutil.rmtree(path) 可以删除非空目录,这个功能非常强大,也非常危险。因为它会递归清除整个目录树,所以在使用前必须反复确认路径无误,避免误删重要数据。

可以说,文件操作解决的是“如何处理单个文件”,而目录操作解决的是“如何组织和管理一批文件”。


十一、练习一:复制二进制文件

理解文件操作最好的方式,不只是记住 API,更是通过具体案例理解它们的使用场景。第一个典型练习是:复制一个二进制文件到指定位置

这个任务背后包含多个知识点。首先,源文件和目标文件都必须以二进制模式打开,即分别使用 rbwb。因为对于音频、视频、图片等二进制文件来说,不能使用文本模式,否则极有可能破坏原始字节结构。其次,为了提升健壮性,目标目录若不存在,应该先自动创建。最后,为了节省内存,不宜一次性把整个文件全部读入,而应采用分块读取、分块写入的方式。

示例代码如下:

import os

source = 'music.mp3'
target = 'D:/media'

if not os.path.isdir(target):
    os.makedirs(target)

with open(source, 'rb') as f1, open(target + '/' + 'my_music.mp3', 'wb') as f2:
    while True:
        data = f1.read(1024)
        if not data:
            break
        f2.write(data)

print('复制完毕')

这段程序体现了几个值得注意的工程意识。第一,目录存在性检查是必要的前置条件。第二,二进制复制应以字节块的形式完成,不能把它当成普通字符串处理。第三,with 可以同时管理多个文件对象,这样源文件和目标文件都能在代码块结束后自动关闭。

这个练习虽然简单,但其思路几乎可以迁移到所有文件复制、上传前缓存、下载后落盘等任务中,是非常经典的基础案例。


十二、练习二:基于文件的日志记录系统

第二个练习是一个更贴近业务开发的场景:用户登录校验与日志记录

需求很明确:用户输入用户名和密码后,程序进行校验。如果用户名不存在,则提示“用户名未注册”,同时记录日志;如果用户名存在但密码错误,则提示“密码错误”,同时记录日志;如果用户名和密码都正确,则提示“登录成功”,并记录日志。

这个案例的价值在于,它不仅使用了条件判断和字典查找,还把文件写入应用到了真实业务流程中。也就是说,文件不再只是“练习 I/O 的对象”,而是承载程序运行痕迹的业务载体。

示例代码如下:

import time

users = {
    '张三': '123456',
    '李四': '888888',
    '王五': 'abc123'
}

username = input('请输入用户名:')
password = input('请输入密码:')
now = time.strftime('%Y-%m-%d %H:%M:%S')

if username not in users:
    print('用户名未注册')
    with open('log.txt', 'at', encoding='utf-8') as file:
        file.write(f'{now}  {username}  登录失败(用户未注册)\n')
elif users[username] != password:
    print('密码错误')
    with open('log.txt', 'at', encoding='utf-8') as file:
        file.write(f'{now}  {username}  登录失败(密码错误)\n')
else:
    print('登录成功!')
    with open('log.txt', 'at', encoding='utf-8') as file:
        file.write(f'{now}  {username}  登录成功\n')

从技术角度看,这个案例展示了几个非常重要的能力点。首先,a 模式适合日志类场景,因为日志本质上是不断增长的历史记录。其次,时间戳的引入使日志具备了基本的可追踪性。再次,通过结构化文本写入,可以为后续排查问题、数据统计、行为审计提供依据。

如果进一步扩展,这个简单案例还可以演化为真正的日志系统。例如为不同日志级别添加标识(INFO、WARNING、ERROR),为不同模块分别写入不同文件,甚至使用 Python 的 logging 标准库替代手写文件追加逻辑。这些都属于文件操作在工程实践中的自然延伸。


十三、文件操作中的常见注意事项

掌握 API 只是第一步,真正写出稳定可靠的文件操作程序,还需要注意一些细节问题。

首先,对于文本文件,编码必须统一。最常见的编码方式是 UTF-8。如果一个文件是以 UTF-8 编码保存的,却用 GBK 去读取,就可能抛出解码异常。因此,建议在文本读写中显式指定 encoding='utf-8',而不是完全依赖默认值。

其次,读取大文件时尽量避免一次性全量加载。虽然 read()readlines() 用起来很方便,但如果文件规模较大,可能导致内存占用过高。更稳妥的方式是逐行读取或分块读取。

再次,写文件前要明确是否允许覆盖原内容。w 模式的风险在于“太方便也太危险”,稍不注意就可能把原始数据清空。因此,在涉及重要数据时,更推荐用 ax 或先做备份。

此外,目录删除类操作尤其要谨慎。特别是 shutil.rmtree() 这类递归删除方法,一旦路径写错,后果往往难以挽回。开发测试时,建议使用专门的临时目录进行实验,避免直接操作真实业务数据目录。

最后,尽量优先使用 with。这是最容易落实、收益也最大的编码习惯之一。它能显著降低资源泄露风险,提高异常情况下程序的可靠性。


十四、最佳实践建议

从实际开发经验出发,关于文件操作可以总结出几条非常值得坚持的实践建议。

第一,处理文本文件时,优先使用 with open(..., 'rt', encoding='utf-8') as file: 的结构。它清晰、稳健、易维护,几乎适用于绝大多数文本读取任务。

第二,处理大文本时,优先用 for line in file 的方式逐行遍历。相比一次性读取全部内容,它更加节省内存,也更符合流式处理思想。

第三,处理图片、音频、视频、压缩包等非文本资源时,务必使用二进制模式,也就是 rbwb。不要试图用文本模式读取这类文件。

第四,在写入重要结果前,先确认模式是否会覆盖原文件。养成“先判断,再写入”的意识,可以避免很多低级错误。

第五,凡是涉及路径拼接的地方,尽量使用 os.path.join(),而不是手工拼接斜杠。这样可以提高跨平台兼容性。

第六,对于需要长期运行或中途就要查看写入结果的程序,可以适时配合 flush() 使用,以提升可观察性与安全性。


十五、总结

Python 文件操作看似只是语言基础中的一小部分,但它连接着程序与真实数据世界,是非常重要的能力模块。通过本文的梳理,我们可以建立一个较为完整的知识框架:

文件分为文本文件和二进制文件,前者关注编码与解码,后者关注格式与字节流;路径分为绝对路径和相对路径,理解当前工作目录是正确定位文件的前提;文件操作通常围绕 open() 展开,不同模式决定了文件对象的行为边界;读取文件可以使用 read()readline()readlines() 以及 for 遍历,不同方式适用于不同规模和场景;with 是资源管理的最佳实践,它让打开与关闭的生命周期更安全;写入操作中要特别注意覆盖、追加与排它性创建的区别;flush() 让缓冲区数据更及时地落盘;组合模式和 seek() 则进一步增强了读写一体化处理能力;而目录操作则把我们的能力从“处理单个文件”扩展到了“管理整个文件系统结构”。

如果把 Python 学习比作构建一座工程大厦,那么文件操作就是其中非常关键的一层基础结构。它并不炫技,却极具实用价值;它看似简单,但越往后学越能感受到它的重要性。无论是脚本开发、自动化处理,还是数据工程与后端系统,文件操作能力都将长期发挥作用。

当你真正熟练掌握这些内容后,接下来完全可以继续深入学习 pathlibjson/csv 文件处理、日志系统、压缩解压、批量文件自动化处理等更高阶主题。届时你会发现,今天理解的每一个基础细节,都会在后续实践中不断被验证和放大。

13进程与线程 2026-04-26
11迭代器与生成器 2026-04-26

评论区