写了一个小脚本,自动生成项目文件树
某天需要给别人展示一个项目的目录结构,用 tree 命令发现输出格式不太理想(比如没有文件类型图标,或者默认会列出很多不需要的目录如 node_modules)。于是花了一点时间写了一个 Python 脚本,递归遍历目录,生成类似 VS Code 文件树的样式。
第一版:打印到控制台
第一版代码很简单,递归打印当前目录下的文件和文件夹,根据层级决定缩进符号(├──、└──、│),并为常见的文件扩展名配上简单的图标。
python
import os
from pathlib import Path
def print_directory_structure(path=".", indent="", is_last=True):
p = Path(path)
items = list(p.iterdir())
items.sort(key=lambda x: x.name)
for i, item in enumerate(items):
is_last_item = (i == len(items) - 1)
prefix = "└── " if is_last_item else "├── "
new_indent = indent + (" " if is_last_item else "│ ")
if item.is_dir():
print(f"{indent}{prefix}{item.name}/")
print_directory_structure(item, new_indent, is_last_item)
else:
icon = get_file_icon(item.suffix)
print(f"{indent}{prefix}{icon} {item.name}")
def get_file_icon(suffix):
icons = {
'.ts': 'TS', '.tsx': 'TS', '.js': 'JS', '.jsx': 'JS',
'.json': '{}', '.html': '<>', '.css': 'CSS', '.md': '📝',
}
return icons.get(suffix.lower(), '📄')运行后会直接在当前终端打印树形结构。适合快速看一眼,但无法保存结果,而且默认会输出所有文件(包括 .git、__pycache__ 等不想看到的目录)。
第二版:排除不需要的目录,输出到 Markdown
改进方向:
- 增加排除列表,过滤掉
node_modules、.git、dist等常见目录。 - 不打印到控制台,而是生成 Markdown 文件(用代码块包裹),方便直接贴到项目文档或博客里。
代码调整后:
python
import os
from pathlib import Path
EXCLUDE_PATTERNS = {
"node_modules", ".git", "dist", "build", "__pycache__",
".next", "coverage", "*.log", "*.tmp", "temp", ".DS_Store", ".vscode"
}
def should_exclude(path: Path) -> bool:
name = path.name
for pattern in EXCLUDE_PATTERNS:
if pattern.endswith('*'):
if name.startswith(pattern[:-1]):
return True
else:
if name == pattern:
return True
return False
def generate_markdown_tree(root_path=".", indent=""):
lines = []
root = Path(root_path)
try:
items = sorted(root.iterdir(), key=lambda x: (x.is_file(), x.name.lower()))
except PermissionError:
return lines
visible_items = [item for item in items if not should_exclude(item)]
for i, item in enumerate(visible_items):
is_last = (i == len(visible_items) - 1)
prefix = "└── " if is_last else "├── "
icon = get_file_icon(item.suffix) if item.is_file() else "📁"
display_name = f"{icon} {item.name}" + ("/" if item.is_dir() else "")
lines.append(f"{indent}{prefix}{display_name}")
if item.is_dir():
extension = " " if is_last else "│ "
lines.extend(generate_markdown_tree(item, indent + extension))
return lines
def get_file_icon(suffix: str) -> str:
icons = {
'.ts': '🔷', '.tsx': '🔷', '.js': '🟢', '.jsx': '🟢',
'.json': '🟨', '.html': '🌐', '.css': '🎨', '.md': '📘',
'.py': '🐍', '.gitignore': '🚫',
}
return icons.get(suffix.lower(), '📄')
def main():
tree_lines = generate_markdown_tree()
markdown_content = "# 项目结构\n\n```\n" + "\n".join(tree_lines) + "\n```\n"
with open("structure.md", "w", encoding="utf-8") as f:
f.write(markdown_content)
print("✅ 已保存到 structure.md")
if __name__ == "__main__":
main()使用效果
对某个实际项目(比如一个资产登记网站)运行脚本,生成的 structure.md 内容类似:
项目结构
shell
📁 Qweb/
├── 📁 backend/
│ ├── 📁 src/
│ │ ├── 📁 config/
│ │ ├── 📁 controllers/
│ │ ├── 📁 middleware/
│ │ │ └── 🔷 auth.ts
│ │ ├── 📁 models/
│ │ ├── 📁 routes/
│ │ │ ├── 🔷 auth.ts
│ │ │ ├── 🔷 collections.ts
│ │ │ └── 🔷 users.ts
│ │ ├── 🔷 app.ts
│ │ ├── 🔷 database.ts
│ │ └── 🔷 server.ts
│ ├── 📁 uploads/
│ ├── 📄 database.sqlite
│ └── 🟨 package.json
├── 📁 frontend/
│ ├── 📁 src/
│ │ ├── 📁 api/
│ │ ├── 📁 components/
│ │ ├── 📁 router/
│ │ ├── 📁 stores/
│ │ ├── 📁 views/
│ │ ├── 📄 App.vue
│ │ └── 🔷 main.ts
│ └── 🟨 package.json
└── 🐍 gen_tree.py遇到的问题
1. 排序问题
第一版按名字简单排序,目录和文件混在一起。第二版改为目录在前、文件在后,通过 key=lambda x: (x.is_file(), x.name.lower()) 实现。
2. 权限不足
访问某些系统目录(如 /proc)会抛出 PermissionError。用 try...except 跳过这些目录,避免脚本中断。
3. 通配符排除.log、.tmp 这类后缀用 endswith('*') 判断前缀匹配。更严谨的做法可以用 fnmatch,但当前需求简单,这样够用。
后续
这个脚本后来在几个项目里复用,每次只需要修改 EXCLUDE_PATTERNS 就能适配不同语言的项目。如果以后需要更丰富的功能(比如统计文件数量、显示文件大小),可以继续扩展,但目前的功能已经满足需求。
完整代码
python
import os
from pathlib import Path
# ====== 配置区 ======
EXCLUDE_PATTERNS = {
"node_modules",
".git",
"dist",
"build",
"__pycache__",
".next",
"coverage",
"*.log",
"*.tmp",
"temp",
".DS_Store",
".vscode", # 如果你不想显示 .vscode 也可以保留
}
# ===================
def should_exclude(path: Path) -> bool:
"""判断是否应排除该路径"""
name = path.name
for pattern in EXCLUDE_PATTERNS:
if pattern.endswith('*'):
if name.startswith(pattern[:-1]):
return True
else:
if name == pattern:
return True
return False
def generate_markdown_tree(root_path=".", indent=""):
lines = []
root = Path(root_path)
try:
items = sorted(root.iterdir(), key=lambda x: (x.is_file(), x.name.lower()))
except PermissionError:
return []
for i, item in enumerate(items):
if should_exclude(item):
continue
is_last = (i == len([x for x in items if not should_exclude(x)]) - 1)
prefix = "└── " if is_last else "├── "
icon = get_file_icon(item.suffix) if item.is_file() else "📁"
display_name = f"{icon} {item.name}" + ("/" if item.is_dir() else "")
lines.append(f"{indent}{prefix}{display_name}")
if item.is_dir():
extension = " " if is_last else "│ "
lines.extend(generate_markdown_tree(item, indent + extension))
return lines
def get_file_icon(suffix: str) -> str:
icons = {
'.ts': '🔷',
'.tsx': '🔷',
'.js': '🟢',
'.jsx': '🟢',
'.json': '🟨',
'.html': '🌐',
'.css': '🎨',
'.scss': '🎨',
'.md': '📘',
'.py': '🐍',
'.yaml': '⚙️',
'.yml': '⚙️',
'.env': '🔒',
'.gitignore': '🚫',
'.dockerfile': '🐳',
'.lock': '🔒',
}
return icons.get(suffix.lower(), '📄')
def main():
print("正在生成项目结构...")
tree_lines = generate_markdown_tree()
markdown_content = "# 项目结构\n\n```\n" + "\n".join(tree_lines) + "\n```\n"
output_file = "structure.md"
with open(output_file, "w", encoding="utf-8") as f:
f.write(markdown_content)
print(f"✅ 已保存到 {output_file}")
print(f"排除的目录/文件: {sorted(EXCLUDE_PATTERNS)}")
if __name__ == "__main__":
main()