课程设计-基于抽象语法树的代码查重系统
课程设计
| 项目名称 | 基于抽象语法树的代码查重系统 |
|---|---|
| 课程名称 | 数据结构 |
| 使用语言 | python |
| 操作者 | 豆芽菜 |
| 课设结束时间 | 2026.07.03 |
一、背景与问题
随着越来越多的编程课程与在线评测系统(如 PTA)的普及,学生代码提交量增加,代码抄袭现象比较突出。传统的代码查重方法多基于文本字符串比对(如最长公共子序列 LCS 和编辑距离),但这类方法极易被简单的代码变换手段规避,比如变换变量的名字、改变语句顺序等。这些变化之后其逻辑结构并没有改变,所以仅靠文本相似度查重依据容易出现漏洞。
抽象语法树AST是将表达式、语句、函数定义等程序语句抽象为一棵树的结点,将代码转换成AST之后判断逻辑结构的方法,更加健壮也更为大家所认可。
二、系统设计
基本思路
- 【数据输入】
- 支持四种灵活输入模式:终端粘贴代码片段;指定两个本地代码文件;扫描整个文件夹批量比对;自定义多文件列表。
- 每次运行自动创建带时间戳的独立结果目录(results/run_时间戳/),确保历史记录不覆盖、不丢失。
- 自动识别代码语言(Python / C / C++)
- 【核心算法】
-
文本基准: 实现LCS和最短编辑距离,将代码当作纯字符列表分析计算相似度;
-
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 最大公共子序列长度
- 【原理】
-
建列表dp[m+1][n+1],预留出[0][0]空间,当输入字符串为空时,公共子序列长度为0;其中dp[i][j]表示的是text1的前i个元素和text2的前j个元素,dp[i-1][j-1] 不是“一个字符”或者“数值”,它里面存的可能是 0、1、2、3,是前面一大段比对结果累加好的总和。
-
比较两个文本text1[i-1]和text2[j-1];
-
【核心决策步骤:动态规划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 最短编辑距离
- 【原理】
- 建列表dp[m+1][n+1],其中,[i]、[j]是指剩余未处理的字符长度,初始化dp[i][0]=i,dp[0][j]=j即将text1的前i个删除需要i次操作,将text2的前j个插入需要j次操作;
- 【核心决策】
-
如果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 抽象语法树结构提取
- 【原理】
-
利用python自带ast库和tree_sitted解析器建树,在内存中是一颗语法树,并获取根结点(整个代码文件)。
-
编写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 等),丢弃变量名、常量、括号表达式等表面信息。
-
把遍历结果线性存储在sqe[]列表中,作为后续LCS的输入。
-
未知语言放入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个)
- get_run_dir()
- list_run_dirs()
- save_code_to_file()
- save_results_to_files()
查重比较功能函数(4个)
核心都是调用compare_two(),下文第四节会给出。
-
compare_pasted_codes()粘贴两份代码,即时进行比较查重:
-
compare_files()输入两个文件路径下的代码源比较
-
batch_compare_folder()比较一个文件夹中所有的代码
-
compare_multiple_files()自由输入多个(>=2)文件路径下的源代码进行比较
四、运行效果与对比
4.1 运行结果
4.1.1 运行run.py的主菜单图

4.1.2 查重结果

4.1.3 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 生成报告示例

六、反思与总结
6.1 反思
历时一周,算是基本完成了基于抽象语法树的查重系统。
刚开始选择python是一味跟风,以为自己会用来画画图什么的,但是我还是选择了做“树”相关的题目,没想到会带来那么大的便捷。
python里有直接把代码变成抽象语法树的函数ast(针对python)、tree_sitter(针对c/c++),极大简化了建复杂树的过程,实现用LCS计算相似度公式计算传统文本和AST结构的相似度。
在基础逻辑上,感受了把立体树转化成列表的映射关系,体会到了数据结构与计算机和数学的紧密结合(作为数学不太好,计算机也不精通的网安小生来说,哭晕了,还好有亲爱的ds老师辅助,完成了此次课设!!!)
同时,如果不用这个ast库把树扁平化成线性列表,计算树的编辑距离会更精确,我能在原来的基础上作出改变吗?
6.2 总结
- 系统完成情况
系统成功实现了基于抽象语法树的代码查重功能,支持四种输入模式:粘贴代码、两个文件、批量文件夹、自定义多文件。系统同时实现了文本基准算法(LCS + 编辑距离)和AST结构算法,通过两种比对模式并行输出两种相似度,并支持AST树形结构的终端可视化。
- 数据结构实践
本次课设重点实践了两种数据结构:
树(多叉树):将源代码建构成AST,通过DFS深度优先遍历提取控制流节点,体现了树形结构在代码逻辑建模中的应用;
线性表(顺序存储):LCS和编辑距离的DP二维表、结构序列seq列表,实现了树→序列的降维转换。
- 对比结论
通过实验验证,在变量重命名、增加冗余步骤等场景下,AST相似度能保持100%,而文本相似度因字符变化而下降。这说明AST方法能有效抵抗表面修改,更准确地识别逻辑抄袭。
- 可改进方向
当前AST解析依赖ast和tree-sitter库,若时间充裕,可尝试手写简化版解析器,进一步体现“建树”过程。
七、附件及声明
【声明】 本实验和报告含有AI辅助成分
【课程设计任务书E题】
【ai辅助编写报告的建议】https://www.cnblogs.com/zhrb/p/19868133
浙公网安备 33010602011771号