网易新闻评论情感分析可视化:Python爬虫到ECharts大屏实战
做这个项目的起因其实特别朴素——我一直在琢磨一个问题某个热点话题出来后网上铺天盖地的评论到底是在夸还是在骂靠感觉说好像都在骂没有说服力有没有办法用Python把某类新闻的评论抓下来用情感分析跑出一个量化结果再用可视化大屏把正面、负面、中性的比例、评论数量趋势、高频关键词一目了然地甩出来这套网易新闻舆论情感分析可视化项目就是奔着这个目标去的。它完整走通了Python爬虫采集、中文文本清洗、情感分析建模、ECharts可视化展示的整条链路适合正在学Python数据分析与可视化、想做完整实战项目而不是碎片化Demo的同学参考。项目难度中等偏上但每个环节都有成熟方案跟着做基本能跑通。1. 做这个项目之前我先把需求拆成了三个问题很多人拿到这个标题就开始写代码结果爬到一半发现接口不对分析完又说不出模型好坏最后可视化只能画个死图。我建议动手前先想清楚三件事这会直接影响后面每一步的技术选型。1.1 舆论情感分析到底在分析什么舆论情感分析的本质是一个文本分类任务给定一条新闻评论判断它是正面、中性还是负面。听起来简单但实际落地时有个关键认知——新闻评论和电商评论不一样。电商评论说物流快、质量好就是正面说坏了、退货就是负面语义很直接。新闻评论里大量存在反讽、网络梗、缩写、表情符号比如一句真是棒棒的又涨价了词典按字面意思会判成正面人却知道这是负面。这说明单靠词表或者单靠模型都会出问题后面我专门花了一个章节讲怎么融合多种方法。另外分析的目的不只是给单条评论打标签而是要回答三个层面问题整体倾向这批评论里正负面比例是多少中性占多少舆论是偏向哪边的时间趋势评论量和正负面占比随时间怎么变化事件有没有二次发酵热点聚焦大家反复提到的关键词是什么这些词背后的情绪是什么可视化环节展示的就是这三个层面后面的章节全部围绕它们展开。1.2 为什么选中网易新闻而不是微博、知乎或抖音平台选型直接决定了爬虫难度和数据质量。我对比过几个主流平台最后选了网易新闻理由有三个。网易新闻的评论接口是半公开的通过分析网页请求就能拿到评论数据不需要登录态不像微博评论区那样需要复杂签名。它的新闻正文页结构规整docid新闻唯一标识就嵌在页面源码里拿到docid就能调评论接口。评论区包含点赞数、回复数、ip归属地、时间戳、用户昵称这些字段对后续分析很有价值。相比抖音、快手这类视频平台的评论接口网易新闻的字段更规整更适合做入门级舆论分析。合规性是另一个重要考量。网易新闻的公开评论是平台向所有用户展示的数据爬取公开数据、控制访问频率、仅用于学习研究是我给自己划的三条红线。项目里我会限制每篇文章只抓取前几页的热门评论请求间隔不低于0.5秒不对平台造成压力。结论只做技术演示不对任何真实社会事件的价值取向做判断。1.3 可视化要解决给谁看的问题可视化最容易犯的错是堆图表——搞了十个图表每个都好看但看的人抓不住重点。我一开始也掉进这个坑后来把需求收敛成一句话看的人要在5秒内知道舆论是偏正还是偏负然后能往下钻到时间线和热词。所以大屏页面我只放了四块内容顶部KPI指标总评论数、正面数、负面数、中性数中间情感占比玫瑰饼图展示正负中性占比右侧评论趋势折线图展示每天评论量和正负面占比变化底部热词词云展示评论中出现频率最高的关键词所有图表数据来自同一个后端接口前端定时刷新这样大屏放在办公室里能自动更新。定好这个框架后我才开始写代码后面每个环节都是为这四块内容服务的。2. 网易新闻评论的数据采集与清洗数据采集是整个项目的地基。这节我完整记录我当时怎么一步步找到接口、绕过反爬、把数据存成干净的结构化表格踩过的坑都会标出来。2.1 找评论接口的思路F12加源码分析爬网易新闻评论的第一步不是写代码而是打开浏览器开发者工具。我是这样操作的先在网易新闻首页随便打开一篇新闻按F12切到Network面板刷新页面盯着XHR请求列表。新闻正文的评论是动态加载的往下滚动页面时会出现一个类似http://comment.api.163.com/api/v1/products/a28696745777777/photos/XXXXX/threads/XXXXX/comments/newList?offset0limit30的请求里面返回的就是JSON格式评论数据。观察后发现接口包含几个关键参数productKey固定值表示网易新闻这个产品线docId新闻的唯一ID在正文页源码里能找到offset分页偏移量每次加30就是翻页limit每页条数最大可以设50docid的获取方式是在新闻正文页查看源代码搜索docid或者postid字段它是一个很长的字符串类似F2K4L5U6000187VI。有了docid评论接口的地址就拼出来了。2.2 爬虫代码实现控制频率比堆并发重要我用的核心库是requests加pandas没有上Scrapy因为项目规模不需要分布式爬虫单线程加合理延时就够了。这里的关键代码分为两部分一是从新闻列表页拿docid集合二是拿每个docid下的评论。import requests import pandas as pd import time import re HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36, Referer: https://news.163.com/ } def get_docid_from_news_list(category_url, pages3): 从新闻列表页提取新闻的docid列表 docids [] for page in range(pages): url f{category_url}page{page 1} resp requests.get(url, headersHEADERS, timeout10) # 页面里 docid 藏在链接中形如 https://news.163.com/25/0301/ 10/XXXX.html pattern rhttps://news\.163\.com/\d{2}/\d{4}/\d{2}/([A-Z0-9])\.html found re.findall(pattern, resp.text) docids.extend(list(set(found))) time.sleep(1) return list(set(docids)) def fetch_comments(docid, limit50): 根据docid获取评论区JSON api_url (https://comment.api.163.com/api/v1/products/a28696745777777 f/photos/{docid}/threads/{docid}/comments/newList?offset0limit{limit}) resp requests.get(api_url, headersHEADERS, timeout10) if resp.status_code ! 200: return [] data resp.json() comments [] # 结构为 { comments: { 评论id: { content: ..., score: 点赞数, ... } } } for cid, item in data.get(comments, {}).items(): comments.append({ comment_id: cid, docid: docid, content: item.get(content, ), like_count: item.get(score, 0), reply_count: item.get(replyCount, 0), create_time: item.get(createTime, ), user: item.get(user, {}).get(nickname, ) }) return comments两个细节值得敲黑板。Referer必须带上否则网易服务器会把它当跨域请求直接拒绝这是新手最容易撞的墙。time.sleep(1)虽然让爬虫慢了一点但能有效避免IP被临时限制。我实测过不加延时连续请求超过50个docid就会出现403加了延时跑300个docid都没问题。2.3 清洗环节容易被忽略的三个地方评论接口返回的是纯JSON文本但直接拿去做情感分析还是会翻车。我总结出三类必须处理的问题第一类是垃圾字符。评论里经常带HTML实体比如quot;表示引号、emoji、特殊符号。我用html.unescape()把HTML实体还原成普通字符再用正则过滤掉非中英文、数字、标点的字符。注意不要直接删掉标点因为感叹号和问号对情感判断有辅助作用。import html import re def clean_comment(text): # 还原HTML实体 text html.unescape(text) # 去掉用户和回复标记 text re.sub(r[\u4e00-\u9fa5\w], , text) text re.sub(r回复[\u4e00-\u9fa5\w], , text) # 只保留中文、英文、数字和基本标点 text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9。、,.!?], , text) return text.strip()第二类是空值和重复。很多评论只有表情符号清洗后变成空字符串要直接删除。还有大量内容完全相同的评论比如刷屏的支持如果不做去重情感统计会被这类重复数据带偏。我的做法是删除content完全重复的行保留点赞数最高的一条。第三类是时间字段。接口返回的createTime是Unix时间戳格式类似1739347200需要转成正常的日期格式。这一步很重要因为后面要按天画评论趋势图直接拿时间戳没法分组。3. 情感分析从分词到情绪量化的完整链路这一节是整个项目的灵魂。市面上讲情感分析的教程很多但大部分只给一个现成库就完事了真正落地时要解决的选型问题和调优问题没人讲。我把自己的完整思路写出来。3.1 选型对比SnowNLP、情感词典还是深度学习做之前我把主流方案拉了一张表比了一圈分别跑了一遍小样本测试SnowNLP自带中文情感分类模型pip install snownlp就能用它返回一个0到1之间的情感倾向值大于0.5偏正面。问题在于它的训练语料偏向电商购物评论对新闻评论里反话和网络梗的识别偏弱但胜在开箱即用。情感词典法维护一组正面词和负面词分词后对句子进行词频和权重的加减计算。这种方法完全可控可以自定义领域词但覆盖不了复杂句式比如虽然没有想象中好但是性价比不错这种转折句。深度学习如BERT微调精度最高但要自己标注大量样本还要准备GPU环境。对一个入门实战项目来说成本太高除非你已经有标注好的新闻评论数据集。我最后采用的是情感词典为主、SnowNLP为辅的融合方案用自定义情感词典处理确定性强的词汇用SnowNLP处理需要上下文语义的场景两者按各自的置信度进行加权融合。这样既保留词典的可解释性又借用了预训练模型的语义能力。3.2 中文分词与停用词处理情感词典方案的第一步是分词。我用的是jieba它是目前中文分词最成熟的库。分词前要加载自定义词典比如把性价比圈粉翻车这些新闻评论高频词加进去避免被切碎。import jieba import jieba.analyse # 加载自定义词典每行格式词语 词频 词性 jieba.load_userdict(news_words.txt) def segment(text): # 去掉单个字和纯数字 words jieba.lcut(text) words [w.strip() for w in words if len(w.strip()) 1 and not w.strip().isdigit()] return words停用词表不能省。新闻评论里的的、了、吗、啊、就、都这类词出现频率极高但它们对情感判断毫无帮助。我用的停用词表是在网上下载的基础停用词表基础上手动加了几十个新闻场景专用词比如觉得感觉真的这种没有情绪色彩的口语词。注意不要过度过滤像不是没有这类否定词如果进了停用词表会把情感逻辑搞乱——它们必须保留下来参与否定判断。3.3 融合打分规则词典权重加否定翻转情感词典打分是我的核心逻辑规则设计直接决定准确率。我用的情感词典来自公开的知网情感词典和网上的中文情感极性词典共收录正面词约8000个负面词约10000个。打分规则如下基础规则正面词记1负面词记-1中性词记0程度副词加权在情感词前面如果出现程度副词调整权重范围0.5到2倍否定词翻转情感词前面出现不、没、无、莫、非等词正负翻转感叹号和问号微调句子有连续感叹号时情感强度乘1.2实现时用了一个滑动窗口逻辑从左到右扫描分词结果检查每个情感词前面1到2个位置有没有程度副词或否定词。# 简化版融合打分核心逻辑 POS_WORDS set([赞, 好评, 厉害, 期待, 好看, 牛, 支持, 优秀]) NEG_WORDS set([差, 垃圾, 失望, 坑, 烂, 无语, 翻车, 抵制]) DEGREE_WORDS {很: 1.5, 太: 1.8, 非常: 1.8, 特别: 1.6, 有点: 0.7, 稍微: 0.6} NEGATION_WORDS set([不, 没, 没有, 无, 并不, 不太]) def dictionary_score(words): score 0.0 for idx, word in enumerate(words): base 0 if word in POS_WORDS: base 1 elif word in NEG_WORDS: base -1 if base 0: continue weight 1.0 # 检查前一个词是否为程度副词 if idx 0 and words[idx - 1] in DEGREE_WORDS: weight * DEGREE_WORDS[words[idx - 1]] # 检查前两个词是否是否定词 if idx 1 and words[idx - 2] in NEGATION_WORDS: base -base score base * weight return score融合方法是这样对每条评论分别得到dictionary_score加噪声后归一化到0到1之间和snownlp_score然后根据评论长度动态分配权重——短文本少于20个字信任词典多一些长文本超过50字信任SnowNLP多一些。原因很直白短评论里情感词密度高靠词典就能抓准长评论有转折和上下文词典容易算错账交给预训练模型更靠谱。3.4 验证分析效果的土办法模型跑完不能直接信尤其是自己拼出来的规则组合必须做验证。我的方法是抽样300条评论人工逐条标注正负中性然后和算法结果比对。# 最终情感判定得分大于0.6算正小于0.4算负中间算中性 def sentiment_label(score): if score 0.6: return positive elif score 0.4: return negative return neutral我实测下来的准确率在78%左右对一条纯规则模型来说还可以。最容易误判的是两类反讽句比如就这真是绝了词典模型完全无能为力谐音梗比如蚌埠住了绷不住了这类需要积累领域词典。我的对策是把识别出来的反讽句在数据里单独打标可视化时归为funny类而不是硬塞进正负。这个细节让最终展示结果更诚实——不会明明没分对却假装分对了。4. 可视化大屏连通Flask与ECharts的完整实现情感分析算完数据只是一张带标签的表格离舆论一目了然还差一个展示层。我选的技术栈是Flask加ECharts这也是目前做数据可视化项目最稳的组合。4.1 技术栈选型为什么用ECharts而不是PyechartsPyecharts虽然写起来少但它本质是ECharts的Python封装生成的图表和Flask模板页面做动态刷新时没那么灵活。我选择直接在HTML里引入ECharts的JavaScript库让Flask只负责提供JSON数据接口前端负责渲染。这样做的好处是前后端责任清晰后续想把前端换成Vue或React也只需保留数据接口。后端结构很简单一个Flask应用四个数据接口对应大屏上的四块内容。前端用AJAX定时按时拉接口实现大屏自动更新。4.2 后端数据接口设计我把所有情感分析结果存进MySQL表news_comment_sentiment字段包括id、docid、comment、sentiment_score、sentiment_label、created_date。后端只需要做聚合查询。from flask import Flask, jsonify import pymysql app Flask(__name__) def get_db(): return pymysql.connect( hostlocalhost, userroot, password123456, databasenews_analysis, charsetutf8mb4 ) app.route(/api/overview) def overview(): 返回总数统计正面数、负面数、中性数 conn get_db() cur conn.cursor() cur.execute( SELECT sentiment_label, COUNT(*) FROM news_comment_sentiment GROUP BY sentiment_label ) rows cur.fetchall() conn.close() data {positive: 0, negative: 0, neutral: 0} for label, count in rows: data[label] count return jsonify(data) app.route(/api/trend) def trend(): 返回按日期的评论量和正负面占比 conn get_db() cur conn.cursor() cur.execute( SELECT created_date, SUM(sentiment_labelpositive) AS pos, SUM(sentiment_labelnegative) AS neg, SUM(sentiment_labelneutral) AS neu, COUNT(*) AS total FROM news_comment_sentiment GROUP BY created_date ORDER BY created_date ) rows cur.fetchall() conn.close() dates [r[0].strftime(%m-%d) for r in rows] result {dates: dates, positive: [r[1] for r in rows], negative: [r[2] for r in rows], neutral: [r[3] for r in rows]} return jsonify(result)/api/words接口用来返回词云数据我从分词结果里按词频统计只保留出现次数超过5次且长度大于1的高频词。需要注意MySQL的utf8mb4字符集用utf8存中文评论会报Incorrect string value错误这是个典型的坑。4.3 前端大屏页面与ECharts配置前端页面我做成一个深色调大屏风格整体布局用CSS Grid分成两行三列核心图表的配置有几点值得说词云是最能体现舆论焦点的图。ECharts本身没有词云组件我引入了第三方扩展echarts-wordcloud。词云的颜色我按情感标签映射正面词用绿色系负面词用红色系中性词用灰色系。这样一眼就能看出舆论是扎堆夸还是扎堆骂。玫瑰饼图比普通饼图更有视觉冲击力它把半径和扇区面积都映射到数值上适合展示正负中性的占比差异。这里有个技巧为了让面积比例更准确要用roseType: area而不是默认的radius否则数值小的类别会显得更小产生误导。div idtrendChart stylewidth:100%;height:320px;/div script // 定时刷新趋势图 function loadTrend() { fetch(/api/trend) .then(res res.json()) .then(data { trendChart.setOption({ xAxis: { data: data.dates }, series: [ { name: 正面, type: line, data: data.positive, areaStyle: { opacity: 0.3 } }, { name: 负面, type: line, data: data.negative, areaStyle: { opacity: 0.3 } }, { name: 中性, type: line, data: data.neutral, areaStyle: { opacity: 0.1 } } ] }); }); } // 每30秒刷新一次 setInterval(loadTrend, 30000); /script自动刷新是可视化大屏的刚需我用的轮询方案简单粗暴但有效。30秒刷一次基本满足实时感又不会给后端太大压力。如果要更高的实时性可以考虑用WebSocket全双工推送但入门项目用轮询完全够。4.4 部署与调试中遇到的三个问题我把跑通后遇到的坑集中列出来这些坑不看文档很难自己排查第一是JSON中文返回乱码。Flask默认的JSON编码不会保留中文字符需要通过app.config[JSON_AS_ASCII] False设置或者在jsonify时指定。我曾被这个问题耗了一个小时浏览器里显示\u4e2d\u6587看着是乱码其实是正常的Unicode转义只是前端没解析好。第二是时区偏移。MySQL默认的datetime存的是服务器本地时间但接口返回的时间戳是UTC两者一混趋势图的时间轴就对不齐。我在建表时就把created_date规划成字符串类型格式YYYY-MM-DD从源头上避免时间类型混乱。数据进入表之前我在Python里用datetime.fromtimestamp(ts).strftime(%Y-%m-%d)完成转换。第三是ECharts图表不刷新的问题。轮询时如果不设置setOption的notMerge参数新旧数据叠加会导致残留折线。我踩了一次之后养成了习惯每次刷新前先调用chart.clear()再重新setOption一劳永逸。5. 跑通之后回顾整个项目的得与失项目从零到跑通一共花了三天其中爬虫半天、情感分析建模一天半、可视化一天。做完之后有几个复盘尤其值得说都是常规项目总结里不会写的内容。5.1 踩坑记录汇总环节典型问题根因解决办法爬虫请求返回403未带Referer头服务器判定跨域在Headers里补上来源页面地址爬虫部分docid拿不到评论新闻类型不同有的文章关闭了评论区请求前先检查接口返回的total字段是否为0清洗评论里有大量JSON接口返回HTML实体用html.unescape()统一还原情感分析不咋样被判成中性分词把不和咋样拆开否定词没识别往自定义词典加不咋样整体词条情感分析长评论里转折句误判词典模型只看局部词长文本融合SnowNLP权重动态调整可视化MySQL存中文报错表格字符集用了utf8建表改用utf8mb4可视化折线图越刷越乱旧数据和新区间叠加刷新前先chart.clear()这套记录里最让我印象深刻的是不咋样那个案例。它提醒我一个道理情感词典模型的本质是查词而中文的口语否定结构极其丰富想靠穷举规则吃透是不现实的。后来我还把还行吧一般般不至于这类口语词都加进了自定义词典配合程度副词逻辑才明显改善。5.2 这个项目还可以往哪些方向扩展做完这套之后我整理了一下扩展思路如果给你参考可以按这些方向继续往下做把情感分析模型换成预训练语言模型比如用bert-base-chinese做迁移学习把人工标注样本量提到1000条以上准确率有望从78%冲到88%以上。代价是需要训练时间但对研究型项目很值。标题里涉及的多模态情感分析可以这样切入新闻配图、标题文本、评论区文本一起分析图文情绪不一致时单独标记这种分析对舆情研判非常有价值。把一次性爬虫改成定时任务用APScheduler每小时增量抓取新评论配合消息队列实时更新Redis缓存就能把可视化大屏升级成真正意义上的实时舆论监测系统。我调研过评论区还可以做观点聚类比如把正面评论聚合起来看大家夸的具体是什么角度负面评论聚合起来看骂的集中点在哪里这比单纯的正负面比例更接近舆论分析的本质。5.3 我最终留下了一个怎样的优化思路这几天跑下来我自己最大的体会是这类项目真正的壁垒不在某一个环节而是在于把各个环节串起来的能力。爬虫爬得再快清洗不干净就是垃圾进垃圾出模型跑得再准展示端不给力就是锦衣夜行。如果你也想复现这个项目我建议先跑通最小的完整闭环——只爬50篇新闻的评论、用最简单的词典模型打个粗糙的情感标签、画一张静态比例饼图——然后再一步步替换升级每一个环节。别一上来就上BERT、上Redis、上大屏全屏动画那不是做项目那是给自己挖坑。我第一版就是一步到位思路结果前后端各写了一半数据没打通调试时根本不知道问题出在爬虫还是模型还是前端。后来痛定思痛按最小闭环再迭代的思路重来一遍三个小时就跑出了第一张图。最后再分享一个小经验这个项目的价值密度其实很高如果你正在找简历项目把网易新闻舆论情感分析可视化按我上面这套链路完整做一遍、记录好踩坑日志面试时从数据采集讲到模型融合再讲到可视化部署这一条链路讲下来比写五个CRUD项目都更能证明你的工程落地能力。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →