杰瑞科技汇

Python analyzer教程该怎么学?

"Python Analyzer" 这个词可以指代很多不同的工具,从简单的静态代码检查工具(如 Pylint, Flake8)到复杂的静态分析框架(如 LibCST, Semgrep),本教程将从一个实用的、自定义的代码分析器入手,教你如何一步步构建一个能够理解 Python 代码并提取有用信息的工具。

Python analyzer教程该怎么学?-图1
(图片来源网络,侵删)

我们将构建一个名为 pyanalyzer 的工具,它能够:

  1. 遍历指定目录下的所有 Python 文件
  2. 解析每个文件,提取出函数和类的定义。
  3. 统计每个文件和整个项目的代码行数
  4. 生成一个清晰的报告,展示分析结果。

这个教程将带你从零开始,最终拥有一个功能完整且可扩展的分析器。


目录

  1. 第一步:项目规划与环境准备
    • 我们要做什么?
    • 为什么使用 ast 模块?
    • 搭建项目环境
  2. 第二步:核心解析器 - 使用 ast 模块
    • ast 模块简介
    • 编写第一个解析函数
    • 遍历抽象语法树
  3. 第三步:构建分析引擎
    • 设计数据结构
    • 实现文件分析逻辑
    • 实现项目遍历逻辑
  4. 第四步:整合与报告生成
    • 编写主程序入口
    • 生成格式化输出
    • 处理命令行参数
  5. 第五步:扩展与进阶
    • 添加更多分析功能(如检测未使用的变量)
    • 输出为 JSON 或 CSV
    • 使用第三方库增强功能
  6. 完整代码与总结

第一步:项目规划与环境准备

1 我们要做什么?

我们的目标是创建一个命令行工具,用户可以通过运行 python pyanalyzer.py <path_to_project> 来分析一个 Python 项目,工具将输出类似下面的报告:

Analyzing project: /path/to/your/python/project
--- File: src/my_module.py ---
- Functions: 3
- Classes: 1
- Total Lines: 45
--- File: tests/test_analyzer.py ---
- Functions: 5
- Classes: 0
- Total Lines: 28
--- Project Summary ---
- Total Files: 2
- Total Functions: 8
- Total Classes: 1
- Total Lines: 73

2 为什么使用 ast 模块?

直接用正则表达式解析 Python 代码是非常困难且不可靠的(想想注释、字符串、多行语句等),Python 标准库中的 ast (Abstract Syntax Trees) 模块是完美的解决方案,它可以将 Python 源代码解析成一个结构化的树形对象,我们可以轻松地遍历这棵树来理解代码的结构。

Python analyzer教程该怎么学?-图2
(图片来源网络,侵删)

3 搭建项目环境

创建一个新的项目目录和文件结构:

pyanalyzer_project/
├── pyanalyzer.py      # 我们的主程序
└── sample_project/    # 一个用来测试的小项目
    ├── main.py
    └── utils.py

我们有了基本的结构,可以开始写代码了。


第二步:核心解析器 - 使用 ast 模块

1 ast 模块简介

ast 模块的核心是 ast.NodeVisitor 类,我们可以继承它,并重写其 visit_NodeType 方法,NodeType 是 AST 节点的类型(如 ast.FunctionDef, ast.ClassDef),当 ast.NodeVisitor 遍历树时,它会自动调用我们定义的相应方法。

2 编写第一个解析函数

让我们创建一个简单的解析器,它能统计一个文件中函数和类的数量。

创建 pyanalyzer.py 文件,并加入以下代码:

import ast
# 继承 ast.NodeVisitor 来创建我们自己的访问器
class CodeAnalyzer(ast.NodeVisitor):
    def __init__(self):
        self.function_count = 0
        self.class_count = 0
    # 当访问到 FunctionDef 节点时,此方法会被自动调用
    def visit_FunctionDef(self, node):
        print(f"Found a function: {node.name}")
        self.function_count += 1
        # 必须调用这个,以便访问器可以继续遍历函数体内的节点
        self.generic_visit(node)
    # 当访问到 ClassDef 节点时,此方法会被自动调用
    def visit_ClassDef(self, node):
        print(f"Found a class: {node.name}")
        self.class_count += 1
        self.generic_visit(node)
def analyze_file(filepath):
    """读取并分析单个 Python 文件"""
    with open(filepath, 'r', encoding='utf-8') as f:
        source_code = f.read()
    # 将源代码解析成 AST
    tree = ast.parse(source_code, filename=filepath)
    # 创建我们的分析器实例并开始遍历
    analyzer = CodeAnalyzer()
    analyzer.visit(tree)
    return analyzer.function_count, analyzer.class_count
