☰
Python Selenium 自动化采集:Facebook 好友列表抓取脚本实战
2026/10/13 21:52:55 网站建设 项目流程

简介:这是一份面向Python爬虫初学者与自动化测试爱好者的Facebook好友列表抓取脚本,基于Python3编写,借助Selenium驱动Firefox浏览器自动完成登录并提取指定账户的好友数据,用户只需填入邮箱与密码即可运行,适合想学习浏览器自动化、登录流程模拟与页面数据解析的开发者练手。资源包共6个文件,包含1个核心py脚本、2个md说明文档、1个gif演示动图,以及license与gitignore等辅助文件,压缩包约4.03MB,体积轻便,便于快速下载与本地调试。脚本通过argparse提供超时设置、登录文件、HTML页面导入等命令行参数,并配合BeautifulSoup4解析页面结构,代码结构清晰,可作为Selenium自动化项目的入门参考模板。目前已有2889人学习下载,适合希望掌握Firefox自动化、Facebook页面抓取与登录态处理思路的读者参考借鉴。

1. 从一份朋友列表说起:这个脚本到底能拿到什么

前阵子有个做海外市场调研的朋友找我,说想看看某个公开主页的关注关系网,手动翻了几百页,眼睛都快瞎了。我第一反应是写个 Selenium 脚本自动翻页抓取,结果翻出这个selenium_facebook_scraper——一个用 Python3 写的轻量级工具,核心逻辑就是驱动 Firefox 浏览器登录后,把用户的朋友列表逐页滚动、解析、落盘。它不依赖官方 API,也不需要申请开发者权限,纯前端自动化路线,适合做小规模、一次性、结构相对固定的关系数据采集。技术栈很直白:selenium负责浏览器控制,beautifulsoup4做 DOM 解析,argparse处理命令行参数,geckodriver作为 Firefox 的驱动桥接。如果你手头有类似的公开页面采集需求,又不想折腾复杂的反爬对抗,这个脚本的骨架值得拆开看看。它解决的不是“海量抓取”问题,而是“把重复翻页动作自动化”的问题,定位很清晰。

2. 环境搭建与依赖安装:把 Firefox 和 geckodriver 配成一条线

2.1 为什么选 Firefox 而不是 Chrome

这个脚本明确指定了 Firefox 浏览器和geckodriver,不是随便选的。Firefox 在 Selenium 体系里对--headless模式的支持比较早,而且geckodriver的版本兼容性相对宽松,不像 ChromeDriver 那样动不动就要求浏览器和驱动版本严格对齐。另一个现实原因是,很多采集场景下 Firefox 的默认用户代理和指纹特征更接近普通用户,不容易一上来就被判定为自动化流量。当然,这不是说 Firefox 就绝对安全,只是在这个脚本的轻量级定位下,Firefox 的配置成本更低。你如果已经装了 Chrome,也可以改,但需要把webdriver.Firefox()换成webdriver.Chrome(),并相应调整驱动路径,后面的代码示例我会以 Firefox 为主线。

2.2 依赖清单与安装命令

先看这个脚本跑起来需要哪些包。我一般会建一个干净的虚拟环境,避免和系统里的其他 Python 项目打架:

python3 -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate pip install selenium beautifulsoup4 argparse

这里有个细节:argparse是 Python 标准库,其实不需要单独 pip 安装,但有些教程会把它列进去,你装了也不报错,只是多余。真正需要额外装的是selenium和beautifulsoup4。selenium负责驱动浏览器,beautifulsoup4负责把拿到的 HTML 解析成可遍历的对象。版本上,selenium4.x 和 3.x 在 API 上有一些差异,比如find_element_by_*系列方法在 4.x 里被标记为废弃,推荐用find_element(By.XPATH, ...)。这个脚本如果写的是老式写法,在 4.x 下会报警告但还能跑;如果用的是 4.x 的新写法,降级到 3.x 就会直接报错。我建议直接用selenium>=4.0,然后按新 API 来改。

2.3 geckodriver 的下载与 PATH 配置

geckodriver是 Firefox 和 Selenium 之间的中间人,没有它,webdriver.Firefox()会直接抛WebDriverException。下载地址在 Mozilla 的官方发布页,选对应你操作系统的压缩包。Windows 下解压得到geckodriver.exe,Linux/macOS 下得到geckodriver。关键一步是把它放到系统 PATH 里,或者显式指定路径。我习惯放在项目根目录的drivers/文件夹下,然后在代码里用executable_path指定:

