批量新建文件夹的三种简便方法

原文地址:https://itxiaozhang.com/batch-create-folders/
本文配合视频食用效果最佳,视频版本在文章末尾。

📌 常见场景

  1. 员工资料:公司新入职 200 名员工,需要批量生成个人资料文件夹。
  2. 学生作业:某班级 60 名学生上交作业,老师按花名册批量生成作业文件夹。
  3. 项目成员:一个跨部门项目组有 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

方法二:纯前端在线工具

特点

  • 无需安装:网页直接用,文件不上传服务器。
  • 操作简单:上传名单 → 一键生成压缩包。
  • 智能处理:自动去除空行与非法字符。

使用步骤

  1. 准备文本文件,每行一个名称。
  2. 打开 批量文件夹生成器
  3. 上传文件并预览。
  4. 点击 生成 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。

使用方法

  1. 新建 name.txt,每行一个文件夹名称。
  2. 保存脚本为 create_dirs.py
  3. 运行命令:
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())

视频版本


▶ 可以在关于或者这篇文章找到我的联系方式。
▶ 本网站的部分内容可能来源于网络,仅供大家学习与参考,如有侵权请联系我核实删除。
我是小章,目前全职提供电脑维修和IT咨询服务。如果您有任何电脑相关的问题,都可以问我噢。


批量新建文件夹的三种简便方法
https://itxiaozhang.com/batch-create-folders/
作者
小章
发布于
2025年8月20日
许可协议