Python构建个性化新闻推荐平台:从文本向量化到点击率模型全流程

发布时间:2026/10/11 21:04:39
Python构建个性化新闻推荐平台:从文本向量化到点击率模型全流程 简介一份基于Python的个性化新闻推荐平台设计源码面向计算机专业学生、算法工程师及推荐系统研究者旨在解决信息爆炸环境下的新闻精准推送难题。资源包共23个文件、约30KB主要包含9个Python源码、6个字节码、4个XML配置以及txt、gitignore、license、iml等辅助文件源码覆盖推荐算法、数据处理、用户交互逻辑字节码可提升加载速度XML支撑灵活参数配置文档部分提供使用说明。平台具备用户行为实时分析、用户画像构建、爬虫抓取与数据库存储等能力可集成pandas、scikit-learn、TensorFlow等库并融合基于内容、协同过滤或混合推荐算法的综合策略。项目按backend、crawler、dataBase等模块拆分层次清晰便于理解推荐系统从数据采集、存储到个性化输出的完整链路。已有389人学习下载适合希望快速掌握个性化新闻推荐平台设计与实现的开发者参考。1. 个性化新闻推荐平台到底在解决什么问题一个点击率模型的落地视角新闻资讯类 App 每天推几十条内容用户真正点进去的往往只有三五条。个性化新闻推荐平台要解决的就是这个匹配问题根据你昨天看过的财经、今早搜过的公司、以及同类型用户的行为把很可能点开的那几条新闻排到前面。基于 Python 落地这套系统文本向量化、用户画像、召回排序都有成熟的库和源码可以参考一周内就能跑通最小链路。这篇文章写给两类人一类是拿它做课程设计或毕业设计的开发者另一类是准备在业务里引入推荐能力但没想清楚边界的产品或算法工程师。我会按数据准备、召回、排序、部署和避坑的顺序把这个方向的源码设计拆明白。2. 数据准备与内容画像用 Python 把新闻变成可计算的特征推荐系统最容易被低估的是数据层。很多人拿到一份新闻源码先去看模型代码结果跑起来才发现字段对不上、时间戳是字符串、内容里有大量空值。新闻推荐和电商推荐最大的区别在于内容时效性强、文本占比高、用户兴趣漂移快所以数据组织方式直接决定后面所有策略能不能成立。2.1 新闻数据组织结构字段设计决定推荐系统的上限我在搭这套平台时第一步永远是把原始数据洗成两张标准表新闻主表和用户行为表。新闻主表至少要有news_id、title、content、category、publish_time最好再加一个click_cnt用于热度计算。用户行为表则记录user_id、news_id、behavior_type、time。别急着写模型先花半天把这两张表的清洗逻辑定下来后面所有特征都从这两张表出。import pandas as pd raw pd.read_csv(news_raw.csv) # 统一字段命名publish_time 转成 Unix 时间戳秒级 news raw.rename(columns{ id: news_id, title: title, content: content, category: category }) news[publish_ts] pd.to_datetime(news[publish_time]).astype(int) // 10**9 news[title_len] news[title].str.len() # 过滤空标题和内容过短的脏数据新闻正文少于50个字符基本没有价值 news news[news[title].notna() (news[content].str.len() 50)] news news.sort_values(publish_ts).reset_index(dropTrue) news.to_parquet(news_clean.parquet)逻辑说明先把原始 CSV 的字段名统一成推荐系统里约定俗成的写法避免后续每个函数都要处理别名。publish_ts转成整型时间戳是为了后面算新闻年龄方便age_hours (now - publish_ts) / 3600是做时间衰减的基本输入。title_len是个很便宜但有效的特征——长标题在信息流里的点击率往往低于短标题可以给排序模型用。参数说明内容长度阈值 50 不是固定值如果你做的是短资讯或者快讯类新闻阈值可以降到 20如果做深度报道这个阈值可以提到 200。原则是过滤掉明显无意义的数据同时不要误伤短小精悍的新闻。sort_values(publish_ts)保证后面做时间窗口召回时数据是按时间有序的减少反复排序的开销。2.2 内容画像构建关键词抽取与文本向量化的最小实现新闻内容不能直接喂给模型必须先变成向量或关键词集合。这里有个常见误用一上来就上 Sentence-BERT 之类的深度学习向量结果机器跑不动还说不清楚效果比 TF-IDF 好多少。我的做法是先拿 TF-IDF 打通全链路后面有需要再换向量模型因为推荐系统的管线设计不会因为向量模型换一种就推翻。import jieba from sklearn.feature_extraction.text import TfidfVectorizer def cut_text(text): return .join(jieba.cut(text, cut_allFalse)) # 标题和正文拼接后分词标题信息在排序阶段还会单独做特征 news[seg] (news[title] news[content]).apply(cut_text) vectorizer TfidfVectorizer( max_features5000, stop_words[的, 了, 是, 在, 和, 与] ) tfidf_matrix vectorizer.fit_transform(news[seg]) news[vec_len] tfidf_matrix.getnnz(axis1)逻辑说明jieba.cut用精确模式适合新闻这种偏书面语的文本网络热词和专有名词比如“碳中和”“芯片法案”jieba 可能切错但 TF-IDF 对切错的容忍度比较高不会直接崩。把标题和正文拼在一起是常见做法标题里的词通常信息密度更高你也可以把标题单独算一份 TF-IDF 再拼到特征里效果会更好一点。参数说明max_features5000是一个平衡点。对几千篇新闻的 demo 来说 5000 维词表够用如果到十万篇文章建议提到 20000 维或者改用词向量平均。stop_words只放了最基础的中文停用词因为 jieba 自带词表已经过滤掉大部分虚词。vec_len这个特征表示一篇文章有多少非零词项某种程度上代表内容长度排序模型可以直接用。2.3 用户行为表推荐的“标签数据”从哪来新闻推荐的标签数据不是人工标注的而是用户行为日志。没有现成行为日志的话常见做法是用公开的新闻推荐数据集起步或者自己写个简单的埋点脚本记录曝光和点击。行为表的关键不只是用户点了什么还有时间窗口——三个月前的点击对今天推荐的意义很小因为新闻兴趣是快速衰减的。log pd.read_csv(user_behavior.csv) log[ts] pd.to_datetime(log[time]).astype(int) // 10**9 # 行为类型只保留有效交互过滤掉曝光未点击 log log[log[behavior_type].isin([click, read, share])] # 只取最近7天的行为新闻推荐里时间窗口别开太长 cutoff_ts log[ts].max() - 7 * 86400 log log[log[ts] cutoff_ts] # 统计用户对每个类别的偏好次数 user_cat log.merge(news[[news_id, category]], onnews_id) user_pref user_cat.groupby([user_id, category]).size().reset_index(namecnt)逻辑说明behavior_type过滤很关键很多日志里会有曝光记录曝光不点击是负样本信号但在冷启动阶段用处有限先过滤掉只保留正向行为避免把噪声带进偏好统计。时间窗口用 7 天是从新闻场景出发的——股票新闻隔 3 天就过期体育新闻一周后也没人关心窗口开太长会稀释短期兴趣。参数说明7 天窗口可以根据产品形态调整。如果你做的是周报类资讯窗口可以放宽到 30 天做实时快讯窗口缩到 3 天更合理。groupby出来的cnt就是后续用户特征里最核心的类别偏好计数可以直接算占比也可以做归一化后当数值特征用。3. 召回与排序Python 实现个性化推荐引擎的核心数据准备完进入推荐引擎主体。这里要区分两个层次召回是从几十万篇新闻里快速选出几百个候选排序是把这几百个候选按点击概率精确排队。很多入门源码不分这两个阶段直接在全部新闻上算相似度新闻量一上去就卡死。正确的做法是先粗后精召回阶段允许粗糙但必须快排序阶段要求精准但候选量已经很小。3.1 召回层三种常用召回策略的取舍新闻推荐的召回策略各有适用场景我一般会同时保留三路热度召回兜底、ItemCF 协同过滤做行为相似召回、向量召回做语义相似召回。三路结果取并集后进入排序保证新用户、老用户、新文章都能被覆盖到。先看最基础的热度召回def hot_recall(news, top_k20, alpha0.5): # 热度分 点击量 / (新闻年龄小时数 2) ** alpha # 2 是为了避免新文章刚发布时除数为0 news[score] news[click_cnt] / ((news[age_hours] 2) ** alpha) return news.sort_values(score, ascendingFalse)[news_id].head(top_k).tolist()逻辑说明这是一个带时间衰减的牛顿冷却公式变体。click_cnt越大越热age_hours越大越冷。alpha 控制衰减速度alpha 越大旧新闻沉得越快适合快资讯alpha 越小老牌权威新闻的曝光机会越多适合深度内容平台。2是平滑项避免刚发布的新闻除数为零。from collections import defaultdict def itemcf_recall(user_id, click_log, top_k20): # 构建 user - items 映射 user_items defaultdict(set) for uid, nid in click_log[[user_id, news_id]].values: user_items[uid].add(nid) # 简化版 ItemCF两个新闻被同一个人点击的次数越多越相似 item_sim defaultdict(dict) for items in user_items.values(): for i in items: for j in items: if i ! j: item_sim[i][j] item_sim[i].get(j, 0) 1 target user_items.get(user_id, set()) scores defaultdict(float) for i in target: for j, w in item_sim.get(i, {}).items(): if j not in target: scores[j] w return sorted(scores, keyscores.get, reverseTrue)[:top_k]逻辑说明这个 ItemCF 是教学版实现生产环境一般用 Spark 或 Faiss 做矩阵近似计算。它解决的问题是当用户点击过几篇新能源新闻系统能找到其他新能源读者还爱看什么。代码里item_sim只统计了共现次数没做归一化便于理解要上线时记得除以sqrt(item_popularity[i])做惩罚否则热门新闻会霸占召回结果。def vector_recall(user_vec, tfidf_matrix, top_k20): from sklearn.metrics.pairwise import cosine_similarity scores cosine_similarity(user_vec.reshape(1, -1), tfidf_matrix)[0] return np.argsort(scores)[::-1][:top_k].tolist()逻辑说明向量召回的做法是先算用户向量和每篇文章向量的余弦相似度取 TopK。这里的user_vec是用户点过的新闻向量的平均体现的是语义层面的兴趣。向量召回最大的优势是能召回没有共现关系但语义相似的新闻比如用户看了“美联储加息”系统能召回“央行MLF操作”。三路召回的取舍用一个对比表总结召回策略速度个性化冷启动友好度主要问题热度召回最快无很好没有个性化差异ItemCF中等较强差对老用户有依赖有流行度偏差向量召回较快较强较好只要有文本就能算容易召回同质化内容实际部署时三路并取召回数量一般控制在 200~500 个候选再交给排序模型精排。这个量级既能保证排序模型效率又不至于因为召回太少漏掉用户感兴趣的内容。3.2 排序层用 LightGBM 训练一个点击率预估模型召回拿到几百个候选后需要用排序模型精排。新闻推荐里最常见的排序模型是 LightGBM 直接做 CTR 预估或者用 GBDTLR 的经典组合。LightGBM 胜在训练快、能自动处理缺失值、特征工程不用做太细。排序特征分三类用户侧特征、新闻侧特征、交互特征。import lightgbm as lgb # train_df 特征列至少包含 # user_click_cnt, user_cat_ratio, hot_score, title_len, news_age_hours, cat_match X train_df.drop([label, user_id, news_id], axis1) y train_df[label] dtrain lgb.Dataset(X, y) params { objective: binary, metric: auc, learning_rate: 0.05, num_leaves: 31, max_depth: 6, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 1, verbose: -1 } model lgb.train(params, dtrain, num_boost_round300) model.save_model(lgb_news_rank.txt)逻辑说明cat_match是交互特征的核心表示当前候选新闻的类别是否命中用户历史偏好 Top 类别命中为 1否则为 0。这个特征比直接输入类别 ID 更好用因为模型不需要学习类别之间的映射关系。user_cat_ratio是用户对某个类别的点击占比代表兴趣强度。参数说明num_leaves31对应深度约 5~6防止过拟合。新闻推荐样本量通常没有电商那么大叶子节点太多容易记住噪声。feature_fraction0.8和bagging_fraction0.8是双重防过拟合配合learning_rate0.05300 轮足够收敛。注意负采样比例——我一般把曝光未点击设负样本正负比控制在 1:5 到 1:10正负比太极端时 LightGBM 会偏向预测负样本导致召回结果全被压到低分。3.3 用户实时特征更新让推荐“看懂”你刚读的那篇新闻推荐区别于其他推荐的一个重要点是实时性。用户刚读了一篇“特斯拉降价”十分钟内再刷推荐结果里应该出现相关的新能源车新闻而不是继续给昨天的娱乐新闻。这要求推荐系统有实时更新用户向量的能力。import redis import numpy as np r redis.Redis(hostlocalhost, port6379, db0) def update_user_vector(user_id, news_embedding, ttl1800): # 用 Redis 存用户短期兴趣向量TTL 设置为 30 分钟 key fuser_vec:{user_id} r.set(key, news_embedding.tobytes(), exttl) def get_user_vector(user_id): raw r.get(fuser_vec:{user_id}) if raw is None: return None return np.frombuffer(raw, dtypenp.float32)逻辑说明用户的长期兴趣可以由离线算好的用户画像承担短期兴趣单独存一份。用户每产生一次点击就把他刚读的新闻向量累加进 Redis过期时间设为 30 分钟。这样用户现在正在读的新闻会直接影响下一次刷新请求的召回结果而且免去了离线重算的等待。参数说明TTL 设 1800 秒是从新闻阅读时长反推的用户看完一篇深度报道平均需要 3~5 分钟连续刷 30 分钟已经算深度使用超过这个时间短期兴趣就该降温。如果产品是早中晚固定时段刷资讯的模式TTL 可以改成 7200 秒覆盖整个通勤时段。阶段用tobytes()存向量是省空间的常见做法比存 JSON 列表少几十倍内存。4. 从离线到在线推荐服务接口与冷启动策略模型训好只是第一步源码要能跑起来给人演示或接入业务必须有服务接口。工程上推荐接口要解决三个问题请求进来时怎么把召回和排序串起来、没有历史行为的新用户怎么办、以及用户不想看到重复内容怎么办。4.1 用 FastAPI 把推荐流程包成推荐接口推荐服务不需要太重FastAPI 足够异步性能好还自带接口文档。核心逻辑是同步调用召回函数、特征构造、模型预测最后返回 Top N 的新闻 ID 列表。from fastapi import FastAPI, Query app FastAPI() app.get(/recommend) def recommend(user_id: str, top_k: int 10): # 第一步多路召回取并集 candidates recall_union(user_id) # 第二步过滤掉已读新闻 candidates filter_read(user_id, candidates) # 第三步构造特征并做模型排序 feat_df build_features(user_id, candidates) scores rank_model.predict(feat_df) ranked [nid for _, nid in sorted(zip(scores, candidates), reverseTrue)] return {user_id: user_id, items: ranked[:top_k]}逻辑说明recall_union内部把热度、ItemCF、向量三路结果合并去重。filter_read必须在排序前做如果排序后才过滤会出现返回不足 top_k 的情况。排序模型用的是上一节 LightGBM 的model.predict输入是特征 DataFrame输出是点击概率。这个接口设计成同步阻塞没有太大问题因为单用户一次推荐的耗时一般控制在 50ms 以内。参数说明top_k由前端传入一般信息流首屏是 10~20 条下拉加载再请求下一页。注意接口要做超时保护和降级我习惯在推荐服务外层加一个 try-except模型预测异常时直接返回热门榜兜底避免用户看到白屏。4.2 冷启动与探索新用户和新文章的分配策略新用户没有任何行为记录个性化无从谈起新文章没有任何点击模型永远学不会推荐它。这两类冷启动都要靠策略硬规则解决而不是靠模型自己悟出来。常见做法是新用户用热门榜加注册时选择的兴趣标签新文章在排序分数上加一个探索奖励分。def explore_bonus(news_age_hours, beta0.3): # 新文章奖励分发布时间越近加的越多 return beta / (news_age_hours 1) def cold_start_recall(user_tags, top_k20): # 用户注册时选了3个兴趣标签按标签匹配新闻并加热门分 matched news[news[tag].isin(user_tags)] matched matched.sort_values(hot_score, ascendingFalse) return matched[news_id].head(top_k).tolist()逻辑说明explore_bonus直接加在最终排序分数上。beta0.3 表示一篇新发布文章的初始得分比同等质量的老文章高 30%这个优势会在发布后几小时内快速衰减到 24 小时后基本归零。这借鉴了多臂老虎机的思路先给新文章一定的探索流量观察它的真实点击表现再决定后续曝光权重。参数说明beta 不要超过 0.5否则新文章会挤压正常的热门内容造成信息流质量下滑。news_age_hours 1的平滑项保留了新文章发布当小时的探索机会。兴趣标签这个字段在新闻主表里如果缺失可以直接用category替代效果差别不大。4.3 避免重复推荐已读过滤与多样性打散已读过滤是新闻推荐最容易忽略的环节。用户今天早上看过一篇“央行降准”下午再刷新时不应该再看到它。这里不只是精准过滤的问题还关系到用户体验的耐心。已读信息我一般存在 Redis 的 Set 里判断一个新闻 ID 是否在集合里是 O(1) 操作。def filter_read(user_id, news_ids): seen r.smembers(fread:{user_id}) return [nid for nid in news_ids if nid not in seen] def mmr_rerank(ranked, sim_matrix, lambda_0.7, top_k10): # MMR在相关性和多样性之间取平衡 selected [] candidates set(ranked) while len(selected) top_k and candidates: def score(i): rel rank_score[i] if selected: max_sim max(sim_matrix[i][j] for j in selected) else: max_sim 0 return lambda_ * rel - (1 - lambda_) * max_sim best max(candidates, keyscore) selected.append(best) candidates.remove(best) return selected逻辑说明MMR 是经典的多样性打散算法lambda_控制相关性和多样性的权重。当lambda_0.7时更多保留排序模型的高分结果降到 0.5 时会明显打散相似内容。函数返回的前几个位置一定给出最相关的新闻越靠后越会考虑补上不同类型的内容避免信息流前三屏全是同一主题。这个策略在新闻场景尤其重要因为用户对重复主题的耐心远低于电商重复商品。参数说明lambda_0.7是我在新闻场景里常用的默认值如果用户反馈信息流太单一往 0.5 方向调如果反馈推荐不够准往 0.8 方向调。sim_matrix可以直接复用 2.2 节算好的tfidf_matrix余弦相似度不需要额外维护一套相似度服务。5. 避坑个性化新闻推荐建模与部署中的 5 个高频问题这一章写的是我自己在这个方向上踩过的坑每一条都对应一次真实翻车。新闻推荐看起来简单跑通 demo 不难但要拿到一个能说服人的效果这几个问题绕不开。5.1 现象离线 AUC 0.85线上点击率不涨原因样本选择偏差。离线训练用的是历史曝光数据而线上推荐展示的是模型没见过的新内容两批数据的分布不一致。新闻推荐尤其严重因为新闻生命周期短离线训练集里的文章两天后可能已经下线。解决离线评估时按时间切分训练集用前 70% 时间验证集用后 30% 时间而不是随机切分。随机切分会造成时间穿越线上效果必然打折。另外要监控线上特征分布和训练时特征分布的差异差异过大说明模型在吃老本。5.2 现象同一个新闻反复出现在不同位置原因已读过滤的时机放错了。召回阶段过滤一遍、排序阶段又重复召回塞回来了或者去重只做了单路召回内部没有对三路召回结果做整体去重。这个坑特别隐蔽因为离线评估不会暴露只在用户反复刷新时会看到。解决过滤写成一个独立函数在召回合并之后、排序之前调用一次。另外在 Redis 里维护已读集合时写入要在用户点击接口里同步做不要等离线批处理——用户秒级刷新时离线任务根本来不及更新。5.3 现象向量召回的结果全是同一个话题的变体原因语义向量把相似内容的距离拉得太近比如所有与“人工智能”相关的新闻在向量空间里都挤在一起排序模型又给了它们相似的高分导致前 10 条全是 AI 新闻其他类别的优质内容没了位置。解决两处下手。召回阶段对召回的候选按类别做配额比如 200 个候选里每个类别最多占 40 个排序之后加 MMR 做多样性打散保证前 10 条里至少覆盖 3~4 个不同类别。只靠排序模型加“类别”特征解决不了这个问题。5.4 现象新用户看到的推荐和热门榜一模一样原因新用户没有行为记录模型给出的分数基本来自新闻侧特征而热度分在所有新闻里都占主导排序结果退化成热门排序。这不是 bug但仍然不满足个性化要求。解决必须显式做冷启动策略而不是依赖模型。注册时让用户选兴趣标签这个动作就能带来很大的个性化增益。如果产品没有注册流程可以用用户的设备地域、来路渠道作为弱信号至少把一二线城市和三四线城市的用户区分开。5.5 现象离线 AUC 高到 0.99怎么看都不对原因特征里混进了未来信息。最常见的是用新闻的总点击量预测点击但这个点击量包含了用户产生行为之后的所有统计量——相当于让模型开卷考试。新闻推荐的时间穿越比电商更隐蔽因为新闻发布后点击集中在头几个小时特征统计稍有不慎就会串。解决构建特征时所有统计特征必须按时间截断。具体做法是在特征管道里加一个断言计算新闻点击量时只统计该用户行为时间之前的数据。更简单粗暴的做法是先把行为日志按时间排序再逐条计算累计统计量别一次性 groupby 完事。6. 进阶用分层 A/B 实验验证推荐效果推荐平台做完最怕自己觉得效果好却拿不出数据说服同事。做法上我会在源码里预留一个 A/B 实验模块按 user_id 哈希流量给实验组和对照组分配不同的排序策略记录曝光和点击日志再算出 CTR 和人均点击量等指标对比。import hashlib def get_group(user_id, split0.5): # 按 user_id 哈希分流保证同一个用户每次请求落到同一组 h int(hashlib.md5(user_id.encode()).hexdigest(), 16) return experiment if h % 100 split * 100 else control def evaluate_ctr(log_df): # 简单算 CTR点击次数 / 曝光次数 click_cnt log_df[is_click].sum() expo_cnt len(log_df) return click_cnt / expo_cnt if expo_cnt 0 else 0逻辑说明按 user_id 哈希分流比按请求随机分流更稳定避免同一个用户前后出现在两个组里造成指标失真。评估时至少积累 3~5 天的点击数据新闻推荐的灰度周期不能太短。我习惯先跑一周第一天看有没有异常后面三天看趋势。这个项目方向做到这里最值钱的并不是某一个模型的精度而是把数据、召回、排序、服务、评估这五段串成闭环的能力。我自己做第一版的时候就是只死磕模型结果模型调得不错接口没有灰度开关最后靠硬切线上被用户骂了一周。希望你一开始就留好 A/B 模块和降级兜底。推荐系统没有一次做对的先例都是小步快跑改出来的希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询