活在梦里

Python导出模块的源码

importlib.util.find_spec

importlib.util.find_spec 用于查找并返回指定模块的"模块规范"(module spec)。模块规范是一个包含模块详细信息的对象,它描述了如何加载该模块。

使用场景

  • 检查模块是否存在:如果返回值为 None,表示模块不存在
  • 获取模块的源代码:结合 loader.get_source() 方法可以获取模块源代码
  • 获取模块的文件路径:通过 origin 属性获取模块文件的位置
  • 检查模块是否为包:通过 submodule_search_locations 是否为 None 来判断

至于为什么这么搞嘛,当然是为了对抗某种python代码加密方式。

实现

import importlib.util
import os
from pathlib import Path


def import_and_output(module_name):
    # 获取模块的规范
    spec = importlib.util.find_spec(module_name)
    if spec is None:
        raise ImportError(f"无法找到模块: {module_name}")
    
    # 使用加载器获取源代码
    source_code = spec.loader.get_source(module_name)
    if source_code is None:
        raise RuntimeError("无法获取模块源代码")
    
    print(source_code)

# 示例:导入并输出名为'example'的模块内容
# import_and_output('app')

def get_loadable_modules_recursive(root_dir=None, exclude_dirs=None):
    """
    递归获取目录下所有可加载的 Python 模块
    :param root_dir: 起始目录(默认当前目录)
    :param exclude_dirs: 要排除的目录名列表(如 venv, .git 等)
    :return: 模块全名列表(例如 "package.sub.module")
    """
    if root_dir is None:
        root_dir = os.getcwd()
    if exclude_dirs is None:
        exclude_dirs = {'__pycache__', 'venv', '.git', '.idea'}

    modules = []
    root_path = Path(root_dir).resolve()

    for dir_path, dir_names, file_names in os.walk(root_dir, topdown=True):
        # 过滤排除目录
        dir_names[:] = [d for d in dir_names if d not in exclude_dirs]

        # 检查是否为有效 Python 包(存在 __init__.py)
        is_package = '__init__.py' in file_names
        relative_path = Path(dir_path).relative_to(root_path)
        package_parts = list(relative_path.parts)

        for file in file_names:
            if file == '__init__.py' or not file.endswith('.py'):
                continue

            # 生成模块层级名称
            module_name = file[:-3]
            full_module_parts = package_parts.copy()
            
            # 修改这里: 不再要求必须是包
            if dir_path == str(root_path):
                # 根目录下的模块
                full_module_parts.append(module_name)
                full_module = '.'.join(full_module_parts)
            elif is_package:
                # 是包内的模块
                full_module_parts.append(module_name)
                full_module = '.'.join(full_module_parts)
            else:
                # 非包内的模块 - 直接构建模块路径
                # 这里我们需要确定相对路径,并形成导入模块名
                if not full_module_parts:  # 空列表处理
                    full_module = module_name
                else:
                    full_module_parts.append(module_name)
                    full_module = '.'.join(full_module_parts)

            # 验证模块名有效性
            if not all(part.isidentifier() for part in full_module_parts + [module_name]):
                continue

            # 添加模块文件的完整路径信息以便后续处理
            module_file_path = os.path.join(dir_path, file)
            modules.append((full_module, module_file_path))

    return modules

# modules = get_loadable_modules_recursive(
#     exclude_dirs=['venv', 'tests']
# )
# print("可加载模块列表:\n", '\n'.join(modules))

# import_and_output('extensions.ext_commands')

def backup_modules_to_target(target_dir, root_dir=None, exclude_dirs=None):
    """
    备份所有可加载的Python模块源代码到目标目录
    
    :param target_dir: 目标目录路径
    :param root_dir: 起始目录(默认当前目录)
    :param exclude_dirs: 要排除的目录名列表
    :return: 成功备份的模块数量
    """
    # 获取所有可加载的模块
    module_info = get_loadable_modules_recursive(root_dir, exclude_dirs)
    backup_count = 0
    
    # 确保目标目录存在
    target_path = Path(target_dir)
    target_path.mkdir(exist_ok=True, parents=True)
    
    for module_name, module_file_path in module_info:
        try:
            # 直接从文件路径读取源代码,而不是通过importlib
            with open(module_file_path, 'r', encoding='utf-8') as f:
                source_code = f.read()
            
            # 构建目标文件路径
            module_parts = module_name.split('.')
            module_dir = target_path.joinpath(*module_parts[:-1])
            module_file = module_parts[-1] + '.py'
            module_path = module_dir / module_file
            
            # 确保目标目录存在
            module_dir.mkdir(exist_ok=True, parents=True)
            
            # 写入源代码到目标文件
            with open(module_path, 'w', encoding='utf-8') as f:
                f.write(source_code)
            
            print(f"已备份模块: {module_name}{module_path}")
            backup_count += 1
            
        except Exception as e:
            print(f"备份模块 {module_name} 时出错: {e}")
    
    print(f"备份完成,共备份了 {backup_count} 个模块")
    return backup_count

# 示例用法
backup_modules_to_target('backup_modules')