简介:基于Python的微博数据采集与词云可视化项目源码包,面向计算机相关专业的毕业设计、课程设计以及爬虫与文本分析入门学习者。项目采用Scrapy框架搭建完整爬虫工程,包含爬虫核心逻辑、中间件、管道处理、设置配置与自定义工具模块,同时提供词云生成脚本与字体文件,支持从微博采集文本数据到生成可视化词云的完整流程。资源共27个文件,以12个Python脚本为主,辅以5个XML工程配置、4张结果展示图片、1个词云字体文件、1个CSV数据文件和1份Markdown说明文档,压缩包整体9.03MB,目录结构清晰便于按模块阅读。代码均经过运行测试,功能可靠,作者上传时说明答辩评审平均分达96分,适合直接用于毕设演示或在此基础上二次扩展。目前已有502人学习下载,下载后可参考README或文档说明快速理解工程结构。
1. 微博公开数据爬虫的立项与“易爬性”判断
拿到“爬取新浪微博并生成词云”这个需求时,很多团队的第一反应是去找现成源码,但把源码拖下来跑通后会撞上两个问题:要么接口已经失效,要么请求频率被限制得厉害,程序跑不过三分钟。我的判断是,新浪微博公开数据的爬取难度并不在破解加密参数,而在登录态的维护成本和请求频率的隐性限制。词云生成则是另一套逻辑:爬到的原始文本里充斥着“展开全文”“转发微博”“查看图片”这类 UI 噪声词,不处理就直接分词,出来的词云呈现的是微博的界面用语,不是内容的语义。这套流程按“登录态→抓取→清洗分词→词云参数→排错”的顺序展开,每一段都给出可直接运行的最小源码,并标出值得调整的参数。适合需要做舆情观察、热点追踪、博主内容分析的工程师参考,新手按步骤也能跑通一条完整链路。
2. 登录态与 cookie 的获取策略
爬新浪微博的第一步不是写爬虫,而是拿到一个可用的登录态。微博的移动端接口 m.weibo.cn 在游客状态下也能调用,但返回的数据量明显缩水,很多接口直接回 -100 或 -101。登录后能拿到的字段更完整,翻页深度也更大。因此,登录态的获取和维护决定了整套爬虫的生命周期。
2.1 游客访问与登录态返回结果的差异
先看一组我在调试时记录的对比数据:
| 访问方式 | 典型返回 | 能拿到的数据范围 |
|---|---|---|
| 游客(无 cookie) | 部分接口返回 -100,时间线最多前几页 | 可见微博数明显稀疏,like、comment 等计数缺失 |
| 登录(Cookie 含 SUB 字段) | ok=1,cards 数组完整 | 可翻页数较多,转发、评论、点赞数齐全 |
游客态能跑通,但不适合做词云分析,因为样本量太小。登录态的核心是 Cookie 里的 SUB、SUBP、XSRF-TOKEN 三个字段,后两个在调用部分鉴权接口时会参与参数生成,缺了会直接报参数错误。常见的做法是手动登录后导出 Cookie,而不是在代码里硬写账号密码,原因有二:一是微博登录的密码加密逻辑会不定期调整,纯 requests 模拟登录常遇到 RSA 公钥和参数拼接的细节改动,维护成本高;二是登录流程里大概率会遇到滑块验证码,这不是 Python 爬虫能稳定处理的问题,人工介入一次比代码跑一天更划算。
2.2 手动登录导出 Cookie 并写入本地文件
具体操作是:用 Chrome 打开 weibo.com 并登录,按 F12 进入开发者工具,切到 Network 面板,刷新页面,找到任意一条 m.weibo.cn 开头的请求,在 Request Headers 里找到 Cookie 字段,复制整段值。然后执行下面的方法把它存成结构化文件:
import json from pathlib import Path def save_cookie_to_file(cookie_str: str, path: str = "cookie.txt") -> None: cookie_dict = {} for item in cookie_str.split(";"): item = item.strip() if not item: continue if "=" in item: key, value = item.split("=", 1) cookie_dict[key] = value Path(path).write_text( json.dumps(cookie_dict, ensure_ascii=False, indent=2), encoding="utf-8" ) print(f"已保存 {len(cookie_dict)} 个字段到 {path}")这段源码做的事很简单:把浏览器里那一长串按分号分隔的 Cookie 拆成字典,再以 JSON 格式落地。注意拆分时用split("=", 1)而不是split("="),因为部分 Cookie 值里可能包含等号。保存后建议打开文件检查 SUB、SUBP、XSRF-TOKEN 是否都在,这三个字段对应微博的登录凭证、辅助凭证和 CSRF 令牌,任何一项缺失都会影响后续接口的鉴权。
2.3 用 requests.Session 加载 Cookie 并维持会话
requests.Session 会保存连接信息和 Cookie,方便后续复用。把上一步生成的 JSON 文件加载进 Session 时,要指定 domain 为.weibo.com而不是m.weibo.cn,因为移动端接口在处理请求时会重定向到 weibo.com 域名补充设置 Cookie,写宽一点能避免 Cookie 丢失:
import json import requests from requests.cookies import RequestsCookieJar def load_cookie_to_session(path: str = "cookie.txt", session: requests.Session | None = None) -> requests.Session: session = session or requests.Session() data = json.loads(open(path, encoding="utf-8").read()) jar = RequestsCookieJar() for k, v in data.items(): jar.set(k, v, domain=".weibo.com", path="/") session.cookies = jar session.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": "https://m.weibo.cn/", "X-Requested-With": "XMLHttpRequest", }) return sessionUser-Agent 和 Referer 是微博移动端接口识别来源的基础字段,缺了会触发接口层面的请求无效。这里传入的requests.Session | None是 Python 3.10 起的类型注解写法,直接写session=None也兼容。注意这里没有做代理相关的处理,微博对普通频率的爬取并不强制要求轮换出口 IP,保持合理间隔比换 IP 更重要。
提示:Cookie 的有效期一般为几天到几周不等,过期后接口会返回 -101。爬虫代码里应该主动检测该状态,提示人工重新登录并更新 cookie.txt,而不是无限重试。
3. 爬虫主流程、接口选取与数据清洗
登录态就绪后,接下来是选择接口。核心原则是优先用移动端 JSON 接口,避免解析桌面版 HTML。桌面版页面大量使用 JS 渲染,requests 抓回来的是空壳,解析成本高且不稳定。移动端接口直接返回结构化 JSON,字段命名稳定,是目前抓取微博公开内容最常见的方案。
3.1 三个常用接口的 containerid 与翻页机制
| 接口场景 | 请求 URL 模板 | 关键参数 |
|---|---|---|
| 用户时间线 | https://m.weibo.cn/api/container/getIndex | containerid 固定为230283+ uid,翻页用 since_id |
| 话题 / 超话时间线 | https://m.weibo.cn/api/container/getIndex | 先请求 containerid 获取超话的容器 ID,再按 since_id 翻页 |
| 实时热搜榜 | https://m.weibo.cn/api/container/getIndex | containerid 固定为106003type=25&t=3&disable_hot=1&filter_type=realtimehot,不需要翻页 |
理解 containerid 是这套爬虫的关键。230283开头的 containerid 表示“用户发布的微博”,100505开头表示“用户主页信息”,100103开头表示“话题容器”。抓用户时间线时,拼接规则是230283 + uid + "_-_HOT",这个_-_HOT后缀决定排序方式为热门优先,不加的话返回的是普通时间线,数据更全但内容噪声也更多。两种排序的词云结果差异明显,做事件分析时一般用_HOT,做博主内容总结时用普通排序。
3.2 用户时间线的最小抓取脚本
下面这段源码抓取指定用户的公开微博正文,控制最大翻页数,并在每页之间做间隔:
import requests import time from typing import List, Dict def fetch_user_timeline(session: requests.Session, uid: str, since_id: str = "", max_pages: int = 5) -> List[Dict]: url = "https://m.weibo.cn/api/container/getIndex" containerid = f"230283{uid}_-_HOT" results = [] for page in range(max_pages): params = { "containerid": containerid, "since_id": since_id, } resp = session.get(url, params=params, timeout=10) data = resp.json() if data.get("ok") != 1: print(f"第 {page + 1} 页返回异常:{data.get('msg')}") break cards = data.get("data", {}).get("cards", []) for card in cards: mblog = card.get("mblog", {}) if not mblog: continue results.append({ "id": mblog.get("id"), "text": mblog.get("text"), "created_at": mblog.get("created_at"), }) since_id = data.get("data", {}).get("since_id", "") if not since_id: break time.sleep(1.5) return results翻页逻辑是这段源码里最值得留意的部分:第一页请求时不传 since_id 或传空字符串,响应里的data.since_id是下一页的凭证。这个设计不同于传统的页码翻页,它是基于时间戳的滚动游标,好处是能避免新发微博插入导致的数据重复。time.sleep(1.5)控制每页间隔,这个值可以根据接口返回速度调整,调成 0.5 在短时抓取中问题不大,长时间跑建议保持在 1 以上,减少触发频率限制的概率。返回的 results 列表里每项都保留了微博的唯一 ID,这个 ID 在下一步去重清洗时要用。
3.3 从原始 HTML 到词云输入的清洗规则
微博接口返回的text字段不是纯文本,而是带了 HTML 标签的富文本。常见的有:<a href="...">#话题#</a>、<br />、以及长微博截断后的<span class="expand">展开全文c</span>。如果直接把这段 HTML 扔给分词器,词云里会出现大量标签字符和界面用语。需要做一层专门针对微博语料的清洗,规则如下:
import re def clean_weibo_text(raw_html, full_text=None): text = raw_html # 长微博截断时,完整内容在展开按钮的请求里,full_text 由外层传入 if full_text: text = full_text # 去掉 a 标签、span 标签,保留话题词 #xx# text = re.sub(r"<[^>]+>", "", text) # 微博特有的展开提示 text = text.replace("展开全文c", "").replace("(作者已设置仅关注可见)", "") # 去掉 URL 和 @用户名 text = re.sub(r"https?://\S+", "", text) text = re.sub(r"@[\w\u4e00-\u9fa5]+", "", text) # 合并连续空白 text = re.sub(r"\s+", " ", text).strip() return text注意这里的去 @ 操作:@[\w\u4e00-\u9fa5]+会匹配 @后直接跟用户名的情况,但如果微博正文里含邮箱地址,可能会误伤。更稳妥的做法是先去掉 URL 再去 @,因为邮箱通常出现在网页链接附近。话题词#xxx#默认保留,如果你是做话题事件分析,这类词是最重要的特征词;如果你做的是博主个人风格分析,反而建议在后续步骤里把话题词加进停用词表。去重逻辑不必单独写模块,把上一步的id字段存进一个 set,遍历结果时先判重即可。
提示:
isLongText字段为 true 时,说明text里的内容是截断版。微博会提供另一个detail_url字段指向完整文章,需要在循环里额外发起请求拼接 full_text。这个字段并不总出现,代码里用full_text参数预留了入口,实测大约 10% 的微博命中长文本场景。
4. jieba 分词与 wordcloud 中文词云的参数调整
这一步是词云效果的分水岭。很多人的词云做得难看,问题不在词云库本身,而在分词阶段没有按微博语料的特点做处理。jieba 和 wordcloud 都是成熟工具,但默认参数跑出来的词云,几乎必然包含大量“转发微博”“查看图片”“网页链接”这类垃圾特征。
4.1 分词输出的两种选择:TF-IDF 关键词与纯词频统计
先区分两种输出形态:jieba 的extract_tags基于 TF-IDF 算法抽取关键词,输出带权重;jieba.lcut做的是全量切词,配合Counter统计得到的是纯词频。两种结果都能喂给词云,但语义不同:
| 输出类型 | 数据结构 | 适用场景 |
|---|---|---|
| TF-IDF 关键词 | [("词", 权重), ...] | 提炼语料中最具区分度的词,适合做总结 |
| 纯词频统计 | {"词": 次数} | 反映语料中词的真实出现密度,词云视觉上更自然 |
我做词云时一般用纯词频统计,因为 wordcloud 的绘图逻辑只看频次,权重数值没有意义。extract_tags的结果如果想用,需要手动转成{"词": 权重}字典,此时词频和权重混在一起,词云的字号比例反而失真。下面是比较稳的分词与统计源码路径:
import jieba from collections import Counter def build_freq_dict(text_list, user_dict_path=None, stopwords=None): if user_dict_path: jieba.load_userdict(user_dict_path) stopwords = stopwords or set() counter = Counter() for text in text_list: words = jieba.lcut(text) for w in words: w = w.strip() if len(w) < 2: # 过滤单字、空格 continue if not w[0].isalpha(): # 过滤数字、符号开头的 token continue if w in stopwords: continue counter[w] += 1 return dict(counter.most_common(300))len(w) < 2这行过滤掉所有的单字,对中文词云至关重要。中文单字独立出现时基本没有语义区分度,比如“的”“是”“在”,这些词应该交给停用词表处理。w[0].isalpha()在微博语料里会过滤掉“2024”“666”“3D”这类非字母开头的 token,避免词云被数字淹没。这里的user_dict_path是可选参数,当语料里有微博特色词汇(人名、产品名、粉丝称谓)时,可以准备一个自定义词典文件,每行一个词,提升 jieba 的分词准确率。
4.2 wordcloud 的中文字体、遮罩与停用词参数
4.2.1 font_path 必须指向具体字体文件
wordcloud 默认的渲染字体不含中文字形,如果不设置font_path,生成的词云里所有中文都会渲染成方块。常见做法是直接指定操作系统自带的中文字体:
- Windows:
C:/Windows/Fonts/simhei.ttf(黑体)或msyh.ttf(微软雅黑) - macOS:
/System/Library/Fonts/PingFang.ttc(苹方) - Linux:先用
fc-list :lang=zh查可用中文字体,例如/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc
字体选黑体或思源黑体这类无衬线字体会比宋体更清晰,因为词云的字号大小差异很大,衬线字在小字号和稀疏排布时容易糊。
4.2.2 遮罩图与生成频率字典的组合
要生成特定形状的词云,需要把遮罩图片转成 numpy 数组传给mask参数。生成频率字典时,如果数据来自上文的 Counter 结果,直接传给generate_from_frequencies,这会比传文本列表更高效,因为跳过了重复分词:
import numpy as np from PIL import Image from wordcloud import WordCloud mask_img = np.array(Image.open("mask.png")) wc = WordCloud( font_path="/System/Library/Fonts/PingFang.ttc", width=1600, height=1000, background_color="white", max_words=200, mask=mask_img, contour_width=1, contour_color="steelblue", ) wc.generate_from_frequencies(build_freq_dict(clean_texts, stopwords=my_stopwords)) wc.to_file("weibo_wordcloud.png")遮罩图的配色不需要刻意处理成纯黑白,wordcloud 会把图片中非白色的区域识别为可填充区域。但有个前提:图片主体必须是闭合的实心形状,细线轮廓类的图片生成的词云会散成碎片。contour_width=1会让遮罩轮廓以描边形式出现在词云外围,视觉上更容易看出形状,不需要时直接删掉这两个参数。
4.2.3 微博语料的停用词表要分两层维护
微博的 UI 噪声词和通用停用词是两个集合。通用停用词用常规的中文停用词表即可,比如“我们”“你们”“这个”“那个”;微博特有的噪声词必须从语料里自己去发现,比较典型的有“转发了”“查看图片”“网页链接”“分享至”“赞”“评论”“收藏”。
我的做法是维护一个独立文本文件 weibo_stopwords.txt,每次爬完一批数据,先用默认停用词跑一版词云,把里面明显属于 UI 元素的词追加进这个文件,再重新生成。这个迭代做两轮,词云质量就能达到可接受的程度。build_freq_dict里的stopwords参数可以直接传这个文件的读取结果:
def load_stopwords(path: str = "weibo_stopwords.txt") -> set: return {line.strip() for line in open(path, encoding="utf-8")}5. 词云效果差、接口返回异常时先查这三个地方
排错顺序很重要。词的层面出了问题,先看分词和停用词;数据层面出了问题,先看接口返回状态码;请求层面出了问题,先看频率控制。
5.1 词云输出全是单字或数字
这个词云基本没法看。问题几乎都出在分词后的过滤条件上:len(w) < 2漏掉了长度等于 1 的判断,或者漏掉了w[0].isalpha()这类对 token 首字符的检查。还有一种情况是语料本身就不干净,清洗阶段没有把 “展开全文c” 里的 “c” 单独切开,它会作为单字高频出现在词云里。排查时把build_freq_dict的输出打印前 50 条,看 top 词里有没有明显的 UI 垃圾词,有就补进 weibo_stopwords.txt。
5.2 接口返回 -100 或 -101
-100 表示参数错误,最先怀疑的是 containerid 拼接出错,其次是 Session 没有正确携带 Cookie。可以打一个探测请求验证登录态:
curl -X GET "https://m.weibo.cn/api/config/list" \ -H "Cookie: $(cat cookie.txt | tr '\n' ';')"如果响应里ok不是 1,说明 Cookie 已失效或格式不对。-101 通常是请求未通过服务端验证,常见原因包括缺少 Referer 头、User-Agent 太老、请求过于频繁。验证方法是把浏览器里最新的完整 Cookie 原样替换到 cookie.txt,再跑一次,大概率恢复。如果恢复后跑十几页又出现 -101,则基本可以判定是频率问题。
5.3 请求频率与断点续爬的工程细节
微博的接口频率限制不是按秒算的固定阈值,而是滑动窗口机制。连续高频率请求会触发 -101,此时退避策略是:第一次暂停 5 秒,第二次 10 秒,第三次 20 秒,指数退避直到恢复。同时把已抓取的微博 id 存到本地文件,做成断点续爬,比每次从头跑更省时间。以下是一个简单的频率控制包装函数:
import time def safe_get(session, url, params, max_retry=3): for attempt in range(max_retry): resp = session.get(url, params=params, timeout=10) if resp.status_code == 200 and resp.json().get("ok") == 1: return resp.json() wait = 5 * (2 ** attempt) print(f"第 {attempt + 1} 次请求失败,等待 {wait} 秒") time.sleep(wait) return {}此外,微博的 since_id 翻页机制里有一个容易被忽略的进阶用法:since_id参数可以直接使用上一页返回的游标值,也可以使用上一页里某一篇微博的原始 ID。把 since_id 换成中间某条微博的 ID,可以快速跳转到对应时间点继续抓取,这在跨天补抓场景下很有用,等于用游标做了时间点定位,比一页一页翻节省大量请求。
本文还有配套的精品资源,点击获取