Showing Posts From

自动化

Python 模拟键盘输入:中文乱码和 Citrix 场景的应对

自动化脚本里想模拟键盘输入——听着简单,一遇到中文、遇到 Citrix / RDP / 游戏窗口,坑就一个接一个来。列一下常用方案和各自适用场景。 方案对比库 优点 缺点pyautogui 跨平台、简单 中文经常挂、后台窗口不响应keyboard 全局事件、监听热键 Windows 常需管理员、后台无效pywin32 支持后台 WM_CHAR 某些程序(浏览器、游戏)不响应 WM_CHARctypes + WinAPI 无依赖、最底层 需要处理 scan code、代码稍长剪贴板 + Ctrl+V 中文、长文本最稳 会覆盖用户剪贴板最简单:pyautogui import pyautogui, time time.sleep(3) # 3 秒切窗口 pyautogui.write("hello world") # 输入英文 pyautogui.press("enter") # 按键 pyautogui.hotkey("ctrl", "c") # 组合键interval 参数控制打字间隔: pyautogui.write("hello", interval=0.05)中文乱码怎么办 pyautogui.write("你好") 本质是把字符串拆成一个个按键事件。中文没有对应的物理按键,很多环境(尤其 Citrix、RDP、游戏、浏览器某些输入框)会输入乱码或干脆吞掉。 最稳的做法是 走系统剪贴板 + Ctrl+V: import pyperclip, pyautogui, timepyperclip.copy("你好世界,长文本也行") time.sleep(2) # 切窗口 pyautogui.hotkey("ctrl", "v")优点:支持中文 / emoji / 特殊字符 支持超长文本(比逐字模拟快百倍) 兼容大多数远程桌面 / Citrix缺点:会覆盖用户当前剪贴板(可以备份再还原) 需要目标窗口支持 Ctrl+V带备份还原的完整版本: import pyperclip, pyautogui, timedef paste_text(text): backup = pyperclip.paste() pyperclip.copy(text) time.sleep(0.1) pyautogui.hotkey("ctrl", "v") time.sleep(0.1) pyperclip.copy(backup)后台发送到指定窗口 前台切换太打扰人,想在不抢焦点的情况下往某个窗口发内容: import win32gui, win32api, win32conhwnd = win32gui.FindWindow(None, "记事本") for ch in "hello": win32api.SendMessage(hwnd, win32con.WM_CHAR, ord(ch), 0)局限:浏览器 / 游戏 / Citrix / 远程桌面通常不响应 WM_CHAR(安全考虑) 只对纯 Win32 原生窗口有效不响应的目标只能回到"抢焦点 + 真实按键"路线。 Citrix 里传大数据 场景:本机 → Citrix → 内网服务器,想把一个几 MB 的文件传过去,只能靠键盘输入。 技巧:本机 base64 编码文件(比 hex 省一半体积) 分块用 pyautogui.write 发(每块 4 KB 左右) 内网服务器上再解码import pyautogui, base64, timewith open("payload.bin", "rb") as f: data = base64.b64encode(f.read()).decode()time.sleep(3) # 切到 Citrix 窗口 CHUNK = 4096 for i in range(0, len(data), CHUNK): pyautogui.write(data[i:i + CHUNK], interval=0.001) time.sleep(0.1) pyautogui.press("enter")内网服务器上有 Python / Bash 就 base64 -d 还原。 监听全局热键 想按 F8 触发某个动作: import keyboard keyboard.wait("f8") print("触发")Windows 上需要管理员运行才能读到全局键盘事件。 一句话总结 英文短文本用 pyautogui.write;中文和长文本用剪贴板 + Ctrl+V;Citrix/RDP 传大数据用 base64 + 分块 write。后台无焦点发送只对纯 Win32 窗口有效。

AI 控制浏览器的四种方案:插件 / Playwright / CDP / Browser Use

