Python+SVM舆情分析系统实战:从Scrapy爬虫到情感分类全链路解析

发布时间:2026/10/1 17:24:29
Python+SVM舆情分析系统实战:从Scrapy爬虫到情感分类全链路解析 简介这套项目是一个基于Python与支持向量机的微博舆情分析系统完整覆盖数据采集、情感分类与Web可视化三大环节面向毕业设计、课程设计或工程实训人群也适合希望掌握爬虫、机器学习与Web开发整合流程的进阶学习者。系统按模块拆分为三个主要文件夹analysis为SpringBoot搭建的Web展示端scrapy是基于Scrapy框架的新浪微博爬虫svm则是采用支持向量机模型进行微博文本正负情感分类的机器学习项目同时附带MySQL建表语句、微博测试数据及Flume启动命令便于从数据库到采集训练再到页面展示全链路运行。资源压缩包共717个文件大小约15.13MB文件类型以Java源码、Python脚本、JS/CSS/HTML前端页面、XML配置、SQL脚本、CSV测试数据为主并包含图片、样式、日志等辅助材料目录结构清晰各模块可独立检索。下载后能够获得一套可直接导入PyCharm与IDEA运行的完整项目源码包含爬虫采集、模型训练、接口调用与可视化展示的完整业务流程适合作为课程设计、毕业设计或初期项目立项的参考基线也便于二次开发与功能扩展。目前已有166人学习下载具备一定的参考与实用价值。1. 基于PythonSVM的舆情分析系统从爬虫到情感分类的一次完整落地很多课程设计的选题都叫舆情分析可大部分交上来的东西只爬到一半爬虫能跑Excel里有几万条数据然后就没了。真正让这套东西从「爬下来」变成「用起来」的是情感分类那一步。这篇笔记拆的是一套基于Python SVM的舆情分析系统Scrapy管采集、SVM管情感正负分类、SpringBoot管展示中间用Flume把数据串起来MySQL存表、Redis做缓存。它不复杂但链条是完整的——从新浪微博爬到原始文本到负面/正面判定的结果再到页面上能看的关键词报告和综合报告每一步都能落地。适合拿来当毕设骨架、课程大作业或者作为初期项目立项的参考实现。下面按我拆包的顺序把每条线的代码和坑位都摊开。2. 把舆情系统拆成三条线Scrapy爬虫、SVM分类与SpringBoot展示的分工2.1 先看目录结构analysis、scrapy、svm、mysql各管什么这套资源解压后根目录下有四个目录加一个txt文件。别急着打开文件先把各自的职责圈清楚后面跑起来才知道哪段报错该去哪个目录里查。目录/文件技术栈职责analysisJava SpringBootWeb展示层舆情报告的页面、接口、导出文件都在这里scrapyPython Scrapy采集新浪微博文本落库或落盘svmPython sklearn读正负样本训练SVM模型对文本做情感分类mysqlSQL表结构脚本 测试数据flume.txtFlumeFlume的启动命令负责日志/数据的采集搬运开发环境上Python用PyCharm Community Edition就够了Java用IntelliJ IDEAMySQL的用户名密码是root/root连接时直接用。这里面有个容易忽略的点三个子项目是相互独立的工程不是一个大项目下的三个模块。所以跑的时候要分开启动顺序是先把MySQL建库导数据再跑Scrapy采集然后训练SVM最后才起SpringBoot看页面。2.2 Scrapy侧的关键代码item、parse与写库pipelinescrapy目录是一个标准Scrapy工程根目录有scrapy.cfg这是项目配置入口。写爬虫时我一般会新建一个spider定义好item字段然后在pipeline里落库。下面这段是items.py的核心字段按舆情分析的最小需要来设计关键词、正文、发布时间、用户名。import scrapy class WeiboItem(scrapy.Item): keyword scrapy.Field() # 搜索关键词用于区分不同舆情事件 content scrapy.Field() # 微博正文文本 publish_time scrapy.Field() # 发布时间原始字符串 user_name scrapy.Field() # 用户名统计维度用字段类型全部用scrapy.Field()这是Scrapy里声明item的标准写法。keyword字段在后面的SVM环节会作为舆情话题分组的依据content才是真正进入模型的文本。spider的parse方法负责解析响应。以m.weibo.cn移动端接口为例返回的是JSON结构cards数组里card_type等于9的才是正常微博内容import json import scrapy from ..items import WeiboItem class WeiboSinaSpider(scrapy.Spider): name weibo_sina allowed_domains [m.weibo.cn] start_urls [ https://m.weibo.cn/api/container/getIndex ?containerid100103type%3D1%26q%3D新能源车 ] def parse(self, response): data json.loads(response.text) cards data.get(data, {}).get(cards, []) for card in cards: if card.get(card_type) ! 9: continue mblog card.get(mblog, {}) item WeiboItem() item[keyword] 新能源车 item[content] mblog.get(text, ).strip() item[publish_time] mblog.get(created_at, ) item[user_name] mblog.get(user, {}).get(screen_name, ) yield item注意content里拿到的是带HTML标签的正文比如里面有a链接和span这个后面进模型前必须清洗否则TF-IDF的特征里全是标签噪声。parse里用yield返回item是Scrapy的标准迭代写法让引擎去调度后续的pipeline处理。pipeline负责把item写入MySQL这里直接用了pymysqlimport pymysql class WeiboPipeline(object): def open_spider(self, spider): self.conn pymysql.connect( host127.0.0.1, userroot, passwordroot, databaseweibo_sentiment, charsetutf8mb4 ) self.cursor self.conn.cursor() def process_item(self, item, spider): self.cursor.execute( INSERT INTO weibo_text(keyword, content, publish_time, user_name) VALUES (%s, %s, %s, %s), (item[keyword], item[content], item[publish_time], item[user_name]) ) self.conn.commit() return item def close_spider(self, spider): self.cursor.close() self.conn.close()charset必须用utf8mb4不能用utf8因为微博正文里经常出现emojiutf8存不下四字节字符插入时会直接报错。process_item里每insert一条就commit一次数据量小无所谓如果爬到几十万条性能会拖后腿那时应该改成攒一批再commit。settings.py里有几个关键参数直接决定爬虫能不能活下来BOT_NAME weibo ROBOTSTXT_OBEY False DOWNLOAD_DELAY 3.0 COOKIES_ENABLED True DEFAULT_REQUEST_HEADERS { User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X), Referer: https://m.weibo.cn/, } ITEM_PIPELINES { scrapy.pipelines.mysql.WeiboPipeline: 300, }DOWNLOAD_DELAY设成3秒是血泪教训换来的低于这个值很快会触发微博的访问频率限制。ROBOTSTXT_OBEY设成False是因为舆情采集接口本身不在robots允许范围内这是爬虫项目的常见做法不是乱来。2.3 用class名反推SpringBoot的模块设计analysis目录下编译好的class文件名其实已经把模块结构暴露得很清楚。拆别人的Java工程时我习惯先看class清单再找源码这套资源里最有信息量的几个class如下class名推测职责Topic.class话题实体类对应舆情话题的ORM对象ReportService.class报告生成服务负责组织舆情报告数据TopicFileWriter.class话题报告导出大概率是生成Excel或CSV文件KeywordReportView.class关键词报告视图对象SynthesisReportView.class综合报告视图对象UserInfo.class用户信息实体对应登录用户InvokingScrapy.class调用Scrapy爬虫的Java类可能是ProcessBuilder方式InvokingPython.class调用Python预测脚本的Java类RedisConfig.classRedis的配置类说明项目里用了Redis缓存从这些class能还原出业务流程用户登录后管理舆情话题系统通过InvokingScrapy触发爬虫采集采集文本交给SVM模型分类分类结果由ReportService汇总成KeywordReportView和SynthesisReportView两种报告页面展示的同时用Redis缓存热点查询最后可以通过TopicFileWriter把报告导出成文件。这套设计对课程设计来说已经偏完整了照着这个模块边界去读源码思路会顺很多。3. SVM情感分类实战从Excel数据集到可部署的模型文件3.1 读入正负样本negative.xlsx、positive.xlsx与标签生成svm目录下有两个Excel文件negative.xlsx存的是负面微博文本positive.xlsx存的是正面微博文本。这是训练语料也是整个SVM模型的输入源头。读文件时不确定Excel有没有表头稳妥做法是不依赖表头直接指定第一列就是文本列import pandas as pd neg pd.read_excel(../svm/data/negative.xlsx, headerNone, names[text]) pos pd.read_excel(../svm/data/positive.xlsx, headerNone, names[text]) print(负面样本数:, len(neg), 正面样本数:, len(pos))如果Excel第一行是表头把headerNone改成header0names改成实际列名就行。读进来之后先看一眼样本量正负样本量差距大不大这直接影响后面的类别不平衡处理。然后做清洗。微博文本里典型的噪声有三种用户、URL、HTML标签。这里写一个clean_text函数统一处理import re def clean_text(s): if not isinstance(s, str): return s re.sub(r回复\w[:], , s) # 去掉「回复某某:」前缀 s re.sub(rhttps?://\S, , s) # 去掉URL s re.sub(r[^], , s) # 去掉HTML标签 return s.strip() neg[text] neg[text].map(clean_text) pos[text] pos[text].map(clean_text) neg neg[neg[text].str.len() 0] pos pos[pos[text].str.len() 0]最后一行过滤清洗后变成空串的样本这是非常容易被忽略的一步。如果不过滤空文本进到TF-IDF里就是一个全零向量对模型训练没有贡献还可能干扰类别分布。打标签时把负面标成0、正面标成1然后合并生成训练集import numpy as np neg[label] 0 pos[label] 1 df pd.concat([neg, pos], ignore_indexTrue) df df.sample(frac1, random_state42).reset_index(dropTrue)shuffle这一步不能省。原始数据是按文件堆积的前面全是负面后面全是正面不洗牌会导致交叉验证时数据分布失真。3.2 中文文本转特征jieba分词与TfidfVectorizer的参数SVM不能直接吃字符串必须把文本转成数值向量。中文和英文不一样词之间没有空格所以先要分词。资源里依赖的是jieba分词后再交给TfidfVectorizer做向量化import jieba from sklearn.feature_extraction.text import TfidfVectorizer stopwords set() try: stopwords set(open(../svm/data/stopwords.txt, encodingutf-8).read().split()) except FileNotFoundError: pass def tokenize(text): words jieba.lcut(text) return [w for w in words if w.strip() and w not in stopwords and len(w) 1] vectorizer TfidfVectorizer( tokenizertokenize, ngram_range(1, 2), min_df2, max_features5000 ) X vectorizer.fit_transform(df[text]) y df[label].values print(特征矩阵形状:, X.shape)参数里有几个值得展开说。ngram_range(1, 2)表示保留单个词和相邻两词的组合舆情文本里「不」「喜欢」这种否定组合如果只按单字词切会丢失语义bigram能部分缓解。min_df2表示至少在2篇文本里出现过的词才保留目的是过滤只在某一条微博里出现的生僻词。max_features5000把特征维度控制在5000以内SVM在高维稀疏向量上表现好但维度太高会增加训练耗时5000对课程设计这个体量是够用的。如果资源包里没有stopwords.txttokenize里len(w) 1这个条件就是兜底把单字词和标点先滤掉。分词器是黑匣子建议第一次跑的时候打印两三条分词结果确认效果再进入训练环节。3.3 训练SVMC、gamma、kernel与GridSearchCV调参SVM的核心参数是kernel、C、gamma。kernel决定映射方式文本分类默认用rbf因为文本特征在高维空间里往往不是线性可分的。C是惩罚系数C越大对误分类的容忍度越小模型越容易在训练集上过拟合gamma是rbf核的宽度参数gamma越大每个样本的影响半径越小决策边界越曲折反之边界越平滑。先划分训练集和测试集注意用stratify保证正负样本在两边比例一致from sklearn.model_selection import train_test_split, GridSearchCV from sklearn.svm import SVC X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) param_grid { C: [0.1, 1, 10], gamma: [0.001, 0.01, 0.1], } svm SVC(kernelrbf, class_weightbalanced, probabilityTrue, random_state42) grid GridSearchCV(svm, param_grid, cv5, scoringf1) grid.fit(X_train, y_train) print(最优参数:, grid.best_params_)class_weightbalanced是处理正负样本量不一致的关键。如果正面样本明显多于负面模型为了整体准确率会把所有样本都判成正面加了这个参数后SVM会按类别频率自动放大少数类的惩罚权重。probabilityTrue是为了拿到predict_proba的置信度后面的预警功能要用。3.4 评估与落盘classification_report与joblib导出训练完不能只看准确率舆情分析里负面样本的召回率比整体准确率重要得多。模型评估代码用classification_report和混淆矩阵一起看from sklearn.metrics import classification_report, confusion_matrix import joblib best grid.best_estimator_ y_pred best.predict(X_test) print(classification_report(y_test, y_pred, target_names[负面, 正面])) print(confusion_matrix(y_test, y_pred)) joblib.dump(best, ../svm/model/svm_model.pkl) joblib.dump(vectorizer, ../svm/model/vectorizer.pkl)classification_report里重点看负面类的recall和f1-score。recall低说明很多负面微博没被抓出来这在舆情场景里意味着漏掉了负面舆情事件比误报更麻烦。模型和vectorizer分别dump成pkl文件原因很直接预测时新文本必须先经过同一个vectorizer转成同维度向量再喂给模型两个文件必须配套使用只存模型不存vectorizer预测时维度对不上会直接报错。4. 数据流转链路与Web展示Flume、MySQL、Redis如何把SVM结果送到页面4.1 一条微博从爬到展示的完整链路把三个子项目串起来看数据流转是这条线Scrapy从新浪微博采集原始文本写入MySQL的weibo_text表SVM模型对这些文本做情感分类把结果写回MySQLSpringBoot从MySQL读分类结果按话题聚合生成报告热点查询结果缓存到Redis页面展示时优先走缓存。这里Flume的角色是数据搬运工。资源包里flume.txt存放的是Flume启动命令说明这套系统用Flume在Scrapy和下游之间做了一层采集管道。常见做法是让Scrapy把采集结果落盘成日志文件Flume用taildir方式监听这个文件把增量数据采集后送往HDFS或Kafka等下游。4.2 Flume启动命令与采集配置flume.txt里到底写了什么flume.txt里核心是一条flume-ng命令格式大致如下bin/flume-ng agent \ --name a1 \ --conf conf \ --conf-file conf/svm_agent.conf \ -Dflume.root.loggerINFO,console--name a1指定agent名称--conf-file指向具体的配置文件。真正决定采集行为的都在conf文件里常见做法是用taildir source监听日志目录a1.sources r1 a1.channels c1 a1.sinks k1 a1.sources.r1.type taildir a1.sources.r1.positionFile /data/flume/taildir_position.json a1.sources.r1.filegroups f1 a1.sources.r1.filegroups.f1 /data/logs/weibo/.*\.log a1.sources.r1.channels c1 a1.channels.c1.type file a1.channels.c1.checkpointDir /data/flume/checkpoint a1.channels.c1.dataDirs /data/flume/data a1.sinks.k1.type hdfs a1.sinks.k1.hdfs.path /warehouse/weibo/sentiment/%Y%m%d a1.sinks.k1.channel c1这段配置里最关键的是positionFile它记录taildir已读取到的文件位置。有了它Flume重启后能从断点继续读不会从头把整个日志再采一遍。channel用file类型而不是memory类型是为了在Flume进程突然退出时数据不丢。hdfs.path按日期分目录方便后面按天统计舆情趋势。4.3 SpringBoot调用SVM模型InvokingPython的两种实现SpringBoot里有个InvokingPython类说明系统是Java侧主动调用Python预测脚本的。最直接的实现是用ProcessBuilder启动python进程ProcessBuilder pb new ProcessBuilder( python, /opt/weibo/svm/predict.py, --text, content ); pb.redirectErrorStream(true); Process p pb.start(); String result IOUtils.toString(p.getInputStream(), StandardCharsets.UTF_8); int exitCode p.waitFor();逻辑说明redirectErrorStream(true)把错误流合并到标准输出这一步是防止Python脚本打印错误信息到stderr时stderr管道没人读导致缓冲区写满进程阻塞住。整个调用是同步的p.waitFor会一直等到Python脚本退出。这种模式简单但每次请求都新建进程开销大并发高时会积压。更保险的做法是把predict.py包成一个HTTP服务SpringBoot用RestTemplate或Feign去调。这个思路和把Python应用融入Spring Cloud Alibaba微服务体系的方案也一致Python侧单独起一个预测服务注册进注册中心Java侧通过服务名调用。改动不大但可扩展性好很多预测服务可以独立升级模型而不影响Java应用。核心预测脚本逻辑很简单加载pkl文件清洗文本向量化输出预测标签和概率import joblib model joblib.load(../svm/model/svm_model.pkl) vectorizer joblib.load(../svm/model/vectorizer.pkl) def predict(text): text_vec vectorizer.transform([clean_text(text)]) label model.predict(text_vec)[0] proba model.predict_proba(text_vec)[0] return label, float(proba[1])4.4 Redis缓存与报告视图KeywordReportView、SynthesisReportViewRedisConfig.class说明系统接入了Redis。缓存的设计思路是舆情报告的计算链路长从MySQL查原始文本、过模型、聚合统计一次完整计算可能要几十秒热点话题被反复查看时不能每次都重算。Configuration public class RedisConfig { Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); return template; } }Value用GenericJackson2JsonRedisSerializer是因为报告视图对象要能被序列化成JSON缓存用JDK默认序列化方案存进Redis的可读性差跨语言也不好处理。缓存key设计上我会按话题ID和报告类型做区分缓存key含义TTLtopic:keyword:12话题12的关键词报告10分钟topic:synthesis:12话题12的综合报告5分钟对应到代码就是String key topic: topicId : reportType; String cached (String) redisTemplate.opsForValue().get(key); if (cached ! null) { return cached; } String report reportService.buildReport(topicId, reportType); redisTemplate.opsForValue().set(key, report, 10, TimeUnit.MINUTES); return report;KeywordReportView和SynthesisReportView是两套视图对象前者展示高频关键词列表和相关微博后者是整体舆情态势的汇总面板。Redis缓存对这两类视图都生效缓存命中时页面响应能从秒级降到毫秒级这在演示场景里特别加分。5. 舆情分析系统避坑手册五个真实翻车点与排查思路5.1 模型向量全是零分词器或脏数据问题现象训练时X.toarray()打印出来几乎全是0模型预测结果集中在某一个类别上classification_report显示所有类别只有一个有数值。原因要么是read_excel读出来的是NaN要么分词函数返回了空列表TfidfVectorizer对空文本直接产出零向量。解决读Excel后强制df[text].astype(str)并dropna分词函数里过滤空串训练前打印X.getnnz(1)检查每行非零元素数量如果有大量0值行回头查清洗环节。5.2 负面样本被全部判成正面类别不平衡现象准确率90%以上但负面类的recall是0混淆矩阵显示所有真实负面都被判成正面。原因训练语料里正面样本远多于负面SVM为了降低整体误分类率把决策边界推到了全部判正的位置。解决分层抽样stratifyy保证训练测试分布一致SVC里加class_weightbalanced评估指标改用f1-score而不是accuracy。如果加了balanced还是拉不回来说明负样本量确实太少需要补充负面语料或者用SMOTE做少数类过采样。5.3 Scrapy一启动就被微博拦Cookie与请求频率现象爬虫启动后能收到响应但解析出来cards是空的或者部分请求302跳转到登录页。原因微博对未登录的匿名请求做了限制只返回登录引导页。解决登录微博网页版后从浏览器开发者工具复制Cookie粘到settings.py同时把DOWNLOAD_DELAY调到3秒到5秒之间。这个delay参数是血泪经验设成1秒大概率爬几百条就触发限制。另外parse到的content要确认不是空字符串微博接口有时候会返回被折叠的内容。5.4 Java调Python进程卡死stderr阻塞与解释器路径现象调用InvokingPython的接口第一次能通连续调用几次后页面一直转圈日志里没有任何异常输出。原因ProcessBuilder默认没有消费子进程的stderr输出Python脚本往stderr里写一行日志缓冲区满了之后进程就不再往下执行外部看起来就是卡死。解决调用pb.redirectErrorStream(true)合并流或者单独起一个线程消费errorStream。还有Windows环境下processBuilder里写python可能解析到WindowsApps里的假python必须写解释器的绝对路径。5.5 Flume重启后重复计算positionFile没落盘现象Flume进程重启后下游发现同一条数据被重复处理MySQL里出现重复记录。原因taildir的positionFile没有配置或者配置到了临时目录重启后丢失了读取位置从头再采一遍。解决把positionFile配到一个持久化目录比如/data/flume/taildir_position.json并确认运行Flume的用户对该目录有写权限。同时在MySQL表上对微博id建唯一索引做兜底即使Flume重复发送insert也能幂等跳过。6. 让模型效果再往前走一步特征扩展与批量回放验证6.1 从TF-IDF到Word2Vec特征怎么扩展才有效TF-IDF对SVM来说够用但它把每个词当成独立维度忽略了词与词之间的语义关联。舆情文本里「暴跌」「跳水」「崩了」在TF-IDF看来是三个完全不相干的特征而Word2Vec能把它们映射到相邻的向量空间。数据量够的情况下可以训练一个word2vec模型把句子向量拼到TF-IDF后面import numpy as np def sentence_vector(tokens, w2v_model, dim100): vecs [w2v_model.wv[w] for w in tokens if w in w2v_model.wv] if not vecs: return np.zeros(dim) return np.mean(vecs, axis0) X_w2v np.vstack([sentence_vector(t, w2v_model) for t in tokenized_texts]) X_combined np.hstack([X_tfidf.toarray(), X_w2v])拼接后特征维度增加不少如果样本量只有几千条效果不一定比纯TF-IDF好这点要有心理预期。特征扩展的前提是语料量足够样本不足时强行上word2vec只会让模型更容易过拟合。6.2 用predict_proba做舆情预警的批量回放课程设计做到模型训练完通常就收尾了但舆情系统真正的价值在预警。predict_proba返回的概率值比二分类标签信息量大得多Probability接近0.5的属于模型拿不准的边缘样本接近1的是强正面接近0的是强负面。批量回放时把历史数据重新过一遍模型挑出低概率和高概率的两端观察它们是否对应真实的舆情事件proba best.predict_proba(X)[:, 1] alert_df df.copy() alert_df[negative_prob] 1 - proba hot_alerts alert_df.sort_values(negative_prob, ascendingFalse).head(50)这个思路可以直接对接SpringBoot把阈值配置放进application.ymlnegative_prob超过0.8的微博自动进入综合报告的预警区域由SynthesisReportView统一展示。从那以后我每跑一套舆情数据都会强制走一遍「原始文本 → 模型回放 → 页面比对」的验证流程先看模型在历史数据上的概率分布合不合理再谈上线。模型是黑匣子但概率分布骗不了人。希望这篇拆解能帮你把整套链路完整跑通少踩我踩过的那些坑。本文还有配套的精品资源点击获取