# --- 测试 ---
if __name__ == '__main__':
    # 分析 sample_project/main.py
    # 假设 sample_project/main.py 内容如下:
    # def greet(name):
    #     print(f"Hello, {name}!")
    #
    # class Greeter:
    #     def __init__(self):
    #         self.message = "Welcome"
    #
    #     def say_hello(self):
    #         print(self.message)
    functions, classes = analyze_file('sample_project/main.py')
    print(f"\n--- Summary for main.py ---")
    print(f"Functions: {functions}")
    print(f"Classes: {classes}")

运行 python pyanalyzer.py,你应该能看到输出:

Found a function: greet
Found a class: Greeter
Found a function: __init__
Found a function: say_hello
--- Summary for main.py ---
Functions: 3
Classes: 1

注意__init__ 方法也被识别为一个函数,这正是 ast 的强大之处。


第三步:构建分析引擎

现在我们有了一个基础的解析器,接下来需要扩展它,使其能统计代码行数,并能处理整个项目。

1 设计数据结构

为了更好地组织数据,我们定义一个简单的类来存储单个文件的分析结果。

# 在 pyanalyzer.py 中添加
class FileInfo:
    def __init__(self, filepath):
        self.filepath = filepath
        self.function_count = 0
        self.class_count = 0
        self.total_lines = 0

2 实现文件分析逻辑

我们将 CodeAnalyzer 类集成到 FileInfo 的分析过程中,并增加行数统计。

# 修改 CodeAnalyzer 类,让它将结果存入 FileInfo 对象
class CodeAnalyzer(ast.NodeVisitor):
    def __init__(self, file_info):
        self.file_info = file_info
    def visit_FunctionDef(self, node):
        self.file_info.function_count += 1
        self.generic_visit(node)
    def visit_ClassDef(self, node):
        self.file_info.class_count += 1
        self.generic_visit(node)
def analyze_file(filepath):
    """读取并分析单个 Python 文件,返回 FileInfo 对象"""
    try:
        with open(filepath, 'r', encoding='utf-8') as f:
            source_code = f.read()
    except (UnicodeDecodeError, FileNotFoundError):
        print(f"Warning: Could not read file {filepath}")
        return None
    tree = ast.parse(source_code, filename=filepath)
    file_info = FileInfo(filepath)
    analyzer = CodeAnalyzer(file_info)
    analyzer.visit(tree)
    # 统计总行数
    file_info.total_lines = len(source_code.splitlines())
    return file_info

3 实现项目遍历逻辑

我们需要一个函数来递归地查找一个目录下的所有 .py 文件。

import os
def analyze_project(project_path):
    """分析一个项目目录下的所有 Python 文件"""
    all_files_info = []
    for root, _, files in os.walk(project_path):
        for file in files:
            if file.endswith('.py'):
                filepath = os.path.join(root, file)
                file_info = analyze_file(filepath)
                if file_info:
                    all_files_info.append(file_info)
    return all_files_info

第四步:整合与报告生成

现在我们有分析文件和分析项目的所有组件,最后一步是将它们整合起来,生成用户友好的报告。

1 编写主程序入口

我们将所有逻辑放在一起,并添加一个 main 函数。

def generate_report(all_files_info):
    """生成并打印分析报告"""
    if not all_files_info:
        print("No Python files found to analyze.")
        return
    total_functions = 0
    total_classes = 0
    total_lines = 0
    print("\n--- Detailed Report ---")
    for file_info in all_files_info:
        print(f"\n--- File: {file_info.filepath} ---")
        print(f"- Functions: {file_info.function_count}")
        print(f"- Classes: {file_info.class_count}")
        print(f"- Total Lines: {file_info.total_lines}")
        total_functions += file_info.function_count
        total_classes += file_info.class_count
        total_lines += file_info.total_lines
    print("\n--- Project Summary ---")
    print(f"- Total Files: {len(all_files_info)}")
    print(f"- Total Functions: {total_functions}")
    print(f"- Total Classes: {total_classes}")
    print(f"- Total Lines: {total_lines}")
def main():
    """主函数"""
    # 为了简单起见,我们暂时写死项目路径
    # 在下一步我们会改进这一点
    project_path = 'sample_project'
    print(f"Analyzing project: {project_path}")
    files_info = analyze_project(project_path)
    generate_report(files_info)
if __name__ == '__main__':
    main()

2 处理命令行参数

让用户能从命令行传入项目路径会更实用,我们可以使用 sys.argv 来实现。

