尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python网络舆情分析系统搭建:从爬虫采集到情感可视化的完整实践

Python网络舆情分析系统搭建:从爬虫采集到情感可视化的完整实践 简介这是一套基于Python与Django的网络舆情分析系统毕业设计项目完整包含项目源码、数据库脚本、毕业论文文档与答辩PPT。项目面向计算机专业毕业生、有Python基础的开发者以及需要快速搭建舆情分析演示系统的学习者帮助理解真实Web系统的分层结构。压缩包共290个文件大小约93.5MB其中42个Python源码文件承载后端业务逻辑34个JavaScript与多个HTML/CSS文件构建前端交互界面75个GIF动图用于操作过程演示另外还有SQL数据库脚本、Word论文、PowerPoint答辩稿以及配置文件与日志文件目录设计清晰便于直接部署和二次开发。目前已有458人学习下载。通过深入研究这套项目读者可以系统掌握Django框架下的MVC分层、ORM数据库映射、用户登录与权限控制、数据可视化等关键技能同时能够借鉴其中舆情数据展示、报表模块和后台管理界面的实现方式快速迁移到自己的课设或毕设中也可在此基础上进行功能扩展和重构。1. 从一条负面热搜到报表Python 网络舆情分析系统在解决什么舆情分析这事外行觉得是“抓数据”内行知道难的是“抓完怎么变成结论”。一句“某品牌手机发布后口碑逆转”背后可能是微博评论、新闻稿、论坛帖、短视频文案几万条文本人工看不完Excel 也拖不动。这套用 Python 搭起来的网络舆情分析系统核心链路并不复杂爬虫定时采集 → 清洗去重 → 情感打分 → 入库 → ECharts 可视化。真正决定系统能用不能用的是数据库脚本怎么设计、情感阈值怎么定、采集频率怎么控。这篇文章按从业者从头搭一遍的顺序来写覆盖源码里最容易被跳过但最要命的三件事数据模型、文本清洗、情感阈值。适合正在做毕业设计、课程设计或者公司内部想快速看到口碑风向的工程师。不是所有舆情都要上大数据平台一台 Linux 机器加 Python 3.8足够撑起日增万条数据的轻量分析。2. 先把数据模型定下来数据库脚本与项目目录怎么搭2.1 数据库脚本舆情表该有哪些字段拿到项目先别急着跑爬虫把数据库脚本打开看一眼。舆情分析系统最忌讳把原始文本和分析结果揉在一张表里常见做法是三张核心表news_article存原始采集数据news_sentiment存情感分析结果news_word_freq存热词统计。这样后期做趋势查询、增量更新、PPT 截图取数都不用大面积改代码。-- 舆情原始数据表 CREATE TABLE news_article ( id INT NOT NULL AUTO_INCREMENT, title VARCHAR(255) NOT NULL COMMENT 标题, content MEDIUMTEXT COMMENT 正文内容, source VARCHAR(64) DEFAULT unknown COMMENT 来源站点, url VARCHAR(500) DEFAULT COMMENT 原文链接, publish_time DATETIME DEFAULT NULL COMMENT 发布时间, crawl_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 抓取时间, md5_hash CHAR(32) NOT NULL COMMENT 去重指纹, PRIMARY KEY (id), UNIQUE KEY uk_md5 (md5_hash), KEY idx_publish_time (publish_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;md5_hash字段是整个去重机制的关键。爬虫每次抓取前对标题加正文拼接后取 MD5插库时靠唯一索引直接挡住重复数据比先 SELECT 再 INSERT 快得多也避免并发采集时写入重复。idx_publish_time索引是为后面按小时、按天聚合走势图准备的没有这个索引数据量过 10 万条后GROUP BY DATE(publish_time)会明显变慢。情感结果表建议做冗余设计把情感分数、情感类别、关键词快照都冗余进去查询时不用 JOIN 原表CREATE TABLE news_sentiment ( id INT NOT NULL AUTO_INCREMENT, article_id INT NOT NULL COMMENT 关联原文ID, sentiment_score DECIMAL(4,2) DEFAULT 0 COMMENT 情感得分 0~1, sentiment_label VARCHAR(16) DEFAULT 中性 COMMENT 积极/消极/中性, keywords VARCHAR(500) DEFAULT COMMENT 关键词逗号分隔, analyze_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_article_id (article_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段类型值得注意两点content用MEDIUMTEXT而不是TEXT因为新闻长文可能超过 64KB尤其网页正文扒下来带着多余空白字符时更容易超限DECIMAL(4,2)存情感得分能存 0.00 到 99.99对 0 到 1 之间的概率值绰绰有余浮点误差也小于 FLOAT。2.2 项目源码目录采集、清洗、分析、展示四层分离毕业设计或者小型团队项目里最常见的坏味道是把爬虫、清洗、分析全写在一个main.py里。后期想跑定时任务、换可视化方案牵一发动全身。我一般推荐的目录拆法是四层project/ ├── crawler/ │ ├── news_spider.py # 列表页抓取 │ └── detail_spider.py # 详情页正文抓取 ├── analysis/ │ ├── clean.py # 文本清洗去标签、去停用词 │ ├── sentiment.py # 情感分析入口 │ └── wordcloud.py # 热词统计 ├── db/ │ ├── init.sql # 建表脚本 │ └── mysql_client.py # pymysql 封装 ├── web/ │ ├── app.py # Flask 提供查询接口 │ └── templates/ └── main.py # 定时调度入口这样分层的好处是每一层都可以单独测试。比如clean.py写好后在命令行直接python -c from analysis.clean import clean_text; print(clean_text(p测试/p))就能验证不必把整个系统跑起来。db/mysql_client.py封装统一的get_connection()和execute()方法避免每个模块自己写一遍连接参数。2.3 环境与依赖python 3.8 起步的安装顺序先明确依赖清单再动手装环境。舆情系统不像 Web 框架那么吃版本但pymysql、requests、jieba、snownlp、pyecharts这几个包的组合在 Python 3.8 到 3.11 下都比较稳。如果你看到项目文档里写的是 Python 3.6也别急着降版本把snownlp升到最新版即可。pip install requests pymysql jieba snownlp pyecharts flask装包时最常见的报错是ModuleNotFoundError: No module named xxx这通常不是包没装上而是当前终端激活的 Python 环境和项目要求的不是同一个。用python --version和which python先确认环境路径。另一个高频坑是pyecharts渲染时提示缺echarts静态文件这是因为版本从 1.x 升级后不再内置静态资源改用在线 CDN服务器没有外网时需要在pyecharts配置里设置本地资源路径这个问题留到第五章展开。提示requirements.txt里如果固定了pyecharts1.9.1别轻易升 2.x接口变动很大旧代码里的Page()和add参数大部分要重写。3. 用 Python 爬虫把舆情文本抓进来请求头、频率与去重3.1 一个最小可用的采集脚本requests 新闻列表页舆情采集不需要花哨的框架requests加BeautifulSoup是性价比最高的组合。抓取逻辑分两步先解析列表页拿到详情页 URL再逐个抓详情页正文。这里给一个列表页最小脚本import requests from bs4 import BeautifulSoup 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 } def parse_list_page(url): resp requests.get(url, headersHEADERS, timeout10) resp.encoding utf-8 # 部分站点是 gbk需要按实际调整 soup BeautifulSoup(resp.text, html.parser) news_links [] # 根据目标站点的实际结构选择可以按 class 精确匹配也可以退而求其次 for a in soup.select(a[href*/news/]): href a.get(href) if href and href.startswith(http): news_links.append(href) return list(set(news_links)) # 列表页内去重 if __name__ __main__: links parse_list_page(https://example.com/news) print(f发现 {len(links)} 条待抓链接)resp.encoding utf-8这行容易漏。requests会优先用响应头里的charset判断编码但不少中文站点响应头没写或写错导致resp.text乱码。更稳妥的做法是resp.apparent_encoding自动探测不过会损失一点性能数据量大时建议手动指定。选择器a[href*/news/]是故意放宽的新闻站点列表页的链接通常都带/news/特征这样写比死磕精确 CSS 类名抗页面改版。3.2 反爬和频率控制time.sleep 与 User-Agent 池舆情采集对实时性要求不算苛刻慢一点没关系但被封了整条采集链路就断了。控制采集频率是刚需常见做法是每次请求之间随机休眠 1 到 3 秒同时维护一个 User-Agent 池轮换。import random import time from itertools import cycle UA_POOL [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Firefox/121.0, Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) Safari/605.1.15, ] def safe_request(url, retry3): ua_cycle cycle(UA_POOL) for attempt in range(retry): try: resp requests.get( url, headers{User-Agent: next(ua_cycle)}, timeout15 ) if resp.status_code 200: return resp elif resp.status_code in (403, 429): time.sleep(10 * (attempt 1)) # 被限流则指数退避 except requests.RequestException as exc: print(f第 {attempt 1} 次请求失败: {exc}) time.sleep(5) return Nonesafe_request把请求封装成可重试的单元业务代码只管调用。这里的关键参数是403/429状态码后的等待时间第一次失败等 10 秒第二次 20 秒第三次 30 秒给站点恢复时间。cycle(UA_POOL)是 Python 内置的工具无限循环轮换 UA比随机取更均匀避免短时间窗口内大量命中同一个 UA。3.3 对比 Selenium轻量采集为何够用维度requests BeautifulSoupSelenium执行速度快每秒可发多个请求慢每个页面要等浏览器渲染反爬能力依赖请求头和频率控制能过部分 JS 渲染校验资源占用轻单进程可跑几千个 URL重每个实例吃数百 MB 内存适用场景静态列表页、详情页动态加载、需点击翻页的页面舆情系统中 90% 的新浪、网易、搜狐类新闻页都是服务端渲染详情页正文在 HTML 里直接存在用requests就能解析。只有当目标站点是Ajax动态加载内容、翻页和点击都靠 JS 驱动时才需要上 Selenium。我一般会先写个小脚本测一个列表页能否在resp.text中搜到正文关键词搜不到再升级方案。4. 文本预处理到情感判读jieba 分词与 SnowNLP 的配合4.1 清洗环节去 HTML 标签、去停用词、去重复爬下来的正文里全是p、script、转义符和空白字符直接丢给情感分析模型会严重影响准确率。清洗是舆情分析里投入产出比最高的一步做得好后面情感判断准确率能提升 10 个百分点以上。import re import jieba def clean_text(raw_html): # 去掉 script 和 style 包裹的整块内容 text re.sub(r(?s)(script|style).*?/\1, , raw_html) # 去掉所有 HTML 标签 text re.sub(r[^], , text) # 反转义nbsp; amp; quot; 等 text text.replace(nbsp;, ).replace(amp;, ).replace(quot;, \) # 压缩连续空白为单个空格 text re.sub(r\s, , text).strip() return text def cut_words(text, stopwords_filestopwords.txt): words jieba.lcut(text) stopwords set() with open(stopwords_file, encodingutf-8) as fp: for line in fp: stopwords.add(line.strip()) # “的、了、在、是”等无意义词以及单个字符全部过滤 return [w for w in words if w not in stopwords and len(w) 1]正则里(?s)让.能匹配换行否则script里的跨行内容会残留下来。反转义的顺序也有讲究先处理nbsp;再压缩空白避免转换后产生多余的连续空格。jieba.lcut默认全模式比jieba.cut返回更全的候选词但噪声也大舆情场景下更推荐lcut配合停用词表做过滤。停用词表可以从 GitHub 上找公开的“中文停用词表”通常一两千行覆盖量级足够。4.2 情感得分与阈值SnowNLP 输出的 0.5 不等于中性SnowNLP 的情感分析返回的是一个 0 到 1 之间的概率值越接近 1 越积极越接近 0 越消极。但新手最容易踩的坑是拿 0.5 当分界线得分 0.49 算消极0.51 算积极。实际使用时0.4 到 0.6 这个区间几乎都是含糊表达应该归为中性。阈值设计直接影响最后报告的口径。from snownlp import SnowNLP def analyze_sentiment(text): s SnowNLP(text) score s.sentiments # float 0~1 if score 0.6: label 积极 elif score 0.4: label 消极 else: label 中性 # 保留两位小数配合数据库 DECIMAL(4,2) return round(score, 2), label阈值 0.6/0.4 不是拍脑袋。SnowNLP 的训练语料偏电商评论对新闻文本先天不敏感很多客观陈述会被打成消极。把中性区拉宽到 0.4 到 0.6能明显减少“误报消极”的情况。想要更精细可以自己标注几百条数据后用SnowNLP的Bayes类重新训练但成本偏高项目源码里给到的阈值如果不带验证数据建议先用默认。常见误用是把SnowNLP(text).sentiments直接存库不做阈值映射。这样出的报表很难讲0.55 算好还是坏做 PPT 时没有故事线。更好的实践是库里同时存sentiment_score和sentiment_label报表直接按 label 分组计数。4.3 热词与聚类TF-IDF 提取关键词让舆情焦点可见情感只能看出“是好事还是坏事”看不出“大家在讨论什么”。这一步用 TF-IDF 提取每篇文章的关键词按词频汇总能做词云、出热词排行也是最后 PPT 里最能体现工作量的图表。from jieba.analyse import extract_tags def extract_keywords(text, top_k10): # allowPOS 限定名词、动词、形容词过滤掉虚词 keywords extract_tags( text, topKtop_k, withWeightTrue, allowPOS(n, ns, nt, vn, v, a) ) return [(word, round(weight, 4)) for word, weight in keywords] # 示例对清洗后的文本提取 for keyword, weight in extract_keywords(clean_text(article)): print(f{keyword}: {weight})extract_tags本质是 TF-IDF 的 jieba 实现allowPOS参数把词性限定在名词、地名、机构名、动词和形容词上避免返回“我们”“这个”之类的高频代词。withWeightTrue返回权重做词云时直接把权重映射到字号比单纯按出现次数排序更能反映主题集中度。5. 让结果讲人话pyecharts 可视化与数据库脚本回写5.1 情感趋势图pyecharts 的最小可用代码舆情系统最后出口是图和报表。用 pyecharts 画时间维度上的情感趋势折线图是所有可视化里信息密度最高的。下面这段代码直接读 MySQL 数据并按天聚合from pyecharts.charts import Line from pyecharts import options as opts import pymysql def load_trend_data(days7): conn pymysql.connect(hostlocalhost, userroot, password123456, databaseopinion, charsetutf8mb4) cursor conn.cursor() sql SELECT DATE(a.publish_time) AS d, SUM(s.sentiment_label 积极) AS pos, SUM(s.sentiment_label 消极) AS neg FROM news_article a JOIN news_sentiment s ON a.id s.article_id WHERE a.publish_time DATE_SUB(NOW(), INTERVAL %s DAY) GROUP BY DATE(a.publish_time) ORDER BY d cursor.execute(sql, (days,)) rows cursor.fetchall() cursor.close() conn.close() return rows rows load_trend_data(7) line ( Line() .add_xaxis([str(r[0]) for r in rows]) .add_yaxis(积极, [r[1] for r in rows]) .add_yaxis(消极, [r[2] for r in rows]) .set_global_opts(title_optsopts.TitleOpts(title近 7 日舆情情感走势)) ) line.render(trend.html)SUM(s.sentiment_label 积极)是 MySQL 的表达式用法布尔条件成立返回 1不成立返回 0直接按天统计出积极和消极数量比先取数再在 Python 里组长更省事。DATE_SUB(NOW(), INTERVAL %s DAY)参数化动态日期配合%s占位符传递 days防止拼接参数时引发 SQL 注入风险。render(trend.html)会生成一个独立 HTML 文件内含全部渲染逻辑直接浏览器打开即可也能嵌入 PPT 演示前的本地展示。5.2 全流程跑通的冷启动顺序从建库到出图拿到一个项目源码包最怕读了一堆代码不知道从哪下手。舆情系统的冷启动顺序是固定的执行db/init.sql建库建表。先确认 MySQL 已启动并且charsetutf8mb4未报错。修改db/mysql_client.py里的连接参数账号、密码、端口逐一核对。单跑爬虫模块抓 50 条数据入库SELECT COUNT(*) FROM news_article;验证写入成功。单跑分析模块对刚才入库的文本做清洗和情感分析回写news_sentiment表。最后启动 Flask 服务或直接运行可视化脚本出 HTML 报告。最后一步的常见坑pyecharts生成的 HTML 图形不显示。检查打开的文件路径是否含中文或空格浏览器对本地文件路径的处理容易出问题换成英文路径基本能解决。另外如果你的部署机器没有外网需要在图表生成那里加一行.add_js_libs([])或在set_global_opts里指定本地 echarts 资源路径否则图表控件加载不出来。把news_sentiment表的sentiment_label字段按天分组算出积极占比再拉出消极占比最高的前 10 条原文章标题这两张图配上一段原因分析文字就是完整的一份舆情周报核心页。论文的 LW 部分把这三个步骤展开写清楚数据建模、文本清洗与情感阈值设计、可视化验证PPT 则按“背景 → 架构 → 关键技术 → 成果展示 → 总结”五页组织这套系统的可交付材料就齐了。本文还有配套的精品资源点击获取
返回列表