• 博客园logo
  • 会员
  • 周边
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
echo-z-z
博客园    首页    新随笔    联系   管理    订阅  订阅

课程设计-基于抽象语法树的代码查重系统

课程设计

项目名称 基于抽象语法树的代码查重系统
课程名称 数据结构
使用语言 python
操作者 豆芽菜
课设结束时间 2026.07.03

目录
  • 一、背景与问题
  • 二、系统设计
    • 基本思路
    • 结果比对
  • 三、算法与数据结构
    • 3.1 文本基准算法
      • 3.1.1 最大公共子序列长度
      • 3.1.2 最短编辑距离
    • 3.2 抽象语法树结构提取
    • 3.3 相似度计算
      • 传统文本比较
      • AST
    • 3.4 其余模块概览
      • 文件与目录操作函数(4个)
      • 查重比较功能函数(4个)
  • 四、运行效果与对比
    • 4.1 运行结果
      • 4.1.1 运行run.py的主菜单图
      • 4.1.2 查重结果
      • 4.1.3 AST结构可视化
    • 4.2 对比
      • 4.2.1 比较函数
      • 4.2.2 文本 VS AST 对比分析
  • 五、LLM辅助生成报告
    • 5.1 设计思路
    • 5.2 调用和提示词设计
    • 5.3 生成报告示例
  • 六、反思与总结
    • 6.1 反思
    • 6.2 总结
  • 七、附件及声明

一、背景与问题

随着越来越多的编程课程与在线评测系统(如 PTA)的普及,学生代码提交量增加,代码抄袭现象比较突出。传统的代码查重方法多基于文本字符串比对(如最长公共子序列 LCS 和编辑距离),但这类方法极易被简单的代码变换手段规避,比如变换变量的名字、改变语句顺序等。这些变化之后其逻辑结构并没有改变,所以仅靠文本相似度查重依据容易出现漏洞。
抽象语法树AST是将表达式、语句、函数定义等程序语句抽象为一棵树的结点,将代码转换成AST之后判断逻辑结构的方法,更加健壮也更为大家所认可。


二、系统设计

基本思路

  • 【数据输入】
  1. 支持四种灵活输入模式:终端粘贴代码片段;指定两个本地代码文件;扫描整个文件夹批量比对;自定义多文件列表。
  2. 每次运行自动创建带时间戳的独立结果目录(results/run_时间戳/),确保历史记录不覆盖、不丢失。
  3. 自动识别代码语言(Python / C / C++)
  • 【核心算法】
  1. 文本基准: 实现LCS和最短编辑距离,将代码当作纯字符列表分析计算相似度;

  2. AST结构: 将代码转为在内存中的抽象语法树,DFS遍历树的结点,转化为线性结构序列,复用LCS计算相似度;

  • 【ai赋能输出】
    结果可视化: 在终端打印 Markdown 格式的对比表格,同时自动生成 CSV 与 Markdown 文件,便于报告撰写与数据分析,在终端也可选择打印查看AST树结构。

    AI 报告生成: 调用本地部署的 LLM Studio(Qwen3.5-0.8B 模型),读取查重结果,自动生成综合性分析报告(llm_analysis_report.md)。


结果比对

  • 同时输出比对两种相似度。
  • 通过典型实验案例(如变量重命名)证明 AST 方法的优越性。

三、算法与数据结构

3.1 文本基准算法

3.1.1 最大公共子序列长度

  • 【原理】
  1. 建列表dp[m+1][n+1],预留出[0][0]空间,当输入字符串为空时,公共子序列长度为0;其中dp[i][j]表示的是text1的前i个元素和text2的前j个元素,dp[i-1][j-1] 不是“一个字符”或者“数值”,它里面存的可能是 0、1、2、3,是前面一大段比对结果累加好的总和。

  2. 比较两个文本text1[i-1]和text2[j-1];

  3. 【核心决策步骤:动态规划dp】 如果相等,取左上角dp[i-1][j-1]的数字(其物理位置在dp[i][j]的左上角)加1,如果不相等,dp取max((i-1,j-1),(i,j-1))

数据结构概念 在代码中的体现 对应关系
树 dp[i][j]代表text1的前i个和text2前j个,其子树的结点有dp[i-1][j]、dp[i][j-1]、dp[i-1][j-1] dp表的格子代表决策树的所有结点,按照拓扑结构从上而下、从左到右扁平化为矩阵

  • 【代码实现】
def lcs_length(text1, text2):
    """计算两个序列的最长公共子序列长度"""
    n, m = len(text1), len(text2)
    dp = [[0] * (m + 1) for _ in range(n + 1)]

    for i in range(1, n + 1):
        for j in range(1, m + 1):
            if text1[i - 1] == text2[j - 1]:
                dp[i][j] = dp[i - 1][j - 1] + 1
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
    return dp[n][m]

