☰
微博评论爬虫与情感分析实战:从接口采集到模型校准
2026/10/11 12:30:59 网站建设 项目流程

简介:这是一份面向微博评论采集与分析的Python实战资源包,项目名称为weibo-comment-crawler-master,涵盖爬虫、数据存储、文本分词与情感分析多个环节,适合正在学习爬虫技术、自然语言处理或从事微博舆情分析的学习者参考。压缩包内共有17个文件,包体约465KB,以2个Python脚本(评论爬虫与情感分析)、8个txt辅助文件(停用词、否定词、程度词、正负向情感词典等)、1个CSV微博评论样例、建表SQL说明及README文档为主体,结构清晰,便于直接运行与二次开发。资源覆盖了requests模拟登录、BeautifulSoup网页解析、cookie/session处理、验证码识别思路,以及pandas+MySQL存储、nltk/jieba分词、SnowNLP情感评分等关键知识点,并附带情感阈值判定与统计思路,可帮助快速上手完整分析流程。目前已有1328人学习,适合希望用小体量项目快速理解微博评论抓取与情感分析全链路的学习者。

1. 微博评论爬虫到底解决了什么问题:从一个真实研究需求说起

做微博分析的人,十个里有九个卡在第一步:评论拿不到。想给某个品牌的新品发布会做舆情复盘,或者写毕业论文研究公众对某个话题的情绪走向,人工截图几百条还行,想拿三万条做情感分析,没有 weibo-comment-crawler 这类评论爬虫工具几乎不可能。这个方向解决的正是“如何稳定、完整地把微博评论区变成结构化数据”,再往下接微博分析和评论情感分析,才是真正出结论的地方。适合谁?有一定 Python 基础、想用真实社交数据做研究的同学,或者公司里要做竞品口碑监控的工程师。不值得一开始就追求分布式爬虫,先把单机跑通,比什么都实在。

2. 先把数据管道打通:登录态、接口参数与微博评论的第一行代码

2.1 为什么不用网页版 HTML 解析,而是走移动端接口

最早做微博爬虫的教程大多教人用 requests 抓网页版 HTML,再用 xpath 去提取评论节点,甚至专门去处理 text() 函数拿文本。这个路线今天依然能跑,但非常难受:网页版评论区是异步加载的,翻页要走额外的 ajax 接口,某些 class 名隔几个月就变一次,你的 xpath 表达式会突然失效,而且 HTML 里夹带的 emoji 和 @用户名很难清洗干净。

我一般直接走微博移动端的评论接口。它返回的是 JSON,字段结构三十年不变(夸张了点,但确实稳定),评论内容、用户信息、点赞数、发布时间全部是独立字段,爬虫逻辑可以写得很干净。另一个好处是移动端接口对 cookie 的校验比网页版宽松,未登录也能拿到一部分数据,但完整度会打折扣。如果你的研究需要全量评论,登录态还是躲不开。

2.2 请求参数:cookie、id、page 到底怎么凑齐

先看一个最基础的请求。以 m.weibo.cn 的评论接口为例,它的核心参数就三个:微博 id、页码、cookie。

import requests def fetch_comments(weibo_id, cookie_value, page=1): # 微博移动端评论接口,返回 JSON,比解析 HTML 稳定得多 url = f"https://m.weibo.cn/api/comments/show?id={weibo_id}&page={page}" headers = { "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) " "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1", "Referer": f"https://m.weibo.cn/detail/{weibo_id}", "Cookie": cookie_value, } resp = requests.get(url, headers=headers, timeout=10) resp.raise_for_status() return resp.json()

这段代码的注意点有几个。weibo_id 不是微博分享链接里那串带字母的 bid,而是数字 id,可以从网页版微博详情页的>import time import random def crawl_comments(weibo_id, cookie_value, max_pages=50): all_items = [] session = requests.Session() session.headers.update({ "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) " "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1", "Cookie": cookie_value, }) for page in range(1, max_pages + 1): data = fetch_comments(weibo_id, cookie_value, page) if data.get("ok") != 1: print(f"第 {page} 页返回异常: {data.get('msg')}") break items = data["data"]["list"] if not items: break all_items.extend(items) # 单账号限速,1.5 到 3 秒随机延迟是底线 time.sleep(random.uniform(1.5, 3.0)) return all_items

