ARTICLE DETAIL

资讯详情

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

微博爬虫全流程:接口抓取、Session重试与jieba词云生成

微博爬虫全流程:接口抓取、Session重试与jieba词云生成 简介一套基于Python的新浪微博爬虫与词云生成完整代码包面向毕业设计、期末大作业及课程设计场景适合具备一定Python基础、希望快速搭建可运行项目的学生参考。资源包含爬虫采集、数据清洗、词云绘制及文档说明代码注释详尽新手也能按步骤部署使用。压缩包共27个文件以py源代码为主辅以xml工程配置、png结果图、ttf字体、md说明文档及csv数据文件整体大小9.03MB结构清晰便于二次修改。目前已有310人学习下载属于导师认可的高分项目可作为答辩展示与功能扩展的起点。下载后简单配置环境即可运行既能理解微博数据获取与文本可视化的完整流程也能直接用于课程报告或毕设演示。1. 微博爬虫别急着解析 HTML先找到能拿到 JSON 的接口很多人拿到微博爬虫需求第一反应是 requests.get 首页然后用正则拼 HTML。微博早就不是服务端渲染PC 端数据由异步脚本加载抓下来的 HTML 只有骨架真正的用户时间线和搜索结果要等 XHR 接口返回 JSON。所以做微博爬虫的第一步是打开开发者工具找到 container/getIndex 接口而不是在 HTML 里挣扎。这个标题里的“高质量代码”我理解有三件事可维护的请求封装、可控的异常重试、能直接跑通的分析闭环。也就是爬虫抓到的数据经过清洗和分词后由 wordcloud 生成词云整个过程拆成爬取、解析、文本分析三层。适合做舆情监控、竞品账号分析或者需要一套完整数据管线的工程师。2. 爬取新浪微博前把数据源锁定在移动端接口和登录态微博的数据源不止一个。PC 端 weibo.com 有最全的数据但它的接口包裹了多层风控参数返回结构也经常跟着前端路由变化。我一般不会从 PC 端开始除非目标数据只在 PC 端展示。更稳定的路径是 m.weibo.cn 的移动端接口返回干净 JSON字段命名统一请求头要求少适合用 requests 直接拿。2.1 移动端接口和 PC 端接口的取舍移动端接口的核心入口有两个一个是根据微博 ID 获取单条内容的 detail/show另一个是列表型数据使用的 /api/container/getIndex。后者承担了绝大部分需求包括用户时间线、搜索、超话、话题页。它通过 containerid 区分数据来源理解这个参数就理解了整张数据地图。用户时间线的 containerid 是 107603 加上用户 UID。搜索接口的 containerid 是 100103type1 加上 q 关键词。这两个场景的返回结构都是 cards 数组每张卡片里有一个 mblog 字段里面包含正文、时间、转发评论点赞数。PC 端类似数据的字段叫 status结构上多一层包装解析起来不如移动端直接。提示不要试图用一个 containerid 通吃所有场景。参数语义不同混用会导致返回空列表或容器异常。2.2 模拟登录的 Cookie 方案与请求头参数爬公开微博不强制登录但只要翻页超过两三页或者请求频率一快未登录状态很快被重定向到 passport 登录页。所以最高效的登录方案是浏览器里登录一次把请求 Cookie 复制到配置文件后续请求直接带上。抓取 Cookie 的步骤很固定用 Chrome 无痕窗口打开 m.weibo.cn登录后按 F12找到任意一个 XHR 请求在 Request Headers 里复制 Cookie 完整值。关键字段是 SUB 和 SUBPSUB 是身份令牌过期前能维持登录态SUBP 记录登录方式。如果看到 401第一反应不是重试而是回去重新复制 Cookie。2.2.1 请求头参数对照表Header推荐值作用User-AgentMozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X)iOS UA 更贴近移动端比某些默认 UA 更不容易被拦X-Requested-WithXMLHttpRequest标记为异步请求Refererhttps://m.weibo.cn/u/目标UID部分接口校验来源页Cookie登录后复制维持会话身份Acceptapplication/json让服务端优先返回 JSON2.2.2 用 Session 发一个最小请求验证登录态拿到 Cookie 后不要急着写完整爬虫先发一个最小请求确认登录态。用 requests.Session 的原因很简单Session 能复用 TCP 连接也能统一管理请求头。下面这段代码可以在 Jupyter 或脚本里直接跑import requests session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X), X-Requested-With: XMLHttpRequest, Referer: https://m.weibo.cn/, Cookie: 你浏览器复制的完整Cookie, }) url https://m.weibo.cn/api/container/getIndex params {type: uid, value: 1234567890, containerid: 1076031234567890} resp session.get(url, paramsparams, timeout10) print(resp.status_code) print(resp.json()[data][cards][0][card_type])如果输出 200 并且 card_type 为 9说明登录态有效。如果返回 401 或跳转问题基本出在 Cookie 过期。返回 418 则说明请求被风控这时候换 Cookie 没有意义需要降低频率或更换出口 IP。IP 代理池是常见做法但不要把代理接入写死到主流程后续维护会非常痛苦。3. 高质量微博爬虫代码用 Session 和重试机制抓取时间线与搜索词设计爬虫类时我优先考虑的不是一次能抓多少页而是请求失败后代码能不能自己缓过来。微博接口不稳定超时、5xx、连接重置都会随机出现。如果把所有请求堆在业务逻辑里遇到一次超时整个流程就断根本谈不上高质量。所以第一步是封装一个带重试和限速的请求层。3.1 封装一个带重试和限速的微博爬虫类下面的代码把 Session、重试策略、随机延迟封装成一个类。它解决三个具体问题网络抖动时自动重试请求间隔不形成固定节奏遇到身份类错误时快速失败而不是傻等。import time import random import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry class WeiboCrawler: def __init__(self, cookie: str, delay_range(2, 5)): self.session requests.Session() self.session.headers.update({ User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X), X-Requested-With: XMLHttpRequest, Referer: https://m.weibo.cn/, Cookie: cookie, }) retry Retry(total3, backoff_factor1, status_forcelist[500, 502, 503, 504]) adapter HTTPAdapter(max_retriesretry, pool_connections10, pool_maxsize10) self.session.mount(https://, adapter) self.delay_range delay_range def get_json(self, url: str, params: dict) - dict: self._safe_delay() response self.session.get(url, paramsparams, timeout10) if response.status_code 401: raise PermissionError(Cookie 已过期请重新登录并更新 Cookie) if response.status_code 418: raise RuntimeError(触发微博风控请降低频率或更换代理IP) response.raise_for_status() return response.json() def _safe_delay(self): time.sleep(random.uniform(*self.delay_range))Retry 是这里最值得讲解的参数。total3 表示单个请求最多自动重试 3 次backoff_factor1 让重试间隔按 1、2、4 秒递增避免每次失败后立刻重试造成更猛烈的风控。status_forcelist 只放 5xx因为 5xx 是服务端临时故障重试大概率有效401 和 418 放进去只会加快被封。delay_range 是每次请求前的随机睡眠区间。随机延迟比固定 sleep 好因为均匀间隔会被识别为机器行为。但要注意如果你在并发环境使用这个类多个线程共用同一个延迟范围可能同时发出请求所以后面的并发设计要单独拿出来考虑。3.2 抓取用户时间线并解析核心字段用户时间线接口只需要三个参数typeuid、value 对应用户 ID、containerid 是 107603 加用户 ID。返回的 data.cards 是微博卡片数组card_type 为 9 的卡片才包含 mblog 字段。解析时把传播数据一起取出来方便后面分析热度。def get_user_timeline(self, uid: str, max_page: int 3): all_statuses [] for page in range(1, max_page 1): params { type: uid, value: uid, containerid: f107603{uid}, page: page, } data self.get_json(https://m.weibo.cn/api/container/getIndex, params) cards data.get(data, {}).get(cards, []) for card in cards: if not isinstance(card, dict): continue mblog card.get(mblog) if card.get(card_type) 9 and mblog: all_statuses.append({ id: mblog.get(id), created_at: mblog.get(created_at), text: mblog.get(text), reposts_count: mblog.get(reposts_count), comments_count: mblog.get(comments_count), attitudes_count: mblog.get(attitudes_count), }) print(f第 {page} 页完成累计 {len(all_statuses)} 条) return all_statuses这里有一个很容易踩的坑时间线里会混入广告卡片、关注推荐卡片、长文章卡片它们不一定有 mblog。如果不判断 card_typeNone 值会直接进列表后续 text 字段会抛 AttributeError。所以先确认 card_type 再访问 mblog顺序不要反。3.3 按关键词搜索微博时用 since_id 而不是页码搜索接口看起来和用户时间线很像但分页逻辑完全不同。搜索接口用 since_id 作为游标如果还是用 page 参数第二页会返回与第一页重复的结果。我第一次写搜索爬虫时就在这里浪费了半天后来抓包才发现 cardlistInfo 里才藏着真正的游标。def search_by_keyword(self, keyword: str, max_pages: int 3): results [] containerid f100103type1q{keyword} since_id for _ in range(max_pages): params { containerid: containerid, page_type: searchall, since_id: since_id, } data self.get_json(https://m.weibo.cn/api/container/getIndex, params) cards data.get(data, {}).get(cards, []) if not cards: break for card in cards: mblog card.get(mblog) if mblog: results.append(mblog.get(text)) since_id data.get(data, {}).get(cardlistInfo, {}).get(since_id) if not since_id: break return results第一次请求时 since_id 传空字符串服务端返回第一页并把下一批游标放在 cardlistInfo.since_id 里。把游标原样传给第二次请求就能持续向后翻。搜索接口对频率更敏感建议把 delay_range 调成 (4, 8)。如果只是少量关键词分析可以先用串行不要一开始就上并发。3.4 解析 text 字段与清理 HTML 的先后顺序时间线返回的 text 是 HTML 片段里面包含 a 标签、表情图、微博营销短链。直接拿去做分词会得到一堆 “网页链接”“http” 这类噪音词。清洗顺序比较讲究先替换标签再删 URL最后处理 和话题。import re def clean_weibo_text(raw_text: str) - str: text re.sub(r[^], , raw_text) text re.sub(rhttps?://\S, , text) text re.sub(r[\u4e00-\u9fa5a-zA-Z0-9_], , text) text re.sub(r#.*?#, , text) text text.replace(微博正文, ).strip() return text顺序不能倒。如果先删 URLHTML 标签里还有 href 指向的地址没有被处理后面正则再想匹配就得写两条规则。话题 #xxx# 默认删除因为词云图里充满 “#” 符号会干扰视觉但如果你的分析主题就是话题词保留也可以。微博正文中常带的 “微博正文” 是套模板时加入的冗余文本属于高发噪源应该替换为空。4. 微博文本到词云jieba 分词、停用词和 wordcloud 的中文字体避坑爬虫拿到的是文本列表词云需要的是词频字典。从字符串到词频中间隔着分词。wordcloud 自带的 generate(text) 是按空格和标点切分的对中文无效必须先用 jieba 把句子切词再用 Counter 统计频次最后用 generate_from_frequencies 传字典。4.1 用 jieba 分词时先加载自定义词典jieba 默认词典基于通用语料对微博内容支持一般。微博里有大量网络用语、垂直行业术语、账号昵称不处理会被切成碎片。例如“知识图谱”可能被切成“知识/图谱”影响词云可读性。解决方案是在初始化时加载自定义词典。import jieba with open(weibo_dict.txt, w, encodingutf-8) as f: f.write(大模型 5 n\n) f.write(知识图谱 5 n\n) f.write(数据可视化 3 n\n) jieba.load_userdict(weibo_dict.txt) text 这个项目的重点是知识图谱和大模型的应用 words jieba.lcut(text) print(words) # [这个, 项目, 的, 重点, 是, 知识图谱, 和, 大模型, 的, 应用]自定义词典每行三列词语、词频、词性。词频也叫权重权重越大 jieba 越倾向于把它切成完整词。一般设 3 到 5 就能生效。词性可写可不写不写就留空。随代码发布时给一个空模板让使用者按行业往里补词。不要试图把所有词都塞进词典只在分词结果出现明显错误时补充。4.2 停用词过滤放在分词后而不是分词前有些人喜欢先删“的、了、是”然后再去分词。这样做有隐患jieba 在切分短句时依赖虚词判断边界提前删掉可能导致“跌跌撞撞”这类词被切坏。正确顺序是先分词再逐词检查是否在停用词集合里同时过滤长度小于 2 的词和纯数字。from collections import Counter def load_stopwords(path: str) - set: with open(path, r, encodingutf-8) as f: return {line.strip() for line in f} def get_word_freq(clean_texts: list, stopwords_path: str, top_n: int 100): stopwords load_stopwords(stopwords_path) counter Counter() for text in clean_texts: for word in jieba.lcut(text): word word.strip() if len(word) 2: continue if word in stopwords: continue if word.isdigit(): continue counter[word] 1 return counter.most_common(top_n)len(word) 2 过滤单字词因为微博文本里的单字大多是语气词或切分残留。纯数字过滤是为了避免年份、微博 ID 这类数字占满词云。如果你的分析对象是手机型号或汽车型号数字反而重要这个判断可以根据业务调整。停用词表不建议用网上随便抄的通用表要往里面补微博专有词转发、分享、网页链接、图片、评论、点赞、全文、展开。4.3 wordcloud 生成词云font_path 和 collocations 是高频坑wordcloud 默认使用 ASCII 字体不知道中文为何物。直接对中文词频调用 generate图片上全是方块。解决方法是显式指定一个支持中文的 ttf 字体路径。Windows 上可以用 C:/Windows/Fonts/msyh.ttcLinux 上通常需要安装 fonts-wqy-zenhei 后再指定路径。from wordcloud import WordCloud import matplotlib.pyplot as plt freq_dict dict(get_word_freq(all_texts, stopwords_pathstopwords.txt, top_n100)) wc WordCloud( width1600, height900, font_pathC:/Windows/Fonts/msyh.ttc, background_colorwhite, max_words200, max_font_size150, colormapviridis, collocationsFalse, ).generate_from_frequencies(freq_dict) plt.figure(figsize(10, 6)) plt.imshow(wc, interpolationbilinear) plt.axis(off) plt.show()collocationsFalse 要特别注意。wordcloud 默认会把相邻词组成二元组比如“数据”“可视化”自动拼成“数据可视化”然后按新词频绘制。这会让词云出现大量不存在的组合词与预期词频不一致。关掉后只画 freq_dict 里的词。colormap 控制颜色映射和 mask 配合时颜色由图片灰度决定。wordcloud 参数建议值说明font_path中文字体绝对路径不设置中文全是方块width / height1600 / 900分辨率太低导致字体模糊background_colorwhite 或 rgba(0,0,0,0)透明背景用于叠加图片max_words100-200词太多视觉上一团乱collocationsFalse关闭二元组更贴合词频统计masknumpy 数组用 PIL 打开黑白图片转数组生成形状模板mask 参数可以让词云形状变成圆形或品牌 logo。常见做法是用 PIL 打开一张纯色背景的图片转换成 numpy 数组后传给 mask。需要额外注意 mask 中白色区域是空白区域非白色才是绘图区域。如果图片背景不干净词云会被背景噪点切割得很碎。5. 文档说明与工程质量把爬虫和词云包装成可交付脚本标题里最后四个字“文档说明”决定了这个爬虫是个人工具还是交付物。别人拿到代码最想看到的不是设计模式而是三分钟内跑起来的路径。所以高质量交付物必须包含 README、requirements.txt、config 文件以及一个最小的运行示例。5.1 文档里必须出现的运行步骤和配置示例README 里只写四步安装依赖、改 Cookie、运行爬虫、生成词云。不要用一大段话描述项目背景直接给代码。requirements.txt 要锁版本因为 jieba 和 wordcloud 在不同版本下分词结果和渲染表现差异很大。pip install -r requirements.txt python weibo_spider.py --uid 123456 --pages 3 python generate_wordcloud.py --input weibo_data.json --top 100配置文件可以用 YAML把 Cookie、延迟、字体路径分门别类。这样代码里不硬编码敏感信息换账号时只需要改配置。# config.yaml weibo: cookie: SUBxxx; SUBPxxx delay_range: [2, 5] timeout: 10 cloud: font_path: C:/Windows/Fonts/msyh.ttc top_n: 100 stopwords_file: stopwords.txtConfigParser 在 Python 里解析 ini 很顺手但 YAML 更直观适合嵌套结构。注意 YAML 里 Cookie 值不要用单引号包裹整个字符串否则请求时会把引号一起发送导致认证失败。5.2 三个必调的参数超时、请求间隔、重试上限超时设置在 10 秒到 15 秒之间比较合理。太短在弱网环境频繁误判太长会拖慢整体速度。请求间隔建议用随机区间而不是固定值比如 delay_range 为 (2, 5)每次请求前随机 sleep 2 到 5 秒。触发 418 后把区间改成 (5, 10)不要只改重试次数。参数默认值调整方向timeout10s网络差调 15频繁超时先看 IPdelay_range(2, 5)触发风控后调成 (5, 10)retry total3不建议超过 5重试过多会扩大封禁面并发设计是另一个话题。微博单账号频率限制非常严格并发 10 个请求几乎必然触发验证码。我建议先用串行加随机延迟跑通再考虑 ThreadPoolExecutor。如果数据量大到必须并发至少要在入口处加 Token Bucket 限速不能直接让线程池全速跑。对于更大规模常见路线是 Scrapy 加 Redis 做分布式爬虫但这不是这个标题的默认路径先不用急着上。5.3 验证词云是否失真对比词频表和手工抽检词云渲染得好看不代表分析正确。生成词云后把 freq_dict 前 30 个词打印出来对照原始微博看有没有明显噪音。如果看到大量“转发”“分享”“网页链接”说明停用词表没补全。另一种情况是某个你应该重视的领域词被切碎了就需要回自定义词典里补词。验证方法不复杂随机抽 20 条原始微博手动提炼你认为的高频词再去词频表里查。如果人工结果与词频表差距大优先检查清洗步骤是不是把有意义的内容误删了。下面这段代码可以直接用来做结果检查for word, num in freq_dict[:30]: print(f{word}\t{num})到这里爬虫、词云、文档已经形成一个可复用的闭环。剩下的工作就是找一个你关心的微博账号把 Cookie 换成自己的将 delay_range 设置为 (3, 6)跑一遍再对照词频表把停用词补满。本文还有配套的精品资源点击获取
返回列表