基于兴趣标签匹配的新闻个性化推荐系统(Python+Django+爬虫)

该文章已生成可运行项目,

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一个开箱即用的本科级新闻推荐系统实现,支持游客浏览和登录用户个性化推荐。前端展示新闻列表与热点板块,登录后可查看按兴趣标签匹配生成的推荐内容、提交评论、管理浏览历史及个人资料。后台由Django Admin驱动,管理员能对用户、新闻、评论和日志进行完整CRUD操作。新闻数据通过requests定向采集网易头条等平台,使用jieba分词结合TF-IDF提取关键词,并与用户注册时选择的兴趣标签做余弦相似度计算,实现轻量但可解释的协同过滤逻辑。内置多版本中文停用词表(哈工大、川大、百度),附带requirements.txt、本地运行说明和演示资源,无需额外配置即可启动调试。适合课程设计或毕业设计直接参考,代码结构清晰,模块职责分明,涵盖数据采集、特征处理、匹配推荐与后台管理全流程。

1. 项目概述:为什么这个“轻量级推荐系统”值得本科生认真做一遍

你可能已经看过太多“基于深度学习的新闻推荐系统”这类标题——模型堆得很高,论文写得很炫,但跑起来要GPU、要TensorFlow环境、要调参三天三夜,最后连数据都凑不齐。而我带过六届毕业设计,最常听到学生说的一句话是:“老师,我想做个推荐系统,但不知道从哪下手,怕做不完。” 这个基于兴趣标签匹配的新闻个性化推荐系统,就是专为这种真实困境设计的:它不追求SOTA指标,不依赖海量用户行为日志,也不需要标注好的训练集;它用最朴素的文本特征+最可解释的相似度计算,把“推荐”这件事拉回到本科阶段真正能理解、能调试、能讲清楚原理的尺度上。

核心关键词——新闻推荐、Python爬虫、Django后台、用户标签匹配、TF-IDF——不是罗列术语,而是五根互相咬合的齿轮:爬虫负责“喂料”,TF-IDF负责“读懂新闻在说什么”,用户标签是“人设说明书”,匹配逻辑是“牵线搭桥的红娘”,Django则是整个系统的“行政中枢+前台窗口”。游客模式和登录模式的区分,不是为了炫技,而是模拟真实产品中“冷启动用户”与“已知用户”的典型分层策略;管理员后台不做花哨定制,直接复用Django Admin,恰恰体现了工程实践中“能用现成的绝不重造轮子”的务实哲学。

它适合谁?如果你是计算机或软件工程专业的大三、大四学生,正在准备课程设计或毕业设计,手头没有服务器资源、没接触过Hadoop/Spark、对PyTorch还停留在安装失败的阶段——这个项目就是为你量身定做的。它完整覆盖了从数据获取(requests定向抓取)、文本处理(jieba+停用词+TF-IDF向量化)、业务逻辑(余弦相似度匹配)、前后端交互(Django RESTful接口+模板渲染)、权限控制(游客/用户/管理员三级)到后台管理(CRUD全链路)的全流程。更重要的是,每一个模块的代码都不超过300行,函数职责单一,变量命名直白,debug时打个print就能看到中间结果。我试过让零基础的学生用三天时间跑通首页新闻展示,一周内加上用户注册和标签选择,两周完成推荐逻辑联调——关键不在于多快,而在于每一步你都知道自己在改什么、为什么这么改、改错了会报什么错。

这不是一个“玩具系统”。它预置了哈工大、四川大学、百度三套主流中文停用词表,不是随便找几个“的、了、在”就完事;它爬取网易头条时做了User-Agent轮换和请求间隔控制,避免被反爬直接封IP;它的TF-IDF不是调sklearn一行搞定,而是手动实现IDF计算过程,让你看清每个新闻关键词的权重是怎么算出来的;它的推荐结果页面甚至加了“为什么推荐这条?”的小提示框,点开就显示匹配的标签和相似度分数——这些细节,才是毕业答辩时评委老师愿意多问两句的地方。

2. 整体架构与设计思路拆解:为什么选这套组合,而不是别的方案

2.1 技术栈选型:拒绝“为用而用”,一切服务于可交付性

很多同学一上来就想用Vue+React+Flask+MongoDB,结果两周过去还在配跨域和环境变量。本项目坚持“最小可行技术栈”原则:Python + Django + requests + jieba + scikit-learn(仅用于TF-IDF向量化)。这里没有刻意回避现代前端框架,而是基于一个现实判断:本科毕设评审重点从来不是UI有多酷,而是逻辑是否自洽、流程是否闭环、代码是否可读。Django自带的模板系统(Template)完全能支撑新闻列表、详情页、用户中心等静态+半动态页面;其ORM天然适配MySQL/SQLite,省去对象关系映射的额外学习成本;Admin后台更是开箱即用,管理员增删新闻、审核评论、查看日志,点几下鼠标就完成,不需要你写一行HTML。

提示:有人问“为什么不用FastAPI做后端?”——FastAPI确实更轻量,但它默认不带用户认证、不带Admin界面、不带模板渲染。你要自己集成Django-Allauth做登录、自己写管理页面、自己处理CSRF防护。对毕设而言,这是在解决“不存在的问题”,反而拖慢进度。Django的“大而全”,在这里恰恰是优势。

2.2 推荐逻辑设计:放弃复杂模型,拥抱可解释性

市面上90%的毕设推荐系统,要么抄协同过滤公式却不会算相似度,要么调用LightFM库但说不清隐向量怎么更新。本项目采用基于用户兴趣标签与新闻关键词的余弦相似度匹配,本质是简化版的Content-Based Filtering(基于内容的推荐)。它的底层逻辑非常清晰:
- 用户注册时勾选“科技、体育、财经、娱乐、军事”等预设标签(共12个),每个标签对应一个权重(默认1.0);
- 新闻入库前,用jieba分词+停用词过滤+TF-IDF向量化,生成一个固定维度(如500维)的关键词向量;
- 用户向量则按所选标签,在对应维度置1,其余为0(例如只选“科技”和“财经”,则向量中“科技”位=1、“财经”位=1,其余498位=0);
- 计算用户向量与每条新闻向量的余弦相似度,取Top 10返回。

