win10小程序(二十四)批量新建文件夹

'''将大量文件按名称新建文件夹,并可迁移进新建的文件夹中。

特殊功能:1.根据剪切板内容命名,即复制一批名字,会按每行内容生成文件夹。

2.添加、删除内容。

3.按指定数量批量复制文件夹并自动添加序号。例如某村某组_1、某组_2。

'''

import os

import json

import shutil

import tkinter as tk

from tkinter import filedialog, messagebox, ttk

CONFIG_FILE = os.path.join(os.path.expanduser("~"), ".batch_folder_creator.json")

class BatchFolderCreator:

def init(self, root):

self.root = root

self.root.title("批量新建文件夹工具")

self.root.minsize(660, 600)

变量

self.last_output_folder = tk.StringVar() # 记忆上次输出位置

self.name_mode = tk.StringVar(value="file") # file / clipboard

self.create_new_folder = tk.BooleanVar(value=True)

self.file_action = tk.StringVar(value="none") # none / copy / move

self.prefix_content = tk.StringVar()

self.suffix_content = tk.StringVar()

self.add_serial = tk.BooleanVar(value=False) # 默认不选中

self.serial_sep = tk.StringVar(value="_") # 序号前连接符

self.max_serial = tk.StringVar(value="10")

self.del_left = tk.StringVar(value="0")

self.del_right = tk.StringVar(value="0")

self.selected_files = \[\] # 完整路径列表(保持顺序)

self._load_config()

self._build_ui()

---------- 配置持久化 ----------

def _load_config(self):

if not os.path.isfile(CONFIG_FILE):

return

try:

with open(CONFIG_FILE, "r", encoding="utf-8") as f:

cfg = json.load(f)

self.last_output_folder.set(cfg.get("last_output_folder", ""))

self.name_mode.set(cfg.get("name_mode", "file"))

self.create_new_folder.set(cfg.get("create_new_folder", True))

self.file_action.set(cfg.get("file_action", "none"))

self.prefix_content.set(cfg.get("prefix_content", ""))

self.suffix_content.set(cfg.get("suffix_content", ""))

self.add_serial.set(cfg.get("add_serial", False))

self.serial_sep.set(cfg.get("serial_sep", "_"))

self.max_serial.set(cfg.get("max_serial", "10"))

self.del_left.set(cfg.get("del_left", "0"))

self.del_right.set(cfg.get("del_right", "0"))

except Exception:

pass

def _save_config(self):

cfg = {

"last_output_folder": self.last_output_folder.get(),

"name_mode": self.name_mode.get(),

"create_new_folder": self.create_new_folder.get(),

"file_action": self.file_action.get(),

"prefix_content": self.prefix_content.get(),

"suffix_content": self.suffix_content.get(),

"add_serial": self.add_serial.get(),

"serial_sep": self.serial_sep.get(),

"max_serial": self.max_serial.get(),

"del_left": self.del_left.get(),

"del_right": self.del_right.get(),

}

try:

with open(CONFIG_FILE, "w", encoding="utf-8") as f:

json.dump(cfg, f, ensure_ascii=False, indent=2)

except Exception:

pass

---------- UI ----------

def _build_ui(self):

pad = {"padx": 10, "pady": 5}

顶部容器

frame_top = ttk.Frame(self.root)

frame_top.pack(side="top", fill="both", expand=True)

1. 源文件 + 命名依据 + 执行按钮(同一行)

frame_src = ttk.Frame(frame_top)

frame_src.pack(fill="x", padx=10, pady=5)

ttk.Button(frame_src, text="执行", command=self.execute).pack(

side="left", padx=(0, 5)

)

ttk.Button(frame_src, text="选择源文件...", command=self.choose_source_files).pack(

side="left", padx=5

)

ttk.Button(frame_src, text="清空", command=self.clear_files).pack(

side="left", padx=5

)

ttk.Separator(frame_src, orient="vertical").pack(side="left", fill="y", padx=10)

ttk.Radiobutton(

frame_src, text="按文件命名",

variable=self.name_mode, value="file",

).pack(side="left", padx=5)

ttk.Radiobutton(

frame_src, text="按剪切板内容命名:每行1个",

variable=self.name_mode, value="clipboard",

).pack(side="left", padx=5)

文件列表(无标题)

frame_list = ttk.Frame(frame_top)

frame_list.pack(fill="both", expand=True, padx=10, pady=5)

list_container = ttk.Frame(frame_list)

list_container.pack(fill="both", expand=True)

self.file_listbox = tk.Listbox(list_container, selectmode="extended", height=4)

