☰
Python文件操作全指南:open、pathlib与shutil实战
2026/10/9 16:19:27 网站建设 项目流程

只要写过几行Python,几乎没人能绕开文件操作。读配置文件、导出报表、批量给图片改名、备份日志、把数据从一个目录挪到另一个目录,这些活看上去不起眼,却实实在在占了日常脚本的大半工作量。Python里的基本文件操作,说难不难,无非是打开、读取、写入、关闭,但恰恰是这些最基础的方法,最容易在路径分隔符、编码格式、权限占用这些不起眼的细节上翻车。这篇文章我打算从实战角度把文件操作的常用方法完整梳理一遍,包括open读写、pathlib路径管理、shutil复制删除的选型差异,读写模式、编码、上下文管理这些核心参数怎么定,再附上我自己踩过的坑和排查思路,给正准备用Python处理文件的新手一份可以直接上手的参考。

1. 动手之前先想清楚:Python里到底该用哪套文件操作API

1.1 内置open():最基础也最灵活的文件入口

很多人学习Python时接触的第一个文件操作函数就是内置的open()。它返回一个文件对象,负责把一个路径变成可读、可写或可追加的字节流。它的底层程度最低,但灵活性最高:小到一个文本文件逐行读取,大到二进制图片的分块拷贝,open()都能直接搞定。

用open()的核心是三个信息:文件路径、打开模式、编码方式。路径决定了去哪个位置找文件,模式决定了对文件做什么操作,编码则决定了文本内容如何被正确解析。这三个参数只要有一个写错,程序就会在运行时抛出异常。举例来说,Windows下常见的路径C:\Users\test\data.txt,在Python字符串里最好写成原始字符串r"C:\Users\test\data.txt",否则反斜杠会被解析成转义字符,这是新手经常遇到的第一道坎。

我自己的习惯是:当脚本逻辑简单、文件数量少、不需要复杂路径处理时,直接用open();一旦涉及目录遍历、多平台兼容、批量文件管理,就切换到别的方法。open()简单直接,但路径和目录相关的操作它不是强项,硬要用字符串拼接去处理路径会非常痛苦。

1.2 pathlib:面向对象的路径与文件操作

Python 3.4以后引入了pathlib,它把路径变成了一个真正的对象,而不是纯字符串。用Path类表示文件和目录,路径拼接直接用/运算符,跨平台自动处理分隔符,这让代码的清晰度和可移植性都有明显提升。

比如要拼接一个路径,传统写法是:

import os file_path = os.path.join("data", "2025", "report.txt")

用pathlib则更直观:

from pathlib import Path file_path = Path("data") / "2025" / "report.txt"

pathlib还内置了非常多的文件操作方法:Path.exists()判断是否存在,Path.read_text()直接读取文本,Path.write_text()直接写入文本,Path.iterdir()遍历目录,Path.glob()按通配符查找文件。它把"路径操作"和"文件操作"打包成一套完整的API,代码写起来更顺手。

我推荐的做法是:新建项目统一用pathlib管理路径,读取写入用Path.read_text()和Path.write_text(),再配合open()处理大文件或二进制流。两者的选择不是互斥关系,而是按场景分工配合。

1.3 os和shutil:文件和目录高级操作的百宝箱

os模块是Python操作系统接口的底层模块,很多文件操作都需要它兜底:os.rename()改名,os.remove()删除文件,os.mkdir()创建目录,os.listdir()列出目录内容。但这些方法有个共同问题:它们分布在模块的不同位置,功能比较发散,而且删除、复制这类操作还需要手动处理异常,用起来不够省心。

shutil模块弥补了os模块的不足,它专门处理文件的高层操作。shutil.copy2()复制文件并保留元数据,shutil.move()移动文件或目录,shutil.rmtree()递归删除整个目录,shutil.disk_usage()查看磁盘剩余空间。这些操作在业务脚本里使用频率极高,而且shutil内部已经帮我们处理了大部分跨平台细节,出问题的概率更低。

三个模块的定位可以用下面这个表概括:

模块/函数定位典型使用场景优点缺点
内置open()文件流读写读文本、写文本、处理二进制文件最灵活,可精确控制读写粒度路径和目录处理能力弱
pathlib.Path路径与常见文件操作路径拼接、遍历目录、按模式匹配文件代码清晰,跨平台,API统一高级操作不如shutil丰富
os模块底层系统接口环境变量、系统路径、底层文件属性功能全面,兼容老代码API分散,操作风险偏大
shutil模块文件和目录高层操作复制、移动、递归删除、归档压缩安全可靠,函数封装完整依赖前置目录存在,部分操作不可逆

