简介:这是一份面向Python爬虫学习者与数据采集需求者的新浪微博数据爬取工具,可连续抓取一个或多个微博用户(如胡歌、迪丽热巴等)的用户信息与微博内容,并支持写入文件或数据库,适合想练习真实站点采集、了解登录鉴权与数据落库的中级学习者。资源包共77个文件,以44个py源码文件为核心,辅以14个md说明文档、11个html页面及yml、json、conf等配置与示例文件,压缩包约111KB,结构清晰便于按模块阅读。已有4070人学习下载。程序支持txt、csv、json及MySQL、MongoDB、SQLite多种输出方式,可选择性只爬取用户信息,并能下载原创与转发微博中的图片、视频及Live Photo视频,免cookie版与常规版功能相近,便于对照理解鉴权差异与采集流程。
1. 从一条微博的字段说起:这个爬虫项目到底能拿到什么
很多人第一次写微博爬虫,目标都很朴素:把某条微博下面的转发、评论、点赞数拉下来看看。真跑起来才发现,页面是动态渲染的,接口有签名参数,翻页到第三页就开始返回空数据,登录态一过期整条链路直接断掉。这个用 Python 写的微博数据采集项目,解决的正是这类“能跑一次、跑不了长期”的问题:它把登录态维护、请求签名、分页翻页、字段清洗这几件事拆成独立模块,最终输出结构化的微博正文、发布时间、互动计数和用户基础信息,落到本地文件或数据库里。
适合谁用?做舆情观察的学生、需要批量整理公开微博内容做文本分析的研究者、想练手接口逆向和工程化爬虫的开发者。它不是一个“一键爬全站”的黑盒工具,而是一套可以按需裁剪的采集骨架——你要采某个话题下的微博,改配置;你要采某个用户的全部历史微博,换入口;你要加字段,改解析层。下面按“能干什么 → 怎么跑 → 怎么改”的顺序,把这个项目的结构、关键代码和踩过的坑讲清楚。
2. 项目结构与技术选型:请求层、解析层、存储层怎么分工
2.1 目录骨架与各模块职责
这个项目的目录不复杂,但分层很清楚,核心思路是“请求归请求、解析归解析、存储归存储”,任何一层出问题都能单独替换。典型结构如下:
weibo_spider/ ├── config/ │ ├── settings.py # 全局配置:并发数、超时、重试次数、存储方式 │ └── cookies.txt # 登录态 Cookie,运行时读取 ├── spider/ │ ├── request.py # 请求封装:会话复用、重试、限速 │ ├── parser.py # 响应解析:JSON 字段提取、HTML 兜底解析 │ └── paginator.py # 分页控制:页码推进、终止条件判断 ├── storage/ │ ├── csv_writer.py # CSV 落盘 │ └── db_writer.py # SQLite/MySQL 落库 ├── utils/ │ ├── logger.py # 日志 │ └── cleaner.py # 文本清洗:去表情、去话题符号、时间格式化 ├── main.py # 入口:读配置 → 建任务 → 调度采集 └── requirements.txtconfig/settings.py是整个项目的“旋钮面板”,并发数、单次请求间隔、最大重试次数、目标存储介质都从这里读。spider/request.py负责把一次 HTTP 请求包装成“失败可重试、频率可控”的动作,parser.py只关心“给我一段响应,我还你一个字典”,paginator.py决定“什么时候继续翻、什么时候停”。这种拆分的好处是:接口字段变了只动 parser,反爬策略变了只动 request,存储换库只动 storage,互不牵连。
技术栈上,请求用requests.Session复用连接和 Cookie,解析以 JSON 为主(微博的移动端接口大多返回结构化 JSON),个别页面用lxml或BeautifulSoup兜底,存储默认 CSV、可选 SQLite,日志用标准库logging。没有引入 Scrapy 这类重框架,原因是采集目标相对固定、量级可控,轻量脚本更容易调试和二次改造。
2.2 请求层:会话复用、限速与重试
请求层是整个项目最容易翻车的地方。微博接口对请求频率敏感,短时间高频请求会触发验证或直接返回空。项目里的做法是“单 Session + 固定间隔 + 指数退避重试”,核心代码如下:
import time import random import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry def build_session(cookies, timeout=10, retries=3): session = requests.Session() # 复用连接,减少握手开销 adapter = HTTPAdapter( pool_connections=10, pool_maxsize=10, max_retries=Retry( total=retries, backoff_factor=1.5, # 退避因子:1.5s, 3s, 6s... status_forcelist=[429, 500, 502, 503], ), ) session.mount("https://", adapter) session.headers.update({ "User-Agent": "Mozilla/5.0 (...) Mobile Safari", "Referer": "https://m.weibo.cn/", }) session.cookies.update(cookies) # 注入登录态 session.request_timeout = timeout return session def fetch(session, url, params=None, min_interval=1.2): # 每次请求前随机抖动,避免固定节奏被识别 time.sleep(min_interval + random.uniform(0, 0.8)) resp = session.get(url, params=params, timeout=session.request_timeout) resp.raise_for_status() return resp.json()build_session里三个参数值得说:pool_maxsize控制连接池大小,单机采集 10 足够;backoff_factor=1.5让重试间隔逐次拉长,避免连续撞墙;status_forcelist只对 429 和 5xx 重试,4xx 里的 403 通常是登录态失效,重试没意义,应该直接抛出让上层处理。fetch里的min_interval是“礼貌间隔”,配合random.uniform抖动,比固定 sleep 更接近真人节奏。这个抖动不是玄学,是血泪经验——固定 1 秒间隔跑几千条,很容易在某个时间点被整段拦截。
2.3 解析层:从 JSON 到干净字段
微博移动端接口返回的 JSON 层级较深,正文、转发数、评论数散在不同节点,还有“全文”和“摘要”两套字段。解析层的职责是把这些差异抹平,输出统一结构:
from datetime import datetime def parse_weibo_item(raw): """把单条微博 JSON 转成扁平字典""" user = raw.get("user", {}) return { "mid": raw.get("id"), "text": clean_text(raw.get("text", "")), # 去 HTML 标签 "created_at": parse_time(raw.get("created_at")), # 统一成 ISO 时间 "reposts": raw.get("reposts_count", 0), "comments": raw.get("comments_count", 0), "attitudes": raw.get("attitudes_count", 0), "user_id": user.get("id"), "user_name": user.get("screen_name"), "followers": user.get("followers_count", 0), } def clean_text(text): # 微博正文常带 <a> 话题标签和表情占位,统一剥离 import re text = re.sub(r"<[^>]+>", "", text) return text.replace("\u200b", "").strip() def parse_time(s): # 接口时间形如 "Mon Jan 01 12:00:00 +0800 2024" return datetime.strptime(s, "%a %b %d %H:%M:%S %z %Y").isoformat()parse_weibo_item里每个get都带默认值,是因为不同接口返回的字段并不完全一致,缺字段时不能让整条解析崩掉。clean_text去掉 HTML 标签和零宽字符,这两类脏数据在后续做文本分析时会污染分词结果。parse_time把微博特有的时间格式转成 ISO 标准串,落库和排序都方便。解析层保持“纯函数”风格——输入原始 JSON,输出字典,不碰网络也不碰磁盘,这样单测好写,字段变更时改一处即可。
2.4 分页与终止条件:什么时候该停
分页是最容易被忽略、又最容易出问题的一环。微博接口用since_id或page推进,翻到一定深度会返回空列表或重复数据。项目里的分页器同时看三个信号:返回条数、since_id是否变化、是否达到配置的最大页数。
def paginate(fetch_page, max_pages=50): since_id = None seen = set() for page in range(max_pages): data = fetch_page(since_id) items = data.get("cards", []) or data.get("data", []) if not items: break # 空列表:到底了 new_count = 0 for item in items: mid = item.get("id") if mid and mid not in seen: seen.add(mid) new_count += 1 yield item if new_count == 0: break # 全是重复:接口在回吐旧数据 since_id = data.get("since_id") or since_id if not since_id: break # 没有游标:无法继续三个终止条件缺一不可:空列表防“翻过头”,new_count == 0防“接口重复返回同一页”,since_id为空防“游标丢失后死循环”。max_pages是兜底保险,防止配置错误导致无限翻页。实际跑的时候,日志里会打印每页新增条数,一旦连续两页新增为 0,基本可以判断到底或触发了限制。
3. 跑起来:环境准备、登录态注入与首次采集
3.1 环境与依赖安装
项目对 Python 版本要求不高,3.8 以上即可。依赖集中在requirements.txt,核心就几个:
python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r requirements.txtrequirements.txt里通常包含requests、lxml、beautifulsoup4、pandas(CSV 落盘时用)。如果只跑基础采集,pandas可以省掉,用标准库csv写也行,但后续做数据清洗时它省事。安装完先跑一次python -c "import requests, lxml"确认没有编译问题,lxml在部分环境需要系统级依赖,装不上就换html5lib兜底。
3.2 登录态注入:Cookie 怎么放、怎么验
这个项目不处理账号密码登录流程,而是要求使用者自己提供有效 Cookie,放进config/cookies.txt。格式是一行一个key=value,运行时读成字典注入 Session:
def load_cookies(path="config/cookies.txt"): cookies = {} with open(path, encoding="utf-8") as f: for line in f: line = line.strip() if not line or line.startswith("#"): continue key, _, value = line.partition("=") cookies[key.strip()] = value.strip() return cookies注入后先做一次“探活”请求,确认登录态有效再开始正式采集,避免跑了一半才发现 Cookie 过期:
def check_login(session): resp = session.get("https://m.weibo.cn/api/config") data = resp.json().get("data", {}) if not data.get("login"): raise RuntimeError("登录态无效,请更新 cookies.txt")check_login打的是配置接口,返回里login为true才算通过。这一步看着多余,但能省掉大量“跑到一半全空”的排查时间。Cookie 有有效期,长期采集建议在入口处加一个定时探活,失效就停任务并告警,而不是硬跑。
3.3 首次采集:从配置到落盘
入口main.py的流程是“读配置 → 建 Session → 探活 → 建任务 → 分页采集 → 逐条解析 → 落盘”。最小可跑配置如下:
# config/settings.py TARGET_TYPE = "user" # user / topic / search TARGET_ID = "1234567890" # 用户 ID 或话题关键词 MAX_PAGES = 20 MIN_INTERVAL = 1.5 STORAGE = "csv" # csv / sqlite OUTPUT_PATH = "output/weibo.csv"TARGET_TYPE决定走哪个入口 URL,TARGET_ID是采集对象,MAX_PAGES控制深度,MIN_INTERVAL控制节奏。跑起来后,控制台会按页打印“第 N 页,新增 M 条”,最终在output/下生成 CSV。第一次建议把MAX_PAGES设小(比如 3),确认字段完整、时间格式正确、中文没有乱码,再放大页数。CSV 用utf-8-sig编码写,Excel 直接打开不乱码。
4. 避坑与排查:五条真实踩过的坑
4.1 现象:前两页有数据,第三页开始全空
原因通常是登录态在采集过程中失效,或者请求频率触发了限制。前者表现为接口返回ok: 0,后者表现为返回空cards但 HTTP 状态仍是 200。解决方式是先看日志里check_login的探活结果,再临时把MIN_INTERVAL调到 3 秒以上重跑。如果调慢后恢复,就是频率问题;如果仍然空,就是 Cookie 过期,换 Cookie 即可。判断依据不要只看状态码,200 不代表拿到数据。
4.2 现象:同一条微博被重复写入多次
原因是分页游标since_id没有正确推进,接口在重复返回同一批数据。项目里靠seen集合去重,但如果中途重启任务,内存里的seen就丢了。解决方式是把已采集的mid落盘,启动时先加载进seen,做“断点续采”。另一个容易忽略的点是since_id有时返回0而不是空,代码里要同时判断not since_id和since_id == 0。
4.3 现象:正文里混着大量 HTML 标签和乱码
原因是直接取了text字段没清洗,或者响应编码判断错误。微博接口有时返回的Content-Type不带 charset,requests会按 ISO-8859-1 解码导致中文乱码。解决方式是在fetch里显式设置resp.encoding = "utf-8",解析前再跑一遍clean_text。如果乱码只出现在个别条目,检查是不是走了 HTML 兜底解析分支,那条分支的编码处理要单独设。
4.4 现象:程序跑一段时间后内存持续上涨
原因是seen集合和日志缓存无限增长,长任务下几万条mid全压在内存里。解决方式是给seen设上限,或者改用磁盘上的布隆过滤器/ SQLite 唯一索引去重。日志方面,logging的 handler 要设maxBytes和backupCount做滚动,否则日志文件也会撑爆磁盘。这类问题短任务看不出来,跑几小时才暴露,属于典型的“上线才翻车”。
4.5 现象:换了个采集目标,解析全报 KeyError
原因是不同入口(用户主页、话题页、搜索页)返回的 JSON 结构不一样,字段名和层级都有差异。解决方式是在parser.py里按TARGET_TYPE分派不同的解析函数,而不是一套parse_weibo_item打天下。公共字段抽成parse_common,差异字段各自处理。改的时候先用一条真实响应存成 JSON 文件做样本,对着样本调解析,比反复请求线上接口快得多,也避免调试过程本身触发限制。
5. 二次开发:加字段、换存储、做增量采集的改造思路
拿到这个项目后,最常见的改造需求有三个:加字段、换存储、做增量。加字段最直接,在parse_weibo_item里补raw.get(...)即可,但要注意新字段在不同入口下可能不存在,默认值必须给。比如想加“是否含图片”,就看pics字段长度,没有就返回False,不要让None流到下游。换存储时,storage/下新增一个 writer,实现和csv_writer相同的write(items)接口,入口处按STORAGE配置分派,这样上层调度代码一行不用改。
增量采集是价值最高的改造。思路是把“已采到的最新mid”持久化,下次启动时作为since_id的起点,只拉新数据。实现上在storage层加一个get_latest_mid(),启动时读出来传给分页器:
def incremental_run(session, storage, target_id): last_mid = storage.get_latest_mid(target_id) # 上次采到的最新一条 fetch_page = lambda since_id: fetch( session, "https://m.weibo.cn/api/container/getIndex", params={"containerid": f"107603{target_id}", "since_id": since_id or last_mid}, ) for item in paginate(fetch_page, max_pages=10): storage.write([parse_weibo_item(item)])last_mid为空时等价于全量首采,非空时只拉比它更新的数据。这里有个细节:since_id传的是“比这个 ID 更新的内容”,所以增量采集天然不会重复拉旧数据,但前提是last_mid存的是真正的最新一条,而不是某一页的中间值。我一般会在写入时同步更新一个meta表记录每个目标的last_mid和最后采集时间,避免多任务并发时互相覆盖。
验证改造是否成功,别只看“跑通了”,要看三个指标:新增条数是否合理(增量首跑应该远小于全量)、last_mid是否随采集推进、重复率是否接近零(用mid去重后计数对比原始条数)。我吃过一次亏,增量逻辑写反了,把since_id传成了最旧的一条,结果每次都在重复拉历史数据,日志里新增条数一直很大却没察觉,直到存储里出现大量重复才反应过来。从那以后我每次改分页或增量逻辑,都强制先跑一个小目标、打印首尾mid和新增计数,确认方向对了再放大。希望这套拆解能帮你少走几步弯路,把采集骨架改成自己顺手的样子。
本文还有配套的精品资源,点击获取