简介:这是一套基于Python技术栈的Web网络舆情分析系统完整项目资料,面向具备一定编程基础、希望深入理解后端开发与数据库集成的开发者,以及需要实战参考的大学生和毕设人群。资源包共290个文件,涵盖42个py源码、34个js脚本、15个css样式、12个html页面及35个pyc编译文件,另含sql数据库脚本、docx说明文档、pptx演示文稿与csv数据文件,压缩包约93.5MB,目录结构清晰,便于按模块检索学习。项目采用主流Python框架与前端layui组件库,完整呈现了从登录界面、后台管理到数据可视化的实现路径。已有462人学习下载,读者可通过研读源码掌握后端开发关键技能、数据库集成方法及Web应用架构设计思路,并借助配套文档与演示材料快速理解系统整体设计,为后续开发实践与技术交流提供扎实参考。
1. 从零搭一套 Python 网络舆情分析系统:为什么“能跑起来”和“能交付”是两回事
很多同学第一次接触网络舆情分析系统,都是被“项目源码+数据库脚本+文档+LW+PPT”这种打包标题吸引进来的。真正动手才发现,代码能跑只是起点,能不能把数据采下来、把情感判对、把结果落进数据库、最后还能写出一份像样的文档,才是分水岭。我见过太多人卡在“爬虫被封”“中文分词装不上”“数据库脚本导入报错”这三道坎上,最后项目烂尾。这套系统的核心链路其实很清晰:数据采集 → 清洗去重 → 中文分词与情感倾向判断 → 关键词/热词统计 → 可视化与报告输出。它适合两类人:一是需要交课程设计或毕业设计的同学,二是想快速搭一个舆情监控原型的开发者。下面我按自己实际搭过一遍的顺序,把每一步的参数、坑和验证方法讲清楚,你照着复现,至少能少熬三个通宵。
2. 数据采集层:用 requests + 正则把“能采到”变成“采得稳”
2.1 为什么我不推荐一上来就上 Scrapy
新手最容易犯的错,是听说 Scrapy 是“专业爬虫框架”,直接开搞。结果光是理解Item Pipeline、Middleware和settings.py的优先级就耗掉两天,最后连一条数据都没入库。对于舆情分析这种“目标站点固定、字段不多、量级在万级以内”的场景,requests + BeautifulSoup或者requests + 正则完全够用,调试成本低一个数量级。我一般会先写一个最小采集脚本,确认能稳定拿到标题、正文、发布时间、来源四个字段,再考虑要不要换框架。常见做法是:把采集逻辑封装成一个fetch_page(url)函数,外面套一层重试和随机延时,这样后面换站点只需要改解析规则。
import requests import time import random 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 Safari/537.36" } def fetch_page(url, retry=3): """带重试和随机延时的页面抓取,返回 HTML 文本或 None""" for i in range(retry): try: # 随机延时 1~3 秒,降低被限流的概率 time.sleep(random.uniform(1, 3)) resp = requests.get(url, headers=HEADERS, timeout=10) if resp.status_code == 200: resp.encoding = resp.apparent_encoding # 自动纠正中文编码 return resp.text except requests.RequestException as e: print(f"第 {i+1} 次请求失败: {e}") return None def parse_list(html): """从列表页提取详情页链接,返回 URL 列表""" soup = BeautifulSoup(html, "html.parser") links = [] for a in soup.select("a.title-link"): # 选择器按目标站点实际结构调整 href = a.get("href") if href and href.startswith("http"): links.append(href) return links这段代码里最关键的三个参数是timeout、retry和随机延时区间。timeout=10是防止某个请求卡死拖垮整个脚本;retry=3配合try/except能扛住偶发的网络抖动;随机延时 1~3 秒是血泪经验——固定间隔的请求特征太明显,很容易触发站点的频率限制。resp.apparent_encoding这一行也别省,很多中文站点不声明 charset,不纠正就是一堆乱码。解析函数里的 CSS 选择器需要你按目标页面实际 DOM 结构改,改完先用print(len(links))验证数量对不对,再往下走。
2.2 正文提取与去重:别让重复数据污染情感统计
采到链接之后,下一步是进详情页拿正文。这里有个容易被忽略的问题:同一篇新闻可能被多个来源转载,如果不去重,后面做关键词统计时“某热词”的频次会被严重放大,情感倾向也会被重复样本带偏。我一般用“标题 MD5 + 正文前 100 字 MD5”做联合去重,存进数据库时加唯一索引。正文提取优先用BeautifulSoup找<article>或class含content的容器,找不到再退回到正则去标签。
import hashlib import re def extract_article(html): """提取标题和正文,返回 (title, content)""" soup = BeautifulSoup(html, "html.parser") title_tag = soup.find("h1") or soup.find("title") title = title_tag.get_text(strip=True) if title_tag else "" # 优先找语义化容器,找不到再取所有 <p> 拼接 container = soup.find("article") or soup.find("div", class_=re.compile("content|article")) if container: content = container.get_text(separator="\n", strip=True) else: content = "\n".join(p.get_text(strip=True) for p in soup.find_all("p")) return title, content def make_fingerprint(title, content): """生成去重指纹:标题 + 正文前100字""" raw = (title + content[:100]).encode("utf-8") return hashlib.md5(raw).hexdigest()extract_article里的separator="\n"是为了保留段落边界,后面分词时按行处理更干净。make_fingerprint用 MD5 而不是直接存长文本,是为了在数据库里做唯一索引时更省空间。实际入库时,INSERT IGNORE或ON DUPLICATE KEY UPDATE配合这个指纹字段,就能自动跳过重复记录。这一步做完,建议先手动查一下数据库里COUNT(*)和COUNT(DISTINCT fingerprint)的差值,如果差值超过 10%,说明你的采集源里有大量转载,需要调整采集策略。
3. 中文处理与情感分析:jieba + SnowNLP 的组合拳怎么打才准
3.1 分词与停用词:为什么你的关键词统计总是不准
中文舆情分析绕不开分词。jieba是目前最稳的选择,但默认词典对网络新词和领域术语覆盖不够,直接分词会把“绝绝子”“破防了”切成碎片。我一般会做两件事:一是加载自定义词典,把近期热词和业务术语加进去;二是用一份停用词表过滤掉“的、了、是、在”这类无意义词。停用词表不用自己造,网上有现成的中文停用词库,但要注意里面可能包含“不”“没”这种情感否定词,过滤掉会直接影响情感判断,我一般会把否定词从停用词表里手动剔除。
import jieba import jieba.analyse # 加载自定义词典,每行格式:词语 词频 词性(词频和词性可省略) jieba.load_userdict("user_dict.txt") # 停用词表,注意剔除否定词 STOPWORDS = set() with open("stopwords.txt", encoding="utf-8") as f: for line in f: w = line.strip() if w and w not in {"不", "没", "无", "非", "莫", "勿"}: STOPWORDS.add(w) def cut_words(text): """分词并过滤停用词,返回词列表""" words = jieba.lcut(text) return [w for w in words if w not in STOPWORDS and len(w) > 1] def extract_keywords(text, topk=20): """基于 TF-IDF 提取关键词""" return jieba.analyse.extract_tags(text, topK=topk, withWeight=True)load_userdict要在所有分词操作之前调用,否则不生效。cut_words里len(w) > 1是过滤单字,因为单字在舆情统计里噪声太大。extract_tags返回的是(词语, 权重)列表,权重可以直接用来做词云的大小映射。这里有个参数要注意:topK设 20 还是 50,取决于你的文本量。文本短的时候设太大,会把低频噪声词也提出来;文本长的时候设太小,会漏掉关键信号。我一般先用 30 跑一遍,看结果里有没有明显不相关的词,再微调。
3.2 SnowNLP 情感打分:0.5 不是分界线,别被默认阈值坑了
SnowNLP是入门级中文情感分析库,s.sentiments返回 0~1 的分数,越接近 1 越正面。但很多人直接拿 0.5 当正负分界,结果发现大量中性文本被误判。实际用下来,0.5 附近(0.4~0.6)的文本情感倾向非常模糊,我一般会把这段区间单独标为“中性”,只对小于 0.4 和大于 0.6 的做明确正负判断。另外,SnowNLP对反讽和双重否定几乎无能为力,所以它适合做趋势统计,不适合做单条精准判断。
from snownlp import SnowNLP def sentiment_score(text): """返回情感分数和标签,中性区间单独处理""" if not text.strip(): return 0.5, "中性" score = SnowNLP(text).sentiments if score > 0.6: label = "正面" elif score < 0.4: label = "负面" else: label = "中性" return round(score, 4), label # 批量处理示例 texts = ["这个功能太好用了", "体验极差,再也不用了", "今天天气不错"] for t in texts: print(t, sentiment_score(t))sentiment_score里先做空文本判断,是因为SnowNLP("")会抛异常。返回的分数保留四位小数,方便后面做排序和分布统计。批量处理时建议加一个进度打印,因为SnowNLP初始化模型有开销,几千条文本跑下来需要几分钟。如果你发现中性比例超过 60%,说明你的文本本身情感倾向就不强,这时候应该把分析重点从“正负比例”转到“关键词热度变化”上,而不是硬调阈值。
4. 数据库与可视化:把分析结果落成能看的图表
4.1 数据库脚本导入:三个最常见的报错和修法
拿到数据库脚本后,导入报错是高频翻车点。我整理了三类最常见的情况:第一,字符集不匹配,脚本里是utf8mb4,但本地数据库默认latin1,导入后中文全是问号;第二,外键约束顺序不对,先插了子表再插主表,直接报foreign key constraint fails;第三,AUTO_INCREMENT起始值和已有数据冲突,导致主键重复。修法分别是:建库时显式指定CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;导入前先SET FOREIGN_KEY_CHECKS=0,导完再打开;导入前清空目标表或调整起始值。
-- 建库时指定字符集,避免中文乱码 CREATE DATABASE IF NOT EXISTS opinion_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE opinion_db; -- 导入前临时关闭外键检查,避免顺序问题 SET FOREIGN_KEY_CHECKS = 0; -- 示例:舆情记录表 CREATE TABLE IF NOT EXISTS opinion_record ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, title VARCHAR(255) NOT NULL, content TEXT, source VARCHAR(100), publish_time DATETIME, sentiment_score DECIMAL(5,4), sentiment_label VARCHAR(10), fingerprint CHAR(32) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_fingerprint (fingerprint), KEY idx_publish_time (publish_time), KEY idx_sentiment (sentiment_label) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; SET FOREIGN_KEY_CHECKS = 1;fingerprint字段加唯一索引,配合前面采集层的去重逻辑,能从数据库层面兜住重复数据。idx_publish_time和idx_sentiment两个索引是为了后面按时间范围和情感标签做聚合查询时不用全表扫描。导入脚本时如果数据量大,建议用source命令而不是复制粘贴,并且把autocommit关掉,每 1000 条提交一次,速度会快很多。
4.2 用 Pyecharts 出图:词云和趋势图的最小可用配置
可视化部分我用Pyecharts,因为它生成的 HTML 可以直接嵌到文档里,不需要额外起服务。词云用WordCloud,趋势图用Line,情感分布用Pie。关键参数是词云的word_gap和word_size_range,设太小词会挤在一起,设太大又显得稀疏。趋势图的xaxis时间格式要和数据库里的publish_time对齐,否则会出现时间轴错乱。
from pyecharts.charts import WordCloud, Line, Pie from pyecharts import options as opts def build_wordcloud(keyword_weights, output="wordcloud.html"): """keyword_weights: [(词语, 权重), ...]""" wc = ( WordCloud() .add("", keyword_weights, word_gap=8, word_size_range=[12, 60]) .set_global_opts(title_opts=opts.TitleOpts(title="舆情关键词词云")) ) wc.render(output) def build_trend(date_counts, output="trend.html"): """date_counts: [(日期, 数量), ...]""" line = ( Line() .add_xaxis([d for d, _ in date_counts]) .add_yaxis("舆情数量", [c for _, c in date_counts], is_smooth=True) .set_global_opts( title_opts=opts.TitleOpts(title="舆情趋势"), xaxis_opts=opts.AxisOpts(name="日期"), yaxis_opts=opts.AxisOpts(name="数量") ) ) line.render(output)word_size_range=[12, 60]是我试过比较平衡的区间,词太多时可以降到[10, 45]。is_smooth=True让趋势线更平滑,但如果你要强调每日精确值,可以设成False。生成的 HTML 文件默认在脚本同级目录,打开就能看。如果词云里出现大量无意义词,回到 3.1 节检查停用词表和自定义词典,大概率是那里漏了。
5. 避坑与排查:那些文档里不会写的翻车现场
5.1 采集被封:现象、原因与解决
现象是脚本跑了几百条之后突然全部返回 403 或 429。原因通常是请求频率过高,或者User-Agent固定不变被识别。解决方法是把随机延时区间拉大到 2~5 秒,并且准备多个User-Agent轮换使用。如果目标站点有robots.txt限制,就换一个允许采集的源,不要硬刚。
5.2 分词结果全是单字:现象、原因与解决
现象是jieba.lcut出来的结果里大量单字,关键词提取全是“的”“了”。原因是自定义词典没加载成功,或者词典文件编码不是 UTF-8。解决方法是确认load_userdict的路径正确,并且词典文件用 UTF-8 无 BOM 格式保存。可以在加载后打印jieba.lcut("测试词语")验证是否生效。
5.3 情感分数全在 0.5 附近:现象、原因与解决
现象是几千条文本的sentiments分数集中在 0.45~0.55。原因是SnowNLP的默认模型在通用语料上训练,对特定领域文本区分度低。解决方法是把中性区间放宽到 0.35~0.65,或者换用带领域词典的模型。如果只是做趋势展示,中性比例高并不影响结论,不必强行调阈值。
5.4 数据库导入后中文乱码:现象、原因与解决
现象是SELECT出来全是问号。原因是建库或连接时字符集不是utf8mb4。解决方法是检查CREATE DATABASE语句、连接字符串里的charset参数、以及表的DEFAULT CHARSET三处是否一致。任何一处是latin1或utf8(非 mb4),都可能出问题。
5.5 可视化 HTML 打开空白:现象、原因与解决
现象是render生成的 HTML 用浏览器打开一片空白。原因是Pyecharts依赖的 JS 资源默认从 CDN 加载,网络不通时渲染失败。解决方法是把Pyecharts的online资源换成本地资源,或者直接用snapshot模式输出图片。我一般会在文档里同时保留 HTML 和 PNG 两种格式,防止演示时翻车。
6. 进阶技巧:把“一次性脚本”变成“可复用的分析流水线”
前面五章走完,你已经能跑通一条完整链路了。但如果你想让这套东西真正可复用,而不是每次换数据都手动改代码,我建议做一件事:把采集、清洗、分析、入库、出图五个步骤拆成独立的函数模块,用一个main.py按顺序调用,中间结果用 JSON 或 CSV 落盘。这样任何一步出问题,你都能从中间结果重新跑,不用从头再来。我自己的习惯是给每个模块加一个if __name__ == "__main__":的测试入口,单独调试时直接跑那个文件,不用动主流程。
另一个值得投入的点是日志。不要用print,用logging把每一步的耗时、处理条数、异常信息记到文件里。舆情分析经常需要回溯“为什么某天的数据少了”,有日志和没日志的排查效率差十倍。下面是一个最小可用的日志配置:
import logging logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", handlers=[ logging.FileHandler("pipeline.log", encoding="utf-8"), logging.StreamHandler() ] ) logger = logging.getLogger(__name__) logger.info("采集开始,目标页数:%d", page_count)FileHandler的encoding="utf-8"必须加,否则中文日志会乱码。StreamHandler让你在终端也能实时看到进度。日志级别用INFO就够,调试时临时改成DEBUG,上线后改回INFO,避免日志文件膨胀太快。
最后说一个验证方法:拿一批你人工标注过情感倾向的文本(哪怕只有 50 条),跑一遍你的sentiment_score,算一下和人工标注的一致率。如果一致率低于 70%,说明你的阈值或模型需要调整;如果高于 85%,说明这套流程在你的数据上已经可用了。这个验证步骤花不了半小时,但能让你在写文档和做演示时心里有底。我自己踩过最大的坑,就是跳过验证直接写结论,结果答辩时被问“你这个准确率怎么来的”,当场卡壳。希望帮到你。
本文还有配套的精品资源,点击获取