简介:这份资料面向希望用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.csv | pandas.read_csv |
| 文本清洗 | 原始评论正文 | clean_comments.csv | re、pandas |
| 情感计算 | 清洗后的短文本 | scored_comments.csv | requests调云 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不直接读原始 CSV,make_report.py不重新调接口。这样你换云厂商时,只改sentiment_api.py里的字段映射;换清洗规则时,只重跑clean_comments.csv;报告层始终读同一张结果表。项目不大,但这种分层能省掉很多“改一处、崩三处”的返工。
3. 评论数据清洗:把短文本变成情感接口能吃的样本
短评论的噪声比长文本高得多。一条评论里可能同时有链接、@、话题标签、表情、重复感叹号,还有“哈哈哈哈”“绝了”“无语子”这类依赖语境的表达。清洗的目标不是把文本洗得干干净净,而是去掉会干扰情感判断的噪声,同时保留语气和否定词。清洗过度比清洗不足更麻烦,因为接口拿到空文本只会返回中性或报错,最后你看到的情感分布是假的。
3.1 字段设计:评论 ID、正文、时间、互动量怎么留
原始表最少留五类字段:comment_id用来去重和追溯,content是原始正文,created_at用来做时间趋势,interaction_count用来排序和分层采样,source用来区分不同来源。不要一上来把用户昵称、头像链接、主页地址带进分析表,这些字段对情感计算没有帮助,反而增加脱敏成本。
| 字段 | 示例 | 用途 |
|---|---|---|
comment_id | c_10086 | 去重、回查原始评论 |
content | 这个版本真的绝了,更新完直接卡死 | 原始文本,保留不改 |
created_at | 2024-06-01 12:30:00 | 按天、按周聚合 |
interaction_count | 128 | 分层采样、热度排序 |
source | topic_a | 区分话题或采集批次 |
字段设计里最容易忽略的是content_hash。同一句话可能被转发多次,comment_id不同但文本完全一样,调接口会重复计费。我的做法是在清洗层就算一个文本哈希,后面批量跑数先查哈希,命中就跳过。这个字段不占地方,但能省下真金白银。
3.2 清洗函数逐段拆:链接、@、话题、表情和重复标点
下面这段清洗代码可以直接抄,核心是“先删明显噪声,再处理重复标点,最后去空”。正则不要贪多,每加一条规则都拿 20 条样本回放,这是血泪经验。
import re import pandas as pd URL_RE = re.compile(r"https?://\S+|www\.\S+") AT_RE = re.compile(r"@[\w\u4e00-\u9fa5_-]+") TOPIC_RE = re.compile(r"#([^#]+)#") EMOJI_RE = re.compile( "[\U0001F300-\U0001FAFF\U00002600-\U000027BF]", flags=re.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, encoding="utf-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": f"Bearer {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, headers=headers, json=payload, timeout=TIMEOUT, ) # 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_ascii=False), } 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_RETRY=3配合指数退避,能扛住短时限流;如果接口持续 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, mode="a", header=not os.path.exists(out_path), index=False, encoding="utf-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")mode="a"是增量追加,header=not 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 条人工标注做守门测试,再跑全量。需要复现的话,完整脚本、配置模板和脱敏样例评论都在下载包里,照着目录跑就行。希望帮到你。
本文还有配套的精品资源,点击获取