
简介面向计算机、通信、人工智能、自动化等相关专业的高校学生、教师及从业者这份基于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, page1): url https://m.weibo.cn/api/container/getIndex params { containerid: f100103type1q{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, paramsparams, headersheaders, timeout10) 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(新能源汽车, pagepage) for row in rows: print(row) time.sleep(2)这段代码的逻辑是通过containerid参数指定搜索关键词page_typesearchall表示综合搜索分类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(rhttps?://\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 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;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_features5000, ngram_range(1, 2), min_df2, max_df0.95, sublinear_tfTrue ) X vectorizer.fit_transform(docs)参数的意义要理解清楚max_features限制特征维度防止词表过大导致矩阵稀疏和训练变慢ngram_range设为(1, 2)除了单个词还把相邻两个词的组合也作为特征这样不好会被识别为独立特征而不是被好完全覆盖min_df2表示只在1篇文档中出现过的词直接丢弃max_df0.95表示超过95%文档都出现的词视为停用词。sublinear_tfTrue对词频做对数压缩避免高频词主导相似度计算。TF-IDF是词袋模型丢失了词序和语义。如果想要更丰富的语义表达可以训练Word2Vec词向量。from gensim.models import Word2Vec sentences [cut_words(d) for d in docs] # 每篇文档是分词后的词列表 w2v_model Word2Vec( sentences, vector_size128, window5, min_count3, sg1 )sg1表示使用skip-gram训练方式在中小规模语料上比CBOW能学到更准确的词义vector_size128控制向量维度太小了区分度不够太大了训练时间明显增加min_count3过滤掉出现次数少于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. 情感分类模型训练与调参从朴素贝叶斯到TextCNN4.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_size0.2, random_state42, stratifylabels )random_state固定下来保证每次实验划分一致调参时可复现stratifylabels让正负样本在训练集和测试集中的分布与全量数据一致防止某个类别在测试集里占比过高导致评估失真。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_features6000, ngram_range(1, 2))), (clf, MultinomialNB(alpha0.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这种非负计数型特征alpha0.5是拉普拉斯平滑系数调低可以让概率估计更贴近真实分布避免在稀疏特征上过分平滑。如果换成线性SVM需要用LinearSVC而不是SVC(kernelrbf)因为线性核在高维文本特征上训练快、效果也不差。from sklearn.svm import LinearSVC svm_model LinearSVC(C1.0, class_weightbalanced)class_weightbalanced会自动根据类别频率调整权重正负样本比例如果偏差较大这一项能让模型不偏向多数类。线性SVM的C是正则化强度的倒数C越小正则化越强默认C1.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_lengthMAX_LEN), Conv1D(NUM_FILTERS, 3, activationrelu), GlobalMaxPooling1D(), Dropout(0.5), Dense(2, activationsoftmax) ]) model.compile(optimizeradam, losssparse_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_split0.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(host0.0.0.0, port5000)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_LEN64线上一条长微博可能超过这个长度直接截断会丢掉后半段的情感转折前面说味道不错后面接但服务太差截断就全偏了。import re def predict_long_text(text, model, max_len64): 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)按标点把长文本切成短句逐句预测后取平均概率比直接截断更合理。这个切片策略同样适用于新闻长文或商品长评论的情感分析如果后续想往多模态情感分析方向扩展可以并行采集微博配图做图像情绪识别把图像情绪分数和文本概率做加权融合系统性误差通常还能再降一点。本文还有配套的精品资源点击获取