self.file_listbox.pack(side="left", fill="both", expand=True)

scrollbar = ttk.Scrollbar(list_container, orient="vertical", command=self.file_listbox.yview)

scrollbar.pack(side="right", fill="y")

self.file_listbox.config(yscrollcommand=scrollbar.set)

3. 新建文件夹

frame_new = ttk.LabelFrame(frame_top, text="3. 输出位置")

frame_new.pack(fill="x", **pad)

row1 = ttk.Frame(frame_new); row1.pack(fill="x", padx=5, pady=3)

ttk.Checkbutton(

row1, text="记住输出位置,下次执行直接使用(不弹窗)",

variable=self.create_new_folder

).pack(side="left")

ttk.Label(row1, text=" | ").pack(side="left")

ttk.Label(row1, text="源文件:").pack(side="left")

ttk.Radiobutton(row1, text="不处理", variable=self.file_action, value="none").pack(side="left", padx=3)

ttk.Radiobutton(row1, text="复制到对应文件夹", variable=self.file_action, value="copy").pack(side="left", padx=3)

ttk.Radiobutton(row1, text="剪切到对应文件夹", variable=self.file_action, value="move").pack(side="left", padx=3)

4. 添加内容(前/后 同一行)

frame_content = ttk.LabelFrame(frame_top, text="4. 在原文件名上添加内容")

frame_content.pack(fill="x", **pad)

ttk.Label(frame_content, text="在前添加:").pack(side="left", padx=(5, 2), pady=3)

ttk.Entry(frame_content, textvariable=self.prefix_content, width=18).pack(

side="left", padx=2, pady=3

)

ttk.Label(frame_content, text="在后添加:").pack(side="left", padx=(15, 2), pady=3)

ttk.Entry(frame_content, textvariable=self.suffix_content, width=18).pack(

side="left", padx=2, pady=3

)

5. 删除字数(左/右两个独立输入框)

frame_del = ttk.LabelFrame(frame_top, text="5. 删除原名称字数")

frame_del.pack(fill="x", **pad)

ttk.Label(frame_del, text="从左往右删除:").pack(side="left", padx=5, pady=3)

ttk.Entry(frame_del, textvariable=self.del_left, width=8).pack(

side="left", padx=5, pady=3

)

ttk.Label(frame_del, text="从右往左删除:").pack(side="left", padx=(20, 5), pady=3)

ttk.Entry(frame_del, textvariable=self.del_right, width=8).pack(

side="left", padx=5, pady=3

)

ttk.Label(frame_del, text="(0 表示不删除)").pack(side="left", padx=10)

6. 添加序号

frame_serial = ttk.LabelFrame(frame_top, text="6. 添加序号")

frame_serial.pack(fill="x", **pad)

ttk.Checkbutton(

frame_serial, text="添加序号", variable=self.add_serial

).pack(side="left", padx=5, pady=3)

ttk.Label(frame_serial, text="序号前符号:").pack(side="left", padx=5)

ttk.Entry(frame_serial, textvariable=self.serial_sep, width=4).pack(

side="left", padx=5

)

ttk.Label(frame_serial, text="最大序号:").pack(side="left", padx=(15, 5))

ttk.Entry(frame_serial, textvariable=self.max_serial, width=8).pack(

side="left", padx=5

)

ttk.Label(frame_serial, text="(0 表示不编号)").pack(side="left", padx=5)

---------- 动作 ----------

def choose_source_files(self):

files = filedialog.askopenfilenames(title="选择源文件(可多选)")

if not files:

return

按文件名排序,但保持完整路径

sorted_pairs = sorted(

(os.path.basename(p), p) for p in files\], key=lambda x: x\[0

)

self.selected_files = p for _, p in sorted_pairs

self.file_listbox.delete(0, tk.END)

for name, _ in sorted_pairs:

self.file_listbox.insert(tk.END, name)

def clear_files(self):

self.selected_files = \[\]

self.file_listbox.delete(0, tk.END)

def _pick_output_folder(self, mode):

"""

勾选(记住输出位置):

  • 有有效记忆 → 直接用,不弹窗

  • 无记忆(第一次运行)→ 弹窗让用户选一次,之后会记住

未勾选:

  • 每次都弹窗让用户选

"""

remembered = self.last_output_folder.get().strip()

remembered_ok = bool(remembered) and os.path.isdir(remembered)

if self.create_new_folder.get() and remembered_ok:

勾选 + 有记忆 → 直接用,不弹窗

return remembered

其余情况全部弹窗(第一次运行必须选一次位置)

