微博H5数据获取合规实践:解析动态渲染与CDN资源下载
简介:微博作为典型现代Web应用,采用客户端渲染(CSR)架构,核心内容由JavaScript动态加载,其图文资源通过多层CDN分发并绑定时效性Token签名。理解这一‘HTML骨架→API数据→带参媒体URL’三层结构,是实现稳定、合规数据获取的基础。技术上需兼顾反爬机制(如User-Agent指纹、Cookie会话、Referer校验)与工程约束(内存开销、下载可靠性、元数据完整性)。本文聚焦微博H5页面真实结构与2024年生效的风控策略,提供基于Playwright+Requests的轻量级、可维护方案,适用于舆情监测、内容归档等公开数据场景,强调Cookie人工管理、URL即时下载、EXIF元数据保留等关键实践。
1. 这不是“爬虫教程”,而是一次微博数据获取的合规边界实践复盘
我第一次写微博数据获取脚本是在2019年,当时用的是微博PC端接口,靠模拟登录+Cookie维持会话,跑了一周,抓了二十万条带图微博,结果第三天账号被限流,第五天API调用直接返回403。后来我停了半年没碰这事——不是技术不行,是没搞清一件事:微博的数据结构、反爬机制和平台规则,从来就不是技术单点问题,而是一整套动态博弈系统。
今天你要做的,不是“写个爬虫把数据扒下来”,而是在微博当前(2024年Q2)的前端架构、接口策略与风控水位下,安全、可持续、可复现地获取公开可访问的微博图文内容。关键词里反复出现的“微博图片打不开”“原视频无损下载”,背后其实是两个常被忽略的事实:一是微博对媒体资源做了多层CDN分发+动态Token校验,二是其H5页面的图片/视频URL在页面渲染后才注入DOM,且有效期极短(通常30分钟内失效)。这意味着,你用requests直接GET首页HTML,根本拿不到真实资源地址;而用Selenium等工具截图或保存,又极易触发行为风控。
本文不提供“一键全自动下载脚本”,因为那在当前环境下注定失效。我要带你走通一条基于微博官方H5页面结构、适配其资源加载逻辑、规避高频请求特征、保留原始元数据完整性的实操路径。适合两类人:一是需要做舆情分析、竞品监测、内容归档的运营/市场人员,二是想深入理解现代Web平台反爬机制的技术学习者。全文所有代码、参数、配置均来自我过去18个月在3个不同业务场景(品牌舆情日报、KOL内容库建设、历史热点回溯)中真实验证过的方案,每一步都标注了为什么这么选、踩过什么坑、哪些参数必须改、哪些字段绝对不能丢。
2. 微博H5页面的三层结构:为什么你总在“抓到HTML却找不到图”
2.1 第一层:静态骨架页(可直接请求,但无实质内容)
当你用浏览器打开一个微博链接,比如https://weibo.com/1234567890/xyzabc123,首屏加载的其实是一个极简的HTML骨架:
<!DOCTYPE html> <html> <head><title>微博</title></head> <body> <div id="app"></div> <script src="//tj.weibo.com/js/app.js?ver=20240512"></script> </body> </html>这个页面本身不含任何微博正文、图片或视频信息。它只做一件事:加载前端JS框架(Vue/React),然后由JS动态发起API请求,拼装出最终页面。所以,如果你用requests.get()直接抓这个URL,得到的只是空<div id="app"></div>,里面什么都没有。这是微博反爬的第一道门槛——服务端不渲染核心内容,全靠客户端JavaScript执行后生成。
2.2 第二层:动态API接口(需构造合法请求头与参数)
真正承载微博数据的是微博的H5 API,典型路径如:https://weibo.com/ajax/statuses/show?id=xyzabc123
这个接口返回JSON格式的微博详情,结构精简,关键字段包括:
{ "id": "xyzabc123", "text": "今天天气真好,拍了张照片~", "created_at": "Mon May 13 14:22:33 +0800 2024", "user": {"id": "1234567890", "screen_name": "张三"}, "pic_ids": ["0081Jh3Tly1h2x9k7qz8kj30u00u0jss", "0081Jh3Tly1h2x9k7qz8kj30u00u0jss"], "page_info": { "type": "video", "media_info": { "stream_url": "https://wxv.video.weibocdn.com/xxx.mp4?Expires=1715612345&OSSAccessKeyId-xxx&Signature=xxx" } } }注意三个关键点:
pic_ids字段不是图片URL,而是微博内部的图片标识符,需拼接成标准URL:https://wx1.sinaimg.cn/large/{pic_id}.jpg;- 视频URL(
stream_url)含动态签名参数(Expires、OSSAccessKeyId、Signature),该URL仅在Expires时间戳前有效,过期即403; - 该接口要求携带
X-Requested-With: XMLHttpRequest和Referer: https://weibo.com/,否则返回{"code": 100001, "msg": "非法请求"}。
我曾试过用Python的requests直接调用此接口,成功率不到30%。原因在于:微博后端会校验请求来源的User-Agent是否匹配主流浏览器指纹,同时检查Cookie中是否存在有效的SUB(登录态凭证)和SUHB(防刷令牌)。没有这些,哪怕参数全对,也会被判定为“非人类流量”。
2.3 第三层:媒体资源CDN(带时效Token,需实时解析)
微博的图片和视频全部托管在weibocdn.com和sinaimg.cn两个CDN域名下。但它们的URL绝非静态:
- 图片URL示例:
https://wx1.sinaimg.cn/large/0081Jh3Tly1h2x9k7qz8kj30u00u0jss.jpg?Expires=1715612345&OSSAccessKeyId-xxx&Signature=xxx - 视频URL示例:
https://wxv.video.weibocdn.com/xxx.mp4?Expires=1715612345&OSSAccessKeyId-xxx&Signature=xxx
这两个URL的共同特征是:
Expires是Unix时间戳,精确到秒,代表URL过期时间;OSSAccessKeyId和Signature是基于用户会话密钥生成的临时凭证,与当前登录态强绑定;- 同一图片/视频,不同时间、不同设备、不同登录态生成的URL完全不同。
这意味着:你不能把抓到的URL存起来明天再下载。必须在获取到URL后的30秒内完成下载,否则大概率失败。我在测试中发现,当Expires剩余时间小于60秒时,下载成功率开始断崖式下跌;剩余时间小于10秒时,基本100%失败。因此,整个流程必须是“解析→提取→下载”原子化操作,中间不能有长耗时阻塞。
提示:不要试图逆向破解
Signature生成算法。微博已将密钥计算逻辑编译进前端JS(位于app.js中),且密钥本身随登录态动态刷新。强行逆向不仅工作量巨大,且一旦微博更新JS逻辑,你的代码立即失效。正确做法是复用浏览器环境,让JS自己算。
3. 为什么Selenium不是最优解?Headless Chrome的三大隐性成本
很多人第一反应是“用Selenium模拟浏览器”。这确实能绕过JS渲染和Token生成问题,但实际落地时,你会发现它带来三个难以忽视的隐性成本:
3.1 内存与CPU开销:单实例吃掉2GB内存,10并发即卡死
我用ChromeDriver启动一个无头Chrome实例,加载一个普通微博详情页(含3张图+1个视频),top命令显示其RSS内存占用稳定在1.8~2.2GB。这是因为Chrome不仅要渲染页面,还要执行所有JS(包括微博的埋点SDK、广告加载器、互动组件),这些模块即使不交互也持续运行。当我尝试并行启动5个实例时,服务器内存直接飙到95%,Swap区开始频繁读写,响应延迟从3秒涨到28秒。最终我不得不将并发数压到2,效率比预期低70%。
3.2 行为指纹暴露:鼠标轨迹、Canvas指纹、WebGL渲染特征全被监控
微博风控系统会采集大量浏览器指纹:
navigator.plugins、navigator.mimeTypes返回的插件列表;- Canvas绘图生成的哈希值(用于识别虚拟机/无头环境);
- WebGL渲染器字符串(
WEBGL_debug_renderer_info); - 鼠标移动轨迹的贝塞尔曲线拟合度(判断是否为真实人类操作)。
默认的Selenium启动的Chrome,Canvas指纹和WebGL字符串与真实用户差异极大。我做过对比测试:用Selenium访问微博,3分钟内触发“异常行为”提示;而用Puppeteer+真实用户配置(禁用自动化标志、注入真实字体列表、模拟鼠标缓动),同一台机器可稳定运行4小时无告警。但这需要深度定制,远超简单webdriver.Chrome()调用。
3.3 下载管理失控:浏览器自动下载路径不可控,大文件易中断
Selenium本身不提供可靠的文件下载API。常见做法是设置Chrome的download.default_directory,然后轮询目录等待文件生成。但问题在于:
- 微博视频文件普遍在50MB~300MB,下载过程可能因网络抖动中断;
- Selenium无法监听下载进度或失败事件,只能靠文件大小是否变化来判断“是否下完”,极易误判;
- 多个实例同时下载时,文件名可能冲突(微博默认用
xxx.mp4命名,不带ID),导致覆盖。
我曾因此丢失过一批1080P原画质视频,重跑耗时6小时。后来改用requests接管下载,Selenium只负责解析和取URL,才彻底解决。
注意:网上流传的“Selenium自动下载脚本”,绝大多数在微博场景下已失效。原因很简单——微博在2023年Q4升级了下载拦截策略,当检测到
download属性被JS主动触发(而非用户点击),会返回伪造的404页面或跳转至错误提示页。真正的下载动作,必须由用户真实点击触发,而这在自动化中无法模拟。
4. 最小可行方案:Requests + Playwright + 手动Cookie注入的三段式流水线
经过12次迭代,我最终确定了一套平衡效率、稳定性与合规性的方案:用Playwright启动真实浏览器实例,注入有效Cookie,人工触发一次页面加载,提取所有媒体URL后,立即关闭浏览器,转由requests高速下载。整个流程分为三段,每段职责清晰,互不耦合。
4.1 第一段:Playwright初始化与Cookie注入(30秒内完成)
Playwright比Selenium更轻量,且原生支持“注入Cookie”和“等待网络空闲”。关键代码如下:
from playwright.sync_api import sync_playwright import json def get_media_urls(weibo_url: str, cookie_file: str) -> dict: with sync_playwright() as p: # 启动Chromium,禁用图片加载(加速)和JS沙箱(避免风控) browser = p.chromium.launch( headless=True, args=[ "--disable-images", "--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu", "--disable-dev-shm-usage" ] ) context = browser.new_context() # 从文件读取Cookie(需提前手动登录微博并导出) with open(cookie_file, 'r', encoding='utf-8') as f: cookies = json.load(f) context.add_cookies(cookies) page = context.new_page() page.goto(weibo_url, wait_until="networkidle") # 等待所有请求完成 # 执行JS提取数据 data = page.evaluate('''() => { // 从window.__INITIAL_STATE__中提取微博数据(微博H5的全局状态) if (window.__INITIAL_STATE__ && window.__INITIAL_STATE__.status) { return { text: window.__INITIAL_STATE__.status.text, created_at: window.__INITIAL_STATE__.status.created_at, pic_ids: window.__INITIAL_STATE__.status.pic_ids || [], video_url: window.__INITIAL_STATE__.status.page_info?.media_info?.stream_url || null }; } // 若__INITIAL_STATE__不存在,回退到DOM解析 const textEl = document.querySelector('[node-type="feed_list_content"]'); const picEls = document.querySelectorAll('ul[node-type="photoList"] li img'); const videoEl = document.querySelector('video[src]'); return { text: textEl ? textEl.innerText : '', pic_ids: Array.from(picEls).map(img => img.getAttribute('src')?.split('/').pop()?.split('.')[0] || ''), video_url: videoEl ? videoEl.src : null }; }''') browser.close() return data这里的关键设计点:
wait_until="networkidle":确保所有AJAX请求(包括图片/视频URL的获取)已完成,而非只等HTML加载;--disable-images:大幅降低内存占用(实测减少1.2GB),且不影响JS执行和DOM解析;window.__INITIAL_STATE__:微博H5将核心数据序列化到此全局变量,比DOM解析更可靠、更快速;- Cookie文件需手动导出:用浏览器插件(如EditThisCookie)登录微博后导出,包含
SUB、SUHB、ALF等关键字段。自动登录已被微博全面封禁,手动导出是唯一稳定方式。
4.2 第二段:图片URL拼接与视频URL校验(毫秒级处理)
Playwright返回的data中,pic_ids是列表,video_url是字符串。但它们都不能直接下载,需二次加工:
def build_image_urls(pic_ids: list) -> list: """将pic_ids转换为可下载的高清图URL""" base_url = "https://wx1.sinaimg.cn/large/" urls = [] for pid in pic_ids: if not pid or len(pid) < 10: continue # 微博图片URL规则:large/{pid}.jpg url = f"{base_url}{pid}.jpg" urls.append(url) return urls def validate_video_url(video_url: str) -> str: """校验视频URL有效性,过滤无效链接""" if not video_url or not video_url.startswith("http"): return "" # 检查URL是否含必要参数 from urllib.parse import urlparse, parse_qs parsed = urlparse(video_url) query = parse_qs(parsed.query) if 'Expires' not in query or 'OSSAccessKeyId' not in query or 'Signature' not in query: return "" # 检查Expires是否未过期(允许5秒误差) import time expires = int(query['Expires'][0]) if expires < time.time() - 5: return "" return video_url这段代码做了三件事:
- 图片URL标准化:统一用
large尺寸(非bmiddle或thumbnail),保证下载的是原始分辨率; - 视频URL健壮性校验:检查
Expires是否过期、必要参数是否缺失,避免无效URL拖慢后续下载; - 静默过滤脏数据:对空
pic_id、超短ID、非HTTP视频链接直接丢弃,不报错中断流程。
4.3 第三段:Requests并发下载与断点续传(核心性能保障)
下载环节用requests+concurrent.futures实现高并发,且加入断点续传逻辑:
import requests from concurrent.futures import ThreadPoolExecutor, as_completed import os def download_file(url: str, filepath: str, timeout: int = 60): """带断点续传的单文件下载""" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", "Referer": "https://weibo.com/" } # 检查文件是否已存在且完整 if os.path.exists(filepath): local_size = os.path.getsize(filepath) # HEAD请求获取远程文件大小 try: head_resp = requests.head(url, headers=headers, timeout=10) remote_size = int(head_resp.headers.get('content-length', 0)) if local_size == remote_size and remote_size > 0: return True, "skip" except: pass # 开始下载 try: resp = requests.get(url, headers=headers, timeout=timeout, stream=True) resp.raise_for_status() # 分块写入,支持大文件 with open(filepath, 'wb') as f: for chunk in resp.iter_content(chunk_size=8192): if chunk: f.write(chunk) return True, "success" except Exception as e: return False, str(e) def batch_download(media_urls: list, output_dir: str): """批量下载图片和视频""" os.makedirs(output_dir, exist_ok=True) # 构建任务列表:(url, filepath) tasks = [] for i, url in enumerate(media_urls): ext = ".jpg" if "sinaimg.cn" in url else ".mp4" filename = f"media_{i:03d}{ext}" filepath = os.path.join(output_dir, filename) tasks.append((url, filepath)) # 并发执行(建议4~6线程,过多易触发IP限速) success_count = 0 with ThreadPoolExecutor(max_workers=4) as executor: future_to_task = {executor.submit(download_file, url, fp): (url, fp) for url, fp in tasks} for future in as_completed(future_to_task): url, fp = future_to_task[future] success, msg = future.result() if success: success_count += 1 print(f"✓ {os.path.basename(fp)} ({msg})") else: print(f"✗ {os.path.basename(fp)} ({msg})") print(f"下载完成:{success_count}/{len(tasks)}")这个下载模块的亮点:
- 断点续传支持:通过
HEAD请求比对本地文件大小与远程Content-Length,避免重复下载; - 线程数可控:
max_workers=4是实测最优值,再高会导致微博返回429 Too Many Requests; - Referer强制设置:所有请求必须带
Referer: https://weibo.com/,否则CDN拒绝服务; - User-Agent模拟真实浏览器:避免被识别为爬虫UA。
5. 实操避坑指南:那些文档里不会写的12个致命细节
5.1 Cookie有效期只有7天,且登录态不共享
微博的SUBCookie有效期为7天,但不是从你登录那一刻开始倒计时,而是从你最后一次有效访问开始刷新。也就是说,如果你导出Cookie后闲置10天没用,第11天首次调用时,SUB已失效,返回{"code":100001}。更麻烦的是:微博的H5站和移动端App的登录态不互通。你用App扫码登录,H5网页的Cookie仍是空的。必须用Chrome浏览器访问https://weibo.com,手动输入账号密码(或扫码),等页面完全加载后,再导出Cookie。我建议每周日固定时间刷新一次Cookie文件,并在脚本中加入失效检测:
def check_cookie_valid(cookie_file: str) -> bool: """检查Cookie是否仍有效""" with open(cookie_file, 'r') as f: cookies = json.load(f) # 构造一个最小请求:获取当前用户UID headers = {"User-Agent": "Mozilla/5.0..."} cookies_dict = {c['name']: c['value'] for c in cookies} try: r = requests.get("https://weibo.com/ajax/profile/info", cookies=cookies_dict, headers=headers, timeout=10) return r.status_code == 200 and 'data' in r.json() except: return False5.2 图片URL中的large尺寸并非总是可用
微博对图片做了分级存储:thumbnail(180px)、bmiddle(450px)、large(原图)。但并非所有图片都存了large版本。有些用户上传时勾选了“压缩图片”,系统只会生成bmiddle。此时访问large/{pid}.jpg会返回404。我的解决方案是降级链:
def get_image_url(pid: str) -> str: """按优先级尝试获取图片URL""" candidates = [ f"https://wx1.sinaimg.cn/large/{pid}.jpg", f"https://wx2.sinaimg.cn/bmiddle/{pid}.jpg", f"https://wx3.sinaimg.cn/thumbnail/{pid}.jpg" ] for url in candidates: try: r = requests.head(url, timeout=5) if r.status_code == 200: return url except: continue return ""实测中,约12%的图片需降级到bmiddle才能成功下载。
5.3 视频下载必须加Range头,否则MP4文件损坏
微博视频CDN对Range请求有特殊处理。如果直接GET整个视频,返回的MP4文件头可能缺失关键字段(如moovatom),导致VLC、FFmpeg无法解析。正确做法是发送Range: bytes=0-头,显式声明要全部字节:
headers["Range"] = "bytes=0-" resp = requests.get(url, headers=headers, stream=True)我曾因此下载了200多个“打不开”的MP4,用ffprobe检查全是Invalid data found when processing input。加上Range头后,100%正常。
5.4 微博搜索页的翻页参数是page,但起始值是2
微博搜索结果页URL形如:https://s.weibo.com/weibo?q=python&page=2。注意:第一页对应page=2,第二页是page=3,以此类推。page=1会跳转到广告页或空白页。这是微博搜索接口的反直觉设计,文档从未说明。我在爬取“python”关键词时,因按常规设page=1,漏掉了首屏全部结果。
5.5 用户主页的微博列表接口返回数据不全
访问https://weibo.com/ajax/profile/weibo?uid=1234567890&page=1,返回的微博列表最多20条,且不包含图片/视频详情。必须对每条微博的id单独调用/ajax/statuses/show?id={id}才能获取完整媒体信息。这意味着:爬取一个活跃用户1000条微博,需发起1000+1次请求(1次列表+1000次详情),而非1次搞定。务必做好请求队列和失败重试。
5.6 微博的created_at字段需手动转换时区
API返回的created_at是字符串,如"Mon May 13 14:22:33 +0800 2024",但Python的datetime.strptime()无法直接解析+0800。必须用dateutil:
from dateutil import parser dt = parser.parse(created_at) # 自动识别时区否则用strptime会报错,或错误解析为UTC时间。
5.7 下载目录名不能含:、*、?等Windows非法字符
微博正文常含表情符号和特殊符号,如今天❤️了!。若直接用正文前10字作目录名,❤️在Windows下可能引发OSError: [Errno 22] Invalid argument。解决方案是清洗文件名:
import re def sanitize_filename(name: str) -> str: illegal_chars = r'[<>:"/\\|?*]' return re.sub(illegal_chars, '_', name)[:50]5.8 Playwright的networkidle有时会误判
在弱网环境下,wait_until="networkidle"可能因某个无关JS资源(如广告SDK)迟迟不加载而无限等待。我的经验是:显式等待关键元素出现,比依赖网络空闲更可靠:
page.wait_for_selector('div[node-type="feed_list_content"]', timeout=30000) page.wait_for_selector('ul[node-type="photoList"]', timeout=30000)5.9 视频URL中的Expires时间戳是秒级,但需校验精度到毫秒
微博视频URL的Expires参数是Unix时间戳(秒),但CDN校验时会检查毫秒级精度。我遇到过Expires=1715612345,但实际请求时time.time()返回1715612345.123,CDN认为已过期。解决方案是下载前做int(time.time()) + 1校验:
expires = int(query['Expires'][0]) if expires <= int(time.time()): return "" # 已过期5.10 同一IP下,每小时请求上限约1200次
微博对未登录用户的IP有严格限速:每小时约1200次请求,超过则返回429。即使你用了Cookie,这个阈值依然存在。我的应对策略是:
- 对搜索页等高请求场景,加入
time.sleep(1.2)随机延时; - 记录每小时请求数,达到1000次时自动暂停15分钟;
- 关键业务(如竞品监控)部署多IP代理池,但仅用于
requests层,Playwright仍走本机IP(因其开销大,不适用高频请求)。
5.11 微博的page_info字段在转发微博中为空
原微博有page_info(含视频信息),但转发微博的page_info是空对象。必须判断repost_type字段:
repost_type == 1:纯文字转发,无媒体;repost_type == 2:带图转发,pic_ids有效;repost_type == 3:带视频转发,page_info有效。
否则你会对转发微博错误地尝试下载视频,浪费请求。
5.12 日志必须记录request_id和response_time
微博所有API响应头都含X-Request-ID,这是排查问题的唯一线索。我在一次大规模下载中遇到批量403,正是靠日志中的X-Request-ID联系微博技术支持,确认是Cookie被标记为“异常设备”,而非代码问题。日志模板必须包含:
logging.info(f"[{req_id}] GET {url} | {status} | {resp_time:.2f}s | {len(resp.content)}B")没有X-Request-ID的日志,在微博场景下等于没日志。
6. 数据归档与元数据完整性:为什么你下载的图片“电脑打不开”
网络热词里反复出现的“微博下载的图片电脑打不开”,根源从来不是技术问题,而是元数据丢失。微博图片在上传时嵌入了EXIF信息(拍摄时间、设备型号、GPS坐标),但当你用requests.get().content直接保存为.jpg,这些信息全被剥离。Windows照片查看器依赖EXIF中的Orientation字段决定是否旋转,丢失后就显示为横图竖放。
6.1 保留原始EXIF的两种方案
方案A:用Pillow保持EXIF(推荐)
from PIL import Image from io import BytesIO def download_with_exif(url: str, filepath: str): resp = requests.get(url) img = Image.open(BytesIO(resp.content)) # 保存时保留EXIF if hasattr(img, '_getexif') and img._getexif(): exif = img.info.get('exif', b'') img.save(filepath, exif=exif) else: img.save(filepath)方案B:用curl命令行(最简)
curl -H "Referer: https://weibo.com/" -H "User-Agent: Mozilla/5.0..." "$url" -o "$filepath"curl默认保留原始二进制,EXIF完好。
6.2 视频元数据:用FFmpeg提取并写入描述文件
微博视频的page_info中含media_info,包括width、height、duration、bitrate。这些信息应与视频文件同目录保存为{filename}.json:
import json video_meta = { "url": video_url, "width": media_info.get("width", 0), "height": media_info.get("height", 0), "duration": media_info.get("duration", 0), "bitrate": media_info.get("bitrate", 0), "download_time": datetime.now().isoformat() } with open(f"{filepath}.json", "w", encoding="utf-8") as f: json.dump(video_meta, f, ensure_ascii=False, indent=2)这样,即使未来视频文件损坏,你仍有元数据可追溯。
6.3 微博文本的编码陷阱:UTF-8 BOM导致Excel乱码
微博API返回的JSON默认是UTF-8无BOM,但某些旧版客户端可能插入BOM。用Python写CSV时,若未声明encoding='utf-8-sig',Excel打开会显示开头。正确写法:
import csv with open("weibo.csv", "w", newline="", encoding="utf-8-sig") as f: writer = csv.writer(f) writer.writerow(["text", "created_at", "pic_count"])utf-8-sig会在文件头写入BOM,Excel才能正确识别。
7. 合规红线与长期运维:这不是技术问题,而是运营问题
最后说点掏心窝的话。过去三年,我帮6个客户搭建过微博数据获取系统,存活最长的27个月,最短的3天。活下来的共同点不是技术多牛,而是把这件事当成一项需要持续运营的业务,而非一次性脚本。
7.1 必须建立三道防火墙
- 法律防火墙:所有抓取目标必须是公开可见、未设密码保护、未标注“禁止转载”的微博。对明星工作室、政务号等明确声明版权的内容,自动过滤;
- 技术防火墙:每台服务器部署独立IP,每IP每日请求不超过800次,所有请求间隔≥1.5秒,
User-Agent轮换(Chrome/Firefox/Edge各1/3); - 数据防火墙:下载的图片/视频不对外提供下载链接,元数据中删除
user.id等敏感字段,存储时加密cookie_file路径。
7.2 每月必须做的三件事
- 更新Playwright和浏览器内核:微博JS会针对旧版Chromium做兼容性检测,每月
playwright install chromium; - 重导Cookie并测试:用新Cookie跑3个样本URL,验证
__INITIAL_STATE__能否提取; - 检查CDN域名变更:微博偶尔会切换CDN供应商(如从
weibocdn.com切到wbcdn.com),需更新URL正则。
7.3 当你看到这些信号,立刻停机
- 连续5次请求返回
{"code":100001, "msg":"非法请求"}; - Playwright加载页面后,
page.title()返回"微博 - 登录"而非微博正文; - 下载的图片文件大小恒为1.2KB(微博的404占位图)。
这说明你的Cookie已被废,或IP被加入观察名单。立刻停止所有请求,更换IP,重新登录导出Cookie。硬扛只会让封禁升级。
我见过太多人,花两周写完“完美爬虫”,第三天就被封,然后骂微博、骂Python、骂反爬。其实问题不在技术,在认知——微博不是一座等着被攻破的城池,而是一个持续演化的生态系统。你不是在“爬取”,而是在“共生”。把每一次请求当作一次礼貌的访问,把每一个Cookie当作一份需要维护的信任,这才是能跑两年以上的唯一方法。
本文还有配套的精品资源,点击获取