"AI 控制浏览器"目前有四类方案,权限差异非常大。做爬虫 / 自动化 / Agent 时选错方案会撞墙——比如插件方案下想调试 JS 是不可能的。 权限矩阵方案 操作 DOM 读 DOM 执行 JS 调试 JS Chrome DevTools 全权限 保留登录态Chrome 扩展 ✅ ✅ ✅ ❌ ❌ ✅Playwright / Puppeteer ✅ ✅ ✅ 部分 ❌ 需配置Chrome DevTools Protocol (CDP) ✅ ✅ ✅ ✅ ✅ ✅Browser Use / Open Operator ✅ ✅ ✅ 部分 部分 ✅方案一:Chrome 扩展 适合:日常使用中的 AI 助手(Sider、Harpa、Monica 等)、需要长期挂在浏览器里的工具。 做法:写一个 Chrome/Edge 扩展,通过 content script 读写 DOM: // content.js document.querySelector("input.search").value = "hello"; document.querySelector("button.submit").click();或者用扩展 API 拿到当前 tab 的 DOM 快照喂给 LLM: chrome.tabs.sendMessage(tabId, { type: "getDOM" }, response => { callLLM(response.html); });局限:无法调试 JS(没 DevTools 权限) 无法访问 Network / Sources / Performance 某些反爬网站可能识别扩展环境方案二:Playwright / Puppeteer 适合:爬虫、自动化测试、CI 里跑的一次性任务。 from playwright.sync_api import sync_playwrightwith sync_playwright() as p: browser = p.chromium.launch(headless=False) page = browser.new_page() page.goto("https://example.com") # 读 DOM html = page.content() # 执行 JS result = page.evaluate("() => document.title") # 点击 page.click("button.submit")配合 LLM:Playwright 抓页面 → 传 DOM 给模型 → 模型返回下一步操作 → Playwright 执行。 局限:每次启动是全新 profile,没登录态(除非 launch_persistent_context) 反自动化检测(Cloudflare、Datadome 之类)会挂 网站升级 selector 就崩有个绕检测的插件叫 playwright-stealth,能过大多数入门反爬。 方案三:Chrome DevTools Protocol(权限最高) 适合:需要完整 DevTools 能力的场景——JS 调试、Network 拦截、Performance profiling、逆向分析。 先以 debug 模式启动 Chrome: chrome.exe --remote-debugging-port=9222 --user-data-dir="D:\chrome-cdp"然后 Playwright 连过去(这里 Playwright 是 CDP 的高层封装): browser = p.chromium.connect_over_cdp("http://127.0.0.1:9222")或者直接用底层 CDP: from pychrome import Browser b = Browser(url="http://127.0.0.1:9222") tab = b.list_tab()[0] tab.start() tab.Runtime.evaluate(expression="document.title")# 拦截 Network tab.Network.enable() tab.call_method("Fetch.enable")# 设置断点 tab.Debugger.enable() tab.Debugger.setBreakpointByUrl(url=".../app.js", lineNumber=100)能力和你按 F12 一样。 方案四:Browser Use / Open Operator 类项目 近一两年新出的一批"给 LLM 用的浏览器 SDK"——比如 browser-use、Anthropic 的 Computer Use、OpenAI Operator: from browser_use import Agent from langchain_openai import ChatOpenAIagent = Agent( task="打开 GitHub 搜索 langchain 项目的最新 star 数", llm=ChatOpenAI(model="gpt-4o"), ) result = agent.run()这类 SDK 帮你封装好:页面截图 + 元素编号 Vision 模型选元素 Playwright 执行操作 失败自动重试写业务代码不用管 selector,直接说自然语言。代价:慢(每步 LLM 调用)、贵(大量 vision token)、有时候错。 怎么选场景 推荐用户浏览时给 AI 助手能力 Chrome 扩展定时任务 / CI 里跑 Playwright需要 JS 断点、Network 拦截、逆向 CDP想让 LLM 自主完成复杂多步任务 Browser Use 类反爬严的网站 CDP + stealth + 真实 profile一句话总结 日常插件、爬虫 Playwright、逆向调试 CDP、Agent 用 Browser Use——四种方案权限从低到高,按需要选。CDP 是"能上 F12 就能做的一切"的上限。

Python 多线程检查 HTTP 状态码并删除 403 文件