initialdir 优先用记忆位置,其次源文件目录,最后用户主目录

initial = remembered if remembered_ok else os.path.expanduser("~")

if initial == os.path.expanduser("~") and mode == "file" and self.selected_files:

src_dir = os.path.dirname(self.selected_files0)

if src_dir and os.path.isdir(src_dir):

initial = src_dir

folder = filedialog.askdirectory(title="选择输出文件夹(文件夹将创建在此目录下)", initialdir=initial)

return folder

def _parse_int(self, text, field_name, allow_zero=True):

text = text.strip()

if text == "":

return 0

try:

value = int(text)

except ValueError:

raise ValueError(f"{field_name}必须是整数")

if value < 0 or (not allow_zero and value == 0):

raise ValueError(f"{field_name}不能为负数" + ("或 0" if not allow_zero else ""))

return value

def execute(self):

先拿到命名模式(_pick_output_folder 需要知道它来做 fallback)

mode = self.name_mode.get()

目标文件夹

folder = self._pick_output_folder(mode)

if not folder:

return

if not os.path.isdir(folder):

messagebox.showwarning("提示", "所选输出位置无效")

return

基础名称来源

source_paths = \[\] # 完整源路径,与 base_names 一一对应(仅 file 模式)

if mode == "file":

if not self.selected_files:

messagebox.showwarning("提示", "请先选择用于命名的源文件")

return

source_paths = list(self.selected_files)

base_names = os.path.splitext(os.path.basename(p))\[0 for p in source_paths]

else: # clipboard

try:

clip = self.root.clipboard_get()

except Exception:

messagebox.showwarning("提示", "剪切板为空或无法读取")

return

base_names = line.strip() for line in clip.splitlines() if line.strip()

if not base_names:

messagebox.showwarning("提示", "剪切板内容为空")

return

prefix = self.prefix_content.get()

suffix = self.suffix_content.get()

删除字数

try:

del_left = self._parse_int(self.del_left.get(), "从左往右删除字数")

del_right = self._parse_int(self.del_right.get(), "从右往左删除字数")

except ValueError as e:

messagebox.showwarning("提示", str(e))

return

序号

try:

max_serial = self._parse_int(self.max_serial.get(), "最大序号")

except ValueError as e:

messagebox.showwarning("提示", str(e))

return

add_serial = self.add_serial.get()

serial_sep = self.serial_sep.get()

if add_serial and max_serial == 0:

messagebox.showwarning("提示", "勾选添加序号时,最大序号不能为 0")

return

文件处理前置校验

file_action = self.file_action.get()

if file_action in ("copy", "move") and mode != "file":

messagebox.showwarning("提示", "复制/剪切源文件需要用『按文件命名』模式")

return

生成名称

names_to_create = \[\] # 每个元素是 (原始base_index, 最终文件夹名)

for idx, base in enumerate(base_names):

base = base.strip()

if not base:

continue

先删左、再删右

if del_left > 0:

base = basedel_left: if del_left < len(base) else ""

if del_right > 0 and base:

base = base:-del_right if del_right < len(base) else ""

加上前后添加内容

name_part = f"{prefix}{base}{suffix}"

if not name_part:

continue

if add_serial and max_serial > 0:

for i in range(1, max_serial + 1):

names_to_create.append((idx, f"{name_part}{serial_sep}{i}"))

else:

names_to_create.append((idx, name_part))

if not names_to_create:

messagebox.showwarning("提示", "没有生成任何文件夹名称")

return

去重(保留首次出现的)

unique_names = \[\]

seen = set()

for idx, name in names_to_create:

if name not in seen:

seen.add(name)

unique_names.append((idx, name))

检查已存在

existing = n for _, n in unique_names if os.path.exists(os.path.join(folder, n))

if existing:

msg = "以下文件夹已存在,将跳过:\n" + "\n".join(existing:10)

if len(existing) > 10:

msg += f"\n... 共 {len(existing)} 个"

if not messagebox.askyesno("确认", msg + "\n\n是否继续?"):

return

创建

created = 0

skipped = 0

errors = \[\]

for idx, name in unique_names:

target = os.path.join(folder, name)

if os.path.exists(target):

skipped += 1

continue

try:

os.makedirs(target)

created += 1

except Exception as e:

errors.append(f"{name}: {e}")

复制/剪切源文件到对应文件夹

copy + 序号:每个 (idx, name) 对都 copy source_pathsidx → 天然实现一份源复制到所有序号文件夹

move + 序号:先按 idx 分组,同一源文件先全部 copy 到各序号文件夹,成功后再删原文件

