基于NLP的微博情感分析系统:从爬虫到模型部署完整实践
2026/9/16 16:13:55 网站建设 项目流程

简介:面向计算机、通信、人工智能、自动化等相关专业的高校学生、教师及从业者,这份基于NLP的微博用户情感分析系统Python源码,以真实微博文本为处理对象,完整覆盖数据清洗、中文分词、停用词过滤、TF-IDF/Word2Vec特征构建、情感分类模型训练与评估等核心环节,清晰呈现了从原始语料到情感极性判定的全链路实现思路,能够有效解决微博文本情感分析场景下的数据建模与工程落地难题。压缩包约85MB,所有代码均经过反复调试与测试,可稳定运行,项目为个人毕业设计成果,答辩评审分达98分,在算法选型、特征工程与结果可视化方面都有可圈可点之处。目前已有144人学习下载,既适合初学者逐行读懂NLP处理流程,也为中高级开发者提供了可扩展的基座代码,便于替换数据集、切换分类算法或增加实时分析接口。借助该源码,读者既能快速复现实验结论,也能按需调整模块结构,善用其可读性与可扩展性,适用于课程设计、期末大作业及毕业设计的深度二次开发,整体具备很高的学习借鉴价值。

1. 一条微博从爬取到情感打标,这条链路值得完整跑一遍

"这家店的火锅排队两小时也值"和"踩雷了,服务差到离谱"这两条微博,在系统里的处理路径完全一致:抓文本、去噪音、分词、向量化、过模型,最后输出正向或负向概率。这套基于NLP的微博用户情感分析系统做的正是整条链路,而不是只给一个训练好的黑盒模型。它将微博数据采集、中文文本预处理、情感分类、结果可视化串成一个完整工程,源码用Python写就,解压后可以按步骤直接运行。答辩评审拿到98分的关键,在于每一层都有代码可讲、有参数可调、有坑可说明。资源适合两类人:准备做NLP自然语言处理方向毕业设计或课程设计的学生,以及想快速搭建文本情感分析服务、之后把微博数据源换成电商评论或新闻资讯就能复用的从业者。

2. 微博数据采集与存储:从m站接口到MySQL的落地细节

2.1 系统模块划分与技术选型

拿到一个毕设级别的NLP情感分析项目,先别急着看模型,把数据链路理清楚更重要。这套系统的整体结构分为六块:数据采集、数据存储、预处理、特征工程、模型训练、结果展示。各层的技术选型如下表所示。

层级技术选型职责
数据采集requests + m.weibo.cn 搜索接口按关键词拉取微博文本
数据存储MySQL 8.0 + utf8mb4保存原始文本与标注结果
预处理jieba + 正则表达式清洗、分词、去停用词
特征工程scikit-learn TfidfVectorizer / gensim Word2Vec文本转数值特征
模型层MultinomialNB / LinearSVC / TextCNN情感分类
展示层Flask + ECharts可视化统计结果

选型的理由要能讲清楚。展示层用Flask而不用Django,是因为工程只需要暴露一个分析页面和两个接口,Flask的轻量足够;分词用jieba而不是HanLP,考虑到毕设周期和调试成本,jieba的自定义词典机制能快速解决微博网络新词问题;存储用MySQL而不用MongoDB,是因为结构化表设计在答辩时更直观,评审老师看ER图就能理解字段含义。模型层同时保留传统机器学习和深度学习两条路,调参时可以做横向对比,这也是答辩的加分点。

2.2 用Python爬虫拉取微博搜索页数据

毕设场景不需要维护分布式爬虫,直接用Python请求微博手机站(m.weibo.cn)的搜索接口即可。这个接口返回JSON数据,比解析网页版HTML要稳定得多,而且天然支持翻页。下面是一段可运行的数据采集代码。

