批量文件处理:循环、预演与回退
批量脚本改错一次,全部文件跟着出错,而且没有撤销键。动手之前先立好三道防线。
本章成品是一个 Python 脚本:把文件夹里的图片按统一规则重命名,其余类型跳过,同时写下新旧名称的对照记录,据此可以还原。
…\第10章> python 第10章演示_批量改名.py DSC01.JPG → 课堂照片_001.jpg IMG_2001.jpg → 课堂照片_002-2.jpg IMG_2002.jpg → 课堂照片_003.jpg (中间省略十余行) 屏幕截图 2026-03-15.png → 课堂照片_022.png 跳过 说明.txt 跳过 课表.xlsx 以上为预演结果,共 24 个文件待处理。
专门用来练习批量操作的目录,由生成脚本建立,重复运行即清空重建。
在脚本旁边生成三十个文件:图片二十四个,另有 .txt、.docx 等六个用来验证筛选;其中特意放了大写扩展名、带空格的中文名和一个与目标名称重复的文件。
循环让同一段代码对清单里的每一项各执行一次,缩进决定哪些语句属于循环体。条件筛选决定哪些文件被处理,条件写得越明确,误改的可能越小。
# ② 循环:对清单里的每一个文件重复同样的处理 for old in files: # ③ 条件筛选:只处理图片,其余文件跳过 if old.suffix.lower() not in ['.jpg', '.png']: print('跳过', old.name) continue number = number + 1 # ④ 组成新名称……(以下省略,同样缩进四格) # ⑦ 顶格写的语句不属于循环体,循环结束后执行一次 if not DRY_RUN: ……
对一批对象重复执行同一段代码。for old in files: 把 files 里的每一项依次取出,叫它 old。
写在 for 下面、缩进的那些语句,清单里每一项执行一次。
只对满足条件的对象执行操作;continue 跳过本次剩下的语句,直接处理下一项。
循环体里的语句少缩进一格
语句移到循环外,执行次数变了
for 下面的语句都缩进四格
清单里每一项各执行一次
old.suffix in ['.jpg', '.png']
DSC01.JPG 被当成不符合条件而跳过
old.suffix.lower()
.JPG 与 .jpg 都能认出来
先把文件列成清单并排序,再逐个处理。文件名按字符逐位比较,序号补足位数后,名称顺序才与数字顺序一致。
一次性列出的待处理对象清单。先取清单再处理,遍历途中文件被改名也不会漏掉或重复。
sorted() 把清单按名称排好,同一批文件每次运行的顺序和序号都相同。
把序号补足固定位数,str(number).zfill(3) 把 1 变成 001。位数按总量预留,超过一千个文件补足四位。
不先取清单,边找边改
已改名的文件可能被漏掉或重复处理
一次性列出文件,再进入循环
每个文件只处理一次
处理顺序由系统决定
同一批文件两次运行,序号可能不同
用 sorted(…) 排好再进入循环
每次运行顺序一致;is_file() 让子文件夹不被计入
绝对路径里写着盘符和用户名,换一台机器就失效。改用脚本自身所在的位置作基准,文件夹跟着脚本走。
文件在硬盘上的位置。
从盘符写起的完整路径,里面带着用户名,换一台机器就不成立。
拆开拼接路径时漏掉分隔符,是常见的出错原因;路径里的空格与中文本身不会出错。
C:\Users\User\Desktop\photos
在你的机器上就跑不起来,发给同组同学也一样
Path(__file__).with_name('测试文件夹')
换台机器照样找得到
预演把全部判断与计算跑完,只打印将要发生的结果,不改动任何文件,是执行之前唯一的检查环节。
| 执行前 · 测试文件夹(节选) | 执行后 |
|---|---|
| DSC01.JPG | 课堂照片_001.jpg |
| IMG_2001.jpg | 课堂照片_002-2.jpg |
| 屏幕截图 2026-03-15.png | 课堂照片_022.png |
| 海报终稿.png | 课堂照片_023.png |
| 说明.txt | 说明.txt |
| 课表.xlsx | 课表.xlsx |
标黄的是被跳过的文件,执行后名称应保持原样。核对时这一点最容易被忽略。
跑完全部判断与计算,把将要发生的结果打印出来,不做任何实际改动。
设置区里的一项:True 只打印不改动,False 真正执行。
AI 默认给出会改动文件的完整脚本
第一次运行就是正式执行,检查的机会已经错过
核对打印结果,通过后再补执行部分
规则有错,改提示词重来即可
old.rename(new) 不受 DRY_RUN 控制
预演也真的改名,预演失去意义
if not DRY_RUN: old.rename(new)
预演不动任何文件
| 预演输出里核对 | 序号 | 扩展名 | 非图片文件 | 总数 |
|---|---|---|---|---|
| 正常情况 | 连续 | 保留了原扩展名 | 显示"跳过" | 与图片数量对得上(本例 24 个) |
有一项不符,就回到提示词改规则,不要直接执行。
文件同步工具 rsync、容器编排工具 kubectl 和多数系统的软件包管理器都提供 dry-run 参数,作用与本章的预演开关相同,熟练的运维人员正式执行前同样先跑一次预演。本章的三道防线对应行业里的三种做法:测试文件夹对应隔离环境,预演对应 dry-run,改名记录对应撤销栈。
操作记录是新旧名称的对照表,回退脚本据此还原。记录只能在执行过程中写入,执行完原名称就不存在了。
新旧名称的对照表,每行记录一次改动。
按操作记录把名称改回执行之前的样子。回退脚本同样先预演,再执行。
执行时没有写记录
只能逐个手工改回去
每改一个文件,记下一行
回退脚本按记录还原
log.write_text(…)
中文名称在记录里变成乱码
log.write_text(…, encoding='utf-8')
中文名称正常保存
目标名称被占用时必须有事先规定,否则脚本中断或覆盖。有的冲突执行时才出现,所以执行后要开文件夹核对:先看数量,再看名称。
目标名称已经被占用。自动生成名称的脚本都要事先规定处理办法,例如追加 -2、-3。
Python 的 rename 在 Windows 上遇到重名时抛出的错误,脚本随即中断。
预演按文件夹的当前状态推算;执行时文件名陆续变化,结果可能与预演不同。
由 AI 自己决定
脚本中断,或者覆盖同名文件且没有提示
"目标名称已被占用时,在序号后面追加 -2"
既不中断,也不覆盖
执行后文件变少,继续操作
改名记录也被弄乱
先用改名记录还原,再去查冲突规则
记录保持完整,还能按它还原
屏幕输出只能证明脚本自认为做了什么。执行之后打开文件夹,按顺序看三样:
| 核对项 | 正常情况 | 不符说明什么 |
|---|---|---|
| 文件数量 | 执行前后完全相同 | 有文件被覆盖或合并,已经消失:立刻停手 |
| 被跳过的文件 | 名称一个字都没变 | 筛选条件写错,误伤了不该动的文件 |
| 改名记录的行数 | 等于屏幕打印的处理数量 | 中途出过错,记录不完整,回退会不彻底 |
批量需求分三步提问:先要预演版本,核对无误再补执行部分,最后要回退脚本。两步之间的人工核对是整个流程里唯一不能省的环节。
from pathlib import Path # ===== 设置区:只需要改下面三项 ===== FOLDER = '测试文件夹' # 待处理的文件夹,与本文件放在同一位置 PREFIX = '课堂照片' # 新文件名的前缀 DRY_RUN = True # True 为预演,只打印不改动;False 为真正执行 # ================================== folder = Path(__file__).with_name(FOLDER) # ① 先取清单:一次性列出文件夹里的文件并排序,再逐个处理 files = sorted(p for p in folder.iterdir() if p.is_file()) records = [] number = 0 # ② 循环:对清单里的每一个文件重复同样的处理 for old in files: # ③ 条件筛选:只处理图片,其余文件跳过 if old.suffix.lower() not in ['.jpg', '.png']: print('跳过', old.name) continue # ④ 组成新名称:序号补足三位,扩展名沿用原文件并转小写 number = number + 1 new = folder / (PREFIX + '_' + str(number).zfill(3) + old.suffix.lower()) # ⑤ 重名处理:目标名称已被占用时,在末尾追加 -2 if new.exists() and new != old: new = folder / (new.stem + '-2' + new.suffix) print(old.name, '→', new.name) records.append(new.name + ' <- ' + old.name) # ⑥ 预演状态下到此为止,不做任何实际改动 if not DRY_RUN: old.rename(new) # ⑦ 真正执行后写入改名记录,作为回退依据 if not DRY_RUN: log = Path(__file__).with_name('改名记录.txt') log.write_text('\n'.join(records), encoding='utf-8') print('已处理', len(records), '个文件,改名记录已写入', log.name) input('按回车键结束')
代码最上方集中放的三项:文件夹名、名称前缀、预演开关。换一批文件、换一套命名只改这三行,处理部分一字不动。
| 提示词中的规定 | 缺少时的表现 | 代码中的对应 |
|---|---|---|
| "先只给预演版本""不要对任何文件做实际改动" | AI 直接给出会改动文件的脚本;两句缺一句都可能拿到完整版 | if not DRY_RUN: old.rename(new) |
| 比较扩展名前先转小写 | .JPG 被当成不符合条件而跳过 | old.suffix.lower() |
| 前缀_三位序号 | 按名称排序时 10 排在 2 前面 | str(number).zfill(3) |
| 处理之前先排序 | 同一批文件两次运行,序号可能不同 | sorted(…) |
| 按脚本自身所在位置找 | 写成 C:\Users\User\Desktop\photos,换台机器就失效 | Path(__file__).with_name(FOLDER) |
| 写入改名记录,指定 utf-8 | 不写记录,改完无法还原;中文乱码 | log.write_text(…, encoding='utf-8') |
| 已被占用时追加 -2 | 中断,或者覆盖且没有任何提示 | new.exists() and new != old,后半句排除"目标名称就是它自己",免得无谓地加上 -2 |
三题选一,只在测试文件夹里练习,完成预演、执行、回退一整轮。
运行生成脚本。B、C 两题自己调整脚本里的文件名列表,文件不少于二十个
产出:练习环境把本题的筛选条件与命名规则写进第一段提示词,不留"注意处理好边界"这类空话;填好设置区,预演开关保持 True
产出:预演脚本核对序号、扩展名、跳过的文件、总数四项并截图;不通过就回第 2 步改规则,不要直接执行
产出:预演输出截图用第二段提示词补上执行部分,执行一次。执行前后各截一张文件夹的图,数一遍文件数量
产出:改名前后截图、改名记录用第三段提示词取得回退脚本,还原后与改前截图逐条比对。不能还原的作品不算完成
产出:还原后的截图三个脚本发给同组同学,用他自己的测试文件夹完整走一遍;重点看路径有没有写死、预演开关是否真的起作用
产出:一行互测记录先按现象找原因。文件数量减少时立刻停手,先用改名记录还原,再排查。
| 现象 | 原因 | 处理 |
|---|---|---|
FileNotFoundError,找不到测试文件夹 | 设置区的文件夹名与实际不符,或者文件夹不在脚本旁边 | 核对名称有没有多余空格,把文件夹与脚本放在同一位置 |
| 预演输出为空,一个文件都没有 | 筛选条件与实际扩展名不符,例如文件是 .jpeg | 把 .jpeg 加进允许列表,并确认比较前已经转小写 |
FileExistsError,执行中途中断 | 目标名称已被占用,脚本没有冲突处理 | 先用改名记录还原已改的部分,补上冲突规则后重新执行 |
| 执行后文件数量减少 | 重名时被覆盖,被覆盖的文件已经消失 | 立刻停止操作,用改名记录还原,再检查冲突规则有没有写进代码 |
PermissionError,拒绝访问 | 文件正被其他程序占用,例如图片预览窗口没关 | 关掉正在查看或编辑该文件的程序,再重新运行 |
| 改名后按名称排序错乱 | 序号没有补零 | 先回退,在设置区确认序号位数后重新执行 |
| 改名记录.txt 是空的 | 仍处在预演状态,并没有真正执行 | 把预演开关改为 False,再运行一次 |
| 改名记录里的中文是乱码 | 写入文件时没有指定编码 | 写文件时加上 encoding='utf-8',重新执行一遍 |
提交四项内容,按七条标准验收;选做两项。
按教师提供的作业模板填写,文档开头注明所选题目;提交渠道见课堂通知,当堂提交。
零代码应用开发 · 第 10 章 批量干活