ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于Python+SVM的微博舆情分析系统:爬虫、模型与Web全链路解析

基于Python+SVM的微博舆情分析系统:爬虫、模型与Web全链路解析

简介:这是一套基于Python与支持向量机(SVM)的完整舆情分析系统,面向完成毕设、课程设计或工程实训的初阶与进阶学习者。系统含三大核心模块:Scrapy框架微博爬虫负责数据采集,SVM模块完成情感分类,Springboot项目实现Web端可视化展示,另附MySQL表结构、测试数据与Flume启动命令,形成从数据抓取到模型训练再到页面呈现的闭环流程。资源共717个文件,以Java源码、Python脚本、HTML页面、JS与CSS前端资源、SQL脚本为主,配合表情图与截图用于界面演示,压缩包约15.13MB,目录清晰便于按模块检索。已有166人浏览学习,适合快速搭建舆情分析原型并理解爬虫、机器学习与Web系统集成思路的读者参考。

1. 基于 Python + SVM 的舆情分析系统:三个模块拼出一条完整数据链路

一批微博评论扔到你面前,几百上千条,靠人肉一条条看极性,既慢又不稳定。这个基于 Python + SVM 的舆情分析系统,做的就是「爬取 → 清洗 → 情感分类 → Web 展示」的闭环:Scrapy 负责从新浪微博抓文本,SVM 模型负责把文本判成正面或负面,SpringBoot 负责把结果用页面展示出来,中间用 MySQL 存数据、用 Flume 做日志采集衔接。它适合两类人:一类是拿它当毕设、课程设计、大作业的模板,想看看爬虫、机器学习、Web 三个东西怎么拼在一起;另一类是已经写过单点脚本,想找一个完整工程结构做参照的进阶学习者。下面按目录结构、爬虫、SVM 训练、联调避坑的顺序,把这套资源拆开讲。

2. 系统架构与数据链路:scrapy、SVM、SpringBoot 三方怎么咬合

拿到资源包先别急着跑,把文件夹结构看懂比什么都重要。整个资源包含三个工程和一个 txt:analysis(SpringBoot 项目)、scrapy(Scrapy 爬虫)、svm(机器学习项目)、flume.txt(Flume 启动命令),外加mysql数据库表结构。这套划分对应了三条不同技术栈,每一条都有独立的开发工具要求:Python 侧建议用 PyCharm Community Edition,Java 侧用 IntelliJ IDEA,两边的工程可以分开调试,最后靠数据库和文件系统汇合。

2.1 目录结构与技术栈划分:每个文件夹到底管什么

scrapy文件夹是入口——数据从这来。它基于 Scrapy 框架抓取新浪微博的公开页面文本,产出的是结构化的微博内容,落库到 MySQL。svm文件夹是核心——它读取svm\data\negative.xlsx和svm\data\positive.xlsx两个标注好的 Excel 数据集,用支持向量机训练一个二分类模型,训练完成后再对爬虫抓来的新文本做情感极性预测,结果写回数据库。analysis文件夹是出口——SpringBoot 读库、生成报表页面,把「每一条微博是正面还是负面、整体趋势如何」用可视化方式呈现出来。

mysql文件夹存的是建表语句加测试数据,登录账号密码固定为root / root,这一点在本地联调时直接用,不用额外改。flume.txt里存的是 Flume 的启动命令,常见做法是把它作为一个采集 Agent 的启动入口,把爬虫运行时产生的日志或数据流转信息接入后续处理通道。整条链路里,Flume 属于数据搬运的胶水层,不是必改项,但联调时要确认它起的 Agent 端口没有被占用。

2.2 数据流闭环:从微博页面到 Web 页面的完整走向

我习惯用一条数据链路来描述这套系统:Scrapy 爬虫 → MySQL 存储 → SVM 训练/预测 → 结果回写 → SpringBoot 查询展示。爬虫把微博文本、发布时间、作者等信息写入 MySQL 的业务表;SVM 工程先从negative.xlsx和positive.xlsx读入标注数据训练模型,训练完用joblib或pickle把模型落盘,然后对爬虫新抓的文本逐条预测,把预测标签和置信度回写到另一张结果表;SpringBoot 通过 MyBatis 或 JPA 查这两张表,生成按时间、按情感极性聚合的报表视图。

