按模板批量创建文件夹并保留结构,这个需求听起来不起眼,但真正做项目初始化、数据归档和文档知识库整理的人都知道,它比想象中更容易出问题。最典型的场景是:你手头有一份标准目录结构,几十个文件夹,有嵌套层级、有命名规则、还有一部分空目录必须存在。如果靠右键一个个新建,不仅慢,还容易漏掉子目录,或者把 src/main/java 这种路径拆成两层建错位置。这篇文章直接给出几条可复现的路线:如何设计模板、如何用 Python 或脚本批量生成、如何验证结构完整、以及重复运行时会遇到哪些坑。
我建议先明确一点:所谓“批量创建文件夹”,重点不是一次性生成很多文件夹,而是按预先定义的模板生成,并且生成后目录层级和模板保持一致。下面按实际落地顺序拆开讲。
1. 为什么按模板批量创建文件夹不是一个小问题
先说结论:批量创建目录本身很简单,难的是“按模板”和“保留结构”这两件事同时成立。
1.1 项目初始化、数据归档、知识库这三种场景最容易遇到
以开发项目为例,很多团队会统一要求新项目使用固定目录结构。比如初始化一个 FastAPI 项目,常见结构可能是 app/api、app/models、app/services、app/schemas、app/core、tests;如果是 Spring Boot 风格的 Java 项目,则可能是 src/main/java/com/xxx/controller、src/main/java/com/xxx/service、src/main/resources、src/test/java。这种结构不是随便建几个文件夹就行,层级、命名、位置都影响后续代码引用和构建配置。
第二种常见场景是数据归档。按年、月、日建目录,按客户、渠道、业务类型建目录,再往下拆出 原始数据、处理结果、导出报告 等子目录。一次任务可能涉及上百个目录,而且这些目录必须在任务开始前就全部存在,否则下游脚本跑一半会因为找不到输出目录而中断。
第三种是文档知识库。团队内部经常需要统一“分类目录 + 附件目录 + 导出目录”的标准结构,每个项目都保持同样骨架。这时候如果没有模板,每个项目经理凭记忆建目录,时间一长结构就漂移了,后续检索和自动化脚本都很难统一。
1.2 手动建目录的最大问题不是慢,而是结构不一致
手动建目录最直接的缺点是慢。但更严重的问题是结构不一致。同一个项目,A 建的是 docs/api,B 建的是 docs/API,C 可能建成了 doc/api。目录一旦散落产生偏差,后续所有依赖该路径的脚本都会出问题。
结构不一致还体现在另一个容易被忽略的点:很多人新建目录时只建了一级,没有把完整的多级路径一次建好。比如模板里要求 src/main/java/com/example/service,手动操作时容易只建了 src/main,然后把后面的层级全部漏掉。这个现象在目录层级越深时越常见。
所以“保留结构”的真正含义,不是“目录名相同”,而是完整的相对路径也要一致。模板里写的是什么路径,生成结果中就应该是同样的路径。这要求创建工具必须支持递归创建父目录,而不是单纯 mkdir 一个普通的单层文件夹。
注意:模板里统一使用相对路径。不要把一个绝对路径或者
..放进模板,否则脚本会把目录创建到目标根目录之外,这种错误一旦发生,日志里很难察觉。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 模板格式决定你能走多远
批量创建文件夹的第一步不是写脚本,而是设计模板。模板本身就是你的需求文档,也是脚本的输入。模板格式不对,脚本写得再好也没用。
2.1 文本模板:一行一个相对路径,先把结构固定下来
最朴素也最适合入门的是纯文本模板,每行写一个相对路径,用 / 表示层级,用 # 开头表示注释,空行会被跳过。示例:
text复制# 项目基础目录
src/main/java/com/example/controller
src/main/java/com/example/service
src/main/java/com/example/mapper
src/main/resources/mapper
src/main/resources/static
src/test/java/com/example
docs/api
docs/design
scripts
data/raw
data/processed
output
这种格式的好处是直观,任何编辑器都能打开,也方便放进 Git 仓库做版本管理。创建时,脚本按行读取,把每一行当成一个相对路径,从目标根目录开始逐级创建。
这个模板格式里最容易踩的坑是行尾空格和行首空格。很多人在复制文本时会把路径后面多一个空格,或者缩进不一致。脚本里如果没有 strip 处理,就会生成 docs/api 这种带空格的目录名。更麻烦的是看不出来,因为资源管理器里可能不会直接显示末尾空格。
另一个坑是路径分隔符。模板里统一使用 /,不要在 Windows 下写 src\main\java。因为模板可能换到 Linux 环境使用,反斜杠在 Linux 下会被当成普通文件名的一部分。如果确实拿到了一份用反斜杠写的模板,我会在脚本里先做一次 .replace("\\", "/") 再创建。
2.2 从已有目录反推模板:用 find 或 PowerShell 导出
如果团队已经有一个跑得很好的标准项目,最好的办法不是手写模板,而是直接从这个项目的目录结构反推模板。
Linux 和 macOS 下可以这样导出一份相对路径列表:
bash复制find . -type d ! -path './.git/*' ! -path '.' | sed 's|^\./||' | sort > dirs_template.txt
执行后,dirs_template.txt 里就是当前项目中所有子目录的相对路径。.git 目录需要排除,因为新建项目时你不希望脚本给每个项目都创建一个旧的 .git 目录。
Windows PowerShell 下可以用类似思路:
powershell复制Get-ChildItem -Directory -Recurse | Where-Object {
$_.FullName -notmatch '\\.git\\'
} | ForEach-Object {
$_.FullName.Substring((Get-Location).Path.Length + 1)
} | Set-Content -Encoding utf8 dirs_template.txt
这个命令会把当前目录下的所有子目录转换成相对路径并输出到模板文件。需要注意,Windows 生成的 UTF-8 文件有时带 BOM,Python 读取时如果出现第一个路径乱码,把 encoding="utf-8" 改成 encoding="utf-8-sig" 即可。
从这里导出的模板通常已经包含完整层级,即使某些目录在模板历史里已经不存在,也能保证新建项目时结构与参考项目一致。
2.3 增加模板字符串变量:从一套模板生成多套结构
纯文本模板的不足是“写死”了目录名。比如要创建用户模块、订单模块、支付模块三套结构,每套都要有一个 controller、service、dao 目录,那就会出现重复的行。
更好的方式是在模板里放占位符,也就是模板字符串。示例:
text复制src/{module}/controller
src/{module}/service
src/{module}/dao
src/{module}/entity
tests/{module}
创建时用脚本里的参数替换掉 {module},比如传入 user,就生成 src/user/controller;传入 order,就生成 src/order/controller。这样一套模板可以生成多套结构,目录命名也统一。
不过我不建议一开始就引入太多占位符。变量越多,脚本需要做的校验越多,模板的可读性也会下降。最稳妥的顺序是先固定一套完整结构,跑通以后再把确实会变化的部分变量化。
3. Python 标准库脚本:把文本模板变成目录树
Python 是最适合做这件事的,因为标准库 pathlib 已经提供了跨平台处理路径的能力,不需要额外安装第三方包。
3.1 最小可运行的创建脚本
先写一个最小版本,功能就是读取文本模板,逐行创建目录:
python复制from pathlib import Path
def create_from_template(template_path: str, base_dir: str) -> None:
base = Path(base_dir)
with open(template_path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
target = base / line
target.mkdir(parents=True, exist_ok=True)
print(f"[OK] {target}")
if __name__ == "__main__":
create_from_template("dirs_template.txt", "new_project")
这个脚本里最关键的三个点:
一是 base / line。pathlib 会正确处理路径合并,不管当前系统是 Windows 还是 Linux,都能保留模板中的层级。
二是 parents=True。它告诉 Python 创建目录时自动补全所有父目录。模板里写 src/main/java/com/example/service,执行后会把 src、src/main、src/main/java 等层级全部创建出来,不会出现只建一层的情况。这正是“保留结构”的核心。
三是 exist_ok=True。目录已经存在时不报错,脚本可以重复执行。很多批量任务不是只跑一次,而是在新增目录后继续跑,幂等很重要。
注意:第一次运行先在临时目录里试。确认脚本输出符合预期后,再在真实目标根目录执行。不要假设模板一定没问题,很多时候问题出在模板里的某一行路径写错了。
3.2 加入创建统计、日志和空目录占位
最小脚本能跑通,但输出太简单。真实批量任务里,我更建议增加统计和日志,把“新建”和“已存在”区分开:
python复制from pathlib import Path
def create_from_template(template_path: str, base_dir: str) -> None:
base = Path(base_dir)
created = []
skipped = []
with open(template_path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
target = base / line
if target.exists():
skipped.append(str(target))
else:
target.mkdir(parents=True, exist_ok=True)
created.append(str(target))
print(f"created: {len(created)}")
print(f"skipped: {len(skipped)}")
加了统计之后,你可以快速判断是不是有大量路径被跳过。比如模板里有一行 src/{module}/controller 没有替换变量,脚本会直接创建名为 {module} 的目录,但统计里看不出来。所以还要在日志里把每个实际创建的路径打印出来,或者至少把含可疑字符的路径列出来。
如果这些空目录要提交到 Git 仓库,还要考虑 .gitkeep 占位。因为 Git 不跟踪空目录,目录建好了,提交代码后别人一拉取,空目录全部消失。常用的解决办法是在每个空目录下放一个空文件 .gitkeep。Python 里可以做这样一个补充步骤:
python复制def ensure_gitkeep(root: Path) -> None:
for p in root.rglob("*"):
if p.is_dir() and not any(p.iterdir()):
(p / ".gitkeep").write_text("", encoding="utf-8")
注意这一步只对确实需要保留空目录的场景做。如果后续会有文件写入该目录,加 .gitkeep 会影响项目里“目录是否为空”的判断逻辑,需要评估。
3.3 重复运行、路径校验和安全边界
批量创建目录有一个容易被忽略的边界:目录创建是幂等操作,已经存在的目录不会被脚本清空,也不会覆盖里面的文件。所以重复运行脚本是安全的。
但幂等不等于没有风险。如果模板里混入了绝对路径,比如 /etc/xxx 或者 C:\Windows\xxx,脚本也会照建。如果在没有权限的目录下创建,Python 会抛 PermissionError。所以模板必须是可预期输入的文本。
我建议在脚本里加一层路径校验,拒绝明显越界的内容:
python复制from pathlib import Path
def safe_relative_path(line: str) -> str | None:
line = line.strip()
if not line or line.startswith("#"):
return None
line = line.replace("\\", "/")
p = Path(line)
if p.is_absolute() or ".." in p.parts:
print(f"[SKIP] 非法路径: {line}")
return None
return line
模板里出现 .. 尤其危险。因为 base / "src/../other" 最终会指向 base/other,目录结构会和模板预期不一致。虽然不会真的跑出根目录,但会制造很难排查的错位路径。
4. CSV/Excel 模板适合更复杂的目录清单
文本模板适合目录数量在几十个以内的场景。如果目录数量上百、需要多人维护、还要附带备注和类型标记,就可以升级到 CSV 或 Excel 模板。
4.1 CSV 模板字段与表格设计
CSV 可以直接用 Excel 编辑,也能放进项目仓库做版本管理。我通常至少保留两列:relative_path 和 remark。
csv复制relative_path,remark,need_gitkeep
src/main/java,Java源码目录,no
src/main/resources,资源配置目录,no
docs/api,接口文档,no
logs,运行日志目录,yes
tmp,临时文件目录,yes
字段本身可以根据需要扩展。比如增加 project_type,用于区分后端项目、前端项目、数据项目;增加 need_gitkeep,告诉脚本创建完目录后是否需要补空文件占位;增加 owner 或 description,方便其他人理解每个目录的用途。
这种表格的价值不只是给脚本看,更是给团队看的。看到 relative_path 只能知道“建什么目录”,看到 remark 才能知道“这个目录为什么存在,里面应该放什么”。没有备注的目录模板,过半年再看可能连创建者自己都想不起用途。
4.2 用 Python 读取 CSV/Excel 再批量创建
读取 CSV 用标准库 csv 就够了。需要注意 Windows 下用 Excel 编辑过的 CSV 可能是 UTF-8 with BOM,所以读取时用 encoding="utf-8-sig" 比较稳妥。
python复制import csv
from pathlib import Path
def create_from_csv(csv_path: str, base_dir: str) -> None:
base = Path(base_dir)
with open(csv_path, encoding="utf-8-sig") as f:
reader = csv.DictReader(f)
for row in reader:
rel = (row.get("relative_path") or "").strip()
if not rel or rel.startswith("#"):
continue
target = base / rel
target.mkdir(parents=True, exist_ok=True)
need_gitkeep = (row.get("need_gitkeep") or "no").lower()
if need_gitkeep in ("yes", "true", "1"):
(target / ".gitkeep").write_text("", encoding="utf-8")
如果要直接读取 .xlsx,标准库不够用,需要安装 openpyxl。读取前先确认表格第一行是字段名,路径列不要有合并单元格,否则行内容会变成 None:
python复制from pathlib import Path
from openpyxl import load_workbook
def create_from_xlsx(xlsx_path: str, base_dir: str) -> None:
base = Path(base_dir)
wb = load_workbook(xlsx_path, read_only=True)
ws = wb.active
header = [str(c.value).strip() if c.value else "" for c in ws[1]]
rel_idx = header.index("relative_path")
for row in ws.iter_rows(min_row=2, values_only=True):
rel = str(row[rel_idx]).strip() if row[rel_idx] else ""
if rel:
(base / rel).mkdir(parents=True, exist_ok=True)
wb.close()
这个脚本比文本模板版本多了几行,但换来的是目录模板可以集中在一个表格里维护,新增目录时不需要改脚本,改表就行。
4.3 什么时候该从文本模板切到表格模板
我判断的标准很简单:模板行数超过 50,或者有两个人以上需要维护这个模板,就切到 CSV/Excel。原因是纯文本模板虽然直观,但缺少备注位置,也难做筛选和排序。Excel 里可以按目录层级排序,可以按模块筛选,可以补一列 remark 说明用途,维护体验明显更好。
但如果模板只有十到二十行,我还是会选文本模板。因为它可以直接放进仓库,Git 差异对比非常清楚;Excel 一旦被另存为 .xlsx,Git 默认就没法看内容差异,评审时还要打开文件,反而麻烦。
5. 没有 Python 也行的 Shell 和 PowerShell 方案
有些服务器没有 Python,或者只是想快速用命令行创建一次,不打算引入脚本文件。这种情况下用 Shell 或 PowerShell 就够。
5.1 Linux/macOS:while + mkdir -p 处理多层结构
Linux 和 macOS 下,目录创建命令是 mkdir -p,这个参数保证了父目录会被递归创建,和 Python 的 parents=True 效果一致。
bash复制base_dir="/workspace/new_project"
trim() {
local s="$1"
s="${s#"${s%%[![:space:]]*}"}"
s="${s%"${s##*[![:space:]]}"}"
printf '%s' "$s"
}
while IFS= read -r raw_line || [ -n "$raw_line" ]; do
line=$(trim "$raw_line")
case "$line" in
""|\#*) continue ;;
esac
mkdir -p "$base_dir/$line"
done < dirs_template.txt
这个脚本里,trim 函数用来去掉行首和行尾空格。直接裸写 mkdir -p "$base_dir/$line" 也能跑,但行尾多一个空格时目录名就会出现奇怪后缀,所以处理空格是值得的。
case "$line" in ""|\#*) continue ;; esac 负责跳过空行和以 # 开头的注释行。注释在模板里很重要,尤其是当模板要交给别人维护时。
5.2 Windows PowerShell:New-Item -Force 创建目录
PowerShell 创建目录用 New-Item -ItemType Directory。-Force 参数的作用是目录已存在时不报错,相当于 Python 的 exist_ok=True。
powershell复制$base = "C:\workspace\new_project"
Get-Content "dirs_template.txt" -Encoding UTF8 | ForEach-Object {
$line = $_.Trim()
if ($line -and -not $line.StartsWith("#")) {
New-Item -ItemType Directory -Force -Path (Join-Path $base $line)
}
}
这里注意两点。
一是 Join-Path 比直接写字符串拼路径更稳,尤其是模板路径来自文件时,斜杠方向可能不一致,Join-Path 会按系统规则处理。
二是如果模板文件是 ANSI 编码,Get-Content -Encoding UTF8 读出来会乱码。解决方法是先把模板文件另存为 UTF-8,或者去掉 -Encoding UTF8,让 PowerShell 按默认编码读取。
5.3 文件管理器里自带批量建目录,但别依赖它做自动化
不少文件管理器提供了“从剪贴板创建目录树”或“导入文本模板”的功能,操作方式是复制一段每行一个相对路径的文本,然后在目标目录粘贴并选择创建目录树。这类功能适合偶尔用一次的场景,比如临时拿到一份目录清单,不想开命令行。
但它不适合长期自动化。原因有三个:一是操作界面不统一,不同软件支持的语法不一样;二是很难做日志和审计,创建了什么、跳过了什么没有记录;三是无法纳入项目仓库,模板和脚本之间没有明确的版本关联。
我一般会把这类功能当作辅助手段,正式的批量创建还是用脚本。因为脚本可以被别的人重复执行,也能在 CI 或初始化工具里被调用。
6. 创建之后怎么验证、排查和让模板长期可用
创建完目录不等于结束。更重要的步骤是验证生成结果是否和模板一致,以及在出问题时能快速找到原因。
6.1 验证模板中的每条相对路径都真实存在
最简单的验证方式是用 find 导出当前目录结构,再和模板做对比。
Linux/macOS:
bash复制find . -type d | sort
Windows 命令行:
cmd复制dir /s /ad /b
我在实际中更常用一个 Python 验证脚本:读取模板文件,逐行检查目标根目录下是否存在对应路径。这样能在一条命令里完成全部检查,并且明确输出缺失项。
python复制from pathlib import Path
def verify_template(template_path: str, base_dir: str) -> None:
base = Path(base_dir)
missing = []
with open(template_path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
target = base / line
if not target.is_dir():
missing.append(str(target))
if missing:
print("以下目录缺失:")
for item in missing:
print(item)
else:
print("模板结构完整,所有路径均存在")
验证时建议连工作目录一起检查。很多人在脚本里用相对路径,但执行时 base_dir 写的是 ./new_project,输出却在别的地方,根源是“当前目录和你以为的当前目录不一致”。所以脚本如果依赖相对路径,建议在开头打印一次 Path.cwd()。
6.2 常见问题排查:先看现象,再按输入、路径、权限、脚本顺序查
批量创建目录出问题时,大多数不是脚本本身逻辑错误,而是输入和环境问题。下面这张表是常见现象和优先排查顺序。
| 现象 | 可能原因 | 优先排查 |
|---|---|---|
| 没有创建任何目录 | base_dir 拼错、模板文件路径不对、脚本没有读到文件 |
先打印模板路径和绝对目标路径 |
| 只创建了一部分 | 模板行尾有不可见字符、某一行权限不足、模板中间有非法路径把脚本带崩 | 逐行打印原始内容,看处理后的路径 |
| 目录层级少了 | 使用了不带 -p 或 parents=True 的创建命令 |
检查创建命令是否递归创建父目录 |
| 中文路径乱码 | 模板文件编码不是 UTF-8,或 Windows 下带 BOM | 换 utf-8-sig,或先转码 |
| Windows 下生成带反斜杠的多层目录 | 模板里用了 \,脚本没有统一转换 |
模板统一用 /,脚本里做替换 |
| 创建到了目标根目录之外 | 模板里出现了绝对路径或 .. |
加入路径校验,拒绝非法相对路径 |
| 空目录提交后消失 | 版本控制系统不跟踪空目录 | 按需添加 .gitkeep 占位文件 |
注意:当批量任务出问题时,不要一上来就改脚本、调参数。先确定三类信息:模板文件内容是否完整、目标根目录是否可写、脚本实际处理后的路径列表是什么。多数报错都可以在日志里找到位置,真正需要改代码的反而很少。
6.3 让模板进入仓库:版本化、注释和订阅规则
最后一个建议是把模板和脚本一起放进项目仓库,而不是只停留在个人电脑里。
模板可以放在项目根目录的 .templates/dirs.txt,或者放在 scripts/dirs_template.txt。至少需要配套一个简短的 README,说明这个模板适用的项目类型、创建规则和更新方式。没有说明的目录模板,三个月后就没人敢改,因为不知道动了哪一行会影响哪个项目。
版本化还有一个好处:目录结构变更可以被 Git 记录下来。今天在模板里加了一个 data/external 目录,提交记录里看得到;这不是什么了不起的功能,但非常重要。目录结构一旦因为项目变大而调整,你能知道是谁、在什么时间、基于什么需求改的。
自动化方面,如果公司已经有项目初始化工具,可以让它调用模板脚本;如果没有,就在本地跑。我见过有人把目录创建脚本绑定到每次部署流程里,反而带来了副作用。目录模板通常只在项目初始化阶段需要执行,不需要在每次发布时重复运行,尤其是有 exist_ok=True 的脚本,虽然不会报错,但会掩盖真实目录状态。
真正用熟之后你会发现,批量创建文件夹这件事最大的价值不在省那点右键时间,而是把目录规范从个人记忆变成了仓库里的一条可审计记录。模板有人维护,脚本能重复执行,创建完有验证,后续新增目录只会越来越有规则,而不是越来越乱。