from selenium import webdriver from selenium.webdriver.firefox.service import Service service = Service(executable_path='./drivers/geckodriver') driver = webdriver.Firefox(service=service)

如果你不想每次写路径,就把geckodriver所在目录加到环境变量PATH里。Linux/macOS 下可以export PATH=$PATH:/your/path/to/geckodriver,Windows 下在系统属性里编辑环境变量。验证是否配置成功,跑一句geckodriver --version,能输出版本号就说明 PATH 生效了。这一步看着简单,但后面很多“驱动找不到”的报错,根源都在这里。

3. 脚本核心逻辑拆解:登录、翻页、解析、落盘四步走

3.1 用 argparse 把账号密码和输出路径参数化

这个脚本用argparse接收命令行参数,而不是把账号密码硬编码在文件里。这是个好习惯,既方便复用,也避免敏感信息泄露。典型的参数设计会包括:--email、--password、--target(目标用户的个人主页 URL 或用户名)、--output(输出文件路径)、--max-pages(最大翻页数,防止无限滚动)。下面是我根据常见写法补全的参数解析片段:

import argparse def parse_args(): parser = argparse.ArgumentParser(description='Facebook friend list scraper') parser.add_argument('--email', required=True, help='登录邮箱') parser.add_argument('--password', required=True, help='登录密码') parser.add_argument('--target', required=True, help='目标用户主页 URL') parser.add_argument('--output', default='friends.txt', help='输出文件路径') parser.add_argument('--max-pages', type=int, default=10, help='最大翻页次数') return parser.parse_args()

required=True表示这几个参数必须传,否则脚本直接退出并提示。type=int让--max-pages自动转成整数,省去手动转换。default给可选参数一个兜底值。这样你在终端里就可以这样调用:

python scraper.py --email your@email.com --password yourpass --target https://www.facebook.com/someuser --output result.txt --max-pages 20

参数化的好处是,同一份脚本可以跑不同的目标用户,不用改代码。注意,密码走命令行参数在本地跑没问题,但如果是在共享服务器上,历史命令可能会被记录,这点后面避坑章节会细说。

3.2 登录流程与显式等待的写法

登录是第一个容易翻车的地方。Facebook 的登录表单字段是动态加载的,如果你用driver.find_element(By.ID, 'email')直接找,很可能在页面还没渲染完时就抛NoSuchElementException。正确做法是用WebDriverWait配合expected_conditions:

from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def login(driver, email, password): driver.get('https://www.facebook.com/login') wait = WebDriverWait(driver, 15) email_input = wait.until(EC.presence_of_element_located((By.ID, 'email'))) email_input.send_keys(email) password_input = driver.find_element(By.ID, 'pass') password_input.send_keys(password) login_btn = driver.find_element(By.NAME, 'login') login_btn.click() # 等待登录完成,通常会出现首页的某个元素 wait.until(EC.presence_of_element_located((By.XPATH, "//div[@role='navigation']")))

WebDriverWait(driver, 15)表示最多等 15 秒,期间每隔 0.5 秒轮询一次条件是否满足。presence_of_element_located只要求元素出现在 DOM 里,不要求可见;如果你要点击,最好用element_to_be_clickable。登录后等待role='navigation'的导航栏出现,是一个比较稳的信号,说明已经进入登录后的页面。如果 15 秒内没等到,就会抛TimeoutException,这时候你要检查网络、账号是否被要求二次验证、或者页面结构是否变了。

3.3 滚动加载与朋友列表的 DOM 定位

朋友列表页面通常是无限滚动或“加载更多”按钮模式。这个脚本的核心动作就是模拟滚动到底部,触发下一页数据加载,然后解析当前 DOM 里所有朋友条目。滚动逻辑一般这样写:

import time def scroll_and_collect(driver, max_pages): all_friends = [] last_height = driver.execute_script("return document.body.scrollHeight") for page in range(max_pages): # 解析当前页面的朋友条目 soup = BeautifulSoup(driver.page_source, 'html.parser') items = soup.select('div[role="main"] a[href*="/friends"]') for item in items: name = item.get_text(strip=True) link = item.get('href') if name and link: all_friends.append((name, link)) # 滚动到底部 driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") time.sleep(2) # 等待新内容加载 new_height = driver.execute_script("return document.body.scrollHeight") if new_height == last_height: break # 没有新内容,停止 last_height = new_height return all_friends