import requests import re import time def fetch_weibo(keyword, page=1): url = "https://m.weibo.cn/api/container/getIndex" params = { "containerid": f"100103type=1&q={keyword}", "page_type": "searchall", "page": page } headers = { "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X)", "Referer": "https://m.weibo.cn/" } resp = requests.get(url, params=params, headers=headers, timeout=10) data = resp.json() cards = data.get("data", {}).get("cards", []) text_list = [] for card in cards: mblog = card.get("mblog", {}) raw = mblog.get("raw_text") or mblog.get("text", "") text_list.append(re.sub(r"<[^>]+>", "", raw)) return text_list if __name__ == "__main__": for page in range(1, 4): rows = fetch_weibo("新能源汽车", page=page) for row in rows: print(row) time.sleep(2)

这段代码的逻辑是:通过containerid参数指定搜索关键词,page_type=searchall表示综合搜索分类,page控制翻页;接口返回的cards列表中,每张card的mblog字段就是微博实体,text里混有HTML标签,用re.sub统一去掉。headers里的User-Agent模拟移动端请求,可以避免部分桌面端接口的限制;每次翻页后sleep两秒,是为了降低采集频率,防止触发访问限制。需要注意,m站的搜索接口翻页深度有限,对毕设几万条的语料量来说足够,如果做大规模采集,要考虑登录态cookie的维护和更严谨的限速策略。

2.3 数据清洗规则与MySQL表结构设计

采集到原始文本后不能直接入库,微博文本里混杂着话题标签、@用户、URL、表情占位符和转发前缀,这些对情感分类都是噪声。清洗函数我一般放在一个独立的utils.py里,保持主流程干净。

import re def clean_weibo(raw): raw = re.sub(r"//@[^::]*[::]", "", raw) # 去掉转发前缀 raw = re.sub(r"#.*?#", "", raw) # 去掉话题标签 raw = re.sub(r"https?://\S+", "", raw) # 去掉URL raw = re.sub(r"\[([^\] ]+)\]", "", raw) # 去掉[微笑]这类表情占位符 raw = re.sub(r"@\S+", "", raw) # 去掉@用户 return raw.strip()

清洗的正则顺序有讲究:先转发前缀再话题,是因为转发文本里可能嵌套话题标签;表情占位符是[微笑]这种格式,直接按方括号匹配去除。清洗后的文本存入MySQL,建表语句如下。

CREATE TABLE weibo_sentiment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, weibo_id VARCHAR(32) UNIQUE, user_name VARCHAR(64), content TEXT, emotion_label TINYINT COMMENT '0负向 1中性 2正向', created_at DATETIME, crawl_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

weibo_id字段需要加唯一索引,避免重复采集时反复入库;emotion_label允许为NULL,因为初始抓取的数据没有标签,要等人工标注或模型预测后再回填。content使用TEXT类型,utf8mb4字符集保证Emoji和生僻字不丢失。写入时用INSERT IGNORE配合唯一索引,重复数据直接跳过,这个细节在答辩时可以展开讲。

3. 中文分词、向量化与情感特征工程:NLP核心环节

3.1 微博短文本的分词难点与自定义词典

微博文本和新闻语料最大的区别在于网络新词密度高。"yyds""绝绝子""集美"这类词在jieba默认词库里不存在,直接分词会被切碎,情感信息随之丢失。比如"这家店真的yyds"如果被切成"yyd"和"s",正向信号就完全消失了。解决办法是加载自定义词典。

import jieba jieba.load_userdict("weibo_dict.txt") text = "这家店真的yyds,集美们冲" print(jieba.lcut(text)) # 加载词典前: ['这家', '店', '真的', 'yyds', ',', '集美', '们', '冲'] # 加载词典后: ['这家', '店', '真的', 'yyds', ',', '集美', '们', '冲']

weibo_dict.txt的格式是每行一个词,可以带词频和词性,例如yyds 100000 n。词频数字影响分词时的优先级,标得越大越容易被保留为整体。另一个容易忽略的API是jieba.lcut_for_search,它会把长词再次切分成细粒度词,适合搜索引擎场景;做情感分类时直接使用lcut即可,细粒度切分反而会增加特征维度、稀释情感词信号。分词完成后,还要过滤停用词。停用词表可以直接用GitHub上的中文停用词库,再手动补充微博场景特有的"哈哈哈""转发微博"等噪声词。