为什么不用矩阵分解或深度学习?因为余弦相似度的结果可以直接解释:“这条新闻推荐给你,是因为它包含‘人工智能’‘芯片’‘华为’三个关键词,与你选择的‘科技’标签高度相关,相似度达0.82”。答辩时你可以指着表格说:“看,用户A选了‘教育’和‘健康’,系统给他推了《双减政策落地效果分析》和《春季养生食谱》,相似度分别是0.76和0.71——这比一句‘模型预测得分高’有力得多。”

2.3 数据采集策略:定向抓取,而非无差别爬虫

项目正文提到“定向抓取网易头条等平台”,这不是一句虚话。实际代码中,爬虫模块(crawler.py)只针对网易新闻的“热点”“科技”“财经”三个频道URL发起请求,且严格遵守robots.txt协议(检查https://news.163.com/robots.txt确认允许访问)。它不模拟登录、不破解加密参数、不滚动加载无限列表,而是解析页面HTML中的<a href="xxx">链接,提取新闻详情页URL,再逐个请求。关键细节包括:
- 使用requests.Session()保持连接复用,减少TCP握手开销;
- 设置headers = {'User-Agent': random.choice(USER_AGENTS)},UA池包含10个主流浏览器标识;
- 每次请求后time.sleep(random.uniform(1, 3)),避免高频请求触发风控;
- 对返回状态码非200的URL记录到failed_urls.log,便于人工复查。

注意:网易头条部分页面使用Ajax加载,但本项目避开了这类动态渲染页面,全部选取服务端直出的频道页(如https://news.163.com/tech/),确保requests.get()能拿到完整HTML。这是“可控性”优先于“覆盖率”的典型取舍——宁可少爬100条新闻,也不能让爬虫成为项目最大不确定因素。

2.4 前后端分离的务实实现

项目描述中说“前后端分离结构”,但这里的“分离”不是指Vue调Django REST API那种标准范式,而是逻辑分离+部署分离
- 前端页面由Django Template渲染,URL路由由urls.py统一管理(如/news/list/NewsListView);
- 用户交互动作(提交评论、收藏新闻、修改资料)通过Form POST提交,由Django View处理并重定向;
- 后台管理完全走Django Admin,无需额外开发;
- 真正的“分离”体现在数据流:爬虫将新闻存入数据库 → 推荐引擎从数据库读取新闻向量 → View根据当前用户ID查出推荐列表 → Template渲染成HTML。

这种设计规避了CORS配置、Token鉴权、前端状态管理等本科生易踩的坑,又保留了“数据与视图解耦”的工程思想。如果你想升级为真前后端分离,只需把views.py里的推荐逻辑封装成API(如/api/recommend/?user_id=123),前端用fetch调用即可——扩展路径清晰,但初始版本绝不增加复杂度。

3. 核心模块详解与实操要点

3.1 爬虫模块:如何稳定获取高质量新闻数据

爬虫不是项目难点,但它是整个系统的“粮仓”。crawler.py文件虽小(约200行),却包含了生产级爬虫的关键要素。我们以抓取网易新闻科技频道为例,拆解其核心逻辑:

# crawler.py 关键片段
import requests
from bs4 import BeautifulSoup
import time
import random
import logging

USER_AGENTS = [
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
    'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Version/14.1.1 Safari/537.36',
    # ... 共10个UA
]

def fetch_news_list(channel_url, max_pages=3):
    """抓取指定频道的新闻列表页(最多3页)"""
    news_urls = []
    session = requests.Session()

    for page in range(1, max_pages + 1):
        # 网易新闻列表页URL规律:https://news.163.com/tech/#p1 / #p2 ...
        url = f"{channel_url}#p{page}"
        try:
            headers = {'User-Agent': random.choice(USER_AGENTS)}
            resp = session.get(url, headers=headers, timeout=10)
            resp.raise_for_status()

            soup = BeautifulSoup(resp.text, 'html.parser')
            # 定位新闻链接:网易新闻列表页中,新闻标题链接在 <div class="news-list"> 下的 <a> 标签
            for link in soup.select('div.news-list a[href]'):
                href = link.get('href')
                if href and href.startswith('http') and 'news.163.com' in href:
                    news_urls.append(href)

            time.sleep(random.uniform(1.5, 2.5))  # 页面间延迟

        except Exception as e:
            logging.error(f"抓取列表页 {url} 失败: {e}")
            continue

    return list(set(news_urls))  # 去重

def parse_news_detail(news_url):
    """解析单条新闻详情页,提取标题、正文、发布时间"""
    session = requests.Session()
    try:
        headers = {'User-Agent': random.choice(USER_AGENTS)}
        resp = session.get(news_url, headers=headers, timeout=15)
        resp.raise_for_status()

        soup = BeautifulSoup(resp.text, 'html.parser')

        # 标题:通常在 <h1> 或 <h2> 标签中
        title_tag = soup.find(['h1', 'h2'])
        title = title_tag.get_text(strip=True) if title_tag else "无标题"

        # 正文:网易新闻正文在 <div class="post_text"> 内
        content_div = soup.select_one('div.post_text')
        content = ""
        if content_div:
            # 移除广告、版权声明等干扰节点
            for unwanted in content_div.select('.ep-source, .ep-tips, .ep-editor'):
                unwanted.decompose()
            content = content_div.get_text(strip=True)

        # 发布时间:在 <div class="post_time_source"> 中
        time_tag = soup.select_one('div.post_time_source')
        pub_time = "未知"
        if time_tag:
            time_text = time_tag.get_text()
            # 提取类似“2023-05-20 14:30:00”格式的时间
            import re
            time_match = re.search(r'\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}', time_text)
            pub_time = time_match.group() if time_match else "未知"

        return {
            'title': title,
            'content': content[:5000],  # 截断过长正文,防爆库
            'pub_time': pub_time,
            'source_url': news_url
        }

    except Exception as e:
        logging.error(f"解析新闻 {news_url} 失败: {e}")
        return None

这段代码的实操价值在于:它展示了如何应对真实网页的“脏数据”。比如网易新闻详情页中,正文区域混杂着来源信息、编辑署名、版权声明,如果不decompose()掉这些节点,jieba分词时会把“本文来源:网易新闻”也当成关键词,严重污染TF-IDF向量。再比如发布时间提取,不是简单soup.find('span', class_='time'),而是用正则匹配标准时间格式,因为不同频道的时间标签class名并不统一。

实操心得:我在指导学生时发现,80%的爬虫失败源于超时和反爬。解决方案不是加大重试次数,而是降低请求密度+精准定位目标元素。建议首次运行时把max_pages设为1,先确保单页能稳定抓取;再打开浏览器开发者工具,右键检查目标新闻链接的HTML结构,复制CSS选择器(如div.news-list a),粘贴到代码中测试;最后才放开页数限制。宁可慢一点,也要稳。

3.2 文本处理与TF-IDF向量化:不只是调包,更要懂原理

新闻推荐的核心是“让机器读懂新闻”。nlp_processor.py模块承担此任,它不依赖sklearn.feature_extraction.text.TfidfVectorizer一键生成,而是手动实现TF-IDF计算流程,目的是让学生真正理解每个数字的来龙去脉。

步骤一:分词与停用词过滤

使用jieba.lcut()进行精确分词,并加载预置的停用词表(stopwords/hit_stopwords.txt):

import jieba

def load_stopwords(filepath):
    """加载停用词表,支持UTF-8和GBK编码"""
    try:
        with open(filepath, 'r', encoding='utf-8') as f:
            return set([line.strip() for line in f if line.strip()])
    except UnicodeDecodeError:
        with open(filepath, 'r', encoding='gbk') as f:
            return set([line.strip() for line in f if line.strip()])

STOPWORDS = load_stopwords('stopwords/hit_stopwords.txt')

def chinese_tokenize(text):
    """中文分词 + 停用词过滤 + 单字过滤"""
    words = jieba.lcut(text.lower())
    # 过滤:停用词、长度<2的词、纯数字、纯英文(除非是常见缩写如AI)
    filtered = []
    for w in words:
        w = w.strip()
        if (len(w) >= 2 and 
            w not in STOPWORDS and 
            not w.isdigit() and 
            not (w.isalpha() and len(w) == 1)):  # 过滤单个英文字母
            filtered.append(w)
    return filtered

注意:停用词表选择有讲究。哈工大版(hit_stopwords.txt)侧重学术场景,含“所述”“该”“其”等;百度版更贴近搜索场景,含“怎么”“什么”“哪里”等疑问词;川大版则补充了大量网络新词。项目默认加载哈工大版,但代码预留了切换接口——load_stopwords('stopwords/baidu_stopwords.txt'),方便你根据新闻语料特性调整。

步骤二:手动计算TF-IDF向量

TF-IDF公式为:TF-IDF(t,d) = TF(t,d) × IDF(t),其中:
- TF(t,d) = 词t在文档d中出现频次 / 文档d总词数
- IDF(t) = log(总文档数 / 包含词t的文档数)

手动实现的意义在于:你能看到每个新闻的向量是如何一步步构建的。假设我们有3条新闻,分词后得到:
- 新闻1:[“人工智能”, “芯片”, “华为”]
- 新闻2:[“人工智能”, “算法”, “深度学习”]
- 新闻3:[“芯片”, “手机”, “华为”]

那么词“人工智能”的IDF = log(3/2) ≈ 0.176,词“芯片”的IDF = log(3/2) ≈ 0.176,词“华为”的IDF = log(3/2) ≈ 0.176,而词“算法”的IDF = log(3/1) ≈ 1.099(因为它只在1篇新闻中出现,区分度更高)。这就是IDF的本质——稀有词权重更高

from collections import defaultdict, Counter
import math

class TFIDFVectorizer:
    def __init__(self, max_features=500):
        self.max_features = max_features
        self.vocabulary_ = {}  # 词 -> 索引
        self.idf_ = {}         # 词 -> IDF值

    def fit(self, documents):
        """训练:统计词频、计算IDF"""
        # 统计每个词出现在多少篇文档中(df)
        doc_freq = defaultdict(int)
        total_docs = len(documents)

        # 遍历所有文档,收集所有词
        all_words = []
        for doc in documents:
            words = chinese_tokenize(doc)
            all_words.extend(words)
            # 去重后计数:每个词在当前文档中是否出现
            unique_words_in_doc = set(words)
            for word in unique_words_in_doc:
                doc_freq[word] += 1

        # 构建词典:按词频降序取前max_features个词
        word_counts = Counter(all_words)
        top_words = word_counts.most_common(self.max_features)
        self.vocabulary_ = {word: idx for idx, (word, _) in enumerate(top_words)}

        # 计算IDF:log(总文档数 / 包含该词的文档数)
        for word, df in doc_freq.items():
            if word in self.vocabulary_:  # 只计算入选词典的词
                self.idf_[word] = math.log(total_docs / df)

    def transform(self, documents):
        """转换:对每个文档生成TF-IDF向量"""
        vectors = []
        for doc in documents:
            words = chinese_tokenize(doc)
            word_count = Counter(words)
            total_words = len(words)

            # 初始化向量(全0)
            vector = [0.0] * self.max_features

            # 填充TF-IDF值
            for word, count in word_count.items():
                if word in self.vocabulary_:
                    tf = count / total_words
                    idf = self.idf_.get(word, 0.0)
                    vector[self.vocabulary_[word]] = tf * idf

            vectors.append(vector)
        return vectors

# 使用示例
vectorizer = TFIDFVectorizer(max_features=500)
news_contents = ["人工智能芯片华为发布...", "深度学习算法研究进展...", "..."]
vectorizer.fit(news_contents)
news_vectors = vectorizer.transform(news_contents)

这段代码的价值在于:它把黑盒变成了白盒。当你在调试时发现某条新闻推荐不准,可以打印news_vectors[0],找到数值最大的几个维度,反查vocabulary_字典,就知道是哪些关键词主导了推荐——比如发现“苹果”这个词权重异常高,顺藤摸瓜就发现是某条新闻里把“苹果公司”误分成了“苹果”,而停用词表里没加“苹果”(作为水果),于是赶紧补进停用词表。这种可追溯性,是调包无法提供的。

3.3 推荐引擎:标签匹配的数学实现与性能优化

推荐逻辑在recommender.py中实现,核心是计算用户向量与新闻向量的余弦相似度。余弦相似度公式为:
similarity = (A·B) / (||A|| × ||B||)
其中A是用户向量,B是新闻向量,·表示点积,|| ||表示向量模长。

用户向量构建

用户注册时选择的兴趣标签(如“科技”“财经”)被存入数据库UserProfile模型的interest_tags字段(JSONField)。推荐时,将其转为稀疏向量:

# models.py
class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    interest_tags = models.JSONField(default=list)  # ['科技', '财经']

# recommender.py
def build_user_vector(user_profile, tag_to_index):
    """根据用户兴趣标签构建向量"""
    vector = [0.0] * len(tag_to_index)
    for tag in user_profile.interest_tags:
        if tag in tag_to_index:
            vector[tag_to_index[tag]] = 1.0  # 权重默认为1.0
    return vector
相似度计算与Top-K筛选

为避免每次推荐都遍历全库新闻(假设有10万条,计算10万次点积太慢),项目采用预计算+内存缓存策略:
- 启动时,将所有新闻向量加载到内存(news_vectors.pkl),建立{news_id: vector}映射;
- 用户请求推荐时,只计算该用户向量与内存中向量的相似度;
- 使用heapq.nlargest高效获取Top 10,时间复杂度O(n log k),远优于全排序O(n log n)。

import heapq
import numpy as np

def cosine_similarity(vec_a, vec_b):
    """计算两个向量的余弦相似度"""
    dot_product = sum(a * b for a, b in zip(vec_a, vec_b))
    norm_a = math.sqrt(sum(a * a for a in vec_a))
    norm_b = math.sqrt(sum(b * b for b in vec_b))
    if norm_a == 0 or norm_b == 0:
        return 0.0
    return dot_product / (norm_a * norm_b)

def get_recommendations(user_profile, news_vectors_dict, tag_to_index, top_k=10):
    """获取用户推荐新闻列表"""
    user_vec = build_user_vector(user_profile, tag_to_index)

    # 使用heapq.nlargest高效获取Top K
    similarities = []
    for news_id, news_vec in news_vectors_dict.items():
        sim = cosine_similarity(user_vec, news_vec)
        # 存储(相似度, 新闻ID),heapq按第一元素排序
        similarities.append((sim, news_id))

    top_similarities = heapq.nlargest(top_k, similarities, key=lambda x: x[0])

    # 查询数据库获取新闻详情
    news_ids = [news_id for _, news_id in top_similarities]
    recommended_news = News.objects.filter(id__in=news_ids).order_by(
        Case(*[When(id=news_id, then=pos) for pos, (_, news_id) in enumerate(top_similarities)])
    )

    return list(recommended_news)

实操心得:我曾遇到学生反馈“推荐很慢”。排查发现他把news_vectors_dict放在函数内每次重新加载,导致每次请求都读取几百MB的pkl文件。正确做法是在Django应用启动时(apps.pyready()方法中)一次性加载到全局变量。另外,tag_to_index字典也应预加载,避免每次请求都重建。这些细节,决定了系统是“能跑”还是“能用”。

4. 实操过程与核心环节实现

4.1 本地环境搭建:从零开始的完整步骤

项目强调“开箱即用”,但“开箱”前仍需几步必要操作。以下是在Windows/macOS/Linux上均可执行的标准流程(以Python 3.9为例):

第一步:克隆代码并创建虚拟环境

# 克隆仓库(假设已下载zip并解压)
cd your-project-folder

# 创建虚拟环境(推荐venv,无需额外安装)
python -m venv venv

# 激活虚拟环境
# Windows:
venv\Scripts\activate.bat
# macOS/Linux:
source venv/bin/activate

# 升级pip(避免旧版pip安装包失败)
pip install --upgrade pip

第二步:安装依赖
项目附带requirements.txt,但需注意两点:
- jieba必须>=0.42.1,否则lcut函数行为异常;
- scikit-learn仅用于TfidfVectorizer的验证对比,非核心依赖,可选装。

# 安装主依赖
pip install -r requirements.txt

# 验证关键库
python -c "import jieba; print(jieba.__version__)"
python -c "import django; print(django.get_version())"

第三步:初始化数据库与超级用户
Django默认使用SQLite(db.sqlite3),无需额外安装数据库服务:

# 生成迁移文件(models.py定义的表结构)
python manage.py makemigrations

# 执行迁移,创建数据表
python manage.py migrate

# 创建超级用户(用于登录Admin后台)
python manage.py createsuperuser
# 按提示输入用户名、邮箱、密码

第四步:预置测试数据
项目附带fixtures/initial_data.json(示例数据),包含:
- 3个预设用户(游客无需注册,登录用户test/test123,管理员admin/admin123);
- 50条已爬取的新闻(标题、正文、发布时间);
- 12个兴趣标签(科技、体育、财经…);
- 20条评论。

# 加载测试数据
python manage.py loaddata fixtures/initial_data.json

第五步:启动开发服务器

# 启动Django开发服务器
python manage.py runserver

# 浏览器访问
# 首页:http://127.0.0.1:8000/
# 登录页:http://127.0.0.1:8000/accounts/login/
# 后台管理:http://127.0.0.1:8000/admin/ (用createsuperuser创建的账号登录)

注意事项:如果启动时报错ModuleNotFoundError: No module named 'xxx',大概率是requirements.txt中某个包版本与当前Python不兼容。此时应打开requirements.txt,找到报错包(如django-crispy-forms==2.0),将其降级为django-crispy-forms==1.14.0(本项目实测版本),再pip install -r requirements.txt重装。切忌盲目升级所有包——稳定压倒一切。

4.2 爬虫运行与数据入库:手把手跑通第一条新闻

爬虫不是一键运行就完事,它需要你理解每一步的输出。以下是安全、可控的运行指南:

运行前检查
- 确认crawler.pyCHANNEL_URLS列表已配置目标站点(如['https://news.163.com/tech/', 'https://news.163.com/finance/']);
- 确认settings.pyCRAWLER_DELAY_RANGE = (1.5, 2.5)符合你的网络环境(校园网可设为(2.0, 4.0));
- 确保media/news_images/目录存在(用于存放新闻配图,本项目暂未实现图片下载,留作扩展)。

首次运行命令

# 在项目根目录执行
python crawler.py --mode list --channel tech --pages 1
# 参数说明:
# --mode list:只抓取列表页URL(不解析详情)
# --channel tech:指定科技频道
# --pages 1:只抓第1页(约20条链接)

观察输出
终端会打印类似:

[INFO] 开始抓取科技频道列表页 #1
[INFO] 成功获取22个新闻URL
[INFO] 列表页抓取完成,耗时 3.2s

此时,crawler_output/tech_list_urls_20231001.txt中已保存22个详情页URL。打开文件,随机复制一个URL(如https://news.163.com/23/1001/12/IIJQKQ7E0001899O.html),粘贴到浏览器,确认能正常打开——这是验证反爬策略有效的关键一步。

解析详情页

python crawler.py --mode detail --url https://news.163.com/23/1001/12/IIJQKQ7E0001899O.html

终端输出:

[INFO] 开始解析新闻详情页...
[INFO] 标题:华为发布全新AI芯片...
[INFO] 正文长度:1248字
[INFO] 发布时间:2023-10-01 12:30:00
[INFO] 解析完成,已存入数据库

此时,打开Django Admin后台(/admin/),进入News模型,就能看到这条新入库的新闻。点击编辑,确认标题、正文、时间字段均正确无乱码。

实操心得:我建议学生首次运行时,永远先跑--mode list,再挑1-2个URL跑--mode detail。这样能快速定位问题:如果列表页抓不到,说明UA或URL规则错了;如果详情页解析失败,说明HTML结构变了(网易可能改版),这时只需调整crawler.pyparse_news_detail()里的CSS选择器(如把div.post_text改成article.article-content),无需重写整个爬虫。

4.3 推荐逻辑调试:如何验证“它真的在按标签推荐”

推荐效果不能只靠眼睛看,要用数据验证。项目提供debug_recommender.py脚本,专门用于离线调试推荐逻辑:

# 运行调试脚本
python debug_recommender.py --user_id 2 --top_k 5

输出示例:

用户ID: 2,兴趣标签: ['科技', '军事']
新闻ID: 105,标题: '歼-20最新改进型亮相航展',相似度: 0.892
新闻ID: 88,标题: '中国航天发射再创纪录',相似度: 0.851
新闻ID: 123,标题: '华为发布昇腾AI芯片',相似度: 0.837
新闻ID: 45,标题: '美军F-35战机故障率上升',相似度: 0.795
新闻ID: 67,标题: '北斗导航系统全球服务',相似度: 0.763

这个输出的价值在于:它把“黑箱推荐”变成了“白盒决策”。你可以清晰看到:
- 用户选了“科技”和“军事”,系统确实优先推送了含“歼-20”“F-35”“北斗”的军事新闻,以及“华为”“AI芯片”的科技新闻;
- 相似度分数从0.892递减到0.763,符合预期;
- 如果发现某条新闻相似度低却排在前面,说明其TF-IDF向量中存在干扰词(如“歼-20”被分词为“歼”“20”,而“20”是停用词但未被过滤),这时就要回头检查chinese_tokenize()函数。

进阶技巧:想进一步验证TF-IDF效果?在debug_recommender.py中添加:
```python

打印新闻105的TF-IDF向量中Top 5权重词

vector = news_vectors_dict[105]
top_indices = np.argsort(vector)[-5:][::-1]
for idx in top_indices:
word = list(vectorizer.vocabulary_.keys())[idx]
print(f”{word}: {vector[idx]:.3f}”)
`` 输出可能是:歼-20: 0.421, 航展: 0.387, 改进型: 0.352, 歼: 0.012, 20: 0.008` ——立刻发现“歼”和“20”权重异常低,证明停用词过滤生效。

4.4 Django后台管理:不只是CRUD,更是业务理解入口

Django Admin不仅是数据库操作界面,更是理解业务逻辑的“活文档”。以News模型为例,admin.py中做了针对性定制:

# admin.py
from django.contrib import admin
from .models import News, UserProfile, Comment, BrowseLog

@admin.register(News)
class NewsAdmin(admin.ModelAdmin):
    list_display = ('title', 'source', 'pub_time', 'created_at', 'is_active')
    list_filter = ('source', 'pub_time', 'is_active')
    search_fields = ('title', 'content')
    date_hierarchy = 'pub_time'
    # 自定义动作:批量标记为“已审核”
    actions = ['make_active', 'make_inactive']

    def make_active(self, request, queryset):
        queryset.update(is_active=True)
    make_active.short_description = "标记为已审核"

    def make_inactive(self, request, queryset):
        queryset.update(is_active=False)
    make_inactive.short_description = "标记为待审核"

@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
    list_display = ('user', 'news', 'content_preview', 'created_at', 'is_approved')
    list_filter = ('is_approved', 'created_at')
    search_fields = ('content', 'user__username')

    def content_preview(self, obj):
        return obj.content[:30] + "..." if len(obj.content) > 30 else obj.content
    content_preview.short_description = '评论预览'

这些配置带来的体验提升是质的:
- list_display让管理员一眼看到新闻标题、来源、发布时间,无需点开每条记录;
- list_filter右侧的筛选栏,可快速筛选“今日发布的科技新闻”或“未审核的评论”;
- search_fields支持按标题或正文关键词全局搜索;
- date_hierarchy按发布时间生成年/月/日导航,查历史新闻效率翻倍;
- 自定义动作make_active,选中多条评论,一键批量通过,比逐个点击“批准”快10倍。

实操心得:很多学生把Admin当成“摆设”,只用来查数据。其实,把Admin配置好,本身就是一项重要的工程能力。比如CommentAdmin中的content_preview方法,避免了长评论挤占列表空间;is_approved字段加list_filter,让审核工作变得可视化。这些细节,会让答辩老师觉得:“这孩子不仅会写代码,还懂产品思维。”

5. 常见问题与排查技巧实录

5.1 爬虫类问题:从“403 Forbidden”到“空内容”

问题现象可能原因排查步骤解决方案
请求返回403 Forbidden目标网站检测到非浏览器请求1. 用浏览器访问同一URL,确认能打开
2. 查看crawler.pyheaders是否设置了User-Agent
3. 检查robots.txt是否禁止爬取
更换UA字符串;在headers中添加AcceptAccept-Language等字段;若robots.txt禁止,则更换目标站点(如从网易换到腾讯新闻)
抓取的新闻标题为空HTML结构变更,CSS选择器失效1. 打开浏览器开发者工具,检查目标页面标题所在标签
2. 查看parse_news_detail()title_tag = soup.find(['h1','h2'])是否匹配
修改选择器,如改为soup.select_one('h1.title');或使用正则从<title>标签提取
正文内容为空或极短正文在Ajax加载的<div id="content">中,requests无法执行JS1. 查看网页源代码(Ctrl+U),确认正文是否在源码中
2. 若不在,说明是动态渲染
放弃该页面,改抓服务端直出的频道页(如https://news.qq.com/的科技频道);或引入Selenium(不推荐,增加复杂度)
爬虫中途停止,无报错请求超时或连接被重置1. 在fetch_news_list()中添加logging.info(f"正在抓取第{page}页")
2. 查看日志文件crawler.log
增加timeout参数(如session.get(url, timeout=20));在except块中添加logging.exception("详细错误")

独家避坑技巧:当怀疑是反爬时,不要立即换代理或加延时,先做“最小化验证”:用curl -H "User-Agent: Mozilla/5.0..." URL命令在终端直接请求,看是否返回正常HTML。如果curl能拿到,说明Python代码没问题;如果curl也403,说明网站真封了这个UA,换一个即可。这个技巧能帮你节省80%的无效调试时间。

5.2 推荐逻辑类问题:为什么推荐结果“不相关”

问题现象可能原因排查步骤解决方案
所有新闻相似度都是0.0用户未选择任何标签,或标签未存入数据库1. 进入Admin后台,查看该用户UserProfile.interest_tags字段
2. 在Django Shell中执行UserProfile.objects.get(user_id=2).interest_tags
检查注册View中是否漏掉了user_profile.interest_tags = request.POST.getlist('tags');确认表单name属性是否为tags
推荐结果全是同一篇新闻新闻向量未归一化,长新闻天然权重高1. 在debug_recommender.py中打印两条新闻向量的模长np.linalg.norm(vector)
2. 比较长新闻(5000字)和短新闻(300字)的模长
TFIDFVectorizer.transform()中,对每个向量执行vector = [x / norm if norm > 0 else 0 for x in vector],即L2归一化
推荐结果与用户标签明显不符停用词表未覆盖领域专有名词1. 用debug_recommender.py打印匹配度最高的新闻的Top词
2. 查看这些词是否应为停用词(如“苹果”作为水果)
将领域词加入停用词表,如在hit_stopwords.txt末尾添加苹果\n华为\n歼20;重启Django服务(因向量已预计算,需重新运行python manage.py rebuild_vectors
推荐速度慢(>5秒)新闻向量未预加载,每次请求实时计算1. 在recommender.py中添加print("向量加载耗时:", time.time()-start)
2. 查看是否每次请求都执行vectorizer.fit()
将向量加载逻辑移至apps.pyready()方法;使用django.core.cache.cache缓存向量字典

独家避坑技巧:推荐不准时,第一个要检查的不是算法,而是数据质量。我遇到过最典型的案例:学生把“体育”标签拼错成“休育”,导致所有体育新闻都无法匹配。解决方案很简单:在Admin后台的Tag模型中,把所有预设标签做成下拉选项(choices),前端用<select>而非<input type="text">,从源头杜绝拼写错误。这种“防呆设计”,比调参重要10倍。

5.3 Django部署类问题:从“runserver”到“生产环境”

问题现象可能原因排查步骤解决方案
静态文件(CSS/JS)404开发模式下DEBUG=True,生产模式需配置STATIC_ROOT1. 查看浏览器开发者工具Network标签,确认/static/css/base.css返回404
2. 检查settings.pySTATIC_URLSTATICFILES_DIRS
生产环境运行python manage.py collectstatic,将所有静态文件收集到STATIC_ROOT指定目录;Nginx配置location /static/ { alias /path/to/static/; }
登录后跳转到/accounts/profile/报404未配置LOGIN_REDIRECT_URL1. 查看settings.py是否有LOGIN_REDIRECT_URL = '/'
2. 检查urls.py中是否漏掉了path('accounts/', include('django.contrib.auth.urls'))
settings.py中添加LOGIN_REDIRECT_URL = '/';确保urls.py包含认证URL路由
Admin后台样式错乱collectstatic未执行或STATIC_ROOT路径错误1. 进入STATIC_ROOT目录,确认admin/css/base.css是否存在
2. 查看Django启动日志,是否有Static files were successfully collected
重新运行python manage.py collectstatic --no-input;确认STATIC_ROOT是绝对路径(如os.path.join(BASE_DIR, 'staticfiles')
数据库迁移失败(no such tablemigrate未执行,或makemigrations生成了空文件1. 运行python manage.py showmigrations,查看哪些迁移未应用
2. 检查migrations/目录下是否有0001_initial.py
若无迁移文件,先python manage.py makemigrations;若有但未应用,执行python manage.py migrate

独家避坑技巧:生产环境部署前,务必运行python manage.py check --deploy。这个命令会自动检查10+项生产安全配置,如:DEBUG=False是否设置、ALLOWED_HOSTS是否为空、SECRET_KEY是否硬编码、密码校验器是否启用等。它能帮你避开90%的线上事故,比手动检查高效100倍。

6. 项目扩展与进阶方向:让它不止于毕设

这个系统不是终点,而是起点。基于现有架构,你可以平滑升级,而不必推倒重来:

6.1 数据层面:从“静态标签”到“动态兴趣建模”

当前用户兴趣完全依赖注册时的静态勾选。进阶方案是引入浏览行为分析
- 记录BrowseLog(用户ID、新闻ID、停留时长),计算“阅读完成率”(停留时长/新闻平均阅读时长);
- 对用户近期浏览的Top 5新闻,提取其TF-IDF向量的加权平均,生成动态兴趣向量;
- 推荐时,融合静态标签向量(权重0.4)与动态行为向量(权重0.6)。

实现要点:新增UserProfile.dynamic_interest_vector字段(JSONField),在用户每次浏览新闻后异步更新(用django-rqcelery),避免阻塞主线程。

6.2 算法层面:从“余弦相似度”到“混合推荐”

单一内容推荐易陷入“信息茧房”。可叠加热度加权
- 给每条新闻增加hot_score字段(基于24小时浏览量、评论数、分享数计算);
- 最终推荐分数 = 0.7 × cosine_similarity + 0.3 × hot_score_normalized
- 热度衰减:hot_score = browse_count / (1 + hours_since_pub),确保新新闻有机会曝光。

实现要点:在News模型中添加hot_score字段;编写定时任务(django-crontab),每小时更新一次热度分。

6.3 工程层面:从“单机SQLite”到“云原生部署”

当前SQLite适合开发,但生产环境需升级:
- 数据库:迁移到PostgreSQL(django.db.backends.postgresql),支持全文检索(SearchVector)替代TF-IDF;
- 缓存:引入Redis,缓存热门新闻列表、用户推荐结果(TTL=30分钟),降低数据库压力;
- 部署:用Docker打包(Dockerfile + docker-compose.yml),Nginx做反向代理,Gunicorn做WSGI服务器。

实现要点:settings.py中根据DEBUG环境变量切换数据库配置;requirements.txt中添加psycopg2-binaryredisdjango-redis

6.4 产品层面:从“功能完整”到“用户体验优化”

技术之外,产品细节决定项目高度:
- 推荐理由可视化:在推荐新闻卡片上,显示“匹配标签:科技(相似度0.82)”,增强可信度;
- 负反馈机制:每条推荐新闻旁加“不感兴趣”按钮,点击后将该新闻ID加入用户黑名单,后续不再推荐;
- 多端适配:用Django模板的@media查询,让新闻列表在手机端变为单列瀑布流,提升移动端体验。

实现要点:前端模板中,{{ recommendation.reason }}动态渲染匹配信息;后端View接收dislike请求,将news_id存入UserProfile.dislike_news JSONField。

最后分享一个小技巧:答辩前,用手机浏览器访问你的系统(http://your-pc-ip:8000),亲自操作一遍注册、选标签、看推荐、提评论。当评委老师问“这个系统在手机上能用吗?”,你掏出手机当场演示,比说100句“支持响应式设计”都有力。真正的工程师,永远让用户视角先行。

这个项目的价值,不在于它有多前沿,而在于它足够真实——真实的数据、真实的反爬、真实的性能瓶颈、真实的用户反馈。当你亲手修复第5个爬虫403错误、第3次调整停用词表、第2次重构推荐向量加载逻辑时,你获得的不是一份毕业设计报告,而是一种笃定:我知道问题在哪,我知道怎么解决,我知道下次遇到类似问题,我能更快搞定。这,才是技术成长最扎实的脚手架。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一个开箱即用的本科级新闻推荐系统实现,支持游客浏览和登录用户个性化推荐。前端展示新闻列表与热点板块,登录后可查看按兴趣标签匹配生成的推荐内容、提交评论、管理浏览历史及个人资料。后台由Django Admin驱动,管理员能对用户、新闻、评论和日志进行完整CRUD操作。新闻数据通过requests定向采集网易头条等平台,使用jieba分词结合TF-IDF提取关键词,并与用户注册时选择的兴趣标签做余弦相似度计算,实现轻量但可解释的协同过滤逻辑。内置多版本中文停用词表(哈工大、川大、百度),附带requirements.txt、本地运行说明和演示资源,无需额外配置即可启动调试。适合课程设计或毕业设计直接参考,代码结构清晰,模块职责分明,涵盖数据采集、特征处理、匹配推荐与后台管理全流程。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

本文章已经生成可运行项目
内容概要:本文介绍了基于噪声抑制半监督学习的锂离子电池SOH(State of Health,健康状态)估计方法的Python代码实现。该方法融合半监督学习框架与先进的噪声抑制机制,旨在利用少量标注样本和大量未标注数据,有效提升电池健康状态预测的精度与模型鲁棒性,特别适用于实际工程中电池老化数据标注成本高、样本稀缺的挑战性场景。通过设计高效的特征提取网络与可靠的伪标签生成及优化策略,模型能够有效识别并抑制训练过程中的噪声干扰,增强在复杂工况和数据波动下的泛化能力与稳定性。; 适合人群:具备一定机器学习理论基础和Python编程能力,从事电池管理系统(BMS)、新能源汽车、储能系统等领域的科研人员、工程师,以及专注于电池寿命预测、设备状态监测与智能运维等方向的硕博研究生;; 使用场景及目标:①解决锂离子电池SOH估计中标注数据获取困难、成本高昂的核心痛点;②提升模型在存在测量误差、传感器漂移或异常数据等噪声环境下的预测准确性与可靠性;③为相关科研课题提供可复现、可扩展的算法基准与开源代码框架,加速算法迭代与工程落地; 阅读建议:此资源以Python代码为核心载体,强调算法的完整复现与实验验证过程,建议读者结合代码逐模块剖析模型架构、损失函数设计与训练流程细节,并积极在自有电池数据集上进行迁移学习、参数调优与性能对比,以深入掌握半监督学习与噪声抑制技术在电池退化建模中的关键应用。
内容概要:本文系统阐述了高速串行信号在传输过程中因信道损耗导致的眼图闭合问题,并介绍了主流的均衡技术解决方案。信道作为低通滤波器,会显著衰减高频分量,造成码间干扰(ISI),尤其在速率高于1GHz时更为严重。为恢复信号质量,发送端采用FFE(前馈均衡器)实施预加重或去加重,提前补偿高频损耗;接收端则结合CTLE与DFE进行二次均衡:CTLE通过模拟高通滤波器“削峰填谷”提升高频响应,而DFE利用反馈机制消除后向码间干扰,提高判决准确性。文章详细解析了各均衡模块的工作原理、电路结构、关键参数及其频率响应特性,并通过PCIe 3.0实例展示均衡前后眼图的改善效果,最后对比了不同技术的适用场景与优缺点。; 适合人群:从事高速电路设计、信号完整性分析等相关领域的工程师和技术人员,以及具备一定通信原理和电子工程基础的研发人员; 使用场景及目标:①理解高速信号传输中的损耗机理与眼图退化成因;②掌握FFE、CTLE、DFE等均衡技术的设计原理与应用场景;③为高速接口(如PCIe、USB等)的信号完整性优化提供理论支持与实践指导; 阅读建议:建议结合仿真工具观察不同均衡参数对眼图的影响,深入理解抽头系数、零极点配置等关键参数的调节逻辑,并关注实际硬件实现中的功耗与噪声权衡问题。
内容概要:本文详细介绍了基于光伏逆变器调节的中低压配电网电压分层协调控制策略的Matlab代码实现方法,旨在应对高渗透率光伏发电接入导致的配电网电压越限问题。该策略构建了分层控制架构,上层采用集中式优化算法进行全局决策,下层则依赖本地测量信息实现快速动态响应,从而实现对多个分布式光伏逆变器的协同调控。文中系统阐述了控制模型的数学建模过程、优化求解算法的设计流程以及仿真验证方案,并基于标准测试系统开展了仿真实验,结果表明该方法在抑制电压波动、提升电压稳定性、降低网络损耗方面具有显著优势,具备良好的工程应用前景。; 适合人群:具备电力系统分析、自动控制理论及新能源并网技术等相关专业知识背景,熟悉Matlab/Simulink仿真平台,从事相关领域科研工作的高校研究生、科研院所研究人员及电力行业工程技术开发人员。; 使用场景及目标:①深入研究高比例分布式光伏接入背景下中低压配电网的电压控制难题;②掌握分层协调控制体系的设计理念与实现机制;③学习并实践基于Matlab的电力系统建模、优化算法编程与仿真分析技术;④为实际配电网电压调控项目提供可复现、可拓展的技术参考与解决方案支撑。; 阅读建议:建议读者结合所提供的Matlab代码与典型配电系统模型开展仿真实践,重点剖析控制架构的层级划分逻辑、通信机制设计以及优化目标函数的构建方法,同时可尝试将其拓展至多时间尺度协调控制、考虑通信延迟与数据丢包等更贴近实际的复杂应用场景中进行深入研究。
源码下载地址: https://pan.quark.cn/s/2e09cbca2d82 【针对青龙面板wxpusher推送问题的修复包】是一个为青龙面板(QL Panel)设计的更新或改进版本,其目的是纠正与wxpusher相关的推送故障。青龙面板是一个被广泛应用的自动化部署及管理解决方案,它赋予用户在图形化环境中执行多种IT运维操作的能力,涵盖脚本操作、周期性任务执行以及文件处理等。wxpusher作为青龙面板的一个附加组件,承担着实现微信消息推送的任务,用以保障用户可以即时获取系统关键状态变动或异常警报。 sendNotify.js 是此修复包内含的关键模块,属于wxpusher插件的一部分,其核心职责是发送各类通知。在JavaScript编程语言中,该脚本或许内含了处理推送流程、调用微信推送端点以及消息内容的格式化和校验等操作。一旦青龙面板监测到新的事件状况,例如任务成功完成、发生错误或系统进入维护阶段,sendNotify.js便会被激活,将相关数据传送至用户的微信账户。 ql.js 另一个重要的构成部分,可能代表了青龙面板的核心代码库或专用于wxpusher的设定文档。它或许包含了整合wxpusher的相关参数,例如启动过程、授权核实、故障应对和日志管理等功能。ql.js与sendNotify.js协同工作,确保wxpusher能够顺利与青龙面板进行数据交互,并可靠地向用户传递通知信息。 CK_WxPusherUid.json 这一文档很可能是储存用户信息或配置参数的JSON格式文件。"CK"通常指代Cookie或某种缓存机制,而"WxPusherUid"可能意指wxpusher用户的专属识别码。该文件用于保存用户微信推送服务的连接详情,让青龙面板可以...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值