这里有两个参数值得你根据实际情况调整。max_pages 是最大页数,用来保护你不小心跑出超长循环;如果你确实需要全量评论,可以用 total_number 除以 20 算出理论页数,但我会建议你先跑到 100 页试试水,观察会不会触发频控。sleep 的随机范围是血泪经验:固定 2 秒的延迟会让风控识别出机器节奏,随机化之后存活率高很多。还有一个细节:尽量用同一个 requests.Session 实例去发请求,它可以复用底层的 TCP 连接,减少握手次数,对降低被风控的概率有帮助。

2.4 单机爬虫足够了:为什么先别上分布式

很多初学者一上来就搜"分布式爬虫",觉得单机跑太慢。实测微博评论接口的瓶颈根本不在带宽,而在账号频控。一个普通账号每秒发一个请求,连续跑十几分钟就可能被要求重新登录。你就算部署十台机器、换十个代理,只要账号被限制,全部白搭。爬虫工具箱里真正该囤的是 cookie 池和限速策略,不是机器数量。把单机链路跑稳,再考虑要不要横向扩展。

3. 从脏评论到有效语料:HTML 清洗、嵌套回复与 SQLite 存储

3.1 评论正文里的脏东西:表情、标签和 @用户

接口返回的 text 字段不是纯文本,而是一段 HTML 片段。实战里你经常能看到这样的字符串:<span class="surl-text">哈哈哈哈</span><a href='/n/某用户'>@某用户</a>。如果直接拿去做情感分析,模型会学进一堆标签噪声。我常用的清洗流程是先反转义、再剥标签、再去 URL、再去 @用户名,最后处理 emoji。

import re import html import emoji def clean_comment_text(raw): # 第一步:反转义,把 &amp; 这类实体还原成 & text = html.unescape(raw) # 第二步:去掉所有 HTML 标签,只留文本内容 text = re.sub(r"<[^>]+>", "", text) # 第三步:去掉超链接,很多垃圾评论会带推广链接 text = re.sub(r"https?://\S+", "", text) # 第四步:去掉 @用户名,保留"回复"语义即可 text = re.sub(r"@[\u4e00-\u9fa5\w]+:?", "", text) # 第五步:emoji 替换成空字符,你也可以选择保留 text = emoji.replace_emoji(text, "") return text.strip()

逻辑说明:html.unescape 必须放在去标签之前,否则&lt;这类实体被还原成<后,你下一轮正则可能误伤。去掉 @用户名时注意,有些研究需要分析用户互动关系,那这步就要跳过。emoji 是否保留取决于你的分析目标:做情感分析时,笑哭😂和狗头🐶其实是重要的情感信号,我一般会先替换成[笑哭]这种占位符再做情感判断,但如果你只想做词频统计,直接删除更省事。

3.2 嵌套回复:root_id 才是评论归属的真相

评论区里大量存在"楼中楼",也就是某条评论下的子回复。接口返回的 list 里,主评论和子回复都混在一起,区分它们的字段是 root_id。如果 root_id 为空字符串,说明这是一条顶级评论;如果非空,说明它挂在某条顶级评论下面。很多人爬完数据后一统计,发现"评论总数"比 total_number 多,就是因为把子回复也算进去了。

这不算 bug,但你要在存储阶段想清楚:做情感分析时,子回复也包含完整观点,可以纳入;做受众画像时,子回复的发布者往往和主评论作者不是同一批人,建议分开统计。我的习惯是在数据表里单独存 root_id,不做清洗删除,这样后续分析时进退自如。

3.3 时间字段的玄学:从"刚刚"到时间戳的换算

接口里的 created_at 字段返回的是相对时间,比如"刚刚"、"3 分钟前"、"2 小时前"、"昨天 12:30",偶尔也有完整的"2024-05-20"。这个字段直接排序会乱套,必须换算成 Unix 时间戳。换算函数不复杂,但边界条件很多,尤其是跨天和跨月:

