Python+Selenium微博数据采集实战:登录态复用与滚动加载 简介面向需要快速采集微博历史数据的Python学习者与爬虫开发者围绕“微博辟谣”账号展示了一套基于selenium的完整采集工程。项目同时覆盖移动版与PC网页版两种渠道在单页面内采用多线程抓取内容并以多线程异步方式处理多个页面对理解动态网页渲染、页面解析和并发调度有直接参考价值。压缩包共15个文件包含10个py源码、3个xlsx结果文件、1个md说明文档及1个gitignore整体仅41KB轻量易部署。py文件按main、crawl_handle、批量/单行爬取等模块拆分xlsx为不同采集场景下的输出样例md记录使用指引结构清晰适合快速定位学习重点。已有1072人学习下载。资料提供的不仅是可直接运行的案例还配合作者博客专题中的源码分析能帮助赶时间的同学快速还原环境配置、驱动安装、多线程调度等关键环节减少从零摸索的成本。1. 微博数据采集离不开Python和Selenium一个能自己跑完的采集管线我在模拟项目X里接过一个需求每天把30个指定博主的微博正文、发布时间、转发量存档。最开始用requests直接请求页面拿到的是半空壳大量内容要靠接口二次签名维护成本极高。最后落地的方案就是基于PythonSelenium的采集工程也就是标题里这类WBCrawler.zip——用真实浏览器渲染页面直接读DOM绕开逆向接口签名的过程。这类工程解决的问题很具体微博页面大量内容由JavaScript动态渲染普通HTTP请求拿不全页面还有登录校验和反自动化检测脚本跑久了容易静默失败。Selenium的定位就是控制浏览器打开页面模拟人的滚动、等待、点击再把正文、时间、评论数按选择器摘下来。如果要做长期、结构化、可复现的微博数据采集这套思路值得投入。下文从选型、登录态、采集循环到踩坑按一个真实工程的顺序展开每段都可以直接抄进你自己的WBCrawler。2. 登录态是采集的第一道关Cookie注入与user-data-dir复用的取舍采集微博数据遇到的第一个问题往往不是解析而是能不能一直带着登录态。网页版不登录能看一部分内容但评论数、完整正文、转发明细这类字段基本都要登录后才稳定出现。更麻烦的是登录态会在采集途中过期脚本一旦没有感知后半段全部采成登录页的同一份空数据。我的习惯是先把登录方案定死再写解析逻辑。常见做法是两种一种是把登录后的Cookie导出程序启动时再注入另一种是让Selenium复用浏览器用户数据目录。我两种都试过最后长期用的是后者。下面先说清为什么Selenium更适合微博采集再给一套可以直接跑的登录代码。2.1 为什么选Selenium而不拿requests硬啃直接请求HTML能不能拿到微博内容能拿到一部分。微博信息流是典型的动态渲染页面首屏HTML只有骨架和少量初始化数据真正的微博正文、发布时间、转发数是由JavaScript在浏览器里异步请求接口后渲染出来的。直接用requests请求页面得到的是半空壳要拿全数据要么去逆向接口签名要么让浏览器替我们执行这些脚本。接口逆向这条路不是不能走但维护成本很现实微博接口的请求里带时间戳、随机参数和签名校验字段某些字段的生成逻辑黑匣子一样页面改版或参数规则调整后爬取代码就要跟着返工。Selenium的做法是把“执行JavaScript”这件事交给真实浏览器脚本只负责打开页面、滚动、等待元素出现、读取DOM。换来的是稳定性代价是速度和资源占用。方案数据完整性维护成本适合场景requests直接抓HTML低拿不到动态渲染内容低但数据残只抓静态元数据requests逆向接口高但依赖签名算法高规则一变就返工有专业逆向能力的小团队Selenium渲染页面高跟随真实页面中选择器有维护成本长期稳定采集如果你的需求是一次性抓几百条数据用requests硬啃没有问题但像WBCrawler这种要每天跑、要断点续采、要换账号的工程Selenium的稳健性会省掉很多半夜被叫醒的麻烦。2.2 用user-data-dir留住登录态第一次手动登录以后自动复用Selenium登录方案有两条常见路线。一条是Cookie注入手工登录后把Cookie导出成字符串启动时通过driver.add_cookie灌进去。优点是可以把Cookie放在配置里多台机器共用缺点是一旦Cookie过期脚本不会自己重新登录而且微博的Cookie里带有校验用的字段简单拷贝有时候会被拒绝。另一条是复用浏览器用户数据目录也就是user-data-dir我更推荐这条。user-data-dir的原理很直接Selenium启动浏览器时指定一个本地目录浏览器的Cookie、LocalStorage、会话数据都写进这个目录。第一次启动时你手动登录一次之后每次启动都复用这份数据不需要再把Cookie字符串塞进代码。对长期采集来说这个方案比Cookie注入更省心也避免了Cookie硬编码在配置文件里的泄露风险。from selenium import webdriver from selenium.webdriver.chrome.options import Options opts Options() # 第一次先手动登录之后复用这个目录里的登录态 opts.add_argument(user-data-dir./wd_profile) # 固定窗口尺寸避免页面响应式布局导致元素位移 opts.add_argument(window-size1440,900) # 去掉“正在受自动化软件控制”的提示条 opts.add_experimental_option(excludeSwitches, [enable-automation]) driver webdriver.Chrome(optionsopts) driver.get(https://weibo.com) # 按实际登录页地址替换这段代码里user-data-dir./wd_profile最关键目录不存在时浏览器会新建登录后所有状态都留在这个目录里window-size1440,900是让页面按固定视口渲染避免不同分辨率下元素坐标漂移excludeSwitches参数去掉自动化横幅减少一部分前端检测的触发概率。第一次运行时浏览器窗口会弹出来你手动登录一次再关掉程序后续启动就自带登录态了。2.3 登录态失效怎么发现不能等到采完才察觉登录态是会过期的而且过期后页面常常不报错只是把内容区悄悄换成登录引导或者空白状态。如果你的采集循环里没有“登录态探测”脚本会继续滚动、继续解析、继续写入最后给你产出一堆没用的数据。我一般会在启动后先做一次轻量探测用“是否还能看到登录入口”作为判断依据。from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def ensure_login(driver): wait WebDriverWait(driver, 10) try: # 已登录页面通常看不到醒目的“登录”按钮 login_btn wait.until( EC.visibility_of_element_located((By.XPATH, //*[contains(text(),登录)])) ) print(登录态已失效请在弹出的浏览器中手动登录) input(登录完成后按回车继续...) except Exception: print(登录态有效开始采集)这段探测代码用的是“反向判断”如果能在10秒内等到一个包含“登录”文本的可见元素就说明当前是未登录状态如果等不到默认登录态还在。注意它只适合有人值守的第一版。无人值守跑过夜时我会把input()改成“检测到失效就停止写数据并保留截图”具体做法放到后面避坑章节里展开。3. 核心采集逻辑滚动加载、字段解析与增量落盘登录打通之后真正决定采集质量的是三个环节页面滚动怎么驱动、DOM字段怎么稳定提取、数据怎么落盘才能断点续采。这三个环节分别对应微博信息流的懒加载机制、前端DOM的易变性和长时间任务的崩溃恢复需求。下面按顺序给出一套可复用的实现。3.1 滚动加载的驱动方式小步滚动加耐心等待微博时间线的加载方式是“滚动到接近底部时异步加载下一页”一次滚到底不会把内容全部加载出来反而可能因为请求过快触发校验。我习惯把滚动拆成小步每次向下滚动800到1000像素然后固定等待1到2秒同时检查页面是否还在变高。连续3次高度不变就认为已经到底。这里有两个容易翻车的细节。一个是等待时间不能太短加载是异步的元素先插入DOM、图片和评论数后补齐等待时间不足会采到缺少字段的中间态。另一个是退出条件有些页面底部有“加载更多”按钮滚动到底后还要判断按钮是否可点击有的则直接无限流判断依据只有页面高度。下面这段先覆盖“无限流”的常见情况。import time def scroll_feed(driver, max_steps60): unchanged 0 last_height driver.execute_script(return document.body.scrollHeight) for step in range(max_steps): # 每次只滚一小段给异步加载留时间 driver.execute_script(window.scrollBy(0, 900);) time.sleep(1.5) new_height driver.execute_script(return document.body.scrollHeight) if new_height last_height: unchanged 1 if unchanged 3: break else: unchanged 0 last_height new_height return last_height这段代码里max_steps60是保险丝防止页面异常时无限循环900是单次滚动像素可以按屏幕高度调整2K屏可以调到12001.5秒是等待间隔网络差时建议加到2秒连续3次高度不变才退出避免网络抖动导致误判。注意unchanged计数器在高度变化时要归零否则容易提前退出。3.2 字段提取选择器要集中配置别赌死一个类名字段定位是整个工程里最脆的一环。微博前端类名经常带混淆前缀还时不时调整。直接依赖class全名等于把鸡蛋放在一个篮子里。我一般优先用相对稳定的位置关系先按文本或者固定属性找到微博容器再在容器内部定位正文、时间和作者。容器级别的选择器会写成一个常量集中放在配置区方便失效时一次替换。from selenium.webdriver.common.by import By import hashlib # 选择器集中配置页面改版时只改这里 ITEM_SELECTOR (By.XPATH, //section[contains(class,feed)]) TEXT_SELECTOR (By.XPATH, .//div[contains(class,content)]) TIME_SELECTOR (By.XPATH, .//time) def extract_weibo(item): data {} try: data[text] item.find_element(*TEXT_SELECTOR).text.strip() except Exception: data[text] try: data[created_at] item.find_element(*TIME_SELECTOR).get_attribute(datetime) or except Exception: data[created_at] # 唯一标识优先取 mid缺失时用正文前32字做备用键 data[mid] item.get_attribute(mid) or if not data[mid]: data[mid] hashlib.md5(data[text][:32].encode(utf-8)).hexdigest() return data这段代码核心是“容器内相对查找”item是上一步拿到的单条微博根节点TEXT_SELECTOR和TIME_SELECTOR都以.开头限定只在这个节点内部找避免采到旁边推荐位的内容。get_attribute(mid)比文本更适合作去重键因为文本可能重复万一微博容器没有mid属性再用正文前32字的MD5兜底。这里的选择器是按微博常见DOM结构写的真实使用前一定要打开开发者工具核对类名以当天页面为准。字段来源处理要点text正文容器text属性首尾清洗去掉多余换行created_attime元素datetime属性统一存ISO格式便于排序mid容器mid属性去重主键缺失时用正文哈希3.3 增量落盘与断点续采JSONL写入加历史去重采集不是一次性任务跑一半挂掉很正常。如果最后一次性写文件一旦进程崩溃前面几小时的数据全丢。我习惯用JSONL每采完一条就追加写入一行同时调用flush()把缓冲落到磁盘。下次启动时把历史文件里的mid读进一个set新数据先判重再写等于天然断点续采。import json import os def load_seen(fp): seen set() if not os.path.exists(fp): return seen with open(fp, encodingutf-8) as f: for line in f: try: obj json.loads(line) if obj.get(mid): seen.add(obj[mid]) except json.JSONDecodeError: continue return seen def save_weibo(driver, out_fp, seen, max_steps60): scroll_feed(driver, max_steps) with open(out_fp, a, encodingutf-8) as f: for item in driver.find_elements(*ITEM_SELECTOR): item_data extract_weibo(item) # 正文为空说明这条没解析成功直接跳过 if not item_data[text]: continue if item_data[mid] in seen: continue seen.add(item_data[mid]) f.write(json.dumps(item_data, ensure_asciiFalse) \n) f.flush()load_seen在启动时把历史文件里的mid全部读进内存作为去重基线save_weibo每写入一条就flush()一次虽然牺牲了一点IO性能但换来的是进程被kill后最多丢一条数据。注意flush()也可以改成每攒够10条调一次数据量特别大时性能更好代价是异常崩溃时最多丢一舍批次。还有一个隐藏坑历史文件如果已经几十万行启动扫描会很慢这种情况建议按天拆分数据文件只加载最近三天的去重基线。4. 微博Selenium采集的5个高频坑从翻车现场到解决方案这章是血泪经验汇总。我在不同项目里用Selenium采微博翻来覆去遇到的坑基本就这五类。每一条都按“现象、原因、解决”写清楚你可以直接对照排查自己的WBCrawler。4.1 滚动太快导致数据漏采页面高度却不长了现象采集跑得很顺速度也快但最后核对数量时发现只采到了预期的一半。回看页面高度滚动到某个位置后就不再增加脚本误以为到底了提前退出。原因微博信息流是懒加载滚动的速度超过异步请求的速度时浏览器会跳过部分加载步骤如果此时连续多次判断高度不变脚本就认为加载结束。实际是页面还在等网络响应高度暂时没变而已。解决把滚动步长从1200改回800到900等待时间加到1.5秒以上。同时不要只看高度还要统计当前已解析的卡片数量连续N轮卡片数不增加才算结束。监控里加上每小时采集条数如果数量突然掉到正常值一半以下基本就是提前退出了。4.2 登录静默失效采回来一堆“请登录”现象采集任务跑了一晚上第二天打开数据文件发现后半部分是重复的“登录”“开通微博”之类文本mid全是同一个哈希值去重完全失效。原因登录态过期后信息流区域被替换成登录引导页。因为页面仍然能滚动、DOM仍在脚本以为一切正常把引导文案当正文采了进来。有的登录失效页面还会反复输出相同内容导致去重也失去作用。解决在采集循环里加校验如果每条正文为空或命中“登录”特征词就认为是登录态失效信号立刻停止写入并检测登录框元素。建议每采完50条做一次轻量检测不要等任务全部跑完。无人值守时把“登录失效”当成最高优先级异常直接触发截图和日志第二天人工登录后断点续采。4.3 选择器今天还能用明天全体失效现象昨天跑得好好的工程今天启动后items列表为空或者extract_weibo拿到的text全是空字符串打开浏览器一看页面结构变了。原因前端发布新版本后类名或DOM层级调整固定写死的class路径失效。微博页面迭代频繁尤其活动期间会临时插入各种推荐模块导致选择器匹配到错误节点。解决选择器收敛到少数几个配置项优先使用带业务语义的稳定属性或固定按钮文本少依赖混淆过的类名。失效后先看浏览器控制台确认当前结构把新选择器更新进配置再重启。有条件的话保留一份当天页面快照回头对比DOM差异能省很多排查时间。4.4 内存去重集合越滚越大进程被杀现象长时间采集后seen集合里存了几十万个字符串内存占用持续上升最终进程被系统杀掉之前的进度也丢了。原因所有mid都放在Python的set里字符串占用的内存比想象中大得多。一天几万条还没事连续跑一周、几百万条之后内存就成了瓶颈。加上很多采集脚本为了省事不重启一个进程跑好几天内存自然撑不住。解决去重集合不要一直驻留内存。可以每采完一批就把新mid追加到一个独立的去重文件并定期清理内存set的前半段或者只保留当天的mid在内存跨天去重交给历史文件扫描。我更倾向于“单次任务跑完就退出重启用历史文件恢复”让每次进程生命周期控制在几个小时以内。4.5 被识别成自动化脚本页面开始弹验证现象开启自动化后页面偶尔打不开内容区或弹出一个验证组件脚本卡在等待元素超时然后整轮失败。原因Selenium启动的浏览器默认带一些自动化标记比如navigator.webdriver属性为true被前端检测到后就会走验证流程。加长等待、重试可以缓解但根源在这些标记上。解决启动浏览器前通过DevTools协议注入一段脚本把这些标记改掉再加随机的滚动节奏和等待时间避免每次都是固定间隔。注意这类反检测手段是持续的不能指望一套参数永久有效。关键是要给脚本留出被验证时的人工接管入口检测到验证组件超过一定时间未消失就停表、截图、告警而不是默默重试。5. 进阶让WBCrawler自己跑过夜异常重试、日志取证与多账号限速把单轮采集跑通只是开始。真正要投入使用的微博采集工程至少要能自己跑过夜中途遇到网络抖动能恢复遇到登录失效能停下来等人跑完还能留下足够排查的证据。这一章讲我常用的三个进阶手段。5.1 异常重试不中断采集循环加护栏采集脚本最常见的死法不是逻辑错而是网络抖动、元素短暂缺失、页面弹窗打断滚动。一个异常就让整个任务退出跑过夜基本不可能。我给采集主循环加了一个包装函数捕获常规异常记录日志后sleep一段时间再继续连续失败次数超过阈值才彻底退出。class TooManyFails(Exception): pass def crawl_with_retry(driver, out_fp, seen, max_retry5): fails 0 while fails max_retry: try: save_weibo(driver, out_fp, seen, max_steps60) fails 0 time.sleep(2) except TooManyFails: raise except Exception as e: fails 1 logger.error(crawl round failed: %s, e) if fails max_retry: raise TooManyFails(连续失败过多停止任务) # 按失败次数递增等待避免快速重试继续踩坑 time.sleep(5 * fails)这里的关键是异常粒度。元素缺失、超时这类可恢复异常适合重试登录失效这类异常不能重试必须直接抛出让上层处理否则重试多少次都是空采。time.sleep(5 * fails)是指数退避的简化版第一次失败等5秒第二次等10秒给页面留出自我恢复的时间。5.2 日志分级加异常截屏第二天起来先看证据跑过夜的脚本第二天早上最怕的是只留下“任务结束”四个字不知道中间发生了什么。我习惯把运行日志和错误日志分开同时异常发生时截屏存证据。截图能把当时的页面状态记下来事后定位是登录失效还是反爬弹窗比看堆栈快得多。import logging from datetime import datetime logging.basicConfig( levellogging.INFO, format%(asctime)s %(levelname)s %(message)s, handlers[ logging.FileHandler(crawler.log, encodingutf-8), logging.StreamHandler(), ], ) logger logging.getLogger(wbcrawler) def snapshot_on_error(driver, tag): name ferror_{datetime.now().strftime(%Y%m%d_%H%M%S)}_{tag}.png driver.save_screenshot(name) logger.info(snapshot saved: %s, name)日志格式里第一项是UTC时间戳跨天跑的时候能清楚看到任务在哪个阶段出了状况。FileHandler把日志持久化到本地StreamHandler让终端也能看到实时输出。截图文件名带时间戳和tag同一轮任务多次报错也不会互相覆盖。这个组合起来以后早上只需要看日志尾部加几张截图就能判断晚上发生了什么。5.3 多账号轮换与限速稳定比速度重要单个账号长时间高频跑触发验证的概率会明显上升。更稳的做法是准备两三个账号用各自的user-data-dir在一个采集周期里按顺序切换每切换一次账号停一段时间再继续。不要试图把速度拉满微博数据采集的瓶颈在稳定性不在单次速度。我一般会写一个简单的账号轮换器一个列表放着多个user-data-dir路径跑完一轮就切换到下一个目录重启driver每个账号单轮采集控制在几百条左右然后sleep随机60到120秒。至于定时启动用操作系统自带的任务计划程序就能实现没必要在程序里再做一套调度。跑过夜的关键是“慢且稳”一条数据都不漏比一小时多采几百条更有价值。6. 收尾技巧用最小字段校验脚本给结果验尸采集工程写完、数据落盘之后不要急着交付。我习惯先拿一个小脚本给结果“验尸”逐行解析JSONL检查必填字段是否完整、文本是否为空、时间是否存在、mid是否重复。这一条检查能快速暴露登录失效、选择器失效、去重失效三类最典型的问题。import json def validate(fp): total 0 bad 0 required {mid, text, created_at} with open(fp, encodingutf-8) as f: for line in f: total 1 try: obj json.loads(line) if not required.issubset(obj): bad 1 elif not obj[text] or not obj[created_at]: bad 1 except json.JSONDecodeError: bad 1 return total, bad if __name__ __main__: total, bad validate(weibo_data.jsonl) print(ftotal{total}, bad{bad}, bad_rate{bad / total:.2%} if total else empty file)这个脚本应该成为每次采集后的固定动作。如果bad_rate超过2%基本可以判断采集过程中出了问题要么登录态掉了要么选择器失效了不要拿这批数据直接跑分析。跑完之后我还会人工抽3到5个博主的最近一条微博去和页面核对数量和内容对上了才认为这轮任务有效。这个习惯救过我一次某个版本采集时遇到置顶内容置顶文案覆盖了正常正文字段校验全过人工抽样才发现。如果在WBCrawler这类工程上让我说一条最值得提前做的事那一定是在第一天就把字段校验和抽样回放写好而不是等到数据用了半天之后才发现是坏的。希望这个习惯对你也有用。本文还有配套的精品资源点击获取