diff --git a/README.md b/README.md index 6355674..33f0b2f 100644 --- a/README.md +++ b/README.md @@ -112,13 +112,29 @@ cd MediaCrawler uv sync ``` -### 🌐 浏览器驱动安装 +### 🌐 浏览器驱动安装(可选) + +> 如果使用默认的 CDP 模式(连接已有 Chrome 浏览器),**无需安装浏览器驱动**。仅在使用标准 Playwright 模式时需要安装。 ```shell -# 安装浏览器驱动 +# 仅在标准 Playwright 模式下需要安装浏览器驱动 uv run playwright install ``` +### 🌍 Chrome 浏览器配置(推荐) + +项目默认使用 CDP 模式连接用户已有的 Chrome 浏览器,可以复用浏览器已有的登录状态、Cookie、扩展等,**大幅降低平台风控检测风险**。 + +使用前需要: + +1. **安装最新版 Chrome 浏览器**(版本 >= 144),[下载地址](https://www.google.com/chrome/) +2. **开启远程调试功能**:在 Chrome 地址栏输入 `chrome://inspect/#remote-debugging`,勾选 **"Allow remote debugging for this browser instance"** +3. 页面显示 `Server running at: 127.0.0.1:9222` 表示已就绪 + +> 💡 **提示**:运行爬虫后,Chrome 浏览器会弹出确认对话框,点击"接受"即可。程序会等待用户确认,60秒内操作完成即可。 +> +> 如果不想使用 CDP 模式,可以在 `config/base_config.py` 中设置 `ENABLE_CDP_MODE = False` 切换为标准 Playwright 模式。 + ## 🚀 运行爬虫程序 ```shell diff --git a/config/base_config.py b/config/base_config.py index a87413e..49c35ce 100644 --- a/config/base_config.py +++ b/config/base_config.py @@ -48,31 +48,38 @@ HEADLESS = False # Whether to save login status SAVE_LOGIN_STATE = True -# ==================== CDP (Chrome DevTools Protocol) Configuration ==================== -# Whether to enable CDP mode - use the user's existing Chrome/Edge browser to crawl, providing better anti-detection capabilities -# Once enabled, the user's Chrome/Edge browser will be automatically detected and started, and controlled through the CDP protocol. -# This method uses the real browser environment, including the user's extensions, cookies and settings, greatly reducing the risk of detection. -ENABLE_CDP_MODE = False +# ==================== CDP (Chrome DevTools Protocol) 配置 ==================== +# 是否启用 CDP 模式 - 使用用户本地的 Chrome/Edge 浏览器进行爬取,具有更好的反检测能力 +# 开启后,会自动检测并启动用户的 Chrome/Edge 浏览器,通过 CDP 协议进行控制 +# 该方式使用真实浏览器环境,包括用户的扩展、Cookie 和设置,大幅降低被风控检测的风险 +ENABLE_CDP_MODE = True -# CDP debug port, used to communicate with the browser -# If the port is occupied, the system will automatically try the next available port +# CDP 调试端口,用于与浏览器通信 +# 如果端口被占用,系统会自动尝试下一个可用端口 CDP_DEBUG_PORT = 9222 -# Custom browser path (optional) -# If it is empty, the system will automatically detect the installation path of Chrome/Edge -# Windows example: "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" -# macOS example: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" +# 自定义浏览器路径(可选) +# 如果为空,系统会自动检测 Chrome/Edge 的安装路径 +# Windows 示例: "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" +# macOS 示例: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" CUSTOM_BROWSER_PATH = "" -# Whether to enable headless mode in CDP mode -# NOTE: Even if set to True, some anti-detection features may not work well in headless mode +# 是否在 CDP 模式下启用无头模式 +# 注意:即使设置为 True,某些反检测功能在无头模式下可能无法正常工作 CDP_HEADLESS = False -# Browser startup timeout (seconds) +# 浏览器启动超时时间(秒) BROWSER_LAUNCH_TIMEOUT = 60 -# Whether to automatically close the browser when the program ends -# Set to False to keep the browser running for easy debugging +# 是否连接用户已打开的浏览器,而不是启动新的浏览器 +# 开启后,程序会连接一个已经启用了远程调试的浏览器 +# 用户需要在 Chrome 中开启远程调试:chrome://inspect/#remote-debugging +# 或者使用命令行参数启动 Chrome:--remote-debugging-port=9222 +# 这种方式反检测效果最好,因为直接使用用户真实浏览器的所有 Cookie、扩展和浏览历史 +CDP_CONNECT_EXISTING = True + +# 程序结束时是否自动关闭浏览器 +# 设置为 False 可以保持浏览器运行,方便调试 AUTO_CLOSE_BROWSER = True # Data saving type option configuration, supports: csv, db, json, jsonl, sqlite, excel, postgres. It is best to save to DB, with deduplication function. diff --git a/docs/CDP模式使用指南.md b/docs/CDP模式使用指南.md index 541cbc3..4ef1b10 100644 --- a/docs/CDP模式使用指南.md +++ b/docs/CDP模式使用指南.md @@ -12,34 +12,63 @@ CDP(Chrome DevTools Protocol)模式是一种高级的反检测爬虫技术 4. **扩展支持**: 可以利用用户安装的广告拦截器、代理扩展等工具 5. **更自然的行为**: 浏览器行为模式更接近真实用户 +### 📌 两种 CDP 模式 + +CDP模式支持两种使用方式: + +| 模式 | 说明 | 适用场景 | +|------|------|----------| +| **连接已有浏览器**(默认推荐) | 连接用户正在使用的 Chrome 浏览器,复用真实的 Cookie、扩展和浏览历史 | 反检测要求高,需要最大程度降低风控风险 | +| **启动新浏览器** | 自动检测并启动一个新的 Chrome/Edge 浏览器实例 | 不需要复用浏览器状态的场景 | + ## 快速开始 -### 1. 启用CDP模式 +### 方式一:连接已有浏览器(默认推荐) -在 `config/base_config.py` 中设置: +这是**默认且推荐**的方式,直接连接你正在使用的 Chrome 浏览器,反检测效果最好。 + +#### 第一步:确保 Chrome 版本 + +需要 Chrome **144 或更高版本**(2026年1月起的稳定版均支持)。在地址栏输入 `chrome://version` 查看当前版本。 + +如果版本过低,请前往 [Chrome 官网](https://www.google.com/chrome/) 下载最新版。 + +#### 第二步:开启远程调试 + +1. 在 Chrome 地址栏输入:`chrome://inspect/#remote-debugging` +2. 勾选 **"Allow remote debugging for this browser instance"** +3. 页面会显示 `Server running at: 127.0.0.1:9222`,表示已就绪 + +#### 第三步:运行爬虫 + +```bash +uv run main.py --platform xhs --lt qrcode --type search +``` + +运行后,Chrome 浏览器会**弹出确认对话框**,点击"接受"即可。程序会等待用户确认(默认60秒超时)。 + +#### 配置说明 + +`config/base_config.py` 中的默认配置: ```python # 启用CDP模式 ENABLE_CDP_MODE = True -# CDP调试端口(可选,默认9222) +# 连接已有浏览器(默认开启) +CDP_CONNECT_EXISTING = True + +# CDP调试端口(与 chrome://inspect 页面显示的端口一致) CDP_DEBUG_PORT = 9222 - -# 是否在无头模式下运行(建议设为False以获得最佳反检测效果) -CDP_HEADLESS = False - -# 程序结束时是否自动关闭浏览器 -AUTO_CLOSE_BROWSER = True ``` -### 2. 运行测试 +### 方式二:启动新浏览器 -```bash -# 运行CDP功能测试 -python examples/cdp_example.py +如果不想连接已有浏览器,可以让程序自动启动一个新的浏览器实例: -# 运行小红书爬虫(CDP模式) -python main.py +```python +ENABLE_CDP_MODE = True +CDP_CONNECT_EXISTING = False # 关闭连接已有浏览器,改为启动新浏览器 ``` ## 配置选项详解 @@ -48,7 +77,8 @@ python main.py | 配置项 | 类型 | 默认值 | 说明 | |--------|------|--------|------| -| `ENABLE_CDP_MODE` | bool | False | 是否启用CDP模式 | +| `ENABLE_CDP_MODE` | bool | True | 是否启用CDP模式 | +| `CDP_CONNECT_EXISTING` | bool | True | 是否连接已有浏览器(推荐开启) | | `CDP_DEBUG_PORT` | int | 9222 | CDP调试端口 | | `CDP_HEADLESS` | bool | False | CDP模式下的无头模式 | | `AUTO_CLOSE_BROWSER` | bool | True | 程序结束时是否关闭浏览器 | @@ -57,8 +87,8 @@ python main.py | 配置项 | 类型 | 默认值 | 说明 | |--------|------|--------|------| -| `CUSTOM_BROWSER_PATH` | str | "" | 自定义浏览器路径 | -| `BROWSER_LAUNCH_TIMEOUT` | int | 30 | 浏览器启动超时时间(秒) | +| `CUSTOM_BROWSER_PATH` | str | "" | 自定义浏览器路径(仅启动新浏览器模式下有效) | +| `BROWSER_LAUNCH_TIMEOUT` | int | 60 | 浏览器连接超时时间(秒) | ### 自定义浏览器路径 @@ -219,15 +249,25 @@ ps aux | grep chrome ## 技术原理 -CDP模式的工作原理: +### 连接已有浏览器模式(推荐) + +1. **用户开启远程调试**: 在 `chrome://inspect/#remote-debugging` 中勾选启用 +2. **WebSocket连接**: 程序通过 `ws://localhost:9222/devtools/browser` 直接连接浏览器 +3. **用户确认**: Chrome 弹出确认对话框,用户点击接受后连接建立 +4. **Playwright集成**: 使用 `connectOverCDP` 方法接管浏览器控制 +5. **上下文复用**: 直接使用浏览器已有的上下文(包含用户的Cookie、登录状态等) + +> 💡 与传统CDP模式的区别:传统方式通过 `--remote-debugging-port` 启动新浏览器,使用 HTTP 接口 `/json/version` 获取 WebSocket URL。而连接已有浏览器方式直接通过 WebSocket 连接,Chrome 新版(136+)的远程调试不提供 HTTP 接口,需要用户在浏览器端确认授权。 + +### 启动新浏览器模式 1. **浏览器检测**: 自动扫描系统中的Chrome/Edge安装路径 2. **进程启动**: 使用`--remote-debugging-port`参数启动浏览器 -3. **CDP连接**: 通过WebSocket连接到浏览器的调试接口 +3. **CDP连接**: 通过 HTTP 获取 WebSocket URL,再连接到浏览器的调试接口 4. **Playwright集成**: 使用`connectOverCDP`方法接管浏览器控制 5. **上下文管理**: 创建或复用浏览器上下文进行操作 -这种方式绕过了传统WebDriver的检测机制,提供了更加隐蔽的自动化能力。 +两种方式都绕过了传统WebDriver的检测机制,提供了更加隐蔽的自动化能力。连接已有浏览器模式的反检测效果更好,因为使用的是用户真实的浏览器环境。 ## 更新日志 diff --git a/docs/常见问题.md b/docs/常见问题.md index e2d294e..cfc0319 100644 --- a/docs/常见问题.md +++ b/docs/常见问题.md @@ -10,7 +10,7 @@ A: windows电脑去网站下载`https://nodejs.org/en/blog/release/v16.8.0` Wind ## xhs登录出现滑块一直验证不通过问题 Q: 小红书扫码登录成功后,浏览器一直在验证滑块,无法登录?
-A: 这种情况一般是因为使用playwright浏览器驱动被识别出来的问题,可以尝试删除项目目录下的`brower_data`文件夹,重新走登录流程。
+A: 小红书平台风控非常严格,**强烈建议使用 CDP 模式连接自己的真实浏览器**(默认配置),不要使用无痕浏览器或标准 Playwright 模式。连接真实浏览器可以复用已有的 Cookie、登录状态和浏览历史,大幅降低被风控检测的概率。如果仍然出现滑块问题,可以尝试删除项目目录下的`brower_data`文件夹,重新走登录流程。
## 如何指定关键词 Q: 可以指定关键词爬取吗?
@@ -43,3 +43,21 @@ A: 打开 config/base_config.py 文件, 找到`ENABLE_GET_WORDCLOUD` 以及`ENAB ## 词云图添加禁用词和自定义词组 Q: 如何给词云图添加禁用词和自定义词组? A: 打开 `docs/hit_stopwords.txt` 输入禁用词(注意一个词语一行)。打开 config/base_config.py 文件找到 `CUSTOM_WORDS `按格式添加自定义词组即可。
+ +## CDP 连接已有浏览器相关问题 + +Q: 运行爬虫后提示无法连接到浏览器,报错 `Cannot connect to existing browser on port 9222`?
+A: 请检查以下几点:
+1. 确保 Chrome 浏览器已经打开并正在运行
+2. 在 Chrome 地址栏输入 `chrome://inspect/#remote-debugging`,确保已勾选 **"Allow remote debugging for this browser instance"**
+3. 页面上应显示 `Server running at: 127.0.0.1:9222`,如果没有显示说明远程调试未成功开启
+4. 确保 Chrome 版本 >= 144,低版本不支持此功能,在地址栏输入 `chrome://version` 查看版本号
+ +Q: 运行爬虫后浏览器弹出了确认对话框,需要怎么操作?
+A: 这是正常行为。Chrome 连接已有浏览器时会弹出确认对话框,点击"接受"即可。程序会等待用户确认,默认超时时间为60秒,在此期间点击确认即可。
+ +Q: 不想连接已有浏览器,想让程序自动启动一个新的浏览器怎么办?
+A: 在 `config/base_config.py` 中设置 `CDP_CONNECT_EXISTING = False`,程序会自动检测并启动一个新的 Chrome/Edge 浏览器实例。
+ +Q: 为什么推荐连接已有浏览器而不是启动新浏览器?
+A: 连接已有浏览器可以直接复用你浏览器中真实的 Cookie、登录状态、扩展插件和浏览历史,平台很难区分这是自动化操作还是真实用户行为,**大幅降低被平台风控检测的风险**。而启动新浏览器是一个"干净"的环境,更容易被平台识别为爬虫。
diff --git a/media_platform/xhs/core.py b/media_platform/xhs/core.py index d536521..ebd8f1b 100644 --- a/media_platform/xhs/core.py +++ b/media_platform/xhs/core.py @@ -356,7 +356,9 @@ class XiaoHongShuCrawler(AbstractCrawler): async def create_xhs_client(self, httpx_proxy: Optional[str]) -> XiaoHongShuClient: """Create Xiaohongshu client""" utils.logger.info("[XiaoHongShuCrawler.create_xhs_client] Begin create Xiaohongshu API client ...") - cookie_str, cookie_dict = utils.convert_cookies(await self.browser_context.cookies()) + cookie_str, cookie_dict = utils.convert_cookies( + await self.browser_context.cookies(self.index_url) + ) xhs_client_obj = XiaoHongShuClient( proxy=httpx_proxy, headers={ diff --git a/tools/cdp_browser.py b/tools/cdp_browser.py index 0e21156..b674f51 100644 --- a/tools/cdp_browser.py +++ b/tools/cdp_browser.py @@ -105,6 +105,10 @@ class CDPBrowserManager: Launch browser and connect via CDP """ try: + if config.CDP_CONNECT_EXISTING: + # Connect to an existing browser that already has remote debugging enabled + return await self._connect_existing_browser(playwright, playwright_proxy, user_agent) + # 1. Detect browser path browser_path = await self._get_browser_path() @@ -133,6 +137,63 @@ class CDPBrowserManager: await self.cleanup() raise + async def _connect_existing_browser( + self, + playwright: Playwright, + playwright_proxy: Optional[Dict] = None, + user_agent: Optional[str] = None, + ) -> BrowserContext: + """ + Connect to an existing browser that already has remote debugging enabled. + User needs to enable remote debugging via chrome://inspect/#remote-debugging + or launch Chrome with --remote-debugging-port flag. + """ + self.debug_port = config.CDP_DEBUG_PORT + utils.logger.info( + f"[CDPBrowserManager] Connecting to existing browser on port {self.debug_port}..." + ) + utils.logger.info( + "[CDPBrowserManager] Make sure remote debugging is enabled in your browser: " + "chrome://inspect/#remote-debugging" + ) + + # Wait for the browser's CDP port to become available + # The user may need time to enable remote debugging or confirm the connection dialog + timeout = config.BROWSER_LAUNCH_TIMEOUT + utils.logger.info( + f"[CDPBrowserManager] Waiting up to {timeout}s for browser CDP connection..." + ) + connected = False + for i in range(timeout): + if await self._test_cdp_connection(self.debug_port): + connected = True + break + if i % 5 == 0 and i > 0: + utils.logger.info( + f"[CDPBrowserManager] Still waiting for browser... ({i}s elapsed) " + "Please enable remote debugging: chrome://inspect/#remote-debugging" + ) + await asyncio.sleep(1) + + if not connected: + raise RuntimeError( + f"Cannot connect to existing browser on port {self.debug_port} " + f"after waiting {timeout}s. Please ensure:\n" + " 1. Your browser is running\n" + " 2. Remote debugging is enabled (chrome://inspect/#remote-debugging)\n" + f" 3. The debug port is {self.debug_port} (configure via CDP_DEBUG_PORT)" + ) + + # Connect via CDP (reuse existing method) + await self._connect_via_cdp(playwright) + + # Create browser context (reuse existing method, will prefer existing context) + browser_context = await self._create_browser_context(playwright_proxy, user_agent) + self.browser_context = browser_context + + utils.logger.info("[CDPBrowserManager] Successfully connected to existing browser") + return browser_context + async def _get_browser_path(self) -> str: """ Get browser path @@ -254,12 +315,23 @@ class CDPBrowserManager: Connect to browser via CDP """ try: - # Get correct WebSocket URL - ws_url = await self._get_browser_websocket_url(self.debug_port) - utils.logger.info(f"[CDPBrowserManager] Connecting to browser via CDP: {ws_url}") - - # Use Playwright's connectOverCDP method to connect - self.browser = await playwright.chromium.connect_over_cdp(ws_url) + if config.CDP_CONNECT_EXISTING: + # For existing browser (e.g. chrome://inspect/#remote-debugging), + # Chrome exposes a WebSocket at /devtools/browser and may show a confirmation + # dialog to the user. Use ws:// with a longer timeout to wait for user confirmation. + ws_url = f"ws://localhost:{self.debug_port}/devtools/browser" + utils.logger.info(f"[CDPBrowserManager] Connecting to existing browser via CDP: {ws_url}") + utils.logger.info( + "[CDPBrowserManager] Please check your browser for a confirmation dialog and accept it" + ) + self.browser = await playwright.chromium.connect_over_cdp( + ws_url, timeout=config.BROWSER_LAUNCH_TIMEOUT * 1000 + ) + else: + # For launched browser, get WebSocket URL first + ws_url = await self._get_browser_websocket_url(self.debug_port) + utils.logger.info(f"[CDPBrowserManager] Connecting to browser via CDP: {ws_url}") + self.browser = await playwright.chromium.connect_over_cdp(ws_url) if self.browser.is_connected(): utils.logger.info("[CDPBrowserManager] Successfully connected to browser") @@ -403,10 +475,14 @@ class CDPBrowserManager: finally: self.browser = None - # Close browser process - # force=True means force close, ignoring AUTO_CLOSE_BROWSER config - # Used for handling abnormal exit or manual cleanup - if force or config.AUTO_CLOSE_BROWSER: + # Close browser process (skip if connected to existing browser - we didn't launch it) + if config.CDP_CONNECT_EXISTING: + utils.logger.info( + "[CDPBrowserManager] Connected to existing browser, skipping process cleanup" + ) + elif force or config.AUTO_CLOSE_BROWSER: + # force=True means force close, ignoring AUTO_CLOSE_BROWSER config + # Used for handling abnormal exit or manual cleanup if self.launcher and self.launcher.browser_process: self.launcher.cleanup() else: diff --git a/uv.lock b/uv.lock index 479f77f..1f5a8a4 100644 --- a/uv.lock +++ b/uv.lock @@ -1,4 +1,5 @@ version = 1 +revision = 1 requires-python = ">=3.11" resolution-markers = [ "python_full_version >= '3.12' and sys_platform == 'darwin'", @@ -863,6 +864,7 @@ dependencies = [ { name = "uvicorn" }, { name = "websockets" }, { name = "wordcloud" }, + { name = "xhshow" }, ] [package.metadata] @@ -900,6 +902,7 @@ requires-dist = [ { name = "uvicorn", specifier = "==0.29.0" }, { name = "websockets", specifier = ">=15.0.1" }, { name = "wordcloud", specifier = "==1.9.3" }, + { name = "xhshow", specifier = ">=0.1.9" }, ] [[package]] @@ -1159,6 +1162,36 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, ] +[[package]] +name = "pycryptodome" +version = "3.23.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276 } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675 }, +] + [[package]] name = "pydantic" version = "2.5.2" @@ -1715,3 +1748,15 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/77/c0/bc14fd7fa96e5b544aac4e9e65b5dd6f753d72184da35e35eb0b24c4dde4/wordcloud-1.9.3-cp312-cp312-win32.whl", hash = "sha256:419acfe0b1d1227b9e3e14ec1bb6c40fd7fa652df4adf81f0ba3e00daca500b5", size = 291251 }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bf/a0/b8fa5f2d7147a7675e2cab99108f7d8d524b67481f81f289cdb2b64ed1ab/wordcloud-1.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:2061a9978a6243107ce1a8a9fa24f421b03a0f7e620769b6f5075857e75aa615", size = 301393 }, ] + +[[package]] +name = "xhshow" +version = "0.1.9" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "pycryptodome" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e5/9a/ed544bea5b6a3702fc232eb774817d7f03a678b5f2ac3d67d3c7698695e6/xhshow-0.1.9.tar.gz", hash = "sha256:5a17cb510e9ab3ca61ef8ce00bd4cae6cbce39bb7b5f8ef38a5a980011883245", size = 55126 } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7f/c0/2783314af317b7207422e94f74302dbe32c5c9a1641bcfe11c4c073b0b04/xhshow-0.1.9-py3-none-any.whl", hash = "sha256:c4b0d38f4746b8a3f9c08fcfe4628f81c887b27c76b7ba73fa61ff990d1839dc", size = 31289 }, +]