具体到日常开发,我的选型思路是:路径问题找pathlib,文件读写用open(),复制移动删除优先shutil,只有涉及系统级路径或环境变量时才手动碰os。这套组合基本能覆盖日常90%以上的文件操作需求。

2. 打开文件时的几个关键参数,决定了你后面会不会踩坑

2.1 模式参数不只是'r'和'w'

open()的第二个参数是模式字符串,常见的有'r'只读、'w'写入、'a'追加、'x'创建新文件,还有'b'二进制模式、'+'读写模式等。很多人记住几个常用模式就开写了,但模式选错往往是最隐蔽的问题来源。

'w'和'a'的差别需要特别留意:'w'模式一旦打开文件,就会立即截断文件内容,哪怕你只是打开还什么都没写,原内容也已经没了。'a'模式则会把写入内容追加到文件末尾,不会破坏已有内容。所以当脚本目标是追加日志时,坚决用'a',千万不要用'w'。

我举个例子,假设要往access.log里追加一行记录:

# 危险:会清空原日志 with open("access.log", "w", encoding="utf-8") as f: f.write("new record\n") # 正确:追加到末尾 with open("access.log", "a", encoding="utf-8") as f: f.write("new record\n")

'+'模式要谨慎使用,'r+'表示可读可写,但写入位置从文件头开始,如果不配合seek()控制指针,很容易把已有内容覆盖掉。二进制模式'b'则是处理图片、音频、压缩包等非文本文件的必备参数,读取时返回字节串,写入时也需要传入字节串。

模式参数的选型逻辑并不复杂:读文件用'r',写新文件或覆盖用'w',只追加不覆盖用'a',需要读又要写再考虑'+',非文本数据加'b'。把这条规则记清楚,能避免大量无谓的数据丢失。

2.2 编码问题:为什么中文经常乱码或报错

编码是Python文件操作里最折腾人的问题,尤其在处理中文内容时。现在主流环境默认编码是UTF-8,但Windows的部分工具和旧文件使用GBK,如果打开文件时不显式指定编码,程序会按照系统默认编码解析,结果就是要么直接抛UnicodeDecodeError,要么读出来的中文变成乱码。

解决思路只有一个:在打开文本文件时,总是显式声明encoding参数。

with open("data.txt", "r", encoding="utf-8") as f: content = f.read()

如果文件是GBK编码,就把encoding改成"gbk"。另一个常见坑是:写文件时如果不指定encoding="utf-8",在Windows默认编辑器里打开可能正常,但换到Linux或Mac上就乱码了。我的习惯是,项目里所有文本文件的读取和写入,一律统一编码为utf-8,这样团队协作和跨平台部署时都不会出问题。

如果实在不确定文件是什么编码,可以用errors="replace"暂时跳过非法字符,但这只能应急排查,不能作为生产代码的长期方案。

2.3 with上下文管理器比手动close更可靠

被open()打开的文件对象,使用完毕后一定要关闭。很多人初学时习惯写f = open(...),然后操作完手动调用f.close()。这种做法在脚本不报错时没问题,可一旦中间抛出异常,后面的close()根本不会执行,文件句柄就一直被占着,Windows下就会出现"文件正在被另一个程序使用"的提示。

更稳的方式是使用with语句,它也叫上下文管理器:

with open("example.txt", "r", encoding="utf-8") as f: data = f.read()

当with代码块执行完毕,无论正常结束还是中途抛出异常,Python都会自动关闭文件对象。这相当于给文件操作套了一层try...finally保护,是我写任何文件读写代码的第一选择。手动管理open()和close()只适合极少数需要把文件对象传递到其他函数处理的高级场景,普通业务脚本完全不需要。

2.4 三个高频细节:路径拼接、换行符、文件指针

打开文件时还有一个容易忽视的细节是换行符。Windows用\r\n,Linux和macOS用\n,Python的文本模式在读取时会把\r\n自动转换成\n,写入时又会把\n转回当前平台默认的换行符。这个自动转换多数时候很贴心,但在处理需要严格保持原样的数据文件时,反而会造成困扰,这时可以传入newline=""来关闭自动转换。

文件指针也是个重要概念。每次read()或readline()之后,文件指针都会往后移动,如果想重新读刚才的内容,就需要用seek(0)把指针挪回文件开头。很多人操作完文件后忘了指针位置,导致后续读取返回空内容,还以为程序有bug。

