
简介这份资源面向具备一定Python基础、希望系统实践网络爬虫与推荐算法的开发者与学习者围绕新浪新闻场景构建了一套完整的新闻推荐平台。爬虫部分实现标题、正文、图片与视频链接的抓取并保留原始排版涵盖URL收集、请求网页、内容解析、数据存储、robots协议遵守及反爬虫应对等关键环节推荐部分则融合权重衰减、标签推荐、区域推荐与热点推荐四种策略便于理解推荐系统的工程落地思路。压缩包共约2000个文件以1607个py源码为主辅以html、js、vue等前端页面与css样式另有c、h底层源码及txt、md、json、xml等配置与说明文件整体约144.59MB目录结构清晰便于按模块检索学习。目前已有184人学习下载适合作为课程设计、毕业项目或算法练手的参考方案帮助读者快速搭建从数据采集到个性化推荐的完整链路。1. 从一份新闻推荐平台源码说起爬虫抓取与推荐算法怎么串起来很多人第一次拿到「Python网络爬虫与推荐算法新闻推荐平台」这类源码包第一反应是打开压缩包找main.py结果翻到一堆fortranobject.c、wrapmodule.c、extra_avx512f_reduce.c、cpu_asimd.c、cpu_neon_vfpv4.c这种 C 文件瞬间懵了——这到底是爬虫项目还是 NumPy 编译产物其实这些.c文件是 NumPy 在特定平台编译时留下的源码痕迹跟业务逻辑没关系真正要看的还是 Python 侧的爬虫模块和推荐模块。这份资源解决的是两件事一是用 Python 把新浪新闻的标题、正文、图片、视频链接按原排版抓下来二是用「权重衰减 标签推荐 区域推荐 热点推荐」四路策略把抓到的新闻推给用户。适合谁适合已经会写requests和BeautifulSoup、但没把「抓取—存储—推荐」串成完整链路的 Python 学习者也适合想拿一个能跑通的新闻推荐 demo 做课程设计或二次开发的人。下面按「资源是什么 → 怎么用 → 坑在哪」的顺序拆开讲每一步都落到能抄的代码和参数上。2. 爬虫侧拆解从新浪新闻列表页到结构化存储2.1 请求层URL 队列、请求头与频率控制爬虫的起点是 URL 收集。新闻站点通常有列表页和详情页两级结构列表页负责发现链接详情页负责提取字段。常见做法是先用一个入口 URL 抓列表解析出所有详情页链接放进队列再逐个请求详情页。请求层要处理三件事请求头伪装、超时重试、访问间隔。import requests import time from urllib.parse import urljoin HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept-Language: zh-CN,zh;q0.9, } def fetch(url, retry3, timeout8): for i in range(retry): try: resp requests.get(url, headersHEADERS, timeouttimeout) resp.encoding resp.apparent_encoding # 防止中文乱码 if resp.status_code 200: return resp.text except requests.RequestException as e: print(f第{i1}次请求失败: {e}) time.sleep(1.5 * (i 1)) # 退避重试 return NoneHEADERS里User-Agent是必须的不带的话很多新闻站直接返回 403Accept-Language影响返回的页面语言版本。fetch里的retry3和timeout8是我一般会设的默认值新闻站响应慢的时候 8 秒足够再长就是对方在限流。resp.apparent_encoding比写死utf-8稳因为部分新闻页是gb2312。退避重试用1.5 * (i1)秒避免连续打同一个 IP。提示请求间隔不要低于 1 秒。新闻站对高频访问敏感抓太快轻则返回空页重则临时封 IP。2.2 解析层标题、正文、图片与视频链接的提取解析层决定你能拿到多少字段。这份资源的定位是「保留排版」意味着正文不能只取纯文本还要保留段落结构、图片位置和视频链接。常见做法是用BeautifulSoup配合lxml解析器先定位正文容器再按子节点顺序遍历。from bs4 import BeautifulSoup def parse_detail(html, base_url): soup BeautifulSoup(html, lxml) article { title: , paragraphs: [], # 保留段落顺序 images: [], videos: [], } title_tag soup.find(h1) if title_tag: article[title] title_tag.get_text(stripTrue) body soup.find(div, class_article) or soup.find(article) if not body: return article for node in body.children: if getattr(node, name, None) p: text node.get_text(stripTrue) if text: article[paragraphs].append({type: text, value: text}) elif getattr(node, name, None) img: src node.get(src) or node.get(data-src) if src: article[images].append(urljoin(base_url, src)) elif getattr(node, name, None) video: src node.get(src) if src: article[videos].append(urljoin(base_url, src)) return articlebody.children遍历而不是find_all是为了保留正文里文字、图片、视频的原始先后顺序这样渲染出来排版不会乱。urljoin处理相对路径新闻站的图片经常是//img.xxx.com/...这种协议相对地址直接存会丢协议头。>import json import os from datetime import datetime def save_news(news_list, out_dirdata): os.makedirs(out_dir, exist_okTrue) day datetime.now().strftime(%Y%m%d) path os.path.join(out_dir, fnews_{day}.json) with open(path, a, encodingutf-8) as f: for news in news_list: f.write(json.dumps(news, ensure_asciiFalse) \n) print(f已写入 {len(news_list)} 条到 {path})用a追加模式而不是w避免中途崩溃把当天数据覆盖掉。ensure_asciiFalse保证中文正常显示。每行一个 JSON 对象JSONL 格式比整个数组一次性写入更抗中断读的时候也能逐行流式处理。tags字段先留空等推荐模块的标签提取跑完再回填。注意如果抓取量上万条JSONL 单文件会变大建议按小时再分片或者换 SQLite。这份资源默认是轻量场景JSON 够用。3. 推荐侧拆解四路策略怎么算、怎么加权3.1 权重衰减让旧新闻自然沉底新闻有时效性三天前的头条今天推给用户就是事故。权重衰减解决的是「时间越久、分数越低」。常见做法是指数衰减公式是score base_score * exp(-lambda * hours)lambda控制衰减速度。import math from datetime import datetime def time_decay(base_score, publish_time, half_life_hours24): now datetime.now() hours (now - publish_time).total_seconds() / 3600 lam math.log(2) / half_life_hours # 半衰期换算成衰减系数 return base_score * math.exp(-lam * hours)half_life_hours24表示 24 小时后权重降到一半。这个参数是调优重点新闻类产品一般设 12 到 48 小时设太小热点撑不过半天设太大旧闻压不住。base_score来自后面三路策略的原始分衰减只做乘法不改变排序的相对关系只改变绝对分值。3.2 标签推荐从正文抽关键词做匹配标签推荐的核心是「用户喜欢什么标签就推什么标签的新闻」。标签来源有两个用户历史行为点击、停留和新闻正文关键词。新闻侧可以用jieba做关键词抽取取 TF-IDF 权重最高的几个词作为标签。import jieba.analyse def extract_tags(text, topk5): # 用 TF-IDF 抽关键词允许词性为名词和动词 tags jieba.analyse.extract_tags(text, topKtopk, withWeightTrue) return [{tag: t, weight: round(w, 4)} for t, w in tags] def tag_match_score(news_tags, user_tags): if not news_tags or not user_tags: return 0.0 user_map {t[tag]: t[weight] for t in user_tags} score 0.0 for nt in news_tags: if nt[tag] in user_map: score nt[weight] * user_map[nt[tag]] return scoreextract_tags的topK5是经验值新闻正文一般 500 到 1500 字5 个标签足够覆盖主题多了会引入噪声词。withWeightTrue返回权重方便后面做加权匹配而不是简单计数。tag_match_score做的是标签交集加权求和用户标签权重来自点击频次归一化新闻标签权重来自 TF-IDF两边相乘再累加避免单个高频标签主导排序。3.3 区域推荐与热点推荐两个容易被忽略的维度区域推荐解决「本地新闻优先」。新闻数据里如果有地区字段比如来源站点、正文提到的地名可以按用户所在区域加权。常见做法是维护一个地区词典命中用户区域加固定分。REGION_KEYWORDS { 北京: [北京, 朝阳, 海淀, 京], 上海: [上海, 浦东, 徐汇, 沪], 广东: [广东, 广州, 深圳, 粤], } def region_score(text, user_region, boost1.5): if user_region not in REGION_KEYWORDS: return 0.0 for kw in REGION_KEYWORDS[user_region]: if kw in text: return boost return 0.0boost1.5是区域命中的加分倍数不要设太高否则会压过标签推荐导致用户只看本地新闻。热点推荐则是全局维度的补充统计最近 N 小时内被点击/抓取次数最多的新闻给一个全局热度分。from collections import Counter def hot_score(news_id, click_counter, top_n20): if not click_counter: return 0.0 most_common click_counter.most_common(top_n) rank_map {nid: rank for rank, (nid, _) in enumerate(most_common)} if news_id not in rank_map: return 0.0 return 1.0 - rank_map[news_id] / top_n # 排名越靠前分越高top_n20表示只取热度前 20 的新闻参与热点加权避免长尾新闻也拿分。1.0 - rank/top_n把排名映射到 0 到 1 之间第一名接近 1第二十名接近 0。3.4 四路融合加权求和与排序四路策略各自出分后要融合。常见做法是加权求和权重按业务调。标签推荐权重最高热点和区域次之时间衰减作为全局乘数。def final_score(news, user_profile, click_counter): s_tag tag_match_score(news.get(tags, []), user_profile.get(tags, [])) s_region region_score(news.get(text, ), user_profile.get(region, )) s_hot hot_score(news[id], click_counter) base 0.6 * s_tag 0.2 * s_region 0.2 * s_hot return time_decay(base, news[publish_time], half_life_hours24)0.6 / 0.2 / 0.2是初始权重实际调优时先固定时间衰减再调这三项。如果发现推荐结果同质化严重优先降s_tag权重、升s_hot如果用户抱怨看不到本地新闻升s_region。time_decay放在最后做乘法保证时效性对所有策略一视同仁。4. 避坑与排查跑这份源码最容易翻车的五个地方4.1 现象导入模块报ModuleNotFoundError: No module named numpy但明明装了原因压缩包里那些fortranobject.c、cpu_asimd.c是 NumPy 源码编译残留不代表环境里已经装好 NumPy。很多人看到.c文件以为依赖已内置直接跑 Python 就报错。解决先建虚拟环境再装依赖不要用系统 Python 直接跑。python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install requests beautifulsoup4 lxml jieba numpy4.2 现象抓到的正文全是乱码或者只有一半原因新闻站部分页面用gb2312或gbk编码requests默认按ISO-8859-1解码另外正文容器 class 名可能随页面模板变化写死的class_article会失效。解决用resp.apparent_encoding自动判断编码解析时准备多个候选容器按优先级尝试。body (soup.find(div, class_article) or soup.find(div, class_article-content) or soup.find(article) or soup.find(div, idcontent))4.3 现象推荐结果全是同一天的新闻旧新闻完全不出现原因time_decay的half_life_hours设得太小比如设成 6 小时超过一天的新闻分数趋近于零排序里永远出不来。解决把半衰期调到 24 到 48 小时并且给旧新闻设一个分数下限避免完全沉底。def time_decay(base_score, publish_time, half_life_hours24, floor0.05): # ... 原逻辑 ... return max(base_score * math.exp(-lam * hours), floor)4.4 现象标签推荐推出来的新闻跟用户兴趣完全不搭原因jieba.analyse.extract_tags抽出来的标签里混入了「记者」「报道」「编辑」这类新闻模板词这些词在所有新闻里都高频匹配时反而主导了分数。解决维护一个停用词表在抽取前过滤掉模板词或者用jieba.analyse.set_stop_words加载自定义停用词文件。jieba.analyse.set_stop_words(stopwords.txt) # stopwords.txt 每行一个词记者、报道、编辑、来源、责任编辑...4.5 现象跑一段时间后请求全部返回 403 或空页面原因请求频率过高触发反爬或者User-Agent被识别为脚本。新闻站对固定 UA 高频访问很敏感。解决加随机间隔和 UA 轮换间隔用random.uniform(1, 3)秒UA 准备 3 到 5 个真实浏览器值轮换。如果仍然被封降低并发改成单线程串行抓取。import random UA_POOL [HEADERS[User-Agent], Mozilla/5.0 (Macintosh; ...), ...] time.sleep(random.uniform(1, 3)) HEADERS[User-Agent] random.choice(UA_POOL)5. 进阶技巧把推荐效果验证做成可复现的离线评估跑通不等于有效。推荐模块最容易犯的错是「凭感觉调权重」调完不知道好坏。我一般会做一个离线评估脚本用历史点击数据构造测试集算 Top-N 命中率。具体做法是把用户点击过的新闻按时间切分前 80% 作为训练后 20% 作为测试对测试集里每条点击新闻用当前权重跑一遍推荐看它有没有出现在 Top-10 里。def evaluate_hit_rate(news_pool, user_profile, click_counter, test_clicks, top_n10): scored [] for news in news_pool: s final_score(news, user_profile, click_counter) scored.append((news[id], s)) scored.sort(keylambda x: x[1], reverseTrue) top_ids {nid for nid, _ in scored[:top_n]} hit sum(1 for nid in test_clicks if nid in top_ids) return hit / len(test_clicks) if test_clicks else 0.0test_clicks是用户测试期点击过的新闻 id 列表top_n10对应实际推荐位数量。命中率低于 0.3 说明权重需要大调0.3 到 0.5 属于可接受区间高于 0.5 在新闻场景已经不错。调参时每次只动一个权重记录命中率变化避免多变量同时改导致无法归因。参数建议范围影响half_life_hours24 ~ 48越小越偏时效越大越偏内容s_tag权重0.5 ~ 0.7越高越贴兴趣过高会窄化s_region权重0.1 ~ 0.3越高本地新闻越多s_hot权重0.1 ~ 0.3越高越偏全局热点top_n10 ~ 20评估时与实际推荐位一致还有一个小技巧把每次调参的权重组合和命中率写进一个 CSV跑十几组之后画个简单散点比拍脑袋靠谱得多。我早期偷懒不记录调了三天回头发现还不如第一版血泪经验。从那以后我每次改推荐权重都强制走一遍离线评估先看命中率再上线。希望帮到你。本文还有配套的精品资源点击获取