资讯动态

油猴脚本开发实战:用JavaScript轻松实现网页自动化增强

发布时间:2026/9/18 22:23:15 来源:尧图企业网站定制
我最近被一个内部管理系统折磨得够呛上午要登录后台把一个表格里几十条记录的状态逐条改掉每条都必须点开详情页下拉选择、保存、返回一套流程下来半个多小时没了。刚开始我选择在浏览器控制台写脚本刷新一下就得重新粘一遍后来想干脆做个浏览器插件但又觉得打包上架太折腾。最后认真用起了油猴脚本才算是找到了最顺手的平衡点。油猴脚本说白了就是一个运行在浏览器里的用户脚本管理器你写一小段 JavaScript它负责在你指定的网页加载时帮你在合适的时机注入执行。你不需要会打包扩展、不需要配置复杂的构建工具一个文本文件保存即生效改起来也方便。这篇教程适合有一点 JavaScript 基础、但没写过油猴脚本的人我会从一个最小可用的原型开始到完整的页面增强脚本再到常见的排查思路争取让你看完就能直接开始写自己的第一款脚本。1. 从“为什么需要油猴脚本”开始用户脚本解决的真正问题1.1 一个用户脚本到底做了什么油猴脚本在业内通常叫“用户脚本”userscript管理脚本的扩展有很多最常用的是 Tampermonkey中文圈俗称“油猴”。还有 Greasemonkey、Violentmonkey 等同类管理器。它们做的事情本质上都是同一件事根据你在脚本头部的元信息配置在特定网站打开时把脚本代码注入到页面里执行。你可以把用户脚本理解成给网页“打补丁”。有些网站有功能缺陷、交互不合理、信息展示不直观或者你的日常操作里存在大量重复点击你都可以通过脚本去修改页面显示、自动填表、监听键盘事件、调用远程接口甚至像写小应用一样给页面加一个全新的侧边栏。相比浏览器插件用户脚本的最大优势是轻量插件需要 manifest.json、图标、权限声明、可能的 background service worker还要经过 store 审核而用户脚本就是一个带特定注释块标记的 JS 文件管理器负责处理浏览器扩展层的复杂事情你只要关心业务逻辑。还有一个容易被忽视的点用户脚本的安装和更新非常轻松在 GreasyFork 这类脚本仓库里点一下安装就完事更新时管理器也会提示。团队成员之间共享一个脚本文件也远比共享一个扩展要简单。1.2 适用场景与不适合的场景以我自己的使用经验来说用户脚本最适合以下场景页面操作增强给列表页加快捷键、合并重复按钮、增加批量选择功能。自动填表针对内部系统或日常办公页面自动填入常用内容减少重复劳动。信息整合把多个页面上分散的信息抓下来汇总到一块展示。布局优化把冗长的侧边栏折叠、把关键信息置顶、调整表格列宽。跨系统数据搬运从一个系统读到数据整理后填入另一个系统。但也有不适合的场景我建议一开始就避开不要拿用户脚本去做绕过登录、破解验证码、绕过反爬机制或未授权批量抓取数据的事。技术上可能做得到但这既可能违反目标网站的使用条款也可能触碰法律红线。学这项技术的目的是提升效率、优化体验不是钻空子。在当前安全合规的前提下把脚本用在自己的内部系统、自己掌握控制权的网站或者明确允许自动化操作的公开网站上才是稳妥的理解方式。了解这些边界之后就可以准备正式开发了。2. 搭建第一个脚本从安装到最小可用原型的完整过程2.1 环境准备与油猴扩展安装开发用户脚本不需要安装 Node.js也不需要装任何框架你只需要一个现代浏览器和对应的脚本管理器扩展。推荐 Tampermonkey原因是它在 Chrome、Edge、Firefox 里都能用且它的脚本兼容做得最完整就算你之后想用脚本管理器的额外 APITampermonkey 也是支持最全的。安装方式很简单在浏览器的扩展商店搜索“Tampermonkey”点安装即可。在 Microsoft Edge 的加载项商店里也有官方版本。安装完成后浏览器工具栏会出现一个图标。打开任意网页点击图标就能看到当前页面有多少条用户脚本在运行、是否可以管理脚本。点“管理面板”或“Dashboard”里面可以看到所有已安装脚本、脚本的执行日志如果有报错也会显示和存储数据。这里有一个新手常常忽略的点安装完扩展后如果要让脚本在某个网站上生效必须确认这个网站地址和脚本里的match规则匹配。很多“我明明安装了脚本为什么没反应”的问题其实都是匹配规则没写对。2.2 新建脚本与元信息块解读在管理面板里点击“新建用户脚本”会生成一个模板。先不必急着写代码我们来一行一行理解开头那段注释块它叫“UserScript 头部元信息”。// UserScript // name 示例脚本 // namespace com.example.my-first-script // version 0.1.0 // description 一个演示用途的脚本 // match https://example.com/* // grant none // run-at document-idle // /UserScript字段作用name脚本显示名称同一个管理器中建议保持唯一namespace区分同名脚本的命名空间一般用域名倒序格式避免和别人冲突version版本号发布到脚本仓库时用于升版提示description脚本的简要说明让别人和我自己一眼看懂用途match限定脚本在哪些网址运行非常重要grant声明脚本能使用哪些增强 APInone表示只用原生 JSrun-at脚本注入到页面的时机影响后续操作 DOM 的稳定性noframes如果不写脚本默认也会注入到页面内的 iframe 中加上它就只在主框架执行match的写法很容易搞错。它是一个 URL 匹配模式由协议、域名、路径组成*可以匹配一定范围。https://example.com/*表示匹配该域下所有路径但不匹配https://sub.example.com/*。如果目标站点有二级域名就得写https://*.example.com/*。另外要注意http和https是两种不同协议如果你希望两个协议都生效可以写两行match。做测试时直接用http://localhost:8080/*或https://www.baidu.com/*这类已知地址最省心。2.3 第一个能跑的脚本新建脚本后把内容改成一个最简单的版本// UserScript // name 我的第一个脚本 // namespace com.example.first // version 0.1.0 // description 页面加载完成后弹个提示 // match *://*/* // grant none // run-at document-idle // /UserScript (function () { use strict; console.log(我的第一个用户脚本开始运行了); alert(脚本注入成功); })();保存后打开任意一个网页如果看到了 alert 弹窗就说明注入链路已经通了。这里需要注意match *://*/*会在所有网站生效实际开发时不要这样写否则你会在每一个网页都被弹窗打扰这只是测试用的万能匹配模式。为什么不把这个脚本写成console.log完就结束因为首次测试的关键不是功能本身而是确认三件事脚本是否被管理器正确识别、匹配规则是否生效、代码是否成功注入到页面执行环境。能弹出 alert就说明这三点都通过了。之后再写复杂逻辑时至少可以排除脚本机制层的错误。3. 真正动手给一个列表页添加批量操作的完整实现3.1 先确认需求与技术选型想象中的学习过程应该是“看完语法直接动手”。但写用户脚本前更重要的其实是“明确需求边界”。我说一下我实际写的第一个正经脚本的需求。某个内部管理页面里有一个表格每行末尾有几个按钮查看、通过、退回。每天要处理一大批记录每一条都需要先在详情页确认信息再回到列表点“通过”或者“退回”。如果只准备右一个普通的“通过当前行”快捷键简单到代但实际高频场景是用户正聚焦在某条记录的评价输入框里按下快捷键就得立刻操作对应行。所以我决定做一个这样的脚本用键盘上的1、2、3三个数字键分别代表“查看详情”“通过”“退回”并且通过当前焦点所在的输入框或按钮自动定位它属于表格中的哪一行然后执行对应操作。这个方案不替换原有按钮而是作为补充避免影响其他人的使用习惯。技术选型上我没有用 jQuery也没有用任何框架。原因是用户脚本运行在页面环境中引入外部库会带来两个问题一是受当前页面 CSP内容安全策略即页面告诉浏览器“只允许执行来自哪些来源的脚本”限制某些网站根本不允许执行外部脚本二是引入库本身增加了脚本的体积和失败风险。现在原生 JS 已经足够强大querySelector、closest、Event足以解决绝大多数需求。3.2 事件委托为什么动态页面要用它新手最容易踩的坑是直接给按钮绑定点击事件像这样const buttons document.querySelectorAll(.approve-btn); buttons.forEach(btn btn.addEventListener(click, handler));这段代码放在脚本里页面初次加载时确实有效。但如果页面是 SPA单页应用切换路由后表格重新渲染新按钮已经不在buttons集合里了点击事件自然失效。又或者你的脚本运行时机比页面渲染早document.querySelectorAll结果为空事件绑定就完全没发生。正确做法是使用事件委托把监听器绑定在一个不会变化的祖先元素上利用事件冒泡特性检查实际点击的元素是否满足条件。这样不管表格重渲染多少次、新增多少行只要事件能冒泡到监听器就能正确处理。document.addEventListener(click, function (event) { const approveBtn event.target.closest(.approve-btn); if (approveBtn) { // 处理通过操作 } });如果你的页面里没有合适的外层容器直接挂在document上最简单。用closest方法可以很方便地从实际点击元素向上查找找到匹配选择器的最近祖先元素。这个方法在动态渲染场景下几乎不会失效也避开了不断绑定和解绑的麻烦。3.3 完整脚本示例下面就是我的列表页快捷键脚本的完整版本我加了一些注释// UserScript // name 列表页快捷键操作 // namespace com.example.list-hotkey // version 1.0.0 // description 在列表页用数字键快速操作当前行 // match https://internal.example.com/approve/list // grant none // run-at document-idle // /UserScript (function () { use strict; // 通过当前焦点元素向上查找定位这一行 function findRowFromFocusedElement() { const activeEl document.activeElement; if (!activeEl) return null; return activeEl.closest(tr); } function handleApprove() { const row findRowFromFocusedElement(); if (!row) return; const approveBtn row.querySelector(.approve-btn); if (approveBtn) { approveBtn.click(); showToast(已点击通过); } else { console.warn(当前行未找到通过按钮); } } function handleReject() { const row findRowFromFocusedElement(); if (!row) return; const reasonInput row.querySelector(.reject-reason-input); if (reasonInput) { reasonInput.value 材料不全请补充; } const rejectBtn row.querySelector(.reject-btn); if (rejectBtn) rejectBtn.click(); } function handleView() { const row findRowFromFocusedElement(); if (!row) return; const viewLink row.querySelector(a.view-detail); if (viewLink) window.open(viewLink.href, _blank); } // 简单的页面提示条 function showToast(message) { let toast document.getElementById(my-script-toast); if (!toast) { toast document.createElement(div); toast.id my-script-toast; toast.style.cssText position:fixed;bottom:20px;right:20px;z-index:99999; background:#333;color:#fff;padding:8px 16px;border-radius:4px;opacity:0.9;; document.body.appendChild(toast); } toast.textContent message; setTimeout(() { toast.remove(); }, 1500); } document.addEventListener(keydown, function (event) { // 优先响应输入框里的数字键避免用户打字时误触 const tagName document.activeElement.tagName; if (tagName INPUT || tagName TEXTAREA || tagName SELECT) { return; } if (event.key 1) { handleView(); event.preventDefault(); } else if (event.key 2) { handleApprove(); event.preventDefault(); } else if (event.key 3) { handleReject(); event.preventDefault(); } }); })();这里面有几个细节值得展开说。handleReject里我除了点击按钮还会先填好退回原因。这是很多脚本新手容易忘的很多操作不是单点一个按钮就完事了往往还伴随着表单状态、输入框内容、确认弹窗等前置条件。写脚本时一定要先完整看一遍人工操作流程把每一个步骤都拆出来再决定脚本需要干预哪几个环节。document.activeElement是当前获得焦点的元素。我之所以用它定位当前行是因为实际使用场景里用户通常是刚在某一行输入框里填完内容然后用快捷键操作。用焦点定位比用鼠标悬停定位可靠得多——不需要额外监听鼠标移动也不担心 hover 状态引起误操作。在keydown监听里我还做了判断如果焦点在输入框、文本域或下拉框里就直接返回不响应快捷键。这个判断极其重要不然用户在搜索框里打数字时就会触发脚本操作后果是灾难性的。4. 操作页面元素的核心机制时机、动态DOM与防抖4.1run-at到底怎么选上一章的脚本使用了run-at document-idle意思是等页面解析完成、DOM 基本就绪后执行。这个时机适合绝大多数场景但它不是万能的。run-at常见有四个取值document-start页面还没开始解析时注入执行最早可以拦截请求、修改document上的初始属性但此时 DOM 基本是空的想操作元素必须等后续节点出现。document-endDOM 解析完成后执行此时 DOM 已经完整很多静态页面可以直接操作元素。document-idle在document-end之后执行是 Tampermonkey 推荐的默认值确保页面结构基本稳定。document-ready部分旧脚本会用实际含义类似document-end但兼容性不如前面几种明确。怎么选择如果你只是给普通页面加按钮、改样式用document-idle即可。如果你要监听 ajax 响应、改最早期行为或者要拦截某些初始化逻辑才考虑document-start。但要注意document-start阶段执行时 DOM 还没生成如果你一上来就document.querySelector拿到的一定是null。我遇到过一个具体场景某个系统的表格是异步加载的页面骨架先渲染出来数据通过 ajax 请求之后才往表格里填充行。用document-idle执行的话脚本开始运行那一刻表格还是空的。这时候单纯靠运行时机解决不了问题需要引入对 DOM 变化的监听。4.2 MutationObserver监听动态加载的节点MutationObserver是浏览器提供的原生 API可以监听一个目标节点的子树变化包括子节点添加、删除、属性变化等。当页面用的是异步渲染时它是我们做脚本增强的好帮手。举个简单例子我们需要在异步表格加载后给每一行添加一个“复制标题”按钮function initCopyButtons(table) { const rows table.querySelectorAll(tr); rows.forEach(row { if (row.querySelector(.copy-title-btn)) return; // 避免重复添加 const titleCell row.querySelector(.title-cell); if (!titleCell) return; const btn document.createElement(button); btn.textContent 复制标题; btn.className copy-title-btn; btn.addEventListener(click, () { navigator.clipboard.writeText(titleCell.textContent.trim()); }); row.appendChild(btn); }); } // 观察表格容器 const observer new MutationObserver(mutations { // 简单做个防抖避免连续的 DOM 变化疯狂触发 clearTimeout(window.__debounceTimer); window.__debounceTimer setTimeout(() { const table document.querySelector(#data-table); if (table) initCopyButtons(table); }, 300); }); const container document.querySelector(#table-container); if (container) { observer.observe(container, { childList: true, subtree: true }); }这里的逻辑是任何子节点增删都会触发回调但我们可以等一下再看等最终达到一个稳定状态后集中处理。debounce的作用就是把 100 次连续触发合并成一次 300ms 后的统一执行避免了无谓的重复遍历和重复添加。为什么必须在回调里检查“是否已添加过按钮”因为表格可能反复重渲染MutationObserver 可能会被触发多次如果不做幂等处理按钮会被加一遍又一遍。这就是新手常说的“按钮怎么变多了”的原因。4.3 防抖与一次性执行避免重复绑定除了 DOM 重复添加资源浪费的另一个来源是事件监听器的重复绑定。有些人一上来在定时器里每分钟执行一次做元素增强结果事件越绑越多页面越来越卡。保持警惕的关键是给增强逻辑设计一个标志位或使用防抖函数。let initialized false; function enhancePage() { if (initialized) return; initialized true; // 真正的初始化逻辑 }用initialized标志位或者直接利用WeakSet记录已经处理过的元素const processed new WeakSet(); function enhanceNode(node) { if (processed.has(node)) return; processed.add(node); // 对 node 进行处理 }WeakSet的好处是不会对节点产生强引用不会干扰垃圾回收当元素被移除后WeakSet 里的引用也会自动释放。写到这里你会发现用户脚本虽然运行在“页面里”但开发者眼里看到的却是一个不断变化的动态页面。你不能假设“页面只会加载一次”而应该把每次 DOM 变化都当成可能的新环境来应对。这份思维转变是用户脚本开发里最核心的一个坎。5. 与页面“外面”的资源打交道GM_* API能干什么用户脚本和普通页面脚本最大的区别在于脚本管理器额外提供了一批GM_*开头的 API。这些 API 让用户脚本拥有“跨过页面限制”的能力但也因为它们能做的事情很多使用时更要谨慎。5.1 本地存储GM_setValue 与 GM_getValue你可能想用localStorage保存脚本的自定义配置但有个问题页面脚本访问的是该域名的 localStorage如果你的脚本跑在 A 网站写的值也只有 A 网站能读这没问题但用户脚本管理的配置和脚本本身是逐个注册的最好用专门的 API 存而不是污染页面的 localStorage。GM_setValue可以把数据保存到扩展的存储区域不干扰页面里的其他逻辑。比如保存一个“上次处理到第几行”的标记// UserScript // name 带存储的脚本 // grant GM_setValue // grant GM_getValue // /UserScript const lastIndex GM_getValue(lastIndex, 0); GM_setValue(lastIndex, 42);使用GM_*API 时必须在脚本头部声明对应的grant否则脚本管理器会把 API 视为未定义。比如上面示例里就必须写// grant GM_setValue和// grant GM_getValue。值得强调的是如果你把grant设成none说明脚本不需要任何额外权限此时 Tampermonkey 会以更接近页面脚本的方式运行避免一些页面环境冲突。一旦你使用GM_setValue就触发了扩展沙箱机制脚本的作用域会发生变化。所以写脚本时要在头部清楚声明需要的权限。5.2 跨域请求GM_xmlhttpRequest 的边界浏览器同源策略限制页面里的fetch和XMLHttpRequest只能访问同源地址。但用户脚本通过扩展的能力可以发起跨域请求只要在脚本头部配置connect声明允许访问的域名。示例把当前页面标题提交到你自己部署的统计接口。// UserScript // name 跨域提交示例 // match https://example.com/* // grant GM_xmlhttpRequest // connect api.my-server.com // /UserScript GM_xmlhttpRequest({ method: POST, url: https://api.my-server.com/receive, data: JSON.stringify({ title: document.title, url: location.href }), headers: { Content-Type: application/json }, onload: function (response) { console.log(提交成功, response.responseText); }, onerror: function (error) { console.error(提交失败, error); } });这个能力非常强大但要有边界意识。合法使用的前提是你要请求的接口是你自己有权限访问的接口或者目标服务明确允许这种自动化调用。在没有授权的情况下用GM_xmlhttpRequest去遍历某个网站未公开的接口、批量拉取数据、尝试未授权的操作这是不对的也不建议出现在任何教学示例里。我在这里只把它作为一种“自己的脚本向着自己的服务通信”的合法方案来展示。另外注意connect支持域名和*但不要随便用*。精确声明要访问的域名既有利于你自己检查脚本行为也避免误触危险地址还能在调试时更快定位问题。5.3 注入样式与打开页面GM_addStyle是一个非常实用的 API。它可以把一段 CSS 字符串注入到页面中方便修改目标页面的排版。用它来给脚本生成的按钮添加样式比在 JS 里逐个设置style属性清爽得多。// UserScript // grant GM_addStyle // /UserScript GM_addStyle( .my-script-button { background-color: #4caf50; color: white; border: none; padding: 4px 10px; border-radius: 3px; cursor: pointer; } );GM_openInTab可以打开一个新的浏览器标签页GM_notification可以发出系统通知。这两个功能用得不多但在处理“需要打开详情页核实”的场景时挺好用。我通常会配合window.open来使用因为它比window.open更可控还能指定是否激活新标签页。熟悉这些 API 之后你的用户脚本就不再只是“操作 DOM 的小工具”而是一个拥有本地存储、跨域通信、系统通知能力的轻量级应用。但能力边界越大就越要在写脚本时反复问自己这个操作合法吗目标网站允许吗有没有更简单的方案6. “脚本写了却没反应”排查链我把踩过的坑都理了一遍无论是我还是周围的朋友写用户脚本初期几乎都经历过“脚本明明安装了页面却毫无反应”的时刻。这里我总结一套排查链路按顺序走完大多数问题都能定位。6.1 从控制台到元信息逐层排查第一步打开有问题的页面按 F12 打开开发者工具切到 Console 面板。如果脚本里有console.log这里应该会看到输出。没有输出说明脚本根本没有执行问题多半出在脚本匹配或管理器配置层面。第二步点击浏览器工具栏上的 Tampermonkey 图标查看“脚本运行状态”。它能明确告诉你当前页面有多少脚本被匹配到了。如果显示“未运行”十有八九是match写得不匹配当前 URL。常见错误包括协议写错https写成了https拼写低级的笔误、域名二级问题、路径带了多余参数没有用*代替。第三步如果控制台有报错仔细看错误信息。常见的有一类错误是“Cannot read properties of null (reading xxx)”说明脚本执行时机太早操作的 DOM 节点还没生成。这时要么把run-at改成document-idle要么用 MutationObserver 等待节点出现。第四步检查页面是否有 CSP。内容安全策略CSP是网站通过响应头告诉浏览器“允许执行哪些脚本”的规则。如果网站设置了非常严格的 CSP可能会阻止 Tampermonkey 注入的脚本或者限制它执行某些操作。最典型的表现是脚本在控制台报 CSP violation。这种时候通常只能换一种方式实现或者用能在document-start阶段注入并绕过部分限制的策略但严格 CSP 网站确实不适合做过于侵入的脚本。6.2 选择器失效与缓存问题还有一种很隐蔽的问题match匹配了控制台也没有明显报错但脚本就是没有达到预期效果。这种情况大概率出在 DOM 选择器上。页面的 class 名称是常变的尤其是前端工程化之后类名可能是构建后自动生成的随机字符串。你写死的.approve-btn在新的部署里可能已经变成了.css-1a2b3。我见过不少脚本作者在代码里写死类名最后页面一改版脚本就崩。处理办法有两个优先用更稳定的选择器比如带id属性的元素、>const DEBUG true; function debug(...args) { if (DEBUG) { console.log([MyScript], ...args); } }跑完一轮后把这个开关关掉就不用等脚本出问题再回来删日志。这个习惯特别适合用户脚本这种“写起来全靠自己观察”的场景。还有一种情况是页面上有 iframe你的脚本只看了主文档。如果你确定内容在 iframe 里需要把匹配规则或选择器换成 iframe 内的 document。Tampermonkey 默认会把脚本注入到所有 iframe 里但noframes会阻止这种行为如果加了noframes却想操作 iframe那就只能主动获取 iframe 的 contentDocument 来操作但跨域 iframe 是拿不到的这时可以单独写一个脚本并让match匹配 iframe 的地址。7. 让脚本长期可用的维护心得7.1 代码组织不要全挤在一个文件里用户脚本虽然是一个单独文件但规模大了以后如果不做组织维护起来会很难受。我一般会把脚本分成几个区域配置区放所有可能变化的参数const CONFIG { keyName: #data-table, buttonClass: .approve-btn }后续调整选择器时不用在代码里到处找。工具函数区放防抖、节流、提示条、字符串处理、日期格式化等通用函数。业务逻辑区核心操作函数比如定位行、点击通过、打开详情。初始化区在合适时机调用初始化入口比如绑定事件委托、启动 MutationObserver。用这种方式组织后即使过了两个月再回来改代码也能快速找到该改的地方。用户脚本生命周期往往比预期长很多内部工具脚本我用了一年还在跑维护成本直接取决于代码清晰度。7.2 发布与版本管理如果你想把脚本分享给更多人GreasyFork 是目前最常见的用户脚本仓库。发布前你应该填写完整的name、description、namespace、version。明确匹配的网站不要用*://*/*。精简代码去掉调试日志。写清楚脚本的功能和使用方法。发布后修改脚本时一定要递增version。脚本管理器会定期检查版本变化提示用户更新。如果忘了递增版本号用户那边可能永远不会收到更新提醒。还有一点如果你的脚本依赖某个页面结构建议在 README 或描述里标明“建议在 XX 版本页面使用”方便使用者判断是否适配。因为页面结构一变脚本可能就要同步更新这是用户脚本开发不可回避的现实。7.3 一些实用技巧与真实感受最后分享几个我在长期写脚本过程中沉淀的小技巧。第一如果要模拟用户点击优先使用原生的.click()方法它比构造并分发MouseEvent更可靠因为.click()会触发元素默认行为和相关事件。但如果页面监听的是自定义事件或者用了 React 等框架合成事件原生.click()可能无效这时可以试试直接调用元素属性里存的事件监听器或者触发对应的mousedown、mouseup事件序列。第二批量操作时小步验证。不要在一次循环里执行十几个操作然后期待一次成功。我习惯先只处理一行确认无误后再扩大范围。遇到反馈回来复杂的列表就把处理逻辑封装成纯函数逐个测试。第三给脚本设置合理的日志级别。平时不打印出问题时再打开调试模式。这样既能在生产环境保持安静也不会在需要排查时抓瞎。从自身经历来看学会写用户脚本之后我处理内部系统那些重复操作的时间从一上午缩短到几分钟。它不复杂但确实是一个人应对孤岛系统和繁琐网页操作的高性价比方案。这篇文章没有覆盖所有 API但一个完整脚本从想法、编码、调试到维护的闭环已经走了一遍。剩下的就是打开编辑器找一张需要处理“脏活”的页面开始你的第一个脚本。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价