Missing X server or $DISPLAY:无头环境跑浏览器的正确姿势
在服务器 / 容器里想跑 Chromium、Electron、Playwright,报错: [...] ozone_platform_x11.cc:259] Missing X server or $DISPLAY意思很直白——程序想弹 GUI,但当前环境没 X11 显示服务。三个典型场景。 场景一:SSH 到 Linux 服务器 SSH 登进去的 shell 默认没有桌面: ssh root@server google-chrome # 直接报 Missing X server两种解法: Headless 模式(首选) google-chrome --headless=new --disable-gpu --no-sandbox https://example.comPlaywright: browser = playwright.chromium.launch(headless=True)Puppeteer: const browser = await puppeteer.launch({ headless: true });非 headless 场景用 X11 转发 ssh -X root@server或用 -Y(信任模式,速度更快)。前提是本机有 X server(Mac 上是 XQuartz,Windows 上是 VcXsrv/X410)。 场景二:Docker 容器里 容器默认没 X server: $ echo $DISPLAY (空)首选还是 headless。要真的跑 GUI 就靠 Xvfb(虚拟帧缓冲): apt update && apt install -y xvfb xvfb-run -a chromium https://example.comxvfb-run 会在后台起一个虚拟 X server 让程序连。Playwright 官方镜像就自带这套。 如果显示环境变量已经写了 DISPLAY=:1,但仍然报错,那是因为 :1 对应的 X server 根本没起: ls /tmp/.X11-unix/看不到 X0 / X1 就是没启动。容器里手动 export DISPLAY=:1 是没用的——DISPLAY 只是"连哪个 X server",不会创建 X server。 场景三:WSL WSL2 现在原生支持 WSLg,一般不会碰到这错。老 WSL1 或者关了 WSLg 的话: export DISPLAY=:0 sudo apt install x11-apps xclock # 测试有没有 GUI再不行就装个 X server(Windows 上 VcXsrv / X410)。 快速判断清单 先看几个变量: echo $DISPLAY # 应该是 :0 或 :1 ls /tmp/.X11-unix/ # 应该有 X0 之类的 socket ps aux | grep -E 'Xorg|Xvfb' # 有 X server 进程在跑 hostname # 类似 6e6340e2e0d2 基本是容器 cat /proc/1/cgroup # 看到 docker 关键字 = 容器对号入座:现象 判断DISPLAY 空 没有桌面环境,用 headless 或 XvfbDISPLAY 有但 X11 socket 目录空 声明了但 X server 没起hostname 是短哈希 Docker 容器WSL 里 xclock 不动 WSL 侧 X server 没通一句话总结 服务器 / 容器跑浏览器,能 headless 就 headless,非要 GUI 上 Xvfb。 手动改 $DISPLAY 只是告诉客户端连哪儿,从来不会凭空生出一个 X server。
BIP39 助记词生成原理与 Python 实现
BIP39 定义了一套将随机数编码为可记忆单词的标准,大多数钱包(MetaMask、Phantom、imToken)都支持。 BIP39 原理生成 128/160/192/224/256 位随机熵 取熵的 SHA-256 哈希前 len/32 位作为校验位,拼接到熵末尾 每 11 位映射一个 BIP39 词表单词(共 2048 个词) 最终得到 12/15/18/21/24 个单词熵长度 校验位 总位数 单词数128 bit 4 bit 132 bit 12256 bit 8 bit 264 bit 24Python 实现(bip-utils) pip install bip-utils从助记词派生以太坊私钥和地址 from bip_utils import ( Bip39SeedGenerator, Bip39MnemonicValidator, Bip44, Bip44Coins, Bip44Changes, )mnemonic = "word1 word2 word3 word4 word5 word6 word7 word8 word9 word10 word11 word12"# 可选:验证助记词是否合法 if not Bip39MnemonicValidator().IsValid(mnemonic): raise ValueError("助记词无效")# 助记词 → 种子(可选 passphrase) seed = Bip39SeedGenerator(mnemonic).Generate(passphrase="")# 种子 → BIP44 派生路径 m/44'/60'/0'/0/0 ctx = Bip44.FromSeed(seed, Bip44Coins.ETHEREUM) addr_ctx = ( ctx.Purpose() .Coin() .Account(0) .Change(Bip44Changes.CHAIN_EXT) .AddressIndex(0) )private_key_hex = addr_ctx.PrivateKey().Raw().ToHex() address = addr_ctx.PublicKey().ToAddress()print(f"私钥: {private_key_hex}") print(f"地址: {address}")派生多个地址 for i in range(5): addr_ctx = ( ctx.Purpose() .Coin() .Account(0) .Change(Bip44Changes.CHAIN_EXT) .AddressIndex(i) ) print(f"[{i}] {addr_ctx.PublicKey().ToAddress()}")Solana(BIP44 Coin 501) from bip_utils import Bip44Coinsctx = Bip44.FromSeed(seed, Bip44Coins.SOLANA) sol_ctx = ctx.Purpose().Coin().Account(0).Change(Bip44Changes.CHAIN_EXT).AddressIndex(0) print(f"Solana 地址: {sol_ctx.PublicKey().ToAddress()}")手动验证 BIP39 校验和(纯 Python) import hashlibdef validate_bip39(mnemonic: str, wordlist_path: str) -> bool: with open(wordlist_path) as f: words = f.read().splitlines() word_index = {w: i for i, w in enumerate(words)} mnemonic_words = mnemonic.strip().split() bits = "" for word in mnemonic_words: if word not in word_index: return False bits += format(word_index[word], "011b") # 最后 len/33 位是校验位 cs_len = len(bits) // 33 entropy_bits = bits[:-cs_len] checksum_bits = bits[-cs_len:] # 还原熵字节 entropy = int(entropy_bits, 2).to_bytes(len(entropy_bits) // 8, "big") # SHA256 前 cs_len 位 h = hashlib.sha256(entropy).digest() expected_cs = format(h[0], "08b")[:cs_len] return checksum_bits == expected_cs注意事项助记词等同于钱包全部资产的控制权,不能在任何联网代码中明文存储 生产环境派生私钥只能在离线机器或 HSM 上操作 仅用于工具开发/验证时,建议使用测试网地址,不要导入真实资产
Python 拿 CPU 型号,以及 Windows 注册表里的 CPU 信息
想在 Python 里拿到完整的 CPU 型号——比如 Intel(R) Xeon(R) CPU E5-2680 v4 @ 2.40GHz 或 12th Gen Intel(R) Core(TM) i5-12400。看着简单,跨平台其实要三条路径。 首选:platform.processor() 标准库一行: import platform print(platform.processor())Linux/macOS 通常能给出可读的字符串。Windows 上经常返回空或者只有个 Intel64 Family 6 Model 158 Stepping 10——CPUID 的原始编码,不是人看的。 Windows:WMI 或注册表 方式 1:wmic import subprocessout = subprocess.check_output("wmic cpu get name", shell=True, text=True) name = out.split("\n")[1].strip() print(name) # 12th Gen Intel(R) Core(TM) i5-12400注意 wmic 在 Windows 11 24H2 之后被标注为已弃用(虽然还能用一段时间)。 方式 2:PowerShell out = subprocess.check_output( ["powershell", "-Command", "(Get-CimInstance Win32_Processor).Name"], text=True, ) print(out.strip())这个在新版 Windows 上更稳。 方式 3:读注册表 Windows 启动时会把 CPUID 结果缓存到注册表: HKLM\HARDWARE\DESCRIPTION\System\CentralProcessor\0 ProcessorNameString REG_SZ 12th Gen Intel(R) Core(TM) i5-12400 VendorIdentifier REG_SZ GenuineIntelPython 读: import winregwith winreg.OpenKey( winreg.HKEY_LOCAL_MACHINE, r"HARDWARE\DESCRIPTION\System\CentralProcessor\0", ) as k: name, _ = winreg.QueryValueEx(k, "ProcessorNameString") print(name)三种方式底层都来自 CPUID 指令,注册表算是"启动时缓存好的解析结果",速度最快,也不依赖外部命令。 Linux:/proc/cpuinfo with open("/proc/cpuinfo") as f: for line in f: if line.startswith("model name"): print(line.split(":", 1)[1].strip()) break或者 shell 一行: grep "model name" /proc/cpuinfo | head -1 lscpu | grep "Model name"macOS:sysctl import subprocess out = subprocess.check_output( ["sysctl", "-n", "machdep.cpu.brand_string"], text=True ) print(out.strip()) # Apple M2 Pro / Intel(R) Core(TM) i7-...跨平台通用函数 import os import platform import subprocessdef get_cpu_name() -> str: system = platform.system() if system == "Windows": try: import winreg with winreg.OpenKey( winreg.HKEY_LOCAL_MACHINE, r"HARDWARE\DESCRIPTION\System\CentralProcessor\0", ) as k: name, _ = winreg.QueryValueEx(k, "ProcessorNameString") return name.strip() except Exception: pass try: out = subprocess.check_output( ["powershell", "-Command", "(Get-CimInstance Win32_Processor).Name"], text=True, ) return out.strip() except Exception: return platform.processor() or "unknown" if system == "Darwin": try: return subprocess.check_output( ["sysctl", "-n", "machdep.cpu.brand_string"], text=True, ).strip() except Exception: return platform.processor() or "unknown" # Linux / *BSD try: with open("/proc/cpuinfo") as f: for line in f: if line.startswith("model name"): return line.split(":", 1)[1].strip() except Exception: pass return platform.processor() or "unknown"if __name__ == "__main__": print(get_cpu_name())CPU 信息会被缓存吗 有个常见疑问:"CPU 型号会不会被系统缓存到什么地方?" 答案分两层:CPU 本身没有"存型号信息"——型号信息是 CPUID 指令的返回值,来自硬件里的固化电路 操作系统会缓存 CPUID 的解析结果:Windows 存在 HKLM\HARDWARE\DESCRIPTION\System\CentralProcessor 注册表下、Linux 通过 /proc/cpuinfo 动态生成、macOS 通过 sysctl 暴露所以你读注册表 / /proc/cpuinfo 拿的是"系统解析过一次的缓存",实时性和 CPUID 指令等价,但在虚拟机 / 容器里可能被覆盖或伪造——CPU 型号做设备指纹是很弱的信号,容易被伪装。 一句话总结 跨平台就 Windows 读注册表、Linux 读 /proc/cpuinfo、macOS sysctl。platform.processor() 在 Linux/macOS 上够用,Windows 上不行。做指纹别只靠 CPU 型号,太容易伪装。
JS 注入标注快捷键面板:数据驱动 radio 分组与可拖拽悬浮窗
基础版:数字键触发单个标签 click (function () { const labels = ["路中障碍物", "施工区域", "基本可见", "部分可见", "运动", "静上"]; // 防止重复注入 const old = document.getElementById("label-hotkey-panel"); if (old) old.remove(); const panel = document.createElement("div"); panel.id = "label-hotkey-panel"; panel.style.cssText = "position:fixed;top:20px;right:20px;z-index:999999;background:rgba(0,0,0,.85);" + "color:#fff;padding:12px;border-radius:10px;font-size:14px;min-width:220px;"; function getEl(name) { return document.querySelector(`span.label[title="${name}"]`); } labels.forEach((name, idx) => { const row = document.createElement("div"); row.style.cssText = "display:flex;align-items:center;justify-content:space-between;margin:6px 0;"; const info = document.createElement("span"); info.textContent = `[${idx + 1}] ${name}`; const btn = document.createElement("button"); btn.textContent = getEl(name) ? "点击" : "不存在"; btn.style.cssText = `cursor:pointer;border:none;padding:4px 8px;border-radius:6px; background:${getEl(name) ? "#409eff" : "#666"};color:white;`; btn.onclick = () => { const el = getEl(name); el ? el.click() : console.warn("未找到:", name); }; row.append(info, btn); panel.appendChild(row); }); document.body.appendChild(panel); document.addEventListener("keydown", (e) => { const n = parseInt(e.key); if (!isNaN(n) && n >= 1 && n <= labels.length) { const el = getEl(labels[n - 1]); el ? el.click() : console.warn("未找到:", labels[n - 1]); } }); })();数据驱动 radio 分组版(推荐维护方式) 多个分组各自单选,按反引号 ` 执行当前选择: (function () { if (document.getElementById("label-panel")) return; // 只改这里就能增减标签 const groups = { g1: { title: "第一组", options: ["路中障碍物", "施工区域"] }, g2: { title: "第二组", options: ["基本可见", "部分可见", "基本不可见"] }, g3: { title: "���三组", options: ["运动", "禁止"] } }; const panel = document.createElement("div"); panel.id = "label-panel"; panel.style.cssText = "position:fixed;top:20px;right:20px;z-index:999999;background:#fff;" + "border:1px solid #ccc;border-radius:8px;padding:12px;" + "box-shadow:0 0 10px rgba(0,0,0,.2);font-size:14px;width:240px;"; // 动态渲染 HTML const html = Object.entries(groups) .map(([key, group]) => { const radios = group.options .map((opt, idx) => ` <label style="display:block;margin:2px 0;"> <input type="radio" name="${key}" value="${opt}" ${idx === 0 ? "checked" : ""}>${opt} </label>`) .join(""); return `<div style="margin-bottom:8px;"><b>${group.title}</b><div>${radios}</div></div>`; }) .join(""); panel.innerHTML = ` <div style="font-weight:bold;margin-bottom:10px;">标签快捷选择</div> ${html} <hr> <div style="font-size:12px;color:#666;">反引号 = 执行</div> `; document.body.appendChild(panel); function clickLabel(title) { const el = document.querySelector(`span.label[title="${title}"]`); el ? el.click() : console.warn("未找到:", title); } function execute() { Object.keys(groups).forEach(key => { const selected = document.querySelector(`input[name="${key}"]:checked`); if (selected) clickLabel(selected.value); }); } document.addEventListener("keydown", e => { if (["INPUT", "TEXTAREA"].includes(e.target.tagName)) return; if (e.key === "`") { e.preventDefault(); execute(); } if (e.key === "Tab") { e.preventDefault(); document.querySelector('input[type="checkbox"][value="开启高层过滤"]')?.click(); } }); })();添加拖拽与折叠 // 拖动 let dragging = false, ox = 0, oy = 0; panel.addEventListener("mousedown", e => { if (e.target.tagName === "INPUT") return; dragging = true; ox = e.clientX - panel.offsetLeft; oy = e.clientY - panel.offsetTop; }); document.addEventListener("mousemove", e => { if (!dragging) return; panel.style.left = (e.clientX - ox) + "px"; panel.style.top = (e.clientY - oy) + "px"; panel.style.right = "auto"; }); document.addEventListener("mouseup", () => dragging = false);// 折叠 let collapsed = false; panel.querySelector("#toggle-btn").onclick = () => { collapsed = !collapsed; panel.querySelector(".body").style.display = collapsed ? "none" : "block"; };面板头部加折叠按钮: <div style="display:flex;justify-content:space-between;align-items:center;cursor:pointer"> <b>label</b> <span id="toggle-btn">[-]</span> </div> <div class="body">...</div>Python 批量转义 JS 中的汉字 把 JS 文件里的中文字符替换成 \uXXXX,代码仍然可以在浏���器运行: import redef encode_chinese(js): def repl(m): return ''.join(f'\\u{ord(c):04x}' for c in m.group(0)) return re.sub(r'[一-鿿]+', repl, js)js_code = ''' const title = "路中障碍���"; document.querySelector('span.label[title="施工区域"]'); '''print(encode_chinese(js_code)) # const title = "路中障碍物"; # document.querySelector('span.label[title="施工区域"]');只替换汉字,JS 语法和 ASCII 字符保持不变,浏览器完全等价执行。
给页面注入一个快捷键面板:数字键快速点击标签
做标注 / 高频点击类工作,鼠标点半天累。给页面注入一个悬浮面板 + 快捷键,数字键触发对应元素的 click()——效率能翻几倍。 直接粘贴到浏览器 Console (() => { const items = [ { key: "1", title: "标签A" }, { key: "2", title: "标签B" }, { key: "3", title: "标签C" }, { key: "4", title: "标签D" }, { key: "5", title: "标签E" }, ]; function findEl(title) { return document.querySelector(`span.label[title="${title}"]`); } function clickByTitle(title) { const el = findEl(title); if (!el) return console.warn(`未找到: ${title}`); // 框架页面用 dispatchEvent 更稳 el.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window, })); console.log(`点击: ${title}`); } // 面板 const panel = document.createElement("div"); panel.style.cssText = ` position: fixed; top: 80px; right: 20px; width: 220px; background: rgba(0,0,0,.85); color: #fff; padding: 12px; border-radius: 12px; z-index: 999999; font-family: system-ui; font-size: 14px; `; panel.innerHTML = "<b>快捷键面板</b>"; function render() { panel.querySelectorAll(".item").forEach(e => e.remove()); for (const it of items) { const row = document.createElement("div"); row.className = "item"; const ok = !!findEl(it.title); row.style.cssText = ` display:flex; justify-content:space-between; margin:6px 0; padding:4px 6px; border-radius:6px; background:${ok ? "rgba(0,255,0,.12)" : "rgba(255,0,0,.12)"}; cursor:pointer; `; row.innerHTML = `<span>[${it.key}] ${it.title}</span><span>${ok ? "🟢" : "🔴"}</span>`; row.onclick = () => clickByTitle(it.title); panel.appendChild(row); } const tip = document.createElement("div"); tip.className = "item"; tip.style.cssText = "margin-top:10px;color:#aaa;font-size:12px;"; tip.textContent = "F8 显示/隐藏"; panel.appendChild(tip); } render(); document.body.appendChild(panel); // 键盘监听 document.addEventListener("keydown", e => { if (e.key === "F8") { panel.style.display = panel.style.display === "none" ? "block" : "none"; return; } // 输入框里不触发 const t = e.target; if (t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement || t?.isContentEditable) return; const item = items.find(x => x.key === e.key); if (item) { e.preventDefault(); clickByTitle(item.title); } }); // 动态渲染的页面定时刷新状态 setInterval(render, 2000); })();功能:右上角浮 220px 宽面板 每项显示 [键] 标题 + 找到/未找到指示 按 1~5 触发对应点击 F8 切换显示/隐藏 面板项本身也能点击 输入框获得焦点时不拦截(不然打字触发 bug)打包成油猴脚本 // ==UserScript== // @name 快捷键面板 // @match https://your-site.com/* // @run-at document-end // @grant none // ==/UserScript==(function () { // ... 上面的代码 })();document-end 保证 DOM 建好之后再挂面板。SPA 页面切路由后可能面板还在但按键失效——加一个 MutationObserver 监听页面变化重新 render 即可。 复杂点击的兼容 某些 Vue / React 页面直接 el.click() 触发不到框架事件(因为它们监听的是自己合成的事件,而不是原生 DOM 事件),改用: el.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window, }));冒泡是关键——很多框架的 delegation 是在文档根节点上监听的。 更进一步的想法 如果是标注工具,还能加:Q/W/E/R/T/Y 左手键位(比数字键位好按) 组合标签(Shift+1 = 同时点 1 和 3) 自动跳下一张图(连点两个标签 → 自动 next) 今日已标数统计 本地保存自定义键位(localStorage) CSV 导出统计放在 localStorage 里存 key 配置: const config = JSON.parse(localStorage.getItem("hotkey-config") || "[]"); if (config.length) items.splice(0, items.length, ...config);一句话总结 Console 里几十行 JS 就能注入一个快捷键面板,dispatchEvent 兼容 Vue/React 框架点击、输入框内不拦截。做高频标注 / 批量操作时效率提升明显。