这个顺序不能乱。如果你先把 SpringBoot 跑起来再启动爬虫,页面就是空的,因为库里还没有数据;如果你先训练 SVM 再爬数据,模型只能拿历史测试集验证,线上文本一条都预测不了。正确启动顺序是:先建库导数据 → 跑爬虫抓一批文本 → 训练 SVM 并导出模型 → 对结果表跑预测 → 最后启动 SpringBoot。Topic.class、KeywordReportView.class这些编译产物其实就对应了 SpringBoot 侧的主题聚合和关键词报表逻辑,它们依赖的数据库表必须在启动前就存在。

提示:scrapy.cfg是 Scrapy 项目的总配置文件,里面约定了项目 module 路径;如果你把scrapy文件夹改过名,必须同步改这个文件里的default配置,否则scrapy crawl会直接报找不到爬虫模块。

3. Scrapy 爬虫层:微博数据采集与字段清洗

Scrapy 抓微博的核心难点不在框架本身,而在页面结构和反爬。这个资源里的scrapy文件夹是完整工程,包含items.py、pipelines.py、settings.py和具体的 spider 文件,用户只需要把 cookie 和请求头换成自己的就能跑。

3.1 爬虫项目初始化与微博页面解析

创建 Scrapy 项目的标准命令是scrapy startproject weibo_spider,但资源包里已经给好了现成结构,拿到手后第一件事是确认scrapy.cfg里的url和project字段正确。微博的移动端或桌面端页面结构不同,这套资源默认解析的是公开页面里的微博正文节点,常见的做法是用 XPath 或 CSS 选择器提取div下的文本节点,再配合extract()拿到列表。

下面是一个典型的 Scrapy spider 骨架,抓取微博搜索页或用户页的文本内容:

import scrapy from scrapy.http import Request from weibo_spider.items import WeiboItem class WeiboSpider(scrapy.Spider): name = "weibo" allowed_domains = ["weibo.com"] start_urls = ["https://weibo.com/u/xxxxx"] def start_requests(self): cookies = { "SUB": "粘贴你自己的微博登录Cookie" } for url in self.start_urls: yield Request(url, cookies=cookies, callback=self.parse) def parse(self, response): # 用XPath提取微博正文,微博的文本常常包裹在多个div里 text_nodes = response.xpath("//div[contains(@class, 'WB_text')]/text()").extract() for node in text_nodes: content = node.strip() if len(content) < 2: continue item = WeiboItem() item["content"] = content item["source"] = response.url yield item

这段代码里,cookies是反爬的关键——不携带登录态的情况下,微博会频繁要求验证或跳转登录页,导致抓到的文本全是空。contains(@class, 'WB_text')是匹配微博正文容器的常用 class 片段,不同时期的微博页面布局会改,如果抓回来是空列表,优先检查这个 XPath 是否命中。len(content) < 2的过滤是为了去掉表情符占位和纯链接等无效行,这类噪声文本会严重影响后续 SVM 的情感判断。

3.2 数据落库:Pipeline 与 MySQL 写入

