QQ空间数据采集:Selenium自动化登录与动态页面解析实战

发布时间:2026/9/27 23:09:57
QQ空间数据采集:Selenium自动化登录与动态页面解析实战 简介这款爬虫项目基于Python与Selenium实现面向希望掌握社交平台数据采集的爬虫开发者、数据分析人员及爬虫技术爱好者专注解决QQ空间自动化模拟登录、动态页面解析以及好友列表、说说内容的大规模抓取等实际需求。压缩包共35个文件包含19个Python脚本分别承担登录控制、好友信息爬取、说说采集、数据去重等核心模块搭配8个文本说明、4个XML配置、1份DOCX附赠资源及Markdown说明文档覆盖环境配置、使用步骤、常见错误解决和扩展说明整体仅77KB结构紧凑清晰。已有95人学习下载。项目覆盖从模拟登录、页面解析到数据备份与去重、异常容错的完整链路代码中内置错误处理机制以应对网络波动、页面结构变化等问题能帮助读者理解Selenium驱动真实浏览器绕过动态渲染与登录限制的思路同时参考其中的合法性设计和合规提示适合作为学习爬虫工程化与反爬对抗的实战样例。1. QQ空间数据采集从登录到解析的完整爬虫链路QQ空间的说说和好友数据用Requests直接拉接口基本拿不到——整个页面是JS异步渲染的接口参数还带时间戳混淆和签名校验。我拆这份基于Python和Selenium的爬虫项目时第一反应是它选对了入场方式用自动化模拟登录打开真实浏览器再靠动态页面解析逐层拆DOM节点。这个资源解决的不只是能不能爬到而是把扫码登录、滚动加载、字段提取、去重备份、异常恢复串成了一条能长时间稳定跑的链路。适合需要批量备份自己QQ空间内容、做个人社交数据留存与分析的从业者而不是拿来扫他人隐私的工具。2. 环境搭建与Selenium配置版本匹配是第一个坑2.1 为什么绕不开Selenium先说结论QQ空间的页面数据全在异步请求里直接打接口需要逆向JS签名成本极高。我见过有人尝试抓手机端接口结果风控更严Cookie有效期只有十几分钟采集到一半就被踢下线。用Selenium的核心价值在于它驱动的是真实浏览器JS全部正常执行。虽然比接口直调慢一个数量级但胜在稳定不用管签名怎么生成不用理请求头里那些混淆参数所有数据都来自渲染后的DOM节点。这套项目的逻辑很直白打开页面、滚动触发加载、定位元素、提取文本。它把逆向接口这个黑匣子问题降级成了调CSS选择器这种可操作的事。版本选择上我建议直接用Selenium 4.xAPI比3.x顺畅不少。注意4.x里find_element_by_*这种旧写法已废弃统一走find_element(By.X, xxx)路线。2.2 依赖安装与浏览器驱动版本对应关系依赖安装本身一句话的事pip install selenium4.15.0真正的坑在ChromeDriver。ChromeDriver的版本必须和本机Chrome大版本一致否则直接抛session not created: This version of ChromeDriver only supports Chrome version XX。这是python爬虫入门阶段最常见的拦路虎。# 查看本机Chrome版本三种方式任选 google-chrome --version # 或 chromium --version # 或在Chrome地址栏输入 chrome://version 查看拿到版本号后去ChromeDriver镜像站找对应版本下载。我习惯把驱动放到固定目录代码里显式指定路径from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.chrome.service import Service options Options() # 显式指定驱动路径避免系统PATH里混入旧版本 service Service(executable_path/Users/yourname/bin/chromedriver) driver webdriver.Chrome(serviceservice, optionsoptions)Service类在Selenium 4.x中负责管理驱动生命周期executable_path就是驱动所在位置。如果不想手动管版本可以用webdriver_manager自动下载匹配驱动pip install webdriver-managerfrom selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager driver webdriver.Chrome(serviceService(ChromeDriverManager().install()))ChromeDriverManager().install()会自动检测本机Chrome版本下载对应驱动并缓存到用户目录。首次运行会等几秒下载后续直接命中缓存基本零成本。2.3 反检测参数配置Selenium启动的浏览器默认带navigator.webdrivertrue标记页面JS一查就能识别。QQ空间虽然不像短视频平台那么激进但检测到自动化特征会增加滑块验证概率。这组参数是我常用的基础配置from selenium.webdriver.chrome.options import Options options Options() # 去掉自动化控制提示条这是最显眼的暴露点 options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) # 隐藏navigator.webdriver标记 options.add_argument(--disable-blink-featuresAutomationControlled) # 替换默认UA去掉HeadlessChrome/WebDriver特征 options.add_argument(--user-agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36) # 窗口尺寸设成正常分辨率太窄的窗口会被识别为脚本 options.add_argument(--window-size1366,768) # 关闭弹窗和提示条 options.add_argument(--disable-infobars) # 禁用图片加载节省带宽提速 options.add_experimental_option(prefs, { profile.managed_default_content_settings.images: 2 }) driver webdriver.Chrome(optionsoptions)参数说明excludeSwitches和useAutomationExtension配合使用去掉Chrome右上角的正在受自动测试软件控制提示条。disable-blink-featuresAutomationControlled能把navigator.webdriver值改回undefined很多基础反爬检测就失效了。禁图片这步能提升20%~30%加载速度但注意图片不加载时滚动高度会比实际小触底判断逻辑要相应调整。提示无头模式headless在这个项目里不建议直接开。二维码登录需要你肉眼扫无头模式还得额外截图确认绕了一圈不如直接开窗口来得省事。2.4 Cookie持久化让采集不至于每次重来模拟登录最烦的是每次启动都要重新扫码。用Cookie持久化相当于买了后悔药第一次扫码的登录态序列化存盘下次启动直接加载Cookie没过期就不用再扫。import pickle import time def save_cookies(driver, pathqzone_cookies.pkl): 保存当前会话Cookie到本地 with open(path, wb) as f: pickle.dump(driver.get_cookies(), f) print(fCookie已保存: {len(driver.get_cookies())}条) def load_cookies(driver, pathqzone_cookies.pkl): 从本地文件加载Cookie到浏览器会话 try: with open(path, rb) as f: cookies pickle.load(f) # 必须先访问目标域名才能写入对应Cookie driver.get(https://user.qzone.qq.com/) time.sleep(2) for cookie in cookies: driver.add_cookie(cookie) print(fCookie已加载: {len(cookies)}条) return True except FileNotFoundError: print(Cookie文件不存在需要重新扫码登录) return False逻辑说明save_cookies在扫码登录成功后调用把driver.get_cookies()序列化为二进制文件。Cookie里的expiry字段会一并存下来。load_cookies里有一个关键步骤先driver.get(https://user.qzone.qq.com/)访问一次目标域名。Selenium不允许在未访问某域名时添加该域名的Cookie这一步遗漏会直接抛InvalidCookieDomainException。加载完Cookie后建议做一次登录态校验具体逻辑在第三章展开。Python虚拟环境建议独立隔离python -m venv qzone_env source qzone_env/bin/activate # Windows下是 qzone_env\Scripts\activate pip install selenium webdriver-manager同一台机器上不同爬虫项目依赖的Selenium版本可能冲突虚拟环境能避免互相污染。3. 自动化模拟登录二维码登录与状态轮询3.1 账号密码登录为什么不可靠很多人第一反应是用Selenium自动填账号密码然后提交。但QQ空间的登录流程早就不是用户名密码回车那么单纯了。当前主流程里有三关滑块验证触发概率极高一旦触发就得图像识别或人工介入。就算过了滑块设备锁验证消息直接发到手机上你还得手动点确认。密码登录的会话风控级别更高采集过程中被强制下线的概率显著增大。所以这个项目用二维码登录是正确选择扫码是腾讯官方推荐的登录方式风控等级最低。整条链路的代码量省掉一大半稳定性却高了一个台阶。3.2 二维码登录实现与状态轮询扫码登录的逻辑分三段打开登录页、等二维码加载、轮询登录状态。from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC import time def qr_login(driver, timeout120): 扫码登录QQ空间timeout为等待扫码的最长秒数 driver.get(https://user.qzone.qq.com/) wait WebDriverWait(driver, timeout) try: qr_img wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, img[src*qrcode]))) print(二维码已加载请用手机QQ扫码) except: print(二维码加载超时可能页面结构变了) return False start time.time() while time.time() - start timeout: current_url driver.current_url # 登录成功后会从登录页跳回个人空间主页 if user.qzone.qq.com in current_url and login not in current_url: print(f登录成功耗时{int(time.time() - start)}秒) return True time.sleep(3) print(等待扫码超时请重试) return False参数说明timeout120给用户留120秒操作时间扫码、手机端确认、页面跳转三步足够。wait.until(EC.presence_of_element_located(...))是显式等待比sleep(10)可靠得多二维码加载快慢不影响后续流程。轮询间隔3秒间隔太短浪费资源太长增加等待焦虑感3秒是比较合理的折中。URL判断逻辑登录页URL包含login关键字登录成功跳回user.qzone.qq.com用这个特征判断状态最稳。3.3 登录态校验与异常恢复登录成功只是起点。采集过程中Cookie随时可能失效每跑一段就校验一次是工程上的好习惯。写一个校验函数def check_login(driver): 检测当前会话是否处于登录状态 try: driver.get(https://user.qzone.qq.com/) time.sleep(2) if login in driver.current_url: print(登录态失效需要重新登录) return False return True except Exception as e: print(f校验异常: {e}) return False校验间隔要控制好频繁访问首页反而容易触发风控。一般每采集50~100条数据校验一次就够了。异常恢复的完整流程可以收敛成这样一个函数def ensure_login(driver, cookie_pathqzone_cookies.pkl): 保证登录态会话有效→Cookie恢复→重新扫码 if check_login(driver): return True if load_cookies(driver, cookie_path): if check_login(driver): return True print(Cookie失效请重新扫码) qr_login(driver) save_cookies(driver, cookie_path) return check_login(driver)执行路径是现有会话有效直接用→无效加载磁盘Cookie→再不行弹窗扫码→成功后覆盖保存新Cookie。整条链路除了扫码那一刻需要人工其余都能自动跑。万一撞上滑块验证处理原则是放弃重来不硬钢。用Selenium模拟滑块拖动带轨迹检测模拟轨迹不像真人反而更容易被标黑。我一般会driver.refresh()重新加载页面大概率换新二维码风控等级也会降下来。异常时刻截图取证也是好习惯def screenshot_on_error(driver, tagerror): 异常时截图保存现场便于回溯DOM结构 timestamp time.strftime(%Y%m%d_%H%M%S) driver.save_screenshot(ferror_{tag}_{timestamp}.png)定位不到元素时看一眼截图比盲试选择器高效十倍。4. 动态页面解析说说与好友列表的抓取核心4.1 页面结构分析与滚动加载策略QQ空间的说说列表是典型瀑布流滚动到页面底部JS自动请求下一页数据追加到DOM。抓取逻辑就是滚动→等待→提取→再滚动的循环。def scroll_and_collect(driver, max_pages50): 滚动页面并收集说说数据max_pages为最大轮询次数 all_items [] last_height driver.execute_script(return document.body.scrollHeight) for page in range(max_pages): # 滚动到底部触发加载 driver.execute_script(window.scrollTo(0, document.body.scrollHeight);) time.sleep(2) # 提取当前页面所有说说 items extract_shuoshuo(driver) if items: all_items.extend(items) # 触底判断高度不再变化说明没有新数据了 new_height driver.execute_script(return document.body.scrollHeight) if new_height last_height: print(f已滚动到底部共加载{page 1}次) break last_height new_height return all_items逻辑说明execute_script(return document.body.scrollHeight)返回页面总高度。滚动到底后如果高度不变说明没有新内容触达退出循环。time.sleep(2)是给JS留出渲染时间。网络慢时2秒不够稳妥做法是改成轮询等待循环检查某个加载指示器是否消失。max_pages50是安全阀。热门账号的说说可能滚动几百次都不到底设上限防止死循环。如果页面改版成查看更多按钮模式还得加一段点击逻辑try: load_more driver.find_element(By.CSS_SELECTOR, .load-more) load_more.click() time.sleep(1) except: pass4.2 说说内容提取与字段清洗说说数据的核心字段包括正文、发布时间、点赞数、来源。提取代码如下def extract_shuoshuo(driver): 提取当前页面所有说说节点 items [] # 说说的容器节点类名随前端改版可能变化以实际页面为准 nodes driver.find_elements(By.CSS_SELECTOR, .feed-item) for node in nodes: try: content node.find_element(By.CSS_SELECTOR, .content).text.strip() create_time node.find_element(By.CSS_SELECTOR, .info).text.strip() like_count node.find_element(By.CSS_SELECTOR, .like).text.strip() source node.find_element(By.CSS_SELECTOR, .source).text.strip() items.append({ content: content, create_time: create_time, like_count: like_count, source: source }) except Exception as e: print(f节点解析异常: {e}) continue return items字段说明目标字段选择器示例提取方法备注正文内容.content.text.strip()可能含换行和冗余空格发布时间.info.text.strip()昨天“3分钟前”需转绝对时间点赞数.like.text.strip()可能是赞或具体数字来源.source.text.strip()手机型号或客户端标识text属性拿到的文本常带大量换行和空格需要清洗import re def clean_text(text): 清理文本中的空白字符和特殊符号 text re.sub(r\s, , text) text text.strip() return text发布时间是中文相对格式比如昨天 20:153分钟前。直接存字符串后续排序会很麻烦建议做解析from datetime import datetime, timedelta def parse_qzone_time(time_str): 把QQ空间时间字符串转成datetime相对时间换算为绝对时间 now datetime.now() if 刚刚 in time_str: return now if 分钟前 in time_str: minutes int(re.search(r(\d)分钟前, time_str).group(1)) return now - timedelta(minutesminutes) if 小时前 in time_str: hours int(re.search(r(\d)小时前, time_str).group(1)) return now - timedelta(hourshours) if 昨天 in time_str: return now - timedelta(days1) try: return datetime.strptime(time_str, %Y-%m-%d %H:%M) except: return None这样3分钟前这类相对时间全换算成绝对时间戳后面做增量采集和筛选就顺手多了。4.3 好友列表抓取与iframe切换好友列表和说说不在同一个页面结构里。好友列表挂在独立的模块中并且经常被包在iframe里。Selenium要抓iframe内的内容必须先switch_to.frame()切进去这是新手最容易踩的坑——元素就在页面上但怎么都定位不到大概率是没切iframe。def get_friend_list(driver, qq_number): 抓取好友列表自动处理iframe切换 driver.get(fhttps://user.qzone.qq.com/{qq_number}) time.sleep(3) iframes driver.find_elements(By.TAG_NAME, iframe) print(f页面共发现{len(iframes)}个iframe) friends [] for frame in iframes: driver.switch_to.frame(frame) try: friend_nodes driver.find_elements(By.CSS_SELECTOR, .friend-item) if friend_nodes: print(f在iframe {frame.get_attribute(id)} 中找到好友节点) for node in friend_nodes: nickname node.find_element(By.CSS_SELECTOR, .nickname).text.strip() friend_qq node.find_element(By.CSS_SELECTOR, .qq).text.strip() friends.append({nickname: nickname, qq_number: friend_qq}) break except: pass # 没找到就切回默认内容试下一个iframe driver.switch_to.default_content() # 用完切回主文档 driver.switch_to.default_content() return friendsiframe的id可能带动态值比如frame_friend_123456这种按完整id定位不靠谱。上面的代码遍历所有iframe逐个试错虽然笨但通用性强。几百个好友同样需要滚动加载注意滚动的是iframe内部的文档不是外层页面。5. 避坑与常见问题五个最容易翻车的环节5.1 登录频繁触发风控扫一次码没过多久被踢下线现象二维码登录成功后跑数据不到10分钟页面突然跳回登录页提示账号存在异常。原因采集速度太快短时间内的页面跳转和滚动频率远超真人。QQ空间服务端会统计行为特征访问频率超出阈值判定为异常强制下线。解决把每次操作间隔从1~2秒拉到3~5秒。体验上每秒滚动一次和每4秒滚动一次整体采集时间只多约30%但被踢概率直线下降。另一个关键做法是随机化等待时间固定间隔反而更容易被识别为脚本import random def random_sleep(base3, jitter2): 在base±jitter范围内随机等待模拟真人操作节奏 time.sleep(random.uniform(base - jitter, base jitter))参数建议滚动间隔3~5秒翻页间隔5~8秒。5.2 元素定位失效前端一改版选择器全废现象昨天还能抓到的.feed-item今天全部找不到抛NoSuchElementException。截图一看DOM结构已完全改版。原因QQ空间前端改版频率在第三方平台里算高的。类名经常带随机后缀比如feed-item-abc123或者干脆换了一套命名方案。解决维护多重候选选择器按顺序尝试SHUOSHUO_SELECTORS [ .feed-item, # 老版本 .qzone-feed-item, # 改版后 [data-typefeed] # 兜底 ] def find_nodes(driver, selectors): 按候选选择器顺序查找元素 for selector in selectors: elements driver.find_elements(By.CSS_SELECTOR, selector) if elements: print(f使用选择器 {selector}找到{len(elements)}个节点) return elements return []配合截图取证失败时自动保存现场。Shua 万一是JS还没渲染完导致定位不到等几秒再试不要一失败就改代码。5.3 滚动加载漏数据以为抓完实际只拿了开头现象采集完统计条数一个几千条说说的账号只抓到两三百条滚动后无新内容出现。原因滚动加载不总是到底就触发。部分场景需要将鼠标悬停在页面内部或点击某个展开按钮才触发新一轮请求。另外快速滚动时浏览器渲染跟不上JS可能丢弃部分异步请求。解决滚动前先点击页面空白区域确保焦点回到页面内再分段滚动def scroll_to_bottom(driver): 安全滚动先聚焦页面再分段滚到底 driver.find_element(By.TAG_NAME, body).click() for i in range(1, 5): driver.execute_script(fwindow.scrollTo(0, {i * 500});) time.sleep(0.5) driver.execute_script(window.scrollTo(0, document.body.scrollHeight);)分段滚动更接近真人操作也给JS留了响应时间。如果滚动多次后停止检查是不是触发了加载更多按钮替代了瀑布流。5.4 数据重复写入去重键设计不对导致数据翻倍现象跑两次采集脚本数据库里同一时间段的说说出双份主键冲突或数据量翻倍。原因去重键设计不合理。只用content文本去重同一条说说被转发两次就当成不同内容只用时间去重同一天多条说说时间相同就互相误杀。解决用组合MD5指纹。把content 时间拼成字符串取MD5单字段唯一性可靠很多import hashlib def build_md5(record): 根据内容时间生成唯一指纹 raw f{record[content]}|{record[create_time]} return hashlib.md5(raw.encode(utf-8)).hexdigest()插入前检查MD5是否存在存在则跳过def insert_if_not_exists(conn, record, md5_hash): 存在则跳过不存在则插入 cursor conn.cursor() cursor.execute(SELECT 1 FROM shuoshuo WHERE md5_hash ?, (md5_hash,)) if cursor.fetchone(): return False cursor.execute( INSERT INTO shuoshuo (content, create_time, md5_hash) VALUES (?, ?, ?), (record[content], record[create_time], md5_hash) ) conn.commit() return True这里content|create_time是组合指纹同一用户同一秒发两条相同内容的情况极为罕见误杀率可接受。5.5 爬取速度过快导致IP临时封禁现象采集半小时后页面加载明显变慢最后直接无响应。换手动浏览器访问同一页面也打不开。原因IP触发了腾讯的访问频率策略临时封禁。这类封禁通常持续几分钟到几小时不等。解决降速之外配合令牌桶限流器控制采集节奏import time class RateLimiter: 简单的令牌桶限速器rate为每秒放行数capacity为桶容量 def __init__(self, rate, capacity): self.rate rate self.capacity capacity self.tokens capacity self.last_refill time.time() def acquire(self, count1): now time.time() self.tokens min(self.capacity, self.tokens (now - self.last_refill) * self.rate) self.last_refill now if self.tokens count: self.tokens - count return True return False limiter RateLimiter(rate0.25, capacity1) # 每4秒放行一次 # 滚动前调用 if limiter.acquire(): scroll_to_bottom(driver) else: time.sleep(2)注意无论速度怎么调抓取他人数据前都要评估合规风险。这个项目最合适的应用场景是备份你自己账号下的内容或在获得明确授权的情况下采集特定账号的公开数据。6. 进阶增量采集与数据校验的工程化收尾6.1 增量采集只抓新数据不全量重跑全量采集跑过一次后第二次再跑很多工作都是重复的。增量采集的思路是按时间过滤只抓上次采集时间点之后的新内容。因为瀑布流按时间倒序排列越往下越旧解析到发布时间早于last_sync_time即可断掉循环。from datetime import datetime def incremental_collect(driver, last_sync_time, max_pages30): 增量采集只抓last_sync_time之后的内容 collected [] for page in range(max_pages): scroll_to_bottom(driver) time.sleep(2) items extract_shuoshuo(driver) for item in items: parsed_time parse_qzone_time(item[create_time]) if parsed_time is None: continue if parsed_time last_sync_time: print(f遇到旧数据{parsed_time}停止滚动) return collected collected.append(item) return collected同步时间的存取用本地文本文件即可LAST_SYNC_FILE last_sync.txt def save_sync_time(time_obj): with open(LAST_SYNC_FILE, w) as f: f.write(time_obj.strftime(%Y-%m-%d %H:%M:%S)) def load_sync_time(): try: with open(LAST_SYNC_FILE, r) as f: return datetime.strptime(f.read().strip(), %Y-%m-%d %H:%M:%S) except: return datetime(2000, 1, 1) # 首跑全量6.2 数据校验跑完发现一堆空记录问题出在哪数据落库后别急着收工。花30秒跑一轮校验SQL能避免把脏数据留给后续分析-- 统计空内容记录 SELECT COUNT(*) FROM shuoshuo WHERE content ; -- 统计重复MD5 SELECT md5_hash, COUNT(*) FROM shuoshuo GROUP BY md5_hash HAVING COUNT(*) 1;空内容记录多多半是选择器定位到了外层容器但没拿到正文。逐条打印空内容对应的截图能看到问题出在哪再针对性调整解析逻辑。改代码前先备份数据库这是我的习惯。SQLite的备份命令sqlite3 qzone_data.db .backup backup_$(date %Y%m%d).sqlite3这个习惯救过我一次。有一次改去重逻辑改出边界问题跑完全量数据发现MD5生成规则变了旧数据全被当成新数据写入。还好跑之前做了备份直接恢复旧库重来几分钟就修复了。从那以后每次动采集或存储逻辑我都强制先做一次备份数据完整性永远是爬虫工程的第一生命线。这份工程源码里已经包含完整的错误处理与备份脚本拿到手就能直接跑通全流程。希望帮到你。本文还有配套的精品资源点击获取