move + 非序号:原有 shutil.move 一对一逻辑

file_op_ok = 0

file_op_fail = \[\]

if file_action in ("copy", "move"):

先把每个 (idx, name) 对变成完整的 (src_path, dst_dir, dst_path, folder_name)

ops = \[\]

for idx, name in unique_names:

src = source_pathsidx

dst_dir = os.path.join(folder, name)

if not os.path.isdir(dst_dir):

continue

if not os.path.isfile(src):

file_op_fail.append(f"{os.path.basename(src)} → {name}(源文件不存在)")

continue

dst_path = os.path.join(dst_dir, os.path.basename(src))

if os.path.exists(dst_path):

file_op_fail.append(f"{os.path.basename(src)} → {name}(目标已存在)")

continue

ops.append((src, dst_dir, dst_path, name))

if not ops:

pass

elif file_action == "copy":

复制模式:直接全部 copy

for src, dst_dir, dst_path, name in ops:

try:

shutil.copy2(src, dst_path)

file_op_ok += 1

except Exception as e:

file_op_fail.append(f"{os.path.basename(src)} → {name}: {e}")

elif add_serial:

剪切 + 序号:先全部 copy 到各文件夹,成功后再删原文件

按源文件分组,确保同一源文件的所有目标文件夹都收到副本

from collections import defaultdict

groups = defaultdict(list)

for src, dst_dir, dst_path, name in ops:

groupssrc.append((dst_path, name))

for src_path, targets in groups.items():

all_copied = True

for dst_path, name in targets:

try:

shutil.copy2(src_path, dst_path)

file_op_ok += 1

except Exception as e:

file_op_fail.append(

f"{os.path.basename(src_path)} → {name}: {e}"

)

all_copied = False

全部副本到位后,删除原文件(实现"剪切到多份"的语义)

if all_copied:

try:

os.remove(src_path)

except Exception as e:

file_op_fail.append(

f"删除源文件 {os.path.basename(src_path)} 失败: {e}"

)

else:

剪切 + 非序号:原有 shutil.move 一对一逻辑

for src, dst_dir, dst_path, name in ops:

try:

shutil.move(src, dst_path)

file_op_ok += 1

except Exception as e:

file_op_fail.append(f"{os.path.basename(src)} → {name}: {e}")

保存配置

self.last_output_folder.set(folder)

self._save_config()

result = f"创建完成!\n成功:{created} 个\n跳过(已存在):{skipped} 个"

if errors:

result += f"\n创建失败:{len(errors)} 个\n" + "\n".join(errors:5)

if file_action in ("copy", "move"):

label = "复制" if file_action == "copy" else "剪切"

result += f"\n\n源文件{label}:成功 {file_op_ok} 个"

if file_op_fail:

result += f",失败 {len(file_op_fail)} 个\n" + "\n".join(file_op_fail:5)

messagebox.showinfo("结果", result)

if name == "main":

root = tk.Tk()

app = BatchFolderCreator(root)

root.mainloop()

相关推荐
Ai-_Man10 小时前
Meta Muse还是不可以导出左侧所有对话,只能点进去单条内容导出,方法不对吗?
开发语言·人工智能·ai·小程序·c#·1024程序员节
andrsted13 小时前
练题簿在线免费刷题:让整理过的资料,变成能反复练习的题库
学习·微信小程序·小程序·学习方法·刷题·1024程序员节
CRMEB系统商城1 天前
家电零售的供应商协同与物流提效
java·开发语言·人工智能·小程序·1024程序员节
CRMEB系统商城1 天前
Redis + 消息队列 + 连接池:CRMEB PRO 性能优化的三驾马车
大数据·数据库·人工智能·小程序·php·1024程序员节
CRMEB系统商城1 天前
服装连锁的导购分销与企微私域
java·大数据·开发语言·人工智能·小程序·php·1024程序员节
00后程序员张2 天前
无需 Mac 的 iOS App 上架工具,开心上架Appuploader 负责哪些环节
android·ios·小程序·https·uni-app·iphone·webview
2501_915909063 天前
Ipa Guard 防护上架,混淆、重签名和回归测试怎么做
android·ios·小程序·https·uni-app·iphone·webview
济南壹软网络科技有限公司3 天前
盲盒源码系统小程序V6MAX底层架构解析,PHP+Uniapp高性能商用底座发布
小程序·架构·uni-app
2501_915918413 天前
iOS 上架注意事项有哪些?Appuploader 的四个阶段自查清单
android·ios·小程序·https·uni-app·iphone·webview