Skip to main content
Glama
xiaohongshu_login.py9.13 kB
""" 小红书登录管理器 提供稳定、可靠的登录流程: - 浏览器初始化和资源清理 - 打开登录弹窗并获取二维码 - 阻塞等待登录完成(登录框消失且"我的"按钮出现) - 登录状态检查(基于DOM元素和Cookie) """ import asyncio from typing import Optional, Tuple from loguru import logger from playwright.async_api import TimeoutError as PlaywrightTimeoutError from ..browser.browser_manager import BrowserManager from ..browser.page_controller import PageController from ..storage.cookie_storage import CookieStorage from ..config.xhs_xpath import XHSXPath class XiaohongshuLogin: """小红书登录管理器""" # 使用配置文件中的选择器 XHS_URL = XHSXPath.XHS_URL QR_CSS = XHSXPath.QR_CSS QR_XPATH = XHSXPath.QR_XPATH LOGIN_BUTTON_XPATH = XHSXPath.LOGIN_BUTTON_XPATH USER_LINK_XPATH = XHSXPath.USER_LINK_XPATH MASK_CSS = XHSXPath.MASK_CSS LOGIN_MODAL_CSS = XHSXPath.LOGIN_MODAL_CSS LOGIN_MODAL_XPATH = XHSXPath.LOGIN_MODAL_XPATH LOGIN_COOKIES = XHSXPath.LOGIN_COOKIES def __init__(self, browser_manager: BrowserManager, cookie_storage: CookieStorage): self.browser_manager = browser_manager self.cookie_storage = cookie_storage self.page_controller: Optional[PageController] = None async def initialize(self) -> None: """启动浏览器并准备页面控制器""" if not self.browser_manager.is_started(): await self.browser_manager.start() page = await self.browser_manager.get_page() self.page_controller = PageController(page) # 不在此处主动加载 cookies,避免重复加载 logger.info("小红书登录管理器初始化完成") async def cleanup(self, save_cookies: bool = True) -> None: """ 关闭浏览器并清理资源 Args: save_cookies: 是否保存cookies,默认为True """ if self.browser_manager.is_started(): await self.browser_manager.stop(save_cookies=save_cookies) logger.info("小红书登录管理器资源清理完成(浏览器已关闭)") async def is_logged_in(self, navigate: bool = False) -> bool: """ 检查是否已登录 逻辑: 1. 正向检查:使用 USER_LINK_XPATH,如果存在说明已登录 2. 负向检查:使用 LOGIN_BUTTON_XPATH,如果存在说明未登录 Args: navigate: 是否导航到探索页 Returns: 是否已登录 """ if not self.page_controller: await self.initialize() try: if navigate: await self.page_controller.navigate(self.XHS_URL, wait_until="domcontentloaded") # 正向检查:如果"我"的链接存在,说明已登录 try: await self.page_controller.wait_for_element(self.USER_LINK_XPATH, timeout=2000, state="visible") logger.info("检测到用户链接元素,判断为已登录") return True except Exception: pass # 负向检查:如果登录按钮存在,说明未登录 try: if await self.page_controller.has_element(self.LOGIN_BUTTON_XPATH, timeout=2000): logger.debug("检测到登录按钮,判定为未登录") return False except Exception: pass except Exception as e: logger.debug(f"登录状态 DOM 检查失败: {e}") return False async def open_login_modal(self) -> bool: """导航到探索页,打开登录弹窗,如果已登录则返回 False""" if not self.page_controller: await self.initialize() await self.page_controller.navigate(self.XHS_URL, wait_until="domcontentloaded") if await self.is_logged_in(navigate=False): logger.info("已登录,跳过打开登录弹窗") return False # 点击"登录"按钮,触发弹窗 try: await self.page_controller.click_element(self.LOGIN_BUTTON_XPATH, timeout=8000) logger.info("已点击登录按钮,等待弹窗与二维码") except Exception as e: logger.warning(f"未找到或无法点击登录按钮: {e}") return True async def get_qrcode(self) -> Optional[str]: """确保弹窗打开并返回二维码图片 URL;如果已登录返回 None""" if not self.page_controller: await self.initialize() opened = await self.open_login_modal() if not opened and await self.is_logged_in(navigate=False): return None # 等待二维码元素出现 try: if await self.page_controller.has_element(self.QR_CSS, timeout=90000): src = await self.page_controller.get_attribute(self.QR_CSS, "src") else: src = await self.page_controller.get_attribute(self.QR_XPATH, "src") if src: logger.info("二维码已获取") return src except Exception as e: logger.error(f"二维码元素未找到或获取失败: {e}") return None return None async def wait_for_login(self, timeout: int = 90) -> Tuple[bool, str, bool]: """ 阻塞等待登录成功:直到"我的"按钮出现 Args: timeout: 超时时间(秒),默认90秒 Returns: (success, message, cookies_saved) 元组 """ if not self.page_controller: await self.initialize() page = await self.browser_manager.get_page() try: await page.wait_for_selector( self.USER_LINK_XPATH, state="visible", timeout=timeout * 1000 ) cookies_saved = await self.browser_manager.save_cookies() logger.info("✅ 登录成功,已保存 cookies") return True, "登录成功", cookies_saved except PlaywrightTimeoutError: logger.warning(f"等待登录超时({timeout}秒)") return False, f"超时({timeout}秒)", False async def login(self, headless: bool = False, timeout: int = 90, fresh: bool = True) -> Tuple[bool, str, bool]: """ 完整登录:打开弹窗→阻塞等待登录完成(登录框消失且"我的"按钮出现) 返回 (success, message, cookies_saved) 默认 fresh=True,强制清空 cookies,确保需要扫码而不是复用旧会话。 默认 timeout=90秒,阻塞等待直到登录框消失且"我的"按钮出现。 """ try: # 在启动前设置 headless self.browser_manager.headless = headless await self.initialize() # fresh 模式:清空 cookies(文件和上下文) if fresh: try: self.cookie_storage.clear_cookies() page = await self.browser_manager.get_page() await page.context.clear_cookies() logger.info("已清空 cookies,开始干净的登录流程") except Exception as ce: logger.warning(f"清空 cookies 失败: {ce}") # 导航后通过 DOM 检查当前是否已登录 if await self.is_logged_in(navigate=True): ok = await self.browser_manager.save_cookies() return True, "用户已登录", ok # 打开登录弹窗并阻塞等待登录完成 await self.open_login_modal() success, message, saved = await self.wait_for_login(timeout=timeout) return success, message, saved except Exception as e: logger.error(f"登录流程失败: {e}") return False, f"登录失败: {e}", False async def logout(self) -> bool: """清除本地与浏览器中的 Cookie""" try: ok = self.cookie_storage.clear_cookies() if self.browser_manager.is_started(): page = await self.browser_manager.get_page() await page.context.clear_cookies() logger.info("已清除 cookies") return ok except Exception as e: logger.error(f"登出失败: {e}") return False async def save_cookies(self) -> bool: """保存当前浏览器的 cookies""" try: if self.browser_manager.is_started(): ok = await self.browser_manager.save_cookies() logger.info("已保存 cookies") return ok else: logger.warning("浏览器未启动,无法保存 cookies") return False except Exception as e: logger.error(f"保存 cookies 失败: {e}") return False

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/luyike221/xiaohongshu-mcp-python'

If you have feedback or need assistance with the MCP directory API, please join our Discord server