Python文章推荐系统实战:从TF-IDF到Flask部署
2026/9/24 18:10:02 网站建设 项目流程

简介:基于Python的文章推荐系统完整项目,面向毕业设计或课程设计实践,解决了从原始文章数据采集、文本预处理到推荐模型训练评估的全链路问题,适合需要综合运用爬虫、Pandas数据清洗、NLTK分词、Scikit-learn协同过滤以及Flask/Django展示的开发者。包内共35个文件,其中15个Python源码覆盖相似度计算、贝叶斯分类、标签提取等核心模块,5个shell脚本便于一键执行训练、分类与推送流程,另有配置文件、说明文档及JS辅助文件,压缩包仅89KB,轻量易部署。项目完整呈现基于内容与协同过滤的混合推荐思路,包含TF-IDF相似度计算、用户行为分析、MongoDB数据存储及推荐效果评价指标等关键环节,能帮助学习者快速理解推荐系统工程化实现。目前已有108人学习参考,对正在设计推荐系统课程项目或求职作品集的读者有较高参考价值。

1. 这个项目为什么值得做:文章推荐系统的完整套路

每年毕业设计选题里,“Python文章推荐系统”都是常青树,但真正能讲清全链路的人不多。很多人答辩时的状态是:贴一段协同过滤代码,说“这个算相似度”,然后一问“你的相似度怎么算的”“冷启动怎么办”“推荐结果怎么验证”就卡住了。问题不在算法难度,而在大多数人只背公式,没把“数据 → 向量 → 相似度 → 召回 → 展示 → 评估”这条链路走通。这篇文章就是要把这条链路拆开,让你知道每一环怎么选型、怎么落代码、怎么避坑,最终做出一个能跑、能答辩、有实际工程感的 Python 文章推荐系统——这也是你写进简历时真正能讲出细节的项目。

2. 技术选型:为什么文章推荐优先走“基于内容”,而不是协同过滤

2.1 三个方案对比:基于内容、协同过滤、混合推荐各自的边界

文章推荐系统最常见的三个方案是基于内容的推荐(Content-Based)协同过滤(Collaborative Filtering)混合推荐(Hybrid)。很多初学者上来就写协同过滤,因为教程多、公式好看,但文章场景下协同过滤有个致命前提——需要大量用户行为数据。课程设计里你没有几万条真实评分数据,冷启动直接翻车。基于内容的推荐只靠文章本身计算相似度,不依赖用户历史行为门槛更低、可解释性更强、答辩时有逻辑可讲。

方案数据依赖推荐逻辑冷启动表现答辩亮点
基于内容仅需文章文本算文本间相似度新文章立即能推可解释性极强,推荐依据直接可查
协同过滤(User/Item)用户-物品交互数据找相似用户/物品新文章无推荐有“猜你喜欢”的味道,但数据不好凑
混合推荐文本 + 行为数据加权融合两类结果依赖方案搭配综合性强,工作量容易撑起来

我一般会建议主推基于内容的文本推荐作为核心链路,再留一个接口给协同过滤做可选融合模块,这样论文工作量足够,答辩时也不会被“你的数据从哪来”问倒。文章推荐系统的本质就是“你这篇文章和哪几篇长得像”,把文本变成向量,算一算距离,Top-N 排序,这就完成了 80% 的工作。

2.2 文本向量化的层级选择:TF-IDF、Word2Vec 还是 BERT

把文章变成机器能算的向量有三个主流层级,选哪个直接决定你的项目是“会用调包”还是“有思考”。

第一层是 TF-IDF,它统计的是词频和逆文档频率,实现简单、内存可控、解释性强,是课程设计和毕业设计的稳妥起点。第二层是 Word2Vec/Doc2Vec,它通过上下文训练分布式词向量,能部分理解同义替换(“汽车”和“轿车”相近),但需要较多语料预训练,训练时间会拉长。第三层是 BERT,效果最好但显存和推理成本高,而且答辩时“你为什么不用 BERT”比“你为什么用 DICT”更难回答——局部看 BERT 能提升语义度,但项目周期和可部署性会下降。