driver.execute_script直接执行 JavaScript 滚动到底部,比模拟键盘END键更可靠。time.sleep(2)是硬等待,简单但不够优雅;更稳的做法是等待某个新元素出现,但硬等待在轻量脚本里够用。soup.select里的选择器div[role="main"] a[href*="/friends"]是一个示例,实际页面结构可能不同,你需要用浏览器开发者工具手动确认。href*="/friends"表示链接里包含/friends字样,这是一种模糊匹配,能过滤掉导航栏和其他无关链接。解析出来的(name, link)元组会追加到all_friends列表里,最后统一落盘。

3.4 去重与写入文件

滚动过程中,同一批数据可能被重复解析,所以落盘前要去重。我一般用set对链接去重,因为链接比名字更唯一:

def save_friends(friends, output_path): seen = set() with open(output_path, 'w', encoding='utf-8') as f: for name, link in friends: if link not in seen: seen.add(link) f.write(f"{name}\t{link}\n") print(f"Saved {len(seen)} unique friends to {output_path}")

用\t分隔名字和链接,方便后续用 Excel 或 pandas 读取。encoding='utf-8'避免中文或特殊字符乱码。seen集合记录已经写过的链接,重复的直接跳过。最后打印去重后的数量,方便你判断是否抓全了。如果数量远小于预期,可能是滚动次数不够,或者选择器没匹配到全部条目。

4. 避坑与常见问题排查:那些让我重跑三次的坑

4.1 登录后跳转到验证页,脚本卡在等待导航栏

现象:脚本输入账号密码后,没有进入首页,而是跳到一个要求输入验证码或确认设备的页面,WebDriverWait等导航栏等到超时,抛TimeoutException。

原因:Facebook 对异地登录、新设备登录、频繁登录会触发安全验证。Selenium 驱动的浏览器在它看来就是一个“新设备”,而且自动化特征可能被识别。

解决:第一次登录时不要用--headless无头模式,手动在弹出的浏览器窗口里完成验证,并勾选“记住此设备”。之后再用同一个浏览器用户目录(--profile参数)启动,可以复用登录状态。如果仍然频繁触发,就降低操作频率,不要短时间内反复登录。

4.2 geckodriver 版本与 Firefox 版本不匹配

现象:webdriver.Firefox()启动时报SessionNotCreatedException,提示 “Expected browser binary version X, got Y” 或类似信息。

原因:geckodriver的版本和已安装的 Firefox 版本之间存在兼容性要求。新版 Firefox 通常需要较新的 geckodriver,旧版 geckodriver 可能不支持。

解决:先跑firefox --version看浏览器版本,再去 geckodriver 的发布页下载对应版本。一般原则是:geckodriver 用最新稳定版,Firefox 也不要太旧。如果公司环境锁定了 Firefox 版本,就去 geckodriver 的 release notes 里找兼容矩阵,选一个匹配的驱动版本。

4.3 滚动到底部后页面高度不变,但数据没加载完

现象:new_height == last_height判断为真,循环提前退出,但实际朋友列表还有更多数据没加载出来。

原因:有些页面的“加载更多”不是靠滚动触发,而是靠点击按钮;或者滚动后需要更长时间等待异步请求完成,2 秒不够。

解决:先确认页面是滚动加载还是按钮加载。如果是按钮,就把scrollTo换成find_element(By.XPATH, "//button[contains(text(),'更多')]").click()。如果是滚动加载但 2 秒不够,就把time.sleep(2)改成time.sleep(5),或者用WebDriverWait等待某个新出现的元素。更稳的方案是记录当前条目数量,滚动后等待数量增加再继续。

4.4 解析到的名字为空或包含大量无关链接

现象:输出文件里有很多空行,或者抓到的链接指向照片、视频而不是朋友主页。

原因:soup.select的选择器太宽泛,把导航栏、侧边栏、广告位的链接也匹配进来了。或者页面结构和你写选择器时不一样。

解决:用浏览器开发者工具右键检查朋友条目的 DOM 结构,找到最内层的容器元素,用更精确的选择器。比如div[role="main"]下面可能还有多层div,你可以用:scope或>限定层级。另外,在写入前加一层过滤:名字长度大于 0 且链接包含/friends或/profile才保留。

4.5 命令行传密码导致历史记录泄露

现象:在共享终端上跑完脚本后,history命令里能看到明文密码。

