简介:这是一份面向Python爬虫初学者与自动化测试爱好者的Facebook好友列表抓取脚本,基于Python3编写,借助Selenium驱动Firefox完成自动登录与数据提取,用户只需填入邮箱和密码即可运行,适合想了解浏览器自动化、登录流程模拟与页面解析的入门者练手。压缩包共6个文件,约4.03MB,包含1个核心py脚本、2个md说明文档、1个gif演示动图,以及license和gitignore等辅助文件,脚本主体清晰,文档与动图便于快速理解运行效果。资源已积累2889人学习下载,热度较高。通过阅读脚本与说明,可掌握Selenium与Firefox的配合方式、argparse命令行参数设计、登录超时与登录文件等选项的用法,并参考BeautifulSoup4相关解析思路,为后续扩展抓取字段、处理登录态与调试排错提供可复用的代码骨架。
1. 朋友列表抓取:一个被低估的自动化入口
做海外社媒数据的朋友多半有过这种经历:运营需要一批种子用户做冷启动,市场部想分析某个垂直圈层的关系网络,或者你自己想搞清楚一个公开主页到底连接了哪些人。手动点开、滚动、复制、粘贴,一百个好友能耗掉整个下午,而且极易触发平台的风控。selenium_facebook_scraper这个标题指向的正是这类需求的最小可行方案——用 Python3 加 Selenium 驱动真实浏览器,把朋友列表这种半结构化页面里的昵称、主页链接、头像地址批量落成 CSV。它不碰私有接口,不逆向加密参数,靠的是模拟真人操作,所以对新手友好,对熟手也留足了调参空间。适合谁?适合需要快速验证关系网络假设、又不想一上来就啃复杂爬虫框架的从业者。下面我把这套方案的选型逻辑、代码骨架、参数边界和血泪坑位一次讲透。
2. 为什么用 Selenium 而不是直接怼接口:选型与前置准备
2.1 朋友列表页面的加载特性决定了工具选择
朋友列表不是一张静态 HTML 表。打开一个公开主页的好友标签,你会看到首屏只渲染十几个卡片,往下滚动才触发异步加载,而且滚动到底部时会出现一个短暂的加载指示器。用requests直接请求,拿到的往往是空壳或者只有第一页数据。Selenium 的价值在于它驱动的是真实浏览器内核,JavaScript 执行、懒加载触发、动态 DOM 插入都和真人操作一致。常见做法是配合WebDriverWait做显式等待,等某个卡片元素出现再继续,而不是死等固定秒数。另一个选型理由是登录态:朋友列表通常要求账号已登录,Selenium 可以复用本地浏览器配置或手动扫码后的会话,省去处理复杂 Token 的麻烦。
2.2 环境搭建与依赖安装的最小命令集
先确认本机有 Python3.8 以上和对应版本的浏览器驱动。我一般用虚拟环境隔离,避免污染全局包。下面这套命令在 macOS 和主流 Linux 发行版上都能跑通,Windows 把激活命令换成venv\Scripts\activate即可。
# 创建并激活虚拟环境 python3 -m venv venv source venv/bin/activate # 安装核心依赖 pip install selenium webdriver-manager pandas # 验证 selenium 版本,建议 4.x 以上 python -c "import selenium; print(selenium.__version__)"webdriver-manager的作用是自动下载并匹配浏览器驱动,省去手动对照版本号的痛苦。pandas只用于最后导出 CSV,如果你习惯用csv标准库也可以不装。安装完成后,新建一个scraper.py,后续所有代码都写在这里。
2.3 初始化 WebDriver 的关键参数与反检测基线
直接webdriver.Chrome()启动的浏览器会带一堆自动化特征,平台的风控系统很容易识别。我一般会加几个启动参数,并把navigator.webdriver这个属性抹掉。注意,这里不涉及任何绕过登录或破解加密的操作,只是让浏览器行为更接近普通用户。
from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.chrome.options import Options options = Options() # 禁用“正受到自动化测试软件控制”的提示条 options.add_experimental_option("excludeSwitches", ["enable-automation"]) options.add_experimental_option("useAutomationExtension", False) # 设置一个常见的桌面窗口尺寸,避免移动端布局干扰 options.add_argument("--window-size=1400,900") # 可选:指定用户数据目录,复用已登录的会话 # options.add_argument("--user-data-dir=/path/to/your/chrome/profile") driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=options) # 抹掉 webdriver 标记 driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", { "source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})" })参数说明:excludeSwitches去掉顶部提示条,减少视觉干扰;window-size固定为桌面尺寸,因为朋友列表在移动端布局下 DOM 结构不同,选择器会失效;user-data-dir是可选的重头戏,如果你已经在该浏览器配置里登录过目标账号,加上这个参数就能直接带着登录态启动,省去每次扫码。注意用户数据目录不要和日常使用的浏览器实例同时打开,否则会锁冲突。
3. 从滚动加载到 CSV 落盘:核心抓取逻辑拆解
3.1 定位朋友卡片:选择器的稳定性与降级策略
朋友列表的 DOM 结构会随平台改版变化,所以选择器不能写死。我的习惯是先用浏览器开发者工具找到卡片容器的共同特征,比如div[role="main"]下带有特定>from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def find_friend_cards(driver, timeout=15): # 候选选择器列表,按优先级排列 candidates = [ 'div[role="main"] div[data-visualcompletion="ignore-dynamic"] a[href*="/friends"]', 'div[role="main"] a[href*="friend"]', 'div[role="main"] div[role="article"] a', ] for sel in candidates: try: WebDriverWait(driver, timeout).until( EC.presence_of_element_located((By.CSS_SELECTOR, sel)) ) cards = driver.find_elements(By.CSS_SELECTOR, sel) if cards: print(f"命中选择器: {sel}, 卡片数: {len(cards)}") return cards except Exception: continue raise RuntimeError("所有候选选择器均未命中,请检查页面是否加载或登录态是否失效")
逻辑说明:WebDriverWait最多等 15 秒,让异步内容有机会渲染;presence_of_element_located只要求元素出现在 DOM 里,不要求可见,因为部分卡片可能在视口外。候选选择器从具体到宽泛,避免因一次改版就全盘失效。如果全部落空,直接抛异常而不是返回空列表,方便上层决定是否重试。
3.2 滚动加载的节奏控制:次数、间隔与终止条件
滚动是这套脚本最容易被风控盯上的动作。我的经验是:不要用window.scrollTo(0, document.body.scrollHeight)一步到底,那样滚动事件触发太密集。改成小步长、随机间隔,并且每滚几次就检查一次卡片数量是否还在增长。
import time import random def scroll_and_collect(driver, max_scrolls=50, pause_range=(1.5, 3.5)): last_count = 0 stable_rounds = 0 for i in range(max_scrolls): # 小步滚动,每次滚一屏的 80% 左右 driver.execute_script("window.scrollBy(0, window.innerHeight * 0.8);") # 随机停顿,模拟真人阅读节奏 time.sleep(random.uniform(*pause_range)) cards = driver.find_elements(By.CSS_SELECTOR, 'div[role="main"] a[href*="friend"]') current_count = len(cards) print(f"第 {i+1} 次滚动,当前卡片数: {current_count}") if current_count == last_count: stable_rounds += 1 # 连续 3 次数量不变,认为已到底 if stable_rounds >= 3: print("卡片数连续稳定,停止滚动") break else: stable_rounds = 0 last_count = current_count return cards参数说明:max_scrolls是硬上限,防止页面无限加载导致脚本卡死;pause_range给随机间隔,1.5 到 3.5 秒之间比较接近真人浏览速度;stable_rounds设为 3 是经验值,太少容易误判,太多浪费时间。注意每次滚动后重新查询卡片,因为 DOM 会动态插入新节点,之前拿到的列表引用可能已经过期。
3.3 提取字段并导出 CSV:去重、编码与列顺序
拿到卡片元素后,逐个提取昵称、主页链接和头像地址。昵称通常在a标签的文本或aria-label里,链接是href属性,头像在img的src。这里有个细节:部分卡片是推荐好友而非真实好友,链接里会带?ref=参数,需要过滤掉。
import csv import re def extract_friends(cards): seen = set() rows = [] for card in cards: try: href = card.get_attribute("href") or "" # 过滤推荐位和空链接 if "friend" not in href or "ref=bookmarks" in href: continue # 用主页 ID 去重 match = re.search(r"profile\.php\?id=(\d+)", href) uid = match.group(1) if match else href.split("?")[0] if uid in seen: continue seen.add(uid) name = card.text.strip() or card.get_attribute("aria-label") or "未知" # 头像通常在卡片内的 img 标签 try: img = card.find_element(By.TAG_NAME, "img") avatar = img.get_attribute("src") or "" except Exception: avatar = "" rows.append({"昵称": name, "主页": href, "头像": avatar}) except Exception as e: print(f"跳过一条异常卡片: {e}") return rows def save_csv(rows, path="friends.csv"): if not rows: print("没有可写入的数据") return with open(path, "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=["昵称", "主页", "头像"]) writer.writeheader() writer.writerows(rows) print(f"已写入 {len(rows)} 条到 {path}")逻辑说明:seen集合按用户 ID 去重,避免滚动过程中同一张卡片被重复采集;utf-8-sig编码让 Excel 打开 CSV 时不乱码;字段顺序固定为昵称、主页、头像,方便后续用 pandas 读取。如果某条卡片解析失败,打印异常并继续,不让单条错误中断整个任务。
4. 避坑与排查:那些让我重跑三次的细节
4.1 登录态失效导致页面直接跳转
现象:脚本启动后浏览器打开的是登录页,所有选择器都找不到元素,报超时。原因:Selenium 每次启动的是全新会话,不带任何 Cookie。解决:要么在启动参数里加--user-data-dir指向已登录的浏览器配置目录,要么在脚本开头留出 30 秒手动扫码时间,用input("扫码后按回车继续")阻塞。我一般用前者,但注意配置目录不能被日常浏览器占用。
4.2 滚动到底部后卡片数不再增长但实际还有数据
现象:连续三次滚动卡片数不变,脚本停止,但手动打开页面发现下面还有内容。原因:平台的懒加载有节流,快速滚动时新内容还没请求回来,导致计数稳定假象。解决:把stable_rounds提高到 5,或者在每次滚动后额外等一个网络空闲信号,比如driver.execute_script("return document.readyState")返回complete再计数。更稳妥的是在停顿区间里加一个 5 秒的长停顿,每 10 次滚动触发一次。
4.3 选择器命中大量非好友元素
现象:抓下来的 CSV 里混进了“推荐好友”“你可能认识的人”甚至广告卡片。原因:候选选择器写得太宽泛,a[href*="friend"]会把所有带 friend 字样的链接都捞进来。解决:在提取阶段加过滤条件,检查链接是否包含profile.php?id=或/friends/路径,同时排除带ref=bookmarks、ref=search的推荐位。更彻底的办法是先用开发者工具确认真实好友卡片的父容器有唯一属性,再缩小选择器范围。
4.4 头像地址是缩略图或空字符串
现象:CSV 里头像列大量为空,或者全是 50x50 的小图。原因:列表页为了性能只加载缩略图,部分卡片甚至用懒加载占位符,src是 base64 或空。解决:如果只需要头像链接,接受缩略图即可;如果需要原图,得再进每个主页抓一次,但那会大幅增加请求量和风控风险。我的建议是列表页只存缩略图地址,后续按需异步补全,不要在主流程里做。
4.5 运行中途被要求验证身份或限制访问
现象:脚本跑了几十次滚动后,页面突然弹出验证码或提示“操作过于频繁”。原因:滚动节奏太机械,或者单次会话抓取量过大。解决:把pause_range下限提高到 2.5 秒,单次会话最多抓 200 条就主动退出,换时段再跑。另外,不要在同一个账号上高频运行,准备一个专门用于数据采集的账号,降低主账号风险。
5. 进阶技巧:把一次性脚本变成可复用的采集管道
5.1 用配置文件管理选择器和滚动参数
硬编码选择器的脚本活不过两次改版。我习惯把选择器、滚动次数、停顿区间写进一个config.yaml,脚本启动时读取。这样改版时只需要改配置,不用动代码。
import yaml def load_config(path="config.yaml"): with open(path, "r", encoding="utf-8") as f: return yaml.safe_load(f) # config.yaml 示例内容: # selectors: # card: 'div[role="main"] a[href*="friend"]' # name: 'span' # avatar: 'img' # scroll: # max_scrolls: 60 # pause_min: 2.0 # pause_max: 4.0 # stable_rounds: 5参数说明:card是卡片选择器,name和avatar是卡片内子元素选择器,max_scrolls控制上限,pause_min和pause_max给随机停顿范围,stable_rounds决定何时停止。把配置和代码分离后,同一套逻辑可以快速适配不同页面结构。
5.2 断点续抓:记录已采集 ID 避免重复劳动
如果一次没抓完,下次重跑时不应该从头再来。我的做法是在 CSV 旁边维护一个seen_ids.txt,每采集一条就追加写入用户 ID。启动时先加载这个文件到集合里,提取阶段直接跳过已存在的 ID。
def load_seen_ids(path="seen_ids.txt"): try: with open(path, "r", encoding="utf-8") as f: return set(line.strip() for line in f if line.strip()) except FileNotFoundError: return set() def append_seen_id(uid, path="seen_ids.txt"): with open(path, "a", encoding="utf-8") as f: f.write(uid + "\n")逻辑说明:load_seen_ids在脚本启动时调用一次,把历史 ID 读进内存;append_seen_id在每提取一条新记录后调用,实时落盘。这样即使脚本中途崩溃,下次也能接着跑。注意文件写入用追加模式,不要每次重写整个文件,否则数据量大时会很慢。
5.3 用 pandas 做后处理:去重、排序与导出 Excel
CSV 只是中间产物,最终交付往往需要 Excel 或数据库。用 pandas 读进来做一次全局去重和排序,再导出成.xlsx,比在采集阶段做更高效。
import pandas as pd def postprocess(csv_path="friends.csv", xlsx_path="friends.xlsx"): df = pd.read_csv(csv_path, encoding="utf-8-sig") # 按主页链接去重,保留第一条 df = df.drop_duplicates(subset=["主页"], keep="first") # 按昵称排序,方便查阅 df = df.sort_values(by="昵称", ascending=True) # 重置索引 df = df.reset_index(drop=True) df.to_excel(xlsx_path, index=False) print(f"后处理完成,共 {len(df)} 条,已导出到 {xlsx_path}") return df参数说明:drop_duplicates的subset指定按主页链接判重,keep="first"保留首次出现的记录;sort_values按昵称排序,如果昵称包含多语言字符,排序结果可能不符合预期,可以改用主页 ID 排序;to_excel需要额外安装openpyxl,用pip install openpyxl即可。这一步放在采集之后,不占用浏览器会话时间,适合批量处理多个 CSV 文件。
5.4 一个我常犯的错误:忽略浏览器版本与驱动匹配
最后说个血泪教训。有次在另一台机器上跑脚本,webdriver-manager下载的驱动版本和本机浏览器差了一个大版本,启动直接报SessionNotCreatedException。排查了半天才发现是浏览器自动更新了,而驱动缓存没刷新。后来我养成的习惯是:每次换机器先手动跑一次python -c "from webdriver_manager.chrome import ChromeDriverManager; print(ChromeDriverManager().install())",确认驱动路径和版本,再启动主脚本。另外,如果公司网络对驱动下载有限制,提前把驱动放到本地路径,用Service(executable_path="...")指定,别等到运行时才发现下不动。希望帮到你。
本文还有配套的精品资源,点击获取