TF-IDF 在文章推荐里的地位稳如老狗,原因很简单:文章是长文本,关键词天然具备区分度,TF-IDF 恰恰抓的就是“这篇文章里反复出现、但在别处少出现的词”。Word2Vec 更多用在短文本或搜索场景,BERT 则适合做精排。对于一篇 8000 字的文章,TF-IDF 的稀疏向量用余弦相似度计算开销远小于 BERT 做语义相似。我的做法是:TF-IDF 做召回,BERT 只放到进阶章当加分项,日常主链绝不碰大模型。

2.3 相似度度量的细节:为什么全局都用余弦相似度

向量算好后,距离度量方式也有讲究。欧氏距离看的是绝对距离,受文章长度影响极大——两篇相同主题的文章,一篇 5000 字一篇 1000 字,欧氏距离会偏大,但余弦相似度只看方向夹角,长度差异被对角公式自然归一化,因此文本场景里它的稳定性明显更好。皮尔逊相关系数适合处理带评分的数据,纯文本场景意义不大。

import numpy as np def cosine_similarity(vec_a, vec_b): # 分母加1e-9,避免零向量导致除零错误 dot = np.dot(vec_a, vec_b) norm_a = np.linalg.norm(vec_a) norm_b = np.linalg.norm(vec_b) return dot / (norm_a * norm_b + 1e-9)

这里的关键是分母的平滑项。真实语料里总会出现整篇文章的词全部出现在停用词表里、向量全零的情况,没有 1e-9 会在算相似度时报 RuntimeWarning 甚至 NaN,这一点是新手最容易忽略的。

3. 数据从哪来:公开数据集、自建语料与清洗规则

3.1 公开数据集怎么找、怎么加载到内存

做文章推荐系统首先要解决语料问题。常见的开源数据集有三类:英文的 20 Newsgroups 由 sklearn 内置下载;中文的 THUCNews 由清华自然语言处理组发布,包含几十万篇新闻文本;搜狗新闻语料虽然量大但格式老,需要花时间解压和预处理。对于快速验证逻辑,20 Newsgroups 是最省心的——一行代码拿到分类别、已分好训练集和测试集的干净语料。

from sklearn.datasets import fetch_20newsgroups # 只取四个类别,减少数据量,方便快速调试 categories = ['rec.sport.baseball', 'sci.electronics', 'talk.politics.mideast'] news = fetch_20newsgroups(subset='all', categories=categories, shuffle=True, random_state=42) print(len(news.data)) # 看一眼目前加载了多少篇 print(news.data[0][:200]) # 预览样本,确认数据格式

参数说明:subset参数有traintestall三档,做验证时建议用train训练、test评估,不然离线评估没有数据可划分。categories传类别列表用来缩小语料范围,课程设计不需要全量跑,四到五个类别就够。拿到的news.data是原始字符串列表,大小几 MB 到几十 MB 不等,一次性读入内存即可,不需要上数据库。

3.2 清洗规则:去 HTML、去特殊符号、去换行

网页抓下来的文章往往带 HTML 标签、乱码符号和长换行,不清洗直接分词会让特征里混入大量噪声。我的清洗规则按顺序执行五步:去 HTML 标签 → 去 URL → 去数字和特殊符号 → 去单字符 → 合并多余空白。每步都必须写在同一个函数里,这样后续加数据集不需要重写一次。

import re def clean_text(raw): # 去掉HTML标签,保留标签内文字 text = re.sub(r'<[^>]+>', '', raw) # 去掉URL链接 text = re.sub(r'http[s]?://\S+', '', text) # 去掉数字、字母、标点,中文场景只保留中文字符 text = re.sub(r'[^\u4e00-\u9fa5a-zA-Z]', ' ', text) # 将多个连续空白压缩为一个 text = re.sub(r'\s+', ' ', text) return text.strip()

逻辑说明:第一行正则r'<[^>]+>'是匹配以<开头>结尾的所有内容,不管标签是<p>还是<div class="x">都能覆盖。第四行\u4e00-\u9fa5表示中文字符的 Unicode 区间,这一步直接抹掉标点和数字,避免后期特征变成“2020”这类无效维度。清洗结果直接用空格连接,这正好喂给后面的 jieba 分词和 TfidfVectorizer。

3.3 jieba 分词与停用词表:三个必调参数

