python文件类图生成

此方法适用于模块方法生成

import ast
import os
import platform
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib.patches import FancyBboxPatch
import textwrap

def get_system_fonts():
    """根据操作系统自动选择适配的中文字体(增加更多备选)"""
    system = platform.system()
    if system == "Darwin":  # macOS系统,增加更多常见中文字体备选
        return [
            "PingFang SC",      # 苹方(优先)
            "Heiti TC",         # 黑体
            "Microsoft YaHei",  # 微软雅黑(兼容)
            "SimHei",           # 黑体(兼容)
            "Arial Unicode MS", # 系统默认Unicode字体
            "Helvetica",
            "Arial"
        ]
    elif system == "Windows":  # Windows系统
        return [
            "Microsoft YaHei",  # 微软雅黑(优先)
            "SimHei",           # 黑体
            "Arial"
        ]
    else:  # Linux或其他系统
        return [
            "WenQuanYi Micro Hei",  # 文泉驿微米黑
            "Heiti TC",             # 黑体
            "Arial"
        ]

def extract_classes_and_functions_from_file(filepath):
    """解析Python文件,提取类、类内方法和全局函数"""
    with open(filepath, "r", encoding="utf-8") as file:
        tree = ast.parse(file.read(), filename=filepath)
    
    # 为所有节点添加parent属性,用于区分全局函数和类内方法
    for node in ast.walk(tree):
        for child in ast.iter_child_nodes(node):
            child.parent = node
    
    class_info = {}  # 存储类名: [方法列表]
    global_functions = []  # 存储全局函数
    
    for node in ast.walk(tree):
        # 提取类和类内方法
        if isinstance(node, ast.ClassDef):
            methods = [f"{subnode.name}()" for subnode in node.body if isinstance(subnode, ast.FunctionDef)]
            class_info[node.name] = methods
        
        # 提取全局函数(排除类内方法)
        if isinstance(node, ast.FunctionDef) and not (hasattr(node, 'parent') and isinstance(node.parent, ast.ClassDef)):
            global_functions.append(f"{node.name}()")
    
    return class_info, global_functions

