Tampermonkey(油猴)是浏览器上最常用的用户脚本管理器:装完之后,随便打开一个网站,它都能往页面里注入一段自己写的 JavaScript。新建脚本时它给的默认模板很短,但那几行 // ==UserScript== 注释才是整个脚本的配置区——决定脚本在哪个网站跑、什么时候跑、能用哪些特权 API。本文先把模板和头注释说清楚,再把 GM_* API 按用途分组整理一遍,最后用一个把 Cookie「钩」出来的实战脚本串起来。
一、默认模板长什么样
// ==UserScript==
// @name New Userscript
// @namespace http://tampermonkey.net/
// @version 0.1
// @description try to take over the world!
// @author You
// @match https://example.com/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
// Your code here...
})();两点要注意:
- 头注释不能删。Tampermonkey 靠解析这几行固定格式的注释来决定脚本的元信息和权限,删掉或写错格式,脚本就匹配不上页面、也拿不到
GM_*函数。 - 注释下面是一个自执行匿名函数(IIFE)。
'use strict'开严格模式,同时把变量关在函数作用域里,避免污染页面的全局变量。
二、头注释(Userscript Header)逐项说明
| 标签 | 作用 | 备注 |
|---|---|---|
@name | 脚本名称 | 显示在 Tampermonkey 列表里 |
@namespace | 脚本命名空间 | 用于区分同名脚本;以 http:// 开头时会兼作主页链接 |
@version | 脚本版本 | 更新检查的依据,只有它变了才会拉新版本 |
@author | 作者 | — |
@description | 简短描述 | 脚本列表和安装页面都会显示 |
@homepage @homepageURL @website @source | 作者主页 | 在选项页里把脚本名链过去 |
@icon @iconURL @defaulticon | 低分辨率图标 | 脚本列表中显示 |
@icon64 @icon64URL | 64×64 图标 | 同时配了 @icon 时,@icon 在选项页会被缩放 |
@updateURL | 更新检查地址 | 必须配合 @version 才生效 |
@downloadURL | 更新下载地址 | 写成 none 表示不做更新检查 |
@supportURL | 反馈问题地址 | — |
@include / @match | 脚本运行范围 | 可写多个;语义相近,@include 支持正则,但不支持 URL 的 hash 部分 |
@exclude | 排除范围 | 优先级高于 include / match,同样可写多个 |
@require | 运行前加载并执行的外部脚本 | 常用来引入 jQuery;支持 #sha256= 校验 |
@resource | 预加载资源 | 配合 GM_getResourceText / GM_getResourceURL 使用 |
@connect | 允许 GM_xmlhttpRequest 访问的域 | 不写就会在运行时弹窗询问 |
@run-at | 注入时机 | 默认 document-end |
@grant | GM_* 函数白名单 | 没写就调不到任何 GM_* |
@noframes | 只在主框架运行 | 避免在 iframe 里重复执行 |
1. @match 与 @include
// @include http://www.tampermonkey.net/*
// @include http://*
// @include https://*
// @include /^https://www\.tampermonkey\.net/.*$/@match 用的是 match pattern(协议://主机/路径,可以用 * 通配),比正则更直观、也更安全;@include 则允许直接写正则。@exclude 用来在已命中的范围内再挖掉一块,比如排除后台页面。
2. @require
// @require https://code.jquery.com/jquery-3.7.1.min.js
// @require https://code.jquery.com/jquery-2.1.3.min.js#sha256=23456...
// @require https://code.jquery.com/jquery-2.1.2.min.js#md5=34567...,sha256=6789...
// @require tampermonkey://vendor/jquery.js
// @require tampermonkey://vendor/jszip/jszip.js被 @require 的脚本会在你的脚本之前加载并执行。加 #sha256= 可以固定文件完整性,防止 CDN 上的内容被换掉;tampermonkey://vendor/ 用的是 Tampermonkey 内置副本,离线也能用。
一个容易忽略的副作用:被加载脚本里的 'use strict' 会影响你的脚本。有些老库不是严格模式写的,引进来后你的脚本可能直接报错,遇到莫名其妙的语法错误先怀疑这里。
3. @resource
// @resource icon1 http://www.tampermonkey.net/favicon.ico
// @resource icon2 /images/icon.png
// @resource html http://www.tampermonkey.net/index.html
// @resource xml http://www.tampermonkey.net/crx/tampermonkey.xml
// @resource SRIsecured1 http://www.tampermonkey.net/favicon.ico#md5=123434...
// @resource SRIsecured2 http://www.tampermonkey.net/favicon.ico#md5=123434...;sha256=234234...适合把图标、HTML 模板、JSON 字典这类静态资源跟着脚本一起预加载,脚本里用 GM_getResourceURL('icon1') 拿 base64 URI、用 GM_getResourceText('html') 拿文本内容。
4. @connect
GM_xmlhttpRequest 虽然是脚本发起的请求,但目标域仍需在 @connect 里声明。取值可以是:
- 域名,如
tampermonkey.net(同时允许其子域) - 具体子域,如
safari.tampermonkey.net self:把脚本当前运行的域加入白名单localhost:访问本机服务- IP 地址,如
1.2.3.4
5. @run-at
| 取值 | 注入时机 |
|---|---|
document-start | 尽可能早地注入,此时 DOM 还没建好 |
document-body | body 元素存在时注入 |
document-end | DOMContentLoaded 之后注入,不写 @run-at 就是这种 |
context-menu | 在浏览器右键菜单里点击该脚本时才注入(仅桌面版 Chromium 内核浏览器) |
官方文档的说法值得抄一遍:@run-at 定义的是脚本想要运行的最早时刻。如果用 @require 拉外部脚本,下载耗时长的话,实际执行可能已经晚于这个时刻;而在注入时刻之后发生的 DOMNodeInserted、DOMContentLoaded 事件会被缓存起来,等脚本注入后再投递给它。
6. @grant
@grant 是 GM_* 函数的白名单——不写、或者写 none,Tampermonkey 就不会把这些函数注入进来,代码里调用会直接报未定义。常用的几个:
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_setClipboard
// @grant unsafeWindow
// @grant window.close
// @grant window.focus
// @grant window.onurlchange还有个隐藏差别:@grant none 时脚本跑在页面上下文里,window 就是页面的 window,但没有任何 GM_*;只要写了任意一个 @grant,脚本就进入沙箱上下文,想读写页面自己的全局变量就得通过 unsafeWindow。
三、GM_* API 按用途分组
1. 访问页面与加样式
// unsafeWindow 不需要 @grant
unsafeWindow.somePageFunction();
// @grant GM_addStyle
GM_addStyle('html body { background-image: url(https://example.com/bg.png) }');2. 数据存储(脚本私有)
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_listValues
GM_setValue('time1', '2026-10-01 20:10:00'); // 存键值对
GM_setValue('time2', '2026-10-01 20:11:00');
console.log('获取 time1', GM_getValue('time1'));
console.log('带默认值的读法', GM_getValue('notExist', '默认值'));
console.log('storage 中所有的 key', GM_listValues());
GM_deleteValue('time1');
console.log('删除后再读', GM_getValue('time1'));这套 API 是同步的,存取在脚本私有 storage 里,和其他脚本、页面自己的 localStorage 互相隔离。Tampermonkey 4.14 起另外提供了 Promise 版的新 API(GM.setValue、GM.getValue、GM.xmlHttpRequest),@grant 里写 GM.setValue 即可,新脚本可以优先用它。
3. 跨标签页通信
// @grant GM_addValueChangeListener
// @grant GM_removeValueChangeListener
const listenerId = GM_addValueChangeListener('token', function (name, oldValue, newValue, remote) {
// remote 为 true:值是被另一个标签页里的脚本改的
console.log(name, oldValue, newValue, remote);
});
GM_removeValueChangeListener(listenerId);回调里的 remote 是关键:true 表示这次改动来自另一个标签页实例,false 表示是本脚本自己改的。多个标签页之间要同步状态,用这个就够了,不必自己搭轮询。
4. 打开新标签页
// @grant GM_openInTab
GM_openInTab('https://www.baidu.com/', { active: true, insert: true, setParent: true });| 选项 | 含义 |
|---|---|
active | 是否聚焦到新标签页 |
insert | 在当前标签后面插入新标签 |
setParent | 新标签关闭后重新聚焦当前标签 |
incognito | 在隐私窗口打开 |
loadInBackground | 第二个参数的简写形式,含义与 active 相反 |
5. 发起网络请求
GM_xmlhttpRequest(details) 是脚本里最常用的一个:它不受页面同源策略限制,还能带自定义头。details 的字段:
| 字段 | 说明 |
|---|---|
method | 请求方法:GET、HEAD、POST |
url | 请求地址 |
headers | 请求头 |
data | POST 发送的内容 |
cookie | 请求所需的 Cookie |
binary | 发送二进制内容 |
nocache / revalidate | 不缓存 / 重新验证可能缓存的内容 |
timeout | 超时时间(毫秒) |
context | 附加到响应对象上的自定义属性 |
responseType | 响应类型:arraybuffer、blob、json |
overrideMimeType | 覆盖响应的 MIME 类型 |
anonymous | 匿名发送,不带 Cookie |
fetch(beta) | 底层改用 fetch 而不是 XHR |
username / password | HTTP 认证凭据 |
onabort onerror onloadstart onprogress onreadystatechange ontimeout onload | 各阶段回调 |
onload 回调收到的响应对象包含:finalUrl(重定向后的最终地址)、readyState、status、statusText、responseHeaders、response、responseXML、responseText。GM_xmlhttpRequest 的返回值是一个带 abort() 方法的对象,可以中途取消请求。
注意:details 参数不支持同步请求,也没有同步写法可用。
6. 下载文件
GM_download(details) 或简写形式 GM_download(url, name)。details 字段:url、name(文件名,出于安全考虑扩展名必须在 Tampermonkey 参数页的白名单里)、headers、saveAs(是否弹出保存对话框)、onerror、onload、onprogress、ontimeout。
7. 桌面通知
GM_notification(details, ondone) 或 GM_notification(text, title, image, onclick)。details 字段:text(内容,只做高亮时可不填)、title、image、highlight(是否高亮当前标签页)、silent(是否静音)、timeout(显示时长,0 表示一直显示)、ondone(被点击或超时关闭时执行)、onclick。
8. 剪贴板
// @grant GM_setClipboard
GM_setClipboard('要复制的内容', { type: 'text', mimetype: 'text/plain' });第二个参数也可以直接给字符串 'text' 或 'html'。
9. 菜单命令
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
const menuId = GM_registerMenuCommand('清空缓存', function () {
console.log('菜单被点击');
}, 'c');
GM_unregisterMenuCommand(menuId);注册的菜单会出现在 Tampermonkey 的扩展菜单里,适合放开关、重置、清缓存这类零散操作,省得在页面上做 UI。
10. 日志与资源
GM_log(message) 向控制台打日志;GM_getResourceText(name) 取 @resource 的文本内容;GM_getResourceURL(name) 取 @resource 预加载资源的 base64 编码 URI。
四、实战:把加密 Cookie 钩出来
做数据采集时经常碰到 Cookie 加密:以 BOSS 直聘为例,Cookie 里的 zp_stoken 是加密生成的,用 Requests 不带它就得不到正常响应。硬扣这个参数的算法很麻烦——要断点调试、把 JS 抠出来用 PyExecJS 之类的 Node 环境执行,而它的生成过程还牵扯分辨率、内核、webdriver 等浏览器环境检测。
换一条路:让油猴在真实浏览器里把 document.cookie 取出来,POST 回本地服务,爬虫再从服务端拿。下面是演示层面的脚本。
// ==UserScript==
// @name BoSSCookie
// @namespace http://tampermonkey.net/
// @version 0.1
// @description 把当前页面的 Cookie 回传到本地服务
// @author You
// @match https://www.zhipin.com/*
// @grant GM_xmlhttpRequest
// @connect 127.0.0.1
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
console.log(document.cookie);
GM_xmlhttpRequest({
method: 'POST',
url: 'http://127.0.0.1:7890/cookie',
data: JSON.stringify(document.cookie),
headers: { 'Content-Type': 'application/json' },
onload: function (res) {
if (res.status === 200) {
console.log('Cookie 发送成功');
} else {
console.log('Cookie 发送失败');
console.log(res);
}
},
onerror: function (err) {
console.log('发生 error');
console.log(err);
}
});
})();本地用 Flask 起一个微型接收端:
from flask import Flask, request
app = Flask(__name__)
@app.route('/cookie', methods=['POST'])
def server_cookie():
raw = request.get_data(as_text=True) # 与 Content-Type: application/json 对应
print(raw)
# 也可以直接写进 Redis,让爬虫从队列里取
return 'ok'
if __name__ == '__main__':
app.run(host='127.0.0.1', port=7890, debug=True)@match https://www.zhipin.com/* 命中该站任意页面,刷新页面脚本就会执行,把 Cookie 打到本地服务的控制台。三个地方最容易踩:
- Content-Type 和数据的对应关系:如果请求头写
application/x-www-form-urlencoded,而data是JSON.stringify()出来的 JSON 字符串,服务端用request.form是解析不出东西的。要么像上面这样头写application/json、服务端用request.get_data()读原文,要么就老老实实按表单编码拼数据。 @connect一定要写:GM_xmlhttpRequest的目标域不在白名单里时,Tampermonkey 会弹窗问「是否允许跨域请求」,点允许才能发出去。演示可以临时点,正式写法直接补上// @connect 127.0.0.1。document-start的时机问题:这个时刻页面还没开始跑,只能拿到服务端下发的 Cookie,后续由页面 JS 写入的部分拿不到。目标 Cookie 是后写的,就把@run-at改成document-end,或者定时轮询document.cookie。
安全提醒:Cookie 基本等同于登录态,把它发到一个本地服务,意味着谁能访问这个服务谁就能接管账号。做这类验证时端口只绑 127.0.0.1、加上校验参数,别暴露到公网;也只在自己的账号上做学习性验证,站点服务条款和 robots 明确禁止的批量采集不要做。
五、常见问题排查
- 脚本完全没反应:先看
@match/@include有没有命中——URL 后面带参数、hash 都会影响匹配(@include不支持 hash)。单页应用内部跳路由不会重新注入脚本,需要自己监听路由变化。 GM_xxx is not defined:@grant里没声明这个 API。用到哪个就补哪个,注意名字要写全(如GM_setClipboard,不是GM_setclipboard)。document-start时取元素是 null:DOM 还没建好。改成document-end,或用DOMContentLoaded、MutationObserver等元素出现再操作。- 引入 jQuery 后报奇怪错误:
@require进来的脚本自带'use strict',会被带进你的脚本;另外 jQuery 的$也可能和页面已有的库冲突,必要时用jQuery.noConflict()。 - 脚本里请求发不出去:
@connect没写,或者用的是页面上下文的fetch/XMLHttpRequest被同源策略拦了——换成GM_xmlhttpRequest。 - 版本不更新:
@version没升;或者没配@updateURL/@downloadURL(@downloadURL none会关闭更新检查);Tampermonkey 自己的更新检查频率设置也会影响。 - 数据存在哪、脚本之间会不会串:
GM_setValue是按脚本隔离的私有存储,和页面localStorage、其他脚本都不共享;在 Tampermonkey 里清理该脚本的数据会一起清掉。 - 调试:在 Tampermonkey 仪表盘里点开脚本,编辑器里改完
Ctrl+S保存即生效,刷新页面就是新代码;@grant none的脚本跑在页面上下文,控制台里能直接看到自己的变量。
六、模板速查卡
// ==UserScript==
// @name 示例脚本
// @namespace https://example.com/
// @version 1.0.0
// @description 一句话说明这个脚本干什么
// @author You
// @match https://example.com/*
// @exclude https://example.com/admin/*
// @icon https://example.com/favicon.ico
// @require https://code.jquery.com/jquery-3.7.1.min.js#sha256=...
// @resource logo https://example.com/logo.png
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_registerMenuCommand
// @grant GM_xmlhttpRequest
// @connect example.com
// @run-at document-end
// @noframes
// ==/UserScript==
(function () {
'use strict';
GM_addStyle('.my-tip { color: #e33 }');
GM_registerMenuCommand('清空本脚本缓存', function () {
GM_deleteValue('cache');
alert('已清空');
});
const tip = document.createElement('div');
tip.className = 'my-tip';
tip.textContent = '脚本已注入 ' + new Date().toLocaleString();
document.body.appendChild(tip);
})();一句话总结:头注释决定脚本「在哪跑、什么时候跑、能用哪些 API」,GM_* 函数必须先 @grant 再调用,跨域请求记得声明 @connect;写站点的自动化脚本时,把 Cookie 这类凭证当作密码来对待。
评论区