中文文本必须分词才能向量化,jieba 是事实标准。分词阶段有三个参数要关注:jieba.load_userdict()加载领域词典保持专业词完整性,jieba.cut(..., cut_all=False)用精确模式而非全模式,以及 HMM 开关控制是否识别未登录词。还有一套不可漏的配套动作:构建停用词表并过滤——中文里的“我们”“这个”“一个”出现频率极高但没有任何区分能力,不过滤的话相似度会被这些虚词主导。

import jieba import jieba.analyse # 加载自定义词典,词典格式:词 词频 词性,词频可以省略 jieba.load_userdict('domain_dict.txt') def tokenize(text): # 精确模式分词,HMM=True 识别词典外新词 words = jieba.cut(text, cut_all=False, HMM=True) # 过滤空白、单字和停用词 stopwords = set() with open('stopwords.txt', 'r', encoding='utf-8') as f: for line in f: stopwords.add(line.strip()) return [w for w in words if w.strip() and w not in stopwords and len(w) > 1]

参数说明:cut_all=False是精确模式,不会把“中华人民共和国”切成“中华/人民/共和/国”,而是尽量保持最长匹配。HMM=True对未录入词典的词做隐马尔可夫切分,能识别一些人名和外来词,代价是偶尔产生错误切分,对推荐场景影响不大。打印一下分词结果,确认停用词是否真的生效——这一步肉眼检查花两分钟,比后面调模型省一晚上。

提示:domain_dict.txt 和 stopwords.txt 是文本文件,按行存放。GitHub 上搜“中文停用词表”能找到现成资源,把哈工大停用词表、百度停用词表合并去重,基本覆盖常见场景。

4. 核心实现:TF-IDF 向量化到 Top-N 推荐的流水线

4.1 TfidfVectorizer 参数:max_features、ngram_range、min_df 的取舍

文本变成向量这一步直接调 sklearn 的 TfidfVectorizer,但参数不能全用默认。三个参数是必调的:max_features限制特征维度,ngram_range决定是否保留词组信息,min_df过滤只出现一两次的生僻词。

from sklearn.feature_extraction.text import TfidfVectorizer corpus = [' '.join(tokenize(doc)) for doc in news.data] # 注意:输入必须是分词后用空格连接的结果,而不是原始文档 vectorizer = TfidfVectorizer( max_features=20000, ngram_range=(1, 2), min_df=2, max_df=0.95, sublinear_tf=True ) tfidf_matrix = vectorizer.fit_transform(corpus) print(tfidf_matrix.shape) # (文章数, 特征维度)

逻辑说明:fit_transform一步完成词典构建和文本向量化,返回的是稀疏矩阵,直接用print看形状不会爆内存。如果直接把原始文章传进去而不在外部调用tokenize,TfidfVectorizer 的默认 token 模式是按空格分英文单词,中文文本会被切成单个字或整段,效果直接崩。

参数说明:max_features=20000控制特征数量,维度太大会让相似度计算变慢,对课程设计 20000 维足够;ngram_range=(1, 2)同时保留单个词和两两词组,能识别“深度学习”和“神经/网络”这类固定搭配;min_df=2表示词至少出现在两篇文章里,过滤生僻词;max_df=0.95表示出现频率超过 95% 的词直接丢掉,比如“前言”“摘要”这类每篇都有的词。sublinear_tf=True把词频用 1+log(tf) 替换,削弱高频词的绝对优势,这个参数经常被忽略但收益明显。

4.2 余弦相似度矩阵与 Top-N 推荐函数

向量化之后,下一步就是算相似度。sklearn 提供了cosine_similarity,但直接调函数前要先想清楚内存——N 篇文章的相似度矩阵是 N×N 维 float 数组,1 万篇文章就是 1 亿个浮点数,约 800MB 内存,课程设计的数据量虽然到不了这个量级,但养成良好的习惯是必要的。

from sklearn.metrics.pairwise import cosine_similarity # 一次性算全文相似度 similarity_matrix = cosine_similarity(tfidf_matrix) def recommend_by_index(article_idx, top_n=10): # 获取目标文章在矩阵中的相似度行 sim_scores = list(enumerate(similarity_matrix[article_idx])) # 排除文章自身(相似度为1.0),按相似度从高到低排序 sim_scores = sorted(sim_scores, key=lambda x: x[1], reverse=True) sim_scores = [x for x in sim_scores if x[0] != article_idx][:top_n] return sim_scores