def plot_class_diagram(class_info, global_functions, output_path, module_name):
    """绘制IDEA风格类图(增强字体兼容性)"""
    # 自动配置系统兼容字体
    plt.rcParams["font.family"] = get_system_fonts()
    plt.rcParams["axes.unicode_minus"] = False  # 解决负号显示异常
    
    # 动态计算画布高度,避免内容重叠
    block_height = 0.35  # 每个类/函数块的高度
    spacing = 0.15       # 块之间的间距
    total_height = 0.6   # 顶部留白(预留模块名位置)
    
    # 累加类所需高度(类名块 + 对应方法块)
    for methods in class_info.values():
        total_height += block_height + (block_height * len(methods)) + spacing
    
    # 累加全局函数所需高度(标题块 + 对应函数块)
    if global_functions:
        total_height += block_height + (block_height * len(global_functions)) + spacing
    
    total_height += 0.4  # 底部留白
    
    # 创建画布(宽度固定10,高度根据内容动态调整)
    fig, ax = plt.subplots(figsize=(10, max(6, total_height)))
    ax.set_xlim(0, 1)
    ax.set_ylim(0, total_height)
    ax.set_facecolor('#ffffff')  # 白色背景,贴近IDEA界面风格
    ax.set_axis_off()  # 隐藏默认坐标轴

    current_y = total_height - 0.5  # 初始绘制位置(从顶部往下)

    # 1. 绘制模块名标题(居中加粗,突出模块标识)
    plt.text(0.5, current_y + 0.15, f"模块: {module_name}", 
             ha="center", va="center", fontsize=14, fontweight='bold', color='#333333')
    current_y -= 0.3  # 下移,为类块预留位置

    # 2. 绘制类(蓝色系,区分类名和方法)
    for class_name, methods in class_info.items():
        # 绘制类名块(深色边框+浅蓝色背景,IDEA类图风格)
        class_rect = FancyBboxPatch(
            (0.1, current_y - block_height), 0.8, block_height,
            boxstyle="square,pad=0", 
            edgecolor='#2a76be', facecolor='#e6f2ff', lw=1.5  # 蓝色系配色
        )
        ax.add_patch(class_rect)
        # 类名文本(左对齐,加粗)
        plt.text(0.15, current_y - block_height/2, class_name,
                 ha="left", va="center", fontsize=12, fontweight='bold', color='#2a76be')
        current_y -= block_height  # 下移,为方法块预留位置

        # 绘制当前类的方法块(浅色边框+更浅背景,与类名块区分)
        for method in methods:
            method_rect = FancyBboxPatch(
                (0.1, current_y - block_height), 0.8, block_height,
                boxstyle="square,pad=0", 
                edgecolor='#8ec0e4', facecolor='#f0f7ff', lw=1  # 浅蓝配色
            )
            ax.add_patch(method_rect)
            # 方法名文本(左对齐,自动换行避免超长)
            wrapped_method = textwrap.fill(method, width=45)  # 控制每行长度
            plt.text(0.15, current_y - block_height/2, wrapped_method,
                     ha="left", va="center", fontsize=10, color='#333333')
            current_y -= block_height  # 下移,为下一个方法/类预留位置

        current_y -= spacing  # 类之间的间距

    # 3. 绘制全局函数(橙色系,与类区分)
    if global_functions:
        # 绘制全局函数标题块(深色边框+浅橙色背景)
        func_title_rect = FancyBboxPatch(
            (0.1, current_y - block_height), 0.8, block_height,
            boxstyle="square,pad=0", 
            edgecolor='#e67e22', facecolor='#fff2e6', lw=1.5  # 橙色系配色
        )
        ax.add_patch(func_title_rect)
        # 标题文本(左对齐,加粗)
        plt.text(0.15, current_y - block_height/2, "全局函数",
                 ha="left", va="center", fontsize=12, fontweight='bold', color='#e67e22')
        current_y -= block_height  # 下移,为函数块预留位置

        # 绘制全局函数块(浅色边框+更浅背景)
        for func in global_functions:
            func_rect = FancyBboxPatch(
                (0.1, current_y - block_height), 0.8, block_height,
                boxstyle="square,pad=0", 
                edgecolor='#f3b664', facecolor='#fff7f0', lw=1  # 浅橙配色
            )
            ax.add_patch(func_rect)
            # 函数名文本(左对齐,自动换行)
            wrapped_func = textwrap.fill(func, width=45)
            plt.text(0.15, current_y - block_height/2, wrapped_func,
                     ha="left", va="center", fontsize=10, color='#333333')
            current_y -= block_height  # 下移,为下一个函数预留位置

        current_y -= spacing  # 与其他内容区隔

    # 保存类图(高分辨率300dpi,避免内容截断)
    plt.tight_layout()
    plt.savefig(output_path, format="png", dpi=300, bbox_inches='tight', facecolor='#ffffff')
    plt.close()
    print(f"类图已保存:{output_path}")

def generate_class_diagram(input_path):
    """批量生成指定文件夹下所有Python文件的类图"""
    # 校验输入路径有效性
    if not os.path.isdir(input_path):
        print(f"错误:路径「{input_path}」不是有效文件夹")
        return []

    # 创建输出文件夹(存放在输入路径下,命名为class_diagrams)
    output_dir = os.path.join(input_path, "class_diagrams")
    os.makedirs(output_dir, exist_ok=True)

    # 筛选所有.py文件(排除__init__.py等特殊文件)
    py_files = [
        os.path.join(root, f) 
        for root, _, files in os.walk(input_path)
        for f in files if f.endswith('.py') and not f.startswith('__')
    ]

    # 批量生成类图
    generated_diagrams = []
    for py_file in py_files:
        class_info, global_funs = extract_classes_and_functions_from_file(py_file)
        
        # 若没有类,创建"模块函数"虚拟类存放全局函数
        if not class_info:
            class_info = {"模块函数": global_funs}
            global_funs = []
        
        # 生成输出文件名(原文件名+_class_diagram.png)
        file_name = os.path.splitext(os.path.basename(py_file))[0]
        output_file = os.path.join(output_dir, f"{file_name}_class_diagram.png")
        
        # 绘制并保存类图
        plot_class_diagram(class_info, global_funs, output_file, os.path.basename(py_file))
        generated_diagrams.append(output_file)

    print(f"\n所有类图已保存至:{output_dir}")
    return generated_diagrams


if __name__ == "__main__":
    # 交互输入文件夹路径,生成类图
    input_folder = input("请输入Python文件所在文件夹路径:")
    result = generate_class_diagram(input_folder)
    
    # 打印生成结果
    if result:
        print("\n生成成功的类图列表:")
        for idx, path in enumerate(result, 1):
            print(f"{idx}. {path}")
    else:
        print("\n未生成任何类图")
# 服务器 

标题:python文件类图生成
作者:admin
地址:http://www.mjdg.store/articles/2025/11/03/1762184505924.html

评论

取消