3.2 TF-IDF与Word2Vec:两种向量化的取舍

文本无法直接输入模型,先要转成数值向量。最常见的做法是TF-IDF向量化,scikit-learn一行就能完成。

from sklearn.feature_extraction.text import TfidfVectorizer # docs 是分词后用空格连接的句子列表,例如 "这家 店 真的 好喝" vectorizer = TfidfVectorizer( max_features=5000, ngram_range=(1, 2), min_df=2, max_df=0.95, sublinear_tf=True ) X = vectorizer.fit_transform(docs)

参数的意义要理解清楚:max_features限制特征维度,防止词表过大导致矩阵稀疏和训练变慢;ngram_range设为(1, 2),除了单个词,还把相邻两个词的组合也作为特征,这样"不好"会被识别为独立特征,而不是被"好"完全覆盖;min_df=2表示只在1篇文档中出现过的词直接丢弃;max_df=0.95表示超过95%文档都出现的词视为停用词。sublinear_tf=True对词频做对数压缩,避免高频词主导相似度计算。

TF-IDF是词袋模型,丢失了词序和语义。如果想要更丰富的语义表达,可以训练Word2Vec词向量。

from gensim.models import Word2Vec sentences = [cut_words(d) for d in docs] # 每篇文档是分词后的词列表 w2v_model = Word2Vec( sentences, vector_size=128, window=5, min_count=3, sg=1 )

sg=1表示使用skip-gram训练方式,在中小规模语料上比CBOW能学到更准确的词义;vector_size=128控制向量维度,太小了区分度不够,太大了训练时间明显增加;min_count=3过滤掉出现次数少于3次的生僻词。这里要给一个明确的取舍建议:传统机器学习模型用TF-IDF做输入,效果稳定、可解释性强;Word2Vec的词向量矩阵则用于深度学习模型的Embedding初始化,而不是直接把词向量平均成句向量——平均操作会把词序和情感强度信息都抹平。

向量化方式优点缺点推荐使用场景
TF-IDF可解释、训练快、n-gram能捕捉局部否定维度高、无语义朴素贝叶斯、SVM基线
Word2Vec语义相似、维度低需要较大语料、句向量难构造TextCNN的Embedding层初始化

相比用SPSS Modeler这类可视化建模工具做情感分析,Python方案的优势在于分词、向量化、模型训练可以在同一个Pipeline里完成,调参和上线之间的衔接成本更低。

3.3 否定词翻转:让"不"字参与情感计算

中文情感分析里有一个经典问题:"味道不错"和"味道不好吃"如果只按词频统计,"好吃"的正向权重会把整句话带偏。解决思路有两种:一是用bigram特征,让"不_好吃"成为一个独立特征;二是在预处理阶段做否定词翻转。

NEGATION = {"不", "没", "别", "无", "莫", "勿", "未曾", "毫无", "不太", "不怎么"} def negation_flip(tokens): new_tokens = [] i = 0 while i < len(tokens): if tokens[i] in NEGATION and i + 1 < len(tokens): new_tokens.append(tokens[i] + "_" + tokens[i + 1]) i += 2 else: new_tokens.append(tokens[i]) i += 1 return new_tokens

这段代码的逻辑是:遇到否定词时,把否定词和后面的词合并为一个token再放回列表,例如"不 好吃"变成"不好吃";同时跳过下一个词,避免"不"和"好吃"分别进入特征空间。实现简单,但能明显提升负向样本的召回率。进一步的做法是叠加情感词典特征:准备一个包含"开心、靠谱、赞、推荐"等正向词和"踩雷、无语、坑、差评"等负向词的词典,统计文本中正负词的数量差,作为一维特征拼接到向量后面。这个特征对SVM和Logistic回归的提升比深度学习模型更明显,可以作为答辩时的实验对比项。