最后一个细节是路径拼接。别直接拿字符串做"data" + "\\" + "report.txt"这种操作,Windows和Linux的分隔符不一样,跨平台必炸。用pathlib或os.path.join()来拼,才是正经做法。

3. 基本文件操作完整实操:读写、复制、移动、删除一网打尽

3.1 读取文本文件的三种常用姿势

读取文本文件时,我经常用三种方式:read()整体读取、readline()按行读取、for循环逐行迭代。它们各有适用场景,选择标准主要看文件大小和后续处理方式。

小文件直接读全文最快:

with open("config.txt", "r", encoding="utf-8") as f: content = f.read()

逐行处理是日常脚本里最高频的操作,直接迭代文件对象即可,不需要额外加readlines()把整个文件一次性装进内存:

with open("server.log", "r", encoding="utf-8") as f: for line in f: if "ERROR" in line: print(line.strip())

readlines()则适合一次性取到所有行,方便后续随机访问或排序,但如果文件有几个GB,这会让内存直接爆掉。我处理超大文件时从不用readlines(),而是用for循环逐行处理,再加一个计数器统计行数,内存占用就只有小小一块。

readline()在需要手动控制读取节奏时用,比如每次读一行、处理完再继续,不过这种场景其实都可以用for循环替代。这三者的取舍,本质就是空间和代码简洁度之间的平衡。

3.2 写入与追加:什么时候用'w',什么时候用'a'

写入文件的第一步是决定要不要保留原内容。前面已经强调过'w'会截断文件,所以业务上"生成报表覆盖旧数据"这种场景用'w'没问题,"每天往日志文件追加运行记录"这种场景就必须用'a'。

一个完整的报表写入示例:

from pathlib import Path report = Path("report.txt") lines = ["项目启动", "任务完成", "文件写入结束"] with open(report, "w", encoding="utf-8") as f: for line in lines: f.write(line + "\n")

如果你担心程序中途崩溃导致写了一半的文件不可用,可以先写入临时文件,全部写完后用os.replace()把临时文件替换成正式文件。这个方法在PowerShell里很常见,Python里一样可以做成原子操作,能有效避免"写到一半断电,整个文件废掉"的惨案。

追加日志的场景则简洁得多:

with open("runtime.log", "a", encoding="utf-8") as f: f.write("2025-01-15 10:30:00 启动备份任务\n")

3.3 JSON、CSV等结构化文件的快速读写

纯文本读写之外,日常项目里最常打交道的其实是JSON和CSV。JSON适合配置文件、API接口返回结果;CSV则适合表格数据、数据分析导入导出。

JSON读取和写入用json模块,写中文时一定要设置ensure_ascii=False,否则中文字符会被转成一堆\uXXXX:

import json data = {"name": "测试任务", "status": "done"} # 读取 with open("config.json", "r", encoding="utf-8") as f: loaded = json.load(f) # 写入 with open("result.json", "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2)

CSV操作则建议用标准库的csv模块,它能自动处理引号转义、逗号分隔等细节。自己用split(",")解析CSV非常容易在字段包含逗号时出错,比如"张三,李四"这个字段本身里就有逗号,手动切割必炸。

import csv with open("data.csv", "w", encoding="utf-8-sig", newline="") as f: writer = csv.writer(f) writer.writerow(["姓名", "分数"]) writer.writerow(["张三", 88])

需要注意,Excel打开CSV时默认按GBK或ANSI解析,所以写CSV时建议用encoding="utf-8-sig",加一个BOM头,Excel才不会乱码。这个细节我踩过多次,写出来给各位参考。

3.4 文件的复制、移动、改名与删除:shutil安全操作

文件复制移动在Python里非常推荐直接使用shutil,它提供了一整套针对文件和目录的高层操作,比手动打开文件、读字节、写字节再关闭的过程安全得多,也省事得多。

复制文件用shutil.copy2,它会保留文件的修改时间、权限等元信息,这在同步或备份场景里很重要:

import shutil shutil.copy2("C:/data/raw.csv", "C:/backup/raw.csv")

移动文件或目录用shutil.move,等价于先复制后删除原文件,但在Windows环境下跨盘移动时,它内部会妥善处理路径问题:

shutil.move("C:/data/raw.csv", "D:/archive/raw.csv")