from datetime import datetime, timedelta import re def parse_weibo_time(value, now=None): if not value: return None now = now or datetime.now() value = value.strip() if value == "刚刚": return int(now.timestamp()) m = re.match(r"(\d+)分钟前", value) if m: return int((now - timedelta(minutes=int(m.group(1)))).timestamp()) m = re.match(r"(\d+)小时前", value) if m: return int((now - timedelta(hours=int(m.group(1)))).timestamp()) m = re.match(r"昨天 (\d{2}:\d{2})", value) if m: dt = now - timedelta(days=1) hh, mm = map(int, m.group(1).split(":")) return int(dt.replace(hour=hh, minute=mm, second=0, microsecond=0).timestamp()) # 兜底:完整日期格式 try: return int(datetime.strptime(value, "%Y-%m-%d %H:%M:%S").timestamp()) except ValueError: return None

注意一个坑:如果爬虫跑在凌晨,"昨天 23:00" 这个时间在跨日之后可能实际上对应的是前天的 23:00,但接口返回的语义就是你眼中的昨天。对于大多数情感分析场景,误差一两个小时无伤大雅,如果你要做严格的时间序列分析,建议在爬取的同时记录请求发出的本地时间,用那个时间作为 now 的基准,而不是事后批量处理时统一取当前时间。

3.4 存储设计:一张表放下评论与用户上下文

爬下来的数据建议直接进 SQLite,别存 CSV。CSV 在几万条规模下还能忍,一旦你后续要做情感分析批量读取,反复读写 CSV 的 IO 开销会让你怀疑人生。建表语句如下:

CREATE TABLE IF NOT EXISTS comments ( id TEXT PRIMARY KEY, weibo_id TEXT NOT NULL, user_id TEXT, user_name TEXT, text TEXT, created_at TEXT, ts INTEGER, like_count INTEGER DEFAULT 0, root_id TEXT DEFAULT '', source TEXT ); CREATE INDEX IF NOT EXISTS idx_weibo_id ON comments(weibo_id); CREATE INDEX IF NOT EXISTS idx_ts ON comments(ts);

插入时用 executemany 批量提交,不要一条一条 commit,否则三万条评论够你等半小时。source 字段我用来记录这次数据来自哪个接口或哪次任务,后面做多轮爬取对比时非常有用。再说一个容易忽略的点:text 字段要存清洗前还是清洗后?我的习惯是清洗后的文本存进正式字段,但原始文本也保留一列或者落一份 JSON 备份。原因很简单:清洗规则有 bug 时你还有后悔药,否则重爬一次的成本太高。

4. 给评论做情感分析:从预训练模型到可用的微博语料校准

4.1 直接调 SnowNLP 会翻车:先认清它的语料出身

拿到干净的评论文本后,最省事的做法是直接用 SnowNLP 的 sentiments 属性打情感分,0 到 1 之间,接近 0 是负面,接近 1 是正面。一段简单的代码长这样:

from snownlp import SnowNLP text = "这个产品真的好用,已经推荐给三个朋友了" score = SnowNLP(text).sentiments print(score) # 通常会在 0.8 以上

这个库跑购物评论还行,但直接拿到微博语境上会翻车。原因在于它预训练用的语料偏电商评价,"物流很快"它能识别成正面,但微博上常见的"哈哈哈哈哈哈""谁懂啊""家人们谁懂"这种表达,它的判断非常飘。更麻烦的是反讽,"太好了,又加班到十点"它很可能打出高分。所以我把 SnowNLP 只当成基线模型,真正落地要看 4.2 的自训练校准。

4.2 用标注语料训练自己的情感模型:操作步骤

常见做法是从爬到的评论里随机抽 1000 到 2000 条,人工标注成正面和负面两类,然后喂给 SnowNLP 的 sentiment.train 重新训练。注意训练时只需要正负两类,中性评论可以后面用阈值区间兜住。