逻辑说明:similarity_matrix[article_idx]取的是文章与其他所有文章的两两相似度,返回一维数组。enumerate把数组转成 (文章索引, 相似度) 的列表,排序后去掉自身再截前 N 个。这里的x[0] != article_idx判断条件必须写,不然自己永远排第一,推荐列表第一项永远是同一篇文章,这也是最常见的翻车点。

4.3 冷启动处理:新文章、新用户各怎么办

冷启动在毕业设计答辩里几乎是必问题。文章推荐系统冷启动有两个维度:新文章没有人看、没有行为记录,但基于内容的方法天然具备处理能力——只需把新文章分词、向量化、放进 TF-IDF 矩阵重新算相似度即可;新用户没有阅读历史,解决办法常见做法是让他注册时选择感兴趣的主题类别,用类别代表向量做推荐,或默认推荐全库覆盖度最好的文章。

def recommend_for_new_article(article_text, top_n=10): # 清洗 + 分词 + 向量化 cleaned = clean_text(article_text) tokenized = ' '.join(tokenize(cleaned)) vec = vectorizer.transform([tokenized]) # 与库里所有文章做点积,TfidfVectorizer的向量本身已经做过L2归一化 sims = cosine_similarity(vec, tfidf_matrix).flatten() top_indices = sims.argsort()[-top_n:][::-1] return top_indices

参数说明:这里的vectorizer.transform用的是已经拟合好的向量化器,不能用fit_transform,否则新文章会重建整个词典,老文章的特征空间全变样。cosine_similarity(vec, tfidf_matrix)返回的是 1×N 的矩阵,flatten()拍平后就是新文章与每篇老文章的相似度。这个函数写出来,答辩时讲“我的系统冷启动怎么处理”时直接拿它说话。

注意:transformfit_transform的区别是整个项目最高频的坑。fit_transform是新建词典并转换,transform是在已建好的词典上做映射。所有新数据进来都必须走transform,否则重新拟合会让特征空间对不上,推荐结果全乱。

5. 避坑与常见问题:向量化到推荐结果的五个翻车现场

5.1 现象:推荐结果永远都是同一篇

排第一的永远是文章自身,之后几篇相似度全部低于 0.1,基本等于随机推。原因是推荐函数没有排除自身,或者自身相似度 1.0 排第一,Top-N 里实际有效结果只有 N-1 个。解决方法是先过滤自身索引再排序,见 4.2 的代码,而且要注意推荐时不能用similarity_matrix[k][k]这个对角线值做判断,统一过滤索引最稳。

5.2 现象:中文文本全变成空格,向量全是零

TfidfVectorizer默认 token 是“按空格切分英文单词”,拿原始中文文档直接喂进去,切出来是一长串没有空格的整句,或者清洗后只剩空格,结果某行向量全零。原因是先 fitted 再 cleaning,处理顺序反了。解决方法是保证严格的流水线顺序:原始文本 →clean_texttokenize' '.joinvectorizer.transform,每一步之后用print打印一行确认结果再继续。

5.3 现象:内存爆掉,进程被系统杀掉

数据量约 2 万篇文章时,直接算 N×N 稠密相似度矩阵,一个 float64 数组约 3.2GB 内存,再加原始文本列表很容易被 OOM Killer 干掉。原因是cosine_similarity默认返回稠密矩阵。解决方法是小数据量(5000 篇以下)可以稠密计算,大数据量改用pairwise_distances(..., metric='cosine', n_jobs=-1)分块计算,或只保存每篇文章 Top-20 近邻的稀疏结构,推荐时走neighbors表而不是全量矩阵。

5.4 现象:你自己觉得 A 和 B 明显同主题,系统不觉得

TF-IDF 抓的是字面重复度,不是语义相似。比如“深度神经网络”和“机器学习模型”两篇文章明明在讨论相似课题,但没有一个字重合,相似度极低。这不是 bug,是基于内容方法的天然天花板。解决方法是接受它并在论文里把它写成“方法局限与改进方向”,然后扩展ngram_range(1, 2)或对标题、摘要做加权。答辩老师问到你为什么不加 BERT,你可以回答“BERT 会在精排阶段做扩展,本项目的核心链路基于 TF-IDF 保证可解释性和可复现性”。