3.1.2 最短编辑距离

  • 【原理】
  1. 建列表dp[m+1][n+1],其中,[i]、[j]是指剩余未处理的字符长度,初始化dp[i][0]=i,dp[0][j]=j即将text1的前i个删除需要i次操作,将text2的前j个插入需要j次操作;
  2. 【核心决策】
  • 如果text1[i-1]==text2[j-1],不用操作,最小操作距离与上一次(dp[i-1][j-1])相同;

  • 如果不相同考虑三种操作:
    删除text1的当前字符,text1待操作数-1,dp[i-1][j]+1;
    text1当前位置插入字符,text2待操作数-1,dp[i][j-1]+1;
    替换text1和text2的当前字符,text1和text2待操作数都-1,dp[i-1][j-1]+1


  • 【代码实现】
def edit_distance(text1, text2):
    n,m=len(text1), len(text2)
    dp=[[0] * (m + 1) for _ in range(n + 1)]
    for i in range(1, n + 1):
        dp[i][0] = i
    for j in range(1, m + 1):
        dp[0][j] = j
    for i in range(1, n + 1):
        for j in range(1, m + 1):
            if text1[i - 1] == text2[j - 1]:
                dp[i][j] = dp[i - 1][j - 1]
            else:
                dp[i][j] = min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) + 1
    return dp[n][m]

3.2 抽象语法树结构提取

  • 【原理】
  1. 利用python自带ast库和tree_sitted解析器建树,在内存中是一颗语法树,并获取根结点(整个代码文件)。

  2. 编写walk()函数 对AST进行深度优先遍历(DFS),为先序遍历,先访问根,再访问孩子节点,seq.append() 是python中向列表list尾部添加节点的操作。

  • python部分:
    其中,,type(node) 返回数据类型,如是ast.If模块还是ast.While模块;
    ast.iter_child_nodes(node)模块是python自带遍历所有孩子结点的迭代生成器;

  • c/c++部分:
    其中,tree_sitted库中,node.type是提取结点类型功能,node.children是遍历子节点,增加过滤集合important_types,只保留要用的模块(if、while、for 等),丢弃变量名、常量、括号表达式等表面信息。

  1. 把遍历结果线性存储在sqe[]列表中,作为后续LCS的输入。

  2. 未知语言放入list()中变成单字符列表,复用LCS。


  • 【结构提取模块(支持python、c/c++、未知语言)】
  • 【特别注意】 以下是最初版本,已经可以实现同种语言的比较,但鉴于存在不同语言之间的抄袭,所以在ai的帮助下,实际的walk()函数还另加了映射功能,对于python和cpp不同的代码源也可以进行比较。
def get_structure_sequence(code):
    lang = detect_language(code)
    if lang == "python":
        try:
            tree = ast.parse(code)
        except SyntaxError:
            return []
        seq = []

        def walk(node):
            seq.append(type(node).__name__)
            for child in ast.iter_child_nodes(node):
                walk(child)
        walk(tree)
        return seq

    elif lang == "cpp":
        tree = cpp_parser.parse(bytes(code, "utf-8"))
        root = tree.root_node
        seq = []
        def walk(node):
            important_types = {
                'if_statement', 'while_statement', 'for_statement',
                'do_statement', 'switch_statement', 'case_statement',
                'function_definition' ,'compound_statement' 
            }
            if node.type in important_types:
                seq.append(node.type)
            for child in node.children:
                walk(child)
        walk(root)
        return seq

    else:
        return list(code)

3.3 相似度计算

  • LCS计算相似度,利用数学公式 (2*len(LCS))/(len(t1)+len(t2))。
  • 最短编辑距离的相似度,利用公式1-(编辑距离dist/两份文本的最长距离)
  • AST线性化后复用LCS

传统文本比较

def text_similarity(text1, text2):
    #LCS长度转换成0-1之间的相似度
    if not text1 and not text2:
        return 1.0
    if not text1 or not text2:
        return 0.0
    lcs_len = lcs_length(text1, text2)
    #LCS相似度
    lcs_sim = (2.0 * lcs_len) / (len(text1) + len(text2))
    # 编辑距离相似度
    dist = edit_distance(text1, text2)
    max_len = max(len(text1), len(text2))
    edit_sim = 1 - (dist / max_len)
    # 取平均值
    return (lcs_sim + edit_sim) / 2


AST

def ast_similarity(code1, code2):

    #两段代码的列表
    seq1 = get_structure_sequence(code1)
    seq2 = get_structure_sequence(code2)

    # 如果任意一个序列为空
    if not seq1 or not seq2:
        return 0.0

    # 复用 LCS 算法
    lcs_len = lcs_length(seq1, seq2)
    return (2.0 * lcs_len) / (len(seq1) + len(seq2))

