Python 环境管理工具选谁:uv / conda / poetry / pixi 对比
Python 世界的环境管理工具太多——pip、virtualenv、pipenv、poetry、conda、mamba、rye、uv、pixi、hatch……哪个最适合现在(2026 年)用?简单结论:大多数项目 uv,AI/CUDA 项目考虑 pixi 或 conda。 对比表工具 环境管理 包管理 速度 主要适用 现状uv ✅ ✅ 🚀 最快 大多数 Python 项目 活跃、事实标准pixi ✅ ✅ 🚀 很快 AI/CUDA/科学计算 新兴,快速崛起conda ✅ ✅ 慢 AI、科学计算 稳定但笨重mamba ✅ ✅ 快 conda 生态替代 活跃poetry ✅ ✅ 中 库/应用打包 成熟,用户多pipenv ✅ ✅ 慢 老项目 维护缓慢rye ✅ ✅ 快 曾是首选 已合并入 uvhatch ✅ ✅ 快 多环境测试 小众pip + venv ✅ ✅ 慢 兜底 官方最基础方案uv:默认选它 uv 是 Astral(写 Ruff 的那家)用 Rust 实现的一体化工具,把这些的能力都整合了:pyenv(Python 版本管理) virtualenv(虚拟环境) pip(包管理) pip-tools(依赖锁定) pipx(全局工具)日常操作: uv init # 初始化项目 uv python install 3.11 # 装 Python 3.11 uv add requests # 加依赖 uv add --dev pytest # 开发依赖 uv sync # 同步依赖到 .venv uv run main.py # 用项目环境跑 uv tool install ruff # 全局装工具(类似 pipx)速度上,pip 装 20 个包要 30 秒,uv 通常 2-3 秒。CI 尤其明显。 什么时候不用 uv CUDA 依赖复杂 + 遇到 wheel 找不到: 比如:Windows 上装 triton — 直接没轮子(见 uv 依赖冲突) OpenCV 特殊构建(Contrib、GPU) CUDA 版本要精确匹配 PyTorch Intel MKL / Nvidia cuDNN 直接依赖这些场景conda / pixi 生态里有预编译好的二进制,uv 从 PyPI 拿不到就得自己编译(往往编不过)。 Pixi:AI 项目的新选择 Pixi 由 conda 生态团队做的下一代工具——Rust 实现,速度接近 uv,能力接近 conda: pixi init pixi add python=3.11 pytorch=2.4 pixi add opencv # 自动解决 cuda / mkl pixi run python main.py优势:兼容 conda-forge 生态(大量预编译二进制) 自动解 CUDA / MKL / OpenCV 类非 Python 依赖 支持多语言(Python、Rust、C++、Julia) 速度快,比 conda 快得多已经有相当多 AI 开发者从 conda 迁移到 pixi。 Conda / Mamba:老牌 AI 首选 conda 的最大优势是 conda-forge 里几十万个预编译二进制包——包括 CUDA、cuDNN、MKL、OpenCV、FFmpeg 等 Python 生态之外的东西。 问题:解依赖巨慢(几分钟起步) 环境臃肿(一个 base 就几个 G) 私有仓库通常不支持Mamba 是 conda 的兼容替代,速度快 10 倍,用法一样: mamba install pytorch cudatoolkit=11.8 mamba env create -f environment.yml新项目直接上 pixi,比 conda + mamba 都简单。 Poetry:库开发 写 Python 库、要发 PyPI,poetry 还是很好的选择: poetry init poetry add requests poetry publish优势:强项在依赖锁定 + 发布 wheel pyproject.toml 规范 依赖冲突处理清晰uv 现在也能做发布(uv build + uv publish),但生态积累不如 poetry。 Rye 呢 Rye 之前是 Astral 主推的项目,2024 年宣布功能合并到 uv,Rye 只维护不新增。已经用 Rye 的可以直接迁到 uv,语法几乎一样。 场景推荐场景 首选 备选Web / 脚本 / 日常开发 uv poetry库开发 + 发 PyPI uv poetryYOLO / PyTorch / LLM 训练 uv(先试) pixi / mamba科学计算 + CUDA + MKL pixi mamba老项目改造 保持原有工具 逐步迁 uvCI 加速 uvuv 装全局工具 uv 不太强调"全局环境",但支持全局工具(类似 pipx): uv tool install ruff uv tool install pre-commit uv tool install pyinstalleruv tool list uv tool upgrade ruff uv tool uninstall ruff装完直接命令行可用。想真的用一个全局 Python 解释器写脚本,可以: uv python install 3.11 uv run --python 3.11 script.py一句话总结 默认 uv,CUDA 复杂就上 pixi。conda/mamba 老 AI 项目继续维护,poetry 专注库发布。Rye 已经并入 uv,不用再学。
YOLO 标注工具选型:X-AnyLabeling / LabelMe / PaddleX
YOLO 训练项目中,标注工具的选择影响效率,特别是在自动预标注和多种格式导出方面差异很大。 X-AnyLabeling(推荐,功能最全) 国内团队维护,目前更新最活跃的智能标注工具。 核心优势:内置 SAM 自动分割(点击目标自动生成 mask) 集成 GroundingDINO 文本驱动检测 支持 YOLOv8 预标注(先训一个小模型,用它预标注剩余数据) 支持矩形、多边形、关键点、旋转框等多种标注类型 支持 YOLO、COCO、VOC 格式导出pip install x-anylabeling x-anylabeling适合:需要 AI 辅助加速标注,或做实例分割。 LabelMe(经典稳定) Python 实现,教程资料最多,适合新手入门。 pip install labelme labelme特点:支持矩形、多边形、圆形、线段标注 JSON 格式保存,可转换为 YOLO/COCO 格式 社区资料丰富不足:纯手动标注,没有 AI 辅助,标注效率相对低。 PaddleX(无代码完整流程) 百度飞桨出品,从标注到训练到部署的一站式平台。 pip install paddlepaddle paddlex完整流程: 数据上传 → 标注 → 训练(可视化监控)→ 评估 → 导出模型 → 部署特点:图形化界面,不需要写代码 支持目标检测、图像分类、OCR、语义分割 自带数据增强 支持 GPU 分布式训练适合:不熟悉深度学习代码的工程师,或做快速验证。 Label Studio(团队协作) 功能最完整的企业级标注平台。 pip install label-studio label-studio start特点:多用户任务分配和审核 支持图片、文本、音频、视频 REST API,可集成到训练流水线 支持 YOLO、COCO 等格式导出 社区版免费,企业版收费适合:团队协作标注大数据集,或需要标注审核流程。 工具对比工具 AI 辅助 团队协作 学习成本 适用场景X-AnyLabeling ✅ SAM+DINO ❌ 低 智能辅助标注LabelMe ❌ ❌ 极低 个人小数据集PaddleX 部分 ❌ 低(有 GUI) 无代码训练Label Studio ✅ ✅ 中 团队大规模标注YOLO 格式快速说明 标注完成后导出为 YOLO 格式(.txt 文件): # 每行格式:class_id cx cy width height 0 0.5 0.3 0.2 0.15 1 0.7 0.6 0.1 0.08坐标均为相对图片尺寸的归一化值(0~1)。class_id 从 0 开始,对应 data.yaml 中的类别列表。 预标注加速工作流 效率最高的策略:手动标注 200~500 张 训练一个初版 YOLOv8 模型 用该模型对剩余数据预标注 人工审核+修正(只改错误的框) 合并数据重新训练通常从第二轮开始,实际手工修改量减少 60~80%。
Canvas 拖拽标注框:startBox 模式避免累计误差与 offsetX 自然拖拽感
问题:mousemove 中累加导致误差 // 错误写法:每次 mousemove 都叠加 canvas.addEventListener('mousemove', (e) => { if (!isDragging) return; const dx = e.clientX - lastX; const dy = e.clientY - lastY; bbox.x += dx; // 浮点累加,误差逐帧积累 bbox.y += dy; lastX = e.clientX; lastY = e.clientY; });每帧的 dx 存在浮点舍入,经过几百次 mousemove 后,bbox 位置与鼠标实际位置产生偏差。 解决:startBox 模式 mousedown 时保存初始状态,mousemove 始终基于初始状态计算绝对位置: let dragState = null;canvas.addEventListener('mousedown', (e) => { const mouseX = getCanvasX(e); const mouseY = getCanvasY(e); if (isInsideBbox(mouseX, mouseY, bbox)) { dragState = { startX: mouseX, startY: mouseY, startBox: { ...bbox } // 拷贝初始位置 }; } });canvas.addEventListener('mousemove', (e) => { if (!dragState) return; const mouseX = getCanvasX(e); const mouseY = getCanvasY(e); const dx = mouseX - dragState.startX; const dy = mouseY - dragState.startY; // 每帧都基于 startBox 计算,不累加 bbox.x = dragState.startBox.x + dx; bbox.y = dragState.startBox.y + dy; redraw(); });canvas.addEventListener('mouseup', () => { dragState = null; });bbox.x = startBox.x + dx 而不是 bbox.x += dx,误差不再累积。 offsetX 模式:自然拖拽手感 上面的写法有个体验问题:点击框的右下角时,鼠标移动会让框的左上角跳到鼠标位置。 offsetX 模式记录点击点与框左上角的偏移量,保持鼠标与抓取点的相对位置不变: canvas.addEventListener('mousedown', (e) => { const mouseX = getCanvasX(e); const mouseY = getCanvasY(e); if (isInsideBbox(mouseX, mouseY, bbox)) { dragState = { offsetX: mouseX - bbox.x, // 鼠标在框内的相对位置 offsetY: mouseY - bbox.y }; } });canvas.addEventListener('mousemove', (e) => { if (!dragState) return; const mouseX = getCanvasX(e); const mouseY = getCanvasY(e); bbox.x = mouseX - dragState.offsetX; bbox.y = mouseY - dragState.offsetY; redraw(); });鼠标无论点击框内哪个位置,拖拽过程中该点始终跟随鼠标,手感自然。 三种方案对比方案 误差 拖拽手感 适用场景bbox.x += dx(lastX 累加) 有累计误差 一般 不推荐startBox 模式 无误差 左上角跳到鼠标 精确位移要求高offsetX 模式 无误差 自然,保持抓取点 用户交互体验优先大多数标注工具选择 offsetX 模式,既无误差又有自然手感。 坐标转换 Canvas 内部坐标需要考虑缩放和滚动: function getCanvasX(e) { const rect = canvas.getBoundingClientRect(); return (e.clientX - rect.left) / scale; }function getCanvasY(e) { const rect = canvas.getBoundingClientRect(); return (e.clientY - rect.top) / scale; }scale 是 Canvas 的缩放比例,不除以 scale 会导致缩放后拖拽位置错误。
油猴脚本加 IndexedDB 本地备份:掉线也不丢数据
在油猴脚本里做数据备份,通常只往本地服务器 POST 一份。但服务器挂掉或网络问题时,这批数据就丢了。IndexedDB 可以作为本地兜底——不管服务器是否正常,先把数据存本地一份。 IndexedDB 基础封装 const DB_NAME = "localBackupDB"; const STORE_NAME = "backups"; let db;function initDB() { return new Promise((resolve, reject) => { const req = indexedDB.open(DB_NAME, 1); req.onupgradeneeded = (e) => { const d = e.target.result; if (!d.objectStoreNames.contains(STORE_NAME)) { d.createObjectStore(STORE_NAME, { keyPath: "id" }); } }; req.onsuccess = (e) => { db = e.target.result; resolve(db); }; req.onerror = (e) => reject(e); }); }function saveToIndexedDB(fileName, content) { if (!db) return; const tx = db.transaction(STORE_NAME, "readwrite"); const store = tx.objectStore(STORE_NAME); store.put({ id: fileName, content, timestamp: Date.now() }); }function getAllBackups() { return new Promise((resolve, reject) => { if (!db) return resolve([]); const tx = db.transaction(STORE_NAME, "readonly"); const req = tx.objectStore(STORE_NAME).getAll(); req.onsuccess = () => resolve(req.result); req.onerror = (e) => reject(e); }); }集成到备份队列 已有服务端备份逻辑时,在 enqueueSave 里同时存 IndexedDB: const savedSet = new Set(); const queue = []; let timer = null;function enqueueSave(fileName, content) { if (savedSet.has(fileName)) return; savedSet.add(fileName); // 本地 IndexedDB 兜底(不管服务端是否正常) saveToIndexedDB(fileName, content); queue.push({ fileName, content }); if (!timer) timer = setTimeout(flushQueue, 1000); }function flushQueue() { const batch = [...queue]; queue.length = 0; timer = null; batch.forEach(({ fileName, content }) => saveToServer(fileName, content)); }这样即使 saveToServer 失败,IndexedDB 里还有一份。 可视化面板 挂到 window.showPanel,在控制台调用即可弹出历史备份列表: function showPanel() { if (document.getElementById("__backup_panel")) return; const panel = document.createElement("div"); panel.id = "__backup_panel"; Object.assign(panel.style, { position: "fixed", top: "20px", right: "20px", width: "320px", maxHeight: "420px", overflowY: "auto", background: "#fff", border: "1px solid #ccc", boxShadow: "0 0 10px rgba(0,0,0,.3)", padding: "12px", zIndex: 99999, fontSize: "12px", fontFamily: "monospace" }); panel.innerHTML = `<div style="font-weight:bold;margin-bottom:8px">📦 历史备份</div> <div id="__backup_list"></div> <button id="__backup_close" style="margin-top:10px">关闭</button>`; document.body.appendChild(panel); document.getElementById("__backup_close").onclick = () => panel.remove(); getAllBackups().then(backups => { const list = document.getElementById("__backup_list"); backups.sort((a, b) => b.timestamp - a.timestamp); backups.forEach(item => { const row = document.createElement("div"); row.style.cssText = "display:flex;justify-content:space-between;margin-bottom:4px"; const name = document.createElement("span"); name.textContent = item.id; name.style.cssText = "flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"; const btn = document.createElement("button"); btn.textContent = "恢复"; btn.onclick = () => { try { console.log("🔄 恢复备份:", item.id); console.log(JSON.parse(item.content)); } catch { console.log(item.content); } }; row.appendChild(name); row.appendChild(btn); list.appendChild(row); }); }); }window.showPanel = showPanel;使用时在浏览器控制台输入 showPanel() 即可查看。 初始化调用 initDB() .then(() => console.log("IndexedDB 已就绪")) .catch(console.error);注意事项IndexedDB 存储受同源策略限制,每个域名独立,跨域无法访问 浏览器私有模式下 IndexedDB 数据在关闭标签页后会清除 存储上限一般是磁盘空间的 50% 左右(浏览器决定),备份大量 JSON 不用担心空间 用 store.put()(不是 add()),同 key 会覆盖而不报错 可以在 DevTools → Application → IndexedDB 直接查看和清理数据
油猴脚本 XHR Hook 数据落地:IndexedDB 本地备份 + 本地服务双写
场景 油猴脚本 Hook XHR/fetch 后捕获接口数据,需要持久化保存。常见方案是 POST 到本机 http://127.0.0.1:PORT,但本地服务可能未启动或崩溃,导致丢失数据。 双写方案:先写 IndexedDB(浏览器本地,无外部依赖),再异步 POST 到本地服务,两者互为备份。 IndexedDB 封装 const DB_NAME = 'XhrBackup'; const STORE_NAME = 'records';let db = null;function initDB() { return new Promise((resolve, reject) => { const req = indexedDB.open(DB_NAME, 1); req.onupgradeneeded = (e) => { const store = e.target.result.createObjectStore(STORE_NAME, { keyPath: 'id', autoIncrement: true }); store.createIndex('fileName', 'fileName', { unique: false }); }; req.onsuccess = (e) => { db = e.target.result; resolve(db); }; req.onerror = (e) => reject(e.target.error); }); }function saveToIDB(fileName, content) { return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, 'readwrite'); const store = tx.objectStore(STORE_NAME); store.add({ fileName, content, savedAt: Date.now() }); tx.oncomplete = resolve; tx.onerror = (e) => reject(e.target.error); }); }队列去重与批量 flush 频繁触发(如分页列表接口每页都调用)时需要去重,避免重复保存同一数据: const savedSet = new Set(); const queue = []; let flushTimer = null;function enqueueSave(fileName, content) { if (savedSet.has(fileName)) return; // 去重 savedSet.add(fileName); queue.push({ fileName, content }); // 1 秒防抖,批量 flush if (!flushTimer) { flushTimer = setTimeout(flushQueue, 1000); } }function flushQueue() { const batch = [...queue]; queue.length = 0; flushTimer = null; batch.forEach(item => saveItem(item.fileName, item.content)); }双写实现 async function saveItem(fileName, content) { // 1. 先写 IndexedDB(不依赖本地服务) try { await saveToIDB(fileName, content); console.log('✅ IDB 保存成功:', fileName); } catch (e) { console.error('❌ IDB 保存失败:', fileName, e); } // 2. 再 POST 到本地服务(可选) const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); fetch('http://127.0.0.1:8099/save', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fileName, content }), signal: controller.signal }) .then(res => { clearTimeout(timeout); if (!res.ok) throw new Error(`HTTP ${res.status}`); console.log('✅ 服务保存成功:', fileName); }) .catch(err => { clearTimeout(timeout); if (err.name === 'AbortError') { console.warn('⚠️ 服务超时,已保留 IDB 备份:', fileName); } else { console.error('❌ 服务保存失败(IDB 已备份):', fileName, err.message); } }); }XHR Hook 集成 const originOpen = XMLHttpRequest.prototype.open; const originSend = XMLHttpRequest.prototype.send;XMLHttpRequest.prototype.open = function (method, url, ...rest) { this._method = method; this._url = url; return originOpen.apply(this, [method, url, ...rest]); };XMLHttpRequest.prototype.send = function (body) { this.addEventListener('readystatechange', function () { if (this.readyState !== 4) return; // 匹配目标接口 URL,按业务修改正则 if (/\/api\/data\/\d+/.test(this._url)) { const url = new URL(this._url, location.origin); const id = url.searchParams.get('id') || Date.now(); enqueueSave(`data/${id}_response.json`, this.responseText); } }); return originSend.apply(this, arguments); };读取 IndexedDB 中已保存的数据 function getAllRecords() { return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, 'readonly'); const store = tx.objectStore(STORE_NAME); const req = store.getAll(); req.onsuccess = () => resolve(req.result); req.onerror = (e) => reject(e.target.error); }); }// 控制台查看所有备份 getAllRecords().then(records => { console.log(`共 ${records.length} 条备份`); console.table(records.map(r => ({ id: r.id, fileName: r.fileName, savedAt: new Date(r.savedAt).toLocaleString() }))); });初始化入口 // 油猴脚本主入口 (async function () { await initDB(); // 在这里挂 XHR Hook console.log('备份系统就绪'); })();IndexedDB 在同源页面(同协议+域名+端口)下持久存储,浏览器关闭后数据不丢失,只有手动清除浏览器数据时才会清空。