spider 产出的item会流向pipelines.py,这里负责清洗和入库。清洗阶段常见的操作是去掉 HTML 标签、把全角空格转半角、过滤转发标记(如「转发微博」「//」等)。清洗完通过pymysql写入 MySQL,连接参数在settings.py里配置。

import pymysql class WeiboPipeline(object): def open_spider(self, spider): self.conn = pymysql.connect( host="127.0.0.1", port=3306, user="root", password="root", database="weibo_db", charset="utf8mb4" ) self.cursor = self.conn.cursor() def process_item(self, item, spider): sql = "INSERT INTO weibo_text(content, source, created_at) VALUES (%s, %s, NOW())" try: self.cursor.execute(sql, (item["content"], item["source"])) self.conn.commit() except Exception as e: self.conn.rollback() spider.logger.error("insert error: %s" % str(e)) return item def close_spider(self, spider): self.cursor.close() self.conn.close()

open_spider里建立数据库连接,process_item里执行插入,charset="utf8mb4"这个是硬性要求——微博文本里夹杂大量 Emoji 和生僻字,用utf8会直接报Incorrect string value错误。commit()必须放在execute之后,否则数据不落盘。如果爬虫跑得很快、数据量很大,可以在process_item里做攒批插入,每攒 50 条再executemany一次,能明显减少数据库连接压力。这套资源的爬虫部分没有做分布式扩展,单机跑完全够用。

提示:微博反爬升级频繁,最常见的封禁信号是「请求返回内容里没有 WB_text 节点,但页面本身正常打开」。遇到这种情况,先看请求头里的User-Agent是否被识别,再检查DOWNLOAD_DELAY是否设置得太短。一般我会在settings.py里把DOWNLOAD_DELAY设成 2 到 3 秒,同时开启RANDOMIZE_DOWNLOAD_DELAY = True,让请求间隔随机化。

4. SVM 情感分类:TF-IDF 特征与超参设置的完整过程

svm文件夹是整个系统的核心,资源预置了negative.xlsx和positive.xlsx两个标注数据集,分别代表负面和正面样本。要跑通这个工程,需要安装pandas、sklearn、jieba、joblib这几个库。Python 环境的坑在最后统一讲,这里先看完整的训练流程。

4.1 数据集读取与中文分词:为什么 SVM 不能直接吃原始文本

SVM 是数值模型,输入必须是固定维度的向量,不是字符串。所以原始微博文本要先做分词,再把分词结果转成 TF-IDF 特征矩阵,才能喂给模型。中文分词这里用的是jieba,它对微博这种短文本的切分效果比较稳定,而且支持自定义词典,可以把「绝绝子」「yyds」这类网络热词追加进去提升准确率。

import pandas as pd import jieba def load_data(neg_path, pos_path): neg = pd.read_excel(neg_path) pos = pd.read_excel(pos_path) neg["label"] = 0 pos["label"] = 1 data = pd.concat([neg, pos], ignore_index=True) # 打乱顺序,避免同类样本扎堆影响训练 data = data.sample(frac=1, random_state=42).reset_index(drop=True) return data def cut_text(text): return " ".join(jieba.lcut(str(text)))

load_data里给负面样本打标签 0、正面样本打标签 1,这是二分类的标准做法。sample(frac=1, random_state=42)是打乱操作,固定随机种子能保证每次复现的顺序一致,方便对比实验。jieba.lcut返回的就是分词后的列表,拼成空格分隔的字符串是为了后续喂给TfidfVectorizer——sklearn 的文本向量化默认按空格分词,已经切好的文本不需要再让它重新切。如果你发现换了一批数据集后准确率波动大,先检查是不是 Excel 里有空单元格或者全英文的长文本,这类样本会让TfidfVectorizer的词典膨胀。

4.2 TF-IDF 特征构造与 SVM 训练:C 和 gamma 怎么调才不玄学

特征构造用TfidfVectorizer,它能把每一条文本变成一个高维稀疏向量,每个维度代表一个词的 TF-IDF 权重。SVM 这里建议用SVC(kernel='rbf'),RBF 核能处理非线性边界,但有两个超参数直接影响结果:C和gamma。C是误分类惩罚系数,越大越容易过拟合;gamma控制单个样本的影响半径,越大模型越复杂。资源自带的数据集量级不大,所以C设在 1 到 10 之间、gamma设在'scale'或 0.1 附近是比较稳的起点。

from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.svm import SVC from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score, classification_report import joblib data = load_data("svm/data/negative.xlsx", "svm/data/positive.xlsx") data["cut"] = data["text"].apply(cut_text) X_train, X_test, y_train, y_test = train_test_split( data["cut"], data["label"], test_size=0.2, random_state=42 ) vectorizer = TfidfVectorizer(max_features=5000, ngram_range=(1, 2)) X_train_vec = vectorizer.fit_transform(X_train) X_test_vec = vectorizer.transform(X_test) model = SVC(kernel="rbf", C=1.0, gamma="scale", probability=True) model.fit(X_train_vec, y_train) y_pred = model.predict(X_test_vec) print("accuracy:", accuracy_score(y_test, y_pred)) print(classification_report(y_test, y_pred, target_names=["negative", "positive"])) joblib.dump(model, "svm_model.pkl") joblib.dump(vectorizer, "tfidf_vectorizer.pkl")

max_features=5000是限制特征维度,微博数据集一般不大,5000 个词足够覆盖大部分有效信息,还能避免维度爆炸拖慢 SVM 训练。ngram_range=(1, 2)让特征里同时包含单字和相邻双词组合,对「不好」「绝了」这类双词情感表达有显著帮助。probability=True必须开,因为后面做阈值调节时要拿predict_proba的概率值。两个joblib.dump把模型和向量器都落盘,预测阶段必须同时加载这两件套——只加载模型不加载向量器,新文本就转不出同样维度的特征矩阵。

4.3 模型预测与结果回写:加载模型对新文本判情感

训练完成后的预测是一个独立脚本,读取 MySQL 里爬虫抓取的新文本,逐条向量化,再搬运到模型里做预测。这里最容易被忽略的问题是向量器的复用——必须用训练时保存的那一个,不能新建一个TfidfVectorizer再fit,否则特征空间的列顺序和前 5000 个词都不一样,预测结果基本等于随机猜。

import joblib import pymysql import jieba model = joblib.load("svm_model.pkl") vectorizer = joblib.load("tfidf_vectorizer.pkl") conn = pymysql.connect(host="127.0.0.1", user="root", password="root", database="weibo_db", charset="utf8mb4") cursor = conn.cursor() cursor.execute("SELECT id, content FROM weibo_text WHERE sentiment IS NULL") rows = cursor.fetchall() for rid, content in rows: cut = " ".join(jieba.lcut(content)) vec = vectorizer.transform([cut]) proba = model.predict_proba(vec)[0] pred = model.predict(vec)[0] sentiment = int(pred) confidence = float(max(proba)) cursor.execute( "UPDATE weibo_text SET sentiment=%s, confidence=%s WHERE id=%s", (sentiment, confidence, rid) ) conn.commit() cursor.close() conn.close()

这条脚本用WHERE sentiment IS NULL做增量预测,跑过一次的文本不会重复计算,幂等性很好。confidence存的是预测类别的概率,这个值后面在 Web 展示和阈值调节时会用到。如果一次要预测几万条,建议分批fetchmany,每批 1000 条左右,避免一次性把所有文本加载进内存把进程拖垮。整个UPDATE操作放在循环结束后统一commit,比每条都 commit 快很多。

5. 联调避坑:五条血泪经验从爬虫到模型

这套系统横跨爬虫、机器学习、Web 三条技术栈,联调阶段的问题比单个模块内部多得多。下面这五条是我实际跑这类资源时踩过的坑,每条都按「现象 → 原因 → 解决」列出。

5.1 PyMySQL 报错:RuntimeError: 'cryptography' package is required for sha256_password

现象:启动爬虫或预测脚本时,第一次连 MySQL 就报RuntimeError: 'cryptography' package is required for sha256_password or caching_sha2_password。

原因:MySQL 8.x 默认认证插件是caching_sha2_password,而 PyMySQL 在未安装cryptography时无法完成这个认证流程。这个问题和代码无关,纯粹是环境依赖缺失。

解决:执行pip install cryptography,然后重跑。还有一种办法是把 MySQL 用户的认证插件改回mysql_native_password:ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'root';。我一般直接用第一种,装完就生效,不动数据库配置。

5.2 Excel 数据读出来中文全是乱码

现象:pd.read_excel读negative.xlsx和positive.xlsx,打印出来中文显示成乱码或者一堆方块。

原因:Excel 文件本身编码格式与 pandas 默认解析不一致,或者文件里混入了特殊字符。注意:这里说的不是csv的utf-8问题,而是xlsx格式在 pandas 底层用openpyxl解析时的文本兼容问题。

解决:先确认openpyxl已安装,然后显式指定引擎重读:pd.read_excel(path, engine="openpyxl")。如果仍然乱码,用 Excel 打开文件另存为xlsx默认格式覆盖保存一次,通常能清掉历史遗留的编码头。另一个隐蔽原因是列名不是text——资源里的 Excel 列名需要自己先print(data.columns)确认,再决定data["text"]还是data["content"]。我在这上面浪费过半天,列名对不上,后面所有apply全报 KeyError。

5.3 Scrapy 抓回来全是空列表,但浏览器打开微博正常

现象:爬虫跑完,MySQL 里一行数据都没写进去,日志显示每页都请求成功了,但parse里 XPath 提取结果永远是[]。

原因:微博 PC 端页面在未登录状态下返回的是跳转页或验证页,请求 URL 正常返回 200,但 HTML 结构跟预期完全不一样,WB_text这个 class 根本不存在。另一个常见原因是页面内容改为动态渲染,原始 HTML 里没有文本节点。

解决:第一,检查请求是否携带了有效登录 Cookie,在start_requests里把cookies换成自己浏览器的值,注意 Cookie 有时效,过期后要重新复制。第二,用scrapy shell <url>交互式调试,直接试 XPath,确认能命中再写进 spider。第三,如果页面是动态渲染,就需要把抓取对象换成移动端接口地址,或者引入 Selenium 渲染方案,但资源里的爬虫面向的是静态 HTML,动态渲染不在默认支持范围内。

5.4 sklearn 版本差异:加载 pkl 模型报ModuleNotFoundError: No module named 'sklearn.svm'

现象:换了一台机器,加载svm_model.pkl时直接报ModuleNotFoundError,或者提示pickle反序列化失败。

原因:joblib.dump保存的模型对象依赖 sklearn 的模块路径,不同版本的 sklearn 里SVC的导入路径可能不同(老版本是sklearn.svm.svc,新版本是sklearn.svm._classes)。跨版本加载时 pickle 按旧路径去找类,找不到就报错。

解决:在训练和预测环境里固定相同的 sklearn 版本,建议pip install scikit-learn==1.0.2这类具体版本号。工程里加一个requirements.txt,把pandas、scikit-learn、jieba、joblib、scrapy、pymysql全部钉死版本,换机器一条命令装齐。不要用del model这种野路子去改 pickle,重新训练一次成本很低,不要跟版本较劲。

5.5 SpringBoot 报表页启动后查不到数据,但 MySQL 里有

现象:analysis项目启动正常,页面也能打开,但报表和关键词视图全是空的,结果表里明明有数据。

原因:大概率是数据库连接串指向的库不对。analysis是 SpringBoot 项目,它的application.yml或application.properties里的jdbc:mysql://...配置的 database 名不是爬虫和 SVM 写入的那个库,或者用户名密码不是root/root。

解决:打开analysis/src/main/resources/application.properties(或yml),确认spring.datasource.url里的数据库名、username、password与实际一致。特别注意 SpringBoot 2.x 之后默认驱动是com.mysql.cj.jdbc.Driver,URL 里要带上serverTimezone=Asia/Shanghai,否则会报时区错误。改完重启项目,先看控制台有没有打印HikariPool-1 - Start completed,这一步通过基本就通了。

6. 进阶技巧:SVM 结果验证与置信度阈值微调

训练完一轮 SVM,accuracy达到 80% 以上并不代表可以直接上线,尤其是舆情分析这种场景,误判一条关键舆情比多判几条次舆情严重得多。我拿到模型后做的第一件事不是看总准确率,而是看classification_report里的每一类 Precision、Recall 和 F1。如果你发现负面类的 Recall 明显低于正面类,说明模型把大量负面文本错判成了正面——这往往是训练数据不平衡导致的,negative.xlsx和positive.xlsx的样本量如果不一致,模型会天然偏向多数类。

解决不平衡有两种常用手段:一是采样平衡,用imblearn的RandomOverSampler对少数类过采样;二是在训练时给SVC传class_weight='balanced',让模型自动按样本量反比调整惩罚权重。我一般先用class_weight='balanced',简单直接,效果不够再上过采样。

另一个容易被忽略的调优点是阈值微调。predict_proba返回的是每个类别的概率,默认predict取概率大于 0.5 作为正面。但在舆情监控场景里,你可能希望只有置信度超过 0.7 才判为负面,低于这个值的都进入人工复核列表。这种做法的价值在于:把「确定」和「不确定」分开,准确率统计才会真实反映模型能力。

proba = model.predict_proba(vec)[0] confidence = float(max(proba)) pred = 1 if confidence >= 0.7 else 0

这段代码的意思是:只要最大概率没到 0.7,一律按 0(负面)处理——这里的业务含义是「拿不准的就按负面预警」,适合舆情监控场景。如果你做的是电商评论口碑分析,可能反过来,拿不准的按正面。这个阈值没有标准答案,要拿一小批人工标注数据跑一遍,看不同阈值下误报和漏报的代价哪个更大。资源自带的测试数据足够你做一轮阈值扫描,把这批数据按 0.5、0.6、0.7、0.8 分别跑一遍,记录各自的精确率和召回率,你会发现阈值对结果的影响比换 kernel 还大。

验证阶段还有一个习惯值得养成:用cross_val_score做五折交叉验证,而不是只看一次手动切分的结果。单次train_test_split的数据划分带有随机性,一轮准确率高不代表稳定,交叉验证能反映模型在不同数据子集上的真实水平。从那以后,我每次训练完 SVM 都强制做三件事:看分类报告、调class_weight、用交叉验证确认分数波动,全部过了才敢让模型进预测脚本。希望这套流程帮你把这套舆情系统真正跑通,少走几圈我走过的弯路。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进