批量检查 data/*.json 里的图片 URL,删除所有返回 403 的 JSON 文件。 单线程版本 import json from pathlib import Pathimport requestsfor json_file in Path("data").glob("*.json"): try: data = json.loads(json_file.read_text(encoding="utf-8")) if not data: continue url = data[0].get("imgHttpUrl") if not url: continue r = requests.get(url, stream=True, timeout=10) if r.status_code == 403: print(f"删除 {json_file}") json_file.unlink() except Exception as e: print(json_file, e)只检查数组第一个元素的 URL,够快。若一个文件里所有 URL 都要检查,把 data[0] 改成循环即可。 多线程版本(100 并发) import json from pathlib import Path from concurrent.futures import ThreadPoolExecutor, as_completedimport requestsDATA_DIR = Path("data") MAX_WORKERS = 100session = requests.Session()def check_file(json_file: Path): try: data = json.loads(json_file.read_text(encoding="utf-8")) if not data: return url = data[0].get("imgHttpUrl") if not url: return # 优先用 HEAD,减少带宽 r = session.head(url, timeout=10, allow_redirects=True) # 部分服务器不支持 HEAD,降级到 GET if r.status_code == 405: r = session.get(url, stream=True, timeout=10) if r.status_code == 403: print(f"[删除] {json_file}") json_file.unlink() except Exception as e: print(f"[错误] {json_file}: {e}")files = list(DATA_DIR.glob("*.json"))with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: futures = [executor.submit(check_file, f) for f in files] for _ in as_completed(futures): passprint("完成")requests.Session 在多线程中是线程安全的,复用 TCP 连接比每次创建新 session 快。 并发数选择文件数 建议 workers< 1000 501000~10000 100> 10000 考虑 aiohttp + asyncioHTTP 请求是 IO 密集型,100 线程一般没问题。若服务端有限流,调小 MAX_WORKERS。 aiohttp 异步版(超大规模) 文件数超过 10 万时,aiohttp 比线程池快几倍: import asyncio import json from pathlib import Pathimport aiohttp import aiofilesDATA_DIR = Path("data") CONCURRENCY = 500async def check_file(session, json_file: Path): try: async with aiofiles.open(json_file, encoding="utf-8") as f: content = await f.read() data = json.loads(content) if not data: return url = data[0].get("imgHttpUrl") if not url: return async with session.head(url, allow_redirects=True, timeout=aiohttp.ClientTimeout(total=10)) as r: if r.status == 403: print(f"[删除] {json_file}") json_file.unlink() except Exception as e: print(f"[错误] {json_file}: {e}")async def main(): files = list(DATA_DIR.glob("*.json")) sem = asyncio.Semaphore(CONCURRENCY) async def bounded(f): async with sem: await check_file(session, f) async with aiohttp.ClientSession() as session: await asyncio.gather(*(bounded(f) for f in files))asyncio.run(main())按 answer 目录过滤删除(补充场景) 如果是 downloads/ 里没有对应 answer/ 文件就删除,可以改为本地比对: from pathlib import Pathdownloads_dir = Path("downloads") answer_dir = Path("answer")# answer 目录中所有文件名(不带后缀) answer_stems = {f.stem for f in answer_dir.iterdir() if f.is_file()}for file in downloads_dir.iterdir(): if not file.is_file(): continue if file.stem not in answer_stems: print("删除", file) file.unlink()不需要网络请求,纯本地文件对比,速度极快。

Python 多线程批量检查 URL 状态码:ThreadPoolExecutor + Session

一个真实场景:data/*.json,每个 JSON 是数组,数组元素里有 imgHttpUrl。想批量检查这些 URL 是不是 403(图片失效),是的话把整个 JSON 文件删掉。文件几千个,单线程要跑一天,加多线程。 单线程版本先跑通 import json from pathlib import Path import requestsfor json_file in Path("data").glob("*.json"): try: data = json.loads(json_file.read_text(encoding="utf-8")) if not data: continue url = data[0].get("imgHttpUrl") if not url: continue r = requests.get(url, stream=True, timeout=10) if r.status_code == 403: print(f"删除 {json_file}") json_file.unlink() except Exception as e: print(json_file, e)要点:只检查数组第一个元素(假设整个文件的图片来自同一源,一个 403 全体 403) stream=True — 只拿响应头,body 不下载,快 用 pathlib.Path.unlink() 删文件,比 os.remove 干净加多线程 IO 密集,直接 ThreadPoolExecutor 就行,不用协程: import json from pathlib import Path from concurrent.futures import ThreadPoolExecutor, as_completed import requestsDATA_DIR = Path("data") MAX_WORKERS = 100session = requests.Session()def check_file(json_file: Path): try: data = json.loads(json_file.read_text(encoding="utf-8")) if not data: return None url = data[0].get("imgHttpUrl") if not url: return None r = session.get(url, stream=True, timeout=10, allow_redirects=True) if r.status_code == 403: json_file.unlink() return json_file except Exception as e: print(f"[错误] {json_file}: {e}") return Nonefiles = list(DATA_DIR.glob("*.json")) deleted = []with ThreadPoolExecutor(max_workers=MAX_WORKERS) as ex: futures = {ex.submit(check_file, f): f for f in files} for fut in as_completed(futures): result = fut.result() if result: deleted.append(result)print(f"完成,共删除 {len(deleted)} 个文件")Session 为什么关键 用 requests.Session() 而不是每次 requests.get():复用 TCP 连接 + TLS 握手,快 5~10 倍 连接池自动管理 全局默认 headers / cookies批量请求同一个域名,Session 是必配的。 HEAD 还是 GET? 只判状态码,不需要 body,理论上 HEAD 更快: r = session.head(url, timeout=10, allow_redirects=True)但:CDN / 图片服务器可能不支持 HEAD,返回 405 Method Not Allowed(常见于阿里 OSS 未开该权限、部分反爬策略) 有些服务器 HEAD 返回和 GET 不同的状态码保险起见先 HEAD、失败降级 GET: def check(url): try: r = session.head(url, timeout=10, allow_redirects=True) if r.status_code == 405: r = session.get(url, stream=True, timeout=10, allow_redirects=True) return r.status_code except requests.RequestException: return None线程数怎么调CPU 密集:os.cpu_count() 一般顶 IO 密集(网络请求):可以开几十甚至几百 目标服务器扛得住多少:看它——同源打 100 并发容易被 WAF ban,不同源随便开试起来:从 20 开始,看服务器响应正常、CPU 也没打满、就慢慢加。100 是 Python requests 常见上限。 更快:httpx + asyncio 千 URL 级别 ThreadPoolExecutor 够用。上万级建议改 asyncio: import asyncio, httpx, json from pathlib import Pathasync def check(client, json_file): try: data = json.loads(json_file.read_text(encoding="utf-8")) if not data: return url = data[0].get("imgHttpUrl") if not url: return r = await client.head(url, follow_redirects=True, timeout=10) if r.status_code == 403: json_file.unlink() print(f"删除 {json_file}") except Exception as e: print(json_file, e)async def main(): files = list(Path("data").glob("*.json")) limits = httpx.Limits(max_connections=200) async with httpx.AsyncClient(limits=limits) as client: await asyncio.gather(*(check(client, f) for f in files))asyncio.run(main())asyncio + httpx 可以轻松跑到 1000+ 并发,机器压力更小。 一句话总结 IO 密集批量请求 = ThreadPoolExecutor + Session。开 50-100 线程,先 HEAD 再降级 GET。上万量级换 asyncio + httpx。

剪映批量导出自动化:AutoHotkey、draft_content.json 和 FFmpeg 三种方案

三种方案对比方案 适用场景 优点 缺点AutoHotkey 模拟 UI 几十个工程、保留特效 最稳、兼容所有剪映功能 慢、易被 UI 改版搞炸修改 draft_content.json 矩阵号、批量换素材/文案 工业化效率高 剪映 6+ 有加密问题FFmpeg / MoviePy 图片转视频、批量混剪 服务器可跑、不依赖剪映 无法保留剪映特效方案一:AutoHotkey(最稳) 适合"几十个工程挨个导出"的场景,不需要了解剪映内部格式: Loop, Files, D:\jianying_projects\*.draft { Run, "C:\Program Files\JianyingPro\JianyingPro.exe" "%A_LoopFileFullPath%" Sleep, 10000 ; 点击导出按钮(坐标需根据实际分辨率调整) Click, 1800, 1000 Sleep, 3000 ; 点击确认导出 Click, 1700, 950 ; 等待渲染完成 Sleep, 60000 WinClose, 剪映专业版 }坐标需要根据自己的屏幕分辨率和剪映版本调整。可以用 AHK 内置的 MouseGetPos 命令在悬停时获取当前坐标。 方案二:修改 draft_content.json(矩阵号核心) 剪映草稿本质是 JSON,位于草稿目录下的 draft_content.json。批量替换流程: 模板工程 ↓ Python 复制草稿目录 ↓ 修改 draft_content.json(替换素材路径/字幕文本) ↓ 生成 N 个草稿 ↓ AutoHotkey 自动打开逐个导出Python 批量复制并替换字幕示例: import shutil import json import re from pathlib import Pathtemplate = Path("D:/drafts/template") output_base = Path("D:/drafts/output")texts = ["第一集内容", "第二集内容", "第三集内容"]for i, text in enumerate(texts): dst = output_base / f"episode_{i+1}" shutil.copytree(template, dst) draft_file = dst / "draft_content.json" data = json.loads(draft_file.read_text(encoding="utf-8")) # 替换字幕文本(具体字段名以实际工程为准) content_str = json.dumps(data, ensure_ascii=False) content_str = re.sub(r'"占位文本"', f'"{text}"', content_str) draft_file.write_text(content_str, encoding="utf-8")注意:剪映 6+ 加密问题 从剪映专业版 6.x 开始,draft_content.json 部分字段开始加密,很多老方案失效。目前社区常见的做法:锁定在剪映 5.9 不升级 使用开源项目如 pyJianYingDraft 或 capcut-cli方案三:FFmpeg(不经过剪映) 如果只是图片转视频、批量加字幕、批量合并,完全不需要剪映: # 图片+音频合成视频 ffmpeg -loop 1 -i cover.jpg -i bgm.mp3 \ -shortest \ -vf "scale=1920:1080" \ output.mp4# 批量处理(bash) for img in *.jpg; do ffmpeg -loop 1 -i "$img" -i bgm.mp3 -shortest -vf "scale=1920:1080" "${img%.jpg}.mp4" doneFFmpeg 的缺点是没有剪映的智能字幕、卡点、AI 特效,对"剪映味"的视频替代不了。 实际推荐路线(混合方案) Python 改草稿 JSON(批量换内容) + 剪映负责特效渲染 + AutoHotkey 自动导出剪映最有价值的是特效引擎和 AI 功能,时间轴批量生成用代码做,特效用剪映渲染,导出用自动化点击。这是目前短视频矩阵账号的主流技术路线。

Playwright 定位网页元素:getByText / getByRole 比 CSS Selector 稳在哪

用 Playwright 自动化网页时,很多人习惯用 CSS selector(page.locator(".btn-submit")),但这类 selector 抗变化能力差——前端一改 class 名就失效。Playwright 提供了一套语义化定位器,稳定性高很多。 语义化定位器 getByText 按可见文本定位,最常用: await page.getByText('登录').click(); await page.getByText('提交订单').click();支持正则: await page.getByText(/确认.*订单/).click();getByRole 按 ARIA role 定位,最语义化: await page.getByRole('button', { name: '提交' }).click(); await page.getByRole('link', { name: '首页' }).click(); await page.getByRole('textbox', { name: '搜索' }).fill('hello');常用 role:button、link、textbox、checkbox、listbox、dialog、heading getByPlaceholder 按 input placeholder 定位: await page.getByPlaceholder('请输入手机号').fill('138xxxx'); await page.getByPlaceholder('密码').fill('password123');getByLabel 按 <label> 文本定位关联的 input: await page.getByLabel('邮箱').fill('test@example.com'); await page.getByLabel('记住我').check();getByTestId 按 data-testid 属性,测试环境最稳定的方案: // HTML: <button data-testid="submit-btn">提交</button> await page.getByTestId('submit-btn').click();为什么比 CSS Selector 稳定位方式 变化敏感度 说明.btn-primary-v2 高 class 名经常随版本变#submit > div > button 高 DOM 结构一变就失效getByText('提交') 低 文本变了才失效getByRole('button', {name: '提交'}) 低 语义稳定getByTestId('submit') 最低 开发者明确标记处理复杂场景 多个相同文本时,用 nth 或组合定位: // 第二个"删除"按钮 await page.getByText('删除').nth(1).click();// 在某个容器内查找 await page.locator('.order-list').getByRole('button', { name: '删除' }).first().click();等待元素可交互: await page.getByRole('button', { name: '提交' }).waitFor({ state: 'visible' }); await page.getByRole('button', { name: '提交' }).click();真正难处理的场景 语义化定位器解决不了的几种情况,才需要 AI Vision Agent:Canvas 元素(无 DOM 结构) Shadow DOM 内的元素 动态随机 class 名(某些 React 框架) 无文字的图标按钮 iframe 跨域内容对于这些场景,可以用 Stagehand 等 AI 驱动的自动化工具: await page.act("点击右上角的关闭按钮");让大模型看截图决定点击位置,不依赖 selector。 实际推荐策略优先用 getByRole + getByText,覆盖 80% 常规场景 次选 getByPlaceholder / getByLabel(表单场景) 开发阶段加 data-testid,测试最稳定 Shadow DOM / Canvas 等场景才用 AI Vision不要一开始就上 AI Vision,定位器够用的情况下更快、更稳、Token 消耗为零。

Python 自动安装 Chrome 插件:策略强制 vs 开发者模式加载

写脚本自动帮机器装个 Chrome 插件——听着简单。Chrome 团队故意不让你静默装本地 crx(安全策略,不是技术问题)。想绕开有几条现实可行的路。 路线一:企业策略强制安装(Web Store 上架的插件) Chrome 支持通过 Windows 注册表 / macOS plist / Linux JSON 策略,指定"必须自动装某扩展"。这是 IT 部门批量部署的标准做法。 前提是扩展在 Chrome Web Store 上架——策略只接受 update URL 拉取,不认本地 crx。 Windows 注册表方式 import winregEXT_ID = "aapocclcgogkmnckokdopfmhonfmgoek" # 举例:Google Docs Offline UPDATE_URL = "https://clients2.google.com/service/update2/crx"path = r"SOFTWARE\Policies\Google\Chrome\ExtensionInstallForcelist"key = winreg.CreateKey(winreg.HKEY_LOCAL_MACHINE, path) winreg.SetValueEx(key, "1", 0, winreg.REG_SZ, f"{EXT_ID};{UPDATE_URL}") winreg.CloseKey(key)写完重启 Chrome,扩展会自动安装、无法卸载。 注意:需要管理员权限 ExtensionInstallForcelist 的编号("1"、"2" ...)要不重复 只支持 Web Store 上架的扩展;企业内部扩展需要自建 update.xml配套删除: import winreg key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, path, 0, winreg.KEY_WRITE) winreg.DeleteValue(key, "1")路线二:Selenium 启动时加载本地插件 不做正式安装,只是启动 Chrome 时把插件目录加载进来——最适合自动化脚本、爬虫、批量测试。 加载解压后的目录(源码形式): from selenium import webdriver from selenium.webdriver.chrome.options import Optionsopts = Options() opts.add_argument(r"--load-extension=D:\extensions\my-plugin")driver = webdriver.Chrome(options=opts) driver.get("https://example.com")加载已打包的 crx: opts = Options() opts.add_extension(r"D:\extensions\my-plugin.crx")局限:不会真的"安装",关闭浏览器就没了 需要 Selenium 控制的 Chrome,不影响用户日常浏览器 Chrome 每次启动会有"以开发者模式使用未打包扩展"警告条路线三:Playwright 加载扩展 Playwright 加载扩展要用 persistent context,且必须用 Chromium(Chrome 分发版不能通过所有测试): from playwright.sync_api import sync_playwrightwith sync_playwright() as p: ctx = p.chromium.launch_persistent_context( user_data_dir="D:/tmp/pw-profile", headless=False, args=[ "--disable-extensions-except=D:\\extensions\\my-plugin", "--load-extension=D:\\extensions\\my-plugin", ], ) page = ctx.new_page() page.goto("https://example.com")不推荐的路线 UI 自动化拖 crx 到 chrome://extensions/:Chrome 会弹确认框、快捷键会变、chrome 版本升级后经常挂。别在这个方向上死磕。 改本地 Extensions 目录里的 Preferences JSON:Chrome 会检测校验和,第二次启动直接把你写进去的扩展禁用掉。 场景对应目的 用什么公司/网吧批量部署 Web Store 扩展 策略强制安装爬虫 / 自动化脚本用自己写的扩展 Selenium 加载需要长期挂着扩展,用户不能卸载 策略强制只是测试自己开发中的插件 Chrome 开发者模式手动一句话总结 要"真安装"就上企业策略(Web Store 扩展),要"临时加载"就 Selenium/Playwright --load-extension。Chrome 就是不让你静默装本地 crx,别硬撞。

油猴脚本虚拟文件输入 + FileReader 封装

在油猴脚本里读取本地文件,思路是:动态创建隐藏的 <input type="file">,触发点击,再用 FileReader 读取内容。 封装函数 /** * 选择文件并读取内容 * @param {Object} options * @param {string} [options.accept] - 文件类型限制,如 ".json,.txt" * @param {string} [options.readAs="text"] - text | dataURL | arrayBuffer * @param {string} [options.encoding="utf-8"] - 文本编码(仅 text 模式有效) * @returns {Promise<{file: File, content: any}>} */ function selectFileAndRead(options = {}) { const { accept = "", readAs = "text", encoding = "utf-8" } = options; return new Promise((resolve, reject) => { let input = document.getElementById("__file_input__"); if (!input) { input = document.createElement("input"); input.type = "file"; input.id = "__file_input__"; input.style.display = "none"; document.body.appendChild(input); } input.accept = accept; input.onchange = () => { const file = input.files[0]; if (!file) { reject(new Error("未选择文件")); return; } const reader = new FileReader(); reader.onload = (e) => resolve({ file, content: e.target.result }); reader.onerror = () => reject(new Error("读取失败")); switch (readAs) { case "dataURL": reader.readAsDataURL(file); break; case "arrayBuffer": reader.readAsArrayBuffer(file); break; default: reader.readAsText(file, encoding); } }; // 必须重置 value,否则选同一个文件不会触发 change 事件 input.value = ""; input.click(); }); }使用示例 读取文本文件 selectFileAndRead({ accept: ".txt,.json" }).then(({ file, content }) => { console.log(file.name, content); });读取 JSON 并解析 selectFileAndRead({ accept: ".json" }) .then(({ content }) => JSON.parse(content)) .then((data) => console.log("JSON 数据:", data)) .catch((err) => console.error(err));读取图片为 base64 selectFileAndRead({ accept: "image/*", readAs: "dataURL" }).then( ({ content }) => { const img = document.createElement("img"); img.src = content; document.body.appendChild(img); } );async/await 版 document.addEventListener("keydown", async (e) => { if (e.ctrlKey && e.key === "u") { try { const { file, content } = await selectFileAndRead({ accept: ".json" }); const config = JSON.parse(content); applyConfig(config); } catch (err) { console.error(err); } } });关键坑 浏览器安全限制:input.click() 必须在用户事件(点击、键盘)的同步调用链里触发,不能在 setTimeout 或 Promise 回调里调用,否则会被拦截。 同一文件无法重复选择:input.value = "" 在 click() 前必须执行,否则选同一个文件第二次不会触发 change 事件。 中文乱码:Windows 下生成的文件有时是 GBK 编码: selectFileAndRead({ encoding: "gbk" });DOM 污染:函数复用同一个 __file_input__ 元素,不会重复创建,避免 DOM 堆积导致的性能问题。 在油猴脚本中的完整示例 // ==UserScript== // @name 配置文件导入 // @namespace http://tampermonkey.net/ // @version 1.0 // @match https://example.com/* // @grant none // ==/UserScript==(function () { "use strict"; // 插入导入按钮 const btn = document.createElement("button"); btn.textContent = "导入配置"; btn.style.cssText = "position:fixed;top:10px;right:10px;z-index:9999"; document.body.appendChild(btn); btn.addEventListener("click", async () => { const { content } = await selectFileAndRead({ accept: ".json" }); const config = JSON.parse(content); console.log("已加载配置:", config); }); // ... selectFileAndRead 函数定义 ... })();