原文地址:https://itxiaozhang.com/batch-create-folders/
如果您需要远程电脑维修或者编程开发,请加我微信咨询。
📌 常见场景 员工资料:公司新入职 200 名员工,需要批量生成个人资料文件夹。 学生作业:某班级 60 名学生上交作业,老师按花名册批量生成作业文件夹。 项目成员:一个跨部门项目组有 50 名成员,项目经理为其批量创建任务资料文件夹。 💡 为什么要批量创建文件夹 手动逐个新建既耗时又容易出错。 无论是为 20 个部门建文件夹,还是整理 100 张照片,重复操作都会浪费时间。 批量方式可节省约 90% 时间,让你专注更重要的工作。
方法一:ChatGPT 帮忙 将以下代码粘贴给 ChatGPT,让其执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 # 请作为Python解释器运行以下代码 import zipfile import os # 文件夹和文件名列表,可以增加或者替换更多 names = """唐僧 孙悟空 猪八戒 沙和尚 白龙马 玉皇大帝 王母娘娘 太上老君 太白金星 托塔李天王 哪吒三太子 二郎神 四大天王 巨灵神 二十八宿 雷公 电母 风伯 雨师 赤脚大仙 福禄寿三星 镇元子 如来佛祖 观音菩萨 文殊菩萨 普贤菩萨 地藏王菩萨 弥勒佛 燃灯古佛 十八罗汉 阿傩 伽叶 寅将军 熊山君 特处士 白骨精 黄袍怪 金角大王 银角大王 九尾狐狸 狮猁怪 红孩儿 鼍龙 虎力大仙 鹿力大仙 羊力大仙 灵感大王 独角兕大王 蝎子精 六耳猕猴 铁扇公主 牛魔王 玉面狐狸 万圣龙王 九头虫 黄眉老祖 蟒蛇精 赛太岁 蜘蛛精 百眼魔君 青狮 白象 大鹏金翅雕 白鹿精 白面狐狸 地涌夫人 南山大王 黄狮精 九灵元圣 辟寒大王 辟暑大王 辟尘大王 玉兔精 混世魔王 黑熊精 凌虚子 白衣秀士 虎先锋 黄风怪 精细鬼 伶俐虫 巴山虎 倚海龙 狐阿七大王 宝象国国王 百花羞公主 乌鸡国国王 车迟国国王 女儿国国王 朱紫国国王 比丘国国王 灭法国国王 寇员外 陈光蕊 殷温娇 刘伯钦 高太公 高翠兰 唐太宗 李世民 房玄龄 杜如晦 徐茂公""".splitlines() base_dir = "/mnt/data/named_folders" zip_filename = "/mnt/data/named_folders.zip" # 创建文件夹 os.makedirs(base_dir, exist_ok=True) for name in names: os.makedirs(os.path.join(base_dir, name), exist_ok=True) # 压缩文件夹 with zipfile.ZipFile(zip_filename, 'w', zipfile.ZIP_DEFLATED) as zipf: for root, dirs, files in os.walk(base_dir): for file in files: file_path = os.path.join(root, file) zipf.write(file_path, os.path.relpath(file_path, base_dir)) for dir in dirs: dir_path = os.path.join(root, dir) zipf.write(dir_path, os.path.relpath(dir_path, base_dir)) zip_filename 方法二:纯前端在线工具 特点 无需安装:网页直接用,文件不上传服务器。 操作简单:上传名单 → 一键生成压缩包。 智能处理:自动去除空行与非法字符。 使用步骤 准备文本文件,每行一个名称。 打开 批量文件夹生成器 上传文件并预览。 点击 生成 ZIP → 下载 → 解压即可。 核心代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 "use client" import type React from "react" import { useState, useCallback } from "react" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { Progress } from "@/components/ui/progress" import { Alert, AlertDescription } from "@/components/ui/alert" import { Badge } from "@/components/ui/badge" import { Upload, Download, FileText, Folder, AlertCircle, CheckCircle2, X } from "lucide-react" import { useToast } from "@/hooks/use-toast" import JSZip from "jszip" interface ProcessedFile { name: string content: string[] status: "pending" | "processing" | "completed" | "error" error?: string } export default function FolderGenerator() { const [files, setFiles] = useState<ProcessedFile[]>([]) const [isProcessing, setIsProcessing] = useState(false) const [progress, setProgress] = useState(0) const { toast } = useToast() const handleFileUpload = useCallback( (event: React.ChangeEvent<HTMLInputElement>) => { const uploadedFiles = Array.from(event.target.files || []) if (uploadedFiles.length === 0) return const newFiles: ProcessedFile[] = uploadedFiles.map((file) => ({ name: file.name, content: [], status: "pending", })) setFiles((prev) => [...prev, ...newFiles]) uploadedFiles.forEach((file, index) => { const reader = new FileReader() reader.onload = (e) => { try { const content = e.target?.result as string const lines = content .split(/\r?\n/) .map((line) => line.trim()) .filter((line) => line.length > 0) .filter((line) => /^[\u4e00-\u9fa5a-zA-Z0-9\s\-_()()]+$/.test(line)) setFiles((prev) => prev.map((f, i) => i === prev.length - uploadedFiles.length + index ? { ...f, content: lines, status: "completed" as const } : f, ), ) toast({ title: "解析成功", description: `${file.name} 共 ${lines.length} 个有效名称`, }) } catch (error) { setFiles((prev) => prev.map((f, i) => i === prev.length - uploadedFiles.length + index ? { ...f, status: "error" as const, error: "解析失败" } : f, ), ) toast({ title: "解析失败", description: `${file.name} 解析错误`, variant: "destructive", }) } } reader.readAsText(file, "utf-8") }) }, [toast], ) const removeFile = useCallback((index: number) => { setFiles((prev) => prev.filter((_, i) => i !== index)) }, []) const generateZip = useCallback(async () => { const validFiles = files.filter((f) => f.status === "completed" && f.content.length > 0) if (validFiles.length === 0) { toast({ title: "无有效文件", description: "请上传有效文本文件", variant: "destructive", }) return } setIsProcessing(true) setProgress(0) try { const zip = new JSZip() let totalItems = 0 let processedItems = 0 validFiles.forEach((file) => { totalItems += file.content.length }) for (const file of validFiles) { const baseName = file.name.replace(/\.[^/.]+$/, "") const fileFolder = zip.folder(baseName) if (fileFolder) { for (const name of file.content) { const folderName = name.replace(/[<>:"/\\|?*]/g, "_") const nameFolder = fileFolder.folder(folderName) if (nameFolder) { nameFolder.file("README.txt", `文件夹: ${name}\n创建时间: ${new Date().toLocaleString("zh-CN")}`) } processedItems++ setProgress((processedItems / totalItems) * 100) await new Promise((resolve) => setTimeout(resolve, 10)) } } } const content = await zip.generateAsync({ type: "blob" }) const url = URL.createObjectURL(content) const a = document.createElement("a") a.href = url a.download = `folders_${new Date().toISOString().slice(0, 10)}.zip` document.body.appendChild(a) a.click() document.body.removeChild(a) URL.revokeObjectURL(url) toast({ title: "生成成功", description: `已生成 ${totalItems} 个文件夹`, }) } catch (error) { toast({ title: "生成失败", description: "ZIP文件生成错误", variant: "destructive", }) } finally { setIsProcessing(false) setProgress(0) } }, [files, toast]) const totalFolders = files.reduce((sum, file) => sum + (file.content?.length || 0), 0) return ( <div className="min-h-screen bg-white dark:bg-gray-900"> <div className="container mx-auto px-6 py-12 max-w-3xl"> <div className="text-center mb-12"> <h1 className="text-4xl font-bold text-gray-900 dark:text-white mb-3 tracking-tight">文件夹生成器</h1> <p className="text-gray-600 dark:text-gray-400 text-lg">批量生成文件夹结构</p> </div> <Card className="mb-8 border-2"> <CardContent className="pt-8"> <div className="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-8 text-center hover:border-gray-400 transition-colors"> <input type="file" multiple accept=".txt,.csv,.text" onChange={handleFileUpload} className="hidden" id="file-upload" /> <label htmlFor="file-upload" className="cursor-pointer"> <Upload className="h-12 w-12 text-gray-400 mx-auto mb-4" /> <p className="font-semibold text-gray-700 dark:text-gray-300 mb-2 text-lg">选择文件</p> <p className="text-gray-500 dark:text-gray-400">支持 .txt, .csv 格式</p> </label> </div> </CardContent> </Card> {files.length > 0 && ( <Card className="mb-8 border-2"> <CardHeader className="pb-4"> <div className="flex items-center justify-between"> <CardTitle className="text-xl font-bold">文件列表 ({files.length})</CardTitle> <Badge variant="secondary" className="text-sm px-3 py-1"> {totalFolders} 个文件夹 </Badge> </div> </CardHeader> <CardContent className="space-y-4"> {files.map((file, index) => ( <div key={index} className="space-y-3"> <div className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border"> <div className="flex items-center gap-3"> {file.status === "completed" && <CheckCircle2 className="h-5 w-5 text-green-600" />} {file.status === "error" && <AlertCircle className="h-5 w-5 text-red-600" />} {file.status === "pending" && <FileText className="h-5 w-5 text-gray-400" />} <div> <p className="font-semibold">{file.name}</p> {file.status === "completed" && ( <p className="text-sm text-gray-600 dark:text-gray-400">{file.content.length} 个名称</p> )} {file.status === "error" && <p className="text-sm text-red-600">{file.error}</p>} </div> </div> <Button variant="ghost" size="sm" onClick={() => removeFile(index)}> <X className="h-4 w-4" /> </Button> </div> {file.status === "completed" && file.content.length > 0 && ( <div className="ml-6 p-4 bg-gray-100 dark:bg-gray-700 rounded-lg border"> <div className="font-semibold mb-3 text-gray-700 dark:text-gray-300">预览内容</div> <div className="h-32 overflow-y-auto space-y-2"> {file.content.slice(0, 20).map((name, i) => ( <div key={i} className="flex items-center gap-2 text-gray-600 dark:text-gray-400"> <Folder className="h-4 w-4" /> <span>{name}</span> </div> ))} {file.content.length > 20 && ( <div className="text-gray-500 italic mt-2">... 还有 {file.content.length - 20} 个</div> )} </div> </div> )} </div> ))} </CardContent> </Card> )} {files.some((f) => f.status === "completed") && ( <Card className="mb-8 border-2"> <CardContent className="pt-8"> {isProcessing && ( <div className="space-y-3 mb-6"> <div className="flex justify-between font-semibold"> <span>生成中...</span> <span>{Math.round(progress)}%</span> </div> <Progress value={progress} className="w-full h-3" /> </div> )} <Button onClick={generateZip} disabled={isProcessing} className="w-full h-12 text-lg font-semibold"> <Download className="h-5 w-5 mr-2" /> {isProcessing ? "生成中..." : "下载ZIP文件"} </Button> </CardContent> </Card> )} <Alert className="border-2"> <AlertCircle className="h-5 w-5" /> <AlertDescription className="text-base"> 支持 .txt/.csv 文件,每行一个名称。本地处理,保护隐私。 </AlertDescription> </Alert> <div className="text-center mt-12 pt-8 border-t border-gray-200 dark:border-gray-700"> <p className="text-sm text-gray-500 dark:text-gray-400"> by{" "} <a href="https://itxiaozhang.com" target="_blank" rel="noopener noreferrer" className="text-gray-600 dark:text-gray-300 hover:text-gray-800 dark:hover:text-gray-100 transition-colors" > IT小章 </a> </p> </div> </div> </div> ) } 方法三:本地 Python 脚本 优势 本地运行:无需联网,速度快。 功能丰富:支持去重、重命名、日志。 高度可定制:可随需修改。 多编码兼容:guess_read_text 支持多种常见编码。 文件名清洗:非法字符、Windows 保留名、长度限制等都处理到位。 并发执行:ThreadPoolExecutor 加速文件夹创建。 参数灵活:支持 --exist 策略、重命名模板、最大长度、dry-run。 使用方法 新建 name.txt,每行一个文件夹名称。 保存脚本为 create_dirs.py。 运行命令: 1 python create_dirs.py 全部代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 批量创建文件夹工具(单层,统一保存到 ./result/)。 - 从 name.txt 读取名称(一行一个),多编码兼容,清洗非法字符。 - 在当前目录下的 result 文件夹中生成对应同名文件夹。 """ from __future__ import annotations import argparse import csv import os import re import sys import time import unicodedata from concurrent.futures import ThreadPoolExecutor, as_completed from dataclasses import dataclass from pathlib import Path from threading import Lock from typing import List, Optional, Tuple # ---------- 常量 ---------- DEFAULT_INPUT = "name.txt" DEFAULT_MAX_LEN = 200 DEFAULT_EXIST = "skip" # skip | rename | fail DEFAULT_RENAME_PATTERN = "{name} ({n})" ENCODING_CANDIDATES = [ "utf-8-sig", "utf-8", "utf-16", "utf-16-le", "utf-16-be", "gb18030", "cp936", "big5", "shift_jis", "cp1252", "latin-1", ] WINDOWS_RESERVED_BASENAMES = { "CON", "PRN", "AUX", "NUL", *{f"COM{i}" for i in range(1, 10)}, *{f"LPT{i}" for i in range(1, 10)}, } ILLEGAL_CHARS_PATTERN = re.compile(r'[<>:"/\\|?*\x00-\x1F]') DOTS_ONLY_PATTERN = re.compile(r"^\.+$") # ---------- 数据结构 ---------- @dataclass class Item: line_no: int original: str cleaned: str @dataclass class Result: item: Item action: str # created | skipped | renamed | error | dryrun final_name: Optional[str] final_path: Optional[str] status: str # OK | ERROR error: Optional[str] # ---------- 工具函数 ---------- def guess_read_text(path: Path, forced_encoding: Optional[str] = None) -> Tuple[str, str]: if forced_encoding: return path.read_text(encoding=forced_encoding), forced_encoding data = path.read_bytes() last_err = None for enc in ENCODING_CANDIDATES: try: return data.decode(enc), enc except Exception as e: last_err = e continue raise RuntimeError(f"读取失败,未知编码;最后错误:{last_err}") def is_windows_reserved(name: str) -> bool: base = name.split(".", 1)[0] return base.upper() in WINDOWS_RESERVED_BASENAMES def sanitize_name(raw: str, max_len: int = DEFAULT_MAX_LEN) -> str: s = unicodedata.normalize("NFKC", raw).strip() if "/" in s or "\\" in s: s = s.replace("/", "_").replace("\\", "_") if ILLEGAL_CHARS_PATTERN.search(s): s = ILLEGAL_CHARS_PATTERN.sub("_", s) s = s.rstrip(" .") if DOTS_ONLY_PATTERN.match(s): return "" if is_windows_reserved(s): s = s + "_" if len(s) > max_len: s = s[:max_len] return s def parse_lines(text: str) -> List[Tuple[int, str]]: res = [] for idx, line in enumerate(text.splitlines(), start=1): raw = line.rstrip("\r\n") if not raw.strip(): continue if raw.lstrip().startswith("#"): continue res.append((idx, raw)) return res # ---------- 主执行逻辑 ---------- class App: def __init__(self, args: argparse.Namespace): self.args = args self.base = Path.cwd() / "result" self.base.mkdir(exist_ok=True) # 自动创建 result 文件夹 self.counter_ok = 0 self.counter_skip = 0 self.counter_err = 0 self.counter_renamed = 0 self.results: List[Result] = [] def log_line(self, result: Result): if self.args.quiet: return name = result.final_name or result.item.cleaned or result.item.original if result.status == "OK": if result.action == "created": print(f"[OK] {name}") elif result.action == "renamed": print(f"[OK] {name} (renamed)") elif result.action == "dryrun": print(f"[DRY] {name}") elif result.action == "skipped": print(f"[SKIP] {name} (exists)") else: reason = (result.error or "unknown").strip() print(f"[ERROR] {name} ({reason})") def make_one(self, item: Item) -> Result: base = self.base name = item.cleaned target = base / name if self.args.dry_run: return Result(item, "dryrun", name, str(target), "OK", None) try: target.mkdir(parents=False, exist_ok=False) return Result(item, "created", name, str(target), "OK", None) except FileExistsError: if self.args.exist == "skip": return Result(item, "skipped", name, str(target), "OK", None) elif self.args.exist == "fail": return Result(item, "error", name, str(target), "ERROR", "already_exists") elif self.args.exist == "rename": base_name = name n = 2 while True: try_name = self.args.rename_pattern.format(name=base_name, n=n) try_name = sanitize_name(try_name, self.args.max_name_length) if not try_name: return Result(item, "error", None, None, "ERROR", "rename_failed") try_path = base / try_name try: try_path.mkdir(parents=False, exist_ok=False) return Result(item, "renamed", try_name, str(try_path), "OK", None) except FileExistsError: n += 1 continue else: return Result(item, "error", name, str(target), "ERROR", "unknown_exist_strategy") except Exception as e: return Result(item, "error", name, str(target), "ERROR", str(e)) def run(self) -> int: start = time.time() inp = Path(self.args.input) if not inp.exists(): print(f"[FATAL] 输入文件不存在: {inp}", file=sys.stderr) return 3 try: text, used_enc = guess_read_text(inp, self.args.encoding) except Exception as e: print(f"[FATAL] 无法读取输入:{e}", file=sys.stderr) return 3 for line_no, raw in parse_lines(text): cleaned = sanitize_name(raw, self.args.max_name_length) if not cleaned: r = Result(Item(line_no, raw, cleaned), "error", None, None, "ERROR", "invalid_or_empty") self.log_line(r) self.results.append(r) self.counter_err += 1 continue self.results.append(Item(line_no, raw, cleaned)) # 并发执行 tasks = [] with ThreadPoolExecutor(max_workers=self.args.max_workers) as ex: future_map = {ex.submit(self.make_one, it): it for it in self.results if isinstance(it, Item)} for fut in as_completed(future_map): r = fut.result() self.log_line(r) if r.status == "OK": if r.action == "created": self.counter_ok += 1 elif r.action == "skipped": self.counter_skip += 1 elif r.action == "renamed": self.counter_renamed += 1 else: self.counter_err += 1 elapsed = time.time() - start total_ok = self.counter_ok + self.counter_renamed print(f"总结: 成功 {total_ok}, 跳过 {self.counter_skip}, 错误 {self.counter_err}, 用时 {elapsed:.2f}s") if self.counter_err > 0 and (total_ok > 0 or self.counter_skip > 0): return 2 elif self.counter_err > 0: return 3 else: return 0 # ---------- 参数 ---------- def build_argparser() -> argparse.ArgumentParser: p = argparse.ArgumentParser(description="从 name.txt 批量创建文件夹,保存到 ./result/") p.add_argument("--input", default=DEFAULT_INPUT, help="输入文件(默认 name.txt)") p.add_argument("--encoding", default=None, help="指定输入编码") p.add_argument("--exist", choices=["skip", "rename", "fail"], default=DEFAULT_EXIST, help="已存在处理策略(默认 skip)") p.add_argument("--rename-pattern", default=DEFAULT_RENAME_PATTERN, help="重命名模板,含 {name} 和 {n}") p.add_argument("--max-name-length", type=int, default=DEFAULT_MAX_LEN, help="文件夹名最大长度(默认200)") default_workers = min(8, (os.cpu_count() or 2) * 4) p.add_argument("--max-workers", type=int, default=default_workers, help=f"并发线程数(默认 {default_workers})") p.add_argument("--dry-run", action="store_true", help="仅预演,不创建目录") p.add_argument("--quiet", action="store_true", help="仅输出汇总") return p def main(argv: Optional[List[str]] = None) -> int: args = build_argparser().parse_args(argv) app = App(args) code = app.run() return code if __name__ == "__main__": sys.exit(main()) 视频版本 哔哩哔哩 YouTube