如何通过Selenium手动注入WhatsApp Web会话以保持登录?
问题:手动保存/恢复WhatsApp Web会话数据,避免重复扫码登录
我正在开发基于Selenium的Python工具,用于WhatsApp Web收发消息,希望避免每次运行都扫码登录。
已知WhatsApp Web的会话数据存储在名为wawc的IndexedDB,并同步到localStorage。我编写了extract_session.js和inject_session.js分别提取、注入会话数据,通过Chrome DevTools看注入成功,但刷新页面后会跳转到登出/扫码界面,IndexedDB数据被清空。
我知道用ChromeOptions的user-data-dir可以解决,但更倾向于手动仅保存WhatsApp Web的用户数据。尝试过在页面加载前用execute_cdp_cmd设置IndexedDB和LocalStorage,依然出现登出页面,推测遗漏了其他存储位置的会话数据。
附相关代码:
extract_session.js
function requestPromise(request) { return new Promise((resolve, reject) => { request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } async function openDatabase(dbName) { const request = indexedDB.open(dbName); return requestPromise(request); } async function getAllFromStore(db, storeName) { const transaction = db.transaction(storeName, "readonly"); const objectStore = transaction.objectStore(storeName); return requestPromise(objectStore.getAll()); } try { const db = await openDatabase(arguments[0]); const data = await getAllFromStore(db, arguments[1]); return JSON.stringify(data); } catch (error) { console.log("Failed to extract session:", error) return null; }
inject_session.js
function requestPromise(request) { return new Promise((resolve, reject) => { request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } async function openDatabase(dbName) { const request = indexedDB.open(dbName); return requestPromise(request); } async function putInStore(db, storeName, data) { const transaction = db.transaction(storeName, "readwrite"); const objectStore = transaction.objectStore(storeName); for (const item of data) { const request = objectStore.put(item); await requestPromise(request); } } try { const session = JSON.parse(arguments[2]); const db = await openDatabase(arguments[0]); await putInStore(db, arguments[1], session); localStorage.clear() for (const item of session) { localStorage.setItem(item.key, item.value); } } catch (error) { console.error("Failed to inject session:", error); }
Python复现代码
from selenium.webdriver import Chrome, ChromeOptions from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By import os def _handle_await(parent, locator, timeout, poll_freq): try: return WebDriverWait(parent, timeout, poll_frequency=poll_freq ).until(EC.presence_of_element_located(locator)) except TimeoutException: return None def await_element(parent, locator, timeout=0, poll_freq=0.05): if timeout == 0: while True: res = _handle_await(parent, locator, timeout, poll_freq) if res is not None: return res else: return _handle_await(parent, locator, timeout, poll_freq) QR_CODE = (By.CSS_SELECTOR, "canvas[role='img']") LOGGED_IN = (By.CSS_SELECTOR, "div[title='Chats']") options = ChromeOptions() driver = Chrome(options=options) driver.get("https://web.whatsapp.com/") await_element(driver, QR_CODE) if os.path.isfile("session.wa"): js_code = open("inject_session.js", "r").read() with open("session.wa", "r", encoding="utf-8") as file: driver.execute_script(js_code, "wawc", "user", file.read()) driver.refresh() input() else: await_element(driver, LOGGED_IN) js_code = open("extract_session.js", "r").read() session_data = driver.execute_script(js_code, "wawc", "user") with open("session.wa", "w", encoding="utf-8") as file: file.write(str(session_data)) input()
问题现象:首次运行生成session.wa,再次运行注入数据后刷新,出现登出界面并跳转至扫码页。
问题分析与解决方案
1. 核心问题:遗漏关键存储位置
WhatsApp Web的会话验证不仅依赖IndexedDB和localStorage,还需要:
- Cookie:包含会话标识的核心验证Cookie(如
wa_session系列) - Service Worker缓存:存储会话相关的资源与状态,用于快速验证
2. 修正后的提取/注入逻辑
完善extract_session.js(新增Cookie和缓存提取)
async function requestPromise(request) { return new Promise((resolve, reject) => { request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } async function openDatabase(dbName) { const request = indexedDB.open(dbName); return requestPromise(request); } async function getAllFromStore(db, storeName) { const transaction = db.transaction(storeName, "readonly"); const objectStore = transaction.objectStore(storeName); return requestPromise(objectStore.getAll()); } // 提取Cookie function getCookies() { return document.cookie.split(';').map(cookie => { const [key, ...valueParts] = cookie.trim().split('='); return {key, value: valueParts.join('=')}; }); } // 提取Service Worker缓存 async function getServiceWorkerCache() { if (!caches) return []; const cacheNames = await caches.keys(); const cacheData = []; for (const name of cacheNames) { const cache = await caches.open(name); const keys = await cache.keys(); for (const key of keys) { const response = await cache.match(key); cacheData.push({ cacheName: name, url: key.url, response: await response.text() }); } } return cacheData; } try { const db = await openDatabase(arguments[0]); const indexedDbData = await getAllFromStore(db, arguments[1]); const cookies = getCookies(); const cacheData = await getServiceWorkerCache(); return JSON.stringify({ indexedDb: indexedDbData, localStorage: Object.fromEntries(Object.entries(localStorage)), cookies: cookies, cache: cacheData }); } catch (error) { console.log("Failed to extract session:", error) return null; }
完善inject_session.js(新增Cookie和缓存注入)
async function requestPromise(request) { return new Promise((resolve, reject) => { request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } async function openDatabase(dbName) { const request = indexedDB.open(dbName); return requestPromise(request); } async function putInStore(db, storeName, data) { const transaction = db.transaction(storeName, "readwrite"); const objectStore = transaction.objectStore(storeName); // 先清空原有数据避免冲突 await requestPromise(objectStore.clear()); for (const item of data) { const request = objectStore.put(item); await requestPromise(request); } } // 注入Cookie function setCookies(cookies) { cookies.forEach(cookie => { document.cookie = `${cookie.key}=${cookie.value}; path=/; domain=.web.whatsapp.com`; }); } // 注入Service Worker缓存 async function setServiceWorkerCache(cacheData) { if (!caches) return; for (const entry of cacheData) { const cache = await caches.open(entry.cacheName); await cache.put(new Request(entry.url), new Response(entry.response)); } } try { const session = JSON.parse(arguments[2]); // 注入IndexedDB const db = await openDatabase(arguments[0]); await putInStore(db, arguments[1], session.indexedDb); // 注入localStorage localStorage.clear(); Object.entries(session.localStorage).forEach(([key, value]) => { localStorage.setItem(key, value); }); // 注入Cookie setCookies(session.cookies); // 注入Service Worker缓存 await setServiceWorkerCache(session.cache); } catch (error) { console.error("Failed to inject session:", error); }
3. 修正Python代码(调整注入时机)
必须在打开WhatsApp Web前完成注入,否则页面会提前执行验证逻辑覆盖数据:
from selenium.webdriver import Chrome, ChromeOptions from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By import os import json def _handle_await(parent, locator, timeout, poll_freq): try: return WebDriverWait(parent, timeout, poll_frequency=poll_freq ).until(EC.presence_of_element_located(locator)) except TimeoutException: return None def await_element(parent, locator, timeout=0, poll_freq=0.05): if timeout == 0: while True: res = _handle_await(parent, locator, timeout, poll_freq) if res is not None: return res else: return _handle_await(parent, locator, timeout, poll_freq) QR_CODE = (By.CSS_SELECTOR, "canvas[role='img']") LOGGED_IN = (By.CSS_SELECTOR, "div[title='Chats']") options = ChromeOptions() # 禁用Service Worker缓存自动清理 options.add_argument("--disable-service-worker-cache") driver = Chrome(options=options) if os.path.isfile("session.wa"): # 先打开空白页,避免WhatsApp提前加载验证 driver.get("about:blank") # 注入会话数据 js_code = open("inject_session.js", "r").read() with open("session.wa", "r", encoding="utf-8") as file: session_data = json.load(file) driver.execute_script(js_code, "wawc", "user", json.dumps(session_data)) # 打开WhatsApp Web driver.get("https://web.whatsapp.com/") # 等待登录状态 await_element(driver, LOGGED_IN, timeout=10) print("会话恢复成功") else: driver.get("https://web.whatsapp.com/") await_element(driver, LOGGED_IN) # 提取会话数据 js_code = open("extract_session.js", "r").read() session_data = driver.execute_script(js_code, "wawc", "user") with open("session.wa", "w", encoding="utf-8") as file: file.write(session_data) print("会话已保存") input("按任意键退出...") driver.quit()
4. 关键注意事项
- 注入时机:必须在打开WhatsApp Web前完成所有数据注入
- Cookie域名:确保Cookie设置的域名为
.web.whatsapp.com,避免作用域错误 - IndexedDB清空:注入前清空原有数据,避免新旧数据冲突
- Service Worker:部分版本的WhatsApp Web依赖其缓存,建议保留相关逻辑
内容的提问来源于stack exchange,提问作者akrentz6
相关产品推荐
相关产品推荐

