侧边栏壁纸
博主头像
Setyg的博客

当秋风染红落叶的时候,就是新生~

  • 累计撰写 86 篇文章
  • 累计创建 66 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

Tampermonkey 油猴脚本实践:头注释、GM_* API 与加密 Cookie 钩子

Setyg
2026-10-01 / 0 评论 / 0 点赞 / 1 阅读 / 0 字

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 @icon64URL64×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
@grantGM_* 函数白名单没写就调不到任何 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-bodybody 元素存在时注入
document-endDOMContentLoaded 之后注入,不写 @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请求头
dataPOST 发送的内容
cookie请求所需的 Cookie
binary发送二进制内容
nocache / revalidate不缓存 / 重新验证可能缓存的内容
timeout超时时间(毫秒)
context附加到响应对象上的自定义属性
responseType响应类型:arraybuffer、blob、json
overrideMimeType覆盖响应的 MIME 类型
anonymous匿名发送,不带 Cookie
fetch(beta)底层改用 fetch 而不是 XHR
username / passwordHTTP 认证凭据
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 这类凭证当作密码来对待。

0
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区