Python~使用playwright操控Metamask实现自动签名授权
前言
在区块链开发和测试过程中,我们经常需要频繁地与 MetaMask 钱包进行交互。手动操作不仅效率低下,还容易出错。本教程将教你如何使用 Python 和 Playwright 来自动化操控 MetaMask 钱包,实现连接钱包、签名交易等常见操作。
核心技术栈:
- Python 3.x
- Playwright(浏览器自动化库)
- Chrome 浏览器(调试模式)
- MetaMask 浏览器扩展
重要说明:
- 本教程使用 Chrome 调试模式 + Playwright CDP 连接
- 不是使用 ChromeDriver 或 Selenium
- 即使使用调试模式,Selenium 也会被 MetaMask 的 LavaMoat 检测,而 Playwright 不会
为什么不用 Selenium?
很多人以为只要用了 Chrome 调试模式就能避免检测,但实际上:
- ❌ Selenium 连接调试端口 → 仍然会注入
navigator.webdriver = true→ 被 LavaMoat 阻止 - ✅ Playwright CDP 连接 → 不注入任何标识 → 完全隐形
这是我踩过的坑,希望能帮到你!
一、为什么不使用 Selenium?遭遇 LavaMoat 的困境
1.1 我最初的尝试:Selenium + Chrome 调试模式
在开始这个项目时,我最初使用的是 Selenium 连接到 Chrome 调试模式。代码看起来很简单:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
# 连接到已经启动的Chrome调试端口
options = Options()
options.add_experimental_option("debuggerAddress", "127.0.0.1:9222")
driver = webdriver.Chrome(options=options)
# 打开MetaMask
driver.get("chrome-extension://nkbihfbeogaeaoehlefnkodbefgpgknn/home.html")
然而,当我尝试操作 MetaMask 时,遇到了一个棘手的问题:LavaMoat 防护机制。
1.2 什么是 LavaMoat?
LavaMoat 是 MetaMask 团队开发的一个安全工具,用于保护浏览器扩展免受供应链攻击。它的工作原理是:
- 运行时隔离:为第三方依赖创建安全沙箱
- 权限最小化:限制代码可以访问的 API
- 自动化检测:识别并阻止可疑的自动化行为
从 MetaMask v10.x 版本开始,LavaMoat 成为了默认启用的安全特性。
1.3 LavaMoat 如何检测 Selenium?
这里是关键点:即使使用了 Chrome 调试模式,Selenium 连接后仍然会在浏览器中注入自动化标识!
当使用 Selenium 连接到调试端口时,LavaMoat 会检测到以下特征:
// 在浏览器控制台检查
console.log(navigator.webdriver);
// Selenium: true ❌
// Playwright CDP: undefined ✅
// MetaMask 内部检测代码(简化版)
if (window.navigator.webdriver === true) {
console.error('检测到自动化工具,LavaMoat 阻止执行');
throw new Error('Automation detected');
}
// 其他检测点
if (window.document.documentElement.getAttribute('webdriver')) {
// Selenium 会在 DOM 中添加 webdriver 属性
}
这就是问题所在:
- 使用 Selenium 连接调试端口:
navigator.webdriver = true❌ - 使用 Playwright CDP 连接:
navigator.webdriver = undefined✅
1.4 我遇到的实际错误
使用 Selenium 操作 MetaMask 时,会看到类似这样的错误:
LavaMoat - Suspicious activity detected
Error: Execution context was destroyed
TypeError: Cannot read properties of undefined
在浏览器控制台中,可以看到:
LavaMoat Preinstall Hook - instrumentation active
[LavaMoat] Potentially unsafe call detected
1.5 尝试过的解决方案(都失败了)
我尝试了多种方法来解决 Selenium 的 navigator.webdriver 问题,但都不理想:
❌ 方法一:使用 CDP 命令隐藏 webdriver 属性
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
options = Options()
options.add_experimental_option("debuggerAddress", "127.0.0.1:9222")
driver = webdriver.Chrome(options=options)
# 尝试删除 webdriver 属性
driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
'source': '''
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
})
'''
})
结果:这个脚本会在新文档加载时执行,但 Selenium 会在更早的时机注入 webdriver 属性,所以这个方法无效。而且 LavaMoat 还会检测其他特征(如 DOM 属性)。
❌ 方法二:使用 undetected-chromedriver
import undetected_chromedriver as uc
options = uc.ChromeOptions()
options.debugger_address = "127.0.0.1:9222"
driver = uc.Chrome(options=options)
结果:undetected-chromedriver 对普通网站的反爬检测有效,但它本质上还是 Selenium,仍然会设置 navigator.webdriver = true,MetaMask 的 LavaMoat 依然能检测到。
❌ 方法三:修改 Selenium 源码
理论上可以修改 Selenium 源码,移除 webdriver 标识的注入,但这:
- 需要深入了解 Selenium 内部机制
- 每次更新都要重新修改
- 维护成本极高
- 可能破坏其他功能
❌ 方法四:使用旧版 MetaMask(不推荐)
使用 v10.x 之前的 MetaMask 版本(没有 LavaMoat),但这:
- 存在安全风险
- 无法使用最新功能
- 与新版 DApp 可能不兼容
- 不是长期解决方案
1.6 最终解决方案:从 Selenium 切换到 Playwright
经过深入研究,我发现关键不在于是否使用调试模式(我一直都在用),而在于用什么工具连接到调试端口!
使用 Playwright 的 CDP 连接可以完美绕过 LavaMoat 的检测:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
# Playwright 通过 CDP 连接,不会注入 webdriver 标识
browser = p.chromium.connect_over_cdp("http://localhost:9222")
context = browser.contexts[0]
page = context.pages[0]
# 在浏览器控制台验证
webdriver_value = page.evaluate("() => navigator.webdriver")
print(webdriver_value) # 输出: None (即 undefined) ✅
核心差异:
Selenium 连接调试端口:
- 使用
options.add_experimental_option("debuggerAddress", "...") - 仍然会注入
navigator.webdriver = true - 仍然会在 DOM 中添加自动化标识
- 被 LavaMoat 检测 ❌
- 使用
Playwright CDP 连接:
- 使用
connect_over_cdp() - 不会修改任何浏览器属性
- 完全像正常用户在操作浏览器
- LavaMoat 无法检测 ✅
- 使用
为什么 Playwright 不会被检测?
- Playwright 的 CDP 连接是纯协议通信
- 不会像 Selenium 那样在浏览器中注入脚本
- 所有操作都通过 Chrome DevTools Protocol 完成
- 浏览器环境保持完全"干净"
1.7 Selenium vs Playwright 连接调试模式对比
两种方案都使用 Chrome 调试模式,区别在于连接方式:
| 特性 | Selenium + 调试端口 | Playwright CDP |
|---|---|---|
| 连接方式 | debuggerAddress | connect_over_cdp() |
| LavaMoat 兼容性 | ❌ 被检测和阻止 | ✅ 完全兼容 |
| navigator.webdriver | ❌ true | ✅ undefined |
| DOM 注入 | ❌ 有自动化标识 | ✅ 无任何注入 |
| 扩展支持 | ✅ 支持 | ✅ 完美支持 |
| 状态持久化 | ✅ 支持 | ✅ 支持 |
| 学习曲线 | 低 | 中等 |
| 性能 | 中等 | 优秀 |
| 社区支持 | 成熟 | 快速增长 |
结论:即使都用调试模式,Selenium 的连接方式会暴露自动化特征,而 Playwright 则完全隐形。
1.8 如何验证差异?(实验对比)
你可以自己验证这个差异:
实验 1:使用 Selenium 连接调试端口
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
options = Options()
options.add_experimental_option("debuggerAddress", "127.0.0.1:9222")
driver = webdriver.Chrome(options=options)
# 检查 webdriver 属性
result = driver.execute_script("return navigator.webdriver")
print(f"Selenium: navigator.webdriver = {result}") # 输出: True ❌
实验 2:使用 Playwright CDP 连接
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp("http://localhost:9222")
context = browser.contexts[0]
page = context.new_page()
# 检查 webdriver 属性
result = page.evaluate("() => navigator.webdriver")
print(f"Playwright: navigator.webdriver = {result}") # 输出: None (undefined) ✅
实验 3:在 MetaMask 中测试
启动调试模式后,分别用两种方式打开 MetaMask:
# 方式1:Selenium
driver.get("chrome-extension://nkbihfbeogaeaoehlefnkodbefgpgknn/home.html")
# 结果:可能看到 LavaMoat 警告或功能异常
# 方式2:Playwright
page.goto("chrome-extension://nkbihfbeogaeaoehlefnkodbefgpgknn/home.html")
# 结果:完全正常工作
通过这些实验,你可以清楚地看到两种方式的本质差异。
二、为什么选择 Playwright + Chrome 调试模式?
基于上述 LavaMoat 的问题,我选择了 Playwright + Chrome 调试模式方案。这个方案的核心优势:
2.1 技术优势
- 无需额外驱动:直接使用系统安装的 Chrome
- 完美支持扩展:可以加载和操作任何浏览器扩展
- 状态持久化:可以复用已登录的 MetaMask 账户
- 绕过 LavaMoat:不会被识别为自动化工具
- 更加灵活:通过 CDP 协议可以实现更多高级功能
2.2 实战验证
使用这个方案后:
# 使用 CDP 连接,LavaMoat 检测结果:
# navigator.webdriver = undefined ✅
# 正常的浏览器环境 ✅
# MetaMask 正常工作 ✅
三、核心概念:CDP(Chrome DevTools Protocol)
3.1 什么是 CDP?
CDP(Chrome DevTools Protocol) 是 Chrome 浏览器提供的一套底层通信协议,允许外部程序通过 WebSocket 与浏览器进行通信和控制。
通俗理解:
- CDP 就像是 Chrome 浏览器的"遥控器接口"
- 开发者工具(F12)就是通过 CDP 与浏览器通信的
- 我们的自动化脚本也可以用同样的协议控制浏览器
CDP 的特点:
- 🔧 底层协议:基于 WebSocket 的 JSON-RPC 通信
- 🎯 功能强大:可以控制页面、网络、性能监控等
- 🔒 原生支持:Chrome/Chromium 内置,无需额外驱动
- 🌐 标准化:W3C 标准的一部分
3.2 Selenium 能用 CDP 吗?
能,但有区别! 这是个关键点:
Selenium 使用 CDP 的两种方式:
方式1:使用 CDP 命令(execute_cdp_cmd)
from selenium import webdriver
driver = webdriver.Chrome()
# Selenium 可以执行 CDP 命令
driver.execute_cdp_cmd('Network.enable', {})
方式2:连接到调试端口
options = webdriver.ChromeOptions()
options.add_experimental_option("debuggerAddress", "127.0.0.1:9222")
driver = webdriver.Chrome(options=options)
核心问题:Selenium 的连接机制
即使 Selenium 能用 CDP 命令,但它的连接机制有根本缺陷:
┌─────────────────────────────────────────────┐
│ Selenium 的连接过程 │
├─────────────────────────────────────────────┤
│ 1. ChromeDriver 启动或连接到 Chrome │
│ 2. 通过 WebDriver 协议建立连接 │
│ 3. ⚠️ 自动注入 navigator.webdriver = true │
│ 4. ⚠️ 在 DOM 中添加 webdriver 属性 │
│ 5. 然后才能使用 CDP 命令 │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ Playwright CDP 的连接过程 │
├─────────────────────────────────────────────┤
│ 1. 直接通过 CDP 协议连接到调试端口 │
│ 2. ✅ 不注入任何 webdriver 标识 │
│ 3. ✅ 不修改任何浏览器属性 │
│ 4. 完全像普通用户在操作浏览器 │
└─────────────────────────────────────────────┘
技术原理对比
Selenium 的问题:
# Selenium 即使连接调试端口,也会:
options.add_experimental_option("debuggerAddress", "127.0.0.1:9222")
driver = webdriver.Chrome(options=options)
# 在浏览器中检查:
driver.execute_script("return navigator.webdriver") # → True ❌
# 为什么?因为 Selenium 的底层是 WebDriver 协议
# WebDriver 规范要求设置 navigator.webdriver = true
# 这是写在规范里的,无法避免!
Playwright 的优势:
# Playwright 纯 CDP 连接:
browser = p.chromium.connect_over_cdp("http://localhost:9222")
page = context.pages[0]
# 在浏览器中检查:
page.evaluate("() => navigator.webdriver") # → None (undefined) ✅
# 为什么?因为 Playwright 不走 WebDriver 协议
# 直接使用 CDP 协议,无需注入任何标识
3.3 为什么 Selenium 必须注入 webdriver?
这是 W3C WebDriver 标准规定的:
The webdriver-active flag is set to true when the
user agent is under remote control which is
signalled by defining the "webdriver" property on
the Navigator object to true.
翻译:当浏览器处于远程控制时,必须将 navigator.webdriver 设置为 true。
这是为了:
- 透明性:网站有权知道用户是否在使用自动化工具
- 安全性:防止恶意自动化攻击
- 标准化:所有 WebDriver 实现必须遵守
所以 Selenium 无法绕过这个限制,因为它遵循 WebDriver 标准。
3.4 为什么 Playwright 不受限制?
Playwright 不使用 WebDriver 协议,而是直接使用 CDP:
WebDriver 协议:
Browser ←→ ChromeDriver ←→ Selenium ←→ Your Code
(必须注入标识)
CDP 协议:
Browser ←→ Playwright ←→ Your Code
(纯协议通信,无注入)
类比说明:
- Selenium + WebDriver:像是租用了一辆有"教练车"标识的车,交警一眼就能看出
- Playwright + CDP:像是远程控制你自己的私家车,外观和正常车完全一样
3.5 Chrome 调试模式是什么?
Chrome 调试模式(Remote Debugging)就是启用 CDP 服务的方式:
# 启动 Chrome 并开启 CDP 服务
chrome --remote-debugging-port=9222
启用后,Chrome 会:
- 在 9222 端口开启 WebSocket 服务
- 接受 CDP 协议的连接
- 允许外部程序通过 CDP 控制浏览器
两种工具都可以连接这个端口,但连接方式不同:
- Selenium:先建立 WebDriver 连接(注入标识),再使用 CDP 命令
- Playwright:直接建立 CDP 连接(无任何注入)
3.6 总结:为什么选择 Playwright?
| 对比项 | Selenium | Playwright |
|---|---|---|
| 底层协议 | WebDriver + CDP | 纯 CDP |
| 能用 CDP 命令吗? | ✅ 能 | ✅ 能 |
| navigator.webdriver | ❌ 必须为 true | ✅ undefined |
| 是否注入标识 | ❌ 是(协议规定) | ✅ 否 |
| LavaMoat 检测 | ❌ 会被检测 | ✅ 无法检测 |
| 适合 MetaMask | ❌ 不适合 | ✅ 完美 |
结论:Selenium 能用 CDP 命令,但无法避免 WebDriver 协议的注入问题。只有完全基于 CDP 的工具(如 Playwright)才能真正"隐形"。
四、Chrome 调试模式详解
4.1 如何启动 Chrome 调试模式?
macOS 系统
方法一:命令行启动
打开终端,执行以下命令:
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
--remote-debugging-port=9222 \
--user-data-dir=~/chrome-debug-profile \
--no-first-run \
--no-default-browser-check
方法二:创建启动脚本
创建一个 start-chrome-debug.sh 文件:
#!/bin/bash
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
--remote-debugging-port=9222 \
--user-data-dir=~/chrome-debug-profile \
--no-first-run \
--no-default-browser-check \
--disable-default-apps
给予执行权限并运行:
chmod +x start-chrome-debug.sh
./start-chrome-debug.sh
Windows 系统
方法一:命令行启动
打开命令提示符(CMD),执行:
"C:\Program Files\Google\Chrome\Application\chrome.exe" ^
--remote-debugging-port=9222 ^
--user-data-dir="%USERPROFILE%\chrome-debug-profile"
方法二:创建快捷方式
- 右键点击 Chrome 快捷方式,选择"属性"
- 在"目标"字段后添加参数:
"C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222 --user-data-dir="%USERPROFILE%\chrome-debug-profile" - 点击"确定"保存
Linux 系统
打开终端,执行:
google-chrome \
--remote-debugging-port=9222 \
--user-data-dir=~/chrome-debug-profile \
--no-first-run \
--no-default-browser-check
4.2 命令行参数详解
--remote-debugging-port=9222:指定调试端口,可以改为其他未占用端口--user-data-dir:指定用户数据目录,用于保存浏览器状态(包括扩展、登录信息等)--no-first-run:跳过首次运行向导--no-default-browser-check:不检查是否为默认浏览器--disable-default-apps:禁用默认应用
4.3 验证调试模式是否启动成功
启动 Chrome 后,在浏览器中访问:
http://localhost:9222/json
如果能看到 JSON 格式的页面信息,说明调试模式已成功启动。
五、环境准备
5.1 安装依赖
创建项目目录并设置虚拟环境:
# 创建项目目录
mkdir metamask-automation
cd metamask-automation
# 创建虚拟环境
python3 -m venv .venv
# 激活虚拟环境
# macOS/Linux:
source .venv/bin/activate
# Windows:
.venv\Scripts\activate
# 安装依赖
pip install playwright python-dotenv
5.2 安装 Playwright 浏览器
playwright install chromium
5.3 安装 MetaMask 扩展
- 启动 Chrome 调试模式(使用上述方法)
- 访问 MetaMask 官网 或 Chrome 应用商店
- 安装 MetaMask 扩展
- 完成钱包的初始化设置(创建或导入钱包)
- 重要:记住你的 MetaMask 密码,后续自动化会用到
5.4 获取 MetaMask 扩展 ID
安装完 MetaMask 后,需要获取其扩展 ID:
- 在 Chrome 地址栏输入:
chrome://extensions/ - 找到 MetaMask 扩展
- 扩展 ID 通常是:
nkbihfbeogaeaoehlefnkodbefgpgknn(官方版本的固定ID)
六、代码实现
6.1 项目结构
metamask-automation/
├── .env # 环境变量配置
├── .venv/ # 虚拟环境
├── chrome/
│ ├── __init__.py
│ ├── launch.py # 浏览器启动模块
│ └── metamask.py # MetaMask 操作模块
├── util/
│ ├── __init__.py
│ └── logger.py # 日志模块
├── config.py # 配置文件
└── main.py # 主程序
6.2 创建配置文件
config.py
import os
from pathlib import Path
from dataclasses import dataclass
from dotenv import load_dotenv
load_dotenv()
@dataclass
class ChromeConfig:
"""Chrome 浏览器配置"""
metamask_password: str
debug_port: int = 9222
debug_dir: str = Path(__file__).resolve().parent / "chrome/chrome_debug"
class Config:
def __init__(self):
# 从环境变量读取 MetaMask 密码
pwd = os.getenv("METAMASK_PASSWORD")
if not pwd:
raise ValueError("请在 .env 文件中设置 METAMASK_PASSWORD")
self.chrome = ChromeConfig(metamask_password=pwd)
.env
# MetaMask 钱包密码
METAMASK_PASSWORD=your_metamask_password_here
6.3 实现浏览器启动模块
util/logger.py
import logging
import os
from pathlib import Path
class Logger:
"""日志工具类"""
@staticmethod
def get_logger(name: str):
"""获取 logger 实例"""
# 创建 logs 目录
log_dir = Path(__file__).resolve().parent.parent / "logs"
log_dir.mkdir(exist_ok=True)
# 配置日志
logger = logging.getLogger(name)
logger.setLevel(logging.INFO)
# 控制台输出
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
# 文件输出
file_handler = logging.FileHandler(
log_dir / "app.log",
encoding='utf-8'
)
file_handler.setLevel(logging.INFO)
# 设置格式
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
console_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)
logger.addHandler(console_handler)
logger.addHandler(file_handler)
return logger
chrome/launch.py
import os
import time
import socket
import subprocess
import platform
import shutil
from util.logger import Logger
logger = Logger.get_logger(__name__)
class LaunchBrowser:
"""浏览器启动类 - 负责启动 Chrome 调试模式"""
def __init__(self, debug_dir: str, debug_port: int = 9222):
"""
初始化浏览器启动器
Args:
debug_dir: Chrome 用户数据目录
debug_port: 调试端口
"""
self.debug_dir = debug_dir
self.debug_port = debug_port
self.chrome_process = None
def get_platform(self):
"""获取当前操作系统平台"""
system_name = platform.system().lower()
if system_name == "windows":
return "windows"
elif system_name == "darwin":
return "macos"
elif system_name == "linux":
return "linux"
else:
return "unknown"
def launch_browser(self):
"""启动浏览器(如果调试端口已开启则跳过)"""
# 检查端口是否已经开放
if self._is_debug_port_open():
logger.info(f"Chrome 调试端口 {self.debug_port} 已开启,跳过启动")
return
# 端口未开放,启动新的 Chrome
logger.info(f"在端口 {self.debug_port} 启动新的 Chrome 实例")
command = self._get_launch_command()
self._execute_command(command)
# 等待 Chrome 启动
if not self._wait_for_debug_port(timeout=30):
raise Exception(f"Chrome 在端口 {self.debug_port} 启动失败")
logger.info(f"Chrome 在端口 {self.debug_port} 成功启动")
def _is_debug_port_open(self):
"""检查调试端口是否已经开放"""
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(1)
result = sock.connect_ex(('127.0.0.1', self.debug_port))
sock.close()
return result == 0
except Exception:
return False
def _wait_for_debug_port(self, timeout=30):
"""等待调试端口开放"""
logger.info(f"等待 Chrome 调试端口 {self.debug_port}...")
start_time = time.time()
while time.time() - start_time < timeout:
if self._is_debug_port_open():
time.sleep(2) # 端口开放后再等待一会,确保完全启动
return True
time.sleep(1)
return False
def _get_launch_command(self):
"""获取启动命令(根据不同平台)"""
platform_name = self.get_platform()
# 确保用户数据目录存在
self._prepare_user_data_dir()
if platform_name == "macos":
return [
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
f"--remote-debugging-port={self.debug_port}",
f"--user-data-dir={self.debug_dir}",
"--no-first-run",
"--no-default-browser-check",
"--disable-default-apps",
]
elif platform_name == "windows":
chrome_paths = [
r"C:\Program Files\Google\Chrome\Application\chrome.exe",
r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
]
chrome_path = None
for path in chrome_paths:
if os.path.exists(path):
chrome_path = path
break
if not chrome_path:
raise Exception("未找到 Chrome 浏览器")
return [
chrome_path,
f"--remote-debugging-port={self.debug_port}",
f"--user-data-dir={self.debug_dir}",
"--no-first-run",
"--no-default-browser-check",
]
elif platform_name == "linux":
return [
"google-chrome",
f"--remote-debugging-port={self.debug_port}",
f"--user-data-dir={self.debug_dir}",
"--no-first-run",
"--no-default-browser-check",
]
else:
raise NotImplementedError(f"不支持的平台: {platform_name}")
def _execute_command(self, command):
"""执行启动命令"""
platform_name = self.get_platform()
if platform_name == "macos" or platform_name == "linux":
# macOS 和 Linux 使用 subprocess.Popen
self.chrome_process = subprocess.Popen(
command,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
else:
# Windows
self.chrome_process = subprocess.Popen(
command,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
def _prepare_user_data_dir(self):
"""准备用户数据目录"""
# 如果目录不存在,尝试从默认 Chrome 目录复制
if not os.path.exists(self.debug_dir):
logger.info("首次运行,准备用户数据目录...")
self._copy_user_data_dir()
else:
logger.info(f"使用现有用户数据目录: {self.debug_dir}")
def _copy_user_data_dir(self):
"""从默认 Chrome 目录复制用户数据"""
platform_name = self.get_platform()
# 根据平台确定源目录
if platform_name == "macos":
source_dir = os.path.join(
os.path.expanduser("~"),
"Library",
"Application Support",
"Google",
"Chrome",
)
elif platform_name == "windows":
source_dir = os.path.join(
os.path.expanduser("~"),
"AppData",
"Local",
"Google",
"Chrome",
"User Data",
)
elif platform_name == "linux":
source_dir = os.path.join(
os.path.expanduser("~"),
".config",
"google-chrome"
)
else:
raise NotImplementedError(f"不支持的平台: {platform_name}")
try:
if os.path.exists(source_dir):
shutil.copytree(source_dir, self.debug_dir, dirs_exist_ok=False)
logger.info(f"已复制 Chrome 用户数据到: {self.debug_dir}")
else:
# 如果源目录不存在,创建一个空目录
os.makedirs(self.debug_dir, exist_ok=True)
logger.warning("未找到默认 Chrome 目录,创建新的用户数据目录")
except FileExistsError:
logger.warning(f"用户数据目录已存在: {self.debug_dir}")
except Exception as e:
logger.error(f"复制用户数据目录时出错: {e}")
os.makedirs(self.debug_dir, exist_ok=True)
def close_browser(self):
"""关闭浏览器进程"""
if self.chrome_process:
self.chrome_process.terminate()
time.sleep(1)
if self.chrome_process.poll() is None:
self.chrome_process.kill()
6.4 实现 MetaMask 操作模块
chrome/metamask.py
from time import sleep
from playwright.sync_api import BrowserContext, Page
from util.logger import Logger
logger = Logger.get_logger(__name__)
class MetaMask:
"""MetaMask 钱包操作类"""
def __init__(self, context: BrowserContext, password: str):
"""
初始化 MetaMask 操作类
Args:
context: Playwright 浏览器上下文
password: MetaMask 钱包密码
"""
self.context = context
self.password = password
# MetaMask 扩展的 URL(官方扩展的固定 ID)
self.url = "chrome-extension://nkbihfbeogaeaoehlefnkodbefgpgknn/home.html"
def _open_metamask_page(self) -> Page:
"""打开 MetaMask 页面"""
logger.info("打开 MetaMask 扩展页面")
page = self.context.new_page()
page.goto(self.url)
return page
def _unlock_metamask(self, page: Page):
"""
解锁 MetaMask 钱包
如果 MetaMask 处于锁定状态,输入密码解锁
"""
sleep(1)
# 查找密码输入框
password_input = page.locator('input[type="password"]')
# 如果找到密码输入框,说明需要解锁
if password_input.count() > 0:
logger.info("MetaMask 处于锁定状态,正在解锁...")
password_input.fill(self.password)
page.get_by_test_id("unlock-submit").click()
sleep(2)
logger.info("MetaMask 解锁成功")
else:
logger.info("MetaMask 已处于解锁状态")
def connect_wallet(self):
"""
连接钱包到网站
用于处理网站的"连接钱包"请求
当网站请求连接 MetaMask 时,会弹出一个新窗口
本方法自动点击"连接"按钮完成授权
"""
logger.info("开始连接 MetaMask 钱包...")
page = self._open_metamask_page()
self._unlock_metamask(page)
# 等待连接按钮出现并点击
try:
page.wait_for_selector('button[data-testid="confirm-btn"]', timeout=5000)
page.get_by_test_id("confirm-btn").click()
logger.info("已点击连接按钮")
sleep(1)
except Exception as e:
logger.warning(f"未找到连接按钮: {e}")
# 关闭 MetaMask 页面
page.close()
logger.info("MetaMask 钱包连接完成")
def sign_transaction(self):
"""
签名交易
当网站请求签名交易时,MetaMask 会弹出确认窗口
本方法自动点击"确认"按钮完成签名
"""
logger.info("开始签名交易...")
page = self._open_metamask_page()
self._unlock_metamask(page)
# 刷新页面以加载最新的交易请求
page.reload()
# 等待确认按钮出现并点击
try:
page.wait_for_selector(
'button[data-testid="confirm-footer-button"]',
timeout=10000
)
page.get_by_test_id("confirm-footer-button").click()
logger.info("已确认交易签名")
sleep(2)
except Exception as e:
logger.error(f"签名交易失败: {e}")
raise
# 关闭 MetaMask 页面
page.close()
logger.info("交易签名完成")
def get_account_address(self) -> str:
"""
获取当前账户地址
Returns:
当前账户的以太坊地址
"""
logger.info("获取账户地址...")
page = self._open_metamask_page()
self._unlock_metamask(page)
try:
# 点击账户菜单
page.click('[data-testid="account-menu-icon"]')
sleep(1)
# 获取账户地址
address_element = page.locator('[data-testid="address-copy-button-text"]')
address = address_element.inner_text()
logger.info(f"当前账户地址: {address}")
page.close()
return address
except Exception as e:
logger.error(f"获取账户地址失败: {e}")
page.close()
raise
def switch_network(self, network_name: str):
"""
切换网络
Args:
network_name: 网络名称,如 "Ethereum Mainnet", "Sepolia" 等
"""
logger.info(f"切换到网络: {network_name}")
page = self._open_metamask_page()
self._unlock_metamask(page)
try:
# 点击网络切换按钮
page.click('[data-testid="network-display"]')
sleep(1)
# 选择目标网络
page.click(f'text="{network_name}"')
sleep(2)
logger.info(f"已切换到网络: {network_name}")
except Exception as e:
logger.error(f"切换网络失败: {e}")
raise
finally:
page.close()
6.5 实现主程序
main.py
from playwright.sync_api import sync_playwright
from chrome.launch import LaunchBrowser
from chrome.metamask import MetaMask
from config import Config
from util.logger import Logger
logger = Logger.get_logger(__name__)
class MetaMaskAutomation:
"""MetaMask 自动化主类"""
def __init__(self, config: Config):
self.config = config
self.launcher = LaunchBrowser(
config.chrome.debug_dir,
config.chrome.debug_port
)
self.metamask = None
self.context = None
self.page = None
def run(self):
"""运行自动化任务"""
try:
# 1. 启动 Chrome 调试模式
logger.info("=" * 60)
logger.info("步骤 1: 启动 Chrome 调试模式")
logger.info("=" * 60)
self.launcher.launch_browser()
# 2. 使用 Playwright 连接到 Chrome
logger.info("=" * 60)
logger.info("步骤 2: 连接到 Chrome 浏览器")
logger.info("=" * 60)
with sync_playwright() as p:
logger.info(
f"正在连接到 http://localhost:{self.config.chrome.debug_port}"
)
browser = p.chromium.connect_over_cdp(
f"http://localhost:{self.config.chrome.debug_port}",
timeout=30000
)
logger.info("成功连接到浏览器")
# 获取浏览器上下文
contexts = browser.contexts
if not contexts:
raise Exception("没有可用的浏览器上下文")
self.context = contexts[0]
# 3. 初始化 MetaMask 操作对象
logger.info("=" * 60)
logger.info("步骤 3: 初始化 MetaMask")
logger.info("=" * 60)
self.metamask = MetaMask(
self.context,
self.config.chrome.metamask_password
)
# 4. 执行自动化任务
self.execute_tasks()
# 保持浏览器打开,等待用户操作
logger.info("=" * 60)
logger.info("自动化任务完成!")
logger.info("浏览器将保持打开状态,按 Ctrl+C 退出")
logger.info("=" * 60)
input() # 等待用户按回车键
except KeyboardInterrupt:
logger.info("\n用户中断程序")
except Exception as e:
logger.error(f"发生错误: {e}")
raise
def execute_tasks(self):
"""执行具体的自动化任务"""
logger.info("=" * 60)
logger.info("开始执行自动化任务")
logger.info("=" * 60)
# 示例任务 1: 获取账户地址
try:
address = self.metamask.get_account_address()
logger.info(f"✓ 成功获取账户地址: {address}")
except Exception as e:
logger.error(f"✗ 获取账户地址失败: {e}")
# 示例任务 2: 打开一个 DApp 网站
logger.info("\n打开示例网站...")
if len(self.context.pages) == 0:
self.page = self.context.new_page()
else:
self.page = self.context.pages[-1]
# 你可以在这里打开任何需要连接 MetaMask 的网站
# 例如:self.page.goto("https://app.uniswap.org")
self.page.goto("https://metamask.io/")
logger.info("✓ 已打开示例网站")
# 示例任务 3: 如果需要连接钱包,可以调用
# self.metamask.connect_wallet()
# 示例任务 4: 如果需要签名交易,可以调用
# self.metamask.sign_transaction()
def main():
"""主函数"""
try:
# 加载配置
config = Config()
# 创建自动化实例
automation = MetaMaskAutomation(config)
# 运行自动化任务
automation.run()
except Exception as e:
logger.error(f"程序异常退出: {e}")
raise
if __name__ == "__main__":
main()
七、使用说明
7.1 首次运行准备
安装 MetaMask 扩展
首先需要手动启动 Chrome 调试模式并安装 MetaMask:
# macOS /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \ --remote-debugging-port=9222 \ --user-data-dir=~/chrome-debug-profile然后在浏览器中:
- 访问 Chrome 应用商店
- 搜索并安装 MetaMask
- 完成钱包初始化(创建或导入)
- 记住密码
配置环境变量
创建
.env文件:METAMASK_PASSWORD=你的MetaMask密码安装依赖
pip install -r requirements.txt playwright install chromium
7.2 运行程序
python main.py
7.3 自定义任务
在 main.py 的 execute_tasks 方法中,你可以:
打开任何 Web3 网站
self.page.goto("https://app.uniswap.org")等待并点击"连接钱包"按钮
# 等待页面上的连接按钮 connect_btn = self.page.locator('button:has-text("Connect Wallet")') connect_btn.click() # 选择 MetaMask self.page.click('text="MetaMask"') # 自动完成 MetaMask 连接授权 self.metamask.connect_wallet()执行交易并自动签名
# 触发交易(例如点击 Swap 按钮) self.page.click('button[data-testid="swap-button"]') # 自动签名交易 self.metamask.sign_transaction()
八、高级技巧
8.1 处理多个账户
MetaMask 可以管理多个账户,你可以通过点击账户切换:
def switch_account(self, account_index: int):
"""切换到指定账户"""
page = self._open_metamask_page()
self._unlock_metamask(page)
# 点击账户菜单
page.click('[data-testid="account-menu-icon"]')
# 选择账户(根据索引)
accounts = page.locator('[data-testid^="account-list-item"]')
accounts.nth(account_index).click()
page.close()
8.2 监听交易状态
def wait_for_transaction(self, timeout: int = 60):
"""等待交易完成"""
page = self._open_metamask_page()
# 等待交易确认通知
try:
page.wait_for_selector(
'text="Confirmed"',
timeout=timeout * 1000
)
logger.info("交易已确认")
return True
except:
logger.error("交易确认超时")
return False
finally:
page.close()
8.3 批量处理交易
def execute_multiple_transactions(self, transaction_funcs: list):
"""批量执行多个交易"""
for i, func in enumerate(transaction_funcs, 1):
logger.info(f"执行第 {i}/{len(transaction_funcs)} 个交易")
try:
func() # 执行触发交易的函数
self.metamask.sign_transaction()
logger.info(f"第 {i} 个交易完成")
except Exception as e:
logger.error(f"第 {i} 个交易失败: {e}")
8.4 错误处理和重试机制
import time
from functools import wraps
def retry(max_attempts=3, delay=2):
"""重试装饰器"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
logger.warning(
f"第 {attempt} 次尝试失败: {e}"
)
if attempt < max_attempts:
time.sleep(delay)
else:
raise
return wrapper
return decorator
# 使用示例
@retry(max_attempts=3, delay=5)
def sign_transaction(self):
"""签名交易(带重试)"""
# ... 原有代码 ...
九、常见问题与解决方案
9.1 Chrome 调试端口被占用
问题:启动时提示端口已被占用
解决方案:
# 查找占用端口的进程
# macOS/Linux:
lsof -i :9222
# Windows:
netstat -ano | findstr :9222
# 结束进程或更换端口
9.2 MetaMask 扩展 ID 不同
问题:MetaMask URL 打不开
解决方案:
- 访问
chrome://extensions/ - 找到 MetaMask 扩展的 ID
- 修改
metamask.py中的 URL
9.3 元素定位失败
问题:找不到按钮或元素
解决方案:
- MetaMask 界面可能更新,使用浏览器开发者工具检查最新的
data-testid - 使用更灵活的选择器,如
text="按钮文字" - 添加等待时间:
page.wait_for_selector()
9.4 密码解锁失败
问题:输入密码后无法解锁
解决方案:
- 检查
.env文件中的密码是否正确 - 确保没有特殊字符导致的编码问题
- 手动解锁一次,让 MetaMask 记住密码
9.5 无法连接到调试端口
问题:Playwright 无法连接到 Chrome
解决方案:
# 增加连接超时时间
browser = p.chromium.connect_over_cdp(
f"http://localhost:{self.debug_port}",
timeout=60000 # 增加到 60 秒
)
9.6 遇到 LavaMoat 错误
问题:在控制台看到 LavaMoat 相关错误
解决方案:
- 确认你使用的是 Playwright CDP 连接而不是 Selenium
- 检查
navigator.webdriver是否为undefined(在浏览器控制台输入检查) - 如果仍然有问题,尝试使用真实的用户数据目录而不是空目录
十、安全注意事项
10.1 保护敏感信息
永远不要提交
.env文件到版本控制在
.gitignore中添加:.env chrome/chrome_debug/ logs/使用测试钱包
不要在自动化程序中使用包含大量资金的主钱包
加密存储密码
对于生产环境,考虑使用密钥管理服务(如 AWS Secrets Manager)
10.2 网络安全
- 调试端口默认只监听
localhost,不要将其暴露到公网 - 使用 HTTPS 网站进行交互
- 验证合约地址和交易详情
10.3 代码审计
在执行交易前,务必:
- 验证目标合约地址
- 检查交易金额
- 确认Gas费用合理
十一、扩展应用场景
11.1 自动化测试
class DAppTester:
"""DApp 自动化测试类"""
def test_token_swap(self):
"""测试代币交换功能"""
# 打开 DApp
self.page.goto("https://your-dapp.com")
# 连接钱包
self.page.click('button:has-text("Connect")')
self.metamask.connect_wallet()
# 输入交易参数
self.page.fill('input[name="amount"]', "0.01")
# 执行交易
self.page.click('button:has-text("Swap")')
self.metamask.sign_transaction()
# 验证结果
success_msg = self.page.locator('text="Swap Successful"')
assert success_msg.is_visible()
11.2 批量空投领取
def claim_airdrops(airdrop_urls: list):
"""批量领取空投"""
for url in airdrop_urls:
try:
page.goto(url)
page.click('button:has-text("Claim")')
metamask.sign_transaction()
logger.info(f"✓ 成功领取: {url}")
except Exception as e:
logger.error(f"✗ 领取失败 {url}: {e}")
11.3 价格监控与自动交易
def monitor_price_and_swap(target_price: float):
"""监控价格并自动交易"""
while True:
current_price = get_token_price() # 自定义价格获取函数
if current_price <= target_price:
logger.info(f"价格达到目标: {current_price}")
execute_swap()
break
time.sleep(60) # 每分钟检查一次
十二、总结
本教程介绍了如何使用 Python 和 Playwright 通过 Chrome 调试模式来自动化操控 MetaMask 钱包。这是一个从失败(Selenium + LavaMoat 问题)到成功(Playwright + CDP)的实战经验分享。
核心要点回顾:
- ✅ 避开 LavaMoat 陷阱:Selenium 会被 MetaMask 的 LavaMoat 安全机制检测和阻止
- ✅ CDP 是关键:使用 Chrome 远程调试协议可以完美绕过自动化检测
- ✅ Chrome 调试模式:使用
--remote-debugging-port启动真实的浏览器环境 - ✅ Playwright CDP 连接:通过
connect_over_cdp连接到 Chrome - ✅ MetaMask 自动化:解锁、连接、签名等操作的自动化实现
- ✅ 最佳实践:错误处理、日志记录、安全注意事项
技术方案对比总结:
| 方案 | 调试模式 | 优势 | 劣势 | 推荐度 |
|---|---|---|---|---|
| Selenium + 调试端口 | ✅ 是 | 简单易用,文档丰富 | ❌ 注入 webdriver 标识,被 LavaMoat 阻止 | ⭐ |
| undetected-chromedriver | ✅ 是 | 能绕过部分网站检测 | ❌ 本质仍是 Selenium,对 MetaMask 无效 | ⭐⭐ |
| Playwright CDP | ✅ 是 | 无任何注入,完美绕过 LavaMoat | 需要理解 CDP 概念 | ⭐⭐⭐⭐⭐ |
关键发现:三种方案都使用 Chrome 调试模式,但只有 Playwright 的 CDP 连接方式不会暴露自动化特征!
后续学习方向:
- 学习 Web3.py 进行链上数据查询
- 研究智能合约交互
- 实现更复杂的 DeFi 操作(如流动性挖矿、Yield Farming)
- 集成机器学习进行交易策略优化
希望本教程能帮助你快速上手 MetaMask 自动化!如果有任何问题,欢迎交流讨论。
附录
A. 完整的 requirements.txt
playwright==1.55.0
python-dotenv==1.1.1
B. 项目文件清单
metamask-automation/
├── .env # 环境变量(不要提交到 Git)
├── .gitignore # Git 忽略配置
├── requirements.txt # Python 依赖
├── README.md # 项目说明
├── chrome/
│ ├── __init__.py
│ ├── launch.py # 浏览器启动模块
│ ├── metamask.py # MetaMask 操作模块
│ └── chrome_debug/ # Chrome 用户数据(不要提交到 Git)
├── util/
│ ├── __init__.py
│ └── logger.py # 日志工具
├── logs/ # 日志目录(不要提交到 Git)
├── config.py # 配置文件
└── main.py # 主程序
C. 参考资源
D. 相关阅读
版权声明:本教程仅供学习和研究使用,请勿用于非法用途。使用自动化工具操作钱包存在风险,请谨慎使用并自行承担风险。
关于 LavaMoat:LavaMoat 是 MetaMask 的重要安全特性,本教程展示的 CDP 方法是合法的自动化技术,不涉及绕过或破解安全机制,而是使用了不触发检测的正常浏览器控制方式。