删除单个文件用os.remove或Path.unlink,删除整个目录用shutil.rmtree。这俩操作都不可恢复,我在删除前一定会先打印或者记录日志。这里给大家一个我自己一直在用的安全习惯:任何批量删除或移动之前,先跑一遍"只打印操作但不执行"的演练模式,确认无误后再真正执行。

from pathlib import Path target_dir = Path("old_files") for file in target_dir.glob("*.tmp"): # 先打印再说,别急着删 print(f"准备删除: {file}") # 确认后再执行 file.unlink()

3.5 目录遍历与批量文件处理

批量处理文件时,最常遇到的需求是遍历某个目录下所有文件,按照某种规则做操作。pathlib的glob和rglob方法比传统os.walk写起来简洁很多。

遍历单层目录下所有.log文件:

from pathlib import Path log_dir = Path("logs") for log_file in log_dir.glob("*.log"): print(log_file.name)

递归遍历子目录下所有文件,然后统一改名,是另一个高频场景。批量重命名的坑在于,一边遍历一边改名,容易把已经改过的文件又匹配进下一轮循环。稳妥做法是先收集文件名,再统一改名:

from pathlib import Path root = Path("documents") files_to_rename = [p for p in root.rglob("*.txt")] for path in files_to_rename: new_name = path.with_name(path.stem + "_backup" + path.suffix) path.rename(new_name) print(f"重命名: {path} -> {new_name}")

4. 进阶一点:用logging给文件操作留一份操作日志

4.1 为什么要在代码里记录文件操作日志

批量脚本跑起来很快,几万个文件几秒钟就处理完了,但一旦操作出错,你不知道是哪个文件出了问题,更不知道它在处理前长什么样。这种情况下,给每次文件操作记录一条日志,就成了救命稻草。

Windows系统本身也有文件操作审计功能,比如在事件查看器里查看文件访问记录,但那需要提前开启审核策略,而且查看的是系统层面的操作,不是我们自己脚本的行为。我更推荐在Python脚本里直接用logging模块写一份自己的操作日志,把操作时间、操作类型、源文件、目标文件、执行结果都记录下来,回头排查问题时一目了然。

为什么不让print输出去充当日志?因为print的输出会混在程序其他输出里,系统一重启就找不到了。恰当的日志会持久化到文件,还支持级别过滤,这才是长期运行脚本该有的配置。

4.2 基于logging的日志文件配置与示例

用logging记录文件操作日志,核心配置是输出到文件、设置时间格式、指定日志级别。下面是一段可直接套用的模板:

