549 lines
18 KiB
Python
549 lines
18 KiB
Python
"""浏览器登录模块 - 使用 DrissionPage 控制浏览器进行登录检测和 cookie 管理"""
|
|
from typing import Dict, List, Optional
|
|
import json
|
|
import time
|
|
from DrissionPage import Chromium, ChromiumOptions
|
|
from applogger import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
class BrowserLogin:
|
|
"""浏览器登录客户端"""
|
|
|
|
def __init__(self, browser_type: str = "edge", headless: bool = False):
|
|
"""
|
|
初始化浏览器登录客户端
|
|
|
|
Args:
|
|
browser_type: 浏览器类型 (chrome 或 edge)
|
|
headless: 是否无头模式
|
|
"""
|
|
self.browser_type = browser_type.lower()
|
|
self.headless = headless
|
|
self.browser = None
|
|
self.tab = None
|
|
|
|
def _create_browser(self):
|
|
"""创建浏览器实例"""
|
|
co = ChromiumOptions()
|
|
|
|
# 设置浏览器路径
|
|
if self.browser_type == "edge":
|
|
co.set_browser_path("C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe")
|
|
elif self.browser_type == "chrome":
|
|
co.set_browser_path("chrome")
|
|
|
|
# 设置无头模式
|
|
if self.headless:
|
|
co.headless = True
|
|
|
|
# 设置其他选项
|
|
co.set_argument('--no-sandbox')
|
|
co.set_argument('--disable-dev-shm-usage')
|
|
|
|
# 创建浏览器
|
|
self.browser = Chromium(co)
|
|
self.tab = self.browser.new_tab()
|
|
|
|
def _close_browser(self):
|
|
"""关闭浏览器"""
|
|
if self.browser:
|
|
try:
|
|
self.browser.quit()
|
|
except Exception as e:
|
|
logger.error(f"关闭浏览器失败: {e}")
|
|
finally:
|
|
self.browser = None
|
|
self.tab = None
|
|
|
|
def _get_domain_from_url(self, url: str) -> str:
|
|
"""
|
|
从 URL 中提取域名
|
|
|
|
Args:
|
|
url: 网站地址
|
|
|
|
Returns:
|
|
域名
|
|
"""
|
|
from urllib.parse import urlparse
|
|
parsed = urlparse(url)
|
|
return parsed.netloc
|
|
|
|
def get_all_cookies(self) -> List[Dict]:
|
|
"""
|
|
获取当前浏览器实例的所有 cookie
|
|
|
|
Returns:
|
|
所有 cookie 的列表
|
|
"""
|
|
# 先获取 cookies 对象,然后调用 as_dict 方法
|
|
cookies_obj = self.tab.cookies
|
|
cookies = cookies_obj() if callable(cookies_obj) else cookies_obj
|
|
cookies = cookies.as_dict() if hasattr(cookies, 'as_dict') else cookies
|
|
|
|
# 确保返回完整的 cookie 列表,包含所有属性
|
|
formatted_cookies = []
|
|
for cookie in cookies:
|
|
formatted_cookie = {
|
|
'name': cookie.get('name', ''),
|
|
'value': cookie.get('value', ''),
|
|
'domain': cookie.get('domain', ''),
|
|
'path': cookie.get('path', '/'),
|
|
'expires': cookie.get('expirationDate', None),
|
|
'secure': cookie.get('secure', False),
|
|
'httpOnly': cookie.get('httpOnly', False),
|
|
'sameSite': cookie.get('sameSite', 'Lax')
|
|
}
|
|
formatted_cookies.append(formatted_cookie)
|
|
|
|
return formatted_cookies
|
|
|
|
def check_login(self, url: str, check_selector: str, success_text: str) -> bool:
|
|
"""
|
|
检查是否已登录
|
|
|
|
Args:
|
|
url: 网站地址
|
|
check_selector: 登录检测选择器
|
|
success_text: 登录成功时显示的文本
|
|
|
|
Returns:
|
|
是否已登录
|
|
"""
|
|
if not self.browser or not self.tab:
|
|
self._create_browser()
|
|
|
|
try:
|
|
# 访问网站
|
|
self.tab.get(url)
|
|
|
|
# 等待页面加载
|
|
time.sleep(2)
|
|
|
|
# 检查登录状态
|
|
element = self.tab.ele(check_selector, timeout=10)
|
|
|
|
if element and element.text and success_text in element.text:
|
|
return True
|
|
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error(f"检查登录状态失败: {e}")
|
|
return False
|
|
|
|
def import_all_cookies(self, cookies: List[Dict]) -> bool:
|
|
"""
|
|
一次性导入所有 cookies 到浏览器
|
|
|
|
Args:
|
|
cookies: cookie 列表
|
|
|
|
Returns:
|
|
是否成功
|
|
"""
|
|
if not self.browser or not self.tab:
|
|
self._create_browser()
|
|
|
|
success_count = 0
|
|
fail_count = 0
|
|
|
|
logger.info(f"开始导入 {len(cookies)} 个 cookie")
|
|
|
|
try:
|
|
# 准备所有 cookie 数据
|
|
formatted_cookies = []
|
|
for cookie in cookies:
|
|
cookie_dict = {
|
|
'name': cookie.get('name', ''),
|
|
'value': cookie.get('value', ''),
|
|
'domain': cookie.get('domain', ''),
|
|
'path': cookie.get('path', '/'),
|
|
}
|
|
if cookie.get('secure'):
|
|
cookie_dict['secure'] = True
|
|
if cookie.get('httpOnly'):
|
|
cookie_dict['httpOnly'] = True
|
|
if cookie.get('sameSite'):
|
|
cookie_dict['sameSite'] = cookie.get('sameSite')
|
|
formatted_cookies.append(cookie_dict)
|
|
|
|
# 根据 DrissionPage 文档,set.cookies() 方法可以直接接收列表格式的多个 cookie
|
|
logger.info("使用一次性注入方式导入 cookie")
|
|
self.tab.set.cookies(formatted_cookies)
|
|
success_count = len(formatted_cookies)
|
|
|
|
except Exception as e:
|
|
logger.error(f"导入 cookie 失败: {e}")
|
|
# 回退到逐个注入
|
|
logger.info("回退到逐个注入方式导入 cookie")
|
|
for cookie_dict in formatted_cookies:
|
|
try:
|
|
self.tab.set.cookies(cookie_dict)
|
|
success_count += 1
|
|
except Exception as e:
|
|
fail_count += 1
|
|
|
|
logger.info(f"Cookie 导入完成: 成功 {success_count}, 失败 {fail_count}")
|
|
return fail_count == 0
|
|
|
|
def verify_login(self, url: str, check_selector: str = "", success_text: str = "") -> bool:
|
|
"""
|
|
验证登录状态(cookies已预先导入)
|
|
|
|
Args:
|
|
url: 网站地址
|
|
check_selector: 登录检测选择器(可选)
|
|
success_text: 登录成功时显示的文本(可选)
|
|
|
|
Returns:
|
|
是否已登录
|
|
"""
|
|
if not self.browser or not self.tab:
|
|
self._create_browser()
|
|
|
|
try:
|
|
self.tab.get(url)
|
|
time.sleep(5)
|
|
|
|
# 如果没有指定选择器,只检查页面是否成功加载
|
|
if not check_selector:
|
|
logger.info(" 未指定登录检测选择器,跳过登录验证")
|
|
return True
|
|
|
|
# 尝试查找元素
|
|
try:
|
|
element = self.tab.ele(check_selector, timeout=10)
|
|
|
|
if element:
|
|
element_text = element.text or ""
|
|
logger.info(f" 找到元素,文本内容: {element_text[:50]}...")
|
|
|
|
if not success_text:
|
|
logger.info(" 未指定成功文本,找到元素即认为登录成功")
|
|
return True
|
|
|
|
if success_text in element_text:
|
|
logger.info(f" 检测到成功文本: {success_text}")
|
|
return True
|
|
else:
|
|
logger.warning(f" 未检测到成功文本 '{success_text}',元素文本: {element_text[:100]}")
|
|
return False
|
|
else:
|
|
logger.warning(f" 未找到元素: {check_selector}")
|
|
return False
|
|
except Exception as ele_error:
|
|
logger.warning(f" 查找元素失败: {ele_error}")
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error(f"验证登录状态失败: {e}")
|
|
return False
|
|
|
|
def login_with_cookies(
|
|
self,
|
|
url: str,
|
|
cookies: List[Dict],
|
|
check_selector: str,
|
|
success_text: str
|
|
) -> bool:
|
|
"""
|
|
使用 cookie 登录
|
|
|
|
Args:
|
|
url: 网站地址
|
|
cookies: cookie 列表
|
|
check_selector: 登录检测选择器
|
|
success_text: 登录成功时显示的文本
|
|
|
|
Returns:
|
|
是否登录成功
|
|
"""
|
|
if not self.browser or not self.tab:
|
|
self._create_browser()
|
|
|
|
try:
|
|
from urllib.parse import urlparse
|
|
parsed = urlparse(url)
|
|
base_url = f"{parsed.scheme}://{parsed.netloc}"
|
|
|
|
self.tab.get(base_url)
|
|
time.sleep(1)
|
|
|
|
logger.info(f"开始设置 {len(cookies)} 个 cookie")
|
|
for cookie in cookies:
|
|
try:
|
|
cookie_dict = {
|
|
'name': cookie.get('name', ''),
|
|
'value': cookie.get('value', ''),
|
|
'domain': cookie.get('domain', parsed.netloc),
|
|
'path': cookie.get('path', '/'),
|
|
}
|
|
if cookie.get('secure'):
|
|
cookie_dict['secure'] = True
|
|
if cookie.get('httpOnly'):
|
|
cookie_dict['httpOnly'] = True
|
|
if cookie.get('sameSite'):
|
|
cookie_dict['sameSite'] = cookie.get('sameSite')
|
|
|
|
self.tab.set.cookies(cookie_dict)
|
|
logger.info(f"设置 cookie: {cookie_dict['name']}")
|
|
except Exception as e:
|
|
logger.error(f"设置 cookie 失败 {cookie.get('name')}: {e}")
|
|
|
|
self.tab.get(url)
|
|
|
|
time.sleep(3)
|
|
|
|
element = self.tab.ele(check_selector, timeout=10)
|
|
|
|
if element and element.text and success_text in element.text:
|
|
return True
|
|
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error(f"使用 cookie 登录失败: {e}")
|
|
return False
|
|
|
|
def refresh_and_save_cookies(self, url: str) -> List[Dict]:
|
|
"""
|
|
刷新页面并保存新的 cookie
|
|
|
|
Args:
|
|
url: 网站地址
|
|
|
|
Returns:
|
|
cookie 列表
|
|
"""
|
|
if not self.browser or not self.tab:
|
|
self._create_browser()
|
|
|
|
try:
|
|
# 刷新页面
|
|
self.tab.refresh()
|
|
|
|
# 等待页面加载
|
|
time.sleep(3)
|
|
|
|
# 获取 cookies
|
|
cookies_obj = self.browser.cookies
|
|
cookies = cookies_obj() if callable(cookies_obj) else cookies_obj
|
|
cookies = cookies.as_dict() if hasattr(cookies, 'as_dict') else cookies
|
|
|
|
# 确保返回的是列表
|
|
if isinstance(cookies, list):
|
|
return cookies
|
|
elif isinstance(cookies, dict):
|
|
# 如果是字典,转换为列表
|
|
return [cookies]
|
|
else:
|
|
# 如果是其他类型,返回空列表
|
|
return []
|
|
|
|
except Exception as e:
|
|
logger.error(f"刷新页面并保存 cookie 失败: {e}")
|
|
return []
|
|
|
|
def close(self):
|
|
"""关闭浏览器"""
|
|
self._close_browser()
|
|
|
|
|
|
class CookieManager:
|
|
"""Cookie 管理器 - 实现浏览器实例级别的 Cookie 共享"""
|
|
|
|
def __init__(self, cookie_file: str):
|
|
"""
|
|
初始化 Cookie 管理器
|
|
|
|
Args:
|
|
cookie_file: cookie 文件路径
|
|
"""
|
|
self.cookie_file = cookie_file
|
|
self.cookies = self._load_cookies()
|
|
|
|
def _load_cookies(self) -> Dict[str, List[Dict]]:
|
|
"""加载 cookie 文件"""
|
|
import os
|
|
|
|
if os.path.exists(self.cookie_file):
|
|
try:
|
|
with open(self.cookie_file, 'r', encoding='utf-8') as f:
|
|
data = json.load(f)
|
|
# 兼容旧格式
|
|
if isinstance(data, dict) and any(isinstance(v, dict) for v in data.values()):
|
|
# 转换旧格式为新格式
|
|
new_data = {}
|
|
for user, websites in data.items():
|
|
all_cookies = []
|
|
for site_cookies in websites.values():
|
|
all_cookies.extend(site_cookies)
|
|
new_data[user] = all_cookies
|
|
return new_data
|
|
return data
|
|
except Exception as e:
|
|
logger.error(f"加载 cookie 文件失败: {e}")
|
|
|
|
return {}
|
|
|
|
def _save_cookies(self):
|
|
"""保存 cookie 文件"""
|
|
try:
|
|
with open(self.cookie_file, 'w', encoding='utf-8') as f:
|
|
json.dump(self.cookies, f, indent=2, ensure_ascii=False)
|
|
except Exception as e:
|
|
logger.error(f"保存 cookie 文件失败: {e}")
|
|
|
|
def get_all_cookies(self, user_name: str) -> List[Dict]:
|
|
"""
|
|
获取指定用户的所有 cookie
|
|
|
|
Args:
|
|
user_name: 用户名
|
|
|
|
Returns:
|
|
cookie 列表
|
|
"""
|
|
return self.cookies.get(user_name, [])
|
|
|
|
def save_all_cookies(self, user_name: str, cookies: List[Dict]):
|
|
"""
|
|
保存指定用户的所有 cookie
|
|
|
|
Args:
|
|
user_name: 用户名
|
|
cookies: cookie 列表
|
|
"""
|
|
self.cookies[user_name] = cookies
|
|
self._save_cookies()
|
|
|
|
def save_cookies(self, user_name: str, website_name: str, cookies: List[Dict]):
|
|
"""
|
|
保存指定用户和网站的 cookie(兼容旧接口)
|
|
|
|
Args:
|
|
user_name: 用户名
|
|
website_name: 网站名(未使用,保持兼容)
|
|
cookies: cookie 列表
|
|
"""
|
|
# 保持向后兼容,将网站的cookie添加到用户的所有cookie中
|
|
if user_name not in self.cookies:
|
|
self.cookies[user_name] = []
|
|
|
|
# 合并新cookie到现有cookie中
|
|
existing_cookies = self.cookies[user_name]
|
|
|
|
# 创建现有cookie的索引,用于快速查找
|
|
cookie_index = {}
|
|
for i, cookie in enumerate(existing_cookies):
|
|
if isinstance(cookie, dict):
|
|
key = (cookie.get('name'), cookie.get('domain'), cookie.get('path'))
|
|
cookie_index[key] = i
|
|
|
|
# 添加或更新cookie
|
|
if isinstance(cookies, list):
|
|
valid_cookies = []
|
|
for cookie in cookies:
|
|
if isinstance(cookie, dict):
|
|
valid_cookies.append(cookie)
|
|
|
|
for cookie in valid_cookies:
|
|
key = (cookie.get('name'), cookie.get('domain'), cookie.get('path'))
|
|
if key in cookie_index:
|
|
# 更新现有cookie
|
|
existing_cookies[cookie_index[key]] = cookie
|
|
else:
|
|
# 添加新cookie
|
|
existing_cookies.append(cookie)
|
|
|
|
if valid_cookies:
|
|
logger.info(f" 已保存 {len(valid_cookies)} 个新 cookie")
|
|
|
|
self._save_cookies()
|
|
|
|
def add_cookie(self, user_name: str, cookie: Dict):
|
|
"""
|
|
为指定用户添加单个 cookie
|
|
|
|
Args:
|
|
user_name: 用户名
|
|
cookie: cookie 字典
|
|
"""
|
|
if user_name not in self.cookies:
|
|
self.cookies[user_name] = []
|
|
|
|
# 检查是否已存在相同的 cookie
|
|
existing_index = -1
|
|
for i, existing_cookie in enumerate(self.cookies[user_name]):
|
|
if (existing_cookie.get('name') == cookie.get('name') and
|
|
existing_cookie.get('domain') == cookie.get('domain') and
|
|
existing_cookie.get('path') == cookie.get('path')):
|
|
existing_index = i
|
|
break
|
|
|
|
if existing_index >= 0:
|
|
# 更新现有 cookie
|
|
self.cookies[user_name][existing_index] = cookie
|
|
else:
|
|
# 添加新 cookie
|
|
self.cookies[user_name].append(cookie)
|
|
|
|
self._save_cookies()
|
|
|
|
def remove_cookie(self, user_name: str, cookie_name: str, domain: str):
|
|
"""
|
|
从指定用户中移除指定的 cookie
|
|
|
|
Args:
|
|
user_name: 用户名
|
|
cookie_name: cookie 名称
|
|
domain: cookie 域名
|
|
"""
|
|
if user_name in self.cookies:
|
|
self.cookies[user_name] = [
|
|
cookie for cookie in self.cookies[user_name]
|
|
if not (cookie.get('name') == cookie_name and cookie.get('domain') == domain)
|
|
]
|
|
self._save_cookies()
|
|
|
|
def clear_cookies(self, user_name: str):
|
|
"""
|
|
清空指定用户的所有 cookie
|
|
|
|
Args:
|
|
user_name: 用户名
|
|
"""
|
|
if user_name in self.cookies:
|
|
self.cookies[user_name] = []
|
|
self._save_cookies()
|
|
|
|
def get_cookies_for_domain(self, user_name: str, domain: str) -> List[Dict]:
|
|
"""
|
|
获取指定用户的指定域名的 cookie
|
|
|
|
Args:
|
|
user_name: 用户名
|
|
domain: 域名
|
|
|
|
Returns:
|
|
cookie 列表
|
|
"""
|
|
user_cookies = self.get_all_cookies(user_name)
|
|
filtered_cookies = []
|
|
|
|
for cookie in user_cookies:
|
|
cookie_domain = cookie.get('domain', '')
|
|
# 统一域名格式
|
|
if cookie_domain.startswith('.'):
|
|
cookie_domain = cookie_domain[1:]
|
|
|
|
if domain in cookie_domain or cookie_domain in domain:
|
|
filtered_cookies.append(cookie)
|
|
|
|
return filtered_cookies
|
|
|
|
|