2人参与 • 2026-08-07 • Python
说白了,程序的本质就是 读数据 → 处理数据 → 写数据。而"数据"这东西,大部分时候不是内存里临时蹦出来的变量,而是躺在硬盘上的文件。
python 对文件操作的设计哲学很 pythonic:
| 语言 | 打开文件 | 关闭文件 | 读一行 |
|---|---|---|---|
| c | fopen() + 手动 fclose() | 手动 | fgets() |
| java | new fileinputstream() + try-finally | 手动 | readline() |
| python | open() | with 自动关 | f.readline() |
python 干了两件漂亮事:
with 语句——让资源释放变成语言级保证,不再依赖程序员记性好不好。这两件事,一个管安全,一个管优雅。本文就围绕这两个核心展开。
当你在 python 里调用 open("test.txt", "r") 时,底层发生了什么?
python 代码 cpython 解释器 操作系统
──────────────────────────────────────────────────────
open("test.txt") → io.open() → _io.fileio → fd = syscall(open) → ① 内核分配文件描述符
f.read() → read(fd, n) → ② 内核把数据从磁盘读入内核缓冲区
→ ③ 再从内核缓冲区拷贝到用户空间
f.close() → close(fd) → ④ 释放文件描述符,flush 缓冲区
文件描述符(file descriptor,简称 fd) 是操作系统给你打开的每个文件分配的一个整数编号:
0 = stdin(标准输入)1 = stdout(标准输出)2 = stderr(标准错误)3+ = 你打开的文件你可以直接看到它:
f = open("test.txt", "w")
print(f.fileno()) # 输出: 3(或更大的数字)
f.close()
python 的文件 i/o 默认走缓冲区,不是直接写磁盘:
你的 write() 调用
↓
python 缓冲区(用户空间,内存) ←── 默认 8kb
↓ 缓冲区满了 or flush() or close()
内核缓冲区(内核空间)
↓ 操作系统决定(或 fsync() 强制)
磁盘
这就是为什么 不 close 文件,数据可能丢——因为数据还躺在缓冲区里没落盘。
f = open("test.txt", "w")
f.write("hello")
# 如果这里程序崩溃了,"hello" 可能还在缓冲区,没写入磁盘!
# 但如果你用了 with:
with open("test.txt", "w") as f:
f.write("hello")
# __exit__ 自动调 flush + close,数据安全落盘
cpython 的文件 i/o 其实是分层的:
┌─────────────────────────────────────────┐
│ io.textiowrapper ← 文本模式 r/w │ ← 编码解码在这一层
├─────────────────────────────────────────┤
│ io.bufferedreader ← 缓冲层 │ ← read1(), peek()
├─────────────────────────────────────────┤
│ io.fileio ← 底层系统调用 │ ← 纯粹的 read/write
└─────────────────────────────────────────┘
↕
操作系统 fd
当你 open("test.txt", "r")(文本模式),拿到的是 textiowrapper;当你 open("test.txt", "rb")(二进制模式),拿到的是 bufferedreader。
# 文本模式
f = open("test.txt", "r")
print(type(f)) # <class '_io.textiowrapper'>
# 二进制模式
f = open("test.txt", "rb")
print(type(f)) # <class '_io.bufferedreader'>
这个区别很重要,后面踩坑点会讲。
open(file, mode='r', buffering=-1, encoding=none, errors=none,
newline=none, closefd=true, opener=none)
| 参数 | 说明 | 常用值 |
|---|---|---|
file | 文件路径(str / pathlike) | "test.txt", path("test.txt") |
mode | 打开模式 | "r", "w", "rb" 等 |
buffering | 缓冲策略 | -1(默认), 0(无缓冲,仅二进制), 1(行缓冲) |
encoding | 文本编码 | "utf-8", "gbk", "latin-1" |
errors | 编码错误处理 | "strict"(默认报错), "ignore", "replace" |
newline | 换行符处理 | none, "", "\n", "\r\n" |
| 字符 | 含义 | 英文助记 |
|---|---|---|
r | 只读(默认) | read |
w | 只写(覆盖已存在文件) | write |
a | 追加写(文件末尾追加) | append |
x | 独占创建(文件已存在则报错) | exclusive |
b | 二进制模式 | binary |
t | 文本模式(默认) | text |
+ | 读写模式(可读可写) | plus |
| 模式 | 读? | 写? | 文件不存在 | 文件已存在 | 指针位置 |
|---|---|---|---|---|---|
r | ✅ | ❌ | 报错 filenotfounderror | — | 开头 |
r+ | ✅ | ✅ | 报错 | — | 开头 |
w | ❌ | ✅ | 创建 | 清空 | 开头 |
w+ | ✅ | ✅ | 创建 | 清空 | 开头 |
a | ❌ | ✅ | 创建 | — | 末尾 |
a+ | ✅ | ✅ | 创建 | — | 末尾 |
x | ❌ | ✅ | 创建 | 报错 fileexistserror | 开头 |
x+ | ✅ | ✅ | 创建 | 报错 | 开头 |
加 b 后缀即为二进制模式,行为一致,只是数据单元从 str 变成 bytes。
# 基本形式
with open("test.txt", "r") as f:
content = f.read()
# 同时打开多个文件
with open("input.txt") as fin, open("output.txt", "w") as fout:
for line in fin:
fout.write(line)
# python 3.10+ 更优雅的写法(括号包裹)
with (
open("input.txt") as fin,
open("output.txt", "w") as fout,
):
for line in fin:
fout.write(line)
需要操作文件?
│
┌──────────┼──────────┐
▼ ▼ ▼
只读? 只写? 读写?
│ │ │
▼ ▼ ▼
r w r+(不清空)
│ ┌────┴────┐ │
│ ▼ ▼ ▼
│ 覆盖写? 追加写? 需要创建新文件且防覆盖?
│ │ │ │
│ ▼ ▼ ▼
│ w/a a x
│ │ │
└─────┴─────────┘
│
数据是二进制?
│
▼
加 b 后缀(rb / wb / ab)
暗黑时代 过渡期 现代
os.path pathlib.path
os.path.join(a, b) → path(a) / b
os.path.exists(p) → path(p).exists()
os.path.basename → path(p).name
os.path.dirname → path(p).parent
os.path.splitext → path(p).suffix
os.listdir(d) → path(d).iterdir()
glob.glob("*.py") → path().glob("*.py")
❌ 返回 str ✅ 返回 path 对象,可链式调用
❌ 字符串拼接地狱 ✅ / 运算符,跨平台
# ❌ 危险写法
f = open("test.txt", "w")
f.write("重要数据")
# 忘了 close()...
# 程序正常退出时 python 会帮你关,但如果异常退出?
# 数据可能还在缓冲区,没落盘!
# ✅ 正确写法
with open("test.txt", "w") as f:
f.write("重要数据")
# 出了 with 块,__exit__ 保证执行 flush + close
# ❌ 用文本模式读 png 图片
with open("photo.png", "r") as f: # 默认 utf-8 解码
data = f.read()
# unicodedecodeerror: 'utf-8' codec can't decode byte 0x89...
# ✅ 二进制模式
with open("photo.png", "rb") as f:
data = f.read()
# 干干净净的 bytes
规则:非文本文件一律加 b。图片、视频、压缩包、pickle……
# ❌ windows 默认编码是 gbk(cp936),不是 utf-8
with open("data.txt", "w") as f: # windows 上用 gbk 写
f.write("中文")
# 别人在 linux/mac 上打开 → 乱码,因为那边默认 utf-8
# ✅ 永远显式指定 encoding
with open("data.txt", "w", encoding="utf-8") as f:
f.write("中文")
规则:涉及中文的文件操作,永远写 encoding="utf-8",别依赖系统默认。
# ❌ 想追加结果,结果把整个文件清空了
with open("log.txt", "w") as f: # w 模式会先截断文件!
f.write("新日志")
# ✅ 追加用 a 模式
with open("log.txt", "a") as f: # 在末尾追加
f.write("新日志\n")
# r+ 是读写模式,但不会清空文件,指针在开头
with open("test.txt", "r+") as f:
content = f.read() # 读完了,指针在末尾
f.write("追加内容") # 写到末尾 ✅
# 但如果这样:
with open("test.txt", "r+") as f:
f.write("替换") # 指针在开头,从头覆盖写入!
# 原文件内容: "hello world"
# 写完后: "替换lo world" ← 只覆盖了前3个字节/字符的位置
坑点:r+ 模式写入是覆盖式写入,不是插入。想插入得自己手动移动文件指针。
# ❌ 手动拼接路径
path = "data" + "/" + "file.txt" # windows 上分隔符是 \,虽然 python 能容忍 /,但不优雅
# ❌ 更糟的写法
path = "data" + "\\" + "file.txt" # 硬编码反斜杠,在 linux 上就是字面量
# ✅ pathlib 方式
from pathlib import path
path = path("data") / "file.txt" # / 运算符自动处理分隔符
# ❌ 10gb 的日志文件
with open("huge.log") as f:
content = f.read() # 全部读进内存 → 崩
# ✅ 逐行读取
with open("huge.log") as f:
for line in f: # 文件对象本身是迭代器,逐行惰性读取
process(line)
# ✅ 或者分块读取
with open("huge.log", "rb") as f:
while chunk := f.read(8192): # 海象运算符,python 3.8+
process(chunk)
在 cpython 源码中,open() 的调用链如下:
# builtins.py (概念示意,非真实源码)
def open(file, mode='r', ...):
# 1. 解析模式字符串,构建 mode flags
flags = _os.o_rdonly if 'r' in mode else 0
if 'w' in mode: flags |= _os.o_wronly | _os.o_creat | _os.o_trunc
if 'a' in mode: flags |= _os.o_wronly | _os.o_creat | _os.o_append
if '+' in mode: flags = (flags | _os.o_rdwr) & ~_os.o_accmode
# 2. 底层系统调用打开文件,获取 fd
fd = _os.open(file, flags, 0o666)
# 3. 根据 b/t 标志选择包装层级
raw = fileio(fd, mode, closefd=true) # 最底层
buffered = bufferedreader(raw) # 缓冲层
if 'b' not in mode:
# 文本模式:再包一层 textiowrapper
return textiowrapper(buffered, encoding, errors, newline)
return buffered
关键点:
fileio 对应 os 层面的 read/write 系统调用bufferedreader 加了缓冲,减少系统调用次数textiowrapper 加了编解码,bytes ↔ str 转换在这里完成with 语句的本质是调用对象的 __enter__ 和 __exit__ 两个魔法方法:
# 上下文管理器协议的等效手动实现
class myfile:
def __init__(self, filename, mode):
self.f = open(filename, mode)
def __enter__(self):
return self.f # with ... as f 中的 f 就是这里返回的
def __exit__(self, exc_type, exc_val, exc_tb):
self.f.close() # 无论有没有异常,都会调用
return false # false = 不吞异常,继续向上传播
# with 语句的等效展开:
# with myfile("test.txt") as f:
# ...
#
# 等价于:
# mgr = myfile("test.txt")
# f = mgr.__enter__()
# try:
# ...
# except:
# if not mgr.__exit__(exc_type, exc_val, exc_tb):
# raise
# else:
# mgr.__exit__(none, none, none)
__exit__ 的三个参数:
| 参数 | 含义 |
|---|---|
exc_type | 异常类型(valueerror 等),无异常时为 none |
exc_val | 异常实例 |
exc_tb | traceback 对象 |
这就是 with 能保证资源释放的根本原因——__exit__ 在 finally 语义中被调用,不可跳过。
from contextlib import contextmanager
@contextmanager
def open_file(filename, mode):
f = open(filename, mode)
try:
yield f # yield 之前的 = __enter__,yield 的值 = with...as 的变量
finally:
f.close() # yield 之后的 = __exit__
# 使用
with open_file("test.txt", "r") as f:
print(f.read())
@contextmanager 装饰器把一个生成器函数变成了上下文管理器,比写类简洁多了。
f = open("test.txt", "r")
# ─── 读取 ───
f.read() # 读全部(可传 size 参数)
f.readline() # 读一行
f.readlines() # 读所有行,返回列表
list(f) # 等价于 f.readlines(),但更 pythonic
# ─── 写入 ───
f.write("text") # 写字符串(返回写入字符数)
f.writelines(lines) # 批量写(注意:不会自动加换行)
# ─── 指针操作 ───
f.tell() # 当前指针位置
f.seek(0) # 移动到开头
f.seek(0, 2) # 移动到末尾(whence: 0=开头, 1=当前, 2=末尾)
# ─── 其他 ───
f.flush() # 强制刷缓冲区到磁盘
f.fileno() # 获取文件描述符
f.close() # 关闭
f.closed # 是否已关闭
f.readable() # 是否可读
f.writable() # 是否可写
f.seekable() # 是否可 seek
import os
from pathlib import path
p_str = "data/subdir/file.txt"
# ========== 路径拼接 ==========
# os.path
p1 = os.path.join("data", "subdir", "file.txt")
# pathlib
p2 = path("data") / "subdir" / "file.txt"
# ========== 获取文件名 ==========
# os.path
name = os.path.basename(p_str) # "file.txt"
dir_ = os.path.dirname(p_str) # "data/subdir"
# pathlib
p = path(p_str)
name = p.name # "file.txt"
dir_ = str(p.parent) # "data/subdir"
# ========== 获取扩展名 ==========
# os.path
root, ext = os.path.splitext(p_str) # ("data/subdir/file", ".txt")
# pathlib
p.stem # "file" ← 文件名(不含扩展名)
p.suffix # ".txt" ← 扩展名
p.suffixes # [".txt"] ← 所有扩展名(如 "file.tar.gz" → [".tar", ".gz"])
# ========== 判断文件是否存在 ==========
# os.path
exists = os.path.exists(p_str)
is_file = os.path.isfile(p_str)
is_dir = os.path.isdir(p_str)
# pathlib
p = path(p_str)
exists = p.exists()
is_file = p.is_file()
is_dir = p.is_dir()
# ========== 创建目录 ==========
# os
os.makedirs("data/subdir", exist_ok=true)
# pathlib
path("data/subdir").mkdir(parents=true, exist_ok=true)
# ========== 列出目录内容 ==========
# os
files = os.listdir("data") # 返回 list[str]
# pathlib
files = [p for p in path("data").iterdir()] # 返回迭代器,产出 path 对象
# ========== 递归查找 ==========
# os + glob
import glob
files = glob.glob("data/**/*.txt", recursive=true) # 返回 list[str]
# pathlib
files = list(path("data").rglob("*.txt")) # 返回 list[path]
for p in path("data").rglob("*.txt"):
print(p)
# pathlib 最爽的地方:直接读写文件!
# 写
path("output.txt").write_text("hello world", encoding="utf-8")
path("output.bin").write_bytes(b"\x00\x01\x02")
# 读
text = path("output.txt").read_text(encoding="utf-8")
data = path("output.bin").read_bytes()
# 对比 os 方式:你得自己 open + read + close
# 完全没法比,pathlib 完胜
| 对比维度 | os.path | pathlib |
|---|---|---|
| 返回类型 | str | path 对象(可链式调用) |
| 路径拼接 | os.path.join(a, b) | path(a) / b |
| 可读性 | 函数调用嵌套 | 面向对象链式 |
| 文件读写 | 需要配合 open() | 内置 read_text() / write_text() |
| 递归搜索 | glob.glob() | path.rglob() |
| 统计信息 | os.stat() | path.stat() |
| 适用版本 | 全版本 | python 3.4+(3.6+ 更完善) |
结论:新项目无脑用 pathlib,只在维护老代码或需要兼容低版本时用 os.path。
python 标准库 csv 模块,专门处理逗号分隔值文件。
import csv
# ─── 写 csv ───
rows = [
["姓名", "年龄", "城市"],
["张三", 25, "北京"],
["李四", 30, "上海"],
]
with open("data.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerows(rows)
# ─── 用 dictwriter(带表头)───
fieldnames = ["name", "age", "city"]
data = [
{"name": "张三", "age": 25, "city": "北京"},
{"name": "李四", "age": 30, "city": "上海"},
]
with open("data.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.dictwriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(data)
# ─── 读 csv ───
with open("data.csv", "r", encoding="utf-8") as f:
reader = csv.reader(f)
for row in reader:
print(row) # ['姓名', '年龄', '城市']
# ─── 用 dictreader ───
with open("data.csv", "r", encoding="utf-8") as f:
reader = csv.dictreader(f)
for row in reader:
print(row["name"], row["age"]) # 张三 25
踩坑点:windows 上写 csv 必须 newline="",否则每行之间会多出一个空行。
原因:python 文本模式默认开启 universal newline 转换,csv.writer 自己又写 \r\n,叠加成 \r\r\n。
import json
data = {
"name": "yance",
"skills": ["python", "x265", "ffmpeg"],
"blog": {"platform": "csdn", "count": 9},
}
# ─── 写 json ───
with open("data.json", "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=false, indent=2)
# ensure_ascii=false: 中文不转义,直接输出
# indent=2: 美化缩进
# ─── 读 json ───
with open("data.json", "r", encoding="utf-8") as f:
data = json.load(f)
# ─── 字符串互转(不走文件)───
json_str = json.dumps(data, ensure_ascii=false, indent=2) # dict → str
data = json.loads(json_str) # str → dict
踩坑点:json.dump 默认 ensure_ascii=true,中文会变成 \u5f20\u4e09 这种鬼东西。
写入含中文的 json,务必加 ensure_ascii=false。
import configparser
# ─── 写 ini ───
config = configparser.configparser()
config["default"] = {
"debug": "false",
"timeout": "30",
}
config["database"] = {
"host": "localhost",
"port": "5432",
"name": "mydb",
}
config["redis"] = {
"host": "127.0.0.1",
"port": "6379",
}
with open("config.ini", "w", encoding="utf-8") as f:
config.write(f)
# config.ini 内容:
# [default]
# debug = false
# timeout = 30
#
# [database]
# host = localhost
# port = 5432
# name = mydb
#
# [redis]
# host = 127.0.0.1
# port = 6379
# ─── 读 ini ───
config = configparser.configparser()
config.read("config.ini", encoding="utf-8")
# 读取值(都是字符串)
host = config["database"]["host"] # "localhost"
port = config.getint("database", "port") # 5432(int 类型)
debug = config.getboolean("default", "debug") # false(bool 类型)
# default 段的值所有 section 都能访问
timeout = config["redis"]["timeout"] # "30" ← 从 default 继承
yaml 比 json 更人类友好,支持注释。但 yaml 不是标准库,需要安装:
pip install pyyaml
import yaml
# ─── 写 yaml ───
config = {
"app": {
"name": "myapp",
"version": "1.0.0",
},
"database": {
"host": "localhost",
"port": 5432,
"pool_size": 10,
},
"features": ["auth", "logging", "cache"],
}
with open("config.yaml", "w", encoding="utf-8") as f:
# allow_unicode=true: 中文不转义
# default_flow_style=false: 用块格式而非流格式
yaml.dump(config, f, allow_unicode=true, default_flow_style=false)
# config.yaml 内容:
# app:
# name: myapp
# version: 1.0.0
# database:
# host: localhost
# port: 5432
# pool_size: 10
# features:
# - auth
# - logging
# - cache
# ─── 读 yaml ───
with open("config.yaml", "r", encoding="utf-8") as f:
config = yaml.safe_load(f) # safe_load! 不要用 yaml.load()
print(config["app"]["name"]) # myapp
print(config["database"]["port"]) # 5432(自动识别为 int)
⚠️ 安全警告:永远用 yaml.safe_load() 而不是 yaml.load()!yaml.load() 不带 loader 参数时,可以执行任意 python 代码,属于严重安全漏洞。yaml.safe_load() 只解析基本数据类型,安全。
| 格式 | 标准库? | 支持注释 | 数据类型 | 典型用途 |
|---|---|---|---|---|
| csv | ✅ csv | ❌ | 表格(二维) | 数据导出、excel 交互 |
| json | ✅ json | ❌ | dict/list/str/int/float/bool/none | api 传输、配置文件 |
| ini | ✅ configparser | ✅ # / ; | 纯字符串(需手动转类型) | 简单键值对配置 |
| yaml | ❌ 需 pyyaml | ✅ # | 全类型 + 引用 + 多行字符串 | 复杂配置(docker/k8s) |
tempfile 模块用于安全地创建临时文件和目录。
import tempfile
import os
# ─── 1. 临时文件(自动删除)───
# 用完即删,适合中间数据处理
with tempfile.namedtemporaryfile(mode="w+", suffix=".txt",
delete=true, encoding="utf-8") as f:
f.write("临时数据")
f.seek(0)
print(f.read())
print(f.name) # 类似: c:\users\...\appdata\local\temp\tmpxxxxxx.txt
# 出了 with 块,文件自动删除
# ─── 2. 临时文件(保留,手动管理)───
# delete_on_close=false (python 3.12+) 或 delete=false
with tempfile.namedtemporaryfile(mode="w", delete=false,
encoding="utf-8") as f:
f.write("需要保留的临时数据")
temp_path = f.name
# 文件保留,你可以之后再用
print(f"临时文件路径: {temp_path}")
# 用完手动删
os.unlink(temp_path)
# ─── 3. 临时目录 ───
with tempfile.temporarydirectory() as tmpdir:
print(f"临时目录: {tmpdir}")
# 在里面随便造文件
filepath = os.path.join(tmpdir, "test.txt")
with open(filepath, "w") as f:
f.write("hello")
# 出了 with 块,整个目录及内容自动删除
# ─── 4. 快捷方式:只拿一个临时文件名 ───
# 返回 (fd, path),底层用 mkstemp
fd, path = tempfile.mkstemp(suffix=".csv")
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
f.write("a,b,c\n1,2,3\n")
finally:
os.close(fd)
os.unlink(path)
命名规则:临时文件名用 6 位随机字符(tmp + 6位字母数字),足够避免冲突。
安全提示:mkstemp 比 mktemp 安全,因为创建和返回是原子操作,不会被竞争攻击。
把前面学的全部串起来,写一个实用的批量重命名工具。
#!/usr/bin/env python3
"""
批量文件重命名工具
支持:前缀添加、序号编号、正则替换、扩展名批量修改
作者:yance
"""
import re
import argparse
from pathlib import path
from dataclasses import dataclass, field
from enum import enum, auto
class renamemode(enum):
"""重命名模式"""
prefix = auto() # 添加前缀
sequence = auto() # 序号编号
regex = auto() # 正则替换
extension = auto() # 批量改扩展名
@dataclass
class renameconfig:
"""重命名配置"""
mode: renamemode
directory: path
pattern: str = "" # 正则模式(regex 模式用)
replacement: str = "" # 替换字符串
prefix: str = "" # 前缀(prefix 模式用)
start: int = 1 # 起始序号(sequence 模式用)
width: int = 3 # 序号宽度(001, 002, ...)
new_ext: str = "" # 新扩展名(extension 模式用)
filter_glob: str = "*" # 文件过滤通配符
dry_run: bool = false # 预览模式
@dataclass
class renameresult:
"""单个文件的重命名结果"""
old_path: path
new_path: path
success: bool = false
error: str = ""
class batchrenamer:
"""批量重命名器"""
def __init__(self, config: renameconfig):
self.config = config
self.results: list[renameresult] = []
def _get_target_files(self) -> list[path]:
"""获取目标文件列表(按文件名排序)"""
directory = self.config.directory
if not directory.is_dir():
raise notadirectoryerror(f"目录不存在: {directory}")
files = sorted(
f for f in directory.iterdir()
if f.is_file()
and f.match(self.config.filter_glob)
)
return files
def _generate_new_name(self, file: path, index: int) -> str:
"""根据模式生成新文件名"""
old_name = file.name
stem = file.stem # 文件名(不含扩展名)
suffix = file.suffix # 扩展名(含 .)
mode = self.config.mode
if mode == renamemode.prefix:
return f"{self.config.prefix}{old_name}"
elif mode == renamemode.sequence:
seq = str(self.config.start + index).zfill(self.config.width)
return f"{seq}{suffix}"
elif mode == renamemode.regex:
new_stem = re.sub(
self.config.pattern,
self.config.replacement,
stem
)
return f"{new_stem}{suffix}"
elif mode == renamemode.extension:
ext = self.config.new_ext
if not ext.startswith("."):
ext = "." + ext
return f"{stem}{ext}"
else:
return old_name
def _get_new_path(self, file: path, index: int) -> path:
"""生成新路径,处理重名冲突"""
new_name = self._generate_new_name(file, index)
new_path = file.parent / new_name
# 如果新路径和旧路径相同,跳过
if new_path == file:
return file
# 如果目标已存在且不是自身,添加序号后缀
counter = 1
while new_path.exists() and new_path != file:
new_name = self._generate_new_name(file, index)
stem = new_path.stem
suffix = new_path.suffix
new_path = file.parent / f"{stem}_{counter}{suffix}"
counter += 1
return new_path
def rename_single(self, file: path, index: int) -> renameresult:
"""重命名单个文件"""
new_path = self._get_new_path(file, index)
# 目标和源相同,不需要重命名
if new_path == file:
return renameresult(
old_path=file,
new_path=new_path,
success=true,
error="no change needed"
)
result = renameresult(old_path=file, new_path=new_path)
try:
if not self.config.dry_run:
file.rename(new_path)
result.success = true
except permissionerror as e:
result.error = f"权限不足: {e}"
except oserror as e:
result.error = f"系统错误: {e}"
return result
def run(self) -> list[renameresult]:
"""执行批量重命名"""
files = self._get_target_files()
if not files:
print(f"目录中没有匹配 '{self.config.filter_glob}' 的文件")
return []
mode_name = self.config.mode.name
print(f"\n{'=' * 50}")
print(f"重命名模式: {mode_name}")
print(f"目标目录: {self.config.directory}")
print(f"匹配文件: {len(files)} 个")
print(f"预览模式: {'是' if self.config.dry_run else '否'}")
print(f"{'=' * 50}\n")
for index, file in enumerate(files):
result = self.rename_single(file, index)
self.results.append(result)
status = "✅" if result.success else "❌"
action = "预览" if self.config.dry_run else "已重命名"
if result.error and result.error != "no change needed":
print(f" {status} {result.old_path.name} → {result.new_path.name}")
print(f" 错误: {result.error}")
elif result.error == "no change needed":
print(f" ⏭️ {result.old_path.name} (无需修改)")
else:
print(f" {status} {result.old_path.name} → {result.new_path.name} ({action})")
# 汇总
total = len(self.results)
success = sum(1 for r in self.results if r.success)
failed = total - success
print(f"\n{'─' * 50}")
print(f"总计: {total} | 成功: {success} | 失败: {failed}")
if self.config.dry_run:
print("\n💡 这是预览模式,未实际修改文件。")
print(" 去掉 --dry-run 参数以实际执行。")
return self.results
def build_config_from_args() -> renameconfig:
"""从命令行参数构建配置"""
parser = argparse.argumentparser(
description="批量文件重命名工具",
formatter_class=argparse.rawtexthelpformatter,
)
parser.add_argument("directory", type=str, help="目标目录路径")
parser.add_argument("--mode", type=str, required=true,
choices=["prefix", "sequence", "regex", "extension"],
help="重命名模式:\n"
" prefix - 添加前缀\n"
" sequence - 序号编号\n"
" regex - 正则替换\n"
" extension - 批量改扩展名")
parser.add_argument("--prefix", type=str, default="", help="前缀内容 (prefix 模式)")
parser.add_argument("--pattern", type=str, default="", help="正则模式 (regex 模式)")
parser.add_argument("--replacement", type=str, default="", help="替换字符串 (regex 模式)")
parser.add_argument("--start", type=int, default=1, help="起始序号 (sequence 模式)")
parser.add_argument("--width", type=int, default=3, help="序号宽度 (sequence 模式)")
parser.add_argument("--new-ext", type=str, default="", help="新扩展名 (extension 模式)")
parser.add_argument("--filter", type=str, default="*", help="文件过滤通配符 (如 *.jpg)")
parser.add_argument("--dry-run", action="store_true", help="预览模式,不实际修改")
args = parser.parse_args()
mode_map = {
"prefix": renamemode.prefix,
"sequence": renamemode.sequence,
"regex": renamemode.regex,
"extension": renamemode.extension,
}
return renameconfig(
mode=mode_map[args.mode],
directory=path(args.directory),
prefix=args.prefix,
pattern=args.pattern,
replacement=args.replacement,
start=args.start,
width=args.width,
new_ext=args.new_ext,
filter_glob=args.filter,
dry_run=args.dry_run,
)
if __name__ == "__main__":
config = build_config_from_args()
renamer = batchrenamer(config)
renamer.run()
# 1. 给所有图片添加前缀 "vacation_" python renamer.py ./photos --mode prefix --prefix "vacation_" --filter "*.jpg" --dry-run # 2. 把所有文件按序号编号(001.jpg, 002.jpg, ...) python renamer.py ./photos --mode sequence --start 1 --width 3 --filter "*.jpg" # 3. 正则替换:把文件名中的空格替换为下划线 python renamer.py ./data --mode regex --pattern "\s+" --replacement "_" --dry-run # 4. 批量修改扩展名:.jpeg → .jpg python renamer.py ./photos --mode extension --new-ext "jpg" --filter "*.jpeg" # 5. 先预览再执行(推荐工作流) python renamer.py ./photos --mode sequence --dry-run # 先预览 python renamer.py ./photos --mode sequence # 没问题再执行
================================================== 重命名模式: sequence 目标目录: photos 匹配文件: 5 个 预览模式: 是 ================================================== ✅ img_001.jpg → 001.jpg (预览) ✅ img_002.jpg → 002.jpg (预览) ✅ img_003.jpg → 003.jpg (预览) ✅ img_004.jpg → 004.jpg (预览) ✅ img_005.jpg → 005.jpg (预览) ────────────────────────────────────────────────── 总计: 5 | 成功: 5 | 失败: 0 💡 这是预览模式,未实际修改文件。 去掉 --dry-run 参数以实际执行。
| 设计点 | 做法 | 为什么 |
|---|---|---|
| 路径管理 | 全程 pathlib.path | 跨平台 + 链式调用 + 面向对象 |
| 配置管理 | dataclass + enum | 类型安全、可读性好 |
| 结果追踪 | renameresult 记录每个文件 | 可审计、可回滚 |
| 安全第一 | --dry-run 预览模式 | 改文件名这种操作,先看后做 |
| 重名处理 | 自动加 _1、_2 后缀 | 避免覆盖已有文件 |
| 异常处理 | 捕获 permissionerror / oserror | 某个文件失败不影响其他文件 |
| 文件排序 | sorted() | 保证序号模式结果可预测 |
一张表速查全文要点:
| 知识点 | 核心记忆 |
|---|---|
open() 模式 | r 读 / w 覆盖写 / a 追加 / x 独占创建 / b 二进制 / + 读写 |
with 语句 | __enter__ + __exit__,保证资源释放,永远用它 |
| 文本 vs 二进制 | 非文本文件一律加 b,文本文件显式指定 encoding="utf-8" |
| csv | newline="" 防 windows 空行 |
| json | ensure_ascii=false 保中文 |
| ini | configparser,值都是字符串需手动转类型 |
| yaml | 永远用 safe_load(),别用 load() |
pathlib vs os.path | 新项目无脑 pathlib,/ 运算符 + 链式调用 |
| 临时文件 | tempfile 模块,with 自动清理 |
| 大文件读取 | 逐行迭代 for line in f,别 read() 全读进来 |
一句话总结:文件操作的核心是"安全"和"优雅"——with 保证安全,pathlib 保证优雅。
别再用 os.path.join 拼字符串了,2026 年了,path(a) / b 不香吗?
以上就是一文详解python文件操作与路径处理的详细内容,更多关于python文件操作与路径处理的资料请关注代码网其它相关文章!
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论