简介这是一份面向微博评论采集与分析的Python实战资源包项目名称为weibo-comment-crawler-master涵盖爬虫、数据存储、文本分词与情感分析多个环节适合正在学习爬虫技术、自然语言处理或从事微博舆情分析的学习者参考。压缩包内共有17个文件包体约465KB以2个Python脚本评论爬虫与情感分析、8个txt辅助文件停用词、否定词、程度词、正负向情感词典等、1个CSV微博评论样例、建表SQL说明及README文档为主体结构清晰便于直接运行与二次开发。资源覆盖了requests模拟登录、BeautifulSoup网页解析、cookie/session处理、验证码识别思路以及pandasMySQL存储、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, page1): # 微博移动端评论接口返回 JSON比解析 HTML 稳定得多 url fhttps://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: fhttps://m.weibo.cn/detail/{weibo_id}, Cookie: cookie_value, } resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() return resp.json()这段代码的注意点有几个。weibo_id 不是微博分享链接里那串带字母的 bid而是数字 id可以从网页版微博详情页的>import time import random def crawl_comments(weibo_id, cookie_value, max_pages50): 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 classsurl-text哈哈哈哈/spana 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(rhttps?://\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, nowNone): 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(minutesint(m.group(1)))).timestamp()) m re.match(r(\d)小时前, value) if m: return int((now - timedelta(hoursint(m.group(1)))).timestamp()) m re.match(r昨天 (\d{2}:\d{2}), value) if m: dt now - timedelta(days1) hh, mm map(int, m.group(1).split(:)) return int(dt.replace(hourhh, minutemm, second0, microsecond0).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, encodingutf-8) as f: for line in f: line line.strip() if line: train_data.append((line, 1)) # 负样本文件每行一条评论标注为 0 with open(neg_path, encodingutf-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 字段变成 0msg 是 -100list 直接消失。 原因登录状态过期服务端识别出当前会话已失效。 解决在爬虫启动时先发一个小请求探测 cookie 有效性无效就直接终止并提示你重新复制 cookie。同时把 cookie 写进独立配置文件别硬编码在爬虫代码里。我自己的习惯是准备两到三个账号的 cookie 轮换使用单个账号爬 1 万条就换下一个。5.2 评论翻到第二页就断层但 total_number 明明还很大现象第一页拿到 20 条第二页返回的 list 为空日志显示 ok1但数据就是没了。 原因不是所有微博评论都支持顺序翻页。有些微博开启了评论精选模式普通接口只能返回被博主精选的部分另一些情况是接口对未登录用户做了深度限制只看得到第一页。 解决先检查该微博在网页端是否显示精选评论标识如果是直接用普通接口数据做分析并明确标注样本不完整如果是登录态原因切换完整 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, labelslabel_counts.index, autopct%1.1f%%) plt.title(微博评论情绪分布) plt.savefig(sentiment_pie.png, dpi150) # 负面评论高频词词云检查模型有没有被噪声词带偏 neg_text .join(df[df[sentiment_label] 负面][text]) wc WordCloud(font_pathsimhei.ttf, width800, height400, max_words80).generate(neg_text) plt.figure(figsize(10, 5)) plt.imshow(wc, interpolationbilinear) plt.axis(off) plt.savefig(negative_wordcloud.png, dpi150)最后的验证技巧是人工抽检从正面和负面预测结果里各随机抽 50 条肉眼读一遍算一个抽样一致率。我自己一般要求抽样一致率不低于 85%低于这个线就回到第 4 章补充标注语料。还有一个小习惯每次跑完爬虫和情感分析把 cookie 版本、模型文件、标注集 hash 值全部记录在一个运行日志里。之前有一次没做这个记录跑完三天数据后模型重新训练结果和第一次差距很大翻日志才发现是训练语料顺序被改动过。从那以后每次跑完先存配置再写结果。希望帮到你。本文还有配套的精品资源点击获取