from snownlp import sentiment def train_weibo_sentiment(pos_path, neg_path, save_path): train_data = [] # 正样本文件每行一条评论,标注为 1 with open(pos_path, encoding="utf-8") as f: for line in f: line = line.strip() if line: train_data.append((line, 1)) # 负样本文件每行一条评论,标注为 0 with open(neg_path, encoding="utf-8") as f: for line in f: line = line.strip() if line: train_data.append((line, 0)) # 训练并保存模型 sentiment.train(train_data) sentiment.save(save_path) # 生成 .marshal 文件

训练完成后,在预测前先加载你训练好的模型:

from snownlp import sentiment sentiment.load("weibo_sentiment.marshal")

然后批量预测时,还是调用 SnowNLP(text).sentiments,不过内部用的已经是你的模型。这里最关键的一个参数是训练语料的比例。我试过的经验是:正负样本数量千万别差太多,最好控制在 1:1 到 1.2:1 之间,否则模型会倾向输出样本多的一方,导致你的情感分布整体偏高或偏低。

4.3 标注策略与准确率验证:别在标注上偷懒

标注是个体力活,但值得做。1000 条标注大概占用一个下午,换来的是模型对微博语境的适配。标注的时候我一般让两个人各标一遍,再对比不一致的条目,这样单人的主观偏差能被摊薄。之后留出 20% 的标注数据做验证,算一下准确率:

from sklearn.metrics import accuracy_score, classification_report # y_true 是人工标注结果,y_pred 是模型预测结果 # 预测时以 0.5 为阈值:得分 >= 0.5 判定为正面 y_true = [1, 0, 1, 1, 0, 0, 1, 0] y_pred = [1, 0, 0, 1, 0, 1, 1, 1] print("准确率:", accuracy_score(y_true, y_pred)) print(classification_report(y_true, y_pred, target_names=["负面", "正面"]))

准确率不是越高越好,关键是看混淆矩阵里哪一类错得多。微博场景里,我见过最多的错误是把"阴阳怪气"的负面评论判成正面。如果验证集里这类错误超过 10%,说明你的负样本里反讽表达太少,需要专门补充一批"看起来像夸、其实是骂"的语料,比如"真的是太棒了,居然能卡成这个样子"。

4.4 阈值调整:0.4 到 0.6 之间留一个"不确定"区间

模型给出的情感分是连续值,但业务上你往往需要离散标签。我的做法是设置双阈值:得分大于等于 0.6 判为正面,小于等于 0.4 判为负面,中间区域标为"中性/不确定"。这个区间不是拍脑袋定的,你可以在验证集上扫描不同阈值组合,找到让正负面 F1 最高的切分点。还有一个实用的补充:对处于中间区间的评论,再用关键词规则做二次判断,比如出现"绝了""醉了""无语"时直接降一档,这样比纯模型更稳。

5. 爬虫避坑与排查:cookie 过期、翻页断层和文本误判的修复记录

5.1 cookie 失效导致接口返回 -100

现象:爬虫正常运行了十几分钟,突然所有请求返回的 JSON 里 ok 字段变成 0,msg 是 -100,list 直接消失。 原因:登录状态过期,服务端识别出当前会话已失效。 解决:在爬虫启动时先发一个小请求探测 cookie 有效性,无效就直接终止并提示你重新复制 cookie。同时把 cookie 写进独立配置文件,别硬编码在爬虫代码里。我自己的习惯是准备两到三个账号的 cookie 轮换使用,单个账号爬 1 万条就换下一个。

5.2 评论翻到第二页就断层,但 total_number 明明还很大

现象:第一页拿到 20 条,第二页返回的 list 为空,日志显示 ok=1,但数据就是没了。 原因:不是所有微博评论都支持顺序翻页。有些微博开启了评论精选模式,普通接口只能返回被博主精选的部分;另一些情况是接口对未登录用户做了深度限制,只看得到第一页。 解决:先检查该微博在网页端是否显示"精选评论"标识,如果是,直接用普通接口数据做分析并明确标注样本不完整;如果是登录态原因,切换完整 cookie 再试。还有一种排查方法:看返回数据里有没有 max_id 或 since_id 字段,有的话要用这些游标参数翻页,而不是用 page 递增。

