
每年这个时候都有一批人倒在同一道坎上毕业设计题目里带着“机器学习”“爬虫”“情感分析”这些词看起来高大上实际动手却发现数据从哪来找不到、模型准确率高不起来、系统做出来像一堆脚本拼盘。我之前接过不少这样的求助也帮人完整做过一个新闻情感分类平台今天就把这套东西的关键链路、踩坑记录和最终能跑的方案一次性讲清楚。这个项目是以“基于机器学习的新闻情感分类平台”为核心用Django做Web框架Scrapy负责新闻采集NLP负责文本清洗与分析朴素贝叶斯承担情感分类TextRank做关键词和摘要提取最后再把数据分析和可视化接进去。适合正在做毕业设计、课程大作业或者想完整走一遍“爬虫—算法—Web系统”全流程的人参考。我尽量把每一步为什么这么选、实际跑起来会遇见什么都说透而不是只给一个看似正确的空壳。1. 为什么选这个题一台“新闻情感分类平台”到底解决了什么问题1.1 从手工标注到自动判别的痛点我曾经帮某公司处理舆情周报他们最原始的做法是让实习生把当天新闻一条条点开人工判断是正面、负面还是中性再按主题汇总。这个流程最大的问题不是慢而是标准不统一。同一条关于数码产品涨价的新闻A实习生觉得是负面B实习生觉得是中性因为两个人对“情感”的定义不一致。更麻烦的是人工标注很难覆盖全天产生的新闻量最后只能抽样抽样的偏差又让周报结论站不住脚。所以这个毕业设计的核心不只是“做一个分类器”而是要做一套能替代人工初筛的流水线自动抓取新闻、自动清洗、自动判断情感倾向、自动提取关键信息最后通过Django把结果展示成可交互的平台。这样领导或者用户打开网页就知道今日正面新闻占比多少、负面集中在哪些主题、哪些关键词正在发酵。这个概念放到毕设里既容易讲清楚背景又有明确的交付物可以演示。1.2 系统五层架构与技术选型对应表整个平台我按五层来拆每一层对应一个独立的模块这样写论文的时候很好组织代码上也能分开调试层级对应模块技术选型核心职责数据采集层Scrapy爬虫Scrapy 自定义Middleware从目标新闻站点抓取标题、正文、发布时间、来源数据预处理层清洗与NLPjieba分词 自定义清洗函数去HTML、去噪、分词、去停用词、去重算法分析层分类与摘要朴素贝叶斯 TextRank情感极性分类、关键词提取、摘要生成应用展示层Web平台Django ECharts提供预测接口、后台管理、可视化看板数据存储层持久化SQLite/MySQL Redis缓存存储新闻与标注结果加速重复查询这个分层不是拍脑袋定的而是根据毕设答辩时老师最容易问的问题设计的数据怎么来的、算法怎么用的、系统怎么落地的。只要每一层都有对应的答案整个项目的逻辑闭环就完整了。1.3 适合谁参考以及需要什么基础如果你是零基础我建议至少先跑通“爬虫抓取—数据入库—jieba分词—朴素贝叶斯训练”这条最小链路再考虑上Web展示。如果你已经有Python基础可以直接按本文后面的步骤做。这个题目比较友好的地方在于朴素贝叶斯和TextRank都是相对容易解释的经典算法比起直接上BERT之类的大模型答辩时反而更容易讲清楚原理。大模型效果虽好但解释成本高而且对设备有要求用在本科毕设里容易被追问细节。2. Scrapy采集与清洗先拿到一批“敢用”的新闻语料2.1 目标站点选择与采集边界新闻语料是整个项目的地基。很多人第一步就死在数据上随便找一个门户网站去爬结果对方反爬策略很强IP被封或者页面结构全部是动态渲染Scrapy默认拿不到正文内容。我第一版就踩过这个坑选了某个新闻聚合站结果它的新闻列表是异步加载的直接写xpath取不到数据后来不得不改用Selenium效率低了很多。做毕设时选数据源有个原则优先选静态页面、结构规整、更新频率可控的站点。比如一些地方性新闻门户、体育资讯站、科技媒体它们的列表页往往是服务端渲染正文是HTML里的静态文本这类站点用Scrapy写起来很顺。我用的是某科技资讯站和某体育新闻站两个数据源抓了标题、发布时间、正文、分类四个字段最终入库差不多12000条新闻其中带情感标注的样本大概8000条。2.2 Item Pipeline三层清洗设计Scrapy里我设计了三个Item Pipeline按顺序执行这样每个管道只管一件事逻辑清晰第一层是HtmlCleanPipeline负责去除正文里的HTML标签、script块、style块和多余换行。新闻页面为了排版会塞很多无用标签直接用正则匹配[^]虽然简单但会把正文里的合法字符也误删。我换成了parsel库的Selector配合xpath提取正文节点再用re.sub去掉残留的空白符。第二层是DeduplicatePipeline基于标题的MD5哈希和正文的前50个字做一个指纹存入Redis的Set里。如果指纹已经存在直接丢弃这条数据。这个设计可以防止定时爬虫重复入库后期我每天跑一次增量爬取就是靠这个管道保证新闻不重复。第三层是StandardizePipeline统一时间格式、来源字段、正文字段长度。这里有个容易被忽略的细节有的站点日期是“昨天”“前天”这种相对表达有的是不同时区的时间戳我写了一个解析函数把相对日期统一换算成yyyy-MM-dd HH:mm:ss格式作为情感趋势分析的基准。# pipelines.py 简化示意 import hashlib, re from parsel import Selector class HtmlCleanPipeline: def process_item(self, item, spider): raw item.get(content, ) sel Selector(textraw) content sel.xpath(//div[contains(class,article)]//text()).getall() text .join(content) text re.sub(r\s, , text).strip() if len(text) 50: raise DropItem(正文过短丢弃) item[content] text return item2.3 分词、停用词与新闻去重清洗完文本后进入NLP预处理jieba分词、去停用词、再把文本向量化。这一块的工作量看起来小实际对后面的模型影响非常大。先说分词。新闻和普通聊天文本不一样有很多专有名词、品牌名、产品名比如“iPhone”“骁龙”“新能源汽车”。jieba默认词典对这些词切得比较碎我测试时发现“新能源汽车”会被切成“新能源”和“汽车”导致特征维度变高而且语义分散。解决办法是加载自定义词典把常见新闻实体词维护到一个user_dict.txt里每行一个词加一个词频分词前用jieba.load_userdict读取。这个操作能把很多关键特征从碎片变成整体对朴素贝叶斯这类基于词频的模型尤其重要。再一个坑是停用词表不能乱用。网上常见的停用词表大部分是给通用文本用的里面包含“我们”“可以”“但是”这类词但新闻里一些看似是停用词的词其实有情感信息比如“不过”“竟然”“遗憾”删掉之后负面信号就丢了。我最后保留了一份专门针对新闻场景整理的停用词表只有在分词结果里出现频次极高且无语义贡献的词才删除比如“记者”“报道”“新闻”“时间”这类通用新闻高频词。去重方面除了Pipeline里的哈希去重文本层面还要做一次相似去重因为同一个新闻事件会被多家媒体转载标题不同但正文高度相似。我用的是difflib.SequenceMatcher对正文前200个字符计算相似度超过0.8就认为是转载只保留最早发布的版本。这一步做完语料质量会有非常明显的提升。3. 朴素贝叶斯分类为什么小样本下它比深度学习更稳3.1 特征工程TF-IDF向量化的关键参数朴素贝叶斯的核心假设是特征之间条件独立所以它的效果上限很大程度取决于特征表达。这里我选择TF-IDF而不是单纯词频理由很直接新闻里“手机”“公司”“产品”这类词出现频率极高但它们不携带情感倾向如果直接按词频统计它们会淹没“好评”“推荐”“失望”“投诉”这些真正有用的情感词。TF-IDF会降低高频无信息量词的权重提高只在特定情感类别中出现的词的区分度。在sklearn.feature_extraction.text.TfidfVectorizer里我调了四个参数这里值得单独拿出来讲max_features8000限制特征数量。全量特征接近3万维朴素贝叶斯虽然不太怕高维但Django后端每次预测都要做同样的向量化特征越多耗时越长。8000维在保效果和保速度之间比较均衡。ngram_range(1, 2)加入2-gram。“不推荐”“太差”“很满意”这类组合词比单字分词更能体现情感测试下来2-gram让准确率提升约3个百分点。min_df3在小于3篇新闻里出现的词直接忽略减少噪声特征。sublinear_tfTrue对词频做对数缩放避免某篇很长的新闻因为重复词多而主导特征的尺度。3.2 模型训练与评估不能只看准确率训练集和测试集的划分比例我用的是7.5:2.5同时保留了一部分完全没参与训练的后台新闻作为压测数据。这里有一个特别容易犯的错误直接把所有新闻随机打乱切分导致同一条新闻的不同转载版本既出现在训练集又出现在测试集模型“作弊”之后准确率高得吓人一上真实数据就崩。所以我做数据集切分时加了一个条件先按新闻事件聚类去重再把去重后的独立样本做切分。这一步之后准确率会有所下降但它才是真实水平。最终三类正面、负面、中性的实验结果大致如下指标数值准确率(Accuracy)0.862精确率(Precision)0.853召回率(Recall)0.847F1值0.850说实话这个数字在实验室环境下不算惊艳但足够说明问题。我印象最深的是一次失败的案例某条新闻标题是“苹果发布新款耳机网友吐槽价格太贵”分词后“吐槽”和“太贵”都很明显但模型却判成了中性。查看特征权重之后发现训练数据里“吐槽”这个词出现在中性样本里的次数更多因为很多娱乐新闻会用“吐槽”指代一种轻松讨论的语气。这暴露了朴素贝叶斯的一个先天短板它把词语情感和句子情感强行对应丢弃了上下文修饰关系。解决思路是我在特征里增加了情感词数量统计、否定词数量、感叹号是否出现这三个手工特征最终把这类样本拉回了一些。3.3 朴素贝叶斯选型的真正理由有人问为什么不直接用深度学习我的观点是毕设项目注重可解释性朴素贝叶斯在几千条样本规模下不会比简单神经网络差太多但它有一个巨大的优势——训练极快且模型参数可以直接导出。我把训练好的朴素贝叶斯用joblib序列化保存Django里加载一次单条文本分类耗时不到30毫秒这在答辩演示时是很大的加分项。相比之下如果在Django里跑BERT推理不仅要额外部署深度学习框架还要考虑显存和响应延迟复杂度会翻好几倍。4. TextRank抽词与摘要让新闻“重点”自动浮出来4.1 TextRank在图上的随机游走逻辑TextRank最开始是用在自动摘要里的本质是一个图上PageRank的变体。把每个词看成一个节点如果两个词在同一个窗口内共现就在这两个节点之间连一条边。词的重要性不是只看它出现的次数而是看它被多少重要邻居指向。这非常像学术圈的影响力一篇论文被很多高引论文引用它自己也很重要一个词如果总跟“价格”“上涨”“发布”这些词一起出现它大概率也是新闻里的关键信息。我用jieba.analyse.textrank直接做关键词提取因为它的底层已经把window、span、pagerank的默认参数封装好了直接用效果就不错。在做摘要时我按句子切分新闻正文用句子作为节点重新构图句子的权重由内部关键词的TextRank分数累加得到最后按得分从高到低抽出三句话作为摘要。4.2 与分词工具结合的落地实现具体实现上首先是分词和词性过滤。我做了个判断TextRank特取时只保留名词、动词、形容词和英文专有名词像语气词、助词、标点符号全部剔除。这个过滤很关键因为新闻里的“的”“了”“在”没有语义价值留着它们会凑出很多听起来很蠢的关键词。另一个细节是窗口大小。jieba的textrank默认窗口是5意味着两个词如果在一句话里间隔不超过5个词它们就算共现。窗口越大词和词之间的联系越稠密噪声越多窗口太小则上下文关联丢失。我测试过窗口为3、5、7的情况5的F1值最稳定我就把它固定下来。# keyword_extractor.py 简化示意 import jieba.analyse def extract_keywords(text, topK8): jieba.analyse.set_stop_words(news_stopwords.txt) jieba.load_userdict(news_userdict.txt) keywords jieba.analyse.textrank( text, topKtopK, withWeightTrue, allowPOS(ns, n, vn, v, a, eng) ) return [(word, round(weight, 4)) for word, weight in keywords]4.3 摘要和情感分类的互相配合这一层不只是做展示它还能反过来帮助分类器。我做的方案是先把新闻正文用TextRank压缩成三句话摘要然后分别对摘要和正文做情感分类如果两者分类结果一致就把这个样本当作“高置信样本”加入训练集如果不一致再判断是摘要漏掉了关键情感信息还是正文存在转折。这算一种简单的置信度采样策略虽然没有复杂半监督学习那么高级但在毕设里作为亮点讲完全够用。另外TextRank抽取的关键词可以直接做主视觉的热词展示。我在平台首页放了一个词云区域词的大小对应TextRank权重。看起来只是可视化装饰实际它在答辩时可以直观说明“算法有效”负面新闻的热词必然会出现“问题”“投诉”“下降”“故障”正面新闻则会出现“突破”“增长”“好评”“上线”。这种反差是对模型可信度的一种侧面验证。5. Django平台整合与可视化从算法脚本到可演示系统5.1 模型序列化与预测接口设计到了这一层前面所有脚本都要从Jupyter Notebook搬到Django工程里。我用的方案是创建独立的ml_engine应用把加载模型、向量化、特征拼接、预测这些逻辑全部封装成类方法Django的View只负责接收请求和返回JSON不直接操作模型。加载模型时有个关键点防止重复加载。朴素贝叶斯模型和TF-IDF向量器加起来有几十MB如果每个请求都重新加载一次接口必然卡死。我用了Django的AppConfig.ready()钩子在服务启动时只加载一次然后存到threading.local()里保证多线程环境下每个线程拿到的模型实例是隔离且只加载一次的。实测接口在并发20个请求下平均响应时间是120ms完全够用。预测接口我用的是Django REST Framework提供两个主要端点一个提交单条新闻文本返回情感类别和置信度另一个提交批次文本返回每条结果和整体统计。前端是原生HTML加Ajax调用页面不用刷新就能看到分类结果。接口的返回结构做了统一包装后面接任何前端模板都不需要改逻辑。5.2 情感趋势、热词分布等可视化看板可视化部分我用的是ECharts尤其是三个图表最能体现项目价值第一个是近7天情感趋势图横轴是日期纵轴是各情感类别的新闻数量。这个图需要的SQL是由Scrapy入库的发布时间来GROUP BY再从分类表里按情感类别聚合。因为新闻数据量不大直接ORM查询即可没必要上复杂的数据仓库。第二个是情感占比饼图展示全部语料中正面、负面、中性占比。这个图有一个隐藏用途可以验证人工标注标准。我最初标注的数据里中性占比超过60%说明很多人把“没有强烈褒贬”的新闻都归为中性但这会稀释分类器的判别能力。后来我把中性定义为“包含明显正负倾向但缺少主观评价”的文本重标注一轮之后三类比例变成了正面35%、负面22%、中性43%模型效果才稳定下来。第三个是热词榜单的Top20横向柱状图数据来自TextRank的结果聚合。这个榜单可以按日筛选也可以按情感类别筛选对比“正面新闻里的高频词”和“负面新闻里的高频词”在日常运维看板里非常实用。5.3 缓存、异步与部署注意点Django开发环境跑起来很容易但要做成能稳定演示的系统有几个细节逃不掉。数据库我建议直接用MySQL或PostgreSQL而不是SQLite。因为在爬虫高频写入和Web查询同时进行时SQLite经常出现database is locked。如果你不想自己装数据库也可以先不改但论文里一定要写明“生产环境替换为MySQL”否则答辩老师问起来会比较被动。查询缓存我用了一层Redis。热门关键词的热词榜、最近24小时情感分布这类高频接口缓存时间设置为180秒。爬虫更新数据后主动删除相关缓存这样新数据进来最多等三分钟就能在页面上看到变化又不会因为频繁刷新拖垮数据库。部署上最省心的方式是使用gunicorn加反向代理。Django自带的runserver只能应付本地开发班级答辩时如果同时十几个人打开页面大概率会卡。我实际部署在一台普通云服务器上关闭了Debug模式静态文件交给反向代理托管。Scrapy定时爬虫我用系统计划任务每天凌晨跑一次爬完自动调用增量分类任务整个过程不需要人工干预。6. 复盘与避坑清单那些跑通之后才明白的事6.1 最隐蔽的坑数据泄漏造成的“假高准确率”这个坑我在前面提过但要再强调一次。有一次我把准确率跑到了0.91当时还挺兴奋结果发现是数据集切分时没有去重同一条事件的转载新闻把相似的文本同时分到了训练集和测试集。模型记住的是一种“模板”而不是真正的“情感规律”。从那之后我养成了一个习惯任何实验前先算一下数据集中标题的重复率如果去重后的样本量和去重前差太多说明语料的冗余度高必须先做相似度合并再划分训练集合测试集。6.2 中文编码与存储字段的坑第二个坑出现在文本写入数据库时。新闻正文里偶尔会带一些特殊符号、全角空格和不可见字符如果数据库表是utf8字符集某些生僻字会写入失败。我在建表时统一用了utf8mb4并把正文的字段类型设为LONGTEXT避免长文被截断。还有一个坑是分词词典文件本身的编码因为我是用Windows编辑的user_dict.txt文件保存成了GBK编码导致线上分词时乱码。最后所有字典文件都统一转成UTF-8并在脚本开头加了一行编码声明问题才彻底解决。6.3 给后续扩展留下的一点建议如果你做到这里还想继续往上走我觉得有三个方向性价比比较高第一是把朴素贝叶斯换成逻辑回归或支持向量机做对比实验放进论文里作为算法对比章节第二是引入情感词典做弱监督打标减少人工标注量第三是把平台改造成支持用户提交任意新闻链接自动完成采集加分析的流程。这些方向都不是新造轮子而是在现有系统上做增量答辩时可以体现出你对项目演进的思考。这个项目最让我满意的部分不是某个算法跑出了多高的准确率而是把“新闻采集—文本清洗—情感分类—关键词提取—可视化展示”这一整条链路完整搭了起来。它不依赖某个单一模型的神奇效果更多靠的是每一层都能稳定衔接、每一层都能看到实际产出。现在回想起来花时间最多的不是写模型而是那些看起来不起眼的清洗、去重、编码、缓存问题。但恰恰是这些问题才把一个“理论上可行”的demo变成了“演示时不会翻车”的系统。如果你也想做类似的毕设建议先按照上面的顺序从数据抓起哪怕模型一开始准一点都很正常先把流水线跑通后面调参才有意义。