写Python到现在,我遇到过不少初学者问我:“学完语法之后该练什么?”我的答案里永远有一个选项——文件操作。不是说文件操作有多难,而是它太实用了:爬虫爬下来的数据要存成文件,处理Excel表格要读写文件,程序运行的日志要记录到文件,就连配置文件也是文件。可以说,只要你的程序不是纯黑盒,早晚都要跟文件打交道。这篇文章就是一份Python文件操作的快速复习笔记,覆盖路径、编码、打开模式、读写方式、上下文管理器,再到目录操作和综合实战,适合已经学过基本语法、想系统梳理文件操作知识点的读者,也适合放在手边随时查阅。
1. 文件操作是Python里最被低估的基础功
很多人觉得文件操作不就是open()加read()吗?有什么好学的。但真正写项目的时候,你会发现文件操作里藏着一堆细节:编码不对打不开、路径写错找不到文件、忘记关闭导致数据没写进去、大文件一次性读入内存直接卡死。这些问题单独拿出来都不难,但放在一起就成了新手劝退组合拳。
文件操作的基本流程其实就三步:打开文件、读写内容、关闭文件。Python内置的open()函数负责第一步和第三步的桥梁,read()、write()这些方法负责第二步。举个最简单的例子:
# 读取一个文本文件 f = open('test.txt', 'r', encoding='utf-8') content = f.read() print(content) f.close()这段代码没有任何问题,但它不是最佳写法。原因后面我会详细讲,先记住一个结论:open()出来的文件对象,用完必须关闭。不关闭的后果在Windows上特别明显——文件会被锁住,你后续想删除、重命名、覆盖它都会报权限错误。在Linux/macOS上表现没那么激烈,但反复打开不关闭,文件描述符会泄漏,程序跑久了就会报Too many open files。
文件操作涉及的知识点其实是一个完整链路:路径定位到文件、指定编码解码内容、选择合适模式打开、用对方法读写、最后正确关闭。任何一个环节出问题,程序都会报错或者产生错误结果。所以我建议你把文件操作当成一个整体来学,而不是零散地记几个方法。
2. 写在open()之前:路径、编码、模式这三个基础决定了成败
open()不是魔法,它能不能成功打开文件,取决于你给它的三个参数:文件路径、打开模式、编码方式。很多人一上来就写open('test.txt'),结果报错FileNotFoundError,其实就是路径没搞对。
2.1 路径:字符串拼接不是好方案
Python里表示路径有两种方式:普通字符串和pathlib.Path对象。早期教程喜欢用字符串拼接,比如:
base_dir = '/home/user/project' data_file = base_dir + '/data/' + 'test.txt'这种写法在Windows下会踩坑,因为Windows的路径分隔符是反斜杠\,Linux/macOS是正斜杠/。你要是直接写'C:\Users\test.txt',Python会把\U、\t当成转义字符,结果路径直接错了。
从Python 3.4开始,官方推荐的方案是pathlib库,它把路径当作对象来操作,跨平台且代码可读性高:
from pathlib import Path # 构建路径 base_dir = Path('/home/user/project') data_file = base_dir / 'data' / 'test.txt' # 用 / 连接路径 # 判断是否存在 print(data_file.exists()) # 获取文件名、父目录 print(data_file.name) # test.txt print(data_file.stem) # test print(data_file.suffix) # .txt print(data_file.parent) # /home/user/project/data用Path对象有个额外好处:你不需要在代码里写死斜杠方向,pathlib会自动处理当前操作系统的路径分隔符规范。比如在Windows上,data_file的字符串表示会自动变成C:\home\user\project\data\test.txt这种反斜杠格式。
2.2 编码:UTF-8是默认选择,但Windows有坑
编码问题是Python文件操作里最容易让新手崩溃的事情。简单理解,编码就是字符和字节之间互相转换的规则。同一个“中”字,在UTF-8里占3个字节,在GBK里占2个字节,你在读取文件的时候如果用了错误的编码,要么得到乱码,要么直接报UnicodeDecodeError。
Python 3的open()函数在文本模式下默认编码是locale.getpreferredencoding(),这个值在不同系统上不一样。Linux/macOS一般默认UTF-8,Windows的中文系统默认是GBK。这就是同一个脚本在Windows上报编码错误、在Linux上运行正常的原因。
我的建议很直接:写代码的时候,open()里显式指定encoding='utf-8',不要依赖默认值。如果你要处理的文件是GBK编码(比如很多Windows下生成的旧文本文件、CSV文件),那就指定encoding='gbk'。读取时不确定编码,可以用chardet库来检测,但那是另外的话题了。
# 显式指定编码,避免环境差异 with open('test.txt', 'r', encoding='utf-8') as f: content = f.read()写入的时候同理。特别是写文件时如果忘记指定编码,Python会用默认编码去写,结果换台机器用UTF-8读取就乱码了。
2.3 模式:r、w、a、x,选错了会清空文件
open()的第二个参数是打开模式。最常用的就是下面这几个,我整理了一个表格:
| 模式 | 含义 | 文件不存在时 | 文件存在时 | 文件指针位置 |
|---|---|---|---|---|
r | 只读(默认) | 报错 | 安全读取 | 开头 |
w | 只写 | 创建 | 清空内容 | 开头 |
a | 追加写 | 创建 | 保留内容 | 末尾 |
x | 独占创建写 | 创建 | 报错 | 开头 |
rb/wb | 二进制读/写 | 同r/w | 同r/w | 同r/w |
r+ | 读写 | 报错 | 安全打开 | 开头 |
w+ | 读写 | 创建 | 清空内容 | 开头 |
a+ | 读和追加写 | 创建 | 保留内容 | 末尾 |
最容易出事故的是w模式。很多人想写文件,随手写了open('data.txt', 'w'),结果把原来的内容全部清空了。如果你要往已有文件里追加内容,一定要用a模式;如果你不确定文件存在且不想覆盖,用x模式最安全——文件已存在会直接报FileExistsError,不会误删数据。
还有个细节:模式字符串里加了b就表示二进制模式,此时读写的内容是bytes类型而不是str。图片、音频、视频、压缩包这些文件必须以二进制模式打开,否则会报TypeError或者数据损坏。
3. 读文件不是只会read():三种读法各有用武之地
read()是最直观的读文件方法,但不一定是最好的。选哪种读法,核心看文件大小和你要怎么处理内容。
3.1 read():小文件一口气读完
with open('small.txt', 'r', encoding='utf-8') as f: content = f.read()read()不传参数时会把整个文件读进内存,返回一个字符串。文件小(比如几MB以内)的时候完全没问题,代码简洁、处理方便。但你得有个概念:如果文件是5GB,read()就会尝试把5GB塞进内存,结果通常是直接OOM(内存溢出)。所以我只在处理小文件时用read(),大文件绝对不用。
3.2 readline():逐行读,处理一行丢一行
with open('data.txt', 'r', encoding='utf-8') as f: line = f.readline() while line: # 处理每一行内容 print(line.strip()) line = f.readline()readline()每次读一行,所以内存占用固定为一行的大小。上面的写法用while循环判断,当readline()读到文件末尾返回空字符串时循环结束。这个方法的缺点是代码有点啰嗦,而且如果你忘了处理行尾的换行符\n,拼接数据时容易出意外。
3.3 迭代器方式:最推荐,没有之一
上面两种写法,Python官方其实都不太推荐日常使用。因为文件对象本身就是一个迭代器,你直接用for循环遍历,代码最简洁,内存占用也最省:
with open('data.txt', 'r', encoding='utf-8') as f: for line in f: # 每读到一行就处理一行 print(line.strip())这个写法背后就是Python在内部帮你调用readline(),每次只缓存一行数据,所以处理大文件也不怕内存爆炸。我做日志分析、爬虫数据清洗,全是这个写法,稳定又高效。
3.4 readlines():慎用,除非你知道文件不大
with open('data.txt', 'r', encoding='utf-8') as f: lines = f.readlines() # 返回每行组成的列表readlines()把整个文件的所有行读进内存,组成一个列表。好处是你可以通过索引访问任意一行,比如lines[100]。坏处和read()一样——文件大了内存扛不住。而且如果你只是需要逐行处理,这个写法完全没必要,白白浪费内存。
文件读取时还有一个概念容易被忽略——文件指针。文件对象内部维护着一个指针,指向下一个要读的位置。read()读完全部内容后,指针在文件末尾,再调用read()返回空字符串。seek(0)可以把指针移回开头重新读,tell()可以查看当前指针位置。做断点续读、跳读时这两个方法特别有用。
4. 写文件:覆盖、追加、flush,这些细节决定了数据能不能持久化
写文件比读文件更容易踩坑,因为很多问题不会立刻暴露,等到程序崩溃你才发现数据没写进去。
4.1 write()和writelines():写入的是字符串,不是数字
with open('output.txt', 'w', encoding='utf-8') as f: f.write('hello\n') f.write('world\n')write()一次写入一个字符串。如果你想写入的不是字符串,比如数字、列表、字典,必须先转成str或者用json序列化。直接写数字会报TypeError: write() argument must be str, not int。
# 错误示范 with open('output.txt', 'w') as f: f.write(123) # TypeError # 正确示范 with open('output.txt', 'w', encoding='utf-8') as f: f.write(str(123)) # 存数字 f.write(str([1, 2, 3])) # 存列表(但存进去的是字符串表示) # 结构化存储建议用json import json data = {'name': '张三', 'age': 30} with open('data.json', 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=2)4.2 覆盖和追加:一个字符的差别,后果天壤之别
w是覆盖写入,文件原本的内容会被全部清空,从空文件开始写。a是追加写入,原有内容保留,新内容从文件末尾开始写入,不会破坏已有数据。
这个区别看起来简单,但实际项目中真的会有人写错。我之前处理一批日志,本来应该用a往旧日志里追加,结果脚本里写成了w,跑完才发现历史日志全没了。所以我自己定的规矩是:除非你确定要清空重建文件,否则一律用a模式,或者先用exists()判断一下文件状态再决定模式。
4.3 缓冲区、flush()和close():数据不是立刻落盘的
很多人以为write()之后数据马上就写到硬盘了,其实不是。Python的文件写入有缓冲区机制,write()先把数据写到内存缓冲区,等缓冲区满了、或者调用close()、或者显式调用flush(),才会真正写入硬盘。这样做的原因是减少磁盘IO频次,提升性能。
这个机制带来一个隐患:如果程序在write()之后、close()之前异常退出了,缓冲区里的数据可能还没写入硬盘,直接丢失。更常见的情况是,你往文件里写了几行日志,程序没正常结束,最后打开文件发现是空的或者少了几行。
看这个例子:
# 写完后立刻查看文件,可能啥也没有 f = open('test.txt', 'w', encoding='utf-8') f.write('hello') # 此时数据还在缓冲区 # 没有close,程序继续运行,如果你此时用别的程序打开test.txt,文件是空的 f.flush() # 强制把缓冲区数据写入硬盘 # 这下其他程序就能看到内容了 f.close()所以有两个实用建议:
- 写完每个逻辑块后,如果你需要立刻查看文件内容验证,调用一次
flush()。 - 最好还是养成用
with的习惯,让Python自动管理关闭时机(后面详细讲)。
4.4 文件编码在写场景下同样重要
写入UTF-8编码的文件时,记得在open()里指定encoding='utf-8'。跨平台运行脚本时,如果不指定编码,在Windows中文系统上写出来的文件是GBK编码,拿到Linux上用UTF-8打开就是乱码。如果你希望写出来的文件是纯ASCII内容,直接在open()里指定encoding='ascii',写入非ASCII字符时立刻报错,防止脏数据。
5. with open()是标配:上下文管理器解决90%的关闭问题
前面我反复提到with,现在系统说说为什么它是文件操作的标配写法。先看传统写法的问题:
f = open('test.txt', 'r', encoding='utf-8') content = f.read() f.close()这段代码在正常情况下没问题。但如果f.read()抛了异常,比如文件编码不对、磁盘出错,f.close()这行就不会执行,文件句柄泄漏。文件句柄是系统资源,不释放的话,次数多了程序会崩溃。而且你还要记得在每个分支都要close(),代码稍微复杂一点就很容易漏。
with的设计就是为了解决这个问题——不管代码块正常执行完还是抛异常,它都会在退出时自动调用close():
with open('test.txt', 'r', encoding='utf-8') as f: content = f.read() # 到这里文件已经自动关闭了很多人把这个行为叫“上下文管理器”。简单理解:with为你构建了一个安全的作用域,进入时打开资源,退出时自动释放资源。不止文件,数据库连接、网络连接、锁这些资源都可以用with管理,原理是一样的。
5.1 同时打开多个文件:一个with搞定
有时候你需要同时读写两个文件,比如把source.txt的内容复制到target.txt。可以嵌套写,也可以把多个open()写在一个with后面:
# 写法一:嵌套 with open('source.txt', 'r', encoding='utf-8') as src: with open('target.txt', 'w', encoding='utf-8') as dst: dst.write(src.read()) # 写法二:并列(Python 3.1+支持) with open('source.txt', 'r', encoding='utf-8') as src, \ open('target.txt', 'w', encoding='utf-8') as dst: dst.write(src.read())第二种写法更简洁,两个文件都能在适当的时候自动关闭。
5.2 自定义上下文管理器:不只是文件能用
with背后的机制,就是协议——类里实现__enter__()和__exit__()两个特殊方法,对象就变成了可上下文管理的资源。你也可以给自己的业务对象实现这个协议,处理“用完必须清理”的逻辑。不过在日常开发中,你更多是用现成的上下文管理器,比如pathlib.Path的对象就自带open()方法,可以和with结合使用:
from pathlib import Path p = Path('test.txt') with p.open('r', encoding='utf-8') as f: content = f.read()效果和open('test.txt', 'r')一样,但路径处理更灵活。
6. 目录操作:从os到pathlib,遍历、筛选、批量处理一把梭
文件操作不只针对单个文件,很多时候你要处理的是一个目录下的全部文件。比如批量重命名图片、遍历项目里的所有Python文件、把多个日志合并成一个。Python里有两个方案:传统os模块和现代pathlib。
6.1 列出目录内容:listdir vs iterdir
os.listdir()返回目录下所有文件和子目录的名称列表,但不包含路径信息,你得自己拼接完整路径。pathlib的iterdir()则直接返回Path对象,更方便:
import os from pathlib import Path # os模块写法 files = os.listdir('.') for name in files: full_path = os.path.join('.', name) if os.path.isfile(full_path): print(full_path) # pathlib写法 p = Path('.') for item in p.iterdir(): if item.is_file(): print(item)pathlib的路径对象直接有is_file()、is_dir()、suffix这些属性,省了各种判断函数,代码读起来也顺。
6.2 递归遍历目录:walk vs rglob
如果目录下面还有子目录,你需要递归遍历。os.walk()是传统方案,它会递归所有子目录,返回(当前目录路径, 子目录列表, 文件列表)三元组。pathlib则提供了更优雅的rglob()方法:
from pathlib import Path # 找出项目里所有Python文件 p = Path('/path/to/project') for py_file in p.rglob('*.py'): print(py_file)rglob('*.py')会递归匹配所有.py文件,返回Path对象生成器。这个需求以前用os.walk()要写好几行,现在一行搞定。
6.3 批量重命名:一个实战示例
目录操作最常见的场景是批量重命名。假设你在整理照片,想把IMG_001.jpg这种命名改成2024-01-01_001.jpg这种格式。用pathlib可以这样写:
from pathlib import Path photo_dir = Path('/path/to/photos') for img in photo_dir.glob('IMG_*.jpg'): # 把旧前缀替换成日期前缀 new_name = f"2024-01-01_{img.stem.split('_')[1]}.jpg" img.rename(photo_dir / new_name) print(f"已重命名: {img.name} -> {new_name}")这个例子说明了一个关键点:直接用pathlib的rename()方法,参数是目标路径的Path对象,比os.rename()需要手动拼接字符串更不容易出错。
6.4 glob模式匹配:文件筛选利器
pathlib的glob()和rglob()都支持通配符匹配:*匹配任意字符串,?匹配单个字符,还可以用[abc]匹配字符集合。这在筛选文件时特别好用:
from pathlib import Path p = Path('/tmp/data') # 所有以 .log 结尾的文件 logs = list(p.glob('*.log')) # 所有以 access_log 开头、以 .txt 结尾的文件 access_txt = list(p.glob('access_log_*.txt')) # 2023年或2024年的数据文件 data_23_24 = list(p.glob('data_202[34]_*.csv'))7. 文件操作避坑指南:这几个坑我踩过之后才彻底明白
这一节我总结几个自己实际踩过的坑,每个都真实发生过,写出来帮你少走点弯路。
7.1 写入后立刻读取,读到的是空内容
场景:我写一个数据处理脚本,先向文件写入结果,然后立刻用另一个函数读取这个文件,结果读到空字符串。排查了半天发现是缓冲区问题——写入的数据还在缓冲区,没有落盘,读取的时候自然啥也读不到。
解决方案:写入完成后调用flush(),或者用with把写入操作完整结束后再读取。如果你要在同一个进程里先写后读,也可以考虑用r+模式打开文件,或者写完后重新open()读取。
7.2 文件编码报错:UnicodeDecodeError
场景:在Windows上生成的一个CSV文件,用我Linux开发环境上的Python脚本读取,直接报UnicodeDecodeError: 'utf-8' codec can't decode byte 0xd6 in position 0。
解决方案:这个错误就是编码不匹配。我先用chardet探测大概编码,发现是GBK:
import chardet with open('data.csv', 'rb') as f: raw = f.read(10000) # 读取前1万字节来检测 result = chardet.detect(raw) print(result) # {'encoding': 'GB2312', 'confidence': 0.99}然后指定正确编码读取:
with open('data.csv', 'r', encoding='gbk') as f: content = f.read()后来我在写脚本时,凡是需要处理来源不固定的文本文件,都会在前面加入编码探测逻辑,避免这个问题。
7.3 路径里带空格或者中文字符,导致找不到文件
场景:一个用户上传的文件路径是C:\Users\张三\My Documents\新建文件夹\report.txt,脚本里用字符串拼接路径,结果报错说找不到文件。
解决方案:这个问题通常是路径拼接不正确导致的,比如用了+拼接而漏掉了分隔符,或者转义字符没有被正确处理。使用pathlib的/运算符可以有效规避这个问题:
from pathlib import Path base = Path('C:/Users/张三') file_path = base / 'My Documents' / '新建文件夹' / 'report.txt'注意即使路径包含空格、中文字符,Path对象也能正确处理,不需要手动转义。
7.4 大文件读取导致内存不足
场景:处理一个8GB的日志文件,直接用read(),程序跑了十几分钟后内存爆掉,进程被系统杀掉。
解决方案:换成迭代器逐行读取,内存占用从GB级别降到几MB:
with open('huge.log', 'r', encoding='utf-8') as f: for line in f: process_line(line)如果需要处理超大文件的某个特定部分,可以用seek()定位到指定偏移量再读,避免从头遍历。
7.5 文件操作抛出PermissionError
场景:在Windows上运行脚本读取一个Excel文件,报PermissionError: [Errno 13] Permission denied。原因就是那个文件正被另一个程序(比如Excel)独占打开,Python没办法同时读取。
解决方案:先关闭占用文件的程序再运行脚本。另外,如果脚本本身之前打开过文件但没关闭,也会导致后续操作失败。用with管理文件对象能从根上避免这个问题。
8. 实战:按日期拆分日志文件的完整脚本
理论聊得差不多了,来一个综合实战。假设你有一个大日志文件app.log,里面的内容按行记录,每行以日期开头:
2024-01-01 10:23:45 INFO 用户登录 2024-01-01 10:25:12 DEBUG 数据库写入成功 2024-01-02 09:31:08 ERROR 连接超时 2024-01-02 11:02:55 INFO 任务完成 2024-01-03 08:15:33 WARNING 磁盘空间不足需求是把这个大文件按日期拆分成多个小文件:log_2024-01-01.txt、log_2024-01-02.txt,以此类推。用文件操作的知识,我们可以这样实现:
from pathlib import Path def split_log_by_day(log_path): log_file = Path(log_path) if not log_file.exists(): print(f"日志文件不存在: {log_file}") return current_date = None output_file = None try: with log_file.open('r', encoding='utf-8') as f: for line in f: # 假设每行前10个字符是日期 YYYY-MM-DD date_str = line[:10].strip() if not date_str: continue # 日期发生变化时,切换输出文件 if date_str != current_date: # 关闭之前的输出文件 if output_file: output_file.close() current_date = date_str output_filename = Path(f"log_{date_str}.txt") # 使用追加模式,防止重复运行时覆盖已有内容 output_file = output_filename.open('a', encoding='utf-8') # 写入当前行 output_file.write(line) finally: # 确保最后一个输出文件也关闭了 if output_file: output_file.close() if __name__ == '__main__': split_log_by_day('app.log')这个脚本有几个值得注意的设计点:
- 用
pathlib处理路径,避免字符串拼接和平台差异问题。 - 用迭代器方式逐行读取大日志,内存开销小,日志文件几个GB也能处理。
- 日期变化时关闭上一个文件、打开新文件,注意文件关闭时机。
- 输出文件用追加模式
a而不是覆盖模式w,这样即使脚本重复运行,也不会丢失之前拆出来的内容。 - 用
try/finally确保所有打开的文件最终都能关闭。
跑完这个脚本,原本一个大文件就变成了按日期划分的多个小文件,后续按日期分析日志就方便多了。同样的思路可以扩展到按小时拆分、按IP拆分、按错误级别拆分,核心逻辑都是“逐行读取 + 按条件切换输出文件”。
如果你想把多个小日志合并成一个大文件,反向操作也很简单:
from pathlib import Path def merge_logs(output_path, log_files): with Path(output_path).open('w', encoding='utf-8') as out: for log_file in log_files: with Path(log_file).open('r', encoding='utf-8') as inf: for line in inf: out.write(line) # 用法 merge_logs('merged.log', ['log_2024-01-01.txt', 'log_2024-01-02.txt'])9. 文件操作进阶方向:JSON序列化、CSV处理、临时文件
到这里,基础的文件读写已经覆盖完了。但实际开发里,你常常不是直接读写纯文本,而是读写特定格式的数据。我简单提几个高频场景,作为进阶方向。
9.1 JSON文件的读写:项目配置、API响应的标准格式
JSON是Python程序之间、前后端之间交换数据最常用的格式。json模块配合文件操作,两行代码就能搞定:
import json from pathlib import Path # 写入JSON data = { "name": "文件操作复习", "tags": ["python", "文件", "复习"], "version": 1.0 } with Path('config.json').open('w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=2) # 读取JSON with Path('config.json').open('r', encoding='utf-8') as f: loaded = json.load(f) print(loaded["name"]) # 文件操作复习几个注意点:ensure_ascii=False保证中文正常显示而不是变成\uXXXX;indent=2让JSON有缩进,方便人阅读;读取时如果JSON格式不对会抛JSONDecodeError,建议用try/except包裹。
9.2 CSV处理:数据分析和脚本自动化绕不开的格式
CSV可以用csv模块处理,比手动按逗号拆分靠谱得多,因为CSV字段里可能包含逗号、引号、换行,只有专用模块能正确解析:
import csv from pathlib import Path # 写入CSV with Path('data.csv').open('w', encoding='utf-8', newline='') as f: writer = csv.writer(f) writer.writerow(['日期', '访问量', '来源']) writer.writerow(['2024-01-01', '1024', '搜索引擎']) # 读取CSV with Path('data.csv').open('r', encoding='utf-8') as f: reader = csv.DictReader(f) for row in reader: print(row['日期'], row['访问量'])注意写入CSV时newline=''这个参数,如果省略,在Windows上可能每行之间多一个空行。
9.3 临时文件:程序运行过程中的存储
如果你需要一个临时文件存放中间结果,跑完自动删除,可以用tempfile模块:
import tempfile from pathlib import Path with tempfile.NamedTemporaryFile(prefix='myapp_', suffix='.tmp', delete=True) as temp: temp_path = Path(temp.name) temp.write(b'temporary data') # 在临时文件上做操作 print(f"临时文件位置: {temp_path}") # 离开with后临时文件自动删除这在处理超大文件、需要临时存储中间结果的场景下特别实用。比如你从网络下载一个大文件,先存到临时文件,校验完成后再移动到正式目录,避免下载一半的文件污染正式目录。
9.4 二进制文件:读写图片、音频、压缩包
前面提到,处理非文本文件必须使用二进制模式。比如复制一张图片:
from pathlib import Path def copy_binary(src_path, dst_path): src = Path(src_path) dst = Path(dst_path) # 分块复制,避免大文件占满内存 with src.open('rb') as src_file, dst.open('wb') as dst_file: while chunk := src_file.read(8192): # 每次读8KB dst_file.write(chunk) copy_binary('photo.jpg', 'photo_copy.jpg')这个例子里用到了海象表达式:=和分块读取,二进制文件不管多大都不会撑爆内存。如果你只是简单复制文件,直接shutil.copy()更省事,但理解分块读写的原理对以后开发大文件处理工具很有帮助。
文件操作这块内容,学起来不难,但真正做到不出错、性能好,需要在实际项目里反复打磨。我见过很多人在项目里写文件操作,代码能跑,但一遇到异常就崩、一处理大文件就卡、一跨平台就乱码。能把路径、编码、模式、生命周期管理这几个点想清楚,文件操作这块基本就稳了。
我个人在写文件处理代码时的习惯是:路径统一用pathlib,打开文件统一用with open(),编码统一显式指定utf-8,大文件统一用迭代器逐行或分块处理。这几个习惯帮我规避掉了绝大部分文件操作相关的生产事故。如果你还没有自己的固定写法,建议从今天开始把这套流程内化成肌肉记忆。