基于Python的论文查重系统:核心算法(余弦相似度、SimHash、编辑距离)与工程实践 📅 发布时间:2026/9/20 17:11:04 👁 浏览次数: 简介这套基于Python的论文查重系统源码面向高校学生、科研人员及期刊审核者可帮助用户快速检测论文与参考文本的重复程度。系统以Simhash算法为核心通过jieba中文分词、去标点空格等预处理步骤计算汉明距离并输出精确到小数点后两位的相似度分数。压缩包共收录7个文件除Python主程序外还包括依赖清单、多个TXT样例数据、性能分析图及项目说明文档包体仅532KB轻便易读。已有174人学习下载适合作为毕业设计、课程项目或日常论文自查工具。通过完整源码与配套说明读者可直观理解相似度计算流程并能替换文本数据直接复用是学习文本相似度算法的实用范例。 毕业设计季论文查重几乎是每个高校学生都绕不开的四个字。我自己当年做毕设时就吃过查重率居高不下的亏后来在实验室帮导师预检论文干脆自己动手从源码级别写了一套基于Python的论文查重系统。这个项目迭代了好几个版本从最初只有一段相似度计算脚本到最后整理成带Web界面、支持PDF和Word上传、能输出标红报告的完整项目前前后后踩了不少坑。这篇博文就把整个系统的源码思路、核心算法和部署细节一次性讲透适合正在做毕设、想参考查重系统怎么实现的同学也适合单纯想了解查重到底怎么算重复的读者。1. 论文查重的核心命题什么算重复1.1 从字符串相等到语义相似的距离先解决一个基本问题论文查重系统到底在比什么很多人第一反应是找一样的句子但真实场景远比这个复杂。一段文字和库里文献的关系至少存在几个层级——逐字不改的完全重复、调整了语序的重复、替换了同义词的重复、以及语义相近但表面文字完全不同的重复。早期查重系统主要靠字符串匹配也就是KMP、Rabin-Karp这类算法去匹配完全相同的片段但这种方式很容易被换几个词绕过。举个例子深度学习在图像识别中的应用和图像识别领域中深度学习的应用词完全一样、顺序变了一下字符串匹配很容易漏掉再换几个同义词比如应用换成实践字符串匹配基本就失效了。所以现代查重系统的核心思路是把文本转化为数学上可计算的向量或指纹再用相似度公式去衡量两篇文本的距离。也就是说查重本质上是信息检索领域的相似度计算问题。这套源码里我同时实现了基于词频向量的余弦相似度、基于SimHash指纹的汉明距离以及基于Levenshtein的编辑距离三种算法分别对应不同粒度的检测需求。1.2 系统要交付的三个能力抛开算法不谈一个能被实际使用的查重系统至少要完成三件事。文档接入用户上传论文系统能抽取其中的纯文本内容。电子版论文最常见的是PDF和Worddocx偶尔还有txt、markdown每种格式的解析方案都不一样。相似度计算把待查文档与本地论文库中的文档逐一比对输出一个查重率以及重复片段的定位信息。这里的难点不在算公式而在怎么让计算过程在论文库膨胀后依然可控。结果呈现不能只给一个数字要告诉用户哪些段落和哪篇文献重复、重复比例多少最好还能标红预览。这套源码的模块划分就是围绕这三件事展开的。很多人拿到代码后喜欢直接看算法文件容易忽略文档解析和结果展示这两块的工程量——实际上在真实项目里这两部分往往比算法本身更费时间后面我会分别展开讲。2. 核心算法的源码级拆解2.1 预处理分词、去停用词与特征向量化不管用哪种相似度算法第一步都是把原始文本拆成有意义的特征单元。中文不像英文天然有空格分词必须依赖分词工具。我这边用的是jieba它是目前中文场景最成熟的Python分词库不到十行代码就能完成基本处理。import jieba import re def preprocess(text): # 去除标点符号和空白 text re.sub(r[\W_], , text) # 精确模式分词返回 list words jieba.lcut(text) return words但只分词还不够。像的了在是这类停用词对相似度计算几乎没贡献反而会稀释关键词的权重。所以项目里维护了一份中文停用词表在向量化之前先把这些词过滤掉。还有一个容易被忽略的细节专业领域的术语比如深度学习卷积神经网络单纯靠jieba默认词典会被切碎成深度学习导致特征表达完全失真。我的做法是在项目根目录放了一个userdict.txt启动时通过jieba.load_userdict()加载把高频专业词先手工维护进去。这个文件看起来不起眼实际对查重结果的准确率影响非常大。2.2 余弦相似度论文间相似度的数学表达预处理完之后就到了核心的相似度计算环节。最经典也最好理解的是余弦相似度——把两篇文档分别表示成向量向量中每个维度对应一个特征词值用TF-IDF权重填充然后计算两个向量夹角的余弦值。import math def cosine_similarity(vec_a, vec_b): dot sum(vec_a.get(k, 0) * vec_b.get(k, 0) for k in set(vec_a) | set(vec_b)) norm_a math.sqrt(sum(v ** 2 for v in vec_a.values())) norm_b math.sqrt(sum(v ** 2 for v in vec_b.values())) if norm_a 0 or norm_b 0: return 0.0 return dot / (norm_a * norm_b)余弦相似度的值域在0到1之间越接近1表示两篇文本在词频分布上越相似。为什么用余弦而不是直接算向量欧氏距离因为论文篇幅差异很大长篇论文天然词频更高余弦相似度只关注方向、不受向量长度影响正好能弱化篇幅不同造成的误判。用TF-IDF而不是纯词频来填向量则是为了降低的我们研究这类高频通用词对结果的干扰。源码里我手写了简单的TF-IDF计算逻辑没有依赖sklearn主要是为了减少项目的第三方包体量也让算法过程在答辩时更好讲清楚。2.3 SimHash与汉明距离批量比对的工程选择余弦相似度虽然直观但遇到大规模论文库就头疼——每一对文档都要维护高维向量并计算内积库里有几千篇文档时计算量增长很快。工程上更常用的方案是SimHash。它的思路是把一篇文档压缩成一个64位指纹然后通过汉明距离——也就是两个指纹二进制位上不同的个数——来估计相似度位数差异越小越相似。import hashlib def simhash(words_weights): v [0] * 64 for word, weight in words_weights: # 用 md5 保证不同平台 hash 结果稳定 h int(hashlib.md5(word.encode(utf-8)).hexdigest()[:16], 16) for i in range(64): v[i] weight if (h i) 1 else -weight fingerprint 0 for i in range(64): if v[i] 0: fingerprint | (1 i) return fingerprint生成指纹后比对两篇文档只需要做一个64位整数的异或再统计二进制位中1的个数速度比余弦相似度快几个数量级。源码里我把SimHash定位成粗筛层先用它快速过滤掉明显不相关的论文再用余弦相似度对候选文档做精细比对两者配合兼顾准确率和性能。2.4 Levenshtein编辑距离补上改写抄袭的漏洞还有一个容易被忽略的算法——编辑距离也叫Levenshtein距离衡量的是从字符串A变成字符串B最少需要多少次编辑操作插入、删除、替换。它和前面两种算法的区别在于余弦相似度与SimHash都建立在词频分布上对局部改写不敏感而编辑距离直接作用于字符序列能捕捉到细颗粒度的改动。def levenshtein_distance(a, b): dp [[0] * (len(b) 1) for _ in range(len(a) 1)] for i in range(len(a) 1): dp[i][0] i for j in range(len(b) 1): dp[0][j] j for i in range(1, len(a) 1): for j in range(1, len(b) 1): cost 0 if a[i - 1] b[j - 1] else 1 dp[i][j] min( dp[i - 1][j] 1, dp[i][j - 1] 1, dp[i - 1][j - 1] cost ) return dp[-1][-1]不过编辑距离的缺点是计算复杂度是O(mn)不适合直接对整篇论文使用。在这套源码里我把它用在重复片段定位上——先用相似度算法锁定疑似重复的段落再对段落的局部文本做编辑距离计算从而标注出具体重复了哪些字句。这也是标红报告能精确到句子的原因。3. 系统架构与模块设计3.1 为什么选Flask而不是Django查重系统本质上是一个带Web界面的工具。后端框架我在Flask和Django之间纠结过一段时间最终选了Flask。原因有三点。第一这个项目以单机、小规模的内部使用为主不需要Django自带的Admin后台、ORM、用户体系等重量级组件Flask轻量一个app.py就能撑起全部路由。第二源码阅读和学习成本低答辩时讲Flask的路由实现比讲Django的中间件链容易得多。第三Flask的扩展机制灵活接入文件上传、Session、模板引擎都非常直接后期加模块也不用重构。如果你的需求是做一个多校区、多角色的完整平台那Django的成熟组件确实能省不少事。但对论文查重这个场景Flask是更合适的体量。项目用到的核心依赖总量非常克制——Flask负责路由和渲染jieba做分词pdfplumber和python-docx做文档解析numpy做矩阵计算每一层都有明确职责。3.2 数据表设计与论文库构建系统的核心数据表有三张论文信息表papers、查重记录表records和重复结果表results。论文信息表主要存论文标题、作者、摘要、抽取出的正文文本、文件存储路径、上传时间查重记录表记录每次查重的任务ID、待查文档ID、比对时间、总体相似度重复结果表则存具体哪篇文献与待查文档相似、相似度多少、重复段落起止位置。这里有个设计细节值得说正文文本字段我建议用MEDIUMTEXT而不是VARCHAR放几十万字的论文也不会溢出。另外随着上传的论文增多每次查重如果全表扫描文本字段会越来越慢所以我给papers表加了全文索引。源码里默认用的SQLite开箱即用环境简单以后要切换MySQL只需改一行数据库连接配置表结构不用动。3.3 PDF和Word文本抽取的实现方案文档解析是源码里看起来不起眼、实际非常磨人的模块。docx格式处理起来最简单python-docx库可以直接读取段落对象保留标题和正文的层级关系。PDF则复杂很多PyPDF2只能做最基础的文本提取遇到扫描版PDF本质是图片就无能为力也经常出现中文乱码。我的实践是用pdfplumber它对文字型PDF的版面还原效果明显更好还能保留文字块的坐标信息方便后续按段落切分。import pdfplumber from docx import Document def extract_text(path): text if path.endswith(.pdf): with pdfplumber.open(path) as pdf: for page in pdf.pages: page_text page.extract_text() if page_text: text page_text elif path.endswith(.docx): doc Document(path) text \n.join(p.text for p in doc.paragraphs) else: with open(path, encodingutf-8, errorsignore) as f: text f.read() return text踩过的坑有些Word文档是从WPS或老版Office保存的docx解析时会遇到奇怪的样式标签PDF里中文全角空格和换行符混在一起提取后需要做一次文本清洗否则分词结果里全是空片段。我在预处理模块里专门写了去不可见字符的逻辑处理完这些脏数据之后后续算法才开始正常工作。3.4 查重结果的可视化呈现查重报告我采用总览详情两层展示。总览页是一个结果面板列出总相似度、各文献来源的相似度排名、重复段落数用Bootstrap的进度条展示相似度等级颜色从绿到红渐变。详情页展示原文与来源文献的对照重复句子在原文中高亮标红鼠标悬停可以看到对应的来源论文标题和该句的相似度贡献。技术上就是前端渲染和后端API的配合后端把重复位置的字符区间传给前端前端用区间切分原文在标红处包一层span标签。这里有个工程技巧不要传整篇原文让前端自行查找替换直接在后台计算好每段文字的重复/非重复区间前端只负责按区间渲染。这样前端逻辑简单很多也不会因为前后端分词规则不一致导致标红位置错位。4. 把zip包跑起来环境准备与部署实测4.1 解压后的目录结构说明源码压缩包解压之后目录结构大致是这样的paper-check-system/ ├── app.py # Flask 入口 ├── requirements.txt # 依赖清单 ├── config.py # 配置项 ├── core/ │ ├── classifier.py # 余弦相似度、SimHash、编辑距离实现 │ ├── preprocess.py # 分词、去停用词、文本清洗 │ └── extractor.py # PDF/Word 文本抽取 ├── templates/ │ └── index.html # 上传页面与结果展示页 ├── uploads/ # 用户上传文档目录 ├── db/ │ ├── paper.db # SQLite 数据库 │ └── userdict.txt # 自定义专业词典 └── README.md # 使用说明这个结构是我在移动和部署过多次之后定下来的算法、预处理、解析三个模块独立成文件方便单独调试config和数据库放一起换机器时只需要复制整个文件夹。4.2 依赖安装的坑与版本锁定依赖清单里包含flask、jieba、pdfplumber、python-docx、numpy等常用包。理论上直接执行pip install -r requirements.txt一次就能装完但实际有几点容易踩。pdfplumber依赖pdfminer.six某些环境下会安装失败建议先单独执行pip install pdfminer.six再装pdfplumber。numpy版本不要追求最新老项目里如果用了旧接口numpy 2.x可能有兼容问题建议锁定requirements.txt里的具体版本号。jieba首次运行会加载默认词典在性能较差的机器上可能要几秒甚至更久这是正常现象不是死循环。安装速度慢的话可以考虑用国内镜像源pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple。装完依赖后运行python app.py看到Running on http://127.0.0.1:5000就说明服务起来了。4.3 本地验证流程服务起来后浏览器访问本地地址进入上传页面。我的验证习惯是准备三篇测试文档一篇和库里已有论文几乎相同的、一篇做了部分同义词替换的、一篇主题相关但内容完全不同的。分别上传观察系统输出的相似度是否符合预期——几乎相同的那篇应该在90%以上同义替换的在60%-80%无关文档低于20%。需要提醒的是首次查重会经历分词和向量化的过程如果传的是几十页的PDF加上库里有几百篇论文响应时间会比较长。如果只是调试建议先用几千字的短文档验证链路通不通再上大文件压测。界面上我加了任务队列提示避免用户把等待误认为系统卡死。5. 调优、踩坑与二次开发方向5.1 查重率忽高忽低的问题排查我在实际调试中遇到过不少结果不合理的情况大部分原因不在算法而在预处理。最常见的是停用词表不合适——有些项目把提出本文方法这类论文高频词也停掉了结果真正表达核心内容的词没剩几个相似度被严重稀释。其次不同来源的PDF抽取出来的排版信息不同有的每行末尾带换行有的把段落合并成一大坨这些都会影响分词粒度。现象常见原因排查方向相似度普遍过高停用词过滤不充分通用词权重过大检查停用词表相似度普遍过低分词过细专业术语被拆散加载自定义词典PDF提取后文本乱码或缺失扫描版PDF、编码问题换pdfplumber或OCR方案响应越来越慢论文库膨胀、未建索引缓存向量结果、SimHash粗筛排查思路也很直接先把预处理结果打印出来人眼扫一遍分词后的词序列基本就能定位问题出在哪一层。很多看起来玄学的查重问题最后都能在分词阶段找到答案。我在项目里加了一个调试模式环境变量设成DEBUGTrue时接口会把分词结果跟着响应一起返回就是这个目的。5.2 长文本比对性能优化实践对长论文做两两余弦相似度计算时间主要消耗在构建向量和遍历词表上。我这边做了三层优化。第一层上传文档时就把分词和TF-IDF向量结果缓存到数据库同一篇文档多次比对不用重新分词。第二层用SimHash粗筛把需要精细计算余弦相似度的候选集从全部论文缩减到前N篇最相近的论文。第三层用numpy的矩阵运算替代纯Python的字典遍历小批量文档的相似度计算可以一次性用矩阵乘法完成。实测效果论文库到1000篇左右时不优化的情况下单次查重可能要等一分钟以上做完三层优化之后同一场景基本能压到几秒。对于毕设演示和个人自检来说这个性能表现已经完全够用了。5.3 从查重工具到查重平台的扩展思路这套系统目前是单机、单用户、内部论文库的模式稍微改造一下就能变成一个可用的查重平台。可以扩展的方向包括接入更多论文格式LaTeX源码、markdown增加用户登录和查重历史管理论文库支持定时增量导入把查重结果导出为PDF或Excel报告。算法层面如果论文库足够大可以尝试用embedding向量替换词频向量做语义级相似度匹配能捕捉到更多同义改写的重复。但要注意语义向量模型的资源消耗比传统算法高不少先跑通传统算法再考虑升级是更务实的路径。最后说点个人的体会。论文查重系统这类项目难点从来不在某一个算法本身而是整个链路——从文档解析、文本清洗、算法选型到前端展示每一环都会影响最终结果的可用性。如果你准备拿这套源码做毕设我的建议是别停留在能跑起来这一步花时间把预处理和报告呈现两个模块打磨扎实答辩时能讲出来的技术深度会完全不一样。遇到问题多打印中间结果大部分所谓查重结果不对的问题都能在预处理阶段找到根源。本文还有配套的精品资源点击获取