原因:--password参数直接写在命令行里,会被 shell 记录到历史文件。

解决:改用环境变量读取密码,比如os.environ.get('FB_PASSWORD'),然后在终端里export FB_PASSWORD=yourpass后再跑脚本。或者用getpass模块在运行时交互输入。如果必须用命令行参数,跑完后执行history -c清空当前会话历史,但这只是补救,不是根本方案。

5. 进阶技巧:把一次性脚本改成可复用的采集模块

5.1 用 Page Object 模式隔离页面结构变化

这个脚本最脆弱的地方是选择器。一旦页面改版,所有find_element和soup.select都要跟着改。我后来习惯把页面元素定位抽到一个单独的类里,比如FriendListPage,里面定义email_input、password_input、friend_items等属性。这样页面结构变化时,只需要改这一个类,主流程逻辑不动。下面是一个简化示例:

class FriendListPage: def __init__(self, driver): self.driver = driver self.wait = WebDriverWait(driver, 15) @property def email_input(self): return self.wait.until(EC.presence_of_element_located((By.ID, 'email'))) @property def password_input(self): return self.driver.find_element(By.ID, 'pass') def get_friend_items(self): soup = BeautifulSoup(self.driver.page_source, 'html.parser') return soup.select('div[role="main"] a[href*="/friends"]')

这样主流程里就变成page.email_input.send_keys(email),可读性更好,也更容易维护。如果你要抓多个目标用户,这个模式能省很多重复代码。

5.2 用显式等待替代硬等待,减少无效轮询

time.sleep(2)的问题是:如果内容 0.5 秒就加载完了,你白等 1.5 秒;如果 3 秒才加载完,你又等不够。更好的做法是等待某个条件成立,比如等待朋友条目数量增加:

def wait_for_more_items(driver, previous_count, timeout=10): wait = WebDriverWait(driver, timeout) wait.until(lambda d: len(d.find_elements(By.XPATH, "//div[@role='main']//a[contains(@href, '/friends')]")) > previous_count)

这个 lambda 表达式每次轮询时重新计算当前条目数量,一旦超过之前的数量就返回 True,等待结束。如果 10 秒内数量没增加,就抛TimeoutException,你可以捕获它并决定是继续还是退出。这样比硬等待更精准,整体采集速度也会快一些。

5.3 输出格式扩展:从 txt 到 CSV 和 JSON

默认输出是制表符分隔的 txt,够用但不够方便。我一般会加一个--format参数,支持csv和json。CSV 用 Python 标准库csv模块写,JSON 用json.dump。这样同一份数据可以直接喂给 pandas 或导入数据库。下面是一个 CSV 写入的片段:

import csv def save_as_csv(friends, output_path): with open(output_path, 'w', newline='', encoding='utf-8') as f: writer = csv.writer(f) writer.writerow(['name', 'profile_url']) for name, link in friends: writer.writerow([name, link])

newline=''是为了避免 Windows 下出现空行。writerow先写表头,再逐行写数据。JSON 格式则更适合嵌套结构,比如你以后想同时保存抓取时间和目标用户信息,就可以包一个字典再序列化。

5.4 控制采集频率与随机延迟

连续快速滚动和请求容易触发风控。我一般会在每次滚动后加一个随机延迟,模拟人工操作的不确定性:

import random time.sleep(random.uniform(1.5, 4.0))

random.uniform(1.5, 4.0)生成 1.5 到 4 秒之间的随机浮点数。这样每次等待时间不一样,比固定sleep(2)更自然。如果目标页面加载慢,可以把上限调到 6 秒。注意,这不是让你去对抗什么,只是让自动化操作更接近正常用户的浏览节奏,减少被误判的概率。

5.5 验证采集完整性的小技巧

跑完脚本后,怎么知道有没有漏抓?我通常会用两个方法交叉验证。一是手动打开目标页面,滚动到底部,看总共有多少条目,和输出文件的行数对比。二是用脚本再跑一次,但这次只滚动不解析,只记录每次滚动后的条目数量,看数量是否单调递增到稳定。如果两次结果差异很大,说明选择器或滚动逻辑有问题。另外,输出文件里的链接可以去重后统计域名分布,如果出现大量非目标域名的链接,说明选择器需要收紧。

从那以后我每次写采集脚本,都会先把选择器在浏览器控制台里验证一遍,再放进代码里跑。这个习惯帮我省了很多次重跑的时间。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询