4. 情感分类模型训练与调参:从朴素贝叶斯到TextCNN

4.1 训练语料与标签分布处理

模型训练前需要一份标注好的微博情感语料。常见做法是使用开源的微博情感数据集,例如GitHub上的weibo_senti_100k,包含十万条正向和负向微博,标签均衡,适合做分类基线。如果网络受限,也可以用SnowNLP自带的中文评论语料,但需要重新训练且效果不如微博原生语料。语料准备好后,按8:2划分训练集和测试集,划分时必须用stratify参数保证标签比例一致。

from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( texts, labels, test_size=0.2, random_state=42, stratify=labels )

random_state固定下来,保证每次实验划分一致,调参时可复现;stratify=labels让正负样本在训练集和测试集中的分布与全量数据一致,防止某个类别在测试集里占比过高导致评估失真。

4.2 机器学习基线:朴素贝叶斯与线性SVM

做深度学习之前,先跑通一个传统机器学习基线,用来确定任务的天花板参考。把所有预处理步骤放进scikit-learn的Pipeline,可以避免数据泄露——TfidfVectorizer只会在训练集上拟合词表,测试集直接复用。

from sklearn.naive_bayes import MultinomialNB from sklearn.pipeline import Pipeline from sklearn.metrics import classification_report pipeline = Pipeline([ ("tfidf", TfidfVectorizer(max_features=6000, ngram_range=(1, 2))), ("clf", MultinomialNB(alpha=0.5)) ]) pipeline.fit(X_train, y_train) pred = pipeline.predict(X_test) print(classification_report(y_test, pred, target_names=["neg", "pos"]))

MultinomialNB适用于TF-IDF这种非负计数型特征,alpha=0.5是拉普拉斯平滑系数,调低可以让概率估计更贴近真实分布,避免在稀疏特征上过分平滑。如果换成线性SVM,需要用LinearSVC而不是SVC(kernel="rbf"),因为线性核在高维文本特征上训练快、效果也不差。

from sklearn.svm import LinearSVC svm_model = LinearSVC(C=1.0, class_weight="balanced")

class_weight="balanced"会自动根据类别频率调整权重,正负样本比例如果偏差较大,这一项能让模型不偏向多数类。线性SVM的C是正则化强度的倒数,C越小正则化越强,默认C=1.0通常可以在验证集上看到一个合理的起点,再根据F1值做网格搜索。通常在这类微博情感语料上,朴素贝叶斯的F1在0.82左右,线性SVM能到0.86以上,这个差距本身就是答辩时值得分析的实验现象。

4.3 深度学习分支:TextCNN短文本分类

如果希望进一步提升效果,同时展现对深度学习方法的掌握,可以在模型层引入TextCNN。它的核心思想是用多个不同宽度的卷积核捕捉句子中的n-gram特征,本质上是bigram、trigram的神经网络版本,非常契合微博这种短文本场景。

from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Embedding, Conv1D, GlobalMaxPooling1D, Dense, Dropout MAX_LEN = 64 VOCAB_SIZE = 30000 EMBED_DIM = 128 NUM_FILTERS = 100 model = Sequential([ Embedding(VOCAB_SIZE, EMBED_DIM, input_length=MAX_LEN), Conv1D(NUM_FILTERS, 3, activation="relu"), GlobalMaxPooling1D(), Dropout(0.5), Dense(2, activation="softmax") ]) model.compile(optimizer="adam", loss="sparse_categorical_crossentropy", metrics=["accuracy"])

这个网络先通过Embedding层把每个词映射成128维向量,然后用100个宽度为3的卷积核扫描整个序列,每个卷积核相当于在寻找一种特定的三词组合模式;GlobalMaxPooling1D把每个卷积核的输出取最大值,保留最显著的特征,再经过Dropout随机丢掉一半神经元防止过拟合,最后用softmax输出正负类概率。训练参数汇总如下。

