简介这是一份围绕微博用户数据采集与文本分析的Python爬虫资源包面向具备基础爬虫知识、希望进阶到数据清洗与可视化分析的开发者。通过给定目标用户UID可抓取个人资料、原创与转发微博含图片、视频进而生成关键词词云、电子名片按日/月/年度统计点赞和转发趋势绘制微博好友关系图同时支持按评论数阈值抓取热门评论统计高活跃用户并挖掘狂热粉丝。压缩包为zip格式整体约8.25MB便于下载解压与按需查阅。目前已有5405人学习。内容以完整功能实现为主线覆盖请求解析、数据存储、文本处理和可视化呈现的典型流程对微博反爬应对、评论筛选阈值设定、关系网络绘图等环节都有可借鉴的实现思路适合作为中级Python学习者的综合实战参考也可为社交媒体舆情分析项目提供胶囊式模板。1. 用爬虫抓微博文本做分析先搞清楚这套流程值不值得做想分析一个微博账号的发文习惯、情绪变化和话题偏好最笨的办法是一页页翻手工复制。但当你面对几百条、上千条微博时手工这条路基本走不通。用 python 爬虫 抓取微博用户的历史微博再对这些文本做清洗、分词、情感分析和可视化是一条成熟且可复现的路线。这套流程适合谁适合需要做用户画像、竞品账号内容分析、舆情监测的从业者也适合拿真实中文数据练手的数据分析新手。难点不在爬虫本身而在微博的登录态维护和反爬频率控制。只要把这两点理顺后面的文本分析和可视化其实都是标准动作。2. 微博数据采集从 Requests 到 Cookie 池把登录态和反爬机制先理顺2.1 为什么微博爬虫首选 Requests 而不是 Scrapy常有人一上来就上 Scrapy觉得分布式爬虫才够专业。但单用户微博采集场景数据量最多几千条Scrapy 的工程成本反而成了负担。Requests 轻量、直观能直接看到每次请求的响应状态调试反爬时特别方便。Scrapy 的下载中间件、Item Pipeline 都要额外配置一旦遇到微博这种登录态频繁失效的站点排查成本会成倍上涨。另一个理由是微博接口返回的是 JSONRequests 拿回来后用 resp.json() 就能直接解析不需要像 Scrapy 那样还要定义 Item 和 Feed 导出。当然如果你要爬几千个用户的全部微博再考虑 Scrapy 的并发和断点续爬。个人做分析Requests 足够。2.2 构造带 Cookie 的请求最小可运行脚本微博大部分数据接口都要求登录态。常见做法是先从浏览器登录微博把 Cookie 复制出来写进代码里。注意不要用模拟登录去撞验证码那是给自己找麻烦手动维护 Cookie 反而更稳。下面是能跑通的最小脚本import requests import time # 从浏览器复制注意 Cookie 会过期过期后重新复制 COOKIE SINAGLOBALxxx; SUBxxx; SUBPxxx; ... HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Cookie: COOKIE, Referer: https://weibo.com/, X-Requested-With: XMLHttpRequest, } def fetch_user_timeline(uid, page1): url https://weibo.com/ajax/statuses/mymblog params {uid: uid, page: page, feature: 0} for attempt in range(3): try: resp requests.get(url, paramsparams, headersHEADERS, timeout10) resp.raise_for_status() return resp.json() except requests.RequestException as e: print(f第{attempt 1}次请求失败: {e}) time.sleep(2) return None这里 url 用的是 weibo.com 的 ajax 接口它返回的 JSON 里 data.list 就是当前页的微博数组。params 里的 uid 是要分析的微博用户 IDpage 是页码。feature 置 0 表示只取原创微博如果想要转发微博可以改成 1不过后面清洗会多一步。参数说明timeout10 是单次请求的等待上限避免某次请求卡死整个脚本重试 3 次且每次睡 2 秒是应对微博偶发超时的最低策略。Referer 和 X-Requested-With 两个头在 ajax 接口上不能省缺了很容易被当成异常请求。2.3 翻页与时间线处理按 page 和 since_id 抓取全量上面的 fetch_user_timeline 只抓一页。微博用户时间线单页返回大约 1020 条要抓全量就得翻页。常见做法是循环递增 page直到返回的 data.list 为空或者解析到的微博 ID 出现重复。还有一种基于 since_id 的分页方式但 ajax 接口用 page 更直观。一个完整的采集循环可以这样写import json import time from sqlalchemy import create_engine, Column, String, DateTime, Text from sqlalchemy.orm import declarative_base, sessionmaker # 数据库表定义 Base declarative_base() class WeiboPost(Base): __tablename__ weibo_posts id Column(String(32), primary_keyTrue) # 微博ID天然去重 uid Column(String(32), indexTrue) text Column(Text) # 清洗后的正文 raw_text Column(Text) # 原始文本 created_at Column(DateTime, indexTrue) reposts_count Column(String(16)) comments_count Column(String(16)) attitudes_count Column(String(16)) engine create_engine(sqlite:///weibo.db) Base.metadata.create_all(engine) Session sessionmaker(bindengine)上面这段把数据表结构先定好下面抓取循环里直接往这张表写。用微博 ID 当主键天然去重重复抓同一页也不会产生脏数据。def crawl_all(uid, max_pages100): session Session() page 1 while page max_pages: data fetch_user_timeline(uid, page) if not data or not data.get(data) or not data[data].get(list): break posts data[data][list] for p in posts: # 部分微博没有文本字段先留空 text p.get(text_raw) or p.get(text) or row WeiboPost( idstr(p[id]), uidstr(uid), texttext, raw_texttext, created_atparse_weibo_time(p[created_at]), reposts_countstr(p.get(reposts_count, )), comments_countstr(p.get(comments_count, )), attitudes_countstr(p.get(attitudes_count, )), ) session.merge(row) # 有则更新无则插入 session.commit() print(fpage {page} done, got {len(posts)} posts) page 1 time.sleep(3) # 单用户采集3秒间隔足够温和 session.close()这里 session.merge() 是 SQLAlchemy 的按主键更新或插入方法比先查询再判断效率高。created_at 需要把微博的时间字符串解析成 Python datetime微博返回的格式类似 Tue May 30 10:00:00 0800 2024建议用 datetime.strptime 解析时区固定 0800。注意 max_pages 只是保护性上限实际循环会在 list 为空时自动退出。sleep(3) 是刻意为之微博对单 IP 高频请求很敏感宁可慢一点也不要让脚本中途死在验证码上。2.4 数据落库用 SQLAlchemy 储存爬虫数据别再把结果堆在 CSV很多人爬完微博喜欢直接存 CSV几行代码就完事。但一旦涉及后续的文本分析和可视化CSV 的缺点就暴露了去重要额外写逻辑字段类型混乱长时间多次采集后文件越来越大Excel 打开还容易乱码。用 SQLAlchemy 存 SQLite 或 MySQL是更稳妥的做法也方便后续用 SQL 做日期筛选、聚合统计。上面代码里已经给出了表结构这里补充一个从数据库读取的示例from sqlalchemy import func # 统计某用户每天发博数 with Session() as s: daily s.query( func.date(WeiboPost.created_at).label(day), func.count(WeiboPost.id).label(cnt) ).filter(WeiboPost.uid uid).group_by(day).all() for day, cnt in daily: print(day, cnt)这个聚合查询后面会直接喂给 ECharts 画时间线图。SQLAlchemy 的 func.date 是把 datetime 截断到天group_by 后得到每日发博量。如果换 MySQL语法完全一样。用数据库的好处就是你不用在内存里维护一堆 dict分析维度多了也方便。提示Cookie 里的 SUB 和 SUBP 是关键字段失效后接口会返回重定向或空 list。建议每次采集前先跑一个测试请求确认返回码 200 再进入循环。3. 数据清洗与文本预处理把“转发微博”“表情符号”“用户”先收拾干净3.1 清洗规则去重复、去转发标记、去URL和话题标签爬下来的微博文本基本不能直接用。常见脏数据有三类以“转发微博”开头的空转发、带 http 链接的推广文案、以及大量 用户 和 #话题#。这些内容对词频和情感分析都会造成干扰需要单独清洗。一个基础清洗函数可以这样写import re def clean_weibo_text(raw): if not raw: return # 去掉“转发微博”这种无内容前缀 text re.sub(r^转发微博, , raw) # 去掉URL text re.sub(rhttp[s]?://\S, , text) # 去掉用户 text re.sub(r[\w\u4e00-\u9fa5\-], , text) # 去掉#话题#但可以保留标签内容另作统计 text re.sub(r#([^#])#, , text) # 去掉HTML实体和图片占位符 text re.sub(r[^], , text) text re.sub(r\[(图片|视频|音乐)\], , text) # 去掉多余的空白 text re.sub(r\s, , text).strip() return text清洗逻辑里有个取舍话题标签里的关键词往往很有分析价值直接删掉会损失信息。常见做法是清洗正文时删掉标签符号但把标签内容单独提取出来存成另一个字段。这样既不影响高频词统计也能单独做话题热度分析。另外微博文本里经常混有表情符号和特殊 emoji比如“哈哈哈[允悲]”“[doge]”。中括号里的内容是微博自带的表情占位符可以直接删掉。如果保留分词时会被切成“允悲”“doge”这类无意义词拉低词频统计的准确性。对于真正的 Unicode emoji可以用正则匹配[\U0001F300-\U0001F9FF]一并移除不过如果你打算做情绪分析也可以先留着观察表情与情感值的关系。3.2 中文分词与停用词jieba 和自定义词典的配合清洗后的文本要分词才能统计词频。中文分词首选 jieba简单好用而且支持自定义词典。微博文本里有很多网络新词、缩写yyds、emo、破防默认词典切不准需要手动维护一个自定义词典文件。import jieba jieba.load_userdict(weibo_dict.txt) # 每行一个词可以带词频和词性例如yyds 100 n stop_words set() with open(stopwords_cn.txt, encodingutf-8) as f: for line in f: word line.strip() if word: stop_words.add(word) def tokenize(text): words jieba.cut(text) return [w for w in words if w not in stop_words and len(w) 1]这里加载的 stopwords_cn.txt 是常见的哈工大停用词表根据场景可以自己增删。注意 filter 条件里 len(w) 1 会把单字词过滤掉这在微博文本里能去掉大量“了、的、我”等语气词但也会误删“梦”“家”这种有实义的单字。如果你的分析对象里单字词很关键这个条件要改成 1。自定义词典是 jieba 用法里最容易忽略的一环。默认词典不包含“yyds”“绝绝子”这类网络表达不加载词典的话“yyds”会被切成“yy”“ds”词频统计直接失效。我一般把微博里高频出现的网络词、人名、产品名按每行一个词维护在 weibo_dict.txt 里积累一段时间后分词效果会明显变好。3.3 构建分析语料按时间、按频率、按关键词筛选子集全量微博文本做出来的分析往往太散。更常见的做法是按维度切片只看某个月的微博、只看转发超过 100 的微博、只看包含某个关键词的微博。切片之后再做词频和情感分析结论才有针对性。from sqlalchemy import Integer, cast def load_texts(session, uid, startNone, endNone, min_repostsNone, keywordNone): q session.query(WeiboPost).filter(WeiboPost.uid uid) if start: q q.filter(WeiboPost.created_at start) if end: q q.filter(WeiboPost.created_at end) if min_reposts is not None: # reposts_count 存的是字符串要转成整数后再比较 q q.filter(cast(WeiboPost.reposts_count, Integer) min_reposts) if keyword: q q.filter(WeiboPost.text.like(f%{keyword}%)) rows q.all() docs [] for row in rows: cleaned clean_weibo_text(row.text) if cleaned: docs.append(cleaned) print(f筛选后语料规模: {len(docs)} 条) return docs注意 reposts_count 在表里定义成了 String因为微博返回的字段有的是字符串有的是数字统一转成字符串最省事。需要数值比较时就 cast。这个筛选函数是后面所有分析的基础建议把 start、end、min_reposts、keyword 做成参数方便在 Jupyter 里反复试。筛选之后一定要看一眼语料规模。如果某个月的微博只有 3 条做词云和情感均值都没有统计意义。我一般会设定一个最低条数比如少于 20 条就直接提示“样本不足”不做后续分析。这样能避免因为数据稀疏得出误导性结论。4. 文本分析与可视化词频、情感倾向、主题分布怎么落到图表上4.1 词频统计与词云从 collections.Counter 到 WordCloud 参数清洗分词之后的文本列表直接喂给 collections.Counter 就能得到词频。词云图是微博文本分析最直观的呈现方式但很多人生成的词云全是高频通用词问题出在没有好好过滤停用词。from collections import Counter from wordcloud import WordCloud all_words [] for text in all_docs: all_words.extend(tokenize(text)) counters Counter(all_words) # 输出前20高频词 for word, cnt in counters.most_common(20): print(word, cnt) # 生成词云 wordcloud WordCloud( font_pathmsyh.ttc, # 中文字体必须指定否则全是方块 width800, height600, background_colorwhite, max_words200, colormapviridis ).generate_from_frequencies(counters) wordcloud.to_file(weibo_wordcloud.png)WordCloud 有两个容易踩的参数font_path 不指定中文会乱码generate_from_frequencies 比 generate(text) 更适合已有词频的情况因为可以避免 jieba 和 WordCloud 内置分词不一致。colormap 控制颜色风格viridis 是 matplotlib 自带配色看起来比默认色更专业。高频词排序之后通常会发现前几名被“真的”“觉得”“感觉”这类词占据。如果停用词表没有覆盖可以补充进去再跑一次。词频分析的目的是找出这个账号区别于其他账号的特色用语而不是验证停用词表够不够全。4.2 情感分析基于情感词典和 SnowNLP 的取舍微博文本情感分析有两个方向一是 SnowNLP开箱即用但它是基于电商评论训练的用在微博上偏消极的阈值需要重新校准二是自定义情感词典准确率高但要人工维护。如果你是做单账号长期观察我建议先用 SnowNLP 跑一遍把分数存下来再抽样看几条人工核对不要直接信分数。from snownlp import SnowNLP def sentiment_score(text): # 空文本直接返回0.5避免影响均值 if not text or len(text) 2: return 0.5 try: s SnowNLP(text) return s.sentiments except Exception: return 0.5SnowNLP 的返回值是 0 到 1越接近 1 越积极0.5 是中性。微博里大量“哈哈哈哈”“笑死”这类文本SnowNLP 经常误判因为训练语料里没有网络表达。实际使用中我一般会把分数低于 0.4 视为消极、高于 0.6 视为积极中间算中性而不是用 0.5 做硬切。这样分析出来的情绪占比更接近人工判断。如果你只想看情绪走势可以不用太纠结单条文本的准确率。对几百条文本求均值后随机误差会相互抵消趋势仍然可信。真正需要人工核对的是那些异常高分或低分的微博比如某天情感均值突然跌到 0.2去翻原始文本往往是几条负面博文集中爆发这时候分析结论才对得上。4.3 ECharts 可视化把时间线、高频词、情感占比做成可交互图表文本分析的结果最终要落到图表上。ECharts 是这里最顺手的工具它支持从 Python 生成的 JSON 直接渲染。常见做法是用 pyecharts也可以把聚合结果导出成 HTML 模板里的 JSON 变量。from pyecharts.charts import Line, Bar, Pie from pyecharts import options as opts # 每日发博量折线图 line Line() line.add_xaxis([str(d) for d in days]) # days 是日期列表 line.add_yaxis(发博数, counts) line.set_global_opts( title_optsopts.TitleOpts(title每日发博量), tooltip_optsopts.TooltipOpts(triggeraxis) ) line.render(daily_trend.html)pyecharts 生成的 HTML 自带 ECharts JS用浏览器打开就能交互。如果你想把多个图集成到一个页面做可视化大屏可以用 Page 对象组合多个图表或者直接把 ECharts 的 option 写进一个 HTML 模板。这里的关键是数据结构要对x 轴是日期y 轴是聚合值前后端分离方便后续换其他数据源。除了折线图情感占比用饼图、高频词用横向柱状图更直观。比如你要做一个“情绪占比”饼图把积极、中性、消极三类文本的数量统计出来用 Pie 组件 30 秒就能出图。展示的时候记得给每个图配一句解读否则可视化只是漂亮的摆设读者不知道你想表达什么。5. 微博爬虫避坑指南登录失效、验证码、频率限制、字段缺失的 4 个典型翻车现场5.1 现象Cookie 明明有效请求却返回 400 或 414有次我把用户 ID 直接拼在 URL 里像 https://weibo.com/ajax/statuses/mymblog?uid1234567890page1结果偶尔返回 400偶尔 414。后来才发现是某些参数拼接方式触发微博的 URL 长度校验而且缺少 Referer 也会 400。原因URL 过长或 header 不完整。解决所有参数一律用 params 字典传给 requests.get不要手拼Referer 固定为 https://weibo.com/另外确认 headers 里没有多余的空格或换行。5.2 现象爬着爬着突然要验证码或者被重定向到 passport.weibo.com这个最常见。开始时一切正常抓了几百条后突然返回的 JSON 里 data 变成了 None或者响应内容变成登录页 HTML。原因请求频率超过阈值或者 Cookie 被服务端主动失效。解决先停掉脚本等 10 分钟再试把 sleep 从 3 秒拉长到 5 秒以上另外检查系统时间是否准确微博的 Cookie 校验偶尔会带上时间戳。不要尝试去自动识别验证码那是另一个灰色领域且对个人分析毫无必要。5.3 现象文本里混着大量“转发微博”空内容你抓到的 list 里有些微博的 text 字段值是“转发微博”真正的正文在 retweeted_status 里。如果只取 text你的语料里会混入大量无意义文本词频统计里“转发”这个词可能排到第一。原因转发微博的正文结构是嵌套的。解决解析时判断如果 text 是“转发微博”且存在 retweeted_status就取 retweeted_status 里的 text同时保留原始转发标记方便后续分析传播行为。def extract_text(post): text post.get(text_raw) or post.get(text) or if text 转发微博 and post.get(retweeted_status): rt post[retweeted_status] text rt.get(text_raw) or rt.get(text) or return text5.4 现象字段缺失或错位有的微博没有 text 字段微博正文有时候是长文章、视频、直播链接text 字段可能为空或者变成了“// :”这种异常值。如果直接 post[text] 会 KeyError全脚本崩溃。原因微博的多种内容形态共用一个接口字段并不是每条都齐全。解决所有字段都用 get() 获取并给默认值对 text 做空值判断。另外注意 list 里还可能混有“置顶”“抽奖”这类特殊条目建议把 raw_text 也存下来怀疑数据质量时回来查原始值。5.5 现象本地能跑一上服务器就反爬严重同样的代码和数据在本地跑一上午没事放到云服务器上几分钟就被限制。主要是服务器 IP 段被微博标记过或者同一出口 IP 有大量其他爬虫活动。原因机房 IP 的信任度比家用宽带低。解决把并发降到 1sleep 提到 8 秒以上或者干脆只在本地或家用网络跑。如果非要在服务器上跑先跑一个低频小任务测试确认连续 30 分钟不出验证码再扩大范围。不要使用任何第三方转发服务那些 IP 更脏被限制的概率反而更高。6. 把分析结果做成长时间序列的观察脚本一个基于日期聚合的可视化模板6.1 模板思路每日发博量、情感均值、Top关键词变化单次采集只能做切片分析更实用的做法是把整个流程包装成一个按日期聚合的观察脚本。输入是一个用户 ID输出是一份包含每日发博量、每日情感均值、每日 Top5 关键词的 HTML 报告。这样你可以每月跑一次观察账号的内容风格和情绪走向。模板分析涉及“微博签到数据”这种带时间戳的文本特征按天切分后能看到用户在特定日期的活跃规律。整体文本分析则是长期趋势比如某账号从日更变成周更或者情感均值逐月下滑都能直观反映在折线图上。6.2 代码骨架按天聚合 ECharts 折线图下面给出一个可扩展的脚本骨架。它先从数据库读数据按天聚合出三个指标最后用 pyecharts 生成两个图发博数折线图、情感均值折线图。import datetime from sqlalchemy import func from pyecharts.charts import Line from pyecharts import options as opts def daily_report(session, uid, months6): since datetime.date.today() - datetime.timedelta(days30 * months) rows session.query( WeiboPost.created_at, WeiboPost.text, WeiboPost.id ).filter( WeiboPost.uid uid, WeiboPost.created_at since ).all() # 按天聚合 daily_count {} daily_sentiment {} for created_at, text, wid in rows: day created_at.date().isoformat() daily_count[day] daily_count.get(day, 0) 1 score sentiment_score(text) daily_sentiment[day] daily_sentiment.get(day, 0) score days sorted(daily_count.keys()) counts [daily_count[d] for d in days] avg_senti [round(daily_sentiment[d] / daily_count[d], 3) for d in days] line1 Line() line1.add_xaxis(days) line1.add_yaxis(发博数, counts, is_smoothTrue) line1.set_global_opts(title_optsopts.TitleOpts(title每日发博量)) line2 Line() line2.add_xaxis(days) line2.add_yaxis(情感均值, avg_senti, is_smoothTrue, yaxis_optsopts.AxisOpts(min_0, max_1)) line2.set_global_opts(title_optsopts.TitleOpts(title每日情感均值)) return line1, line2这个函数返回两个 ECharts 图表对象调用方可以分别 render 到 HTML也可以用 Page 拼在一起。半个月以上的数据画折线图才有意义所以默认拉取 6 个月。如果用户微博量很大建议把时间跨度做成参数避免一次加载太多数据把页面拖垮。我个人的习惯是每次跑完都把生成的 HTML 文件名带上日期比如 daily_report_20240601.html这样月底回看时能快速对比不同阶段的变化。还有一点情感均值这种指标在样本量少于 10 条的天会很不稳定我会在图表 Tooltip 里补上当日发博数否则看单条折线很容易误读。希望这个模板能帮你少走一段弯路把精力真正放到解读数据上。本文还有配套的精品资源点击获取