5.3 嵌套回复和主评论混在一起,统计总数对不上

现象:爬完所有页后,比 total_number 多出几千条,而且大量评论的 user_name 是"回复@xxx"这种格式。 原因:接口把楼中楼回复也平铺在同一个 list 里返回,你没按 root_id 区分。 解决:在清洗阶段判断 root_id 是否为空,为空的是主评论,非空的是子回复。做情感分析时两者都保留没问题,但统计评论总量和点赞对比时,一定要分开算。还有一点:子回复对应的父评论可能不在你的数据范围内,这时候 root_id 会在数据库里形成孤儿记录,属正常现象。

5.4 情感分析把"哈哈哈哈哈哈"打成负面

现象:验证集抽检时发现,大量由"哈哈哈哈""笑死"组成的评论被判成负面。 原因:训练语料里这类口语化表达覆盖少,模型的先验概率把这词跟负面情绪关联了。 解决:在标注阶段专门收集 50 到 100 条这种"纯笑声评论"并标为正面,加进训练集重新训练。如果不想重新训练,也可以在规则层加一个白名单:文本长度小于 20 且包含"哈哈""笑死"的,直接判正面。这个方法虽然暴力,但实测很有效。

5.5 请求频率没控好,IP 被临时封禁

现象:每秒一个请求连跑 5000 条后,突然所有接口都返回 403 或超时,网页端登录也提示异常。 原因:单 IP 对单接口的请求频率超过了服务端的隐式限制,触发临时封禁。 解决:把请求间隔从固定值改成随机值,sleep 范围拉到 2 到 4 秒;单账号每天控制在一万条左右;不要用多线程同时怼同一个接口。这里用代理或者分布式爬虫确实能缓解,但在微博这个场景下,代价远超收益,不建议普通研究项目投入。

6. 用可视化验证结果:情绪画像、词云和时间趋势的落地技巧

当模型判完几万条评论,先别急着写报告,做一轮可视化确认它没有在整体上跑偏。我用的三件套是情绪占比饼图、高频词词云和按小时聚合的情绪走势折线图。情绪占比饼图能一眼看出数据分布是否合理,词云用来检查模型有没有被某个奇怪的高频词带偏。比如你做某个产品的口碑分析,词云里全是"发货"这种物流相关词,说明用户关注点不在产品本身,情感分析结论也要相应调整。

import matplotlib.pyplot as plt from wordcloud import WordCloud # 假设 df 里有 sentiment_label 列(正面/负面/中性) label_counts = df["sentiment_label"].value_counts() plt.figure(figsize=(6, 6)) plt.pie(label_counts.values, labels=label_counts.index, autopct="%1.1f%%") plt.title("微博评论情绪分布") plt.savefig("sentiment_pie.png", dpi=150) # 负面评论高频词词云,检查模型有没有被噪声词带偏 neg_text = " ".join(df[df["sentiment_label"] == "负面"]["text"]) wc = WordCloud(font_path="simhei.ttf", width=800, height=400, max_words=80).generate(neg_text) plt.figure(figsize=(10, 5)) plt.imshow(wc, interpolation="bilinear") plt.axis("off") plt.savefig("negative_wordcloud.png", dpi=150)

最后的验证技巧是人工抽检:从正面和负面预测结果里各随机抽 50 条,肉眼读一遍,算一个抽样一致率。我自己一般要求抽样一致率不低于 85%,低于这个线就回到第 4 章补充标注语料。还有一个小习惯:每次跑完爬虫和情感分析,把 cookie 版本、模型文件、标注集 hash 值全部记录在一个运行日志里。之前有一次没做这个记录,跑完三天数据后模型重新训练,结果和第一次差距很大,翻日志才发现是训练语料顺序被改动过。从那以后,每次跑完先存配置再写结果。希望帮到你。

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

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

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

立即咨询