每年这个时候,都有一批人倒在同一道坎上:毕业设计题目里带着“机器学习”“爬虫”“情感分析”这些词,看起来高大上,实际动手却发现数据从哪来找不到、模型准确率高不起来、系统做出来像一堆脚本拼盘。我之前接过不少这样的求助,也帮人完整做过一个新闻情感分类平台,今天就把这套东西的关键链路、踩坑记录和最终能跑的方案一次性讲清楚。这个项目是以“基于机器学习的新闻情感分类平台”为核心,用Django做Web框架,Scrapy负责新闻采集,NLP负责文本清洗与分析,朴素贝叶斯承担情感分类,TextRank做关键词和摘要提取,最后再把数据分析和可视化接进去。适合正在做毕业设计、课程大作业,或者想完整走一遍“爬虫—算法—Web系统”全流程的人参考。我尽量把每一步为什么这么选、实际跑起来会遇见什么都说透,而不是只给一个看似正确的空壳。
1. 为什么选这个题:一台“新闻情感分类平台”到底解决了什么问题
1.1 从手工标注到自动判别的痛点
我曾经帮某公司处理舆情周报,他们最原始的做法是让实习生把当天新闻一条条点开,人工判断是正面、负面还是中性,再按主题汇总。这个流程最大的问题不是慢,而是标准不统一。同一条关于数码产品涨价的新闻,A实习生觉得是负面,B实习生觉得是中性,因为两个人对“情感”的定义不一致。更麻烦的是,人工标注很难覆盖全天产生的新闻量,最后只能抽样,抽样的偏差又让周报结论站不住脚。
所以这个毕业设计的核心,不只是“做一个分类器”,而是要做一套能替代人工初筛的流水线:自动抓取新闻、自动清洗、自动判断情感倾向、自动提取关键信息,最后通过Django把结果展示成可交互的平台。这样领导或者用户打开网页就知道今日正面新闻占比多少、负面集中在哪些主题、哪些关键词正在发酵。这个概念放到毕设里,既容易讲清楚背景,又有明确的交付物可以演示。
1.2 系统五层架构与技术选型对应表
整个平台我按五层来拆,每一层对应一个独立的模块,这样写论文的时候很好组织,代码上也能分开调试:
| 层级 | 对应模块 | 技术选型 | 核心职责 |
|---|---|---|---|
| 数据采集层 | Scrapy爬虫 | Scrapy + 自定义Middleware | 从目标新闻站点抓取标题、正文、发布时间、来源 |
| 数据预处理层 | 清洗与NLP | jieba分词 + 自定义清洗函数 | 去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(text=raw) 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_features=8000:限制特征数量。全量特征接近3万维,朴素贝叶斯虽然不太怕高维,但Django后端每次预测都要做同样的向量化,特征越多耗时越长。8000维在保效果和保速度之间比较均衡。ngram_range=(1, 2):加入2-gram。“不推荐”“太差”“很满意”这类组合词比单字分词更能体现情感,测试下来2-gram让准确率提升约3个百分点。min_df=3:在小于3篇新闻里出现的词直接忽略,减少噪声特征。sublinear_tf=True:对词频做对数缩放,避免某篇很长的新闻因为重复词多而主导特征的尺度。
3.2 模型训练与评估:不能只看准确率
训练集和测试集的划分比例,我用的是7.5:2.5,同时保留了一部分完全没参与训练的后台新闻作为压测数据。这里有一个特别容易犯的错误:直接把所有新闻随机打乱切分,导致同一条新闻的不同转载版本既出现在训练集又出现在测试集,模型“作弊”之后准确率高得吓人,一上真实数据就崩。
所以我做数据集切分时加了一个条件:先按新闻事件聚类去重,再把去重后的独立样本做切分。这一步之后,准确率会有所下降,但它才是真实水平。最终三类(正面、负面、中性)的实验结果大致如下:
| 指标 | 数值 |
|---|---|
| 准确率(Accuracy) | 0.862 |
| 精确率(Precision) | 0.853 |
| 召回率(Recall) | 0.847 |
| F1值 | 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, topK=8): jieba.analyse.set_stop_words('news_stopwords.txt') jieba.load_userdict('news_userdict.txt') keywords = jieba.analyse.textrank( text, topK=topK, withWeight=True, 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变成了“演示时不会翻车”的系统。如果你也想做类似的毕设,建议先按照上面的顺序从数据抓起,哪怕模型一开始准一点都很正常,先把流水线跑通,后面调参才有意义。