5.5 现象:在线推荐每请求都要重新算全部相似度,响应慢到怀疑人生

本地实验跑得好好的,一上 Flask 接口就发现每次推荐要全库算一遍相似度,文章多起来卡到发指。原因是每次请求都在循环矩阵运算,而没有做缓存。解决方法是把相似度矩阵启动时计算一次存入内存,推荐时只做查表;文章库有更新时,做增量向量化再更新矩阵,用单独函数控制。另外要给 Flask 接口加@lru_cache做连续重复请求的结果缓存,具体见下一章。

6. 进阶落地:用 Flask 做展示层与离线评估,让答辩多讲五分钟

6.1 用 Flask 包一个推荐接口,别人能直观点点点

展示层是毕业设计的加分项,直接把推荐函数暴露成 Web 服务是最快的路径。Flask 比 Django 更适合这个场景,因为它只做轻量接口,没有重型结构。核心代码是把recommend_by_index封装成 API,返回 JSON 格式的文章列表前端可以直接渲染。

from flask import Flask, request, jsonify app = Flask(__name__) # 模拟文章库,实际项目中替换为从数据库读取 articles = [{'id': i, 'title': '标题', 'content': news.data[i]} for i in range(len(news.data))] @app.route('/recommend', methods=['POST']) def recommend(): # 请求体:{"article_id": 0} data = request.get_json() article_id = data.get('article_id') if article_id is None or article_id >= len(articles): return jsonify({'error': 'invalid article id'}), 400 rec_list = recommend_by_index(article_id, top_n=10) results = [] for idx, score in rec_list: results.append({'id': idx, 'title': articles[idx]['title'], 'score': round(score, 4)}) return jsonify({'code': 0, 'data': results})

逻辑说明:接口设计为 POST 传入要推荐的源文章 ID,返回的每条结果带相似度分数。round(score, 4)是为了让 JSON 输出简洁,不要直接回浮点长尾。前端用普通 HTML + 下拉框就能在本地跑起来,不写复杂框架,简历里写“基于 Flask 构建推荐服务接口,支持 Top-N 推荐与可视化验证”直接成立。

6.2 用离线的准确率、召回率证明你不是玄学

答辩时老师大概率会问“你的推荐结果怎么评价”。纯功能演示不够,还需要量化指标。推荐系统常用的评估方法是在文本分类语料上做precision@Krecall@K——把已知类别的文章切出测试集,给测试文章推荐同类别文章算命中率。

from sklearn.model_selection import train_test_split # 用类别标签做评估标签:推荐结果中同类别文章算“命中” labels = news.target train_idx, test_idx = train_test_split( range(len(labels)), test_size=0.2, stratify=labels, random_state=42 ) def evaluate_precision(top_n=10): hit = 0 total = len(test_idx) for i in test_idx: # 与4.2相同逻辑获取推荐列表 sim_scores = sorted(enumerate(similarity_matrix[i]), key=lambda x: x[1], reverse=True) sim_scores = [x for x in sim_scores if x[0] != i][:top_n] hits = [x[0] for x in sim_scores if labels[x[0]] == labels[i]] if hits: hit += 1 precision = hit / (total * top_n) if total > 0 else 0 recall = hit / total if total > 0 else 0 return precision, recall

核心参数是stratify=labels,它保证训练集和测试集的类别比例一致,否则切分出来的测试集可能完全没有某一类文章,评估结果会被拉偏。用这个函数跑一遍,你会得到一个真正的数值——也许 0.4,也许 0.6,然后把它写进论文里作为基础版成绩。答辩时再补一句“后续通过引入 Word2Vec 做特征扩展,指标提升到 0.7”,这就形成了完整的实验对比。

这些年做下来,我的习惯是每个推荐项目先跑通最小可行链路,再谈优化——最少 500 篇文章、一个 TF-IDF、一个cosine_similarity,出结果后打印几条相似文本自己肉眼验证一遍,发现问题再往上层调。这个习惯帮我省掉了大量“跑完了发现输入格式错了”的返工时间。希望这篇拆解能帮你在课程设计和毕业设计里少走一段弯路,把时间花在真正有增量的部分。

把你的项目做成一个能讲清决策过程的作品,而不是堆代码的黑匣子。祝你顺利。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询