3.4 其余模块概览

文件与目录操作函数(4个)

  1. get_run_dir()
  2. list_run_dirs()
  3. save_code_to_file()
  4. save_results_to_files()

查重比较功能函数(4个)

核心都是调用compare_two(),下文第四节会给出。

  1. compare_pasted_codes()粘贴两份代码,即时进行比较查重:

  2. compare_files()输入两个文件路径下的代码源比较

  3. batch_compare_folder()比较一个文件夹中所有的代码

  4. compare_multiple_files()自由输入多个(>=2)文件路径下的源代码进行比较



四、运行效果与对比

4.1 运行结果

4.1.1 运行run.py的主菜单图

终端菜单

4.1.2 查重结果

查重结果

4.1.3 AST结构可视化

AST结构可视化

4.2 对比

4.2.1 比较函数

from core.text_check import text_similarity
from core.ast_check import ast_similarity

def compare_two(code1, code2):
    text_score = text_similarity(code1, code2)
    ast_score = ast_similarity(code1, code2)
    return text_score, ast_score

4.2.2 文本 VS AST 对比分析

比较对象 文本相似度 AST相似度
origin.py VS extra_steps.py 89.34% 100.00%

由此分析,在原始代码上增加步骤不会改变逻辑结构,仅管文本不完全相同,但是代码逻辑完全相同,存在抄袭嫌疑。AST比较会优于文本基准比较。


五、LLM辅助生成报告

5.1 设计思路

传统查重系统输出的是数字和md表格,缺乏对数据的解释与总结。此次课程设计通过调用本地部署的 Qwen3.5-0.8B 模型,将查重结果(report.md)作为输入,自动生成一份结构化的分析报告。


5.2 调用和提示词设计

  • 定位到用户指定的 run_时间戳/ 目录,读取其中的.md文件;
  • 提示词“你是一个代码查重和数据分析专家,擅长生成学术报告,对每一组数据你都会进行分析,反思与总结”,具体部分包含总结表格、案例分析、结论和建议等。
  • 通过 OpenAI 兼容接口连接本地 LLM Studio,发送请求并获取生成的报告内容。生成的报告保存为 llm_analysis_report.md,存放在对应的结果目录中。

5.3 生成报告示例

llm报告


六、反思与总结

6.1 反思

历时一周,算是基本完成了基于抽象语法树的查重系统。
刚开始选择python是一味跟风,以为自己会用来画画图什么的,但是我还是选择了做“树”相关的题目,没想到会带来那么大的便捷。
python里有直接把代码变成抽象语法树的函数ast(针对python)、tree_sitter(针对c/c++),极大简化了建复杂树的过程,实现用LCS计算相似度公式计算传统文本和AST结构的相似度。
在基础逻辑上,感受了把立体树转化成列表的映射关系,体会到了数据结构与计算机和数学的紧密结合(作为数学不太好,计算机也不精通的网安小生来说,哭晕了,还好有亲爱的ds老师辅助,完成了此次课设!!!)
同时,如果不用这个ast库把树扁平化成线性列表,计算树的编辑距离会更精确,我能在原来的基础上作出改变吗?


6.2 总结

  1. 系统完成情况

系统成功实现了基于抽象语法树的代码查重功能,支持四种输入模式:粘贴代码、两个文件、批量文件夹、自定义多文件。系统同时实现了文本基准算法(LCS + 编辑距离)和AST结构算法,通过两种比对模式并行输出两种相似度,并支持AST树形结构的终端可视化。

  1. 数据结构实践

本次课设重点实践了两种数据结构:
树(多叉树):将源代码建构成AST,通过DFS深度优先遍历提取控制流节点,体现了树形结构在代码逻辑建模中的应用;
线性表(顺序存储):LCS和编辑距离的DP二维表、结构序列seq列表,实现了树→序列的降维转换。

  1. 对比结论

通过实验验证,在变量重命名、增加冗余步骤等场景下,AST相似度能保持100%,而文本相似度因字符变化而下降。这说明AST方法能有效抵抗表面修改,更准确地识别逻辑抄袭。

  1. 可改进方向

当前AST解析依赖ast和tree-sitter库,若时间充裕,可尝试手写简化版解析器,进一步体现“建树”过程。


七、附件及声明

【声明】 本实验和报告含有AI辅助成分
【课程设计任务书E题】
【ai辅助编写报告的建议】https://www.cnblogs.com/zhrb/p/19868133


posted on 2026-06-30 22:43  豆芽菜zzzz  阅读(25)  评论(1)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2026
浙公网安备 33010602011771号 浙ICP备2021040463号-3