简介面向Python爬虫学习与社交媒体数据分析的微博数据抓取项目包适合具备基础Python语法、希望掌握requests请求与BeautifulSoup解析的开发者。压缩包共77个文件约106KB以44个py源文件为核心配套14个md说明文档、11个html页面以及配置、测试与辅助脚本便于对照代码理解反爬策略、动态User-Agent、代理IP及异常处理等关键实现。目前已有1187人学习参考。项目内含完整爬虫框架从配置加载、请求下载、内容解析到数据存储均有清晰模块划分并带有测试用例与常见问题文档可帮助读者快速搭建微博数据采集流程进一步结合Pandas开展热门话题、用户行为等分析同时兼顾Robots协议与合法抓取边界。1. 这个新浪微博爬虫项目到底能干什么做舆情监测那阵子我发现门户新闻的更新速度总比微博慢一拍热搜里挂出来的话题往往比新闻稿早半小时。这套用 python 写的新浪微博爬虫就是当时为了解决「微博数据怎么快速落库」这个问题沉淀下来的资源。它不依赖 Selenium 模拟点击也不去碰复杂的网页解析直接走微博移动端接口用 requests 就能跑通关键词搜索、翻页、字段清洗、MySQL 入库这条完整链路。适合要做舆情分析、竞品关键词监控、或者拿微博数据做研究的人下载后改个关键词就能跑二次开发也有现成的位置可以插。2. 技术选型为什么走移动端接口而不是网页端这个项目最初的设计目标只有一个在可控的反爬压力下用最少的依赖拿到最干净的数据。所以技术栈没有追求大而全核心就三样requests 做网络请求、json 做解析、PyMySQL 做入库。没有引入 Scrapy是因为这个场景搜索词数量不大、单机跑就够Scrapy 的中间件和管道反而让新手改起来费劲。没有用 Selenium是因为微博网页端的风控对无头浏览器并不友好验证码一弹就得人工介入还不如直接换接口方案。2.1 三个可选的采集入口我选了哪个我拆过的微博爬虫资源里采集入口基本逃不出下面这三个各有各的坑入口返回格式登录要求典型问题s.weibo.com 网页端HTML需要完整登录态页面结构频繁变解析规则经常失效weibo.com/ajax 新接口JSON需要 cookie 且风控严格高频请求容易触发验证需要维护较完整的 Cookiem.weibo.cn 移动端接口JSON登录态要求稍低访问频率受限但字段全结构稳定我这个版本默认走m.weibo.cn/api/container/getIndex这个移动端搜索接口。选它的原因很直接返回的是标准 JSON省去了写正则抠 HTML 的时间字段比网页端完整昵称、发布时间、转评赞数都在同一层结构里不需要二次请求。代价是请求频率必须压住后面避坑章节会细说这个接口的脾气。2.2 登录态是怎么处理的新浪系接口对匿名请求限制得很死不带登录态的话翻几页就会收到{ok:0}的拒绝响应。这个项目的做法是「手动登录一次成果存起来复用」避免在代码里维护一整套模拟登录逻辑那样既容易被风控盯上又牵扯短信验证码之类的麻烦事。常见的做法是先用浏览器打开https://m.weibo.cn手动登录后从开发者工具里把Cookie请求头复制出来放进data/cookies.json。爬虫启动时用requests.Session把这个 Cookie 注入进去整个爬取过程都复用这个会话。要注意 Cookie 是有时效的一般能撑几小时到几天失效了重新复制一次即可这也是绝大多数同类爬虫资源的通用处理方式。2.3 依赖清单requirements.txt里只有四个包装起来没什么门槛requests2.31.0 PyMySQL1.1.0 pandas2.1.4 pyyaml6.0.1pandas 不是给爬虫用的是给最后的数据导出准备的二次开发时如果想把结果直接存成 Excel它已经在环境里了。PyYAML 用来读配置文件把关键词、翻页深度、请求间隔这些经常要改的东西从代码里剥出去。3. 目录结构与数据存储设计拿到项目先看目录这个结构的组织方式是按「每个环节一个文件」来拆的配置、登录、搜索、解析、存储各司其职改某个环节不会影响其他文件。我习惯把这种结构叫「线性爬虫」数据从网络到数据库是一条直线走下来的调试的时候打日志也方便顺着链路找问题。3.1 项目目录长什么样weibo_spider/ ├── config.yaml # 关键词、翻页页数、请求间隔等运行参数 ├── requirements.txt # 依赖清单 ├── login.py # 读取并注入 Cookie建立 Session ├── search.py # 关键词搜索请求负责翻页 ├── parser.py # 解析 JSON 响应提取微博字段 ├── storage.py # 数据入库带简单的去重 ├── main.py # 入口串联整个流程 ├── data/ │ └── cookies.json # 存放浏览器复制的 Cookie ├── logs/ │ └── spider.log # 运行日志 └── sql/ └── init.sql # 建表语句config.yaml是改得最频繁的文件关键词、页数、间隔都在这里。日志文件单独放logs/目录是因为爬虫跑起来之后排查反爬问题基本都靠它日志里我会刻意把每次请求的 URL 和返回码都打出来其中返回码是判断有没有被封的关键依据。3.2 数据库表设计建表语句在sql/init.sql里核心思路是用微博 ID 做主键做天然去重同一个搜索词反复跑不会插入重复数据。CREATE TABLE weibo_post ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, user_name VARCHAR(64), content TEXT, publish_time DATETIME, repost_count INT DEFAULT 0, comment_count INT DEFAULT 0, like_count INT DEFAULT 0, keyword VARCHAR(64), crawl_time DATETIME ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段类型有几个需要留意的地方。content用 TEXT 而不是 VARCHAR因为微博正文最长可以到几千字VARCHAR 在 MySQL 里默认上限不够。publish_time在接口返回里是毫秒级 Unix 时间戳入库前先转成 DATETIME这样后面按时间段筛选舆情数据时才不会对着时间戳发愁。crawl_time记录本次抓取时间是做增量抓取留下的伏笔二次开发那一章会专门说它。3.3 入库时的去重策略接口返回的id字段在微博体系内是全局唯一的所以建表时就把它定义成主键。插入时用INSERT IGNORE重复主键会被 MySQL 直接丢弃而不是报错这样就省掉了「先查询再插入」的两次往返INSERT IGNORE INTO weibo_post (id, user_id, user_name, content, publish_time, repost_count, comment_count, like_count, keyword, crawl_time) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)这个写法在爬虫里非常实用。因为翻页时经常会出现「上一页的最后一条和下一页的第一条是同一条」这种情况对爬虫来讲这是家常便饭。用主键去重比用SELECT先判断再插入省了一半时间在高频抓取场景下优势明显。4. 核心模块拆解一个关键词从输入到入库的完整链路这一章顺着代码执行顺序把五个核心文件过一遍每个文件只讲最关键的那一段逻辑。看完这部分你对这个项目的运行机制心里就有底了。4.1 login.py把 Cookie 变成会话import json import requests def load_session(cookie_pathdata/cookies.json): with open(cookie_path, r, encodingutf-8) as f: cookie_data json.load(f) cookie_str cookie_data.get(cookie, ) session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1, Referer: https://m.weibo.cn/ }) for item in cookie_str.split(;): item item.strip() if in item: key, value item.split(, 1) session.cookies.set(key, value) return session这里把移动端浏览器的 UA 写成了 iPhone Safari。因为访问的是m.weibo.cn移动端 UA 拿到的接口响应最稳定。Cookie 解析那段用的是最简单的字符串切分没有引额外的库。如果复制出来的 Cookie 里有多个键值对每一段之间是用分号隔开的split(;)能直接拆开。4.2 search.py搜索请求与翻页参数import time from urllib.parse import quote def search_keyword(session, keyword, page1): url https://m.weibo.cn/api/container/getIndex containerid f100103type1q{quote(keyword)} params { containerid: containerid, page_type: searchall, page: page } resp session.get(url, paramsparams, timeout10) resp.raise_for_status() data resp.json() return datacontainerid是这个接口最关键的参数100103type1q是固定的搜索类型前缀后面的关键词要经过 URL 编码中文词直接拼上去会出问题。page_typesearchall表示搜索全部微博而不是只搜热门或者只搜关注的人。page控制页数这个接口从 1 开始数但问题在于翻页到了一定深度之后page参数会失效改成用since_id才能继续翻避坑章节会有详细说明。4.3 parser.py从 JSON 里取出需要的字段def parse_search_result(data, keyword): cards data.get(data, {}).get(cards, []) rows [] for card in cards: if card.get(card_type) ! 9: continue mblog card.get(mblog, {}) row { id: mblog.get(id), user_id: mblog.get(user, {}).get(id), user_name: mblog.get(user, {}).get(screen_name), content: mblog.get(text, ), publish_time: mblog.get(created_at), repost_count: mblog.get(reposts_count, 0), comment_count: mblog.get(comments_count, 0), like_count: mblog.get(attitudes_count, 0), keyword: keyword } rows.append(row) return rows移动端搜索返回的 JSON 结构里cards是一个列表里面混着多种卡片类型只有card_type为 9 的才是微博正文卡片。这个判断是新手最容易漏的不写的话解析结果里会混进一堆推荐位和话题位。mblog是微博内容的载体里面user对象嵌套着用户信息。发布时间created_at这里取的是原始值可能是「刚刚」「5分钟前」这种相对时间也可能是「12月5日」统一换算成绝对时间戳的工作放在入库前做。这个细节看着小但如果不处理后续按时间线做分析时数据会乱得一塌糊涂。4.4 storage.py入库与时间戳转换import pymysql import time def save_rows(rows): conn pymysql.connect( host127.0.0.1, userroot, password123456, databaseweibo, charsetutf8mb4 ) cursor conn.cursor() sql (INSERT IGNORE INTO weibo_post (id, user_id, user_name, content, publish_time, repost_count, comment_count, like_count, keyword, crawl_time) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)) for row in rows: ts convert_time(row[publish_time]) cursor.execute(sql, ( row[id], row[user_id], row[user_name], row[content], ts, row[repost_count], row[comment_count], row[like_count], row[keyword], time.strftime(%Y-%m-%d %H:%M:%S) )) conn.commit() cursor.close() conn.close()convert_time这个函数负责把接口返回的相对时间转成YYYY-MM-DD HH:MM:SS针对「刚刚」「N分钟前」「今天」「MM-DD」分别做分支处理这是微博接口比较坑的地方同一个字段返回值却不统一网上很多抓下来的数据这块都是脏的。这里先不展开整段逻辑二次开发那章你可以自己补一个符合自己场景的时间解析版本。4.5 main.py调度与限速import random import time from login import load_session from search import search_keyword from parser import parse_search_result from storage import save_rows import yaml cfg yaml.safe_load(open(config.yaml, encodingutf-8)) session load_session() for keyword in cfg[keywords]: for page in range(1, cfg[max_page] 1): data search_keyword(session, keyword, pagepage) rows parse_search_result(data, keyword) if rows: save_rows(rows) time.sleep(random.uniform(cfg[min_interval], cfg[max_interval]))调度逻辑很直白外层循环遍历关键词内层循环遍历页数每爬一页随机睡几秒。随机延迟用的是random.uniform在最小值和最大值之间浮动比固定time.sleep(3)更难被风控识别出规律。config.yaml里min_interval和max_interval我一般分别设 3 和 6也就是平均 4.5 秒一页这个频率跑移动端接口相对安全。5. 微博爬虫常见问题排查我踩过的四个坑爬虫这个行当跑通一次不算本事能持续跑不被封才算。下面四条是我在调试过程中真实遇到过的坑每条都按「现象 → 原因 → 解决」的方式说清楚你照着排查能少走很多弯路。5.1 接口一直返回{ok:0}代码却没有任何报错这个现象最迷惑人因为 HTTP 状态码是 200requests也正常返回了但 JSON 里的ok字段是 0表示服务端拒绝服务。原因基本是没带登录态或登录态失效m.weibo.cn对未登录请求只放行很小的流量额度。解决办法是检查data/cookies.json里的 Cookie 是否过期重新从浏览器复制一份替换进去。我一般会额外加一段判断如果data.get(ok) 0直接抛异常并写日志防止静默失败的问题——否则爬虫会傻乎乎地把空结果录进库你还以为今天数据没更新。代码里加上判断后整个项目会健壮很多尤其是挂定时任务的时候失败要让它响起来而不是静默过去。5.2 翻页超过 20 页后page 参数彻底失灵现象是前 20 页数据正常第 21 页之后返回的内容和前面完全重复。原因在于这个搜索接口的深翻页机制不是靠page字段而是依赖响应 JSON 里返回的since_id它是下一页的游标。解决办法是改一下搜索逻辑把since_id保存下来并在下一次请求时带进paramsdef search_loop(session, keyword, max_page20): page 1 since_id None while page max_page: params { containerid: f100103type1q{quote(keyword)}, page_type: searchall } if since_id: params[since_id] since_id else: params[page] page data session.get(https://m.weibo.cn/api/container/getIndex, paramsparams).json() cards data.get(data, {}).get(cards, []) since_id data.get(data, {}).get(since_id) if not cards: break yield cards, since_id page 1注意这里每次请求后要把响应里的since_id存起来下一次请求时换成它page参数就不能再带了否则两个参数会打架。如果你只想抓热门领域的前几页数据改这个逻辑的必要性不大但想深挖数据的话就得忍受这个接口的「游标翻页」机制。5.3 入库后中文全部变成问号第一次入库数据后查表我整个人是懵的内容字段里的中文全是「??」。原因很典型MySQL 建库时charset用的默认latin1装不下中文。解决办法是建库时明确指定utf8mb4CREATE DATABASE weibo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;同时连接串里也要带charsetutf8mb4就是我 4.4 节里写的那样。另外表结构里VARCHAR和TEXT字段长度超过一定阈值时MySQL 对索引长度有硬限制但这里只有主键是索引不存在这个问题。如果你自己加了别的索引字段注意控制长度不然建表会报Specified key was too long。5.4 从网页端复制 Cookie 后频繁触发验证码这个坑有点反向踩的感觉Cookie 明明有效但请求频率一快就弹验证码页面。原因是网页端 Cookie 的权限比移动端大但风控也更敏感。解决办法有两个一是尽量用移动端m.weibo.cn登录后再复制 Cookie二是把请求间隔拉长。我在config.yaml里把间隔调成 5 到 10 秒之后验证码出现的频率明显下降。很多人觉得慢但微博数据采集本来就是细水长流的活与其被封了换号重来不如一开始就慢一点。6. 二次开发把它改成增量抓取与定时任务拿到这套代码最常见的改造诉求是「我不想每次全量爬只想每天抓新增的微博」。这个项目在设计时其实已经埋好了伏笔就是weibo_post表里的crawl_time和主键去重。做增量只需要改两个地方。第一个地方是config.yaml里增加一个时间窗口配置比如since_time: 2025-01-01 00:00:00然后在search.py的请求参数里把这个时间转成 Unix 时间戳传进去移动端搜索接口会根据发布时间过滤第二个地方是main.py里把全量翻页改成按crawl_time判断入库前先查一下这条微博的crawl_time是否比上次执行时间新是才插入。这样每次运行就只处理新增数据跑完自动结束配合系统的定时任务管理器设置每天的固定时间执行python main.py一套轻量舆情监控就成型了。如果想更省事可以在main.py最后加一段用 pandas 导出 CSV 的逻辑把当天的数据整理成表格方便交差这也是我为什么提前装了 pandas 的原因。我个人的习惯是每次改完代码先在测试库里跑一遍main.py观察日志里的页数和入库条数是否符合预期再切到正式环境。因为这类爬虫最容易出的问题就是接口字段悄悄地变你看着程序在跑实际上啥数据都没抓到这也算是做爬虫的老生常谈了。这个方法帮我避过好几次数据断更事故从那以后我每次上线任何爬虫项目都强制走一遍「日志观察加样本抽查」这套流程希望也能帮到你。本文还有配套的精品资源点击获取