
简介这是一份面向Python爬虫与文本分析初学者的实战项目资源聚焦微博平台用户行为数据采集与多维可视化分析。资源提供完整的微博UID定向抓取方案支持获取用户资料、原创及转发微博含图文、热评数据并基于内容生成词云、电子名片绘制点赞/转发趋势图与好友关系网络挖掘狂热粉丝群体。压缩包为8.25MB的ZIP文件内含可运行脚本、配置说明及分析结果示例主要文件类型包括Python源码实现爬取与分析逻辑、JSON/CSV数据文件存储用户资料与微博内容、HTML/PNG可视化输出词云、关系图、趋势图表。目前已有5405人学习下载配套代码结构清晰、模块解耦明确包含异常处理与参数配置说明便于读者快速复现、调试并拓展至其他社交平台分析场景。1. 微博文本爬取分析可视化不是“一键获取全网数据”而是用可控尺度做可复现的舆情切片分析你搜“python爬微博”时看到的90%代码要么跑不通微博反爬已升级到动态渲染行为验证要么一运行就被封IP没做请求节流、UA轮换、登录态管理而真正能落地的场景其实是盯住30个目标用户比如某品牌官微、竞品KOL、行业垂类博主连续7天抓取其公开微博正文发布时间转发/评论数清洗后做词频TOP20、情感倾向趋势、话题聚类热力图——这才是企业市场部、高校社科团队、自媒体运营者真实需要的“小而准”分析闭环。这篇笔记不讲“如何黑进微博后台”只讲怎么用requestsBeautifulSoup正则jiebapyecharts在2024年Q2环境下稳定跑通这个闭环。重点在登录态维持策略、Ajax接口逆向定位、文本清洗的5个硬坑、以及为什么用WordCloud不如用Pyecharts的词云图时间轴联动更易解释。适合有Python基础但没实操过微博生态的同学也适合老手查漏补缺——毕竟我去年帮三个团队搭这套流程光是“cookie过期自动重登”就写了三版逻辑。2. 从登录到数据落库用requests模拟登录抓取Ajax接口的最小可行路径微博PC端已全面转向Ajax异步加载直接请求https://weibo.com/u/xxxx返回的是空壳HTML真实微博列表藏在https://weibo.com/ajax/statuses/mymblog?uidxxxpage1count10这类接口里。关键不在“能不能爬”而在“怎么让接口认你是合法用户”。2.1 登录态构造绕过扫码用账号密码验证码登录2024年实测有效微博登录页https://login.sina.com.cn/已弃用旧版form提交现用/sso/login.php接口需携带servertime、nonce、pubkey等动态参数。这些值来自https://login.sina.com.cn/sso/prelogin.php?entryweibocallbacksinaSSOController.preloginCallBacksuxxxrsaktmodcheckpin1clientssologin.js(v1.4.23)——注意callback参数必须原样带上否则返回JSON被包裹在JS函数里。import requests import re import time import base64 from urllib.parse import quote_plus def get_prelogin_params(username): su base64.b64encode(quote_plus(username).encode()).decode() url fhttps://login.sina.com.cn/sso/prelogin.php?entryweibocallbacksinaSSOController.preloginCallBacksu{su}rsaktmodcheckpin1clientssologin.js(v1.4.23) resp requests.get(url, headers{User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36}) # 提取 callback 函数体内的 JSON json_str re.search(rpreloginCallBack\((.*?)\), resp.text).group(1) import json return json.loads(json_str) def login_weibo(username, password): session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 }) # 1. 获取预登录参数 prelogin get_prelogin_params(username) servertime prelogin[servertime] nonce prelogin[nonce] pubkey prelogin[pubkey] # 2. RSA加密密码微博用RSA公钥加密 from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_v1_5 import binascii key RSA.import_key(f-----BEGIN PUBLIC KEY-----\n{pubkey}\n-----END PUBLIC KEY-----) cipher PKCS1_v1_5.new(key) encrypted_pw cipher.encrypt(f{servertime}\t{password}.encode()) sp binascii.b2a_hex(encrypted_pw).decode() # 3. 构造登录表单 login_data { entry: weibo, gateway: 1, from: , savestate: 7, qrcode_flag: false, userticket: 1, vsnf: 1, service: miniblog, servertime: str(servertime), nonce: nonce, pwencode: rsa2, sp: sp, sr: 1920*1080, encoding: UTF-8, cdult: 3, domain: sina.com.cn, prelt: 123, returntype: TEXT, altf: 1, url: https://weibo.com/ajax/antispam/submit, useticket: 1, username: username, password: , rsakv: prelogin[rsakv], door: , # 若有验证码此处填识别结果 } # 4. 发起登录请求 login_url https://login.sina.com.cn/sso/login.php?clientssologin.js(v1.4.23) resp session.post(login_url, datalogin_data, allow_redirectsFalse) # 5. 检查是否跳转302带Location头说明成功 if resp.status_code 302 and Location in resp.headers: redirect_url resp.headers[Location] # 跟跳获取最终cookie session.get(redirect_url, allow_redirectsTrue) return session else: raise Exception(Login failed: resp.text[:200]) # 使用示例需替换为真实账号 # s login_weibo(your_username, your_password)提示这段代码依赖pycryptodome库pip install pycryptodome不是pycrypto已停更。RSA加密必须严格按servertime\t密码格式拼接\t不能省略否则服务端解密失败。door字段为空表示无验证码若触发验证码需调用OCR接口如百度文字识别或人工输入本方案默认走无验证码路径高频登录会触发低频稳定。2.2 定位并调用用户微博列表接口避开浏览器渲染直击数据源头登录成功后不要访问https://weibo.com/u/1234567890而要调用Ajax接口。打开浏览器开发者工具→Network→XHR刷新目标用户主页找到mymblog或profile开头的请求。2024年主流接口是https://weibo.com/ajax/statuses/mymblog?uid1234567890page1count10feature0其中uid用户ID非昵称需从主页URL或https://weibo.com/ajax/profile/info?usernamexxx接口获取page页码从1开始count每页条数最大20设太大易被限流feature0表示获取全部微博1仅原创2仅转发def fetch_user_weibo(session, uid, page1, count10): url fhttps://weibo.com/ajax/statuses/mymblog?uid{uid}page{page}count{count}feature0 try: resp session.get(url, timeout10) if resp.status_code ! 200: raise Exception(fHTTP {resp.status_code}) data resp.json() if data not in data or list not in data[data]: raise Exception(No weibo list in response) return data[data][list] except Exception as e: print(fFetch page {page} failed: {e}) return [] # 示例获取用户前2页微博20条 # weibos [] # for p in range(1, 3): # weibos.extend(fetch_user_weibo(s, 1234567890, p))关键逻辑说明session必须是上一步登录成功的会话对象它自动携带SUB、SUHB等关键cookie接口返回JSON中list数组每项含id微博ID、text_raw原始HTML文本、created_at发布时间字符串、reposts_count、comments_count、attitudes_counttext_raw含br、span等标签需用re.sub(r[^], , text)清洗不能用html.unescape()会把gt;转成但微博里大量quot;需保留每次请求后time.sleep(1.5)这是硬性要求——微博服务端对同一IP的/ajax/statuses/接口有QPS限制实测0.8次/秒即返回418Im a teapot。3. 文本清洗与结构化从raw HTML到可分析DataFrame的5道过滤工序微博文本不是干净语料含广告链接t.cn/xxx、提及张三、话题标签#iPhone15#、emoji[doge]、转发原文转发微博xxx、以及大量无意义符号//、///。直接扔给jieba分词结果全是http、t、cn、xxx——这步不做后面所有分析都是玄学。3.1 清洗流水线正则链式处理语义过滤import re import pandas as pd from datetime import datetime def clean_weibo_text(raw_text): # 1. 移除HTML标签保留换行符 text re.sub(r[^], \n, raw_text) # 2. 移除广告短链t.cn/xxx 或 weibo.cn/xxx text re.sub(r(https?://)?(t\.cn|weibo\.cn)/\w, , text) # 3. 移除提及保留用户名用于后续社交分析此处先删 text re.sub(r\w, , text) # 4. 移除话题标签#内容#但保留内部文字用于话题挖掘 text re.sub(r#([^#])#, r\1, text) # 5. 移除转发标识及原文匹配“转发微博”或“转发了”开头 text re.sub(r^转发[了\s]*微博[:]\s*, , text, flagsre.MULTILINE) # 6. 移除纯符号行如“//////”、“----------” text re.sub(r^[/\-_]{3,}$, , text, flagsre.MULTILINE) # 7. 合并多余空白包括换行、制表、空格 text re.sub(r\s, , text).strip() return text def parse_weibo_list(weibo_json_list): records [] for w in weibo_json_list: # 时间标准化微博返回如 Mon May 20 15:32:18 0800 2024 try: dt datetime.strptime(w[created_at], %a %b %d %H:%M:%S %z %Y) except: dt datetime.now() # fallback records.append({ weibo_id: w[id], text_raw: w[text_raw], text_clean: clean_weibo_text(w[text_raw]), created_at: dt, reposts: w.get(reposts_count, 0), comments: w.get(comments_count, 0), attitudes: w.get(attitudes_count, 0), source: w.get(source, ).strip(), # 如 “iPhone 14” }) return pd.DataFrame(records) # 使用示例 # df parse_weibo_list(weibos) # print(df[[text_clean, created_at]].head())参数说明与踩坑点clean_weibo_text()第2步必须匹配weibo.cn微博官方短链域名很多教程只写t.cn漏掉weibo.cn导致广告残留第4步用\1捕获组保留话题内文字比直接删除更利于后续#AI#→AI的关键词归一datetime.strptime()的格式串必须严格匹配微博API返回格式%z解析0800时区若环境不支持如旧版Python改用dateutil.parser.parse(w[created_at])source字段常含设备信息“来自 iPhone客户端”可提取设备类型做用户画像但需strip()去空格否则 来自 iPhone客户端 无法精确匹配。3.2 去重与时间窗口裁剪避免同一条微博被多次抓取微博用户可能发同一条内容多次如定时发布或因网络问题重复请求导致数据冗余。按weibo_id去重是底线但更要按业务需求裁剪时间范围def filter_by_time_window(df, days_back7): 保留最近N天的数据 cutoff pd.Timestamp.now() - pd.Timedelta(daysdays_back) return df[df[created_at] cutoff].copy() def deduplicate_by_id(df): 按weibo_id去重保留最新一条created_at最大 return df.sort_values(created_at, ascendingFalse).drop_duplicates(weibo_id, keepfirst) # 组合使用 # df_clean parse_weibo_list(weibos) # df_clean deduplicate_by_id(df_clean) # df_clean filter_by_time_window(df_clean, days_back7)注意drop_duplicates(keepfirst)默认保留第一次出现但我们要的是最新时间所以必须先sort_values(..., ascendingFalse)再去重。这个细节翻车过三次——有团队用错顺序导致分析结果里全是旧数据。4. 文本分析实战用jiebaTF-IDFTextRank做可解释的关键词提取可视化不是堆图表而是让业务方一眼看懂“用户在聊什么”。词频统计CountVectorizer只能告诉你“苹果”出现120次但无法区分“苹果手机”和“苹果公司”TF-IDF能压低停用词权重但对长尾新词如“华为三折叠”敏感度低TextRank结合语法结构更适合微博这种短文本场景。4.1 构建领域停用词表比通用停用词表多3类关键过滤微博文本停用词不能只用哈工大停用词表必须叠加三类动态词平台噪声词微博、weibo、转发、评论、点赞这些在微博语境中无意义设备/客户端词iPhone、华为、小米、安卓除非分析设备偏好否则干扰主题高频无意义动词来啦、冲啊、哈哈哈、呜呜呜情绪表达非内容主题。# 自定义停用词实际项目中存为stopwords_weibo.txt custom_stopwords { 微博, weibo, 转发, 评论, 点赞, 关注, 粉丝, 主页, 私信, iPhone, 华为, 小米, OPPO, vivo, 安卓, iOS, 客户端, 来啦, 冲啊, 哈哈哈, 呜呜呜, 嘿嘿嘿, 耶耶耶, 啦啦啦 } # 加载jieba词典并添加自定义词提升“华为三折叠”等新词识别率 import jieba jieba.add_word(华为三折叠) jieba.add_word(iPhone15) jieba.add_word(大模型) def extract_keywords_jieba_textrank(text_series, topK20): import jieba.analyse # TextRank关键词提取基于词性过滤只保留名词、动词、形容词 keywords jieba.analyse.textrank( .join(text_series), topKtopK, withWeightTrue, allowPOS(ns, n, vn, v, a, ad, an) # 地名、名词、动名词、动词、形容词等 ) return pd.DataFrame(keywords, columns[keyword, weight]) # 示例对清洗后的text_clean列提取关键词 # keywords_df extract_keywords_jieba_textrank(df_clean[text_clean])为什么不用TF-IDFTF-IDF在单文档内计算词频在多文档间算逆文档频率。但微博分析常是单用户7天文本约50-200条文档数太少IDF值失真严重。TextRank把每条微博当句子节点词共现当边用PageRank算法迭代对短文本鲁棒性更强——实测在“数码评测”类博主文本中TextRank给出的三折叠、铰链、UTG等专业词排名比TF-IDF高4位以上。4.2 情感倾向分析用SnowNLP而非BERT兼顾速度与可解释性BERT类模型精度高但部署成本大、推理慢且黑匣子输出难向业务解释。SnowNLP是中文轻量级情感分析库基于朴素贝叶斯训练对微博短文本准确率82%我们用1000条人工标注样本交叉验证且返回0~1的分数0.5为中性业务方一眼看懂。from snownlp import SnowNLP def sentiment_score(text): try: s SnowNLP(text) return s.sentiments # 返回0~1浮点数越接近1越正面 except: return 0.5 # 异常时返回中性 # 批量计算注意SnowNLP不是线程安全需单线程 df_clean[sentiment] df_clean[text_clean].apply(sentiment_score) # 按天聚合情感均值生成时间序列 daily_sentiment df_clean.groupby(df_clean[created_at].dt.date)[sentiment].mean().reset_index()血泪经验SnowNLP的s.sentiments对含多个感叹号或问号的文本会过度乐观需加规则修正def robust_sentiment(text): base sentiment_score(text) if text.count() 2: base min(base * 0.8, 0.9) # 降权 if text.count() 2: base max(base * 1.2, 0.1) # 升权疑问常隐含不满 return base5. 可视化落地用Pyecharts实现“词云时间轴情感热力”三联屏ECharts是前端可视化事实标准Pyecharts是其Python封装。不推荐WordCloud——它无法联动交互且字体渲染在Linux服务器上常出错。三联屏设计原则左词云看主题分布中时间轴看发布节奏右热力图看情感波动三者共享同一数据源点击词云某词时间轴自动高亮含该词的微博时段。5.1 词云图用Pyecharts WordCloud而非matplotlibfrom pyecharts import options as opts from pyecharts.charts import WordCloud from pyecharts.globals import SymbolType def render_wordcloud(keywords_df, output_pathwordcloud.html): # keywords_df格式keyword | weight word_data [(row[keyword], row[weight]) for _, row in keywords_df.iterrows()] c WordCloud() c.add( series_name关键词热度, data_pairword_data, shapeSymbolType.DIAMOND, # 钻石形比圆形更聚焦 word_size_range[12, 60], # 字体大小区间 width100%, height600px ) c.set_global_opts( title_optsopts.TitleOpts(title微博关键词云图), tooltip_optsopts.TooltipOpts(is_showTrue), legend_optsopts.LegendOpts(is_showFalse) ) c.render(output_path) print(f词云已保存至 {output_path}) # 使用示例 # render_wordcloud(keywords_df)关键参数说明shapeSymbolType.DIAMOND钻石形布局比默认圆形更节省空间且视觉重心居中word_size_range[12, 60]避免“的”“了”等小词过大设置下限12pxwidth100%响应式宽度适配大屏可视化项目输出HTML文件可直接双击打开无需启动Web服务。5.2 时间轴情感热力图用LineHeatMap组合实现联动from pyecharts.charts import Line, HeatMap from pyecharts.components import Table from pyecharts.options import VisualMapOpts def render_timeline_heatmap(df_clean, output_pathanalysis.html): # 1. 时间轴每日发博数量 daily_count df_clean.groupby(df_clean[created_at].dt.date).size().reset_index(namecount) line Line() line.add_xaxis(daily_count[0].astype(str).tolist()) line.add_yaxis(日发布量, daily_count[count].tolist()) line.set_global_opts( title_optsopts.TitleOpts(title7日发布趋势), yaxis_optsopts.AxisOpts(min_0), tooltip_optsopts.TooltipOpts(triggeraxis) ) # 2. 情感热力图小时×日期的情感均值 # 构造热力图数据[(date, hour, sentiment_mean)] df_clean[hour] df_clean[created_at].dt.hour df_clean[date] df_clean[created_at].dt.date heat_data df_clean.groupby([date, hour])[sentiment].mean().reset_index() # 转为ECharts所需格式[[date, hour, value]] heat_series [[str(row[date]), row[hour], round(row[sentiment], 3)] for _, row in heat_data.iterrows()] heatmap HeatMap() heatmap.add_xaxis(sorted(df_clean[date].astype(str).unique().tolist())) heatmap.add_yaxis(小时, list(range(24)), heat_series) heatmap.set_global_opts( title_optsopts.TitleOpts(title情感热度小时分布), visualmap_optsopts.VisualMapOpts( min_0.3, max_0.7, # 情感值合理区间 is_piecewiseFalse, orienthorizontal, pos_bottom5% ), tooltip_optsopts.TooltipOpts(triggeritem, formatter{c}) ) # 3. 组合成页面用Page from pyecharts.charts import Page page Page(layoutPage.SimplePageLayout) page.add(line, heatmap) page.render(output_path) print(f分析报告已保存至 {output_path}) # 使用示例 # render_timeline_heatmap(df_clean)为什么热力图用小时×日期微博用户活跃时段高度集中早8-9点通勤、午12-13点休息、晚20-22点娱乐按小时切片能发现“某KOL总在21:00发测评此时情感分最高”这种洞察远超单纯日均值。visualmap_opts设min_0.3, max_0.7是经验值——微博情感分集中在0.2~0.8两端截断避免色阶失真。6. 避坑指南5个让项目卡在上线前的真实问题与解法微博反爬策略每月迭代以下问题均来自2024年Q2真实翻车现场按发生频率排序6.1 现象登录成功但调用/ajax/statuses/mymblog返回{ok:0,msg:这里还没有内容}原因微博服务端校验Referer和Origin头。session.get()默认不带Referer而接口要求Referer: https://weibo.com/u/1234567890且Origin: https://weibo.com。解决在请求前手动设置headersheaders { Referer: fhttps://weibo.com/u/{uid}, Origin: https://weibo.com, User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } resp session.get(url, headersheaders, timeout10)6.2 现象text_raw中大量span classurl-icon标签未被清洗导致词频统计出现urlicon原因微博新版HTML中短链被包裹在span classurl-icon内正则[^]未覆盖闭合标签。解决增强清洗正则匹配自闭合和闭合标签text re.sub(r[^]*?(?:/|/[^]*), , text) # 匹配 xxx/ 和 xxx/xxx6.3 现象jieba.analyse.textrank返回空列表或全是单字“的”、“了”原因TextRank需要足够文本长度50字符和词共现关系单条微博太短平均32字导致图构建失败。解决合并所有微博文本后再提取而非逐条提取all_text .join(df_clean[text_clean].tolist()) # 拼成长文本 keywords jieba.analyse.textrank(all_text, topK20, ...)6.4 现象SnowNLP.sentiments对含“不”“没”“未”的句子判断错误如“这手机不错”判0.3原因SnowNLP训练语料偏新闻对微博口语否定句泛化弱。解决前置规则修正检测否定词程度副词def fix_negation(text, score): neg_words [不, 没, 未, 勿, 莫, 非, 休] degree_words [很, 非常, 特别, 超级, 巨, 超] if any(neg in text for neg in neg_words): if any(deg in text for deg in degree_words): return max(0.1, score * 0.4) # 强否定大幅降权 else: return max(0.1, score * 0.7) # 普通否定适度降权 return score6.5 现象Pyecharts生成HTML在Chrome打开空白控制台报Uncaught ReferenceError: echarts is not defined原因Pyecharts 2.x默认使用在线CDNhttps://cdn.jsdelivr.net/npm/echarts5.4.3国内网络不稳定导致加载失败。解决切换为本地资源模式并指定echarts版本from pyecharts.globals import CurrentConfig, NotebookType CurrentConfig.ONLINE_HOST https://cdn.jsdelivr.net/npm/echarts5.4.3/dist/ # 确保版本匹配 # 或更稳妥下载echarts.min.js到本地static/目录用 # CurrentConfig.ONLINE_HOST ./static/7. 进阶技巧用Redis缓存增量抓取把日报流程变成可持续流水线单次分析是demo可持续日报才是生产力。我给客户搭的生产环境核心是Redis缓存微博ID时间戳每次只抓取created_at 最新缓存时间的新微博避免全量重抓。7.1 Redis缓存设计每个用户一个keyvalue为最新微博ID和时间import redis r redis.Redis(hostlocalhost, port6379, db0, decode_responsesTrue) def get_last_fetched(uid): 获取用户上次抓取的最新微博ID和时间 key fweibo:last:{uid} data r.hgetall(key) if data: return { weibo_id: data.get(weibo_id), created_at: pd.Timestamp(data[created_at]) } return None def update_last_fetched(uid, weibo_id, created_at): 更新用户最新微博记录 key fweibo:last:{uid} r.hset(key, mapping{ weibo_id: weibo_id, created_at: created_at.isoformat() }) r.expire(key, 30*24*3600) # 缓存30天 # 抓取逻辑改造只抓新微博 def fetch_incremental_weibo(session, uid): last get_last_fetched(uid) if last: # 构造参数从last[created_at]之后开始翻页 pass # 具体实现见下文 else: # 首次抓取取最近1页 return fetch_user_weibo(session, uid, page1)7.2 增量抓取策略用时间范围ID双保险避免漏抓微博API不支持since_id参数只能靠created_at过滤。但用户可能删博或修改时间所以必须结合IDdef fetch_new_weibo(session, uid, since_timeNone, since_idNone): 获取指定时间/ID之后的新微博 all_new [] page 1 while True: weibos fetch_user_weibo(session, uid, pagepage, count20) if not weibos: break # 过滤created_at since_time 且 weibo_id since_id for w in weibos: dt datetime.strptime(w[created_at], %a %b %d %H:%M:%S %z %Y) if since_time and dt since_time: continue if since_id and w[id] since_id: continue all_new.append(w) # 若当前页最后一条已满足条件停止翻页 if weibos and (not since_time or dt since_time): page 1 else: break return all_new # 生产调用 # last get_last_fetched(uid) # new_weibos fetch_new_weibo(s, uid, last[created_at], last[weibo_id]) # if new_weibos: # df_new parse_weibo_list(new_weibos) # update_last_fetched(uid, new_weibos[0][id], new_weibos[0][created_at])为什么用双条件created_at可能被用户修改如编辑微博时间weibo_id是全局递增整数不会变但微博ID并非严格时间序有批量发布场景双保险确保不漏、不重。实测某政务微博一天发200条单用时间过滤漏3条加ID后100%覆盖。最后说句实在的这套流程我跑了两年从手动改cookie到全自动日报最大的教训是——别跟微博反爬赛跑要跟业务需求赛跑。与其花两周破解新验证码不如用现有能力先产出“竞品官微近7天情感趋势对比”PPT让老板看到价值再申请预算买商业API或部署代理池。技术是手段交付才是目的。希望帮到你。本文还有配套的精品资源点击获取