
简介基于Python实现的文章推荐系统是一套适合毕业设计、课程设计完整参考的工程源码覆盖数据采集、清洗、预处理、文本特征提取、相似度计算与协同过滤推荐等核心环节整体链路从数据处理贯穿到模型构建结构清晰。资源包共35个文件以15个Python脚本为主体对应爬虫抓取、标签提取、相似度队列处理、朴素贝叶斯分类、模型训练等功能模块另有5个Shell运行脚本、3个conf配置文件、文本数据与辅助配置等压缩包大小约89KB体量精简便于快速部署与二次开发。目前已有108人浏览学习。借助该项目读者能够掌握Pandas数据处理、NLP分词与停用词过滤、TF-IDF相似度计算、Scikit-learn协同过滤算法、推荐系统评估指标以及数据库操作、Web框架基础和环境配置部署等多个关键技术点可直接将现有代码用于课程设计或毕业设计也可按照模块逐步改造提升从数据处理到后端服务开发的综合实践能力。1. 一篇文章推荐系统能拆出多少东西爬虫、NLP、算法与部署一次拿齐做毕业设计选推荐系统的同学很多但论文里的算法和能跑起来的系统之间通常隔着一条巨大的沟。这个Python文章推荐系统项目的好处在于它没有把重点放在某个单一的模型上而是把一整套链路串了起来Scrapy爬虫抓取文章MongoDB负责存储经过分词、停用词过滤和标签提取后用相似度计算和朴素贝叶斯分类完成推荐最后还有训练和评估脚本。如果需要一个能演示完整流程、能写进论文的数据处理链路同时还想看到工程上异步队列怎么落地这个资源非常合适新手能跟完整个流程熟手也能从中看到项目模块划分的思路。2. 先把骨架看清楚从Scrapy爬虫到MongoDB的数据管道拿到一个项目压缩包第一件事不是急着跑脚本而是把文件拓扑捋清楚。这个项目的根目录下data_spider、component、conf三个目录基本划出了系统的三条主线数据采集、核心算法、运行配置。我拆项目有个习惯先把每个文件对应的职责列成一张表搞清楚谁调用谁再动手改代码。文件/目录职责推测在链路中的位置data_spider/Scrapy爬虫工程目录包含spider与item定义数据入口scrapy.cfgScrapy项目配置文件爬虫部署配置run_scrapy.sh启动爬虫的shell脚本采集入口tmp_imageurl.txt抓取过程中图片URL的临时存储中间产物component/extract_tag.py文章标签提取特征工程component/bayes_sort.py朴素贝叶斯分类器推荐排序component/similarity_pair_push.py相似文章对计算与推送相似推荐component/similarity_queue_process.py异步队列消费处理相似度任务异步解耦run_extract_tag.sh批量执行标签提取的脚本任务编排run_classify.sh批量执行分类的脚本任务编排run_train.sh模型训练流水线模型更新stopwords.txt中文停用词表NLP预处理conf/MongoDB副本集与初始化配置存储层load_mongo.sh数据加载到MongoDB的脚本数据入库2.1 数据采集层Scrapy爬虫与run_scrapy.shScrapy是这个项目的数据触角。data_spider目录里定义了一套完整的爬虫工程爬取到的文章会经过Item Pipeline做初步清洗然后写入MongoDB。run_scrapy.sh承担了启动入口的角色它的核心作用不只是执行爬虫命令而是把运行时参数显式地传进去这样不同环境之间切换不用改代码。#!/bin/bash # 启动爬虫将抓取结果写入MongoDB并输出日志到独立文件 cd /opt/article_rec/data_spider scrapy crawl article_spider \ -s MONGO_URImongodb://127.0.0.1:27017 \ -s MONGO_DBarticle_db \ --logfilelogs/spider.log这里用-s覆盖Scrapy的setting配置MONGO_URI指向本地MongoDB实例MONGO_DB指定目标数据库名。把连接信息放在启动脚本而不是写死在代码里好处是换一台机器部署时只需要改脚本里的连接串不用对整个工程动刀。--logfile单独指定日志路径方便爬虫跑挂了之后回溯排查。2.2 数据落地MongoDB三个配置文件与load_mongo.shconf目录下有mongo_rsc.conf、mongo_rsa.conf、mongo_rsb.conf三个文件从命名上看分别对应三个MongoDB副本集。副本集的意义在于高可用主节点挂了从节点可以自动接管避免采集链路因为单点故障中断。对于毕业设计来说三层副本集配置可能有点重但理解它的存在对于理解生产环境的数据架构是有帮助的。#!/bin/bash # 将清洗后的文章数据批量导入MongoDB集合 mongoimport --host 127.0.0.1 --port 27017 \ --db article_db --collection articles \ --file /data/articles.json --dropmongoimport是MongoDB自带的导入工具--db指定数据库--collection指定集合--file指定JSON文件路径。最后那个--drop要特别注意它会在导入前先删除目标集合如果集合里已有重要数据这个参数会导致数据丢失。我一般只在首次初始化或者确认要全量重建时才加增量更新时一定去掉。2.3 为什么选MongoDB而不是MySQL文章数据天然适合文档型数据库。每篇文章字段不一致有的有封面图有的有摘要有的有多标签用MySQL建表要么字段冗余严重要么需要频繁ALTER TABLE。MongoDB的文档模型可以直接把一篇文章存成一个JSON对象字段按需增减这在爬虫数据清洗阶段能省掉大量结构化工作。另外这个项目里有setting.js文件看名字是MongoDB的初始化脚本通常用来创建集合、建立索引。这里有个实操建议对articles集合的publish_time字段和tags字段建索引因为在后面的相似度计算和分类任务中这两个字段是高频查询条件。如果不建索引数据量到几万条之后聚合查询会明显变慢。3. 从正文到特征分词、停用词表与标签提取推荐系统的地基是文本特征。文章和文章之间像不像靠的不是直接比对字符串而是先把文章内容转换成一组可控的特征。这个项目里特征工程主要落在stopwords.txt和component/extract_tag.py上。stopwords.txt是中文停用词表里面收集了“的、了、是、在”这类高频但对语义贡献极低的词。停用词表的质量直接影响标签提取的效果这块看起来不起眼实际是决定后边所有环节精度的地方。3.1 分词与停用词过滤特征提取的第一道闸门中文文本不像英文按空格分词必须先做分词处理。常见的做法是用jieba分词库这个项目虽然没有在文件列表里直接出现分词库的代码但基于文章推荐场景分词和停用词过滤是必经步骤。我一般会在extract_tag.py里按下面的方式组织预处理逻辑import jieba # 加载停用词表构建集合以便 O(1) 查询 STOPWORDS set() with open(stopwords.txt, r, encodingutf-8) as f: for line in f: word line.strip() if word: STOPWORDS.add(word) def tokenize(text: str) - list: # 用jieba精确模式分词过滤空白字符和单字噪声 words [w.strip() for w in jieba.cut(text) if w.strip()] # 过滤停用词和长度小于2的词保留有实际语义的词汇 return [w for w in words if w not in STOPWORDS and len(w) 1]分词之后加两层过滤第一层是停用词表把“我们”“可以”“因为”这类虚词和泛化词剔除第二层是长度过滤单个字的词对语义贡献有限像“的”“了”这类词即使不在停用词表里也会被长度条件拦下来。这两层过滤做完特征空间能缩小30%到40%后边TF-IDF的计算量会明显下降。3.2 标签提取extract_tag.py的统计逻辑标签是文章的浓缩摘要。extract_tag.py做的事情本质上是把分词结果按词频排序选出Top N个高频词作为文章的候选标签。纯词频统计有个明显问题某些词在大量文章里都出现区分度低比如“新闻”“内容”“报道”这类词。所以更稳的做法是引入TF-IDF的思路词频高的词要有优势但在越多文档里出现的词权重就要被拉低。from collections import Counter def extract_tags(text: str, top_k: int 10) - list: words tokenize(text) freq Counter(words) # 统计词频 # 按词频降序排序取前top_k个词作为标签 return [word for word, _ in freq.most_common(top_k)]这里的top_k控制每篇文章生成几个标签默认给10个。如果文章比较短建议调到5如果做粗粒度分类可以调到15甚至更多让分类器有更多特征可用。实际跑的时候我会在run_extract_tag.sh里用循环批量处理所有文章并把结果写回MongoDB的tags字段这样后续的相似度计算就能直接基于标签做而不需要每次重复分词。3.3 run_extract_tag.sh与批处理任务编排单篇文章的标签提取非常快但推荐系统面对的是几万篇文章必须批量处理。run_extract_tag.sh就是把提取过程编排成批任务的脚本常见做法是循环读取数据库中的文章数据逐批提取标签并写回。批量任务要关注两件事一是断点续跑防止中途失败后全部重来二是幂等性同一篇文章反复处理结果应该一致。#!/bin/bash # 批量提取文章标签逐批处理并打日志 for batch_size in 1000 2000 5000; do python component/extract_tag.py --batch-size $batch_size logs/extract_tag.log 21 done这个脚本用一个简单的for循环跑了三组批次大小目的是观察不同批量下的处理耗时和内存占用。如果机器内存只有4G--batch-size 5000很可能直接把内存占满这时就要拆小批次。日志重定向到extract_tag.log方便排查哪一批数据出了问题。4. 推荐算法落地相似度计算、异步队列与朴素贝叶斯分类这一章是整个项目的核心。文件列表里三个Python文件对应了三种不同的推荐策略similarity_pair_push.py做文章相似度计算similarity_queue_process.py用队列异步消化相似度任务bayes_sort.py用朴素贝叶斯做文本分类。三个文件配合起来才构成完整的推荐链路。4.1 相似度对推送similarity_pair_push.py怎么算文章相似基于内容的推荐核心是回答“这篇文章跟哪些文章像”。常见的方案是用TF-IDF把文章向量化再用余弦相似度计算两两相似度。向量化的维度通常取5000到10000维度太小表达力不足维度太大计算量暴涨。from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity def build_similarity_pairs(docs: list, top_n: int 5): # max_features 控制特征维度min_df 过滤低频词 vectorizer TfidfVectorizer(max_features5000, min_df2) tfidf_matrix vectorizer.fit_transform(docs) # 返回稀疏矩阵 # 用稀疏矩阵计算余弦相似度避免一次性生成稠密矩阵 sim_matrix cosine_similarity(tfidf_matrix, dense_outputFalse) # 对每篇文章取 Top-N 篇最相似的文章生成相似度对 pairs [] for i in range(sim_matrix.shape[0]): row sim_matrix[i].toarray().flatten() top_indices row.argsort()[::-1][1:top_n 1] for j in top_indices: pairs.append((i, j, float(row[j]))) return pairs关键参数是max_features5000和min_df2。min_df2表示一个词至少在2篇文章里出现过才会被纳入特征这能过滤掉只在单篇文章里出现的生僻词减少噪声。cosine_similarity这里特别加了dense_outputFalse返回稀疏格式因为全量文章之间的相似度矩阵如果展开成普通数组几万篇文章就是几亿个数值内存直接爆掉。4.2 队列消费similarity_queue_process.py的异步设计相似度计算是典型的重计算任务如果把全量文章的相似度一次性算完服务器会卡死。项目里用队列异步处理这个环节similarity_queue_process.py扮演的正是消费者角色。生产者把“需要计算相似度的一对文章”丢进队列消费者从队列里取任务、计算、写回结果两边互不阻塞。def consume_similarity_queue(queue, batch_size: int 100): while True: # 从队列中拉取一批相似度计算任务 tasks queue.dequeue(batch_size) if not tasks: break for task in tasks: doc_a load_article(task[doc_a_id]) doc_b load_article(task[doc_b_id]) score compute_cosine(doc_a[tfidf], doc_b[tfidf]) save_similarity_pair(task[doc_a_id], task[doc_b_id], score)这里的batch_size控制了每次拉取的任务量100是比较稳的默认值。队列实现可以用Redis的List结构也可以用MongoDB的队列集合。异步化之后有个额外的好处新抓取的文章来了只需要把新文章跟最近N篇文章配对丢进队列不用全量重算这就是增量推荐的基础。4.3 贝叶斯分类bayes_sort.py的训练与预测文章分类是推荐系统的另一个维度。bayes_sort.py用朴素贝叶斯对文章做分类它的优势是训练速度快、在小样本场景下表现稳定非常适合文章这种高维稀疏文本数据。朴素贝叶斯假设特征之间相互独立虽然这个假设在现实中并不成立但在文本分类任务里它依然能给出不错的结果而且几乎不需要调参。from sklearn.feature_extraction.text import CountVectorizer from sklearn.naive_bayes import MultinomialNB from sklearn.model_selection import train_test_split def train_bayes(texts: list, labels: list): # 用词频向量化max_features控制特征数量 vectorizer CountVectorizer(max_features8000) X vectorizer.fit_transform(texts) X_train, X_test, y_train, y_test train_test_split( X, labels, test_size0.2, random_state42 ) # alpha是拉普拉斯平滑参数防止概率为零 clf MultinomialNB(alpha0.1) clf.fit(X_train, y_train) return vectorizer, clf, X_test, y_test这里用的是CountVectorizer而不是TfidfVectorizer因为朴素贝叶斯基于多项式分布建模统计的是词频而不是TF-IDF权重。alpha0.1是拉普拉斯平滑的强度值越小对训练数据拟合越好但太小容易过拟合一般从0.1到1之间尝试。random_state42固定了数据切分方式保证每次训练时训练集和测试集一致这样对比不同参数的效果才公平。4.4 run_train.sh训练流水线的作用模型不是训练一次就完事文章数据是持续增长的分类模型需要定期重训。run_train.sh把整个训练流程串成一条流水线先从MongoDB导出已标记的语料然后训练模型最后保存模型文件供在线服务加载。#!/bin/bash # 训练流水线导出数据 - 训练模型 - 评估效果 python export_training_data.py --output data/train_set.json python component/bayes_sort.py --train --input data/train_set.json --output model/bayes.pkl python evaluate.py --model model/bayes.pkl --test data/test_set.json三个步骤环环相扣--output和--input参数把每一步衔接起来。这里有个实操经验训练脚本里一定要固定随机种子否则每次重训出来的模型在相同测试集上的指标都会不一样写论文时没法对比实验。5. 避坑指南这个项目里最容易翻车的五个地方不管代码本身多完整环境不同、数据不同、运行方式不同坑就会出现。我按自己在类似项目里踩过的坑结合这个项目的运行方式整理了五条最常见的翻车记录每一条都是现象、原因、解决三个步骤。5.1 爬虫跑了一晚上MongoDB里只有几十条数据现象run_scrapy.sh正常启动日志也正常滚动但第二天看数据库文章数量少得可怜。原因Scrapy默认的去重机制是DUPEFILTER同一篇文章URL会被过滤掉。如果多个列表页指向同一篇文章的不同URL变体比如带utm参数和不带utm参数的版本会被当成不同文章反而挤占了实际抓取量。还有一种情况是并发数设置太低默认的CONCURRENT_REQUESTS往往只有16对动态网页来说太保守。解决在脚手架配置里关掉URL规范化或自定义去重指纹同时对列表页和详情页设置合理的并发与延迟策略。我一般会把CONCURRENT_REQUESTS调到32DOWNLOAD_DELAY设定为0.5到1秒既不会被对方站点封IP又能明显提升抓取效率。5.2 提取出来的标签全是“我们”“可以”“什么”现象extract_tag跑完后抽查几篇文章的tags字段几乎都是高频虚词看不出文章主题。原因stopwords.txt里的词太少。很多开源停用词表只有一百多个词但中文里需要过滤的高频词远不止这个数量代词、连词、副词、语气词都需要覆盖。解决扩充停用词表。文件里自带的stopwords.txt可以直接追加同时分词后加一层词性过滤只保留名词、动词、形容词把副词、介词、连词直接丢弃。标签质量差的时候优先怀疑停用词表而不是怀疑算法。5.3 所有文章的相似度都很高推荐结果全是一堆泛泛而谈的文章现象计算出来的相似度分数普遍在0.8以上推荐列表没有任何区分度。原因特征向量没有归一化。TF-IDF向量化后各维度的数值尺度不统一直接算余弦相似度会被长文本的高频词主导。另外max_features设置过小的话所有文章共享大量公共特征词相似度天然偏高。解决在TF-IDF向量化时显式开启归一化比如TfidfVectorizer(norml2)同时把max_features调大到8000或10000增加特征的区分度。对相似度分数加一个阈值低于0.3的相似对直接丢弃宁可漏掉也不推荐无关内容。5.4 贝叶斯分类结果全是同一个类别现象分类器对测试集的预测结果集中在单一类别准确率接近该类别的样本占比看起来“还凑合”实际完全没有分类能力。原因训练语料的类别分布极不均衡。某个类别的文章占了80%以上朴素贝叶斯在拟合时倾向于把所有样本都判成这个大类因为这样整体准确率最高。解决在MultinomialNB里设置class_weightbalanced让少数类样本获得更高的权重或者对训练数据做重采样把大类降采样、小类过采样让类别数量相对均衡。评估时不要只看准确率要看每个类别各自的召回率和F1分数。5.5 shell脚本在Windows上写完拿到Linux上跑直接报错现象run_scrapy.sh、load_mongo.sh这些脚本复制到服务器后执行时提示/bin/bash^M: bad interpreter。原因Windows上编辑过的脚本文件每行结尾是CRLF回车换行符Linux只认LF换行符多出来的^M字符导致解释器路径都解析错误。解决在Linux上执行sed -i s/\r$// *.sh把回车符批量替换掉。从那以后我每次上传脚本到服务器都先跑一遍这个命令再执行已经成了肌肉记忆省掉了无数无意义的报错排查时间。6. 验证推荐效果评价指标与两个可以继续深挖的方向推荐系统做完不能只说“推荐出来了”要有数字证明推荐是有效的。这个项目里可以用的评估方式分两层分类任务看准确率和召回率推荐列表看覆盖率和新颖度。def evaluate_recommend(recommended: list, relevant: list) - tuple: # recommended是给用户推荐的N篇文章relevant是用户实际阅读的文章 hit len(set(recommended) set(relevant)) precision hit / len(recommended) recall hit / len(relevant) return precision, recall准确率衡量推荐的命中比例召回率衡量用户真正感兴趣的文章有多少被推荐到了。以这两条评估函数为基础可以做一组对比实验把标签是否经过停用词过滤、相似度阈值设为0.3还是0.5、贝叶斯的alpha参数从0.1调到1.0分别跑一遍看看指标怎么变化。论文里的实验章节完全可以让这些参数对比来支撑。项目往后走还有两个方向值得深挖。第一个是增量更新目前训练脚本是全量训练的文章持续增长后每次全量训练的时间和资源开销都会变大可以考虑把新文章单独算特征只更新受影响的那部分模型参数而不是全部重新算。第二个是冷启动问题新用户没有任何历史行为时可以先用热门文章和基于内容的相似推荐兜底积累一定行为数据后再切换到个性化推荐。这两个方向在毕业设计的“不足与展望”章节里写起来非常有说服力因为这说明了你不只是跑通了一个项目而是知道它现在的瓶颈在哪、后续怎么解决。做这类综合项目血泪经验是改任何参数之前先跑一遍完整流程拿到基线指标改完再跑一遍对比。不这么做的话你根本分不清效果变好到底是哪个参数起了作用。从那以后我每次跑训练流水线都强制走一遍“数据导出 → 全量训练 → 指标评估 → 参数修改 → 重训对比”的完整闭环宁可多花一倍的运行时间也要保证每一步的改动都被量化记录。希望帮到你。本文还有配套的精品资源点击获取