import logging logging.basicConfig( filename="file_ops.log", level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s", datefmt="%Y-%m-%d %H:%M:%S" ) logger = logging.getLogger("file_ops")

在具体执行文件操作时,先记录"准备执行",再执行,最后记录"完成"或"失败":

from pathlib import Path import shutil src = Path("raw.xlsx") dst = Path("backup/raw.xlsx") logger.info(f"准备复制: {src} -> {dst}") try: shutil.copy2(src, dst) logger.info(f"复制成功: {src} -> {dst}") except Exception as e: logger.error(f"复制失败: {src} -> {dst}, 错误: {e}", exc_info=True)

这份日志写清楚之后,哪怕脚本运行完你人不在现场,回来翻file_ops.log也能知道每一步都发生了什么。批量删除、移动这些高危操作尤其推荐先记录、再执行这个次序。

4.3 日志操作与目录遍历结合:让批量操作可回溯

批量处理时,把日志和目录遍历结合起来,能最大程度提升脚本的可维护性。假设要把docs下所有超过100KB的文件备份到backup目录,每处理一个文件就写一条日志:

from pathlib import Path import shutil import logging logging.basicConfig( filename="backup_ops.log", level=logging.INFO, format="%(asctime)s | %(message)s", datefmt="%Y-%m-%d %H:%M:%S" ) docs = Path("docs") backup = Path("backup") backup.mkdir(parents=True, exist_ok=True) for file in docs.rglob("*"): if file.is_file() and file.stat().st_size > 100 * 1024: target = backup / file.relative_to(docs) target.parent.mkdir(parents=True, exist_ok=True) logging.info(f"开始备份: {file} -> {target}") shutil.copy2(file, target) logging.info(f"完成备份: {file}, 大小: {file.stat().st_size}")

这套思路在真实项目里非常实用。数据文件被误删时,日志能告诉你文件最后被复制去了哪儿;脚本跑报错时,日志能帮你在几百个文件里精准定位是哪一个出了幺蛾子。养成写日志的习惯,比用任何花哨的调试工具都更可靠。

5. 高频报错排查与我的防翻车习惯

5.1 提示"文件不存在",路径到底错在哪

FileNotFoundError是文件操作中出现频率最高的报错。很多人第一反应是"这个文件明明存在啊",代码却仍然找不到。这里头通常有两个原因:一是当前工作目录不是你以为的目录,二是在Windows和Linux环境下用了错误的分隔符。

Python脚本默认的相对路径是相对于当前执行目录解析的,比如在终端里运行python script.py,相对路径data.txt就会到终端当前所在目录去寻找,而不是到你脚本文件所在的目录去找。这个问题可以用打印当前工作目录的方式排查:

import os from pathlib import Path print("当前工作目录:", os.getcwd()) print("脚本所在目录:", Path(__file__).resolve().parent)

如果你要读取的文件就在脚本文件旁边,就不要依赖相对路径,直接用Path(__file__).resolve().parent / "data.txt"构造绝对路径。这个习惯能避开绝大多数"文件明明存在却找不到"的诡异状况。

5.2 提示"文件正在被使用"或权限不足

PermissionError是Windows下的老朋友了。最常见的原因是文件被Excel、记事本或其他程序打开着,Python尝试删除或覆写时就会报权限错误。排查思路是先关掉占用文件的程序,再看看是不是杀毒软件或其他后台进程暂时锁住了文件。

如果脚本自己刚写完文件又立刻去删除,那也可能是文件句柄没有关闭。用with打开文件写完内容后,文件会自动关闭,但如果用了手动f = open()却没调用close(),句柄就会一直占着。这种问题排查起来最折腾,因为报错看起来像权限问题,实际是代码里漏了关闭文件。

针对需要覆盖已有文件的场景,可以用os.replace()替代os.rename(),它在Windows下对目标文件的占用兼容性更好。删除文件前也建议先用Path.exists()判断,再进入删除流程,避免竞态条件。

5.3 提示"无法完成操作,因为文件中包含..."这类安全拦截

有时候你运行一个写好的脚本,明明代码没错,Windows却弹出一个"无法完成操作,因为文件中包含..."的提示,导致文件无法读取、复制或移动。这种提示一般是操作系统的安全策略或杀毒软件在介入,它对文件内容做了检查,认为该文件存在风险,于是直接阻止了后续文件操作。

遇到这种情况,第一反应不应该是强行绕过安全拦截,而是先确认这个文件从哪里来、内容是什么。把文件放到隔离的目录里,用文本编辑器或格式工具打开看一眼,确认它是不是我们期望的数据格式。如果是脚本要处理的文件,排查口径是:检查文件来源是否可靠,检查文件是否损坏,检查路径是否有特殊字符或权限设置。处理完之后,再和本地安全策略的设置做核对,确实误报再考虑放行。

这个问题的核心不在于Python代码,而在于文件本身和系统策略的组合。纠结代码语法没有意义,先把"文件是否合法"这个前置条件确认清楚,比任何代码层面的hack都靠谱。

5.4 我的防翻车习惯:先备份、先演练、再执行

关于文件操作,我最后想分享几个自己长期坚持的习惯。第一个习惯是批量操作之前先备份,哪怕是一个简单的shutil.move,我也不会在正式数据上直接试。第二个习惯是删除、覆盖这类不可逆操作,一定要先做"预演",把将要执行的操作打印出来或者写入日志,人眼确认一遍再真正执行。第三个习惯是统一编码、统一路径风格,项目里所有文本文件都用utf-8,所有路径都用pathlib,从源头上减少编码错乱和分隔符兼容问题。

还有一个容易被忽略的点:文件操作脚本要尽量保持可重入性。什么叫可重入?就是同一个脚本跑第二遍时,不会因为第一遍已经处理过而产生报错或重复处理。方法是在操作前都做检查,比如复制前先判断目标文件是否已存在,删除前先判断文件是否还在。这个习惯能让你在调试脚本时少很多麻烦,因为就算跑失败了,修复完再跑一次也不会有副作用。

文件操作看着基础,可它是一切数据处理的地基。把open()、pathlib、shutil这几套方法用熟,再配合日志和预演习惯,日常80%的文件处理需求都能处理得稳稳当当。我个人在实际项目里验证下来,这套组合最省心的就是可维护性:脚本三个月后再回来看,日志清清楚楚,路径逻辑一眼就懂。下次你写文件处理脚本时,不妨也先把这三层工具在脑子里过一遍,再动键盘。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询