import sys
# ... (其他代码保持不变) ...
def main():
    """主函数,处理命令行参数"""
    if len(sys.argv) != 2:
        print("Usage: python pyanalyzer.py <path_to_project>")
        sys.exit(1) # 退出程序,返回错误码 1
    project_path = sys.argv[1]
    if not os.path.isdir(project_path):
        print(f"Error: Directory '{project_path}' not found.")
        sys.exit(1)
    print(f"Analyzing project: {project_path}")
    files_info = analyze_project(project_path)
    generate_report(files_info)
if __name__ == '__main__':
    main()

你可以这样运行你的工具: python pyanalyzer.py sample_project

3 生成格式化输出

我们的报告已经很好了,但可以稍微美化一下。generate_report 函数中的打印语句已经足够清晰。


第五步:扩展与进阶

你的 pyanalyzer 已经可以工作了!下面是一些让它变得更强大的方向。

1 添加更多分析功能

让我们添加一个新功能:检测未使用的变量,这需要更复杂的 AST 分析,因为我们不仅要定义变量,还要跟踪它的使用情况。

# 在 CodeAnalyzer 类中添加新方法
class CodeAnalyzer(ast.NodeVisitor):
    def __init__(self, file_info):
        self.file_info = file_info
        self.defined_vars = set()
        self.used_vars = set()
    def visit_FunctionDef(self, node):
        self.defined_vars.clear() # 函数作用域
        self.used_vars.clear()
        self.generic_visit(node)
        # 分析函数结束后,比较已定义和已使用的变量
        unused_vars = self.defined_vars - self.used_vars
        if unused_vars:
            print(f"  [Warning] Unused variables in {node.name}: {', '.join(unused_vars)}")
    def visit_Assign(self, node):
        # 简单处理:只考虑单变量赋值,如 x = 1
        if isinstance(node.targets[0], ast.Name):
            var_name = node.targets[0].id
            self.defined_vars.add(var_name)
        self.generic_visit(node)
    def visit_Name(self, node):
        # 当看到一个变量名时,认为它被使用了
        if isinstance(node.ctx, ast.Load): # 确保是加载(使用)而非存储(定义)
            self.used_vars.add(node.id)
        self.generic_visit(node)
    # ... (visit_FunctionDef, visit_ClassDef 保持不变) ...

将这个新的 CodeAnalyzer 版本替换掉旧的,然后运行分析器,它现在会报告未使用的变量!

2 输出为 JSON 或 CSV

为了方便其他程序处理结果,我们可以将报告输出为结构化数据。

输出 JSON:

import json
def generate_json_report(all_files_info):
    """生成 JSON 格式的报告"""
    report_data = {
        "project_summary": {
            "total_files": len(all_files_info),
            "total_functions": sum(f.function_count for f in all_files_info),
            "total_classes": sum(f.class_count for f in all_files_info),
            "total_lines": sum(f.total_lines for f in all_files_info),
        },
        "files": [
            {
                "path": f.filepath,
                "functions": f.function_count,
                "classes": f.class_count,
                "lines": f.total_lines
            } for f in all_files_info
        ]
    }
    print(json.dumps(report_data, indent=4))
# 在 main 函数中添加一个选项来选择报告格式
# ...

输出 CSV:

import csv
def generate_csv_report(all_files_info):
    """生成 CSV 格式的报告"""
    import io
    output = io.StringIO()
    writer = csv.writer(output)
    writer.writerow(["Filepath", "Functions", "Classes", "Lines"])
    for f in all_files_info:
        writer.writerow([f.filepath, f.function_count, f.class_count, f.total_lines])
    print(output.getvalue())
# 在 main 函数中添加一个选项来选择报告格式
# ...