参数说明
MAX_LEN64文本序列长度,超过截断不足补零
VOCAB_SIZE30000只保留训练集中最高频的3万个词
EMBED_DIM128词向量维度,与Word2Vec保持一致
NUM_FILTERS100卷积核个数,决定特征图数量
Dropout0.5全连接层随机失活比例

训练时如果前一步已经用gensim训练了Word2Vec,可以把词向量矩阵直接灌进Embedding层,而不是从零开始学。

embedding_matrix = w2v_model.wv.vectors model.layers[0].set_weights([embedding_matrix]) model.layers[0].trainable = False

先冻结Embedding层训练两个epoch,让卷积层适应预训练向量的分布,然后再把trainable设为True做整体微调。这个两阶段训练策略能避免预训练向量在前几步反向传播时被严重破坏。训练时配合validation_split=0.1观察验证集loss,连续三个epoch不下降就该早停。在十万级别的微博语料上,TextCNN的准确率通常比线性SVM再高2到3个百分点,但训练时间和显存开销也明显增加,毕设文档里可以把这两个模型的性能对比单独写成一个小节。

5. 模型导出与接口验证:把分类器做成可调用的服务

5.1 用joblib持久化整个Pipeline

模型训练好后,不能每次预测都重新跑一遍训练。把Pipeline整体持久化是标准做法,注意保存的是整个Pipeline而不是单独的分类器。

import joblib # 保存完整Pipeline joblib.dump(pipeline, "sentiment_pipeline.pkl") # 加载后直接预测 loaded_model = joblib.load("sentiment_pipeline.pkl") label = loaded_model.predict(["这家店真的值得推荐"])[0]

只保存分类器而忘记保存TfidfVectorizer,是线上推理最常见的错误。向量化器的词表偏移一个词,预测输入就和新特征空间对不上,会直接报维度错误。保存整个Pipeline之后,加载和预测都是同一个对象,少踩很多坑。用Flask把这个模型包成HTTP接口,答辩演示时可以在浏览器里实时输入文本看结果。

from flask import Flask, request, jsonify import joblib app = Flask(__name__) model = joblib.load("sentiment_pipeline.pkl") @app.route("/analyze", methods=["POST"]) def analyze(): data = request.get_json() text = data.get("text", "") if not text: return jsonify({"error": "empty text"}), 400 label = model.predict([text])[0] prob = model.predict_proba([text])[0].tolist() return jsonify({"sentiment": int(label), "prob": prob}) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000)

predict_proba返回两个类别的概率数组,前端拿到后可以直接渲染成情感强度条。host设成0.0.0.0允许局域网访问,方便在演示时用手机连同一个WiFi直接测试。

5.2 部署后的两个验证技巧

接口上线后,还要验证模型在真实微博文本上的表现和训练集是否一致。第一个技巧是抽样人工标注算Kappa一致性系数。

from sklearn.metrics import cohen_kappa_score # manual_labels 是人工标注结果,pred_labels 是模型预测结果 kappa = cohen_kappa_score(manual_labels, pred_labels) # kappa > 0.6 说明模型与人工标注有可接受的一致性

Kappa系数能排除一部分靠猜对的概率,比单纯看准确率更严格。第二个技巧是长文本切片预测。模型训练时MAX_LEN=64,线上一条长微博可能超过这个长度,直接截断会丢掉后半段的情感转折,前面说"味道不错",后面接"但服务太差",截断就全偏了。

import re def predict_long_text(text, model, max_len=64): segments = re.split(r"[。!?!?;;]", text) scores = [] for seg in segments: seg = seg.strip() if not seg: continue prob = model.predict_proba([seg])[0] scores.append(prob[1]) # 正向概率 return sum(scores) / len(scores)

按标点把长文本切成短句,逐句预测后取平均概率,比直接截断更合理。这个切片策略同样适用于新闻长文或商品长评论的情感分析;如果后续想往多模态情感分析方向扩展,可以并行采集微博配图做图像情绪识别,把图像情绪分数和文本概率做加权融合,系统性误差通常还能再降一点。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询