简介:本资源是一个面向Python中级开发者与数据采集实践者的QQ空间自动化爬虫项目,聚焦于解决社交平台动态内容(好友列表、说说、个人资料)的合法合规抓取难题,适用于学术研究、个人数据备份及小规模社交图谱分析等场景。压缩包共35个文件,含19个核心Python脚本(如friend_spider.py、mood_spider.py、spide_controller.py实现模块化爬取)、8个文本配置与说明文件(含登录凭证管理、已爬/失败QQ记录、使用说明)、4个XML配置文件支撑流程控制,另有README.md、.iml开发配置及附赠的Word扩展文档,整体仅77KB,轻量易部署。已有93人学习下载。读者可直接复用完整的Selenium模拟登录框架、动态页面等待与解析逻辑、多线程任务调度结构、基于哈希的数据去重机制及分级错误日志记录方案,并通过init_messages.py与public_methods.py快速理解模块协作关系与异常恢复策略。
1. 项目概述与核心价值
最近在整理个人数字资产,想把过去十几年在QQ空间里留下的“黑历史”说说和好友动态做个本地备份。手动操作显然不现实,几千条说说和几百个好友的信息,点开、复制、粘贴,这工作量想想就头大。于是,一个基于Python和Selenium的自动化爬虫项目就提上了日程。这个项目的核心目标很明确:模拟真人登录QQ空间,然后像我们平时浏览一样,自动翻页、抓取好友列表和说说内容,最后把数据规整地保存下来,并且要聪明一点,能识别哪些是已经抓过的,避免重复劳动,还要能应对网络波动、页面改版这些幺蛾子。
听起来像是典型的爬虫应用,但QQ空间这种重度依赖JavaScript渲染的动态页面,加上复杂的登录验证和好友关系链,让它成了一个不错的“练手”项目。它不仅能帮你掌握Selenium操控浏览器的核心技巧,更能让你深入理解如何处理动态加载数据、管理会话状态以及设计一个健壮的、面向持久化运行的数据采集系统。无论你是想备份自己的青春记忆,还是学习企业级爬虫的架构思路,这个项目都能提供一条清晰的实践路径。
2. 技术选型与整体架构设计
2.1 为什么是Selenium,而不是Requests?
这是第一个要回答的关键问题。QQ空间的页面,特别是好友动态流(说说),数据是通过Ajax动态加载的。你用Requests直接请求网页地址,拿到的只是一个空壳HTML,里面没有我们想要的说说内容。这些内容是在浏览器执行了特定的JavaScript代码后,再从服务器获取并渲染到页面上的。
Selenium的核心价值就在这里:它能够驱动一个真实的浏览器(如Chrome、Firefox),完整地执行页面上的所有JavaScript,让页面完全渲染出来。这样,我们就能像真人一样,“看到”并“拿到”最终呈现的数据。虽然速度上比直接发HTTP请求的Requests慢,但对于QQ空间这种场景,Selenium是唯一可靠的选择。它解决了动态页面解析这个根本性难题。
2.2 核心工具栈与依赖
工欲善其事,必先利其器。这个项目主要依赖以下工具和库:
- Python 3.7+: 项目的基础语言环境。
- Selenium: 用于浏览器自动化的核心库。通过
pip install selenium安装。 - WebDriver: 这是Selenium控制浏览器的桥梁。你需要根据你使用的浏览器下载对应的驱动。
- Chrome: 下载
chromedriver,版本需与你的Chrome浏览器版本匹配。 - Edge: 下载
msedgedriver。 - 将下载的驱动文件放在系统PATH路径下,或者直接在代码中指定其路径。
- Chrome: 下载
- Pandas & Openpyxl: 用于将爬取的数据结构化地保存到Excel文件中。
pip install pandas openpyxl。 - SQLite (内置): 用于实现增量爬取和去重的轻量级数据库。Python标准库自带,无需额外安装。
- Schedule 或 APScheduler: 可选,用于实现定时爬取任务。
pip install schedule。
2.3 项目架构思路拆解
整个爬虫的运作流程可以抽象为以下几个核心模块,它们共同构成了项目的骨架:
- 登录模块: 负责启动浏览器,导航到QQ登录页,输入账号密码(或处理二维码登录),完成登录并跳转到QQ空间主页。这是所有后续操作的前提,必须保证登录状态的稳定维持。
- 导航与等待模块: 登录成功后,需要精准地跳转到目标页面,如“好友”页面或“说说”页面。由于页面加载和渲染需要时间,这个模块必须包含智能等待逻辑,确保目标元素出现后再进行下一步操作,这是避免程序报错的关键。
- 数据解析模块: 这是爬虫的“眼睛”和“大脑”。在页面完全加载后,使用Selenium提供的
find_element或find_elements方法,结合CSS选择器或XPath,定位到包含好友信息或说说内容的HTML元素,然后从中提取出文本、时间、点赞数等我们需要的数据。 - 数据存储模块: 将解析出来的数据保存下来。通常采用“内存缓存+批量写入”的策略。例如,在内存中积累100条说说数据后,一次性写入Excel文件或数据库,以减少I/O操作,提升效率。
- 翻页与循环控制模块: 说说和好友列表通常是分页的。这个模块负责模拟点击“下一页”按钮,或者在滚动到底部时触发新的数据加载,并控制爬取的页数或条数,防止无限循环。
- 状态管理与去重模块 (核心): 这是实现“大规模”和“备份”功能的关键。我们需要一个机制来记录已经爬取过哪些数据。最常用的方法是在本地建立一个SQLite数据库,为每条说说或好友记录存储一个唯一标识(如说说的ID或发布时间戳)。每次爬取新数据前,先查询这个ID是否已存在,如果存在则跳过。这实现了增量爬取,下次运行时只会抓取新增的内容。
- 错误处理与日志模块: 网络不稳定、页面元素变更、验证码弹出……爬虫运行中总会遇到意外。一个健壮的系统必须有完善的异常捕获机制(try-except),并在出错时记录详细的日志(使用Python的
logging模块),方便事后排查。对于临时性错误(如网络超时),还应设计重试机制。
这个架构设计确保了爬虫的功能性(能抓到数据)、健壮性(能应对错误)和效率性(支持增量更新)。
3. 核心环节实现与实操详解
3.1 模拟登录:跨越第一道门槛
QQ登录主要有两种方式:账号密码登录和扫码登录。由于密码登录常触发安全验证(滑块、短信等),而扫码登录更接近真人操作,成功率更高,因此我们优先采用扫码登录方案。
from selenium import webdriver 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 login_with_qrcode(): """ 通过二维码登录QQ空间 """ # 1. 初始化浏览器,配置参数 options = webdriver.ChromeOptions() # 建议添加无头模式参数用于测试,正式运行可注释掉以便观察 # options.add_argument('--headless') # 防止被检测为自动化工具,可添加如下参数(非绝对有效) options.add_argument('--disable-blink-features=AutomationControlled') options.add_experimental_option("excludeSwitches", ["enable-automation"]) options.add_experimental_option('useAutomationExtension', False) driver = webdriver.Chrome(options=options) driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', { 'source': 'Object.defineProperty(navigator, \"webdriver\", {get: () => undefined})' }) try: # 2. 访问QQ登录页 login_url = "https://i.qq.com/" driver.get(login_url) print("已打开登录页面,请准备扫码...") # 3. 切换到二维码登录框架 # QQ登录页的二维码在一个iframe内,必须先切换进去 wait = WebDriverWait(driver, 20) login_frame = wait.until(EC.presence_of_element_located((By.ID, "login_frame"))) driver.switch_to.frame(login_frame) # 4. 等待二维码出现 qrcode_img = wait.until( EC.visibility_of_element_located((By.CSS_SELECTOR, "#qr_area .qr-img img")) ) print("二维码已加载,请使用手机QQ扫码登录。") # 5. 循环检测登录成功 # 扫码成功后,页面会自动跳转。我们可以通过检测某个登录后才会出现的元素来判断。 while True: try: # 尝试查找登录后的用户昵称元素,如果找到,说明登录成功 driver.find_element(By.ID, "user_nickname") print("扫码登录成功!") break except: # 没找到,继续等待 time.sleep(2) # 额外检查:如果二维码过期,页面会刷新,这里可以增加重新获取二维码的逻辑(略复杂,首次实现可暂缓) print("等待扫码...") continue # 6. 登录成功后,跳转到QQ空间主页 driver.get("https://user.qzone.qq.com/") # 这里填你自己的QQ号 # 等待空间主页加载完成 wait.until(EC.presence_of_element_located((By.ID, "container"))) print("已进入QQ空间主页。") return driver except Exception as e: print(f"登录过程发生错误: {e}") driver.quit() return None实操要点与避坑指南:
- iframe切换:QQ登录框是一个独立的
<iframe>,所有在它内部的操作(如找二维码)都必须先使用driver.switch_to.frame()切换进去。操作完后,如果需要操作主页内容,要用driver.switch_to.default_content()切回来。 - 显式等待是美德:绝对不要用
time.sleep(固定秒数)来等待页面加载。使用WebDriverWait配合expected_conditions(如等待元素可见、可点击)是更可靠、更高效的做法。它会在条件满足时立即继续,而不是傻等固定时间。 - 防检测策略:虽然添加了
--disable-blink-features和CDP命令来隐藏WebDriver特征,但这不是银弹。腾讯的反爬策略会升级。如果登录频繁失败,可以考虑:- 手动登录一次,然后将浏览器的
user-data-dir(用户数据目录)路径通过options.add_argument指定给Selenium,复用已经登录的Cookie和会话。这是最稳定、最推荐的方法。 - 适当降低操作频率,模拟人类行为(如随机延迟)。
- 手动登录一次,然后将浏览器的
- 扫码超时处理:上述代码是一个简单循环。实际应用中,应设置一个总等待超时(比如300秒),超时后自动退出,并记录日志。更完善的逻辑还需要检测二维码是否过期并自动刷新。
3.2 解析好友列表:定位与数据提取
登录成功后,我们可以进入好友页面。好友列表通常也是一个动态加载的页面。
def fetch_friend_list(driver): """ 抓取QQ好友列表 """ friend_list = [] try: # 1. 导航到好友页面 friend_url = "https://user.qzone.qq.com/proxy/domain/r.qzone.qq.com/cgi-bin/tfriend/friend_show_qqfriends.cgi" driver.get(friend_url) # 等待好友列表容器加载 wait = WebDriverWait(driver, 15) # 注意:这里的元素选择器需要根据实际页面结构调整,可能需要分析页面HTML friend_list_container = wait.until( EC.presence_of_element_located((By.CSS_SELECTOR, “某个包含所有好友的div的类名或ID”)) ) # 2. 解析好友条目 # 假设每个好友在一个 class='friend-item' 的div里 friend_items = driver.find_elements(By.CSS_SELECTOR, “.friend-item”) for item in friend_items: try: # 提取QQ号 (可能在某个data属性或链接里) qq_number = item.find_element(By.CSS_SELECTOR, “a”).get_attribute(‘href’).split(‘uin=’)[-1] # 提取昵称 nickname = item.find_element(By.CSS_SELECTOR, “.nickname”).text # 提取备注(如果有) remark_elem = item.find_elements(By.CSS_SELECTOR, “.remark”) remark = remark_elem[0].text if remark_elem else “” # 提取头像链接 avatar = item.find_element(By.CSS_SELECTOR, “img”).get_attribute(‘src’) friend_info = { ‘qq’: qq_number, ‘nickname’: nickname, ‘remark’: remark, ‘avatar’: avatar, ‘crawl_time’: time.strftime(‘%Y-%m-%d %H:%M:%S’) } friend_list.append(friend_info) print(f”已抓取好友: {remark or nickname} ({qq_number})“) except Exception as e: print(f”解析单个好友信息时出错: {e}“) continue except Exception as e: print(f”抓取好友列表过程发生错误: {e}“) return friend_list注意事项:
- 页面结构分析是关键:上述代码中的CSS选择器(如
.friend-item)是示例。你必须亲自打开QQ空间的好友页面,使用浏览器的开发者工具(F12)查看真实的HTML结构,找到包含好友信息的正确元素和属性。这是爬虫开发中最耗时但也最核心的一步。页面结构可能会更新,所以选择器要有一定的容错性。 - 数据清洗:提取到的文本可能包含多余的空格、换行符,需要进行
strip()等清洗操作。 - 异常处理粒度:在循环解析每个好友时,单个好友解析失败不应该导致整个任务崩溃,所以每个好友的解析都用
try-except包裹,记录错误后继续下一个。
3.3 抓取说说内容:应对动态加载与翻页
说说页面是最复杂的部分,因为它涉及无限滚动或分页按钮点击,以及多层嵌套的数据解析。
def fetch_shuoshuo(driver, max_pages=50): """ 抓取说说内容 :param driver: 已登录的浏览器驱动 :param max_pages: 最大爬取页数,防止无限循环 """ all_shuoshuo = [] current_page = 0 try: # 1. 导航到说说页面 shuoshuo_url = “https://user.qzone.qq.com/proxy/domain/taotao.qq.com/cgi-bin/emotion_cgi_msglist_v6” driver.get(shuoshuo_url) time.sleep(5) # 初始页面加载需要较长时间,这里可以先用sleep,后续用更智能的等待 # 2. 循环翻页抓取 while current_page < max_pages: print(f”正在抓取第 {current_page + 1} 页说说...“) # 等待说说列表区域加载 wait = WebDriverWait(driver, 20) # 这里同样需要根据实际页面确定选择器 list_container = wait.until( EC.presence_of_element_located((By.ID, “msgList”)) ) # 3. 解析当前页的所有说说条目 shuoshuo_items = driver.find_elements(By.CSS_SELECTOR, “li[data-id]”) # 示例选择器 for item in shuoshuo_items: try: # 提取说说唯一ID (用于去重) data_id = item.get_attribute(‘data-id’) # 提取说说正文 content_elem = item.find_element(By.CSS_SELECTOR, “.content”) content = content_elem.text if content_elem else “” # 提取发布时间 time_elem = item.find_element(By.CSS_SELECTOR, “.c_tx .goDetail”) pub_time = time_elem.get_attribute(‘title’) if time_elem else “” # 提取点赞数 (可能需要点击展开) like_elem = item.find_elements(By.CSS_SELECTOR, “.like .num”) like_count = like_elem[0].text if like_elem else “0” # 提取评论数 comment_elem = item.find_elements(By.CSS_SELECTOR, “.comment .num”) comment_count = comment_elem[0].text if comment_elem else “0” shuoshuo_info = { ‘id’: data_id, ‘content’: content, ‘pub_time’: pub_time, ‘like_count’: like_count, ‘comment_count’: comment_count, ‘crawl_time’: time.strftime(‘%Y-%m-%d %H:%M:%S’) } all_shuoshuo.append(shuoshuo_info) print(f”已抓取说说: {content[:50]}...“) except Exception as e: print(f”解析单条说说时出错: {e}“) continue # 4. 尝试翻页 current_page += 1 try: # 查找“下一页”按钮。可能是链接,也可能是需要滚动的“加载更多” next_button = driver.find_element(By.CSS_SELECTOR, “a[title=‘下一页’]”) # 如果按钮被禁用或不可见,说明没有下一页了 if not next_button.is_enabled() or not next_button.is_displayed(): print(“已到达最后一页。”) break # 点击下一页 next_button.click() # 等待新页面加载 time.sleep(3) # 翻页后等待数据加载,可根据网络情况调整 # 更优方案:等待某个新元素出现,例如等待第一个说说的元素更新 # wait.until(EC.staleness_of(shuoshuo_items[0])) except Exception as e: # 如果找不到“下一页”按钮,可能是采用了滚动加载 print(f”未找到分页按钮,可能为滚动加载或已结束。错误: {e}“) # 这里可以改为模拟滚动到底部触发加载 # driver.execute_script(“window.scrollTo(0, document.body.scrollHeight);”) # time.sleep(2) # 然后需要判断是否真的有新内容加载出来,否则跳出循环 break except Exception as e: print(f”抓取说说主流程发生错误: {e}“) return all_shuoshuo核心难点与技巧:
- 动态加载识别:QQ空间的说说可能是点击“下一页”分页,也可能是滚动到页面底部自动加载更多。你需要通过观察页面交互来确定。上述代码提供了分页点击的逻辑,注释里给出了滚动加载的思路。
- 智能等待新内容:翻页或滚动后,不能简单用
time.sleep。最佳实践是等待上一页的某个元素失效(EC.staleness_of)或新页面的某个特征元素出现。这能确保数据确实已更新。 - 数据提取的复杂性:一条说说可能包含文本、图片、视频、定位、@的人、话题标签等多种元素。你需要根据备份需求,决定抓取的粒度。上述代码只抓取了核心文本和互动数据。要抓图片,需要解析
img标签的src属性;抓视频更复杂,可能需要解析视频源地址。 - 反爬应对:频繁翻页或快速滚动可能触发风控。需要在翻页/滚动操作间加入随机延迟(如
time.sleep(random.uniform(1, 3))),模拟人类阅读速度。
3.4 数据持久化与去重:构建可靠的数据层
抓取到的数据需要妥善保存。我们采用“SQLite记录状态 + Excel/CSV存储全量数据”的策略。
import sqlite3 import pandas as pd import os class DataManager: def __init__(self, db_path=‘qq_space_data.db’): self.db_path = db_path self._init_database() def _init_database(self): “”“初始化数据库,创建用于去重的表”“” conn = sqlite3.connect(self.db_path) cursor = conn.cursor() # 创建说说去重表 cursor.execute(‘‘‘ CREATE TABLE IF NOT EXISTS shuoshuo_processed ( id TEXT PRIMARY KEY, -- 说说唯一标识 pub_time TEXT, crawl_time TEXT ) ‘‘‘) # 创建好友去重表(可选,因为好友变动不频繁) cursor.execute(‘‘‘ CREATE TABLE IF NOT EXISTS friend_processed ( qq TEXT PRIMARY KEY, nickname TEXT, last_crawl_time TEXT ) ‘‘‘) conn.commit() conn.close() def is_shuoshuo_processed(self, shuoshuo_id): “”“检查某条说说是否已抓取过”“” conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute(“SELECT id FROM shuoshuo_processed WHERE id = ?”, (shuoshuo_id,)) result = cursor.fetchone() conn.close() return result is not None def mark_shuoshuo_processed(self, shuoshuo_info): “”“将抓取到的说说标记为已处理”“” conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute(‘‘‘ INSERT OR REPLACE INTO shuoshuo_processed (id, pub_time, crawl_time) VALUES (?, ?, ?) ‘‘‘, (shuoshuo_info[‘id’], shuoshuo_info[‘pub_time’], shuoshuo_info[‘crawl_time’])) conn.commit() conn.close() def save_to_excel(self, data_list, data_type=‘shuoshuo’): “”“将数据列表保存到Excel文件,支持追加模式”“” if not data_list: print(f”{data_type}数据为空,不保存。“) return df = pd.DataFrame(data_list) filename = f”qq_space_{data_type}_{time.strftime(‘%Y%m%d’)}.xlsx” # 如果文件已存在,则读取原有数据并追加 if os.path.exists(filename): existing_df = pd.read_excel(filename) combined_df = pd.concat([existing_df, df], ignore_index=True) # 根据ID去重,保留最新的记录(假设后抓取的是更新的) combined_df.drop_duplicates(subset=[‘id’] if data_type == ‘shuoshuo’ else [‘qq’], keep=‘last’, inplace=True) combined_df.to_excel(filename, index=False) print(f”数据已追加保存至 {filename},去重后总条数: {len(combined_df)}“) else: df.to_excel(filename, index=False) print(f”数据已保存至新文件 {filename},条数: {len(df)}“) # 在主流程中使用 def main_crawl_flow(): driver = login_with_qrcode() if not driver: return dm = DataManager() # 抓取说说示例 raw_shuoshuo_list = fetch_shuoshuo(driver, max_pages=10) filtered_shuoshuo_list = [] for shuo in raw_shuoshuo_list: if not dm.is_shuoshuo_processed(shuo[‘id’]): filtered_shuoshuo_list.append(shuo) dm.mark_shuoshuo_processed(shuo) else: print(f”说说 ID {shuo[‘id’]} 已存在,跳过。“) dm.save_to_excel(filtered_shuoshuo_list, ‘shuoshuo’) # 抓取好友示例(好友通常不需要频繁去重) friend_list = fetch_friend_list(driver) dm.save_to_excel(friend_list, ‘friend’) driver.quit()设计思路解析:
- SQLite用于状态跟踪:
shuoshuo_processed表只存储说说的ID、发布时间和抓取时间。它体积小,查询快,专门用于快速判断一条数据是否已处理。这就是“增量爬取”的核心。 - Excel用于数据归档:Excel文件便于人类阅读、分享和用其他工具(如Excel、Numbers)打开分析。采用“追加-去重”的写入模式,即使程序多次运行,最终文件里也是全量且唯一的数据。
- 分离关注点:
DataManager类封装了所有数据操作逻辑,主程序流程变得清晰:抓取 -> 过滤 -> 保存。这种设计也便于未来更换存储后端(比如换成MySQL或MongoDB)。
4. 错误处理、优化与高级技巧
4.1 常见问题排查与解决实录
爬虫在运行时就像在雷区行走,以下是几个我踩过的坑和解决方案:
NoSuchElementException(找不到元素)- 原因:页面还没加载完就去找元素;页面结构变了,CSS选择器/XPath失效;元素在
iframe里。 - 解决:
- 增加等待:使用
WebDriverWait配合EC.presence_of_element_located或EC.visibility_of_element_located。 - 更新选择器:定期检查页面结构,使用更稳定、容错率更高的选择器(如通过
style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />
- 增加等待:使用
- 原因:页面还没加载完就去找元素;页面结构变了,CSS选择器/XPath失效;元素在