it编程 > 前端脚本 > Python

一文详解Python文件操作与路径处理

2人参与 2026-08-07 Python

一、介绍:文件操作为什么是基本功

说白了,程序的本质就是 读数据 → 处理数据 → 写数据。而"数据"这东西,大部分时候不是内存里临时蹦出来的变量,而是躺在硬盘上的文件。

python 对文件操作的设计哲学很 pythonic:

语言打开文件关闭文件读一行
cfopen() + 手动 fclose()手动fgets()
javanew fileinputstream() + try-finally手动readline()
pythonopen()with 自动关f.readline()

python 干了两件漂亮事:

  1. with 语句——让资源释放变成语言级保证,不再依赖程序员记性好不好。
  2. pathlib——把路径操作从"字符串拼接地狱"升级成"面向对象优雅操作"。

这两件事,一个管安全,一个管优雅。本文就围绕这两个核心展开。

二、原理:文件 i/o 的底层机制

2.1 操作系统层面:文件描述符

当你在 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) 是操作系统给你打开的每个文件分配的一个整数编号:

你可以直接看到它:

f = open("test.txt", "w")
print(f.fileno())   # 输出: 3(或更大的数字)
f.close()

2.2 缓冲区机制

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,数据安全落盘

2.3 三层 i/o 架构

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() 与文件模式速查表

3.1 open() 完整签名

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"

3.2 模式字符含义

字符含义英文助记
r只读(默认)read
w只写(覆盖已存在文件)write
a追加写(文件末尾追加)append
x独占创建(文件已存在则报错)exclusive
b二进制模式binary
t文本模式(默认)text
+读写模式(可读可写)plus

3.3 模式组合速查表

模式读?写?文件不存在文件已存在指针位置
r报错 filenotfounderror开头
r+报错开头
w创建清空开头
w+创建清空开头
a创建末尾
a+创建末尾
x创建报错 fileexistserror开头
x+创建报错开头

b 后缀即为二进制模式,行为一致,只是数据单元从 str 变成 bytes

3.4 with 语句语法

# 基本形式
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)

四、图示:一张图看懂文件模式与路径演进

4.1 文件模式决策树

                    需要操作文件?
                         │
              ┌──────────┼──────────┐
              ▼          ▼          ▼
          只读?      只写?      读写?
              │          │          │
              ▼          ▼          ▼
             r          w          r+(不清空)
              │     ┌────┴────┐         │
              │     ▼         ▼         ▼
              │  覆盖写?  追加写?   需要创建新文件且防覆盖?
              │     │         │         │
              │     ▼         ▼         ▼
              │    w/a       a         x
              │     │         │
              └─────┴─────────┘
                    │
              数据是二进制?
                    │
                    ▼
                加 b 后缀(rb / wb / ab)

4.2 路径操作演进史

  暗黑时代                过渡期                 现代
  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 对象,可链式调用
  ❌ 字符串拼接地狱     ✅ / 运算符,跨平台

五、踩坑点:那些年我踩过的文件坑

坑 1:不 close 文件 = 数据丢失 + 资源泄漏

# ❌ 危险写法
f = open("test.txt", "w")
f.write("重要数据")
# 忘了 close()...
# 程序正常退出时 python 会帮你关,但如果异常退出?
# 数据可能还在缓冲区,没落盘!

# ✅ 正确写法
with open("test.txt", "w") as f:
    f.write("重要数据")
# 出了 with 块,__exit__ 保证执行 flush + close

坑 2:文本模式读二进制文件 → 编码炸裂

# ❌ 用文本模式读 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……

坑 3:windows 下的编码噩梦

# ❌ 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",别依赖系统默认。

坑 4:w 模式直接清空文件

# ❌ 想追加结果,结果把整个文件清空了
with open("log.txt", "w") as f:     # w 模式会先截断文件!
    f.write("新日志")

# ✅ 追加用 a 模式
with open("log.txt", "a") as f:     # 在末尾追加
    f.write("新日志\n")

坑 5:r+ 模式的指针陷阱

# 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+ 模式写入是覆盖式写入,不是插入。想插入得自己手动移动文件指针。

坑 6:路径拼接用字符串 + → 跨平台翻车

# ❌ 手动拼接路径
path = "data" + "/" + "file.txt"    # windows 上分隔符是 \,虽然 python 能容忍 /,但不优雅

# ❌ 更糟的写法
path = "data" + "\\" + "file.txt"  # 硬编码反斜杠,在 linux 上就是字面量

# ✅ pathlib 方式
from pathlib import path
path = path("data") / "file.txt"   # / 运算符自动处理分隔符

坑 7:read() 一次性读取大文件 → 内存爆炸

# ❌ 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)

六、源码拆解:从 open() 到上下文管理器

6.1 open() 的 cpython 实现路径

在 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

关键点:

6.2 上下文管理器协议

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_tbtraceback 对象

这就是 with 能保证资源释放的根本原因——__exit__finally 语义中被调用,不可跳过。

6.3 contextlib:不用写类也能搞上下文管理器

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 装饰器把一个生成器函数变成了上下文管理器,比写类简洁多了。

6.4 python 文件对象的方法一览

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

七、效果对比:pathlib vs os.path 实战

7.1 基础操作对比

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"])

7.2 文件操作对比

# ========== 判断文件是否存在 ==========
# 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)

7.3 文件读写对比

# 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 完胜

7.4 一句话总结

对比维度os.pathpathlib
返回类型strpath 对象(可链式调用)
路径拼接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

八、csv / json / ini / yaml 文件处理

8.1 csv 文件

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

8.2 json 文件

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

8.3 ini 文件

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 继承

8.4 yaml 文件

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() 只解析基本数据类型,安全。

8.5 四种格式横向对比

格式标准库?支持注释数据类型典型用途
csvcsv表格(二维)数据导出、excel 交互
jsonjsondict/list/str/int/float/bool/noneapi 传输、配置文件
iniconfigparser# / ;纯字符串(需手动转类型)简单键值对配置
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位字母数字),足够避免冲突。
安全提示:mkstempmktemp 安全,因为创建和返回是原子操作,不会被竞争攻击。

十、实操 demo:批量文件重命名工具

把前面学的全部串起来,写一个实用的批量重命名工具。

10.1 功能设计

10.2 完整代码

#!/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()

10.3 使用示例

# 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            # 没问题再执行

10.4 运行效果

==================================================
重命名模式: 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 参数以实际执行。

10.5 代码设计要点回顾

设计点做法为什么
路径管理全程 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"
csvnewline="" 防 windows 空行
jsonensure_ascii=false 保中文
iniconfigparser,值都是字符串需手动转类型
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文件操作与路径处理的资料请关注代码网其它相关文章!

(0)

您想发表意见!!点此发布评论

推荐阅读

Python结合PyQt6实现屏幕截图工具(附源码)

08-07

Python中定时任务APScheduler框架使用教程

08-07

Python基础入门之模块搜索路径sys.path用法详解

08-07

Python+PyQt6实现将PDF文件转高清图片

08-07

Python+PyQt6实现跨平台串口设备热插拔检测

08-07

Python difflib库实现文本差异比较与合并实战指南

08-07

猜你喜欢

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论