ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

微博公开数据爬取实战:登录态维护、文本清洗与中文词云生成

微博公开数据爬取实战:登录态维护、文本清洗与中文词云生成 简介基于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 字段ok1cards 数组完整可翻页数较多转发、评论、点赞数齐全游客态能跑通但不适合做词云分析因为样本量太小。登录态的核心是 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_asciiFalse, indent2), encodingutf-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, encodingutf-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 起的类型注解写法直接写sessionNone也兼容。注意这里没有做代理相关的处理微博对普通频率的爬取并不强制要求轮换出口 IP保持合理间隔比换 IP 更重要。提示Cookie 的有效期一般为几天到几周不等过期后接口会返回 -101。爬虫代码里应该主动检测该状态提示人工重新登录并更新 cookie.txt而不是无限重试。3. 爬虫主流程、接口选取与数据清洗登录态就绪后接下来是选择接口。核心原则是优先用移动端 JSON 接口避免解析桌面版 HTML。桌面版页面大量使用 JS 渲染requests 抓回来的是空壳解析成本高且不稳定。移动端接口直接返回结构化 JSON字段命名稳定是目前抓取微博公开内容最常见的方案。3.1 三个常用接口的 containerid 与翻页机制接口场景请求 URL 模板关键参数用户时间线https://m.weibo.cn/api/container/getIndexcontainerid 固定为230283 uid翻页用 since_id话题 / 超话时间线https://m.weibo.cn/api/container/getIndex先请求 containerid 获取超话的容器 ID再按 since_id 翻页实时热搜榜https://m.weibo.cn/api/container/getIndexcontainerid 固定为106003type25t3disable_hot1filter_typerealtimehot不需要翻页理解 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 f230283{uid}_-_HOT results [] for page in range(max_pages): params { containerid: containerid, since_id: since_id, } resp session.get(url, paramsparams, timeout10) 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 classexpand展开全文c/span。如果直接把这段 HTML 扔给分词器词云里会出现大量标签字符和界面用语。需要做一层专门针对微博语料的清洗规则如下import re def clean_weibo_text(raw_html, full_textNone): 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(rhttps?://\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_pathNone, stopwordsNone): 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生成的词云里所有中文都会渲染成方块。常见做法是直接指定操作系统自带的中文字体WindowsC:/Windows/Fonts/simhei.ttf黑体或msyh.ttf微软雅黑macOS/System/Library/Fonts/PingFang.ttc苹方Linux先用fc-list :langzh查可用中文字体例如/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, width1600, height1000, background_colorwhite, max_words200, maskmask_img, contour_width1, contour_colorsteelblue, ) wc.generate_from_frequencies(build_freq_dict(clean_texts, stopwordsmy_stopwords)) wc.to_file(weibo_wordcloud.png)遮罩图的配色不需要刻意处理成纯黑白wordcloud 会把图片中非白色的区域识别为可填充区域。但有个前提图片主体必须是闭合的实心形状细线轮廓类的图片生成的词云会散成碎片。contour_width1会让遮罩轮廓以描边形式出现在词云外围视觉上更容易看出形状不需要时直接删掉这两个参数。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, encodingutf-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_retry3): for attempt in range(max_retry): resp session.get(url, paramsparams, timeout10) 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可以快速跳转到对应时间点继续抓取这在跨天补抓场景下很有用等于用游标做了时间点定位比一页一页翻节省大量请求。本文还有配套的精品资源点击获取
返回列表