├── .python-version ├── requirements.txt ├── img ├── DrissionPageMCP-logo.png ├── install_to_Cursor1.png ├── install_to_cursor2.png ├── install_to_vscode0.png ├── install_to_vscode1.png └── install_to_vscode2.png ├── .gitignore ├── pyproject.toml ├── 提示词.txt ├── MCP安装教程.md ├── ToolBox.py ├── README.md ├── domTreeToJSON.js ├── CodeBox.py ├── DrissionPage使用教程.md ├── DrissionPage_code_guide.md ├── main-2.py ├── main-1.py ├── main.py └── uv.lock /.python-version: -------------------------------------------------------------------------------- 1 | 3.13 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | drissionpage>=4.1.0.18 2 | fastmcp>=2.4.0 3 | uv>=0.1.0 -------------------------------------------------------------------------------- /img/DrissionPageMCP-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxhzhwxhzh/DrissionPageMCP/HEAD/img/DrissionPageMCP-logo.png -------------------------------------------------------------------------------- /img/install_to_Cursor1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxhzhwxhzh/DrissionPageMCP/HEAD/img/install_to_Cursor1.png -------------------------------------------------------------------------------- /img/install_to_cursor2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxhzhwxhzh/DrissionPageMCP/HEAD/img/install_to_cursor2.png -------------------------------------------------------------------------------- /img/install_to_vscode0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxhzhwxhzh/DrissionPageMCP/HEAD/img/install_to_vscode0.png -------------------------------------------------------------------------------- /img/install_to_vscode1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxhzhwxhzh/DrissionPageMCP/HEAD/img/install_to_vscode1.png -------------------------------------------------------------------------------- /img/install_to_vscode2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxhzhwxhzh/DrissionPageMCP/HEAD/img/install_to_vscode2.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python-generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/ 6 | wheels/ 7 | *.egg-info 8 | test_*.py 9 | test.py 10 | 11 | # Virtual environments 12 | .venv 13 | .vscode 14 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "drssionpagemcp" 3 | version = "0.1.0" 4 | description = "A Python package for the Drissionpage MCP" 5 | readme = "README.md" 6 | requires-python = ">=3.13" 7 | dependencies = [ 8 | "drissionpage>=4.1.0.18", 9 | "fastmcp>=2.4.0", 10 | ] 11 | 12 | 13 | [[tool.uv.index]] 14 | url = "https://pypi.tuna.tsinghua.edu.cn/simple" -------------------------------------------------------------------------------- /提示词.txt: -------------------------------------------------------------------------------- 1 | 2 | ##################################### 3 | 1.用DrissionPageMCP访问 https://gitee.com/login?redirect_to_url=%2F 这个网址,点击 点此注册 的那个链接,然后填写网页,参考数据,另外 网页中的复选框都打上勾 4 | sanguo_user = { 5 | "用户名": "诸葛亮", 6 | “个人空间地址”:“aaabbb” 7 | "密码": "LonelyWolf36计", 8 | "手机号": "13333554444" 9 | } 10 | 11 | 12 | 2.把你整个流程中所有用到的元素列个表格,表头有 元素名,xpath,操作动作,动作内容,定位失败的元素不要 13 | 14 | 3.把你整个操作流程,整理成一个python 脚本文件,用到了DrissionPage这个库,我可以运行这个py文件重复整个流程,运行失败的流程不要写进去 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ###############################################################提示2 24 | 25 | 访问酷我这个网址 https://www.kuwo.cn/search/list 26 | 开始监听网页接受的音乐数据包 27 | 搜索 最后一次的温柔 28 | 然后点击 播放全部 按钮 29 | 把监听到的歌曲下载到当前工作空间目录 30 | 31 | 32 | 33 | #################################################### 34 | 35 | 1.访问 百度图片 网站 , 36 | 2.搜索 迪丽热巴, 然后对网页进行截图,把截图保存到当前工作目录,名字叫 迪丽热巴截图 37 | 38 | 3. 然后 访问 https://picui.cn/upload,点击网页中的上传元素,刚才保存的截图上传上去 39 | 4.最后 点击 上传这张图片 40 | 41 | 42 | ####################################### 43 | 44 | 参考当前目录下 DrissionPage使用教程.md 这个教程,写一个python脚本到当前目录,可以让我重复你刚才的操作流程 -------------------------------------------------------------------------------- /MCP安装教程.md: -------------------------------------------------------------------------------- 1 | # DrissionPageMCP 简单操作指南 2 | 快速完成项目搭建、测试与配置,共6步。 3 | 4 | 5 | ## 1. 装 uv(包管理工具) 6 | 打开命令行,输下面的命令: 7 | ```bash 8 | pip install uv 9 | ``` 10 | 11 | 12 | ## 2. 克隆项目仓库 13 | 继续输命令,拉取项目文件: 14 | ```bash 15 | git clone https://github.com/wxhzhwxhzh/DrissionPageMCP 16 | ``` 17 | 18 | 19 | ## 3. 进入项目文件夹 20 | 输命令切换到项目目录(后续操作都在这): 21 | ```bash 22 | cd DrissionPageMCP 23 | ``` 24 | 25 | 26 | ## 4. 装项目需要的库 27 | 输命令自动安装依赖: 28 | ```bash 29 | uv sync 30 | ``` 31 | 32 | 33 | ## 5. 测试程序能不能跑 34 | 输命令启动主程序,出现 *DrissionPage MCP server is running...* 说明程序正常: 35 | ```bash 36 | uv run main.py 37 | ``` 38 | 39 | 40 | ## 6. 写 MCP 配置文件 41 | 1. MCP对应的JSON配置文件的"mcpServers"项新增子项"DrssionPageMCP"; 42 | 2. 把 `D:\test4\DrissionPageMCP` 改成你电脑上 `main.py` 所在的绝对路径(比如你项目放 `E:\DrissionPageMCP`,就改这个) 43 | 3. Windows中路径用双反斜杠 '\\\\',Linux中用单反斜杠 '/'; 44 | 45 | ```json 46 | { 47 | "mcpServers": { 48 | "DrissionPageMCP": { 49 | "type": "stdio", 50 | "command": "uv", 51 | "args": ["--directory", "D:\\test4\\DrissionPageMCP", "run", "main.py"] 52 | } 53 | } 54 | } 55 | ``` 56 | 57 | ## 如何找到MCP配置文件 58 | [查看帮助](./README.md) 59 | -------------------------------------------------------------------------------- /ToolBox.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | #!/usr/bin/env python 3 | 4 | import sqlite3 5 | import json 6 | import os 7 | 8 | def save_dict_to_sqlite(data, db_path='data.db', table_name='my_table'): 9 | """ 10 | 将字典或JSON字符串保存到SQLite数据库中。 11 | 12 | 参数: 13 | data (dict or list of dict or str): 字典、列表字典,或JSON字符串。 14 | db_path (str): SQLite 数据库文件路径。 15 | table_name (str): 要创建的表名。 16 | """ 17 | # 如果是 JSON 字符串,则解析为 Python 对象 18 | if isinstance(data, str): 19 | data = json.loads(data) 20 | 21 | # 如果是单个字典,则转成列表 22 | if isinstance(data, dict): 23 | data = [data] 24 | 25 | if not isinstance(data, list) or not all(isinstance(item, dict) for item in data): 26 | raise ValueError("输入必须是字典、字典列表,或 JSON 字符串。") 27 | 28 | # 提取字段名(以第一个字典为准) 29 | columns = data[0].keys() 30 | 31 | # 构建字段定义 32 | col_defs = ', '.join([f'"{col}" TEXT' for col in columns]) 33 | 34 | # 连接数据库 35 | conn = sqlite3.connect(db_path) 36 | cursor = conn.cursor() 37 | 38 | # 创建表(如果不存在) 39 | cursor.execute(f'DROP TABLE IF EXISTS "{table_name}"') 40 | cursor.execute(f'CREATE TABLE "{table_name}" ({col_defs})') 41 | 42 | # 插入数据 43 | placeholders = ', '.join(['?' for _ in columns]) 44 | insert_query = f'INSERT INTO "{table_name}" ({", ".join(columns)}) VALUES ({placeholders})' 45 | for row in data: 46 | values = tuple(str(row.get(col, '')) for col in columns) 47 | cursor.execute(insert_query, values) 48 | 49 | # 提交并关闭连接 50 | conn.commit() 51 | conn.close() 52 | 53 | return (f"数据已保存到 {db_path} 的表 {table_name} 中。") 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DrissionPage MCP Server -- 骚神出品 2 | 3 | 基于DrissionPage和FastMCP的浏览器自动化MCP服务器,提供丰富的浏览器操作API供AI调用。 4 | 5 | ## 项目简介 6 | ![logo](img/DrissionPageMCP-logo.png) 7 | 8 | DrissionPage MCP 是一个基于 DrissionPage 和 FastMCP 的浏览器自动化MCP server服务器,它提供了一系列强大的浏览器操作 API,让您能够轻松通过AI实现网页自动化操作。 9 | 10 | ### 主要特性 11 | 12 | - 支持浏览器的打开、关闭和连接管理 13 | - 提供丰富的页面元素操作方法 14 | - 支持 JavaScript 代码执行 15 | - 支持 CDP 协议操作 16 | - 提供便捷的文件下载功能 17 | - 支持键盘按键模拟 18 | - 支持页面截图功能 19 | - 增加 网页后台监听数据包的功能 20 | - 增加自动上传下载文件功能 21 | 22 | #### Python要求 23 | - Python >= 3.9 24 | - pip(最新版本) 25 | - uv (最新版本) 26 | 27 | 28 | #### 浏览器要求 29 | - Chrome 浏览器(推荐 90 及以上版本) 30 | 31 | 32 | #### 必需的Python包 33 | - drissionpage >= 4.1.0.18 34 | - fastmcp >= 2.4.0 35 | - uv 36 | 37 | ## 安装说明 38 | - 把本仓库git clone到本地,核心文件是main.py: 39 | - 首先要进行[💖MCP安装环境准备工作](./MCP安装教程.md) 40 | 41 | ### 安装到Cursor编辑器 42 | 43 | ![安装说明](img/install_to_Cursor1.png) 44 | ![安装说明](img/install_to_cursor2.png) 45 | 46 | ### 安装到vscode编辑器 47 | 48 | ![安装说明](img/install_to_vscode0.png) 49 | ![安装说明](img/install_to_vscode1.png) 50 | ![安装说明](img/install_to_vscode2.png) 51 | 52 | 53 | 请将以下配置代码粘贴到编辑器的`mcpServers`设置中(请填写`你自己电脑上 main.py 文件的绝对路径`): 54 | 55 | ```json 56 | { 57 | "mcpServers": { 58 | "DrissionPageMCP": { 59 | "type": "stdio", 60 | "command": "uv", 61 | "args": ["--directory", "D:\\test10\\DrissionPageMCP", "run", "main.py"] 62 | } 63 | } 64 | } 65 | ``` 66 | 新增mcp配置 ,填写下面的配置: 67 | ``` json 68 | "DrissionPageMCP": { 69 | "type": "stdio", 70 | "command": "uv", 71 | "args": ["--directory", "D:\\test10\\DrissionPageMCP", "run", "main.py"] 72 | } 73 | ``` 74 | 75 | 注意事项: 76 | - 请根据实际路径修改`args`中的路径 77 | - Windows中路径中的反斜杠需要转义(使用`\\`) 78 | - 确保`uv`命令在系统PATH中可用 79 | - [《MCP安装参考教程》](https://docs.trae.ai/ide/model-context-protocol) 80 | 81 | 82 | ## 调试命令 83 | 84 | 调试 85 | ``` 86 | npx -y @modelcontextprotocol/inspector uv run D:\\test10\\DrissionPageMCP\\main.py 87 | ``` 88 | 或者 89 | ``` 90 | mcp dev D:\\test10\\DrissionPageMCP\\main.py 91 | ``` 92 | 93 | ## 更新日志 94 | ### v0.1.3 95 | 增加 自动上传下载文件功能 96 | ### v0.1.2 97 | 增加 网页后台监听数据包的功能 98 | 99 | ### v0.1.0 100 | 101 | - 初始版本发布 102 | - 实现基本的浏览器控制功能 103 | - 提供元素操作 API 104 | -------------------------------------------------------------------------------- /domTreeToJSON.js: -------------------------------------------------------------------------------- 1 | function isVisuallyHidden(node) { 2 | if (!(node instanceof Element)) return true; 3 | 4 | const invisibleTags = ['script', 'style', 'meta', 'link', 'template', 'noscript']; 5 | const tagName = node.nodeName.toLowerCase(); 6 | 7 | if (invisibleTags.includes(tagName)) return true; 8 | 9 | const style = getComputedStyle(node); 10 | const hiddenByStyle = ( 11 | style.display === 'none' || 12 | style.visibility === 'hidden' || 13 | style.opacity === '0' 14 | ); 15 | 16 | const hasNoSize = node.offsetWidth === 0 && node.offsetHeight === 0; 17 | 18 | return hiddenByStyle || hasNoSize; 19 | } 20 | 21 | function domTreeToJson(node = document.body, tagCounters = {}) { 22 | const getNodeLabel = (node) => { 23 | let name = node.nodeName.toLowerCase(); 24 | if (node.id) name += `#${node.id}`; 25 | if (node.className && typeof node.className === 'string') { 26 | const classList = node.className.trim().split(/\s+/).join('.'); 27 | if (classList) name += `.${classList}`; 28 | } 29 | const text = node.textContent?.trim().replace(/\s+/g, ' ') || ''; 30 | const content = text ? ` content='${text.slice(0, 5)}${text.length > 5 ? "…" : ""}'` : ''; 31 | return `${name}/` + content; 32 | }; 33 | 34 | if (isVisuallyHidden(node)) { 35 | return {}; // 过滤不可见节点 36 | } 37 | 38 | const tagName = node.nodeName.toLowerCase(); 39 | tagCounters[tagName] = (tagCounters[tagName] || 0); 40 | const nodeKey = `${tagName}${tagCounters[tagName]++}`; 41 | 42 | const children = Array.from(node.children) 43 | .map(child => domTreeToJson(child, tagCounters)) 44 | .filter(childJson => Object.keys(childJson).length > 0); // 去掉空节点 45 | 46 | if (children.length === 0) { 47 | return { [nodeKey]: getNodeLabel(node) }; 48 | } else { 49 | const childJson = {}; 50 | children.forEach(child => Object.assign(childJson, child)); 51 | return { [nodeKey]: childJson }; 52 | } 53 | } 54 | 55 | function buildDomJsonTree(root = document.body) { 56 | const topTag = root.nodeName.toLowerCase(); 57 | const result = {}; 58 | result[topTag] = domTreeToJson(root); 59 | return result; 60 | } 61 | 62 | // 用法示例 63 | const domJson = buildDomJsonTree(); 64 | 65 | // console.log(domJson) 66 | //console.log(JSON.stringify(domJson, null, 2)); 67 | return JSON.stringify(domJson, null, 2) 68 | -------------------------------------------------------------------------------- /CodeBox.py: -------------------------------------------------------------------------------- 1 | domTreeToJson = ''' 2 | 3 | function isVisuallyHidden(node) { 4 | if (!(node instanceof Element)) return true; 5 | 6 | const invisibleTags = ['script', 'style', 'meta', 'link', 'template', 'noscript']; 7 | const tagName = node.nodeName.toLowerCase(); 8 | 9 | if (invisibleTags.includes(tagName)) return true; 10 | 11 | const style = getComputedStyle(node); 12 | const hiddenByStyle = ( 13 | style.display === 'none' || 14 | style.visibility === 'hidden' || 15 | style.opacity === '0' 16 | ); 17 | 18 | const hasNoSize = node.offsetWidth === 0 && node.offsetHeight === 0; 19 | 20 | return hiddenByStyle || hasNoSize; 21 | } 22 | 23 | function domTreeToJson(node = document.body, tagCounters = {}) { 24 | const getNodeLabel = (node) => { 25 | let name = node.nodeName.toLowerCase(); 26 | if (node.id) name += `#${node.id}`; 27 | if (node.className && typeof node.className === 'string') { 28 | const classList = node.className.trim().split(/\s+/).join('.'); 29 | if (classList) name += `.${classList}`; 30 | } 31 | const text = node.textContent?.trim().replace(/\s+/g, ' ') || ''; 32 | const content = text ? ` content='${text.slice(0, 5)}${text.length > 5 ? "…" : ""}'` : ''; 33 | return `${name}/` + content; 34 | }; 35 | 36 | if (isVisuallyHidden(node)) { 37 | return {}; // 过滤不可见节点 38 | } 39 | 40 | const tagName = node.nodeName.toLowerCase(); 41 | tagCounters[tagName] = (tagCounters[tagName] || 0); 42 | const nodeKey = `${tagName}${tagCounters[tagName]++}`; 43 | 44 | const children = Array.from(node.children) 45 | .map(child => domTreeToJson(child, tagCounters)) 46 | .filter(childJson => Object.keys(childJson).length > 0); // 去掉空节点 47 | 48 | if (children.length === 0) { 49 | return { [nodeKey]: getNodeLabel(node) }; 50 | } else { 51 | const childJson = {}; 52 | children.forEach(child => Object.assign(childJson, child)); 53 | return { [nodeKey]: childJson }; 54 | } 55 | } 56 | 57 | function buildDomJsonTree(root = document.body) { 58 | const topTag = root.nodeName.toLowerCase(); 59 | const result = {}; 60 | result[topTag] = domTreeToJson(root); 61 | return result; 62 | } 63 | 64 | // 用法示例 65 | const domJson = buildDomJsonTree(); 66 | 67 | // console.log(domJson) 68 | //console.log(JSON.stringify(domJson, null, 2)); 69 | return JSON.stringify(domJson, null, 2) 70 | 71 | ''' -------------------------------------------------------------------------------- /DrissionPage使用教程.md: -------------------------------------------------------------------------------- 1 | 2 | # DrissionPage 使用教程 3 | 4 | > 📌 DrissionPage® 是一个基于 Python 的网页自动化工具,能控制浏览器,功能强大,语法简洁优雅,代码量少,对新手友好.支持:Chromium 内核浏览器(如 Chrome 和 Edge) 5 | 6 | --- 7 | 8 | ## ✨ 安装 9 | 10 | ```bash 11 | pip install -U DrissionPage 12 | ``` 13 | 14 | 如需使用浏览器功能,请安装对应的浏览器驱动(如 ChromeDriver)并确保版本匹配。 15 | 16 | --- 17 | 18 | 19 | 20 | ## 🛠 启动或者连接浏览器(带配置),然后打开一个网页 21 | 默认状态下,程序会自动在系统内查找 Chrome 路径 22 | 23 | ```python 24 | 25 | #!/usr/bin/env python 26 | # -*- coding:utf-8 -*- 27 | #-导入库 28 | from DrissionPage import Chromium, ChromiumOptions 29 | # 创建配置对象 30 | co = ChromiumOptions() 31 | co.headless(True) 32 | co.set_argument('--start-maximized') 33 | # 设置debug port 34 | co.set_local_port('9222') 35 | # 设置浏览器路径,不设置则默认是谷歌浏览器 36 | co.set_browser_path(r'C:\chrome.exe') 37 | 38 | # 创建浏览器对象 39 | browser = Chromium(co) 40 | tab = browser.latest_tab 41 | 42 | #访问网页.. 43 | tab.get("https://www.baidu.com/") 44 | print(tab.title) 45 | 46 | ``` 47 | 48 | --- 49 | 50 | ## 📄 自动登录gitee网站 51 | 52 | ```python 53 | # 使用默认模式访问网页 54 | from DrissionPage import Chromium 55 | 56 | # 启动或接管浏览器,并获取标签页对象 57 | tab = Chromium().latest_tab 58 | # 跳转到登录页面 59 | tab.get('https://gitee.com/login') 60 | 61 | # 定位到账号文本框,获取文本框元素 62 | ele = tab.ele('#user_login') 63 | # 输入对文本框输入账号 64 | ele.input('您的账号') 65 | # 定位到密码文本框并输入密码 66 | tab.ele('#user_password').input('您的密码') 67 | # 点击登录按钮 68 | tab.ele('@value=登 录').click() 69 | ``` 70 | 71 | --- 72 | 73 | ## 🔍 查找元素 74 | 75 | 支持 CSS 选择器、XPath 和自定义选择器: 76 | 77 | ```python 78 | # 查找tag为div的元素 79 | ele = tab.ele('tag:div') # 原写法 80 | ele = tab('t:div') # 简化写法 81 | 82 | # 用xpath查找元素 83 | ele = tab.ele('xpath://****') # 原写法 84 | ele = tab('x://****') # 简化写法 85 | 86 | # 查找text为'something'的元素 87 | ele = tab.ele('text=something') # 原写法 88 | ele = tab('tx=something') # 简化写法 89 | ``` 90 | 91 | --- 92 | 93 | ## ✏️ 获取电商网站的评论 94 | 95 | ```python 96 | 97 | #!/usr/bin/env python 98 | # -*- coding:utf-8 -*-# 99 | 100 | # 电脑内需要提取安装谷歌浏览器或者其他chromium内核的浏览器 比如 edge浏览器 qq浏览器 360浏览器 101 | 102 | 103 | import time 104 | from DrissionPage import Chromium 105 | from loguru import logger 106 | 107 | # 设置日志记录到文件 108 | logger.add("JD_comment.log", format="{time} {message}") 109 | 110 | # 初始化浏览器 111 | browser = Chromium() 112 | 113 | # 打开京东首页 114 | main_tab = browser.new_tab('https://www.jd.com/') 115 | 116 | # 获取搜索框并输入关键词 117 | search_input = main_tab.ele('tag:input@@id=key') 118 | search_input.input('小米手机') 119 | 120 | # 点击搜索按钮 121 | main_tab('tag:button@@aria-label=搜索').click() 122 | 123 | # 获取搜索结果列表 124 | search_results = main_tab.eles('t:li@@class=gl-item') 125 | 126 | # 打印每个搜索结果的文本 127 | # for result in search_results: 128 | # logger.info(result) 129 | 130 | # 点击搜索结果中的第二个商品以打开商品详情页 131 | product_detail_tab = search_results[1].ele('t:a').click.for_new_tab() 132 | # 点击评论标签页 133 | product_detail_tab.ele('@data-anchor=#comment').click() 134 | 135 | # 获取并打印商品评论 136 | def get_comments(tab): 137 | for comment in tab.eles('t:div@@class=comment-item'): 138 | # logger.info(comment) 139 | 140 | logger.info(comment('.comment-con').text) # 记录评论内容 141 | if recomment:=comment.ele('.recomment',timeout=2): 142 | logger.error(recomment.text) 143 | 144 | time.sleep(2) 145 | 146 | # 获取第一页评论并点击下一页 147 | get_comments(product_detail_tab) 148 | product_detail_tab.ele('t:a@@rel=2').click() 149 | 150 | # 循环获取剩余页码的评论 151 | for _ in range(4): 152 | get_comments(product_detail_tab) 153 | product_detail_tab.ele('下一页').click() 154 | ``` 155 | 156 | --- 157 | 158 | ## 📦 截图 159 | 160 | ```python 161 | # 对整页截图并保存 162 | tab.get_screenshot(path='tmp', name='pic.jpg', full_page=True) 163 | ``` 164 | 165 | --- 166 | 167 | ## 📂 文件下载 168 | 169 | ```python 170 | from DrissionPage import Chromium 171 | 172 | tab = Chromium().latest_tab 173 | tab.set.download_path('save_path') # 设置文件保存路径 174 | tab.set.download_file_name('file_name') # 设置重命名文件名 175 | tab('t:a').click() # 点击一个会触发下载的链接 176 | tab.wait.download_begin() # 等待下载开始 177 | tab.wait.downloads_done() # 等待下载结束 178 | ``` 179 | 180 | --- 181 | 182 | ## 📤 文件上传 183 | 184 | ```python 185 | # 设置要上传的文件路径 186 | tab.set.upload_files('demo.txt') 187 | # 点击触发文件选择框按钮 188 | btn_ele.click() 189 | # 等待路径填入 190 | tab.wait.upload_paths_inputted() 191 | ``` 192 | --- 193 | 194 | ## 📤 浏览器等待 195 | 196 | ```python 197 | tab.wait(10) # 等待10秒 198 | ``` 199 | 200 | --- 201 | ## 📤 关闭浏览器 202 | 203 | ```python 204 | browser.quit() 205 | ``` 206 | ## 📤 关闭标签页 207 | ```python 208 | tab.close() 209 | ``` 210 | 211 | --- 212 | ## 📤 访问指定小红书页面,监听并下载评论json数据 213 | ```python 214 | #!/usr/bin/env python 215 | # -*- coding: utf-8 -*- 216 | 217 | """ 218 | 小红书评论数据下载脚本 219 | 220 | 功能:访问指定小红书页面,监听并下载评论json数据 221 | """ 222 | 223 | import json 224 | import time 225 | from DrissionPage import Chromium, ChromiumOptions 226 | from loguru import logger 227 | 228 | # 配置日志 229 | logger.add("xiaohongshu_comment.log", format="{time} {level} {message}") 230 | 231 | def download_xiaohongshu_comments(url: str, output_file: str): 232 | """ 233 | 下载小红书评论数据 234 | 235 | :param url: 小红书页面URL 236 | :param output_file: 输出文件路径 237 | """ 238 | try: 239 | # 配置浏览器选项 240 | co = ChromiumOptions() 241 | co.headless(False) # 显示浏览器窗口 242 | co.set_local_port('9222') # 设置调试端口 243 | 244 | # 创建浏览器对象 245 | browser = Chromium(co) 246 | tab = browser.latest_tab 247 | 248 | # 监听网络请求 249 | logger.info("开始监听评论数据...") 250 | tab.listen.start('comment') # 监听包含comment的请求 251 | logger.info(f"正在访问页面: {url}") 252 | tab.get(url) 253 | tab.eles(".title")[4].click() # 点击视频 254 | 255 | # 等待足够时间获取数据 256 | time.sleep(4) 257 | 258 | # 获取监听到的数据 259 | data = tab.listen.wait() 260 | if not data: 261 | logger.error("未获取到评论数据") 262 | return 263 | 264 | with open(output_file, 'w', encoding='utf-8') as f: 265 | json.dump(data.response.body, f, ensure_ascii=False, indent=4) 266 | 267 | logger.success(f"评论数据已保存到: {output_file}") 268 | 269 | except Exception as e: 270 | logger.error(f"发生错误: {str(e)}") 271 | finally: 272 | # 关闭浏览器 273 | if 'browser' in locals(): 274 | browser.quit() 275 | 276 | if __name__ == '__main__': 277 | # 配置参数 278 | target_url = 'https://www.xiaohongshu.com/explore/' 279 | output_path = "comments_data.json" 280 | # 执行下载 281 | download_xiaohongshu_comments(target_url, output_path) 282 | ``` 283 | 284 | --- 285 | 286 | 287 | 288 | 289 | ## 🔗 官方资源 290 | 291 | - GitHub 项目主页: [https://github.com/g1879/DrissionPage](https://github.com/g1879/DrissionPage) 292 | --- 293 | 294 | 295 | 296 | > 本教程适合初学者快速上手,更多高级功能请参考官方文档。 297 | https://drissionpage.cn/ -------------------------------------------------------------------------------- /DrissionPage_code_guide.md: -------------------------------------------------------------------------------- 1 | 2 | # DrissionPage 代码语法规范指南教程 3 | 4 | 📌 DrissionPage® 是一个基于 Python 的网页自动化工具,能控制浏览器,功能强大,语法简洁优雅,代码量少,对新手友好.支持:Chromium 内核浏览器(如 Chrome 和 Edge) 5 | 6 | --- 7 | 8 | ## ✨ 安装 9 | 10 | ```bash 11 | pip install -U DrissionPage 12 | ``` 13 | 14 | 15 | --- 16 | 17 | 18 | 19 | ## 🛠 启动或者连接浏览器(带配置),然后打开一个网页 20 | 默认状态下,程序会自动在系统内查找 Chrome 路径 21 | 22 | ```python 23 | 24 | #!/usr/bin/env python 25 | # -*- coding:utf-8 -*- 26 | #-导入库 27 | from DrissionPage import Chromium, ChromiumOptions 28 | # 创建配置对象 29 | co = ChromiumOptions() 30 | co.headless(True) 31 | co.set_argument('--start-maximized') 32 | # 设置debug port 33 | co.set_local_port('9222') 34 | # 设置浏览器路径,不设置则默认是谷歌浏览器 35 | co.set_browser_path(r'C:\chrome.exe') 36 | 37 | # 创建浏览器实例,浏览器实例不能直接打开网页 38 | browser = Chromium(co) 39 | 40 | tab = browser.latest_tab #获取标签页实例,标签页实例可以直接get打开网页 41 | 42 | #访问网页.. 43 | tab.get("https://www.baidu.com/") 44 | print(tab.title) 45 | 46 | ``` 47 | 48 | --- 49 | 50 | ## 📄 自动登录gitee网站 51 | 52 | ```python 53 | # 使用默认模式访问网页 54 | from DrissionPage import Chromium 55 | 56 | # 启动或接管浏览器,并获取标签页对象 57 | tab = Chromium().latest_tab 58 | # 跳转到登录页面 59 | tab.get('https://gitee.com/login') 60 | 61 | # 定位到账号文本框,获取文本框元素 62 | ele = tab.ele('#user_login') 63 | # 输入对文本框输入账号 64 | ele.input('您的账号') 65 | # 定位到密码文本框并输入密码 66 | tab.ele('#user_password').input('您的密码') 67 | # 点击登录按钮 68 | tab.ele('@value=登 录').click() 69 | ``` 70 | 71 | --- 72 | 73 | ## 🔍 查找元素 74 | 75 | 支持 CSS 选择器、XPath 和自定义选择器: 76 | 77 | ```python 78 | # 查找tag为div的元素 79 | ele = tab.ele('tag:div') # 原写法 80 | ele = tab('t:div') # 简化写法 81 | 82 | # 用xpath查找元素 83 | ele = tab.ele('xpath://****') # 原写法 84 | ele = tab('x://****') # 简化写法 85 | 86 | # 查找text为'something'的元素 87 | ele = tab.ele('text=something') # 原写法 88 | ele = tab('tx=something') # 简化写法 89 | ``` 90 | 91 | --- 92 | 93 | ## ✏️ 获取电商网站的评论 94 | 95 | ```python 96 | 97 | #!/usr/bin/env python 98 | # -*- coding:utf-8 -*-# 99 | 100 | # 电脑内需要提取安装谷歌浏览器或者其他chromium内核的浏览器 比如 edge浏览器 qq浏览器 360浏览器 101 | 102 | 103 | import time 104 | from DrissionPage import Chromium 105 | from loguru import logger 106 | 107 | # 设置日志记录到文件 108 | logger.add("JD_comment.log", format="{time} {message}") 109 | 110 | # 初始化浏览器 111 | browser = Chromium() 112 | 113 | # 打开京东首页 114 | main_tab = browser.new_tab('https://www.jd.com/') 115 | 116 | # 获取搜索框并输入关键词 117 | search_input = main_tab.ele('tag:input@@id=key') 118 | search_input.input('小米手机') 119 | 120 | # 点击搜索按钮 121 | main_tab('tag:button@@aria-label=搜索').click() 122 | 123 | # 获取搜索结果列表 124 | search_results = main_tab.eles('t:li@@class=gl-item') 125 | 126 | # 打印每个搜索结果的文本 127 | # for result in search_results: 128 | # logger.info(result) 129 | 130 | # 点击搜索结果中的第二个商品以打开商品详情页 131 | product_detail_tab = search_results[1].ele('t:a').click.for_new_tab() 132 | # 点击评论标签页 133 | product_detail_tab.ele('@data-anchor=#comment').click() 134 | 135 | # 获取并打印商品评论 136 | def get_comments(tab): 137 | for comment in tab.eles('t:div@@class=comment-item'): 138 | # logger.info(comment) 139 | 140 | logger.info(comment('.comment-con').text) # 记录评论内容 141 | if recomment:=comment.ele('.recomment',timeout=2): 142 | logger.error(recomment.text) 143 | 144 | time.sleep(2) 145 | 146 | # 获取第一页评论并点击下一页 147 | get_comments(product_detail_tab) 148 | product_detail_tab.ele('t:a@@rel=2').click() 149 | 150 | # 循环获取剩余页码的评论 151 | for _ in range(4): 152 | get_comments(product_detail_tab) 153 | product_detail_tab.ele('下一页').click() 154 | ``` 155 | 156 | --- 157 | 158 | ## 📦 截图 159 | 160 | ```python 161 | # 对整页截图并保存 162 | tab.get_screenshot(path='tmp', name='pic.jpg', full_page=True) 163 | ``` 164 | 165 | --- 166 | 167 | ## 📂 文件下载 168 | 169 | ```python 170 | from DrissionPage import Chromium 171 | 172 | tab = Chromium().latest_tab 173 | tab.set.download_path('save_path') # 设置文件保存路径 174 | tab.set.download_file_name('file_name') # 设置重命名文件名 175 | tab('t:a').click() # 点击一个会触发下载的链接 176 | tab.wait.download_begin() # 等待下载开始 177 | tab.wait.downloads_done() # 等待下载结束 178 | ``` 179 | 180 | --- 181 | 182 | ## 📤 文件上传 183 | 184 | ```python 185 | # 设置要上传的文件路径 186 | tab.set.upload_files('demo.txt') 187 | # 点击触发文件选择框按钮 188 | btn_ele.click() 189 | # 等待路径填入 190 | tab.wait.upload_paths_inputted() 191 | ``` 192 | --- 193 | 194 | ## 📤 浏览器等待 195 | 196 | ```python 197 | tab.wait(10) # 等待10秒 198 | ``` 199 | 200 | --- 201 | ## 📤 关闭浏览器 202 | 203 | ```python 204 | browser.quit() 205 | ``` 206 | ## 📤 关闭标签页 207 | ```python 208 | tab.close() 209 | ``` 210 | 211 | --- 212 | ## 📤 访问指定小红书页面,监听并下载评论json数据 213 | ```python 214 | #!/usr/bin/env python 215 | # -*- coding: utf-8 -*- 216 | 217 | """ 218 | 小红书评论数据下载脚本 219 | 220 | 功能:访问指定小红书页面,监听并下载评论json数据 221 | """ 222 | 223 | import json 224 | import time 225 | from DrissionPage import Chromium, ChromiumOptions 226 | from loguru import logger 227 | 228 | # 配置日志 229 | logger.add("xiaohongshu_comment.log", format="{time} {level} {message}") 230 | 231 | def download_xiaohongshu_comments(url: str, output_file: str): 232 | """ 233 | 下载小红书评论数据 234 | 235 | :param url: 小红书页面URL 236 | :param output_file: 输出文件路径 237 | """ 238 | try: 239 | # 配置浏览器选项 240 | co = ChromiumOptions() 241 | co.headless(False) # 显示浏览器窗口 242 | co.set_local_port('9222') # 设置调试端口 243 | 244 | # 创建浏览器对象 245 | browser = Chromium(co) 246 | tab = browser.latest_tab 247 | 248 | # 监听网络请求 249 | logger.info("开始监听评论数据...") 250 | tab.listen.start('comment') # 监听包含comment的请求 251 | logger.info(f"正在访问页面: {url}") 252 | tab.get(url) 253 | tab.eles(".title")[4].click() # 点击视频 254 | 255 | # 等待足够时间获取数据 256 | time.sleep(4) 257 | 258 | # 获取监听到的数据 259 | data = tab.listen.wait() 260 | if not data: 261 | logger.error("未获取到评论数据") 262 | return 263 | 264 | with open(output_file, 'w', encoding='utf-8') as f: 265 | json.dump(data.response.body, f, ensure_ascii=False, indent=4) 266 | 267 | logger.success(f"评论数据已保存到: {output_file}") 268 | 269 | except Exception as e: 270 | logger.error(f"发生错误: {str(e)}") 271 | finally: 272 | # 关闭浏览器 273 | if 'browser' in locals(): 274 | browser.quit() 275 | 276 | if __name__ == '__main__': 277 | # 配置参数 278 | target_url = 'https://www.xiaohongshu.com/explore/' 279 | output_path = "comments_data.json" 280 | # 执行下载 281 | download_xiaohongshu_comments(target_url, output_path) 282 | ``` 283 | 284 | --- 285 | 286 | 287 | 288 | 289 | ## 🔗 官方资源 290 | 291 | - GitHub 项目主页: [https://github.com/g1879/DrissionPage](https://github.com/g1879/DrissionPage) 292 | --- 293 | 294 | 295 | 296 | > 本教程适合初学者快速上手,更多高级功能请参考官方文档。 297 | https://drissionpage.cn/ 298 | -------------------------------------------------------------------------------- /main-2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from typing import Any,Literal 5 | import re 6 | from pathlib import Path 7 | from DrissionPage import Chromium,ChromiumOptions 8 | from mcp.server.fastmcp import FastMCP,Image,Context 9 | 10 | from DrissionPage.items import SessionElement, ChromiumElement, ShadowRoot, NoneElement, ChromiumTab, MixTab, ChromiumFrame 11 | from DrissionPage.common import Keys 12 | 13 | 14 | 提示=''' 15 | DrissionPage MCP 是一个基于 DrissionPage 和 FastMCP 的浏览器自动化MCP server服务器,它提供了一系列强大的浏览器操作 API,让您能够轻松通过AI实现网页自动化操作。 16 | 点击元素前,需要先获取页面所有可点击元素的信息,使用get_all_clickable_elements()方法。 17 | 输入元素前,需要先获取页面所有可输入元素的信息,使用get_all_input_elements()方法。 18 | 19 | ''' 20 | 21 | 22 | 23 | #region DrissionPageMCP 24 | class DrissionPageMCP(): 25 | def __init__(self): 26 | self.browser = None 27 | self.session = None 28 | self.current_tab = None 29 | self.current_frame = None 30 | self.current_shadow_root = None 31 | self.cdp_event_data = [] 32 | self.response_listener_data=[] 33 | 34 | def test(self): 35 | return "test" 36 | def get_version(self)-> str: 37 | """ 获取版本号""" 38 | return "1.0.3" 39 | async def connect_or_open_browser(self, config: dict={'debug_port':9222}) -> dict: 40 | """ 41 | 用DrissionPage 打开或接管已打开的浏览器,参数通过字典传递。 42 | 必要参数: 43 | config (dict): 可选键包括 、debug_port、browser_path、headless 44 | 返回: 45 | dict: 浏览器信息 46 | """ 47 | co = ChromiumOptions() 48 | if config.get("debug_port"): 49 | co.set_local_port(config["debug_port"]) 50 | if config.get("browser_path"): 51 | co.set_browser_path(config["browser_path"]) 52 | if config.get("headless", False): 53 | co.headless(True) 54 | 55 | self.browser = Chromium(co) 56 | tab = self.browser.latest_tab 57 | 58 | return { 59 | "browser_address": self.browser._chromium_options.address, 60 | "latest_tab_title": tab.title, 61 | "latest_tab_id": tab.tab_id, 62 | } 63 | async def new_tab(self, url: str) -> str: 64 | """用DrissionPage 控制的浏览器,打开新标签页并 打开一个网址""" 65 | tab = self.browser.new_tab(url) 66 | return {"title": tab.title, "tab_id": tab.tab_id, "url": tab.url,"dom":self.getSimplifiedDomTree()} 67 | 68 | def wait(self, a:int) : 69 | """等待a秒""" 70 | self.browser.latest_tab.wait(a) 71 | return f"等待{a}秒成功" 72 | 73 | async def get(self,url:str)->str: 74 | """在当前标签页打开一个网址""" 75 | if not self.browser: 76 | await self.connect_or_open_browser() 77 | # return "请先打开或者连接浏览器" 78 | self.lastest_tab.get(url) 79 | tab=self.browser.latest_tab 80 | return {"title": tab.title, "tab_id": tab.tab_id, "url": tab.url,"dom":self.getSimplifiedDomTree()} 81 | 82 | 83 | #region 上传和下载 84 | def download_file(self, url: str, path: str, rename: str) -> str: 85 | """控制浏览器下载文件到指定路径 86 | 87 | Args: 88 | url (str): 文件的URL地址 89 | path (str): 文件保存的路径 90 | rename (str): 重命名文件名 91 | 92 | Returns: 93 | str: 下载结果信息 94 | """ 95 | tab = self.lastest_tab 96 | result = tab.download(file_url=url, save_path=path, rename=rename) 97 | return str(result) 98 | 99 | def upload_file(self, file_path: str) -> str: 100 | """点击当网页上的 元素触发上传文件的操作,上传file_path文件到当前网页 101 | 102 | Args: 103 | file_path (str): 要上传的文件路径 104 | 105 | Returns: 106 | str: 上传结果信息,如果元素不存在则返回错误信息 107 | """ 108 | x="//input[@type='file']" 109 | t:ChromiumTab=self.lastest_tab 110 | if e:= t(f"xpath:{x}"): 111 | t.set.upload_files(file_path) 112 | e.click(by_js=True) 113 | t.wait.upload_paths_inputted() 114 | return f"{file_path} 上传成功 {e}" 115 | else: 116 | return f"元素{x}不存在,无法触发上传文件" 117 | 118 | 119 | 120 | @property 121 | def lastest_tab(self) -> ChromiumTab: 122 | """获取最新标签页""" 123 | return self.browser.latest_tab 124 | 125 | def send_enter(self) -> str: 126 | """向当前页面发送 enter 回车键""" 127 | tab = self.browser.latest_tab 128 | try: 129 | result = tab.actions.type(Keys.ENTER) 130 | return f"{tab.title} 网页发送 enter 回车键成功" 131 | except Exception as e: 132 | return f"{tab.title} 网页发送 enter 回车键失败" 133 | 134 | def getInputElementsInfo(self) -> list: 135 | """获取当前标签页的所有可进行输入操作的元素,对元素进行输入操作前优先使用这个方法""" 136 | tab = self.browser.latest_tab 137 | js_code=''' 138 | const inputElements = Array.from(document.querySelectorAll('input, select, textarea, button')); 139 | return inputElements.filter(el => !el.disabled); // 排除禁用的元素 140 | ''' 141 | elements = tab.run_js(js_code) 142 | return elements 143 | 144 | def click_by_xpath(self, xpath: str) -> dict: 145 | """通过xpath点击当前标签页中某个元素,最好先获取页面dom信息,再决定Xpath的写法""" 146 | 147 | locator = f"xpath:{xpath}" 148 | element = self.browser.latest_tab.ele(locator, timeout=3) 149 | result = {"locator": locator, "element": str(element), "click_result": element.click()} 150 | return result 151 | 152 | def click_by_containing_text(self, content: str, index: int = None) : 153 | """ 154 | 根据包含指定文本的方式点击网页元素。 155 | 156 | 参数: 157 | content: 要查找的文本内容。 158 | index: 当匹配到多个元素时指定要点击的索引,默认不指定。 159 | 160 | 返回: 161 | 点击结果说明,或错误提示。 162 | """ 163 | 164 | # 获取包含指定文本的所有元素,等待最多 3 秒 165 | elements = self.browser.latest_tab.eles(content, timeout=3) 166 | 167 | # 如果没有匹配到任何元素,返回错误提示 168 | if len(elements) == 0: 169 | return f"元素{content}不存在,需要getInputElementsInfo先获取元素信息" 170 | 171 | # 如果只找到一个元素,直接点击它 172 | if len(elements) == 1: 173 | self.lastest_tab(content).click() 174 | return f" 点击成功" 175 | 176 | # 如果找到多个元素 177 | if len(elements) > 1: 178 | # 如果未指定 index,提示用户提供索引 179 | if index is None: 180 | return f"元素{content}存在多个,请调整 index 参数,index=0表示第一个元素,{elements}" 181 | else: 182 | # 根据指定索引点击对应的元素 183 | elements[index].click() 184 | return f" 点击成功" 185 | 186 | 187 | 188 | def input_by_xapth(self, xpath: str, input_value: str, clear_first: bool = True) : 189 | """通过xpath给当前标签页中某个元素输入内容,最好先判断元素是否存在 190 | 191 | Args: 192 | xpath (str): 元素的XPath表达式 193 | input_value (str): 要输入的内容 194 | clear_first (bool): 是否先清除已有内容,默认为True 195 | 196 | Returns: 197 | Any: 输入操作的结果,如果元素不存在则返回错误信息 198 | """ 199 | locator = f"xpath:{xpath}" 200 | if e := self.browser.latest_tab.ele(locator, timeout=4): 201 | result = {"locator": locator, "result": e.input(input_value, clear=clear_first)} 202 | return result 203 | else: 204 | return f"元素{locator}不存在,需要getInputElementsInfo先获取元素信息" 205 | 206 | def get_body_text(self) -> str: 207 | """获取当前标签页的body的文本内容""" 208 | 209 | tab = self.browser.latest_tab 210 | body_text = tab('t:body').text 211 | return body_text 212 | def run_js(self, js_code: str) : 213 | """ 214 | 在当前标签页中运行JavaScript代码并返回执行结果 215 | 查找网页元素,获取元素信息,操作网页元素优先使用这个方法 216 | 217 | Args: 218 | js_code (str): 要执行的JavaScript代码 219 | 220 | Returns: 221 | Any: JavaScript代码执行结果 222 | 223 | Note: 224 | 想要获取执行的js代码的返回值,可以在js_code中使用return语句。 225 | 想要获取异步函数的返回值,可以参考下面代码 226 | return (async (url) => { 227 | const response = await fetch(url); 228 | const data = await response.json(); 229 | return data; 230 | })("https://www.baidu.com/"); 231 | """ 232 | tab = self.browser.latest_tab 233 | result = tab.run_js(js_code) 234 | return result 235 | 236 | def run_cdp( self,cmd, **cmd_args) : 237 | """在当前标签页中运行谷歌CDP协议代码并获取结果 238 | 239 | Args: 240 | 241 | cmd: CDP协议命令 242 | **cmd_args: CDP命令参数 243 | 244 | Returns: 245 | Any: CDP命令执行结果 246 | 247 | Note: 248 | 举例1说明 run_cdp('Page.stopLoading') 249 | 举例2说明 run_cdp('Page.navigate', url='https://example.com') 250 | """ 251 | result=self.browser.latest_tab.run_cdp(cmd, **cmd_args) 252 | return result 253 | def listen_cdp_event(self,event_name: str) : 254 | """设置监听CDP事件 255 | 256 | 应该先运行cdp 命令 激活对应的域,比如 Network.enable 257 | """ 258 | # b=Chromium(debug_port) 259 | def r(**event): 260 | self.cdp_event_data.append({"event_name": event_name, "event_data": event}) 261 | 262 | try: 263 | self.browser.latest_tab.driver.set_callback(event_name, r) 264 | return f"CDP event callback for '{event_name}' set successfully." 265 | except Exception as e: 266 | return e 267 | 268 | def get_cdp_event_data(self) -> list: 269 | """获取CDP事件回调函数收集到的数据""" 270 | return self.cdp_event_data 271 | 272 | 273 | 274 | #region 监听网页接收的数据包 275 | 276 | def get_url_with_response_listener(self, 277 | tab_url: str, 278 | mimeType: Literal[ 279 | # 文本类 280 | "text/html", 281 | "text/css", 282 | "text/javascript", 283 | "application/javascript", 284 | "text/plain", 285 | "text/xml", 286 | "text/csv", 287 | "application/json", 288 | 289 | # 应用类 290 | "application/octet-stream", 291 | "application/zip", 292 | "application/pdf", 293 | "multipart/form-data", 294 | "application/xml", 295 | 296 | # 图片类 297 | "image/jpeg", 298 | "image/png", 299 | "image/gif", 300 | "image/webp", 301 | "image/svg+xml", 302 | "image/x-icon", 303 | 304 | # 音视频类 305 | "audio/mpeg", 306 | "audio/ogg", 307 | "video/mp4", 308 | "video/webm", 309 | "video/ogg" 310 | ], 311 | url_include: str = "." 312 | ) : 313 | ''' 314 | 开启一个新的标签页,设置监听,访问tab_url, 315 | tab_url: 被监听的标签页的url 316 | mimeType: 需要监听的接收的数据包的mimeType类型 317 | url_include: 需要监听的接收的数据包的url包含的关键字 318 | refresh: 是否刷新页面, 319 | ''' 320 | t = self.browser.new_tab(tab_url) 321 | 322 | t.run_cdp("Network.enable") 323 | 324 | def r(**event): 325 | _url = event.get("response", {}).get("url", "") 326 | _mimeType = event.get("response", {}).get("mimeType", "") 327 | 328 | if mimeType in _mimeType and url_include in _url: 329 | self.response_listener_data.append({ 330 | "event_name": "Network.responseReceived", 331 | "event_data": event 332 | }) 333 | 334 | t.driver.set_callback("Network.responseReceived", r) 335 | t.get(tab_url) 336 | 337 | return f"开启监听{tab_url}, 数据包url包含关键字:{url_include},mimeType:{mimeType}" 338 | 339 | 340 | 341 | def response_listener_stop(self,clear_data:bool=False) -> str: 342 | """关闭监听网页发送的数据包""" 343 | t=self.browser.latest_tab 344 | t.run_cdp("Network.disable") 345 | if clear_data: 346 | self.response_listener_data = [] 347 | return f"监听网页发送的数据包关闭成功 ,是否清空数据: {clear_data}" 348 | 349 | 350 | def get_response_listener_data(self) -> list: 351 | """获取监听到的数据,返回数据列表""" 352 | return self.response_listener_data 353 | 354 | def get_current_tab_screenshot(self) -> bytes: 355 | """ 356 | 获取当前标签页的网页截图 357 | 358 | Returns: 359 | bytes: 截图的二进制数据 360 | """ 361 | t:ChromiumTab=self.browser.latest_tab 362 | screenshot=t.get_screenshot(as_bytes='jpeg') 363 | return screenshot 364 | 365 | def get_current_tab_screenshot_as_file(self,path:str=".",name:str="screenshot.png") -> str: 366 | """ 367 | 获取当前标签页的屏幕截图并保存为文件 368 | 369 | Args: 370 | path (str): 截图保存路径,默认为当前目录 371 | 372 | Returns: 373 | str: 截图的文件路径 374 | """ 375 | 376 | screenshot=self.browser.latest_tab.get_screenshot(path=path,name=name) 377 | return screenshot 378 | 379 | def get_current_tab_info(self) -> dict: 380 | """获取当前标签页的信息,包括url, title, id""" 381 | tab =self.browser.latest_tab 382 | info = { 383 | "url": tab.url, 384 | "title": tab.title, 385 | "id": tab.tab_id, 386 | } 387 | return info 388 | 389 | def send_key(self, key: Literal["Enter, Backspace, HOME, END, PAGE_UP, PAGE_DOWN, DOWN, UP, LEFT, RIGHT, ESC, Ctrl+C, Ctrl+V, Ctrl+A, Delete"]) -> str: 390 | """向当前标签页发送特殊按键""" 391 | tab = self.browser.latest_tab 392 | k={"Enter": Keys.ENTER, 393 | "Backspace": Keys.BACKSPACE, 394 | "HOME": Keys.HOME, 395 | "END": Keys.END, 396 | "PAGE_UP": Keys.PAGE_UP, 397 | "PAGE_DOWN": Keys.PAGE_DOWN, 398 | "DOWN": Keys.DOWN, 399 | "UP": Keys.UP, 400 | "LEFT": Keys.LEFT, 401 | "RIGHT": Keys.RIGHT, 402 | "ESC": Keys.ESCAPE, 403 | "Ctrl+C": Keys.CTRL_C, 404 | "Ctrl+V": Keys.CTRL_V, 405 | "Ctrl+A": Keys.CTRL_A, 406 | "Delete": Keys.DELETE,} 407 | try: 408 | result = tab.actions.type(k.get(key)) 409 | return f"{tab.title} 网页发送 {key} 键成功" 410 | except Exception as e: 411 | return f"{tab.title} 网页发送 {key} 键失败" 412 | 413 | def getSimplifiedDomTree(self) -> dict: 414 | """获取当前标签页的简化版DOM树""" 415 | from CodeBox import domTreeToJson 416 | tab = self.browser.latest_tab 417 | dom_tree = tab.run_js(domTreeToJson) 418 | return dom_tree 419 | 420 | #region 拖动 421 | 422 | def move_to(self,xpath:str) -> dict: 423 | """鼠标移动悬停到指定xpath的元素上""" 424 | tab = self.browser.latest_tab 425 | locator = f"xpath:{xpath}" 426 | element = tab.ele(locator, timeout=3) 427 | if element: 428 | element.hover() 429 | result = {"locator": locator, "element": str(element)} 430 | return result 431 | else: 432 | return f"元素{locator}不存在,需要getSimplifiedDomTree先获取元素信息" 433 | def drag(self,xpath:str, offset_x: int, offset_y: int, duration: int = 1000) -> dict: 434 | 435 | """ 436 | 将元素拖动到指定偏移位置 437 | 438 | Args: 439 | xpath: 要拖动的元素xpath路径 440 | offset_x: x轴偏移量(像素) 441 | offset_y: y轴偏移量(像素) 442 | duration: 拖动持续时间(毫秒),默认为1000 443 | 444 | Returns: 445 | dict: 包含偏移量和持续时间的字典,格式为{"offset_x": int, "offset_y": int, "duration": int} 446 | 或 str: 当元素不存在时返回错误信息 447 | 448 | Raises: 449 | 无显式抛出异常,但内部可能因元素不存在而返回错误信息 450 | """ 451 | tab = self.browser.latest_tab 452 | if e:=tab.ele(f'xpath:{xpath}', timeout=3): 453 | tab.actions.move_to(e).wait(0.5).hold().move(offset_x, offset_y).release() 454 | result = {"offset_x": offset_x, "offset_y": offset_y, "duration": duration} 455 | return result 456 | else: 457 | return f"元素{xpath}不存在,需要getSimplifiedDomTree先获取元素信息" 458 | 459 | #region 初始化mcp 460 | mcp = FastMCP("DrissionPageMCP", log_level="ERROR",instructions=提示) 461 | b=DrissionPageMCP() 462 | 463 | mcp.add_tool(b.get_version) 464 | mcp.add_tool(b.connect_or_open_browser) 465 | mcp.add_tool(b.new_tab) 466 | mcp.add_tool(b.wait) 467 | mcp.add_tool(b.get) 468 | mcp.add_tool(b.download_file) 469 | mcp.add_tool(b.upload_file) 470 | mcp.add_tool(b.send_enter) 471 | mcp.add_tool(b.getInputElementsInfo) 472 | mcp.add_tool(b.click_by_xpath) 473 | mcp.add_tool(b.click_by_containing_text) 474 | mcp.add_tool(b.input_by_xapth) 475 | mcp.add_tool(b.get_body_text) 476 | mcp.add_tool(b.run_js) 477 | mcp.add_tool(b.run_cdp) 478 | mcp.add_tool(b.listen_cdp_event) 479 | mcp.add_tool(b.get_cdp_event_data) 480 | mcp.add_tool(b.get_url_with_response_listener) 481 | mcp.add_tool(b.response_listener_stop) 482 | mcp.add_tool(b.get_response_listener_data) 483 | mcp.add_tool(b.get_current_tab_screenshot) 484 | mcp.add_tool(b.get_current_tab_screenshot_as_file) 485 | mcp.add_tool(b.get_current_tab_info) 486 | mcp.add_tool(b.send_key) 487 | mcp.add_tool(b.getSimplifiedDomTree) 488 | 489 | mcp.add_tool(b.move_to) 490 | mcp.add_tool(b.drag) 491 | 492 | #region 保存数据到sqlite 493 | from ToolBox import save_dict_to_sqlite 494 | mcp.add_tool(save_dict_to_sqlite) 495 | 496 | 497 | 498 | 499 | def main(): 500 | # 启动MCP服务器 501 | print("DrissionPage MCP server is running...") 502 | mcp.run(transport='stdio') 503 | 504 | 505 | if __name__ == "__main__": 506 | main() 507 | -------------------------------------------------------------------------------- /main-1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # mcp程序说明:通过DrissionPage这个库控制浏览器进行各种网页自动化操作 4 | 5 | # 调试 npx -y @modelcontextprotocol/inspector uv run D:\\test10\\DrssionPageMCP\\main.py 6 | ''' 7 | # mcp配置 8 | "DrissionPageMCP": { 9 | "command": "uv", 10 | "args": [ 11 | "run", 12 | "D:\\test10\\DrssionPageMCP\\main.py" 13 | ] 14 | } 15 | 16 | ''' 17 | 18 | from typing import Any,Literal, LiteralString 19 | import re 20 | from pathlib import Path 21 | from DrissionPage import Chromium,ChromiumOptions 22 | from mcp.server.fastmcp import FastMCP,Image,Context 23 | 24 | from DrissionPage.items import SessionElement, ChromiumElement, ShadowRoot, NoneElement, ChromiumTab, MixTab, ChromiumFrame 25 | from DrissionPage.common import Keys 26 | 27 | # from PIL import Image as PILImage 28 | import io 29 | 30 | # Initialize FastMCP server 31 | # mcp = FastMCP("DrissionPageMCP", log_level="ERROR") 32 | 提示=''' 33 | DrissionPage MCP 是一个基于 DrissionPage 和 FastMCP 的浏览器自动化MCP server服务器,它提供了一系列强大的浏览器操作 API,让您能够轻松通过AI实现网页自动化操作。 34 | 点击元素前,需要先获取页面所有可点击元素的信息,使用get_all_clickable_elements()方法。 35 | 输入元素前,需要先获取页面所有可输入元素的信息,使用get_all_input_elements()方法。 36 | 37 | ''' 38 | 39 | mcp = FastMCP("DrissionPageMCP", log_level="ERROR",instructions=提示) 40 | 41 | 42 | 43 | 44 | # Browser:Chromium=None 45 | class DP: 46 | browser:Chromium=None 47 | cdp_event_data=[] 48 | listener_data=[] 49 | Drissionpage_python_code=None 50 | mime_types = [ 51 | # 文本类 52 | "text/html", 53 | "text/css", 54 | "text/javascript", 55 | "application/javascript", 56 | "text/plain", 57 | "text/xml", 58 | "text/csv", 59 | "application/json", 60 | 61 | # 应用类 62 | "application/octet-stream", 63 | "application/zip", 64 | "application/pdf", 65 | "application/x-www-form-urlencoded", 66 | "multipart/form-data", 67 | "application/xml", 68 | 69 | # 图片类 70 | "image/jpeg", 71 | "image/png", 72 | "image/gif", 73 | "image/webp", 74 | "image/svg+xml", 75 | "image/x-icon", 76 | 77 | # 音视频类 78 | "audio/mpeg", 79 | "audio/ogg", 80 | "video/mp4", 81 | "video/webm", 82 | "video/ogg" 83 | ] 84 | 85 | 86 | @mcp.resource("dir://cwd") 87 | def get_current_directory() -> str: 88 | """返回当前工作目录的路径""" 89 | return str(Path.cwd()) 90 | 91 | 92 | 93 | @mcp.resource("elments://tagname={tagname}") 94 | def get_input_elements(tagname:str) -> list: 95 | """返回页面中所有tagname的元素信息""" 96 | if DP.browser is None: 97 | return "没有打开浏览器" 98 | aa=DP.browser.latest_tab.eles("t:"+tagname) 99 | return aa 100 | 101 | 102 | 103 | @mcp.resource("browser://{port}/info") 104 | def browser_info(port:int) -> dict: 105 | """获取浏览器的信息""" 106 | b=Chromium(port) 107 | a={'browser_address':b._chromium_options.address, 108 | "latest_tab_title":b.latest_tab.title, 109 | "latest_tab_id":b.latest_tab.tab_id, 110 | } 111 | return a 112 | 113 | @mcp.tool() 114 | def convert_elemnet_to_drissionpage(element: str) -> str: 115 | """把元素转换为drissionpage格式的字符串""" 116 | e= Use.raw(element) 117 | return e 118 | 119 | # @mcp.tool() 120 | def get_all_clickable_elements() -> list: 121 | """获取当前标签页的所有可点击元素""" 122 | tab = DP.browser.latest_tab 123 | js_code = ''' 124 | const clickableElements = Array.from(document.querySelectorAll('a, button, input[type="button"], input[type="submit"], [onclick]')); 125 | return clickableElements.map(el => el.outerHTML); 126 | ''' 127 | elements = tab.run_js(js_code) 128 | return elements 129 | mcp.add_tool( 130 | name="get_all_clickable_elements", 131 | description="获取当前标签页的所有可点击元素", 132 | fn=get_all_clickable_elements, 133 | ) 134 | 135 | @mcp.tool() 136 | def get_all_input_elements() -> list: 137 | """获取当前标签页的所有可输入元素""" 138 | tab = DP.browser.latest_tab 139 | js_code = ''' 140 | const inputElements = Array.from(document.querySelectorAll('input, select, textarea, button')); 141 | return inputElements.filter(el => !el.disabled).map(el => el.outerHTML); 142 | ''' 143 | elements = tab.run_js(js_code) 144 | return elements 145 | 146 | #region 处理数据 147 | @mcp.tool() 148 | async def process_data( ctx: Context) -> dict: 149 | """ 150 | 从指定的资源 URI 读取数据,报告处理进度,并请求客户端的 LLM 对数据进行摘要。 151 | 152 | 参数: 153 | 154 | - ctx (Context): FastMCP 提供的上下文对象,包含日志记录、资源访问、进度报告等功能。 155 | 156 | 返回: 157 | - dict: 包含数据长度和摘要的字典。 158 | """ 159 | data_uri="browser://9222/info" 160 | # 使用 ctx.info() 记录信息日志,通知客户端正在处理的数据 URI 161 | await ctx.info(f"Processing data from {data_uri}") 162 | 163 | # 使用 ctx.read_resource() 读取指定 URI 的资源内容 164 | resource = await ctx.read_resource(data_uri) 165 | # 如果资源存在,提取第一个资源的内容;否则,设置为空字符串 166 | data = resource[0].content if resource else "" 167 | 168 | # 使用 ctx.report_progress() 报告处理进度为 50% 169 | await ctx.report_progress(progress=50, total=100) 170 | 171 | 172 | 173 | # 使用 ctx.report_progress() 报告处理进度为 100% 174 | await ctx.report_progress(progress=100, total=100) 175 | 176 | # 返回包含数据长度和摘要的字典 177 | return { 178 | "length": len(data) 179 | 180 | } 181 | 182 | #region 注册提示 183 | @mcp.prompt() 184 | def ask_about_topic(topic: str) -> str: 185 | """生成一个请求解释某个主题的用户消息。""" 186 | return f"Can you please explain the concept of '{topic}'?" 187 | 188 | 189 | 190 | @mcp.tool() 191 | async def test(text: str) -> str: 192 | """生成一个请求对文本进行摘要的用户消息。""" 193 | a=await mcp.get_prompt("ask_about_topic", {"topic": text}) 194 | 195 | return a 196 | 197 | 198 | 199 | #region 连接浏览器 200 | @mcp.tool() 201 | async def connect_or_open_browser(params: dict,ctx: Context) -> int: 202 | """ 203 | 用DrissionPage 打开或者接管已打开的浏览器,参数通过字典传递。如果url不为空则打开指定网址。 204 | 205 | 参数: 206 | params (dict): 包含以下可选键的字典: 207 | - url (str, 可选): 要打开的网址。如果未提供则不自动打开页面。 208 | - debug_port (int, 可选): 调试端口,默认9222。 209 | - browser_path (str, 可选): 浏览器可执行文件路径。 210 | - headless (bool, 可选): 是否以无头模式启动浏览器,默认False。 211 | - use_system_user_path (bool, 可选): 是否使用系统默认用户配置,默认False。 212 | 213 | 返回: 214 | srt: 浏览器各种信息 215 | """ 216 | """用DrissionPage 控制接管浏览器,参数通过字典传递。如果url不为空则打开指定网址""" 217 | url = params.get("url") 218 | debug_port = params.get("debug_port", 9222) 219 | browser_path = params.get("browser_path") 220 | headless = params.get("headless", False) 221 | use_system_user_path = params.get("use_system_user_path", False) 222 | 223 | co = ChromiumOptions() 224 | co.set_local_port(debug_port) 225 | if browser_path: 226 | co.set_browser_path(browser_path) 227 | if headless: 228 | co.headless(True) 229 | if use_system_user_path: 230 | co.use_system_user_path(True) 231 | 232 | b = Chromium(co) 233 | tab = b.latest_tab 234 | DP.browser = b 235 | if url: 236 | tab.get(url) 237 | info2= await ctx.read_resource(f"browser://{debug_port}/info") 238 | info={ 239 | "browser_address": b._chromium_options.address, 240 | "latest_tab_title": tab.title, 241 | "latest_tab_id": tab.tab_id, 242 | "ctx":[ctx.client_id,ctx.model_computed_fields,ctx.request_id,ctx.request_context] 243 | } 244 | # info1=await ctx.info(str(info)) 245 | 246 | return info 247 | 248 | @mcp.tool() 249 | def new_tab(url: str="") -> str: 250 | """用DrissionPage 控制的浏览器,打开新标签页并 打开一个网址""" 251 | t=DP.browser.new_tab( url) 252 | return f'{t.title} {t.tab_id} {t.url} 已经打开' 253 | 254 | @mcp.tool() 255 | def download_file(url: str, path: str, rename: str ) -> str: 256 | """下载文件到指定路径 257 | 258 | Args: 259 | url (str): 文件的URL地址 260 | path (str): 文件保存的路径 261 | rename (str, optional): 重命名文件名. 262 | 263 | Returns: 264 | str: 下载结果信息 265 | """ 266 | tab = DP.browser.latest_tab 267 | result = tab.download(file_url=url, save_path=path, rename=rename) 268 | 269 | 270 | return result 271 | 272 | @mcp.tool() 273 | 274 | def send_enter() -> str: 275 | """向当前页面发送 enter 回车键""" 276 | 277 | tab=DP.browser.latest_tab 278 | try: 279 | result=tab.actions.type(Keys.ENTER) 280 | return f"{tab.title} 网页发送 enter 回车键成功" 281 | except Exception as e: 282 | return f"{tab.title} 网页发送 enter 回车键失败" 283 | 284 | 285 | @mcp.tool() 286 | def is_element_exist(element_xpath: str, content_include_keyword: str) -> str: 287 | """通过xpath或者文本节点是否包含关键词判断标签页中某个元素是否存在""" 288 | 289 | xpath_locator = f"xpath:{element_xpath}" 290 | 291 | if elements := DP.browser.latest_tab.eles(xpath_locator, timeout=2): 292 | return elements 293 | if elements := DP.browser.latest_tab.eles(content_include_keyword, timeout=2): 294 | return elements 295 | 296 | return "没找到元素" 297 | @mcp.tool() 298 | def getInputElementsInfo() -> list: 299 | """获取当前标签页的所有可进行输入操作的元素,对元素进行输入操作前优先使用这个方法""" 300 | tab = DP.browser.latest_tab 301 | js_code=''' 302 | const inputElements = Array.from(document.querySelectorAll('input, select, textarea, button')); 303 | return inputElements.filter(el => !el.disabled); // 排除禁用的元素 304 | ''' 305 | aaa=tab.run_js(js_code) 306 | return aaa 307 | 308 | 309 | #region 点击元素 310 | @mcp.tool() 311 | def element_click(element_xpath: str) -> Any: 312 | """通过xpath点击标签页中某个元素,最好先判断元素是否存在""" 313 | 314 | l=f"xpath:{element_xpath}" 315 | e=DP.browser.latest_tab.ele(l,timeout=3) 316 | result={"locator":l,"element":e,"click_result":e.click()} 317 | return result 318 | 319 | 320 | #region 元素输入 321 | @mcp.tool() 322 | def element_input(element_xpath: str,input_value: str) -> Any: 323 | """通过xpath给标签页中某个元素输入内容,最好先判断元素是否存在""" 324 | # b=Chromium(debug_port) 325 | locator=f"xpath:{element_xpath}" 326 | if e:=DP.browser.latest_tab.ele(locator,timeout=4): 327 | result={"locator":locator,"result":e.input(input_value)} 328 | return result 329 | else: 330 | return f"元素{locator}不存在,需要getInputElementsInfo先获取元素信息" 331 | 332 | 333 | @mcp.tool() 334 | def get_current_tab_element_html(element_xpath: str) -> str: 335 | """获取当前标签页的某个元素的html""" 336 | # b=Chromium(debug_port) 337 | elem=DP.browser.latest_tab.ele(f"xpath:{element_xpath}") 338 | if elem: 339 | return elem.run_js('return this.outerHTML') 340 | else: 341 | return "No element found" 342 | 343 | @mcp.tool() 344 | def get_body_text() -> str: 345 | """获取当前标签页的body的文本内容""" 346 | 347 | tab:ChromiumTab=DP.browser.latest_tab 348 | body_text=tab('t:body').text 349 | return body_text 350 | 351 | 352 | @mcp.tool() 353 | def run_js(js_code: str) -> Any: 354 | """ 355 | 在当前标签页中运行JavaScript代码并返回执行结果 356 | 查找网页元素,获取元素信息,操作网页元素优先使用这个方法 357 | 358 | Args: 359 | 360 | js_code (str): 要执行的JavaScript代码 361 | 362 | Returns: 363 | Any: JavaScript代码执行结果 364 | 365 | Note: 366 | 想要获取执行的js代码的返回值,可以在js_code中使用return语句。 367 | 想要获取异步函数的返回值,可以参考下面代码 368 | return (async (url) => { 369 | const response = await fetch(url); 370 | const data = await response.json(); 371 | return data; 372 | })("https://www.baidu.com/"); 373 | """ 374 | 375 | # b=Chromium(debug_port) 376 | 377 | result=DP.browser.latest_tab.run_js(js_code) 378 | return result 379 | @mcp.tool() 380 | def run_cdp( cmd, **cmd_args) -> Any: 381 | """在当前标签页中运行谷歌CDP协议代码并获取结果 382 | 383 | Args: 384 | 385 | cmd: CDP协议命令 386 | **cmd_args: CDP命令参数 387 | 388 | Returns: 389 | Any: CDP命令执行结果 390 | 391 | Note: 392 | 举例1说明 run_cdp('Page.stopLoading') 393 | 举例2说明 run_cdp('Page.navigate', url='https://example.com') 394 | """ 395 | """在当前标签页中运行谷歌CDP协议代码并获取结果""" 396 | # b=Chromium(debug_port) 397 | result=DP.browser.latest_tab.run_cdp(cmd, **cmd_args) 398 | return result 399 | 400 | @mcp.tool() 401 | def on_cdp_event(event_name: str) -> any: 402 | """设置监听CDP事件 403 | 404 | 应该先运行cdp 命令 激活对应的域,比如 Network.enable 405 | """ 406 | # b=Chromium(debug_port) 407 | def r(**event): 408 | DP.cdp_event_data.append({"event_name": event_name, "event_data": event}) 409 | 410 | try: 411 | DP.browser.latest_tab.driver.set_callback(event_name, r) 412 | return f"CDP event callback for '{event_name}' set successfully." 413 | except Exception as e: 414 | return e 415 | 416 | @mcp.tool() 417 | def get_cdp_event_data() -> list: 418 | """获取CDP事件回调函数收集到的数据""" 419 | return DP.cdp_event_data 420 | 421 | #region 监听网页接收的数据包 422 | @mcp.tool() 423 | def response_received_listener( 424 | mimeType: Literal[ 425 | # 文本类 426 | "text/html", 427 | "text/css", 428 | "text/javascript", 429 | "application/javascript", 430 | "text/plain", 431 | "text/xml", 432 | "text/csv", 433 | "application/json", 434 | 435 | # 应用类 436 | "application/octet-stream", 437 | "application/zip", 438 | "application/pdf", 439 | "multipart/form-data", 440 | "application/xml", 441 | 442 | # 图片类 443 | "image/jpeg", 444 | "image/png", 445 | "image/gif", 446 | "image/webp", 447 | "image/svg+xml", 448 | "image/x-icon", 449 | 450 | # 音视频类 451 | "audio/mpeg", 452 | "audio/ogg", 453 | "video/mp4", 454 | "video/webm", 455 | "video/ogg" 456 | ], 457 | url_include: str = "." 458 | ) -> any: 459 | ''' 460 | 开启监听网页接收的数据包, 461 | mimeType: 需要监听的接收的数据包的mimeType类型 462 | url_include: 需要监听的接收的数据包的url包含的关键字 463 | ''' 464 | t = DP.browser.latest_tab 465 | 466 | if mimeType not in DP.mime_types: 467 | # 如果mimeType不在列表中,返回错误信息 468 | return f"{mimeType} 错误!请在{DP.mime_types}列表中选择mimeType类型" 469 | 470 | t.run_cdp("Network.enable") 471 | 472 | def r(**event): 473 | _url = event.get("response", {}).get("url", "") 474 | _mimeType = event.get("response", {}).get("mimeType", "") 475 | 476 | if mimeType in _mimeType and url_include in _url: 477 | DP.listener_data.append({ 478 | "event_name": "Network.responseReceived", 479 | "event_data": event 480 | }) 481 | 482 | t.driver.set_callback("Network.responseReceived", r) 483 | 484 | return f"开启监听网页接收的数据包, url包含关键字:{url_include},mimeType:{mimeType}" 485 | 486 | 487 | @mcp.tool() 488 | def response_received_listener_stop(): 489 | """关闭监听网页发送的数据包""" 490 | t=DP.browser.latest_tab 491 | t.run_cdp("Network.disable") 492 | return f"监听网页发送的数据包关闭成功 Network.disable" 493 | 494 | @mcp.tool() 495 | def get_response_received_listener_data() -> list: 496 | """获取监听到的数据,返回数据列表""" 497 | return DP.listener_data 498 | 499 | 500 | #region 截图 501 | @mcp.tool() 502 | def get_current_tab_screenshot() -> bytes: 503 | """ 504 | 获取当前标签页的网页截图 505 | 506 | 507 | Returns: 508 | bytes: 截图的二进制数据 509 | """ 510 | """获取当前标签页的屏幕截图 """ 511 | t:ChromiumTab=DP.browser.latest_tab 512 | screenshot=t.get_screenshot(as_bytes='jpeg') 513 | i=Image(data=screenshot,format="jpeg") 514 | return i 515 | @mcp.tool() 516 | 517 | def get_current_tab_screenshot_as_file(path:str=".",name:str="screenshot.png") -> str: 518 | """ 519 | 获取当前标签页的屏幕截图并保存为文件 520 | 521 | Args: 522 | path (str): 截图保存路径,默认为当前目录 523 | 524 | Returns: 525 | str: 截图的文件路径 526 | """ 527 | 528 | screenshot=DP.browser.latest_tab.get_screenshot(path=path,name=name) 529 | return screenshot 530 | 531 | 532 | @mcp.tool() 533 | def get_current_tab_info(debug_port: int) -> int: 534 | """获取当前标签页的信息,包括url, title, id""" 535 | b=Chromium(debug_port) 536 | tab=b.latest_tab 537 | info={ 538 | "url":tab.url, 539 | "title":tab.title, 540 | "id":tab.tab_id, 541 | } 542 | return info 543 | 544 | @mcp.tool() 545 | def get_tab_list(debug_port: int) -> list: 546 | """获取当前浏览器的所有标签页的信息,包括url, title, id""" 547 | b=Chromium(debug_port) 548 | tabs=b.get_tabs 549 | tab_list=[] 550 | for tab in tabs: 551 | info={ 552 | "url":tab.url, 553 | "title":tab.title, 554 | "id":tab.id, 555 | } 556 | tab_list.append(info) 557 | return tab_list 558 | 559 | 560 | 561 | 562 | @mcp.tool() 563 | def page_down( ) -> Any: 564 | """向当前标签页发送按键 page_down""" 565 | 566 | tab=DP.browser.latest_tab 567 | result=tab.actions.type(Keys.PAGE_DOWN) 568 | return result 569 | 570 | @mcp.tool() 571 | def page_up( ) -> Any: 572 | """向当前标签页发送按键 page_up""" 573 | 574 | tab=DP.browser.latest_tab 575 | result=tab.actions.type(Keys.PAGE_UP) 576 | return result 577 | 578 | @mcp.tool() 579 | def arrow_down( ) -> Any: 580 | """向当前标签页发送按键 arrow_down""" 581 | 582 | tab=DP.browser.latest_tab 583 | result=tab.actions.type(Keys.DOWN) 584 | return result 585 | @mcp.tool() 586 | def arrow_up( ) -> Any: 587 | """向当前标签页发送按键 arrow_up""" 588 | 589 | tab=DP.browser.latest_tab 590 | result=tab.actions.type(Keys.UP) 591 | return result 592 | 593 | @mcp.tool() 594 | def wait(a:int) -> Any: 595 | """网页等待a秒""" 596 | 597 | tab=DP.browser.latest_tab 598 | result=tab.wait(a) 599 | return result 600 | 601 | @mcp.tool() 602 | def get_dom_tree(depth:int) -> str: 603 | """获取当前标签页的DOM树结构信息 604 | 605 | Args: 606 | depth (int): 指定获取DOM树的深度 607 | 608 | Returns: 609 | str: 返回DOM树的JSON结构数据 610 | """ 611 | """获取当前标签页的DOM树的信息""" 612 | 613 | tab=DP.browser.latest_tab 614 | tab.run_cdp("DOM.enable") 615 | # 获取DOM树 616 | result=tab.run_cdp("DOM.getDocument",depth=depth) 617 | return result 618 | 619 | # 合并重复的元素获取接口,统一用 get_elements_by_tagname,并保留兼容性接口 620 | @mcp.tool() 621 | def get_elements_info_by_tagname(tagname: str) -> list: 622 | """获取当前标签页中所有指定tagname的元素信息,定位元素时优先使用 ,参数是tagname""" 623 | tab = DP.browser.latest_tab 624 | elements = tab.eles(f'tag:{tagname}') 625 | return elements 626 | 627 | 628 | 629 | @mcp.tool() 630 | def get_input_elements_info() -> list: 631 | """获取当前标签页中所有input元素标签的信息,定位元素时优先使用""" 632 | return get_elements_info_by_tagname('input') 633 | 634 | @mcp.tool() 635 | def get_button_elements_info() -> list: 636 | """获取当前标签页中所有button元素标签的信息,定位元素时优先使用""" 637 | return get_elements_info_by_tagname('button') 638 | 639 | @mcp.tool() 640 | def get_a_elements_info() -> list: 641 | """获取当前标签页中所有a元素标签的信息,定位元素时优先使用""" 642 | return get_elements_info_by_tagname('a') 643 | 644 | # 根据关键词,获取当前标签中的所有文本节点包含这个关键词的元素列表,返回元素列表 645 | @mcp.tool() 646 | def get_elements_info_by_keyword(keyword: str) -> list: 647 | """根据关键词获取当前标签页中包含该关键词的文本节点的所有元素列表 定位元素时优先使用 648 | 649 | Args: 650 | keyword (str): 要搜索的关键词 651 | 652 | Returns: 653 | list: 返回包含关键词的元素列表 654 | """ 655 | tab = DP.browser.latest_tab 656 | # 获取所有文本节点 657 | text_nodes = tab('t:body').eles(keyword) 658 | # 筛选包含关键词的文本节点 659 | return text_nodes 660 | 661 | 662 | class Use: 663 | @staticmethod 664 | def extract_text(s): 665 | # 直接使用正则表达式提取并返回结果 666 | return ''.join(re.findall(r'(?<=>)(.+?)(?=<)', s)) 667 | 668 | @staticmethod 669 | def extract_attrs_value(input_string): 670 | # 直接返回匹配结果 671 | return re.findall(r'"[^"]+"', input_string) 672 | 673 | @staticmethod 674 | def extract_attrs_name(input_string): 675 | # 改进正则表达式以更精确地匹配属性名 676 | return re.findall(r'\b\w+(?==")', input_string) 677 | 678 | @staticmethod 679 | def extract_innertext(input_string): 680 | # 使用正则表达式简化内部文本提取 681 | match = re.search(r'>(.*?)<', input_string) 682 | return match.group(1) if match else '' 683 | 684 | @staticmethod 685 | def raw(input_str): 686 | input_str = input_str.strip() 687 | tag_name_match = re.match(r'<(\w+)', input_str) 688 | tag_name = tag_name_match.group(1) if tag_name_match else '' 689 | 690 | tag_attr_values = Use.extract_attrs_value(input_str) 691 | tag_attr_names = Use.extract_attrs_name(input_str) 692 | 693 | attr_all = ''.join([f'@@{name}={value}' for name, value in zip(tag_attr_names, tag_attr_values)]) 694 | attr_all = attr_all.replace('"', '') 695 | 696 | txt = Use.extract_text(input_str) 697 | tag_txt = f'@@text()={txt}' if txt else '' 698 | 699 | transformed_str = f'tag:{tag_name}{attr_all}{tag_txt}' 700 | print(transformed_str) 701 | return transformed_str 702 | 703 | 704 | def main(): 705 | # 启动MCP服务器 706 | print("DrissionPage MCP server is running...") 707 | mcp.run(transport='stdio') 708 | 709 | 710 | if __name__ == "__main__": 711 | main() 712 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from typing import Any,Literal 5 | import re 6 | from pathlib import Path 7 | from DrissionPage import Chromium,ChromiumOptions 8 | from mcp.server.fastmcp import FastMCP,Image,Context 9 | 10 | from DrissionPage.items import SessionElement, ChromiumElement, ShadowRoot, NoneElement, ChromiumTab, MixTab, ChromiumFrame 11 | from DrissionPage.common import Keys 12 | 13 | 14 | 提示=''' 15 | DrissionPage MCP 是一个基于 DrissionPage 和 FastMCP 的浏览器自动化MCP server服务器,它提供了一系列强大的浏览器操作 API,让您能够轻松通过AI实现网页自动化操作。 16 | 点击元素前,需要先获取页面所有可点击元素的信息,使用get_all_clickable_elements()方法。 17 | 输入元素前,需要先获取页面所有可输入元素的信息,使用get_all_input_elements()方法。 18 | 19 | ''' 20 | 21 | 22 | 23 | #region DrissionPageMCP 24 | class DrissionPageMCP(): 25 | def __init__(self): 26 | self.browser = None 27 | self.session = None 28 | self.current_tab = None 29 | self.current_frame = None 30 | self.current_shadow_root = None 31 | self.cdp_event_data = [] 32 | self.response_listener_data=[] 33 | 34 | def test(self): 35 | return "test" 36 | def get_DrissionPage_code_guide(self)-> str: 37 | """ 获取 DrissionPage 代码指南""" 38 | with open(Path(__file__).parent / "DrissionPage_code_guide.md", "r", encoding="utf-8") as f: 39 | return f.read() 40 | # return "1.0.3" 41 | def get_version(self)-> str: 42 | """ 获取版本号""" 43 | return "1.0.5" 44 | async def connect_or_open_browser(self, config: dict={'debug_port':9222}) -> dict: 45 | """ 46 | 用DrissionPage 打开或接管已打开的浏览器,参数通过字典传递。 47 | 必要参数: 48 | config (dict): 可选键包括 、debug_port、browser_path、headless 49 | 返回: 50 | dict: 浏览器信息 51 | """ 52 | co = ChromiumOptions() 53 | if config.get("debug_port"): 54 | co.set_local_port(config["debug_port"]) 55 | if config.get("browser_path"): 56 | co.set_browser_path(config["browser_path"]) 57 | if config.get("headless", False): 58 | co.headless(True) 59 | 60 | self.browser = Chromium(co) 61 | tab = self.browser.latest_tab 62 | 63 | return { 64 | "browser_address": self.browser._chromium_options.address, 65 | "latest_tab_title": tab.title, 66 | "latest_tab_id": tab.tab_id, 67 | "等价Python代码":f''' 68 | from DrissionPage import Chromium, ChromiumOptions 69 | form DrissionPage.common import Keys 70 | # 创建配置对象 71 | co = ChromiumOptions() 72 | co.set_local_port({config["debug_port"]}) 73 | # 创建浏览器对象,浏览器对象不能打开网址,只有标签页对象才能打开网址 74 | browser = Chromium(co) 75 | # 获取最新标签页 76 | tab = browser.latest_tab 77 | ''' 78 | } 79 | 80 | async def new_tab(self, url: str) -> str: 81 | """用DrissionPage 控制的浏览器,打开新标签页并 打开一个网址""" 82 | tab = self.browser.new_tab(url) 83 | return {"title": tab.title, "tab_id": tab.tab_id, "url": tab.url,"dom":self.getSimplifiedDomTree(), 84 | "等价Python代码":f''' 85 | tab = browser.new_tab('{url}') 86 | ''' } 87 | 88 | def wait(self, a:int) : 89 | """等待a秒""" 90 | self.browser.latest_tab.wait(a) 91 | return {"rsult":f"等待{a}秒成功", "等价Python代码":f"tab.wait({a})"} 92 | 93 | async def get(self,url:str)->str: 94 | """在当前标签页打开一个网址""" 95 | if not self.browser: 96 | await self.connect_or_open_browser() 97 | # return "请先打开或者连接浏览器" 98 | self.lastest_tab.get(url) 99 | tab=self.browser.latest_tab 100 | return {"title": tab.title, "tab_id": tab.tab_id, "url": tab.url,"dom":self.getSimplifiedDomTree(),"等价Python代码":f'''tab.get('{url}')'''} 101 | 102 | 103 | 104 | #region 上传和下载 105 | def download_file(self, url: str, path: str, rename: str) -> str: 106 | """控制浏览器下载文件到指定路径 107 | 108 | Args: 109 | url (str): 文件的URL地址 110 | path (str): 文件保存的路径 111 | rename (str): 重命名文件名 112 | 113 | Returns: 114 | str: 下载结果信息 115 | """ 116 | tab = self.lastest_tab 117 | result = tab.download(file_url=url, save_path=path, rename=rename) 118 | return str(result) 119 | 120 | def upload_file(self, file_path: str) -> str: 121 | """点击当网页上的 元素触发上传文件的操作,上传file_path文件到当前网页 122 | 123 | Args: 124 | file_path (str): 要上传的文件路径 125 | 126 | Returns: 127 | str: 上传结果信息,如果元素不存在则返回错误信息 128 | """ 129 | x="//input[@type='file']" 130 | t:ChromiumTab=self.lastest_tab 131 | if e:= t(f"xpath:{x}"): 132 | t.set.upload_files(file_path) 133 | e.click(by_js=True) 134 | t.wait.upload_paths_inputted() 135 | return f"{file_path} 上传成功 {e}" 136 | else: 137 | return f"元素{x}不存在,无法触发上传文件" 138 | 139 | 140 | 141 | @property 142 | def lastest_tab(self) -> ChromiumTab: 143 | """获取最新标签页""" 144 | return self.browser.latest_tab 145 | 146 | def send_enter(self) -> str: 147 | """向当前页面发送 enter 回车键""" 148 | tab = self.browser.latest_tab 149 | try: 150 | result = tab.actions.type(Keys.ENTER) 151 | return {"result":f'{tab.title} 网页发送 enter 回车键成功', "等价Python代码":f"tab.actions.type(Keys.ENTER)"} 152 | except Exception as e: 153 | return f"{tab.title} 网页发送 enter 回车键失败" 154 | 155 | def getInputElementsInfo(self) -> list: 156 | """获取当前标签页的所有可进行输入操作的元素,对元素进行输入操作前优先使用这个方法""" 157 | tab = self.browser.latest_tab 158 | js_code=''' 159 | const inputElements = Array.from(document.querySelectorAll('input, select, textarea, button')); 160 | return inputElements.filter(el => !el.disabled); // 排除禁用的元素 161 | ''' 162 | elements = tab.run_js(js_code) 163 | return elements 164 | 165 | def click_by_xpath(self, xpath: str) -> dict: 166 | """通过xpath点击当前标签页中某个元素,最好先获取页面dom信息,再决定Xpath的写法""" 167 | 168 | locator = f"xpath:{xpath}" 169 | element = self.browser.latest_tab.ele(locator, timeout=3) 170 | result = {"locator": locator, "element": str(element), "click_result": element.click(), "等价Python代码":f"tab.ele('{locator}', timeout=3).click()"} 171 | return result 172 | 173 | def click_by_containing_text(self, content: str, index: int = None) : 174 | """ 175 | 根据包含指定文本的方式点击网页元素。 176 | 177 | 参数: 178 | content: 要查找的文本内容。 179 | index: 当匹配到多个元素时指定要点击的索引,默认不指定。 180 | 181 | 返回: 182 | 点击结果说明,或错误提示。 183 | """ 184 | 185 | # 获取包含指定文本的所有元素,等待最多 3 秒 186 | elements = self.browser.latest_tab.eles(content, timeout=3) 187 | 188 | # 如果没有匹配到任何元素,返回错误提示 189 | if len(elements) == 0: 190 | return f"元素{content}不存在,需要getInputElementsInfo先获取元素信息" 191 | 192 | # 如果只找到一个元素,直接点击它 193 | if len(elements) == 1: 194 | self.lastest_tab(content).click() 195 | return f" 点击成功" 196 | 197 | # 如果找到多个元素 198 | if len(elements) > 1: 199 | # 如果未指定 index,提示用户提供索引 200 | if index is None: 201 | return f"元素{content}存在多个,请调整 index 参数,index=0表示第一个元素,{elements}" 202 | else: 203 | # 根据指定索引点击对应的元素 204 | elements[index].click() 205 | return f" 点击成功" 206 | 207 | 208 | 209 | def input_by_xapth(self, xpath: str, input_value: str, clear_first: bool = True) : 210 | """通过xpath给当前标签页中某个元素输入内容,最好先判断元素是否存在 211 | 212 | Args: 213 | xpath (str): 元素的XPath表达式 214 | input_value (str): 要输入的内容 215 | clear_first (bool): 是否先清除已有内容,默认为True 216 | 217 | Returns: 218 | Any: 输入操作的结果,如果元素不存在则返回错误信息 219 | """ 220 | locator = f"xpath:{xpath}" 221 | if e := self.browser.latest_tab.ele(locator, timeout=4): 222 | result = {"locator": locator, "result": e.input(input_value, clear=clear_first), "等价Python代码":f"tab.ele('{locator}', timeout=4).input({input_value}, clear={clear_first})"} 223 | return result 224 | else: 225 | return f"元素{locator}不存在,需要getInputElementsInfo先获取元素信息" 226 | 227 | def get_body_text(self) -> str: 228 | """获取当前标签页的body的文本内容""" 229 | 230 | tab = self.browser.latest_tab 231 | body_text = tab('t:body').text 232 | r={"body_text":body_text,"等价Python代码":f"tab('t:body').text"} 233 | return r 234 | def run_js(self, js_code: str) : 235 | """ 236 | 在当前标签页中运行JavaScript代码并返回执行结果 237 | 查找网页元素,获取元素信息,操作网页元素优先使用这个方法 238 | 239 | Args: 240 | js_code (str): 要执行的JavaScript代码 241 | 242 | Returns: 243 | Any: JavaScript代码执行结果 244 | 245 | Note: 246 | 想要获取执行的js代码的返回值,可以在js_code中使用return语句。 247 | 想要获取异步函数的返回值,可以参考下面代码 248 | return (async (url) => { 249 | const response = await fetch(url); 250 | const data = await response.json(); 251 | return data; 252 | })("https://www.baidu.com/"); 253 | """ 254 | tab = self.browser.latest_tab 255 | result = tab.run_js(js_code) 256 | r={"result":result,"等价Python代码":f"r=tab.run_js('{js_code}')"} 257 | return r 258 | 259 | 260 | def run_cdp( self,cmd, **cmd_args) : 261 | """在当前标签页中运行谷歌CDP协议代码并获取结果 262 | 263 | Args: 264 | 265 | cmd: CDP协议命令 266 | **cmd_args: CDP命令参数 267 | 268 | Returns: 269 | Any: CDP命令执行结果 270 | 271 | Note: 272 | 举例1说明 run_cdp('Page.stopLoading') 273 | 举例2说明 run_cdp('Page.navigate', url='https://example.com') 274 | """ 275 | result=self.browser.latest_tab.run_cdp(cmd, **cmd_args) 276 | return result 277 | def listen_cdp_event(self,event_name: str) : 278 | """设置监听CDP事件 279 | 280 | 应该先运行cdp 命令 激活对应的域,比如 Network.enable 281 | """ 282 | # b=Chromium(debug_port) 283 | def r(**event): 284 | self.cdp_event_data.append({"event_name": event_name, "event_data": event}) 285 | 286 | try: 287 | self.browser.latest_tab.driver.set_callback(event_name, r) 288 | return f"CDP event callback for '{event_name}' set successfully." 289 | except Exception as e: 290 | return e 291 | 292 | def get_cdp_event_data(self) -> list: 293 | """获取CDP事件回调函数收集到的数据""" 294 | return self.cdp_event_data 295 | 296 | 297 | 298 | #region 监听网页接收的数据包 299 | 300 | def get_url_with_response_listener(self, 301 | tab_url: str, 302 | mimeType: Literal[ 303 | # 文本类 304 | "text/html", 305 | "text/css", 306 | "text/javascript", 307 | "application/javascript", 308 | "text/plain", 309 | "text/xml", 310 | "text/csv", 311 | "application/json", 312 | 313 | # 应用类 314 | "application/octet-stream", 315 | "application/zip", 316 | "application/pdf", 317 | "multipart/form-data", 318 | "application/xml", 319 | 320 | # 图片类 321 | "image/jpeg", 322 | "image/png", 323 | "image/gif", 324 | "image/webp", 325 | "image/svg+xml", 326 | "image/x-icon", 327 | 328 | # 音视频类 329 | "audio/mpeg", 330 | "audio/ogg", 331 | "video/mp4", 332 | "video/webm", 333 | "video/ogg" 334 | ], 335 | url_include: str = "." 336 | ) : 337 | ''' 338 | 开启一个新的标签页,设置监听,访问tab_url, 339 | tab_url: 被监听的标签页的url 340 | mimeType: 需要监听的接收的数据包的mimeType类型 341 | url_include: 需要监听的接收的数据包的url包含的关键字 342 | refresh: 是否刷新页面, 343 | ''' 344 | t = self.browser.new_tab(tab_url) 345 | 346 | t.run_cdp("Network.enable") 347 | 348 | def r(**event): 349 | _url = event.get("response", {}).get("url", "") 350 | _mimeType = event.get("response", {}).get("mimeType", "") 351 | 352 | if mimeType in _mimeType and url_include in _url: 353 | self.response_listener_data.append({ 354 | "event_name": "Network.responseReceived", 355 | "event_data": event 356 | }) 357 | 358 | t.driver.set_callback("Network.responseReceived", r) 359 | t.get(tab_url) 360 | 361 | return f"开启监听{tab_url}, 数据包url包含关键字:{url_include},mimeType:{mimeType}" 362 | 363 | 364 | 365 | def response_listener_stop(self,clear_data:bool=False) -> str: 366 | """关闭监听网页发送的数据包""" 367 | t=self.browser.latest_tab 368 | t.run_cdp("Network.disable") 369 | if clear_data: 370 | self.response_listener_data = [] 371 | return f"监听网页发送的数据包关闭成功 ,是否清空数据: {clear_data}" 372 | 373 | 374 | def get_response_listener_data(self) -> list: 375 | """获取监听到的数据,返回数据列表""" 376 | return self.response_listener_data 377 | 378 | def get_current_tab_screenshot(self) -> bytes: 379 | """ 380 | 获取当前标签页的网页截图 381 | 382 | Returns: 383 | bytes: 截图的二进制数据 384 | """ 385 | t:ChromiumTab=self.browser.latest_tab 386 | screenshot=t.get_screenshot(as_bytes='jpeg') 387 | return screenshot 388 | 389 | def get_current_tab_screenshot_as_file(self,path:str=".",name:str="screenshot.png") -> str: 390 | """ 391 | 获取当前标签页的屏幕截图并保存为文件 392 | 393 | Args: 394 | path (str): 截图保存路径,默认为当前目录 395 | 396 | Returns: 397 | str: 截图的文件路径 398 | """ 399 | 400 | screenshot=self.browser.latest_tab.get_screenshot(path=path,name=name) 401 | return screenshot 402 | 403 | def get_current_tab_info(self) -> dict: 404 | """获取当前标签页的信息,包括url, title, id""" 405 | tab =self.browser.latest_tab 406 | info = { 407 | "url": tab.url, 408 | "title": tab.title, 409 | "id": tab.tab_id, 410 | } 411 | return info 412 | 413 | def send_key(self, key: Literal["Enter, Backspace, HOME, END, PAGE_UP, PAGE_DOWN, DOWN, UP, LEFT, RIGHT, ESC, Ctrl+C, Ctrl+V, Ctrl+A, Delete"]) -> str: 414 | """向当前标签页发送特殊按键""" 415 | tab = self.browser.latest_tab 416 | k={"Enter": Keys.ENTER, 417 | "Backspace": Keys.BACKSPACE, 418 | "HOME": Keys.HOME, 419 | "END": Keys.END, 420 | "PAGE_UP": Keys.PAGE_UP, 421 | "PAGE_DOWN": Keys.PAGE_DOWN, 422 | "DOWN": Keys.DOWN, 423 | "UP": Keys.UP, 424 | "LEFT": Keys.LEFT, 425 | "RIGHT": Keys.RIGHT, 426 | "ESC": Keys.ESCAPE, 427 | "Ctrl+C": Keys.CTRL_C, 428 | "Ctrl+V": Keys.CTRL_V, 429 | "Ctrl+A": Keys.CTRL_A, 430 | "Delete": Keys.DELETE,} 431 | try: 432 | result = tab.actions.type(k.get(key)) 433 | return f"{tab.title} 网页发送 {key} 键成功" 434 | except Exception as e: 435 | return f"{tab.title} 网页发送 {key} 键失败" 436 | 437 | def getSimplifiedDomTree2(self) -> dict: 438 | """获取当前标签页的简化版DOM树""" 439 | from CodeBox import domTreeToJson 440 | tab = self.browser.latest_tab 441 | dom_tree = tab.run_js(domTreeToJson) 442 | return dom_tree 443 | 444 | def getSimplifiedDomTree(self): 445 | tab=self.browser.latest_tab 446 | tab.run_cdp('Accessibility.enable') 447 | tree = tab.run_cdp('Accessibility.getFullAXTree') 448 | tempAXTree = AXTreeFormatter(tree) 449 | tempAXTree.parse_tree() 450 | return tempAXTree.all_data 451 | 452 | #region 拖动 453 | 454 | def move_to(self,xpath:str) -> dict: 455 | """鼠标移动悬停到指定xpath的元素上""" 456 | tab = self.browser.latest_tab 457 | locator = f"xpath:{xpath}" 458 | element = tab.ele(locator, timeout=3) 459 | if element: 460 | element.hover() 461 | result = {"locator": locator, "element": str(element)} 462 | return result 463 | else: 464 | return f"元素{locator}不存在,需要getSimplifiedDomTree先获取元素信息" 465 | def drag(self,xpath:str, offset_x: int, offset_y: int, duration: int = 1000) -> dict: 466 | 467 | """ 468 | 将元素拖动到指定偏移位置 469 | 470 | Args: 471 | xpath: 要拖动的元素xpath路径 472 | offset_x: x轴偏移量(像素) 473 | offset_y: y轴偏移量(像素) 474 | duration: 拖动持续时间(毫秒),默认为1000 475 | 476 | Returns: 477 | dict: 包含偏移量和持续时间的字典,格式为{"offset_x": int, "offset_y": int, "duration": int} 478 | 或 str: 当元素不存在时返回错误信息 479 | 480 | Raises: 481 | 无显式抛出异常,但内部可能因元素不存在而返回错误信息 482 | """ 483 | tab = self.browser.latest_tab 484 | if e:=tab.ele(f'xpath:{xpath}', timeout=3): 485 | tab.actions.move_to(e).wait(0.5).hold().move(offset_x, offset_y).release() 486 | result = {"offset_x": offset_x, "offset_y": offset_y, "duration": duration} 487 | return result 488 | else: 489 | return f"元素{xpath}不存在,需要getSimplifiedDomTree先获取元素信息" 490 | 491 | #region AXtree 492 | class AXTreeFormatter: 493 | def __init__(self, raw_data): 494 | self.nodes_map = {} 495 | self.root_id = None 496 | self._build_lookup(raw_data.get('nodes', [])) 497 | self.all_data = "" 498 | 499 | def _build_lookup(self, nodes_list): 500 | all_child_ids = set() 501 | 502 | for node in nodes_list: 503 | self.nodes_map[node['nodeId']] = node 504 | if 'childIds' in node: 505 | for child_id in node['childIds']: 506 | all_child_ids.add(child_id) 507 | 508 | for node_id in self.nodes_map: 509 | if node_id not in all_child_ids: 510 | self.root_id = node_id 511 | break 512 | 513 | def _get_value(self, obj, key): 514 | """获取属性值""" 515 | if not obj: 516 | return None 517 | target = obj.get(key) 518 | if isinstance(target, dict) and 'value' in target: 519 | return target['value'] 520 | return target 521 | 522 | def _get_properties(self, node): 523 | """提取 tagName, id, class""" 524 | result = {} 525 | properties = node.get('properties', []) 526 | 527 | for prop in properties: 528 | name = prop.get('name') 529 | value = prop.get('value', {}) 530 | 531 | if isinstance(value, dict): 532 | prop_value = value.get('value') 533 | else: 534 | prop_value = value 535 | 536 | # 只提取我们关心的属性 537 | if name in ['htmlTag', 'id', 'class']: 538 | result[name] = prop_value 539 | 540 | return result 541 | 542 | def _format_node(self, node): 543 | """格式化节点信息""" 544 | role = self._get_value(node, 'role') or "Unknown" 545 | name = self._get_value(node, 'name') or "" 546 | 547 | props = self._get_properties(node) 548 | tag = props.get('htmlTag', '') 549 | node_id = props.get('id', '') 550 | node_class = props.get('class', '') 551 | 552 | # 构建简洁的输出 - 优先显示 HTML 标签 553 | parts = [] 554 | 555 | if tag: 556 | # 如果有标签名,就显示标签,role 放在后面(可选) 557 | parts.append(f"<{tag}>") 558 | # 如果 role 不是 generic,也显示出来 559 | if role != 'generic': 560 | parts.append(f"[{role}]") 561 | else: 562 | # 没有标签就显示 role 563 | parts.append(role) 564 | 565 | if node_id: 566 | parts.append(f"#{node_id}") 567 | 568 | if node_class: 569 | # class 太长就截断 570 | if len(node_class) > 40: 571 | node_class = node_class[:37] + "..." 572 | parts.append(f".{node_class}") 573 | 574 | if name: 575 | # 文本内容太长就截断 576 | if len(name) > 50: 577 | name = name[:47] + "..." 578 | parts.append(f'"{name}"') 579 | 580 | return " ".join(parts) 581 | 582 | def print_tree(self, node_id=None, level=0): 583 | if node_id is None: 584 | node_id = self.root_id 585 | 586 | node = self.nodes_map.get(node_id) 587 | if not node: 588 | return 589 | 590 | indent = " " * level 591 | self.all_data += f"{indent}- {self._format_node(node)}\n" 592 | 593 | # 递归子节点 594 | for child_id in node.get('childIds', []): 595 | self.print_tree(child_id, level + 1) 596 | def parse_tree(self, node_id=None, level=0): 597 | if node_id is None: 598 | node_id = self.root_id 599 | 600 | node = self.nodes_map.get(node_id) 601 | if not node: 602 | return 603 | 604 | indent = " " * level 605 | self.all_data += f"{indent}- {self._format_node(node)}\n" 606 | 607 | # 递归子节点 608 | for child_id in node.get('childIds', []): 609 | self.parse_tree(child_id, level + 1) 610 | 611 | 612 | 613 | 614 | 615 | #region 初始化mcp 616 | mcp = FastMCP("DrissionPageMCP", log_level="ERROR",instructions=提示) 617 | b=DrissionPageMCP() 618 | 619 | mcp.add_tool(b.get_version) 620 | mcp.add_tool(b.get_DrissionPage_code_guide) 621 | mcp.add_tool(b.connect_or_open_browser) 622 | mcp.add_tool(b.new_tab) 623 | mcp.add_tool(b.wait) 624 | mcp.add_tool(b.get) 625 | mcp.add_tool(b.download_file) 626 | mcp.add_tool(b.upload_file) 627 | mcp.add_tool(b.send_enter) 628 | mcp.add_tool(b.getInputElementsInfo) 629 | mcp.add_tool(b.click_by_xpath) 630 | mcp.add_tool(b.click_by_containing_text) 631 | mcp.add_tool(b.input_by_xapth) 632 | mcp.add_tool(b.get_body_text) 633 | mcp.add_tool(b.run_js) 634 | mcp.add_tool(b.run_cdp) 635 | mcp.add_tool(b.listen_cdp_event) 636 | mcp.add_tool(b.get_cdp_event_data) 637 | mcp.add_tool(b.get_url_with_response_listener) 638 | mcp.add_tool(b.response_listener_stop) 639 | mcp.add_tool(b.get_response_listener_data) 640 | mcp.add_tool(b.get_current_tab_screenshot) 641 | mcp.add_tool(b.get_current_tab_screenshot_as_file) 642 | mcp.add_tool(b.get_current_tab_info) 643 | mcp.add_tool(b.send_key) 644 | mcp.add_tool(b.getSimplifiedDomTree) 645 | 646 | mcp.add_tool(b.move_to) 647 | mcp.add_tool(b.drag) 648 | 649 | #region 保存数据到sqlite 650 | from ToolBox import save_dict_to_sqlite 651 | mcp.add_tool(save_dict_to_sqlite) 652 | 653 | 654 | 655 | 656 | def main(): 657 | # 启动MCP服务器 658 | print("DrissionPage MCP server is running...") 659 | mcp.run(transport='stdio') 660 | 661 | 662 | if __name__ == "__main__": 663 | main() 664 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 2 3 | requires-python = ">=3.13" 4 | 5 | [[package]] 6 | name = "annotated-types" 7 | version = "0.7.0" 8 | source = { registry = "https://pypi.org/simple" } 9 | sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } 10 | wheels = [ 11 | { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, 12 | ] 13 | 14 | [[package]] 15 | name = "anyio" 16 | version = "4.9.0" 17 | source = { registry = "https://pypi.org/simple" } 18 | dependencies = [ 19 | { name = "idna" }, 20 | { name = "sniffio" }, 21 | ] 22 | sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } 23 | wheels = [ 24 | { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, 25 | ] 26 | 27 | [[package]] 28 | name = "certifi" 29 | version = "2025.4.26" 30 | source = { registry = "https://pypi.org/simple" } 31 | sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" } 32 | wheels = [ 33 | { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" }, 34 | ] 35 | 36 | [[package]] 37 | name = "charset-normalizer" 38 | version = "3.4.2" 39 | source = { registry = "https://pypi.org/simple" } 40 | sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } 41 | wheels = [ 42 | { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, 43 | { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, 44 | { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, 45 | { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, 46 | { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, 47 | { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, 48 | { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, 49 | { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, 50 | { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, 51 | { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, 52 | { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, 53 | { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, 54 | { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, 55 | { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, 56 | ] 57 | 58 | [[package]] 59 | name = "click" 60 | version = "8.2.1" 61 | source = { registry = "https://pypi.org/simple" } 62 | dependencies = [ 63 | { name = "colorama", marker = "sys_platform == 'win32'" }, 64 | ] 65 | sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } 66 | wheels = [ 67 | { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, 68 | ] 69 | 70 | [[package]] 71 | name = "colorama" 72 | version = "0.4.6" 73 | source = { registry = "https://pypi.org/simple" } 74 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } 75 | wheels = [ 76 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, 77 | ] 78 | 79 | [[package]] 80 | name = "cssselect" 81 | version = "1.3.0" 82 | source = { registry = "https://pypi.org/simple" } 83 | sdist = { url = "https://files.pythonhosted.org/packages/72/0a/c3ea9573b1dc2e151abfe88c7fe0c26d1892fe6ed02d0cdb30f0d57029d5/cssselect-1.3.0.tar.gz", hash = "sha256:57f8a99424cfab289a1b6a816a43075a4b00948c86b4dcf3ef4ee7e15f7ab0c7", size = 42870, upload-time = "2025-03-10T09:30:29.638Z" } 84 | wheels = [ 85 | { url = "https://files.pythonhosted.org/packages/ee/58/257350f7db99b4ae12b614a36256d9cc870d71d9e451e79c2dc3b23d7c3c/cssselect-1.3.0-py3-none-any.whl", hash = "sha256:56d1bf3e198080cc1667e137bc51de9cadfca259f03c2d4e09037b3e01e30f0d", size = 18786, upload-time = "2025-03-10T09:30:28.048Z" }, 86 | ] 87 | 88 | [[package]] 89 | name = "datarecorder" 90 | version = "3.6.2" 91 | source = { registry = "https://pypi.org/simple" } 92 | dependencies = [ 93 | { name = "openpyxl" }, 94 | ] 95 | sdist = { url = "https://files.pythonhosted.org/packages/b4/a1/43d52eada4e1364f8d1a5da99b855798f945228ffb7dfb199f675c1d2c2b/DataRecorder-3.6.2.tar.gz", hash = "sha256:8c9024736692af68b947fd884589e685c4f27bc29d031b81164457b09c60e1e5", size = 29567, upload-time = "2024-10-15T08:58:36.923Z" } 96 | wheels = [ 97 | { url = "https://files.pythonhosted.org/packages/33/05/f0064d93a0b922ea7ad5d40a0dfaac45de37f783a85a2900e6ce8a01dbc3/DataRecorder-3.6.2-py3-none-any.whl", hash = "sha256:41ad022c4c1db58a0f4236f6a4991d1f98cd76ec049484b172bbb3040455e5ff", size = 37539, upload-time = "2024-10-15T08:58:35.419Z" }, 98 | ] 99 | 100 | [[package]] 101 | name = "downloadkit" 102 | version = "2.0.7" 103 | source = { registry = "https://pypi.org/simple" } 104 | dependencies = [ 105 | { name = "datarecorder" }, 106 | { name = "requests" }, 107 | ] 108 | sdist = { url = "https://files.pythonhosted.org/packages/6b/35/9bbaed54f5d170b1fd8bcfdd3ce5e24ea71f4cac5026a37e6b21ab69726e/DownloadKit-2.0.7.tar.gz", hash = "sha256:601e423d1d4d0bd3e933524d06c913e744577d455990ec20afc3fb1f79aea9a7", size = 17488, upload-time = "2024-11-30T01:24:30.539Z" } 109 | wheels = [ 110 | { url = "https://files.pythonhosted.org/packages/1b/89/979ad2407eb8e0a2ac67461b7bec7db8627218fe3ba287db49636ca0a60c/DownloadKit-2.0.7-py3-none-any.whl", hash = "sha256:55b142d26b4e7ae01fff2181991c92aca9b895f75e2cd8a54f4a3b48c50b90b4", size = 21706, upload-time = "2024-11-30T01:24:17.599Z" }, 111 | ] 112 | 113 | [[package]] 114 | name = "drissionpage" 115 | version = "4.1.0.18" 116 | source = { registry = "https://pypi.org/simple" } 117 | dependencies = [ 118 | { name = "click" }, 119 | { name = "cssselect" }, 120 | { name = "downloadkit" }, 121 | { name = "lxml" }, 122 | { name = "psutil" }, 123 | { name = "requests" }, 124 | { name = "tldextract" }, 125 | { name = "websocket-client" }, 126 | ] 127 | sdist = { url = "https://files.pythonhosted.org/packages/3f/54/9e99d96c7a5909a7f2ecc7bdc2978702ca5df20b45ea7f331b306c7e9b57/drissionpage-4.1.0.18.tar.gz", hash = "sha256:ea3193c628bed6f6f11b401d1e07161355ed3060d9c9ce12163df381bd09bf32", size = 206670, upload-time = "2025-03-24T15:39:23.039Z" } 128 | wheels = [ 129 | { url = "https://files.pythonhosted.org/packages/e6/83/7ed71f4e0c8d60c9aefb24f0bb95f8a8b14e090718cf3fff8a0083d33164/DrissionPage-4.1.0.18-py3-none-any.whl", hash = "sha256:5492189161e6bde036737aab0874dec7723c38153cb22815f24dba88a2fdfa57", size = 256899, upload-time = "2025-03-24T15:39:21.261Z" }, 130 | ] 131 | 132 | [[package]] 133 | name = "drssionpagemcp" 134 | version = "0.1.0" 135 | source = { virtual = "." } 136 | dependencies = [ 137 | { name = "drissionpage" }, 138 | { name = "fastmcp" }, 139 | ] 140 | 141 | [package.metadata] 142 | requires-dist = [ 143 | { name = "drissionpage", specifier = ">=4.1.0.18" }, 144 | { name = "fastmcp", specifier = ">=2.4.0" }, 145 | ] 146 | 147 | [[package]] 148 | name = "et-xmlfile" 149 | version = "2.0.0" 150 | source = { registry = "https://pypi.org/simple" } 151 | sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" } 152 | wheels = [ 153 | { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, 154 | ] 155 | 156 | [[package]] 157 | name = "exceptiongroup" 158 | version = "1.3.0" 159 | source = { registry = "https://pypi.org/simple" } 160 | sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } 161 | wheels = [ 162 | { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, 163 | ] 164 | 165 | [[package]] 166 | name = "fastmcp" 167 | version = "2.4.0" 168 | source = { registry = "https://pypi.org/simple" } 169 | dependencies = [ 170 | { name = "exceptiongroup" }, 171 | { name = "httpx" }, 172 | { name = "mcp" }, 173 | { name = "openapi-pydantic" }, 174 | { name = "python-dotenv" }, 175 | { name = "rich" }, 176 | { name = "typer" }, 177 | { name = "websockets" }, 178 | ] 179 | sdist = { url = "https://files.pythonhosted.org/packages/50/11/2ccd6219eb65692a298e764fa84a15fd756e03c811c7ea217129d6ca545f/fastmcp-2.4.0.tar.gz", hash = "sha256:a08d812939d16c0d4490bdbdaf17ab136f1bdaa8ddcc14a37e33335727343c05", size = 1020290, upload-time = "2025-05-21T19:58:46.836Z" } 180 | wheels = [ 181 | { url = "https://files.pythonhosted.org/packages/ab/a4/26396706c1b48ebd051da4f7d321594207364077a5c308827722536b927c/fastmcp-2.4.0-py3-none-any.whl", hash = "sha256:fccd0768028a31eec488707fb6bbe4f8659f84ca0c206c4c32dd33947c0faae9", size = 101108, upload-time = "2025-05-21T19:58:45.738Z" }, 182 | ] 183 | 184 | [[package]] 185 | name = "filelock" 186 | version = "3.18.0" 187 | source = { registry = "https://pypi.org/simple" } 188 | sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } 189 | wheels = [ 190 | { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, 191 | ] 192 | 193 | [[package]] 194 | name = "h11" 195 | version = "0.16.0" 196 | source = { registry = "https://pypi.org/simple" } 197 | sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } 198 | wheels = [ 199 | { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, 200 | ] 201 | 202 | [[package]] 203 | name = "httpcore" 204 | version = "1.0.9" 205 | source = { registry = "https://pypi.org/simple" } 206 | dependencies = [ 207 | { name = "certifi" }, 208 | { name = "h11" }, 209 | ] 210 | sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } 211 | wheels = [ 212 | { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, 213 | ] 214 | 215 | [[package]] 216 | name = "httpx" 217 | version = "0.28.1" 218 | source = { registry = "https://pypi.org/simple" } 219 | dependencies = [ 220 | { name = "anyio" }, 221 | { name = "certifi" }, 222 | { name = "httpcore" }, 223 | { name = "idna" }, 224 | ] 225 | sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } 226 | wheels = [ 227 | { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, 228 | ] 229 | 230 | [[package]] 231 | name = "httpx-sse" 232 | version = "0.4.0" 233 | source = { registry = "https://pypi.org/simple" } 234 | sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624, upload-time = "2023-12-22T08:01:21.083Z" } 235 | wheels = [ 236 | { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819, upload-time = "2023-12-22T08:01:19.89Z" }, 237 | ] 238 | 239 | [[package]] 240 | name = "idna" 241 | version = "3.10" 242 | source = { registry = "https://pypi.org/simple" } 243 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } 244 | wheels = [ 245 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, 246 | ] 247 | 248 | [[package]] 249 | name = "lxml" 250 | version = "5.4.0" 251 | source = { registry = "https://pypi.org/simple" } 252 | sdist = { url = "https://files.pythonhosted.org/packages/76/3d/14e82fc7c8fb1b7761f7e748fd47e2ec8276d137b6acfe5a4bb73853e08f/lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd", size = 3679479, upload-time = "2025-04-23T01:50:29.322Z" } 253 | wheels = [ 254 | { url = "https://files.pythonhosted.org/packages/87/cb/2ba1e9dd953415f58548506fa5549a7f373ae55e80c61c9041b7fd09a38a/lxml-5.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0", size = 8110086, upload-time = "2025-04-23T01:46:52.218Z" }, 255 | { url = "https://files.pythonhosted.org/packages/b5/3e/6602a4dca3ae344e8609914d6ab22e52ce42e3e1638c10967568c5c1450d/lxml-5.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de", size = 4404613, upload-time = "2025-04-23T01:46:55.281Z" }, 256 | { url = "https://files.pythonhosted.org/packages/4c/72/bf00988477d3bb452bef9436e45aeea82bb40cdfb4684b83c967c53909c7/lxml-5.4.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76", size = 5012008, upload-time = "2025-04-23T01:46:57.817Z" }, 257 | { url = "https://files.pythonhosted.org/packages/92/1f/93e42d93e9e7a44b2d3354c462cd784dbaaf350f7976b5d7c3f85d68d1b1/lxml-5.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d", size = 4760915, upload-time = "2025-04-23T01:47:00.745Z" }, 258 | { url = "https://files.pythonhosted.org/packages/45/0b/363009390d0b461cf9976a499e83b68f792e4c32ecef092f3f9ef9c4ba54/lxml-5.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422", size = 5283890, upload-time = "2025-04-23T01:47:04.702Z" }, 259 | { url = "https://files.pythonhosted.org/packages/19/dc/6056c332f9378ab476c88e301e6549a0454dbee8f0ae16847414f0eccb74/lxml-5.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551", size = 4812644, upload-time = "2025-04-23T01:47:07.833Z" }, 260 | { url = "https://files.pythonhosted.org/packages/ee/8a/f8c66bbb23ecb9048a46a5ef9b495fd23f7543df642dabeebcb2eeb66592/lxml-5.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c", size = 4921817, upload-time = "2025-04-23T01:47:10.317Z" }, 261 | { url = "https://files.pythonhosted.org/packages/04/57/2e537083c3f381f83d05d9b176f0d838a9e8961f7ed8ddce3f0217179ce3/lxml-5.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff", size = 4753916, upload-time = "2025-04-23T01:47:12.823Z" }, 262 | { url = "https://files.pythonhosted.org/packages/d8/80/ea8c4072109a350848f1157ce83ccd9439601274035cd045ac31f47f3417/lxml-5.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60", size = 5289274, upload-time = "2025-04-23T01:47:15.916Z" }, 263 | { url = "https://files.pythonhosted.org/packages/b3/47/c4be287c48cdc304483457878a3f22999098b9a95f455e3c4bda7ec7fc72/lxml-5.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8", size = 4874757, upload-time = "2025-04-23T01:47:19.793Z" }, 264 | { url = "https://files.pythonhosted.org/packages/2f/04/6ef935dc74e729932e39478e44d8cfe6a83550552eaa072b7c05f6f22488/lxml-5.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982", size = 4947028, upload-time = "2025-04-23T01:47:22.401Z" }, 265 | { url = "https://files.pythonhosted.org/packages/cb/f9/c33fc8daa373ef8a7daddb53175289024512b6619bc9de36d77dca3df44b/lxml-5.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61", size = 4834487, upload-time = "2025-04-23T01:47:25.513Z" }, 266 | { url = "https://files.pythonhosted.org/packages/8d/30/fc92bb595bcb878311e01b418b57d13900f84c2b94f6eca9e5073ea756e6/lxml-5.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54", size = 5381688, upload-time = "2025-04-23T01:47:28.454Z" }, 267 | { url = "https://files.pythonhosted.org/packages/43/d1/3ba7bd978ce28bba8e3da2c2e9d5ae3f8f521ad3f0ca6ea4788d086ba00d/lxml-5.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b", size = 5242043, upload-time = "2025-04-23T01:47:31.208Z" }, 268 | { url = "https://files.pythonhosted.org/packages/ee/cd/95fa2201041a610c4d08ddaf31d43b98ecc4b1d74b1e7245b1abdab443cb/lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a", size = 5021569, upload-time = "2025-04-23T01:47:33.805Z" }, 269 | { url = "https://files.pythonhosted.org/packages/2d/a6/31da006fead660b9512d08d23d31e93ad3477dd47cc42e3285f143443176/lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82", size = 3485270, upload-time = "2025-04-23T01:47:36.133Z" }, 270 | { url = "https://files.pythonhosted.org/packages/fc/14/c115516c62a7d2499781d2d3d7215218c0731b2c940753bf9f9b7b73924d/lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f", size = 3814606, upload-time = "2025-04-23T01:47:39.028Z" }, 271 | ] 272 | 273 | [[package]] 274 | name = "markdown-it-py" 275 | version = "3.0.0" 276 | source = { registry = "https://pypi.org/simple" } 277 | dependencies = [ 278 | { name = "mdurl" }, 279 | ] 280 | sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } 281 | wheels = [ 282 | { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, 283 | ] 284 | 285 | [[package]] 286 | name = "mcp" 287 | version = "1.9.0" 288 | source = { registry = "https://pypi.org/simple" } 289 | dependencies = [ 290 | { name = "anyio" }, 291 | { name = "httpx" }, 292 | { name = "httpx-sse" }, 293 | { name = "pydantic" }, 294 | { name = "pydantic-settings" }, 295 | { name = "python-multipart" }, 296 | { name = "sse-starlette" }, 297 | { name = "starlette" }, 298 | { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, 299 | ] 300 | sdist = { url = "https://files.pythonhosted.org/packages/bc/8d/0f4468582e9e97b0a24604b585c651dfd2144300ecffd1c06a680f5c8861/mcp-1.9.0.tar.gz", hash = "sha256:905d8d208baf7e3e71d70c82803b89112e321581bcd2530f9de0fe4103d28749", size = 281432, upload-time = "2025-05-15T18:51:06.615Z" } 301 | wheels = [ 302 | { url = "https://files.pythonhosted.org/packages/a5/d5/22e36c95c83c80eb47c83f231095419cf57cf5cca5416f1c960032074c78/mcp-1.9.0-py3-none-any.whl", hash = "sha256:9dfb89c8c56f742da10a5910a1f64b0d2ac2c3ed2bd572ddb1cfab7f35957178", size = 125082, upload-time = "2025-05-15T18:51:04.916Z" }, 303 | ] 304 | 305 | [[package]] 306 | name = "mdurl" 307 | version = "0.1.2" 308 | source = { registry = "https://pypi.org/simple" } 309 | sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } 310 | wheels = [ 311 | { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, 312 | ] 313 | 314 | [[package]] 315 | name = "openapi-pydantic" 316 | version = "0.5.1" 317 | source = { registry = "https://pypi.org/simple" } 318 | dependencies = [ 319 | { name = "pydantic" }, 320 | ] 321 | sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892, upload-time = "2025-01-08T19:29:27.083Z" } 322 | wheels = [ 323 | { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" }, 324 | ] 325 | 326 | [[package]] 327 | name = "openpyxl" 328 | version = "3.1.5" 329 | source = { registry = "https://pypi.org/simple" } 330 | dependencies = [ 331 | { name = "et-xmlfile" }, 332 | ] 333 | sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" } 334 | wheels = [ 335 | { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, 336 | ] 337 | 338 | [[package]] 339 | name = "psutil" 340 | version = "7.0.0" 341 | source = { registry = "https://pypi.org/simple" } 342 | sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" } 343 | wheels = [ 344 | { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" }, 345 | { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" }, 346 | { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" }, 347 | { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" }, 348 | { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" }, 349 | { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" }, 350 | { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" }, 351 | ] 352 | 353 | [[package]] 354 | name = "pydantic" 355 | version = "2.11.4" 356 | source = { registry = "https://pypi.org/simple" } 357 | dependencies = [ 358 | { name = "annotated-types" }, 359 | { name = "pydantic-core" }, 360 | { name = "typing-extensions" }, 361 | { name = "typing-inspection" }, 362 | ] 363 | sdist = { url = "https://files.pythonhosted.org/packages/77/ab/5250d56ad03884ab5efd07f734203943c8a8ab40d551e208af81d0257bf2/pydantic-2.11.4.tar.gz", hash = "sha256:32738d19d63a226a52eed76645a98ee07c1f410ee41d93b4afbfa85ed8111c2d", size = 786540, upload-time = "2025-04-29T20:38:55.02Z" } 364 | wheels = [ 365 | { url = "https://files.pythonhosted.org/packages/e7/12/46b65f3534d099349e38ef6ec98b1a5a81f42536d17e0ba382c28c67ba67/pydantic-2.11.4-py3-none-any.whl", hash = "sha256:d9615eaa9ac5a063471da949c8fc16376a84afb5024688b3ff885693506764eb", size = 443900, upload-time = "2025-04-29T20:38:52.724Z" }, 366 | ] 367 | 368 | [[package]] 369 | name = "pydantic-core" 370 | version = "2.33.2" 371 | source = { registry = "https://pypi.org/simple" } 372 | dependencies = [ 373 | { name = "typing-extensions" }, 374 | ] 375 | sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } 376 | wheels = [ 377 | { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, 378 | { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, 379 | { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, 380 | { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, 381 | { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, 382 | { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, 383 | { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, 384 | { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, 385 | { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, 386 | { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, 387 | { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, 388 | { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, 389 | { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, 390 | { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, 391 | { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, 392 | { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, 393 | { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, 394 | ] 395 | 396 | [[package]] 397 | name = "pydantic-settings" 398 | version = "2.9.1" 399 | source = { registry = "https://pypi.org/simple" } 400 | dependencies = [ 401 | { name = "pydantic" }, 402 | { name = "python-dotenv" }, 403 | { name = "typing-inspection" }, 404 | ] 405 | sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234, upload-time = "2025-04-18T16:44:48.265Z" } 406 | wheels = [ 407 | { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356, upload-time = "2025-04-18T16:44:46.617Z" }, 408 | ] 409 | 410 | [[package]] 411 | name = "pygments" 412 | version = "2.19.1" 413 | source = { registry = "https://pypi.org/simple" } 414 | sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } 415 | wheels = [ 416 | { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, 417 | ] 418 | 419 | [[package]] 420 | name = "python-dotenv" 421 | version = "1.1.0" 422 | source = { registry = "https://pypi.org/simple" } 423 | sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload-time = "2025-03-25T10:14:56.835Z" } 424 | wheels = [ 425 | { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" }, 426 | ] 427 | 428 | [[package]] 429 | name = "python-multipart" 430 | version = "0.0.20" 431 | source = { registry = "https://pypi.org/simple" } 432 | sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } 433 | wheels = [ 434 | { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, 435 | ] 436 | 437 | [[package]] 438 | name = "requests" 439 | version = "2.32.3" 440 | source = { registry = "https://pypi.org/simple" } 441 | dependencies = [ 442 | { name = "certifi" }, 443 | { name = "charset-normalizer" }, 444 | { name = "idna" }, 445 | { name = "urllib3" }, 446 | ] 447 | sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } 448 | wheels = [ 449 | { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, 450 | ] 451 | 452 | [[package]] 453 | name = "requests-file" 454 | version = "2.1.0" 455 | source = { registry = "https://pypi.org/simple" } 456 | dependencies = [ 457 | { name = "requests" }, 458 | ] 459 | sdist = { url = "https://files.pythonhosted.org/packages/72/97/bf44e6c6bd8ddbb99943baf7ba8b1a8485bcd2fe0e55e5708d7fee4ff1ae/requests_file-2.1.0.tar.gz", hash = "sha256:0f549a3f3b0699415ac04d167e9cb39bccfb730cb832b4d20be3d9867356e658", size = 6891, upload-time = "2024-05-21T16:28:00.24Z" } 460 | wheels = [ 461 | { url = "https://files.pythonhosted.org/packages/d7/25/dd878a121fcfdf38f52850f11c512e13ec87c2ea72385933818e5b6c15ce/requests_file-2.1.0-py2.py3-none-any.whl", hash = "sha256:cf270de5a4c5874e84599fc5778303d496c10ae5e870bfa378818f35d21bda5c", size = 4244, upload-time = "2024-05-21T16:27:57.733Z" }, 462 | ] 463 | 464 | [[package]] 465 | name = "rich" 466 | version = "14.0.0" 467 | source = { registry = "https://pypi.org/simple" } 468 | dependencies = [ 469 | { name = "markdown-it-py" }, 470 | { name = "pygments" }, 471 | ] 472 | sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" } 473 | wheels = [ 474 | { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" }, 475 | ] 476 | 477 | [[package]] 478 | name = "shellingham" 479 | version = "1.5.4" 480 | source = { registry = "https://pypi.org/simple" } 481 | sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } 482 | wheels = [ 483 | { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, 484 | ] 485 | 486 | [[package]] 487 | name = "sniffio" 488 | version = "1.3.1" 489 | source = { registry = "https://pypi.org/simple" } 490 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } 491 | wheels = [ 492 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, 493 | ] 494 | 495 | [[package]] 496 | name = "sse-starlette" 497 | version = "2.3.5" 498 | source = { registry = "https://pypi.org/simple" } 499 | dependencies = [ 500 | { name = "anyio" }, 501 | { name = "starlette" }, 502 | ] 503 | sdist = { url = "https://files.pythonhosted.org/packages/10/5f/28f45b1ff14bee871bacafd0a97213f7ec70e389939a80c60c0fb72a9fc9/sse_starlette-2.3.5.tar.gz", hash = "sha256:228357b6e42dcc73a427990e2b4a03c023e2495ecee82e14f07ba15077e334b2", size = 17511, upload-time = "2025-05-12T18:23:52.601Z" } 504 | wheels = [ 505 | { url = "https://files.pythonhosted.org/packages/c8/48/3e49cf0f64961656402c0023edbc51844fe17afe53ab50e958a6dbbbd499/sse_starlette-2.3.5-py3-none-any.whl", hash = "sha256:251708539a335570f10eaaa21d1848a10c42ee6dc3a9cf37ef42266cdb1c52a8", size = 10233, upload-time = "2025-05-12T18:23:50.722Z" }, 506 | ] 507 | 508 | [[package]] 509 | name = "starlette" 510 | version = "0.46.2" 511 | source = { registry = "https://pypi.org/simple" } 512 | dependencies = [ 513 | { name = "anyio" }, 514 | ] 515 | sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload-time = "2025-04-13T13:56:17.942Z" } 516 | wheels = [ 517 | { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" }, 518 | ] 519 | 520 | [[package]] 521 | name = "tldextract" 522 | version = "5.3.0" 523 | source = { registry = "https://pypi.org/simple" } 524 | dependencies = [ 525 | { name = "filelock" }, 526 | { name = "idna" }, 527 | { name = "requests" }, 528 | { name = "requests-file" }, 529 | ] 530 | sdist = { url = "https://files.pythonhosted.org/packages/97/78/182641ea38e3cfd56e9c7b3c0d48a53d432eea755003aa544af96403d4ac/tldextract-5.3.0.tar.gz", hash = "sha256:b3d2b70a1594a0ecfa6967d57251527d58e00bb5a91a74387baa0d87a0678609", size = 128502, upload-time = "2025-04-22T06:19:37.491Z" } 531 | wheels = [ 532 | { url = "https://files.pythonhosted.org/packages/67/7c/ea488ef48f2f544566947ced88541bc45fae9e0e422b2edbf165ee07da99/tldextract-5.3.0-py3-none-any.whl", hash = "sha256:f70f31d10b55c83993f55e91ecb7c5d84532a8972f22ec578ecfbe5ea2292db2", size = 107384, upload-time = "2025-04-22T06:19:36.304Z" }, 533 | ] 534 | 535 | [[package]] 536 | name = "typer" 537 | version = "0.15.3" 538 | source = { registry = "https://pypi.org/simple" } 539 | dependencies = [ 540 | { name = "click" }, 541 | { name = "rich" }, 542 | { name = "shellingham" }, 543 | { name = "typing-extensions" }, 544 | ] 545 | sdist = { url = "https://files.pythonhosted.org/packages/98/1a/5f36851f439884bcfe8539f6a20ff7516e7b60f319bbaf69a90dc35cc2eb/typer-0.15.3.tar.gz", hash = "sha256:818873625d0569653438316567861899f7e9972f2e6e0c16dab608345ced713c", size = 101641, upload-time = "2025-04-28T21:40:59.204Z" } 546 | wheels = [ 547 | { url = "https://files.pythonhosted.org/packages/48/20/9d953de6f4367163d23ec823200eb3ecb0050a2609691e512c8b95827a9b/typer-0.15.3-py3-none-any.whl", hash = "sha256:c86a65ad77ca531f03de08d1b9cb67cd09ad02ddddf4b34745b5008f43b239bd", size = 45253, upload-time = "2025-04-28T21:40:56.269Z" }, 548 | ] 549 | 550 | [[package]] 551 | name = "typing-extensions" 552 | version = "4.13.2" 553 | source = { registry = "https://pypi.org/simple" } 554 | sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } 555 | wheels = [ 556 | { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, 557 | ] 558 | 559 | [[package]] 560 | name = "typing-inspection" 561 | version = "0.4.1" 562 | source = { registry = "https://pypi.org/simple" } 563 | dependencies = [ 564 | { name = "typing-extensions" }, 565 | ] 566 | sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } 567 | wheels = [ 568 | { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, 569 | ] 570 | 571 | [[package]] 572 | name = "urllib3" 573 | version = "2.4.0" 574 | source = { registry = "https://pypi.org/simple" } 575 | sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" } 576 | wheels = [ 577 | { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" }, 578 | ] 579 | 580 | [[package]] 581 | name = "uvicorn" 582 | version = "0.34.2" 583 | source = { registry = "https://pypi.org/simple" } 584 | dependencies = [ 585 | { name = "click" }, 586 | { name = "h11" }, 587 | ] 588 | sdist = { url = "https://files.pythonhosted.org/packages/a6/ae/9bbb19b9e1c450cf9ecaef06463e40234d98d95bf572fab11b4f19ae5ded/uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", size = 76815, upload-time = "2025-04-19T06:02:50.101Z" } 589 | wheels = [ 590 | { url = "https://files.pythonhosted.org/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483, upload-time = "2025-04-19T06:02:48.42Z" }, 591 | ] 592 | 593 | [[package]] 594 | name = "websocket-client" 595 | version = "1.8.0" 596 | source = { registry = "https://pypi.org/simple" } 597 | sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648, upload-time = "2024-04-23T22:16:16.976Z" } 598 | wheels = [ 599 | { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826, upload-time = "2024-04-23T22:16:14.422Z" }, 600 | ] 601 | 602 | [[package]] 603 | name = "websockets" 604 | version = "15.0.1" 605 | source = { registry = "https://pypi.org/simple" } 606 | sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } 607 | wheels = [ 608 | { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, 609 | { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, 610 | { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, 611 | { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, 612 | { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, 613 | { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, 614 | { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, 615 | { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, 616 | { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, 617 | { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, 618 | { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, 619 | { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, 620 | ] 621 | --------------------------------------------------------------------------------