背景:某些框架的“脚手架缺失”
可能对于很多人来说不是难题,对于我来说 ,用习惯了django springboot3 等 遇到fastAPI这种 有工具的 也可以 通过一些标准库 不过总有一些时候 有一些比较轻量的框架没有脚手架
pipinstallfastapi-scaff fastapi-scaff new backend-fastapi Starting new project... Done. Now run:>1.cdbackend-fastapi>2. modify config, eg: db>3. pipinstall-r requirements.txt>4. python runserver.py ----- More see README.md -----本文商业互吹价值在于: 当轻量框架缺乏脚手架,一个基于 Python 标准库的通用工具如何优雅地解决项目初始化之痛…
真实价值: 三天不练手生,没事写点东西,不至于太生疏对于IT工作者来说。
聚焦脚本设计:五个关键考量点
不关注过程的直接跳过 ,完整脚本在本文最后
1. 零依赖原则
只用Python标准库,意味着用户无需安装任何额外包,真正做到“下载即用”。这个选择背后的考虑是:脚手架工具本身应该是轻量级的,不应该成为项目的负担。
# 仅需三个标准库importosimportsysimportargparse2. 跨平台兼容性处理
Windows与Linux/macOS的差异需要特别处理:
- 路径分隔符:使用
os.path.join()自动处理/和\ - 文件编码:依次尝试UTF-8、GBK编码读取文件
- 控制台输出:避免使用可能在某些终端显示异常的Unicode字符
# 智能编码检测defread_file_safely(filename):encodings=['utf-8','gbk','utf-16']forencodinginencodings:try:withopen(filename,'r',encoding=encoding)asf:returnf.read()exceptUnicodeDecodeError:continueraiseValueError(f"无法解码文件{filename}")3. 帮助信息与用户体验
详细的帮助信息是命令行工具的门面。我设计了多级帮助:
- 简要用法:运行
python create_tree.py -h查看 - 示例说明:展示最常见的几种用法
- 参数详解:每个参数的作用和默认值
# 清晰的帮助系统$ python create_tree.py -h 使用方法: create_tree.py[选项]选项: -f, --file FILE 指定tree.txt文件路径(默认: tree.txt)--dry-run 只预览不实际创建 -v, --verbose 显示详细过程 -y, --yes 跳过确认直接创建 -h, --help 显示此帮助信息 示例: create_tree.py --dry-run# 安全预览create_tree.py -v# 详细模式create_tree.py -y# 跳过确认直接创建4. 幂等性保证
好的工具应该可以安全地反复运行。我确保了:
- 已存在的目录不会被重复创建
- 已有的文件不会被意外覆盖
- 随时可以重新运行以补全缺失的结构
# 安全的目录创建os.makedirs(path,exist_ok=True)# 关键:exist_ok=True# 安全的文件创建ifnotos.path.exists(filepath):withopen(filepath,'w',encoding='utf-8')asf:f.write('')# 只创建空文件,不覆盖内容5. 交互式确认机制
为了防止误操作,我添加了多层确认:
- 解析完成后显示预览
- 用户确认无误后才开始创建
- 支持
-y参数跳过确认(用于自动化脚本)
核心技术:栈式解析算法
脚本的核心是理解tree格式的层级结构。我采用了一种栈式解析算法,模拟人类阅读树形结构的方式:
defparse_tree_structure(content):stack=[]# 栈存储(路径, 层级)items=[]forlineincontent.splitlines():level=calculate_indent_level(line)name=extract_item_name(line)is_dir=name.endswith('/')# 关键:通过栈找到正确的父目录whilestackandstack[-1][1]>=level:stack.pop()# 构建完整路径parent=stack[-1][0]ifstackelse''full_path=os.path.join(parent,name)items.append((full_path,is_dir))ifis_dir:stack.append((full_path,level))returnitems这种算法的优势在于:
- 线性时间复杂度:只需遍历一次文本
- 内存效率高:栈的深度就是目录的最大嵌套层级
- 容错性强:即使输入格式略有偏差也能正确处理
为什么选择tree格式而非Jinja2模板?
在技术选型时,我仔细考虑过使用Jinja2模板引擎。Jinja2确实强大,但最终我选择了更简单的tree格式,原因如下:
Jinja2的过度设计问题
# Jinja2方案需要这样fromjinja2importEnvironment,FileSystemLoader env=Environment(loader=FileSystemLoader('templates'))template=env.get_template('project_structure.j2')output=template.render(project_name='my_project')# 然后还需要解析输出,创建文件...Jinja2方案带来的复杂性:
- 双重依赖:需要Jinja2库和模板文件
- 学习成本:团队成员需要了解Jinja2语法
- 维护负担:模板文件需要额外维护
tree格式的独特优势
相比之下,tree格式(tree命令的输出)具有以下优势:
1. 人类可读与机器可读的统一
my-project/ ├── src/ │ └── main.py └── README.md这种格式既能让开发者直观理解结构,又能被脚本精确解析。
2. 广泛的工具支持
- Linux/macOS自带
tree命令 - VS Code有目录树插件
- 许多IDE可以导出目录结构
3. 零学习成本
任何开发者都能立即理解,无需学习新的模板语法。
4. 版本控制友好
纯文本格式,diff清晰,合并冲突容易解决。
实用主义的选择
考虑到脚手架工具的核心需求是创建目录和空文件,而不是生成复杂的内容,简单的tree格式完全够用。这正是“技术内敛”理念的体现——用最简单的技术解决核心问题。
使用方式:三步完成项目初始化
第一步:创建结构蓝图
使用任何工具生成项目结构描述:
# Linux/macOStree -I'__pycache__|node_modules'>tree.txt# 或手动创建vimtree.txt第二步:预览验证
# 安全第一,先预览python create_tree.py --dry-run -v# 输出示例========================================解析结果预览========================================📁 vue-fastapi-demo(目录)📁 backend(目录)📄 .env(文件)📄 requirements.txt(文件)📁 models(目录)📄 __init__.py(文件)📄 user.py(文件)...========================================第三步:一键创建
# 确认无误后创建python create_tree.py# 或跳过确认直接创建(适合CI/CD)python create_tree.py -y对症易懂,炫技则空;技术内敛,体验外显。
这十六个字不仅总结了这次工具开发的体会,也指引着我在技术道路上的每一次选择。
完整脚本
#!/usr/bin/env python3""" 目录结构创建工具 - 稳定解析版 (修正) 专注解决 tree.txt 解析问题,确保创建正确的目录结构 """importosimportsysimportargparsedefparse_tree_structure(content):""" 核心解析函数:正确解析 tree 命令的输出格式 返回列表,每个元素为 (完整路径, 是否是目录) """lines=[line.rstrip('\n\r')forlineincontent.split('\n')]items=[]ifnotlinesornotlines[0].strip():returnitems# 1. 处理根目录(第一行)root_line=lines[0].strip()root_name=root_line.rstrip('/')items.append((root_name,True))# 栈记录当前路径和层级:[(路径, 层级), ...]stack=[(root_name,0)]forline_num,lineinenumerate(lines[1:],start=2):line=line.rstrip()ifnotline.strip():continue# 2. 计算当前行的缩进层级# tree 格式:每层用 4 个字符indent=0i=0whilei<len(line):char=line[i]ifcharin' \t':indent+=1i+=1elifi+1<len(line)andline[i:i+2]in('│ ','├─','└─'):# tree图形字符组合indent+=2i+=2elifcharin('│','├','└'):# 单独的tree图形字符indent+=1i+=1else:break# 3. 清理行内容,提取项目名称clean_line=line[i:].lstrip('─ ')# 移除可能剩余的 ──# 如果有"── "前缀,移除它ifclean_line.startswith('── '):clean_line=clean_line[3:]# 移除注释if'#'inclean_line:clean_line=clean_line.split('#')[0].strip()clean_line=clean_line.strip()ifnotclean_line:continue# 4. 判断是目录还是文件item_name=clean_line is_dir=Falseifitem_name.endswith('/'):is_dir=Trueitem_name=item_name.rstrip('/')else:# 通过常见规则判断basename=item_name# 判断条件优先级:# 1. 以点开头的特殊文件(如 .env)ifbasename.startswith('.'):is_dir=False# 2. 有常见扩展名的是文件elifany(basename.endswith(ext)forextin['.py','.txt','.js','.ts','.vue','.html','.json','.css','.md','.config.ts']):is_dir=False# 3. 特殊文件名elifbasenamein['__init__','package','vite.config']:is_dir=False# 4. 没有扩展名且不是特殊文件的,可能是目录elif'.'notinbasename:is_dir=True# 5. 其他情况默认是文件else:is_dir=False# 5. 计算当前层级(tree 格式通常每4字符一级)level=indent//4# 6. 根据层级找到正确的父目录whilestackandstack[-1][1]>=level:stack.pop()ifstack:parent_path,_=stack[-1]full_path=os.path.join(parent_path,item_name)else:full_path=os.path.join(root_name,item_name)items.append((full_path,is_dir))# 7. 如果是目录,压入栈ifis_dir:stack.append((full_path,level))returnitemsdefpreview_structure(items,show_numbers=True):"""预览解析结果,使用清晰的层级显示"""ifnotitems:print("未解析到任何项目")return0,0print("\n"+"="*60)print("解析结果预览")print("="*60)# 为每个项目生成编号item_map={}foridx,(path,is_dir)inenumerate(items):item_map[idx]=(path,is_dir)root_name=items[0][0]ifitemselse""foridx,(path,is_dir)inenumerate(items):# 计算缩进层级ifidx==0:level=0else:# 通过计算路径中分隔符的数量来确定层级level=path.count(os.sep)-root_name.count(os.sep)indent=" "*level# Windows兼容性:如果控制台不支持Unicode,使用简单字符try:# 尝试输出Unicode字符icon="📁"ifis_direlse"📄"sys.stdout.write("")# 测试Unicode支持except:icon="[DIR]"ifis_direlse"[FILE]"type_desc="目录"ifis_direlse"文件"# 显示名称name=os.path.basename(path)iflevel>0orpath!=root_nameelsepath# 显示编号(如果需要)ifshow_numbers:# 生成层级编号:A, A.1, A.1.1, B, B.1 等ifidx==0:num_str="ROOT"else:# 简单数字编号num_str=f"[{idx:2d}]"print(f"{indent}{num_str}{icon}{name}({type_desc})")else:print(f"{indent}{icon}{name}({type_desc})")print("="*60)# 统计信息dirs=sum(1for_,is_dirinitemsifis_dir)files=sum(1for_,is_dirinitemsifnotis_dir)print(f"\n📊 统计信息:")print(f" 根目录:{root_name}")print(f" 目录数:{dirs}")print(f" 文件数:{files}")print(f" 总项目:{len(items)}")# 特别检查关键结构backend_path=os.path.join(root_name,"backend")frontend_path=os.path.join(root_name,"frontend")backend_exists=any(path==backend_pathforpath,_initems)frontend_exists=any(path==frontend_pathforpath,_initems)ifbackend_existsandfrontend_exists:print(f"\n✅ 关键结构检测:")print(f" -{backend_path}/")print(f" -{frontend_path}/")print(" (backend 和 frontend 为平级目录)")elifbackend_existsorfrontend_exists:print(f"\n⚠️ 注意: 只检测到部分结构")returndirs,filesdefget_user_confirmation(prompt,default_no=True):"""获取用户确认"""try:options=" (y/N)"ifdefault_noelse" (Y/n)"response=input(f"{prompt}{options}: ").strip().lower()ifdefault_no:returnresponsein('y','yes','是','1')else:returnresponsenotin('n','no','否','0')except(KeyboardInterrupt,EOFError):returnFalsedefcreate_structure(items,verbose=False,dry_run=False):"""创建目录和文件结构"""created=[]existing=[]errors=[]print("\n开始创建结构..."+(" (模拟运行)"ifdry_runelse""))forpath,is_dirinitems:try:ifis_dir:# 创建目录ifdry_run:print(f"[模拟] 创建目录:{path}")else:ifnotos.path.exists(path):os.makedirs(path,exist_ok=True)ifverbose:print(f"✓ 创建目录:{path}")created.append(("目录",path))else:ifverbose:print(f"✓ 目录已存在:{path}")existing.append(("目录",path))else:# 创建文件dir_path=os.path.dirname(path)# 先确保父目录存在ifdir_pathandnotos.path.exists(dir_path)andnotdry_run:os.makedirs(dir_path,exist_ok=True)# 创建文件ifdry_run:print(f"[模拟] 创建文件:{path}")else:ifnotos.path.exists(path)oros.path.getsize(path)==0:withopen(path,'w',encoding='utf-8')asf:f.write('')# 创建空文件ifverbose:print(f"✓ 创建文件:{path}")created.append(("文件",path))else:ifverbose:print(f"✓ 文件已存在:{path}")existing.append(("文件",path))exceptExceptionase:error_msg=f"创建失败{path}:{str(e)}"print(f"✗{error_msg}")errors.append(error_msg)returncreated,existing,errorsdefmain():# 设置参数解析器parser=argparse.ArgumentParser(description='从 tree.txt 创建目录结构 - 稳定解析版',add_help=False)parser.add_argument('-f','--file',default='tree.txt',help='tree.txt 文件路径 (默认: tree.txt)')parser.add_argument('-v','--verbose',action='store_true',help='显示详细过程')parser.add_argument('--dry-run',action='store_true',help='模拟运行,只预览不实际创建')parser.add_argument('-y','--yes',action='store_true',help='跳过确认,直接创建')parser.add_argument('--no-numbers',action='store_true',help='不显示项目编号')parser.add_argument('-h','--help',action='store_true',help='显示帮助信息')# 解析参数args=parser.parse_args()ifargs.help:print(""" 目录结构创建工具 - 稳定解析版 使用方法: python tree_creator.py [选项] 选项: -f, --file FILE 指定 tree.txt 文件 (默认: tree.txt) -v, --verbose 显示详细过程 --dry-run 模拟运行,只预览不创建 -y, --yes 跳过确认,直接创建 --no-numbers 不显示项目编号 -h, --help 显示此帮助信息 示例: python tree_creator.py --dry-run # 只预览 python tree_creator.py -v # 详细模式 python tree_creator.py -y # 直接创建 """)return0# 检查文件是否存在ifnotos.path.exists(args.file):print(f"错误: 找不到文件 '{args.file}'")print(f"请确保文件存在,或使用 -f 参数指定其他文件")return1print(f"📁 读取文件:{args.file}")# 读取文件try:withopen(args.file,'r',encoding='utf-8')asf:content=f.read()exceptUnicodeDecodeError:try:withopen(args.file,'r',encoding='gbk')asf:content=f.read()except:print("错误: 无法读取文件,请检查文件编码")return1exceptExceptionase:print(f"错误: 读取文件失败 -{e}")return1ifnotcontent.strip():print("错误: 文件为空")return1print(f"✅ 文件读取成功 ({len(content)}字符)")# 解析结构print("\n🔍 正在解析 tree 结构...")items=parse_tree_structure(content)ifnotitems:print("错误: 无法解析出任何项目")print("请检查 tree.txt 格式是否正确")return1# 预览解析结果print("\n"+"="*60)print("第一步:解析结果确认")print("="*60)dirs,files=preview_structure(items,notargs.no_numbers)# 检查解析质量iffiles==0anddirs>5:print("\n⚠️ 警告: 解析出大量目录但没有文件")print("可能是解析逻辑将文件误判为目录")ifnotargs.yes:response=input("是否继续? (y/N): ").lower()ifresponsenotin('y','yes'):print("操作取消")return0# 如果是dry-run模式,到此结束ifargs.dry_run:print("\n"+"="*60)print("模拟运行完成 - 未实际创建任何文件")print("="*60)return0# 第二步:确认是否继续print("\n"+"="*60)print("第二步:创建确认")print("="*60)ifnotargs.yes:print(f"\n将在当前目录创建{len(items)}个项目:")print(f" -{dirs}个目录")print(f" -{files}个文件")print(f"\n根目录:{items[0][0]}")ifnotget_user_confirmation("是否创建上述目录结构",default_no=True):print("操作取消")return0# 第三步:创建结构print("\n"+"="*60)print("第三步:创建目录结构")print("="*60)created,existing,errors=create_structure(items,verbose=args.verbose,dry_run=False)# 第四步:结果显示print("\n"+"="*60)print("第四步:操作结果")print("="*60)ifcreated:print(f"\n✅ 成功创建{len(created)}个项目:")foritem_type,pathincreated[:10]:# 最多显示10个print(f"{item_type}:{os.path.relpath(path)}")iflen(created)>10:print(f" ... 还有{len(created)-10}个项目")ifexisting:print(f"\n📌 跳过{len(existing)}个已存在的项目")iferrors:print(f"\n❌ 遇到{len(errors)}个错误:")forerrorinerrors[:5]:# 最多显示5个错误print(f"{error}")print(f"\n🎯 总计:{len(created)+len(existing)}个项目已就绪")# 最终检查root_path=items[0][0]ifos.path.exists(root_path):print(f"\n📂 根目录位置:{os.path.abspath(root_path)}")print("="*60)return1iferrorselse0if__name__=="__main__":try:sys.exit(main())exceptKeyboardInterrupt:print("\n\n⏹️ 操作被用户中断")sys.exit(130)exceptExceptionase:print(f"\n💥 程序执行出错:{e}")importtraceback traceback.print_exc()sys.exit(1)