3 使用第三方库增强功能

  • argparse: 对于更复杂的命令行参数(如 --format json, --path ./my_projectargparse是标准库中的首选工具,比sys.argv` 更强大、更易用。
  • libcst: 这是一个由 Meta (Facebook) 开发的 CST (Concrete Syntax Tree) 库,与 AST 不同,CST 保留了代码的所有格式信息(如空格、换行),如果你想构建代码格式化工具(如 Black)或进行基于代码样式的分析,libcst 是更好的选择。
  • semgrep: 这是一个非常强大的、可定制的静态分析工具,你可以学习它的规则语法,并编写自己的 semgrep 规则来实现复杂的代码检查,这通常比从头构建一个分析器要快得多。

第六步:完整代码与总结

下面是 pyanalyzer.py 的完整代码,整合了所有功能。

import ast
import os
import sys
# --- 数据结构 ---
class FileInfo:
    def __init__(self, filepath):
        self.filepath = filepath
        self.function_count = 0
        self.class_count = 0
        self.total_lines = 0
# --- 核心分析器 ---
class CodeAnalyzer(ast.NodeVisitor):
    def __init__(self, file_info):
        self.file_info = file_info
        # 用于检测未使用变量(进阶功能)
        self.defined_vars = set()
        self.used_vars = set()
    def visit_FunctionDef(self, node):
        # 函数作用域重置
        self.defined_vars.clear()
        self.used_vars.clear()
        self.file_info.function_count += 1
        self.generic_visit(node)
        # 检查未使用变量
        unused_vars = self.defined_vars - self.used_vars
        if unused_vars:
            print(f"  [Warning] Unused variables in '{node.name}': {', '.join(unused_vars)}")
    def visit_ClassDef(self, node):
        self.file_info.class_count += 1
        self.generic_visit(node)
    def visit_Assign(self, node):
        # 简单处理:只考虑单变量赋值
        if isinstance(node.targets[0], ast.Name):
            var_name = node.targets[0].id
            self.defined_vars.add(var_name)
        self.generic_visit(node)
    def visit_Name(self, node):
        # 当看到一个变量名时,认为它被使用了
        if isinstance(node.ctx, ast.Load):
            self.used_vars.add(node.id)
        self.generic_visit(node)
# --- 文件分析逻辑 ---
def analyze_file(filepath):
    """读取并分析单个 Python 文件,返回 FileInfo 对象"""
    try:
        with open(filepath, 'r', encoding='utf-8') as f:
            source_code = f.read()
    except (UnicodeDecodeError, FileNotFoundError):
        print(f"Warning: Could not read file {filepath}")
        return None
    tree = ast.parse(source_code, filename=filepath)
    file_info = FileInfo(filepath)
    analyzer = CodeAnalyzer(file_info)
    analyzer.visit(tree)
    file_info.total_lines = len(source_code.splitlines())
    return file_info
# --- 项目分析逻辑 ---
def analyze_project(project_path):
    """分析一个项目目录下的所有 Python 文件"""
    all_files_info = []
    for root, _, files in os.walk(project_path):
        for file in files:
            if file.endswith('.py'):
                filepath = os.path.join(root, file)
                file_info = analyze_file(filepath)
                if file_info:
                    all_files_info.append(file_info)
    return all_files_info
# --- 报告生成 ---
def generate_report(all_files_info):
    """生成并打印分析报告"""
    if not all_files_info:
        print("No Python files found to analyze.")
        return
    total_functions = 0
    total_classes = 0
    total_lines = 0
    print("\n--- Detailed Report ---")
    for file_info in all_files_info:
        print(f"\n--- File: {file_info.filepath} ---")
        print(f"- Functions: {file_info.function_count}")
        print(f"- Classes: {file_info.class_count}")
        print(f"- Total Lines: {file_info.total_lines}")
        total_functions += file_info.function_count
        total_classes += file_info.class_count
        total_lines += file_info.total_lines
    print("\n--- Project Summary ---")
    print(f"- Total Files: {len(all_files_info)}")
    print(f"- Total Functions: {total_functions}")
    print(f"- Total Classes: {total_classes}")
    print(f"- Total Lines: {total_lines}")
# --- 主程序入口 ---
def main():
    """主函数,处理命令行参数"""
    if len(sys.argv) != 2:
        print("Usage: python pyanalyzer.py <path_to_project>")
        sys.exit(1)
    project_path = sys.argv[1]
    if not os.path.isdir(project_path):
        print(f"Error: Directory '{project_path}' not found.")
        sys.exit(1)
    print(f"Analyzing project: {project_path}")
    files_info = analyze_project(project_path)
    generate_report(files_info)
if __name__ == '__main__':
    main()

恭喜!你已经成功地从零开始构建了一个功能齐全的 Python 代码分析器。

通过这个教程,你学到了:

  1. ast 模块是进行 Python 静态分析的核心工具。
  2. 继承 ast.NodeVisitor 是遍历和解析代码结构的标准模式。
  3. 如何将分析逻辑模块化,分离文件分析、项目遍历和报告生成。
  4. 如何通过扩展 NodeVisitor 来添加新的分析功能。
  5. 构建命令行工具的基本流程。

这个 pyanalyzer 是一个很好的起点,你可以继续扩展它,比如添加对复杂导入、代码复杂度(圈复杂度)、甚至是安全漏洞的检测,希望这个教程能帮助你开启 Python 代码分析的大门!

分享:
扫描分享到社交APP
上一篇
下一篇