资讯动态

Python新闻推荐系统实现与优化:数据流、算法与避坑指南

发布时间:2026/10/13 11:29:32 来源:尧图企业网站定制
简介基于Python的新闻推荐系统完整源码包面向推荐系统入门者与自然语言处理应用开发者帮助理解新闻数据清洗、中文分词、特征提取、相似度计算到最终推荐结果生成的完整链路。压缩包共一百八十一个文件整体大小约十点三二兆字节其中三十五个Python脚本构成系统核心逻辑一百零二个文本文件提供语料、停用词表与配置说明三十二个编译缓存文件便于快速加载七个概率模型文件为结巴分词所需的发射概率与状态转移参数另含测试数据文件及项目说明文档目录结构按功能模块划分。资源目前已有四千零七十一人学习下载适合用作课程设计、毕业设计或推荐系统实践入门参考。通过该包可获得一套可直接运行的新闻推荐实现包括分词与特征选择脚本、相似度匹配策略、测试样本以及完整的工程配置无需额外训练语言模型即可复现基础流程也可在此基础上进行算法调优与二次开发。1. 拿到「基于python的新闻推荐系统.zip」之后先别急着调算法「基于python的新闻推荐系统」这类代码包解压后看起来功能齐全但真正跑通它并让推荐结果可解释90%的时间会耗在数据清洗和效果验证上而不是算法调参。很多人第一步就搞反了急着去看协同过滤和神经网络的代码结果数据没洗干净推荐出来的全是乱码和旧闻。这个压缩包代表了一类典型的工程形态——离线导入新闻文本做分词与向量化用相似度或行为日志召回候选再通过Web层把结果暴露出去。它适合有Python基础、想快速拥有一套可演示可扩展的原型也想搞明白推荐系统落地链路的人。下面按我自己的拆解顺序来讲。2. 拆解系统骨架数据流与模块边界决定你要改哪里拿到代码包先别读算法文件先把数据流认清楚。推荐系统最怕的不是模型写不出来而是你根本不知道一条新闻从进来到展示给你中间经过了几道手。新闻推荐和电商推荐的差别也在数据流上新闻时效性强用户只看一次重复推荐同一篇等于推荐失败所以数据流里必须有时效控制这是第一个要建立的认知。2.1 先认准四层数据流采集、清洗、召回、服务我一般会把这类代码包从逻辑上拆成四层。第一层是数据接入常见做法是RSS拉取、爬虫入库或者直接读本地CSV/JSON第二层是文本清洗去HTML标签、去停用词、分词有时候还要做去重和正文抽取第三层是召回与排序把用户特征和新闻特征算成向量做相似度或行为预测产出一个有序列表第四层是服务层用Flask或FastAPI把列表包装成接口前端拿到JSON渲染。这四层之间的边界决定了你要改哪里。如果推荐结果不准问题可能出在第二层的分词也可能出在第三层的相似度计算还可能出在数据接入层的时效字段根本没解析。我见过不少人在服务层调了半天参数最后发现是采集层把发布时间全存成了字符串时间衰减函数拿到的是脏数据。所以第一步永远是画一条数据流标清楚每个字段从哪来到哪去。一个典型的目录结构长这样不一定每个包都完全一致但大方向差不多news_recommend/ ├── data/ │ ├── news.csv # 新闻正文与元数据 │ └── user_behavior.csv # 用户浏览、点击、停留日志 ├── src/ │ ├── preprocess.py # 清洗、去重、分词 │ ├── recall.py # 候选集召回相似度/协同过滤 │ ├── rank.py # 排序时间衰减、热度加权 │ └── server.py # Flask/FastAPI 接口 ├── models/ # 训练好的向量和索引文件 └── requirements.txt动手改代码之前先把依赖装齐。没有requirements.txt就按下面这个清单装基本覆盖这类项目的最小依赖pip install jieba scikit-learn pandas numpy flask四个依赖对应四件事jieba负责中文分词scikit-learn提供TF-IDF向量化和相似度计算pandas做数据加载和透视flask把推荐结果暴露成HTTP接口。装完之后第一件事不是跑全流程而是先跑preprocess.py看它输出的清洗结果是否符合预期。我自己的习惯是先把数据读出来打印几条确认标题、正文、发布时间、分类这几个核心字段没缺没乱再往下走。2.2 最小启动配置把代码包从「能看」变成「能跑」这类代码包最常见的启动方式是把数据路径和参数集中在一个配置里通常是config.py或者直接写在文件开头的常量区。你需要确认三件事数据路径指向真实文件、停用词表路径存在、服务端口没被占用。我一般会先写好config再单独验证数据加载这一段。# config.py DATA_PATH ./data/news.csv BEHAVIOR_PATH ./data/user_behavior.csv STOP_WORDS_PATH ./data/stopwords.txt # 向量化参数 VECTORIZER_MAX_FEATURES 5000 # 特征维度上限过高会慢且噪声大 VECTORIZER_MIN_DF 2 # 至少在2篇文档中出现才保留 VECTORIZER_MAX_DF 0.8 # 超过80%文档都出现的词视为停用词 # 排序参数 TIME_DECAY_DAYS 3 # 时间衰减半衰期新闻超过3天权重减半 HOT_WEIGHT 0.3 # 热度分占最终排序的权重参数不是越大越好。max_features设到10000并不是更准反而会把长尾噪声带进来min_df设太大会把低频但有价值的专有名词全部丢掉。min_df和max_df是配合使用的min_df控制词频下限max_df控制词频上限两者都是用来挡噪声的。数据量只有几千条时max_features保持3000到5000就够用了。配置完成后跑通的最小命令就是两步先预处理再启动服务。预处理脚本会输出清洗后的DataFrame服务脚本会加载这个DataFrame并启动推荐接口。如果预处理这一步就报错别急着查算法先看数据文件的编码和字段分隔符这类代码包最常见的问题是数据文件用了GBK编码但代码按UTF-8读取或者日期字段格式不统一。3. 基于内容的推荐三步分词向量化、相似度计算、排序加权标题上写着新闻推荐系统大部分代码包的主干线其实是基于内容的推荐也就是把每一篇新闻变成向量用户读了什么就把那篇新闻的向量当成用户兴趣向量然后找和它最像的新闻。这条路实现简单、冷启动友好是这类代码包的首选算法。3.1 TF-IDF 向量化与余弦相似度推荐最核心的一行基于内容推荐的第一步是让机器能「读」新闻。中文不像英文天然按空格分词所以先用jieba把标题和正文切碎再用TF-IDF把词序列变成固定维度的向量。TF-IDF干的事很直接一个词在一篇新闻里出现得多同时在所有新闻里出现得少那它就对这篇新闻有代表性。比如「央行」在财经新闻里高频出现但在全库出现频率不高它的TF-IDF权重就高而「新闻」「记者」这种到处都有的词权重会被压下去。import jieba import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity def load_and_tokenize(path): df pd.read_csv(path, encodingutf-8-sig) df[title] df[title].fillna() df[content] df[content].fillna() df[text] df[title] df[content] df[tokenized] df[text].apply( lambda x: .join(jieba.lcut(x)) ) return df df load_and_tokenize(./data/news.csv) vectorizer TfidfVectorizer( max_features5000, min_df2, max_df0.8, ngram_range(1, 2), # 保留词和相邻词组合缓解分词误差 sublinear_tfTrue # 词频取对数抑制高频词 ) tfidf_matrix vectorizer.fit_transform(df[tokenized]) similarity_matrix cosine_similarity(tfidf_matrix, tfidf_matrix)上面这段代码做了三件事读数据并分词、把分词结果向量化、计算新闻两两之间的余弦相似度矩阵。注意fit_transform的对象是分词后用空格拼起来的字符串不是原始文本这是新手最容易漏掉的一步。直接喂原始中文文本TfidfVectorizer会把每个连续的汉字串当成一个特征效果很差。几个参数值得单独说。ngram_range(1, 2) 的作用是保留「降准」「央行降准」这样的相邻词组合因为单字分词容易把「央行降准」切成「央行 / 降准」丢掉了组合语义。sublinear_tfTrue 是对词频做对数变换防止一篇长文里重复出现的词直接压过其他词。cosine_similarity 计算出来的是稠密矩阵新闻量超过一万条时内存会明显吃力后面避坑章节再说怎么处理。3.2 排序加权加时间衰减和热度不做新闻「复读机」纯相似度排序很快暴露一个问题最相似的往往是老新闻或者长文。原因很简单相似度只看内容不看时间。热门政治新闻的相似度可能几个月前就被算好了如果直接按相似度返回用户打开系统看到的是上周甚至上月的旧闻。这就是新闻推荐和商品推荐最大的区别商品可以反复推荐同款新闻推荐同一条就是翻车。处理办法是排序阶段做加权把时间衰减和热度分数揉进最终得分。我常用的排序公式是最终分 相似度 * 时间衰减因子 热度分 * 热度权重。时间衰减因子用指数函数越老的新闻权重越低热度分可以用点击数、评论数或者转发数归一化得到。import numpy as np from datetime import datetime, timedelta def time_decay_factor(publish_date, half_life_days3): 新闻时效衰减超过半衰期天数权重减半 if pd.isna(publish_date): return 0.5 try: publish_dt datetime.strptime(str(publish_date)[:10], %Y-%m-%d) except ValueError: return 0.5 age_days (datetime.now() - publish_dt).days return float(np.power(0.5, max(age_days, 0) / half_life_days)) def rank_for_user(similarity_row, news_df, clicked_idx, top_n10): content_score similarity_row.copy() # 排除用户已经读过的新闻避免复读 for idx in clicked_idx: content_score[idx] 0.0 # 排除自己和自己比的那一项 content_score[clicked_idx[-1]] 0.0 decay news_df[publish_date].apply( lambda d: time_decay_factor(d, TIME_DECAY_DAYS) ) hot news_df[hot_score].fillna(0) / (news_df[hot_score].max() 1e-9) final_score content_score * np.array(decay) HOT_WEIGHT * np.array(hot) top_indices final_score.argsort()[::-1][:top_n] return news_df.iloc[top_indices][[title, publish_date, hot_score]]这段代码的关键是把向量化得到的相似度当成一个基础分再用时间衰减和热度去修正。排除已读新闻是必须的不然用户每次刷新看到的都是同一批文章。half_life_days是半衰期参数新闻类场景我一般设2到3天设太短会导致当天新闻霸榜设太长又会让一周前的旧闻持续出现。hot_weight设0.3左右比较稳妥设太高会让推荐变成纯热榜失去了个性化。hot_score字段如果上游没给可以用click_cnt或comment_cnt替代完全没有行为数据时就先设0别硬算。这一步做完一个最小可用的基于内容的新闻推荐其实已经成立了。先把这三步跑通再考虑协同过滤不要一上来就上模型。4. 协同过滤与混合策略用户行为数据够厚时再加基于内容的方法有天花板它只懂新闻内容不懂人。两个用户可能兴趣完全不同但因为看了同一篇新闻理论上就应该有某种关联。协同过滤补的正是这一层它不看新闻长什么样只看行为记录。用户和新闻的交互数据越厚协同过滤的价值越大。4.1 基于用户的协同过滤构造用户-新闻矩阵并计算相似用户基于用户的协同过滤思路很朴素找到和你读过同样新闻的人把那些人读过但你没读过的新闻推荐给你。实现上要构造一个用户-新闻矩阵行是用户列是新闻值是行为强度。行为强度可以简单用点击数更完整的做法是点击为1、点赞为3、收藏为5用不同权重表达用户偏好程度。import pandas as pd import numpy as np behavior pd.read_csv(./data/user_behavior.csv, encodingutf-8-sig) # 行为强度映射点击1分点赞3分收藏5分 behavior[value] behavior[action].map( {click: 1, like: 3, collect: 5} ) user_item behavior.pivot_table( indexuser_id, columnsnews_id, valuesvalue, fill_value0 ) def pearson_sim(a, b): 皮尔逊相关系数处理不同用户打分尺度差异 mask (a 0) (b 0) if mask.sum() 2: return 0.0 a_common, b_common a[mask], b[mask] return np.corrcoef(a_common, b_common)[0, 1] def user_cf_recommend(user_id, user_item, top_k5, top_n10): target user_item.loc[user_id].values.astype(float) scores [] for other_id, row in user_item.iterrows(): if other_id user_id: continue sim pearson_sim(target, row.values.astype(float)) if sim 0: continue scores.append((other_id, sim)) scores.sort(keylambda x: x[1], reverseTrue) top_users scores[:top_k] rec_scores np.zeros(user_item.shape[1]) for other_id, sim in top_users: other_vec user_item.loc[other_id].values.astype(float) # 只累加目标用户没看过的新闻 unseen target 0 rec_scores[unseen] sim * other_vec[unseen] news_cols user_item.columns top_idx rec_scores.argsort()[::-1][:top_n] return news_cols[top_idx[rec_scores[top_idx] 0]]核心逻辑是三重循环先算目标用户和其他人的相似度取最像的top_k个用户再把这些用户看过的新闻按相似度加权累加最后只保留目标用户没看过的、且分数大于0的新闻。皮尔逊相关系数比余弦相似度更适合这里因为它做了均值中心化两个用户一个喜欢给1分、一个喜欢给5分皮尔逊也能识别出趋势一致。user_item矩阵的稀疏度是这个算法的命门。1000个用户、5000篇新闻矩阵里95%以上是0是常态。如果top_k取太大把相似度很低的用户也拉进来推荐结果会被噪声淹没。我一般控制在5到15之间。这个算法的时间复杂度是O(用户数×新闻数)用户量过万后纯Python循环会慢得明显这时候就该考虑用矩阵分解降维也就是SVD。4.2 混合推荐与冷启动兜底什么时候值得上 SVDSVD类方法的核心假设是用户和物品可以映射到一个低维隐空间用户向量和新闻向量做内积就能得到偏好分数。这类代码包里如果用了surprise库最常见的就是SVD或NMF。它比基于用户的协同过滤更省内存泛化能力也更强但需要调的参数多了好几个。三种方案的选型边界我自己的判断标准大概是这样方案需要的数据冷启动表现实现成本适合阶段基于内容只要新闻文本好新新闻直接可用低冷启动、行为数据不足时基于用户协同过滤至少千级用户行为差新用户和新新闻都拿不到推荐中行为数据积累到一定量级SVD矩阵分解行为数据越厚越好差需要交互才能生成向量高数据量大、追求精度时冷启动是这个领域最现实的问题。新用户一条行为都没有协同过滤和SVD都无法给出任何候选新新闻没有任何交互矩阵里那一列全是0算出来的分数也是0。所以务实的做法是混合用基于内容做兜底保证新用户也有推荐可看行为数据积累后再逐步加大协同过滤的权重。def hybrid_score(content_score, cf_score, alpha0.5): alpha越大越偏向协同过滤冷启动阶段建议0.2 return alpha * cf_score (1 - alpha) * content_scorealpha是混合比例没有唯一正确答案。产品上线初期用户行为稀疏我建议alpha设0.2让内容推荐主导跑两周之后行为数据厚了再慢慢调到0.5。调这个参数要看数据用后面的离线评估来看效果变化而不是拍脑袋。SVD不是银弹它的隐向量解释性差出了问题很难定位是数据问题还是参数问题这也是很多代码包把SVD写成可选分支而不是主线的真实原因。5. 避坑指南把新闻推荐系统跑起来最常踩的5个坑这类代码包看着完整实际跑起来每一步都有翻车的可能。下面这几条是我自己踩过、也看别人踩过的典型问题按现象到解决的方式写清楚照着排查能省不少时间。5.1 分词结果全是单字或乱码停用词表和词典先兜底现象preprocess之后打印分词结果发现大量单字例如「的」「了」「在」占了大半或者出现一堆类似乱码的字符「央行」被切成「央」和「行」。原因停用词表没加载成功或者加载了但路径指错了文件词典缺失导致专有名词和领域词被切碎。解决先确认停用词表路径真实存在再看加载函数用的是不是utf-8编码。词典缺失时的补救办法是给jieba加自定义词典把新闻领域的专有词加进去。注意自定义词典的每行格式必须是「词 词频 词性」词频会直接影响切分优先级。# user_dict.txt 内容示例词频数字越大越容易被识别为完整词 央行 1000 n 降准 800 n 美联储 900 n加载方式就是jieba.load_userdict(./data/user_dict.txt)放在分词逻辑之前执行一遍。加完词典之后重新跑一次分词如果单字比例明显下降说明是词典缺失的问题如果还是乱回到编码上查。5.2 相似度计算内存爆掉几千条新闻直接卡死现象新闻量只有几千条跑cosine_similarity时内存占用飙升到几十GB进程被杀或者卡到没法操作。原因默认算出来的是稠密矩阵5000篇新闻就是5000×5000的浮点数矩阵大约200MB数据量到20000篇矩阵就膨胀到3GB以上。如果同时把tfidf_matrix和similarity_matrix都保留在变量里内存翻倍。解决改用稀疏相似度计算或者按需要分批计算。cosine_similarity其实可以只计算当前用户需要的那些行不要一次性算全量矩阵。更彻底的做法是保存稀疏的TF-IDF向量再用linear_kernel对单个向量做计算。from sklearn.metrics.pairwise import linear_kernel # 不生成全量矩阵只算当前新闻和全库的相似度 query_vec tfidf_matrix[target_idx] sim_row linear_kernel(query_vec, tfidf_matrix).flatten()这里的思路是把「先算全量再取一行」改成「只算一行」。用linear_kernel替代cosine_similarity是因为两者对归一化后的TF-IDF向量结果等价但linear_kernel在稀疏矩阵上更快。五千条新闻的场景下这个改动能把秒级延迟控制在可接受范围内内存占用也从GB级降到MB级。5.3 推荐结果变成「自己的影子」相似度最高的永远是本文现象用户读完某篇新闻后推荐列表第一名就是这篇新闻本身或者排在前面的全是标题几乎一样的转载稿。原因计算相似度时没有排除目标新闻自身。每篇新闻和自身的余弦相似度都是1.0必然排在第一位。另外转载稿如果内容重复度高相似度自然也很高但用户已经读过了。解决排序前把已读新闻的相似度分数置0具体代码在第3.2节已经写过了。同时要在数据层面做去重按标题或正文的MD5哈希去掉转载副本否则去重不彻底推荐列表会反复出现同一条新闻的不同转载版本。df[dup_key] df[content].str.strip().apply( lambda x: hashlib.md5(x.encode(utf-8)).hexdigest() ) df df.drop_duplicates(subsetdup_key, keepfirst)去重保留最先入库的那条转载的副本直接丢掉。这一步放在分词之前做能减少不少无效计算量。5.4 服务接口时好时坏路径写死和进程残留是两大元凶现象本地跑Flask接口第一次启动正常改了几个参数重启后报端口占用或者换了一台机器数据路径直接失效接口能启动但返回空列表。原因代码包里常见两种坏习惯——端口号写死不做冲突检测文件路径用绝对路径写死换环境必挂。解决服务启动前先检测端口占用数据路径全部改成相对路径基于项目根目录做拼接。换机器时只需要保证数据文件放在约定目录下不用改代码。# 快速排查端口占用 lsof -i:5000 # 如果被占用要么kill旧进程要么换端口启动接口启动后我习惯先手动请求一次推荐接口确认返回的JSON里新闻标题不是空的再继续往前端接。这一步能避免前端跑来问「为什么页面空白」时才发现是服务层挂了。5.5 推荐结果和热榜没区别个性化失效的两个隐蔽原因现象用户A和用户B看到的推荐列表几乎一样全是全站热门新闻个性化形同虚设。原因两个可能。第一热榜权重hot_weight设太高把个性化分数压没了第二用户行为数据太稀疏协同过滤部分拿不到有效信号混合结果被内容分主导而内容分本身趋同。解决先看hot_weight如果超过0.5降下来再测。再看行为数据量如果单用户的历史行为少于5条协同过滤本来就发挥不出来这时候应该承认系统处于冷启动阶段靠内容推荐兜底是正常现象。个性化不是算法决定的是数据量决定的行为数据不够厚之前推荐结果趋同是常态而不是bug。6. 让推荐效果可验证离线评估与最小日志埋点推荐系统做完了不能只靠「感觉还行」得让效果变成能对比的数字。我最常用的验证方法是离线评估拿历史点击日志当测试集看推荐列表能不能命中用户真正点击过的新闻。def precision_at_k(recommended, actual, k10): ret recommended[:k] hit len(set(ret) set(actual)) return hit / k按天划分训练集和测试集用前7天数据训练或生成推荐第8天用户的真实点击当标准答案算PrecisionK。这个数字能回答「推荐列表里有多少比例是用户真会点的」是调参时最有用的一个指标。线上要做的另一件事是日志埋点把每次推荐的用户ID、推荐结果、排序位置和曝光时间写下来。我习惯在推荐接口里追加一行JSON日志这样后续可以复现每一次推荐排查问题不用猜也能支撑将来做用户行为和兴趣漂移分析。这套最小验证下来推荐系统就不再是黑匣子了。最后说个我自己的教训第一次做这类系统时我把时间精力全花在调SVD参数上结果发现评估指标纹丝不动因为行为数据只有几百条矩阵稀疏到根本没有有效信号。后来老老实实把内容推荐做扎实、把离线评估建起来才真正开始有可迭代的方向。推荐系统里数据决定了上限算法只是逼近这个上限的方式调参是最后的步骤而不是第一步。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