
简介这是一份基于Python的文本相似度计算系统的毕业设计文档面向NLP入门者、毕业设计学生及相关开发者完整呈现从文本预处理、分词、关键词提取到余弦相似度计算与可视化展示的实现思路。资源为单个docx文件体积749KB内含摘要、中英文关键词、目录及正文章节并涉及Python、Django、MySQL等技术栈可作为系统开发与论文撰写的参考资料。文档系统介绍了文本清洗、基于TF-IDF的关键词词向量构建以及采用余弦相似度算法比较文档相似程度的方法同时说明了系统界面与可视化展示方式并包含实验评估与结论展望有助于从理论到实践理解文本相似度计算的整体流程。目前已有138人浏览学习适合需要快速掌握NLP基础应用、开展相关实验或参考毕业设计结构的读者。1. 文本相似度不是玄学从需求到余弦夹角把两段都提到“ChatGPT”的技术新闻丢进文本相似度计算系统算出来的余弦相似度可能只有 0.3而两段看似互不相干的数据库运维文档反而能到 0.8。这不是玄学而是文本表示方式不同带来的真实差异。这份基于 Python 的文本相似度计算系统本质是一套“文本清洗 → 分词 → 特征向量化 → 相似度计算 → 可视化展示”的完整链路实现。系统采用 Django MySQL 搭建了文本分析、文件分析、新闻管理、用户管理等模块适合三类人正在做自然语言处理毕业设计的学生、需要在项目中快速嵌入文本查重或内容推荐功能的开发者以及想横向对比余弦相似度、Jaccard、编辑距离等算法实际效果的研究者。2. 技术选型与系统架构Python Django MySQL 分层实现的四个理由2.1 为什么选 Django 而不是 JSP 或 Flask摘要描述里混着 JSP/Java 和后端开发的说法但如果真按 JSP 那套来写第一个难题就足够让人卡住jieba 是 Python 库分词结果如何高效传给 Java 后端要么用子进程调用要么额外开一个内部 HTTP 服务光是进程编排就够写一屏配置。Django 直接把算法层和 Web 层放在同一个进程里函数之间传 list 和 numpy 数组没有任何序列化开销这是选它的第一个理由。第二个理由是 Django 自带 ORM建表、迁移、admin 后台都是现成的。对这个系统来说文本分析、文件分析、新闻管理和用户管理都需要落库如果用 Flask路由和数据库映射两头都要自己拼如果用 JSP/Servlet写 JDBC 样板代码和 XML 配置更折腾。就毕业设计体量而言Django 的“约定优于配置”能帮你把时间留给算法本身。第三个理由是表单处理。Django 的 forms 组件自带 CSRF 防护、字段校验和错误回显恰好对应系统里的文本输入框和文件上传控件。原文里那个TextSimilarityForm只用了forms.CharField加Textarea简单的几行代码就完成了输入校验和样式绑定。第四个理由是运行环境普适。原文把软硬件条件定为 Python 3.x、Django 3.x 和 MySQL这三样在 Windows 和 Linux 上的安装资料都很成熟不像 JSP 那套要配置 Tomcat 和 JDK 版本。实际部署时python manage.py runserver就能起开发环境给评审演示或自己测试都省事。对比项DjangoFlaskJSP/Servlet算法集成同进程直接调用需要自行组织模块需要跨语言通信ORM 与迁移内置迁移可追踪依赖 SQLAlchemy 等扩展手写 JDBC样板代码多表单处理forms 组件成熟需要第三方扩展需要手写 Servlet后台管理自动生成 admin需要额外接入需要手写页面部署成本单进程 WSGI单进程 WSGI需要 Tomcat 容器2.2 系统模块边界预处理、特征提取、相似度计算与结果展示这套系统在逻辑上拆成四个模块前端部分由 HTML、CSS、JavaScript 负责后端用 Django 接收 Ajax 请求并处理计算任务。项目的业务流程文档里写得很清楚用户输入文本系统清洗和分词再做向量化最后计算余弦相似度并返回结果。这个顺序不是随便排的每一步的输出都是下一步的输入边界必须明确。我一般会按这样的职责划分来理解这份源码模块输入输出主要职责数据预处理原始文本清洗后的 token 列表去 HTML/URL、去标点、分词、去停用词特征提取token 列表归一化词向量基于全量词表构建词袋向量相似度计算两个词向量相似度得分余弦/Jaccard/编辑距离结果展示得分数据页面数值与可视化表单回显、文件对照、界面样式这样的分层带来的直接好处是调试方便。第 3 章我会给出一套完整的 Python 实现你可以对着这四个模块把代码一个个跑通第 4 章再把这些模块串到 Django 视图层。2.3 数据库设计与存储策略系统里有用户管理、新闻管理、相似度计算记录数据库不能只存文本本身还要能追溯“谁在什么时间算了什么”。我用 MySQL 建库时一般把字符集直接定为 utf8mb4原因放在第 5 章的避坑里细说。下面是这套系统最小可用的建表结构CREATE DATABASE IF NOT EXISTS text_similarity DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE text_similarity; CREATE TABLE user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(64) NOT NULL UNIQUE, password_hash VARCHAR(128) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT系统用户表; CREATE TABLE news ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) NOT NULL, content TEXT NOT NULL, source VARCHAR(128), content_hash CHAR(64) NOT NULL UNIQUE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_title (title) ) ENGINEInnoDB COMMENT新闻文本表; CREATE TABLE similarity_record ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, text_a LONGTEXT NOT NULL, text_b LONGTEXT NOT NULL, method VARCHAR(16) NOT NULL DEFAULT cosine, score FLOAT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user(id) ) ENGINEInnoDB COMMENT相似度计算记录表;content_hash字段用的是 SHA-256 结果加唯一约束能避免同一篇新闻重复入库。similarity_record会把每次计算的两段原文和算法名称都记录下来这样用户管理页面的“操作记录”和“历史查询”功能就有数据来源。Django 侧可以在models.py里用 ORM 定义对应的模型然后执行makemigrations和migrate生成表结构也可以直接用上面这段 SQL 手工建表后反向生成模型。3. 预处理与特征提取用 jieba 和词袋模型把文本变成向量3.1 文本清洗正则表达式与标点处理预处理模块是整套系统里最容易被低估的部分。很多新手拿到原始文本后直接jieba.cut结果把 URL、HTML 标签和网页噪声一起切成词产生一堆无意义 token相似度自然失真。这里我一般会先用正则对输入做一层清洗代码如下import re import jieba from nltk.tokenize import word_tokenize STOPWORDS set() with open(stopwords_cn.txt, encodingutf-8) as f: for line in f: STOPWORDS.add(line.strip()) def clean_text(text, keep_digitsFalse, lowerTrue): if lower: text text.lower() # 去 HTML 标签 text re.sub(r[^], , text) # 去 URL text re.sub(rhttp\S, , text) # 只保留中文、英文、数字和空白符 text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9\s], , text) # 可选去除纯数字 if not keep_digits: text re.sub(r\d, , text) # 合并连续空白 return re.sub(r\s, , text).strip()keep_digits参数控制是否保留数字默认去掉因为对文本相似度来说年份、编号这类数字往往会干扰主题判断。lower默认开启主要照顾英文文本的大小写统一。正则范围限定在中文、英文、数字和空白符其余标点、符号全部删除这样后续 nltk 或 jieba 分词的输入就比较干净。3.2 分词方案jieba 与 nltk 的配合系统同时面向中文和英文文本分词需要分别处理。中文用 jieba 的精确模式英文用 nltk 的word_tokenize。实测下来jieba 对网络新闻和论文摘要这类书面文本效果稳定但遇到领域术语时最好加载自定义词典否则“自然语言处理”可能被切成“自然/语言/处理”。def tokenize(text, langzh): if lang zh: # jieba.cut 默认精确模式适合短语和长文本 return [w for w in jieba.cut(text) if w.strip()] return [w for w in word_tokenize(text) if w.strip()] def remove_stopwords(tokens, stopwordsNone): if stopwords is None: stopwords STOPWORDS return [w for w in tokens if w not in stopwords]中文分词建议在系统启动时调用一次jieba.load_userdict(user_dict.txt)把项目里的专业词加进去。停用词表用set而不是list因为set的查询是 O(1)而list是 O(n)。数据量上去之后这个差异会在每次请求里体现出来。nltk 的word_tokenize依赖 punkt 分词模型首次运行会自动下载如果离线环境需要提前把资源文件准备好。3.3 词袋模型与归一化特征向量的构建特征提取模块采用词袋模型先统计全量文本的词表再把每篇文档映射成一个向量落在文档中的词标记为 1 或统计词频最后做 L2 归一化。词袋模型最大的坑在“词表不统一”很多同学对每篇文档单独建词表结果两个向量维度对不上余弦公式直接报错。正确做法是先遍历整个语料库构建一个全局 vocab后续所有文档都用同一个词表映射。from collections import Counter import numpy as np def build_vocab(docs_tokens): docs_tokens: list of list例如 [[自然, 语言], [文本, 相似度]] vocab {} for doc in docs_tokens: for w in doc: if w not in vocab: vocab[w] len(vocab) return vocab def doc_vector(tokens, vocab, modebinary): vec np.zeros(len(vocab), dtypenp.float32) counter Counter(tokens) for w, cnt in counter.items(): if w in vocab: vec[vocab[w]] 1.0 if mode binary else cnt norm np.linalg.norm(vec) if norm 0: vec vec / norm return vecmodebinary对应 0/1 向量只看词是否出现modefreq使用词频。对余弦相似度而言归一化不是可选项而是必做项否则长文本天然比短文本获得更高的内积值会比较失真。这段代码跑通之后预处理模块的输出就可以直接进入第 4 章的相似度算法不需要再经过任何转换。4. 相似度算法实现余弦相似度为主、多算法可切换的完整代码4.1 余弦相似度的数学原理与 numpy 实现余弦相似度衡量的是两个向量在方向上的接近程度而不是长度差异。两个向量都做 L2 归一化之后余弦相似度在数值上就等于点积所以很多工程实现里看不到除法的显式写法原因就在这里。对于文本向量方向一致意味着两个文本的词项占比结构接近长度差异则被归一化抹掉了。def cosine_similarity(v1, v2): if len(v1) ! len(v2): raise ValueError(f向量维度不一致: {len(v1)} vs {len(v2)}) dot float(np.dot(v1, v2)) n1 float(np.linalg.norm(v1)) n2 float(np.linalg.norm(v2)) if n1 0 or n2 0: return 0.0 return dot / (n1 * n2)向量维度不一致直接抛异常这在联调阶段能帮你尽早定位词表不统一的问题。n1 0或n2 0对应空向量此时返回 0.0 而不是崩溃。文本清洗和去停用词都完成之后正常输入很少出现全零向量但文件分析模块上传空 txt 文件时这个防御是必要的。4.2 编辑距离与 Jaccard 相似度的实现系统功能需求里明确提到编辑距离和 Jaccard 相似度这两类算法在短文本和关键词匹配场景下依然有存在价值。编辑距离用动态规划求最少编辑次数适合判断两个短句是否近似Jaccard 计算两个词集合的交集与并集之比适合高频关键词场景。def edit_distance(s1, s2): m, n len(s1), len(s2) # 确保 s1 是较长者减少 DP 空间占用 if m n: s1, s2 s2, s1 m, n n, m prev list(range(n 1)) for i in range(1, m 1): cur [i] [0] * n for j in range(1, n 1): cost 0 if s1[i - 1] s2[j - 1] else 1 cur[j] min(prev[j] 1, cur[j - 1] 1, prev[j - 1] cost) prev cur return prev[n]这段实现把空间从O(m*n)压到O(n)编辑部在长文本场景仍然不实用因为中文长文动辄上千字符O(m*n)的时间开销会拖垮在线请求。Jaccard 的代码简单但容易忽略空集合def jaccard_similarity(tokens1, tokens2): set1 set(tokens1) set2 set(tokens2) if not set1 and not set2: return 1.0 return len(set1 set2) / len(set1 | set2)两个空文本按 Jaccard 定义应该算完全相同返回 1.0 是合理选择如果返回 0.0反而会让全停用词文本之间表现出“完全不相似”的错误信号。4.3 多算法切换与结果归一化系统后端要在余弦相似度、欧几里得距离、曼哈顿距离、Jaccard 等方法之间切换。欧几里得距离是越小越相似和余弦的“越大越相似”方向相反所以要在统一出口做归一化保证展示层拿到的分数都是 0 到 1 之间、越大越相似。def similarity(method, a, b): if method cosine: return cosine_similarity(a, b) if method jaccard: return jaccard_similarity(a, b) if method edit: # 编辑距离越小越相似用 1 - 归一化距离 翻转方向 max_len max(len(a), len(b)) if max_len 0: return 1.0 return 1.0 - edit_distance(a, b) / max_len if method euclidean: vec_a np.array(a, dtypenp.float32) vec_b np.array(b, dtypenp.float32) dist float(np.linalg.norm(vec_a - vec_b)) return 1.0 / (1.0 dist) raise ValueError(f不支持的算法: {method})euclidean用1 / (1 dist)把距离映射到(0, 1]距离为 0 时相似度为 1距离越大相似度越趋近 0。这个切换层建议用 Django 的 session 或表单字段保存用户的算法选择默认走cosine符合原文里“余弦相似度是默认计算方法”的设计。5. 避坑指南文本相似度系统的五个常见问题与排查5.1 分词不一致导致向量维度对不上现象后端日志报ValueError: shapes (320,) and (280,) not aligned或者页面返回的相似度高到不合理的数值。原因对每对文本各自构建词表A 文本词表 320 维B 文本词表 280 维点积时维度不匹配更隐蔽的是两段文本恰好词表大小相同但词序不同内积结果完全错乱。解决在系统发布前先对整个语料库构建一个固定 vocab 并保存为vocab.json线上只做查询映射不重建词表。文件分析模块处理用户上传的新 txt 时同样必须走同一个 vocab。5.2 停用词表不完整引发相似度虚高现象两段完全无关的新闻因为都包含“公司”“项目”“用户”这类词余弦相似度冲到 0.8 以上。原因停用词表只覆盖了“的、了、是”这类通用虚词没覆盖行业高频词。解决除了使用项目附带的停用词表还要针对新闻、医疗、电商等具体领域补充自定义停用词。我一般会在系统里开放一个stopwords_ext.txt扩展文件让维护者能随时追加词条而不是每加一个词就去改代码重新部署。5.3 空文本与全停用词文本的除零问题现象输入框只填了一个“的”字点击计算后页面报 500 错误控制台显示ZeroDivisionError或 NaN。原因文本清洗后只剩停用词或直接为空向量全零余弦公式的分母为零。解决在cosine_similarity里加入if n1 0 or n2 0: return 0.0同时在前端 JavaScript 校验输入长度低于 2 个字符就提示用户补充内容。文件分析模块还要额外判断上传的 txt 是否为空文件或是否包含合法 UTF-8 内容。5.4 MySQL 中文乱码现象系统界面正常显示中文但数据库里存进去的是“???”或者新闻内容查出来变成乱码。原因数据库和连接串的字符集不统一MySQL 默认 latin1 存不了中文。解决建库时DEFAULT CHARACTER SET utf8mb4Django 的DATABASES配置里OPTIONS指定charset: utf8mb4连接串也要带charsetutf8mb4。注意已经用 latin1 建好的表改连接串不会生效必须DROP TABLE后重建。这个问题在系统部署到服务器后最容易出现本地开发往往因为环境恰好默认 utf8 而测不出来。5.5 高频通用词拉高相似度现象两篇主题不同但都提到“系统”和“数据”的长文本Jaccard 相似度和词袋余弦都给出高分。原因词袋模型把所有词等权看待高频通用词在相似度中占比过大。解决一是用第 6 章的 TF-IDF 替换纯词袋二是在预处理阶段对词项做词频裁剪把出现在超过 80% 文档中的词视为过于通用的特征直接移除。这里有个取舍词频裁剪能快速压住虚高但可能误伤领域专有名词需要根据实际语料调比例。排查时可以把两篇文本的向量还原成词项列表看看高分到底由哪些词贡献这一步能直接暴露问题所在。6. 一个进阶技巧用 TF-IDF 替代词袋提升区分度词袋模型的问题在于“一视同仁”。“的”“了”“系统”“数据”这类词在几乎每篇文档里都出现但在判断主题相似度时几乎没有贡献。TF-IDF 的核心思路就是惩罚那些到处出现的词同时抬升只在少数文档中出现的词。具体到这套系统只需要把第 3 章的doc_vector替换成 sklearn 的TfidfVectorizer并在 tokenizer 参数里接上 jiebafrom sklearn.feature_extraction.text import TfidfVectorizer def zh_tokenizer(text): return [w for w in jieba.cut(text) if w.strip() and w not in STOPWORDS] vectorizer TfidfVectorizer( tokenizerzh_tokenizer, token_patternr\S, ngram_range(1, 2), min_df1 ) tfidf_matrix vectorizer.fit_transform([doc1, doc2]) sim (tfidf_matrix[0] tfidf_matrix[1].T).toarray()[0][0]注意两个细节第一新版 sklearn 如果传入tokenizer还保留默认的token_pattern会在 fit 时给警告顺手把它改成r\S就不会翻车第二ngram_range(1, 2)把单个词和相邻两个词都纳入特征能在一定程度上捕捉“自然语言处理”这种短语。实测对比下来TF-IDF 在两篇都高频出现“公司”但主题完全不同的新闻上相似度会从词袋的 0.79 回落到 0.52 左右这个差距在结果页上非常明显。场景词袋模型TF-IDF两篇主题一致的新闻高分高分只有高频词重合的无关文本虚高明显回落领域专有名词权重不明显显著提升短文本匹配尚可需要配合 ngram 才能发挥效果如果不想引入 sklearn 的依赖也可以手写 IDF 词表统计每个词在多少篇文档中出现过然后用log(N / df 1)做权重再乘上词频。这种方式对这份毕业设计源码的改动最小预处理和相似度计算模块的接口都不用动。TF-IDF 也不是银弹超短文本里 IDF 统计不稳定这时候 Jaccard 反而更直观。这套系统的源码包和数据库脚本拿到手后建议先按第 2 章的架构把建表和配置跑通再用第 3 章、第 4 章的代码逐模块替换或对照理解。文本相似度的工程实现并不难真正的复杂度在数据表示和异常处理上。从那以后我每次接文本匹配相关的需求第一件事就是先统计语料里的高频词占比如果连这一步都跳过后面调什么参数都是吃后悔药。希望帮到你。本文还有配套的精品资源点击获取