
简介这份资料面向希望用Python开展中文文本情感分析与舆情监测的初学者和课程实验者核心是用百度云API对微博评论逐条计算情感得分再将得分标准化为可解释的倾向结果帮助读者理解从原始评论文本到积极或消极判断的完整链路。资源包共15个文件以6个xml、3个py、2个xls和1个csv为主xml多用于IDE与项目配置py脚本承担接口调用和得分处理xls与csv则提供评论数据及结果整理参考压缩包约421KB体量轻便便于本地复现。已有203人学习下载说明其在情感计算入门场景中具有一定参考价值。读者可据此对照评论数据结构、API返回字段与标准化思路快速搭建微博评论情感分析小项目也可迁移到品牌口碑分析、公共事件舆论监测或个人研究等场景。1. 从一条差评说起用云 API 做评论情感偏向分析能落到什么程度运营同学导出一批社交平台短评论几千条里真正需要立刻处理的负面反馈可能只有几十条靠人眼翻到半夜很容易漏。这个项目解决的就是这类场景用 Python 把评论数据读进来、清洗成短文本再调用云厂商的情感分析 API把每条评论打成正面、负面、中性并附一个置信度。它适合做舆情初筛、课程作业和运营日报不适合直接替代人工判断更不适合拿来做严谨的学术结论。真正有价值的地方不是“判断一句话好坏”而是把评论数据变成可排序、可聚合、可复查的情感计算流水线。2. 技术栈与目录骨架Python 脚本如何串起评论采集、清洗和情感计算这份项目的重心不在训练模型而在工程串联。标题里点的是云 API落到代码里就是一组 HTTP 请求Python 负责读评论、清洗文本、控制调用节奏、保存结果云厂商的情感分析接口负责输出情感倾向和置信度。常见组合是requests发请求、pandas管表格、re做正则清洗报告层再按需补jieba做关键词、matplotlib或pyecharts出图。这个组合不炫技但足够把“评论数据 → 情感标签 → 负面率趋势”跑通。2.1 为什么用云 API 而不是本地训模型评论情感分析听起来像个 NLP 任务真到落地时大多数团队没有标注数据、没有 GPU 资源也没有持续维护模型的精力。自己训一个情感分类器最难的往往不是写训练脚本而是定义标签体系什么叫负面“质量一般”算不算“不是不好用”到底是正还是负这些边界不统一模型训出来也只是把标注人的分歧固化下来。云 API 的优势是开箱即用能把第一版流程快速跑起来特别适合先验证业务价值。但云 API 不是免费的午餐。调用量、QPS、超时、返回字段差异、数据出域都是要提前算清楚的账。我的习惯是先把评论按来源和时间分层抽 500 到 1000 条做小批量试跑看看接口对反讽、否定句、表情符号的敏感度再决定要不要全量。公开评论数据还要做脱敏用户昵称、主页链接、手机号这类字段不进分析表避免把情感计算做成隐私收集。技术栈可以压成一张表看模块输入输出常见实现评论导入CSV、接口导出文件、人工整理表raw_comments.csvpandas.read_csv文本清洗原始评论正文clean_comments.csvre、pandas情感计算清洗后的短文本scored_comments.csvrequests调云 NLP 接口汇总分析情感标签、置信度、时间字段负面率、趋势、关键词pandas、可视化库这张表的意义是划边界清洗层不关心接口怎么鉴权接口层不关心评论从哪来报告层只读结果表。后面换模型、换云厂商、加人工复核都只动一层不至于牵一发动全身。2.2 目录结构与模块边界从 raw_comments.csv 到 scored_comments.csv我一般会把这类项目拆成下面这种目录。名字不花哨但每一层都能单独跑、单独测出问题也容易定位。comment_sentiment/ ├── config.py # 读取环境变量不把密钥写进代码 ├── collect/ │ └── import_comments.py # 导入 CSV 或接口导出数据 ├── clean/ │ └── clean_text.py # 链接、、话题、表情、重复标点清洗 ├── analysis/ │ ├── sentiment_api.py # 单条打分、重试、字段归一 │ └── batch_run.py # 批量跑数、断点续跑、结果落盘 ├── data/ │ ├── raw_comments.csv # 原始评论只读不改 │ ├── clean_comments.csv # 清洗后样本 │ └── scored_comments.csv # 情感结果表 ├── report/ │ └── make_report.py # 负面率、趋势、关键词汇总 └── requirements.txtraw_comments.csv是原始层尽量只追加不改写后面发现清洗规则错了还能回放。clean_comments.csv是中间层保留comment_id、clean_text、created_at、interaction_count这些字段方便按时间切片和按互动量排序。scored_comments.csv是结果层除了情感标签还要留confidence、model_version、content_hash、error_msg否则接口一升级你连哪批结果对不上都查不出来。模块边界要守住一条clean_text.py不读环境变量sentiment_api.py不直接读原始 CSVmake_report.py不重新调接口。这样你换云厂商时只改sentiment_api.py里的字段映射换清洗规则时只重跑clean_comments.csv报告层始终读同一张结果表。项目不大但这种分层能省掉很多“改一处、崩三处”的返工。3. 评论数据清洗把短文本变成情感接口能吃的样本短评论的噪声比长文本高得多。一条评论里可能同时有链接、、话题标签、表情、重复感叹号还有“哈哈哈哈”“绝了”“无语子”这类依赖语境的表达。清洗的目标不是把文本洗得干干净净而是去掉会干扰情感判断的噪声同时保留语气和否定词。清洗过度比清洗不足更麻烦因为接口拿到空文本只会返回中性或报错最后你看到的情感分布是假的。3.1 字段设计评论 ID、正文、时间、互动量怎么留原始表最少留五类字段comment_id用来去重和追溯content是原始正文created_at用来做时间趋势interaction_count用来排序和分层采样source用来区分不同来源。不要一上来把用户昵称、头像链接、主页地址带进分析表这些字段对情感计算没有帮助反而增加脱敏成本。字段示例用途comment_idc_10086去重、回查原始评论content这个版本真的绝了更新完直接卡死原始文本保留不改created_at2024-06-01 12:30:00按天、按周聚合interaction_count128分层采样、热度排序sourcetopic_a区分话题或采集批次字段设计里最容易忽略的是content_hash。同一句话可能被转发多次comment_id不同但文本完全一样调接口会重复计费。我的做法是在清洗层就算一个文本哈希后面批量跑数先查哈希命中就跳过。这个字段不占地方但能省下真金白银。3.2 清洗函数逐段拆链接、、话题、表情和重复标点下面这段清洗代码可以直接抄核心是“先删明显噪声再处理重复标点最后去空”。正则不要贪多每加一条规则都拿 20 条样本回放这是血泪经验。import re import pandas as pd URL_RE re.compile(rhttps?://\S|www\.\S) AT_RE re.compile(r[\w\u4e00-\u9fa5_-]) TOPIC_RE re.compile(r#([^#])#) EMOJI_RE re.compile( [\U0001F300-\U0001FAFF\U00002600-\U000027BF], flagsre.UNICODE, ) SPACE_RE re.compile(r\s) def clean_comment(text: str, keep_topic: bool False) - str: if not isinstance(text, str): return # 链接对情感判断没有帮助直接替换成空格 text URL_RE.sub( , text) # 某人会打断句子保留用户昵称反而会污染分词 text AT_RE.sub( , text) # 话题标签看场景保留做话题分析时留词做纯情感判断时删掉 if keep_topic: text TOPIC_RE.sub(r\1, text) else: text TOPIC_RE.sub( , text) # 表情符号在短评论里情绪很强但多数云接口不稳定先清掉 text EMOJI_RE.sub( , text) # 连续感叹号、问号、逗号压成一个保留语气但降低噪声 text re.sub(r([!?。,.~])\1{2,}, r\1, text) text SPACE_RE.sub( , text).strip() return text def load_and_clean(path: str) - pd.DataFrame: df pd.read_csv(path, encodingutf-8-sig) df df[[comment_id, content, created_at, interaction_count]].copy() df[clean_text] df[content].map(clean_comment) # 少于 3 个字符的文本基本没有情感信息先过滤掉 df df[df[clean_text].str.len() 3] # 先按评论 ID 去重再按清洗后文本去重挡住转发和复制粘贴 df df.drop_duplicates(subset[comment_id]) df df.drop_duplicates(subset[clean_text]) return dfkeep_topic这个参数值得单独说。默认False时#话题#会被整个删掉适合纯情感判断做话题对比时改成True只保留话题词本身去掉井号。EMOJI_RE的范围覆盖常见表情但不可能百分百全遇到漏网的表情可以补充范围不要为了一个表情写十几条正则。SPACE_RE放在最后避免前面替换出来的多个空格进入分词器。load_and_clean里先按comment_id去重再按clean_text去重这个顺序不要反。先按文本去重会把同一用户在不同时间的重复评论误删先按 ID 去重能保留真实的多条表达再处理复制粘贴。过滤少于 3 个字符的文本也要留日志记录被丢掉的条数和原因否则报告里负面率突然变高你连样本怎么少掉的都不知道。清洗完不要急着全量调接口。抽 30 条打印content和clean_text对照重点看否定句、反讽句、带表情的短句有没有被洗坏。比如“不是不好用是根本不能用”被洗成“不是不好用 是根本不能用”否定词还在接口大概率还能判但如果把“不”当成噪声删掉情感方向直接反转后面调阈值也救不回来。4. 情感 API 调用封装鉴权、限流、重试和结果落库接口封装层是整个项目最容易被低估的部分。很多人写个requests.post循环就开跑跑到一半超时、限流、字段对不上结果表里一半是空的。我的习惯是把单条打分和批量调度分开单条函数只负责一次请求和字段归一批量函数负责去重、断点、落盘。这样接口返回什么奇怪结构都在单条函数里消化掉不污染上层。4.1 单条打分函数把不同云厂商返回统一成三类不同云厂商的返回字段名不一样有的叫sentiment有的叫label有的把正面负面藏在score正负号里。封装层不要跟着厂商字段走统一成sentiment、confidence、error_msg、raw_json四个字段后面换接口只改映射。import hashlib import json import os import time import pandas as pd import requests API_URL os.getenv(SENTIMENT_API_URL, https://api.example.com/v1/sentiment) API_KEY os.getenv(SENTIMENT_API_KEY, ) TIMEOUT 8 MAX_RETRY 3 QPS_SLEEP 0.25 def text_hash(text: str) - str: return hashlib.md5(text.encode(utf-8)).hexdigest() def score_one(text: str) - dict: headers { Authorization: fBearer {API_KEY}, Content-Type: application/json, } payload {text: text, lang: zh} last_error for attempt in range(1, MAX_RETRY 1): try: resp requests.post( API_URL, headersheaders, jsonpayload, timeoutTIMEOUT, ) # 429 表示限流退避后再试不要立刻打死接口 if resp.status_code 429: time.sleep(QPS_SLEEP * (2 ** attempt)) continue resp.raise_for_status() data resp.json() return { sentiment: data.get(sentiment, unknown), confidence: float(data.get(confidence, 0)), error_msg: , raw_json: json.dumps(data, ensure_asciiFalse), } except Exception as exc: last_error str(exc) time.sleep(QPS_SLEEP * attempt) return { sentiment: error, confidence: 0.0, error_msg: last_error, raw_json: , }API_URL和API_KEY从环境变量读不写进代码这是底线。TIMEOUT设 8 秒是常见起点评论短文本一般够用网络抖动严重可以调到 10 秒但不要无限等。MAX_RETRY3配合指数退避能扛住短时限流如果接口持续 429说明 QPS 超了应该降并发或加队列而不是把重试次数调到 10 次。resp.raise_for_status()不能省。有些接口限流时返回 200 但 body 里带错误码这种情况要在字段映射时判断data.get(error_code)别把错误当情感结果写进表。raw_json建议保留出问题时能回看原始返回相当于给接口调用留个黑匣子。注意confidence不是准确率只是模型对当前结果的置信程度阈值要拿人工标注集调不能拍脑袋定 0.8。4.2 批量跑数与断点续跑别让一次超时毁掉整批任务批量层要解决三个问题重复跑不重复计费、失败任务能重试、结果能增量落盘。下面这段用content_hash做幂等已经成功的结果跳过错误行保留用于排查。def run_batch(clean_path: str, out_path: str) - None: df pd.read_csv(clean_path) done set() if os.path.exists(out_path): old pd.read_csv(out_path) # 只把成功结果视为已完成error 行留给下一轮重试 done set( old.loc[old[sentiment] ! error, content_hash].dropna() ) for _, row in df.iterrows(): h text_hash(row[clean_text]) if h in done: continue result score_one(row[clean_text]) record { comment_id: row.get(comment_id, ), clean_text: row[clean_text], content_hash: h, sentiment: result[sentiment], confidence: result[confidence], error_msg: result[error_msg], raw_json: result[raw_json], scored_at: pd.Timestamp.now().strftime(%Y-%m-%d %H:%M:%S), } pd.DataFrame([record]).to_csv( out_path, modea, headernot os.path.exists(out_path), indexFalse, encodingutf-8-sig, ) if result[sentiment] ! error: done.add(h) time.sleep(QPS_SLEEP) if __name__ __main__: run_batch(data/clean_comments.csv, data/scored_comments.csv)modea是增量追加headernot os.path.exists(out_path)保证第一次写表头、后面不重复写。scored_at记录打分时间后面接口升级或模型换版本能按时间切分新旧结果。错误行不加入done下一轮会重试但会在结果表里留下多条错误记录跑完后按content_hash去重、保留最新一条即可。结果表建议至少保留这些字段字段含义为什么要留comment_id原始评论 ID回查原文clean_text清洗后文本复查清洗规则content_hash文本哈希幂等、去重、断点sentiment正面/负面/中性核心结果confidence置信度阈值过滤、人工抽检error_msg错误信息排查超时、限流、字段错raw_json原始返回字段映射出问题时回看scored_at打分时间区分批次和版本提示接口返回的confidence不是准确率只是模型对当前结果的置信程度阈值要拿人工标注集调。批量跑数时不要一上来开多线程。云接口的 QPS 限制往往按账号算你开 20 个线程最后就是 20 个线程一起等 429。先用单线程加QPS_SLEEP跑 500 条观察平均耗时和错误率再决定要不要上小并发。并发上来后每个线程单独重试、单独记录错误别让一个线程的异常把整批任务带崩。5. 避坑与排查评论情感分析最容易翻车的 5 个现场情感分析项目的坑一半在数据一半在接口。下面这 5 个现场是我在类似项目里踩过的现象看起来都像“模型不准”真正的原因往往在采样、清洗和调用策略上。5.1 负面比例忽高忽低先查样本不要先调阈值现象同一批评论早上跑出来负面率 18%下午重跑变成 31%接口版本没变代码也没改。原因两次跑的样本范围不一样或者清洗后空文本被过滤掉剩下的样本天然偏负面。还有一种情况是接口对表情符号的处理不稳定带表情的评论被删掉后语气强的短句反而更容易被标成负面。解决每次跑数先固定样本快照记录总条数、清洗后条数、过滤条数、空文本条数。负面率变化超过 5 个百分点先对比两次的comment_id集合再看被过滤样本里正面和负面的分布。阈值不要背锅样本变了阈值调多少都没用。5.2 跑几百条后大量超时QPS 和重试策略要一起改现象前 200 条跑得挺顺后面开始大量timeout和429结果表里error行越来越多。原因单线程跑太快或者重试没有退避接口一限流就立刻重试越重试越堵。也有可能是TIMEOUT设得太短短文本接口在高峰期 3 秒内返回不了。解决先降速把QPS_SLEEP从 0.25 调到 0.5 或 1 秒再把重试改成指数退避429时至少等 2 秒、4 秒、8 秒。错误行单独导出跑完后按content_hash补跑不要在主循环里无限重试。并发不是越高越好云接口的配额通常按账号算开太多线程只会让错误率更高。5.3 清洗后全是空文本正则删得太狠现象clean_comments.csv行数比原始表少了一大半剩下的很多是空字符串接口返回大量中性。原因链接、、话题、表情的正则范围写得太宽把正常词也吃掉了或者先删表情再删标点短评论直接变成空。还有一种低级错误把content列当成clean_text写回覆盖了原始文本。解决清洗函数加单元测试拿“不是不好用”“哈哈哈哈”“#话题# 真的绝了”这几类样本回放。空文本不要静默丢弃输出dropped_reason字段记录是被链接规则、表情规则还是长度规则过滤的。原始content永远保留清洗结果写新列不要覆盖。5.4 结果和平台热评观感不一致采样偏差在作怪现象报告显示负面率只有 8%但打开社交平台一看热评里一半在吐槽。原因采集时只抓了热门评论或者按互动量排序取了前 1000 条。热门评论里情绪强的表达更容易被顶上来但你的样本没有覆盖长尾另一种情况是转发的重复文本被去重后负面声音被稀释。解决按时间、互动量、来源做分层采样热门池和长尾池分别算负面率。报告里写清楚采样口径是“全量公开评论”还是“热门评论前 1000 条”。如果只看趋势固定每天同一时间段、同一采样规则别今天抓热门、明天抓最新两个口径混在一起负面率一定忽上忽下。5.5 重复跑重复计费没有 content_hash 做幂等现象同一批评论跑了两遍接口账单翻倍结果表里出现重复行。原因批量脚本每次从头跑没有读已有结果或者用comment_id去重但转发和复制粘贴的文本 ID 不同接口照样重复调用。解决清洗层生成content_hash批量层先读结果表里sentiment ! error的哈希集合命中就跳过。结果表按content_hash建唯一索引或跑完后去重。错误行保留但不要当成成功结果下一轮补跑时只补错误和新增样本。6. 二次开发把一次性脚本改成可复用的评论洞察流水线6.1 三个改造点策略接口、任务队列、版本字段二次开发时我一般先改三处。第一把score_one抽成策略接口今天用云 API明天换本地模型批处理层不动。第二把run_batch换成任务队列按content_hash分片失败任务单独重试别让一个超时卡住整批。第三给结果表补model_version、scored_at、sample_batch接口一升级能按版本回滚报告里也能标注“这批数据用的是哪版模型”。6.2 验证与收尾先人工标注 100 条再跑全量验证方法别只看总体准确率。抽 100 条人工标正面、负面、中性分别算负面召回和中性误判。低于 0.75 先查否定句和反讽不要急着调阈值。还有一个反直觉技巧先跑 500 条做分布画像再决定要不要全量评论量大的项目10% 分层样本就够看趋势。如果要做周报把sentiment按created_at重采样负面率按周看比单日波动可靠。从那以后我每次接情感接口都先用 100 条人工标注做守门测试再跑全量。需要复现的话完整脚本、配置模板和脱敏样例评论都在下载包里照着目录跑就行。希望帮到你。本文还有配套的精品资源点击获取