Skip to content

写了一个小脚本,自动生成项目文件树

某天需要给别人展示一个项目的目录结构,用 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.gitdist 等常见目录。
  • 不打印到控制台,而是生成 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()