Mrli
别装作很努力,
因为结局不会陪你演戏。
Contacts:
QQ博客园

玩玩油猴脚本Tampermonkey

2020/12/01
Word count: 3,264 | Reading time: 15min

由于在研究如何优化网盘直链下载助手**baidupan,如何将直链的结果提取出来供IDM批量下载。由于baidupan**是用油猴脚本写的,因此借机学习一下。

Greasy Fork

这里是一个提供用户脚本的网站。

Tampermonkey

其为浏览器插件,目前主流浏览器皆支持,油猴叫法来源:「油猴」是从「Greasemonkey」来的。「Greasemonkey」最初是运行在Firefox浏览器中的脚本,「Tampermonkey」在Google Chrome浏览器上实现了几乎相同的功能,所以也被中文用户称之为「油猴」。

新建:

新建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ==UserScript==
// @name New Userscript
// @namespace http://tampermonkey.net/
// @version 0.1
// @description try to take over the world!
// @author You
// @match https://www.1949la.com/post/10351.html
// @grant none
// ==/UserScript==

(function() {
'use strict';

// Your code here...
})();

脚本编写方法

注释——功能注释

首先来看看脚本的内容,上面是一大排注释,这些注释可以非常有用的,它表明了脚本的各个属性。下面来简单介绍一下。

属性名 作用
name 油猴脚本的名字
namespace 命名空间,类似于Java的包名,用来区分相同名称的脚本,一般写成作者名字或者网址就可以了
version 脚本版本,油猴脚本的更新会读取这个版本号
description 描述,用来告诉用户这个脚本是干什么用的
author 作者名字
match 只有匹配的网址才会执行对应的脚本,例如*http://*http://www.baidu.com/*等,参见谷歌开发者文档
grant 指定脚本运行所需权限,如果脚本拥有相应的权限,就可以调用油猴扩展提供的API与浏览器进行交互。如果设置为none的话,则不使用沙箱环境,脚本会直接运行在网页的环境中,这时候无法使用大部分油猴扩展的API。如果不指定的话,油猴会默认添加几个最常用的API
require 如果脚本依赖其他js库的话,可以使用require指令,在运行脚本之前先加载其他库,常见用法是加载jquery
connect 当用户使用GM_xmlhttpRequest请求远程数据的时候,需要使用connect指定允许访问的域名,支持域名、子域名、IP地址以及*通配符
updateURL 脚本更新网址,当油猴扩展检查更新的时候,会尝试从这个网址下载脚本,然后比对版本号确认是否更新

grant中几个常用的权限:

1
2
3
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_setClipboard

注意, match写法由于支持通配符,可以写的通用些:

1
2
// @match        *://10.10.244.11/a70.htm*
// @match *://p.njupt.edu.cn/a70.htm*

脚本权限

下面简单介绍一下grant指令那里可以填写的一些权限,详情请查看油猴脚本文档。这里就简单介绍几个常用的,可以调用的函数全部以GM_作为开头。

权限名 功能
unsafeWindow 允许脚本可以完整访问原始页面,包括原始页面的脚本和变量。
GM_getValue(name,defaultValue) 从油猴扩展的存储中访问数据。可以设置默认值,在没成功获取到数据的时候当做初始值。如果保存的是日期等类型的话,取出来的数据会变成文本,需要自己转换一下。
GM_setValue(name,value) 将数据保存到存储中
GM_xmlhttpRequest(details) 异步访问网页数据的API,这个方法比较复杂,有大量参数和回调,详情请参考官方文档。
GM_setClipboard(data, info) 将数据复制到剪贴板中,第一个参数是要复制的数据,第二个参数是MIME类型,用于指定复制的数据类型。
GM_log(message) 将日志打印到控制台中,可以使用F12开发者工具查看。
GM_addStyle(css) 像网页中添加自己的样式表。
GM_notification(details, ondone), GM_notification(text, title, image, onclick) 设置网页通知,请参考文档获取用法。
GM_openInTab(url, loadInBackground) 在浏览器中打开网页,可以设置是否在后台打开等几个选项

还有一些API没有介绍,请大家直接查看官方文档吧。

GM_xmlhttpRequest DEMO:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
GM_xmlhttpRequest({
method: "POST",
url: url,
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8"
},
data: form_data,
onload: function(response) {
console.log("请求成功");
let success = "认证成功页";
let resp = response.responseText;
let flag = resp.indexOf(success);
if (flag === -1) {
do_login_old(username, password);
} else {
//alert("登录成功");
window.location.href = "https://cn.bing.com/"; //避免重复登录导致瞬间三个设备同时登录的状态
}
},
onerror: function(response) {
do_login_old(username, password);
}
});

MyCode

我的第一个脚本,简简单单打开自己的个人博客吧,修改如下

  • 1
    2
    3
    >   // @match        https://www.baidu.com
    > // @grant GM_openInTab
    >
  • 1
    2
    3
    >       const URL = "https://nymrli.top";
    > GM_openInTab(URL, true)
    >

访问百度的时候就会在当前session中打开我的个人博客了,(URL, true)不会切换到URL上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ==UserScript==
// @name New Userscript
// @namespace http://tampermonkey.net/
// @version 0.1
// @description try to take over the world!
// @author Mrli
// @match https://www.baidu.com
// @grant GM_openInTab
// ==/UserScript==

(function() {
'use strict';
const URL = "https://nymrli.top";
GM_openInTab(URL, true); // 打开URL后当前tab不变; false会切换当前tab为URL页面
// GM_openInTab(URL, {incognito :true }); options中貌似有有限级, 加了incognito后,insert会失效

// Your code here...
})();
Bilibili倍速
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// ==UserScript==
// @name BilibiliFast
// @namespace http://tampermonkey.net/
// @version 0.1
// @description try to take over the world!
// @author You
// @match https://www.bilibili.com/video/*
// @require https://code.jquery.com/jquery-2.1.4.min.js
// @grant none
// ==/UserScript==

(function() {
'use strict';
// Your code here...
console.log("导入成功");
$("body").append(`<div id='video_set' style="position:fixed; right:10px; top:10px; z-index:9999; background:red">
<input id="setPlay" value=1 type="number" style="padding:10px;">
</div>`
);
// $(document).append()会报错Cannot read property 'createDocumentFragment' of undefined

$(document).on("change", "#video_set #setPlay", function(){
console.log(this.value);
if(this.value <= 16){
document.querySelector('video').playbackRate=this.value;
}else{
alert("最大为16")
}
});
})();

学习baidupan源码

学到新东西:SweetAlert2 漂亮可定制的 JavaScript 弹窗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// @require           https://cdn.jsdelivr.net/npm/sweetalert2@9
// 基础语法:
Swal.fire({
title: "是否删除",
text: "是否删除?一旦提交,无法恢复!",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#DD6B55",
confirmButtonText: "确定",
cancelButtonText: "取消"
}).then((isConfirm) =>{
// 是否成功在then里面用if判断
if (isConfirm.value) {
Swal.fire("删除成功", "成功", "success");
}else{
Swal.fire("取消操作", "点击了取消", "error");
}
});

来源于checkVersion

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
function checkUpdate() {
setValue('up',0)
GM_xmlhttpRequest({
method: "GET",
url: `https://api.baiduyun.wiki/update?ver=${version}`,
responseType: 'json',
onload: function (r) {
let res = r.response
setValue('lastest_version', res.version)
userAgent = res.ua
ids = res.ids
if (res.vcode === 200 && compareVersion(res.version,version)) {
setValue('up',1)
}
if (res.scode != getValue('scode')) {
let dom = $('<div><img style="width: 250px;margin-bottom: 10px;" src="https://img.tool22.com/image/5f365d403c89f.jpg"><input class="swal2-input" id="scode" type="text" placeholder="请输入暗号,可扫描上方二维码免费获取!"></div>')
Swal.fire({
title: "初次使用请输入暗号",
html: dom[0],
allowOutsideClick: false,
confirmButtonText: '确定'
}).then((result) => {
if (res.scode == $('#scode').val()) {
setValue('scode', res.scode)
setValue('init', 1)
Toast.fire({
icon: 'success',
text: '暗号正确,正在初始化中。。。',
}).then(() => {
history.go(0)// go() 方法可加载历史列表中的某个具体的页面。(-1上一个页面,1前进一个页面, 0就是当前页面)
})
} else {
setValue('init', 0)
Swal.fire({
title: "🔺🔺🔺",
text: '暗号不正确,请通过微信扫码免费获取',
imageUrl: 'https://img.tool22.com/image/5f365d403c89f.jpg',
})
}
})
} else {
loadPanhelper()
}
}
})
}

将console.log输出分组

1
2
3
4
5
6
7
8
function clog(c1, c2, c3) {
c1 = c1 ? c1 : ''
c2 = c2 ? c2 : ''
c3 = c3 ? c3 : ''
console.group('[网盘直链下载助手]') // 分组
console.log(c1, c2, c3)
console.groupEnd() // 要想将其他内容显示在外面得取消分组
}

程序的逻辑

  • 开始:

    1
    2
    3
    4
    5
    6
    7
    $(() => {
    //阻止在其他网站运行
    if (hostname.match(/(pan|yun).baidu.com/i)) {
    let plugin = new PanPlugin()
    plugin.init()
    }
    })
  • 进行检查更新->创建菜单

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function PanPlugin() {
    clog('RPC:', ariaRPC)
    this.init = () => {
    main()
    addGMStyle()
    checkUpdate()
    if (getValue('SETTING_H')) createHelp()
    createMenu()
    }
    • 主要的应用是在checkUpdate中的loadPanhelper完成的,其会根据参数创建PanHelper(网盘页面的下载助手)或PanShareHelper(分享页面的下载助手)对象, 显然PanHelper就是我们最想分析的

PanHelper逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function PanHelper() {
let yunData, sign, timestamp, bdstoken, logid, fid_list
let fileList = [], selectFileList = [], batchLinkList = [], batchLinkListAll = [], linkList = []
let dialog, searchKey
let panAPIUrl = location.protocol + "//" + location.host + "/api/"
let restAPIUrl = location.protocol + "//pcs.baidu.com/rest/2.0/pcs/"
let clientAPIUrl = location.protocol + "//pan.baidu.com/rest/2.0/"

this.init = () => {
yunData = unsafeWindow.yunData
if (yunData === undefined) {
clog('初始化信息:', yunData)
clog('页面未正常加载,或者百度已经更新!')
return false
}
initVar()
registerEventListener()
addButton()
createIframe()
dialog = new Dialog({addCopy: true})
clog('下载助手加载成功!当前版本:', version)
}

最核心的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//我的网盘 - 获取PCS下载地址
function getPCSBatchLink(callback) {
let fsids = []
$.each(selectFileList, (index, element) => {
if (element.isdir == 1)
return
fsids.push(element.fs_id)
})
fsids = encodeURIComponent(JSON.stringify(fsids))
let link = clientAPIUrl + `xpan/multimedia?method=filemetas&access_token=undefined&fsids=${fsids}&dlink=1`
GM_xmlhttpRequest({
method: "GET",
responseType: 'json',
headers: {"User-Agent": userAgent},
url: link,
onload: (res) => {
let response = res.response
if (response.errno === 0) {
callback(response.list)
}
}
})
}

附录

官方文档

Tampermonkey油猴用户脚本API文档-教程

脚本debug建议

jquery使用

踩了几天坑,最后总结一下编写油猴脚本的一点步骤。首先要思考脚本的实现方式,需要用到什么API和权限,然后填写好脚本的注释信息。

然后将功能封装成函数的形式,最后在脚本末尾调用实现的函数。写的差不多的时候复制到浏览器中尝试运行。

遇到困难的时候,可能需要直接在F12开发者工具里进行调试。有些网页不用jQuery,为了方便,我们需要自己将jQuery导入到页面中,可以将下面的代码复制到浏览器控制台中。

1
2
3
var jq = document.createElement('script');
jq.src = "https://cdn.staticfile.org/jquery/3.4.1/jquery.min.js";
document.getElementsByTagName('head')[0].appendChild(jq);

debug方法:

第一种方法就是最原始的打印日志,可以利用console.logGM_log来将关键信息打印出来,上面的脚本就是我靠打印日志一点点发现各种参数错误的。说实话这种办法有点笨。

第二种就是利用浏览器的调试功能(推荐),在脚本需要调试的地方插入debugger;语句,然后在打开F12开发者工具的情况下刷新页面,就会发现网页已经暂停在相应位置上。这样就可以利用F12开发者工具进行单步调试、监视变量等操作了。

将文章同步复制到Csdn和思否编辑器的脚本demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
// ==UserScript==
// @name copy_jianshu_to_csdn_and_segmentfault
// @namespace https://github.com/techstay/myscripts
// @version 0.1
// @description 将简书文章复制到csdn和思否编辑器中
// @author techstay
// @match https://editor.csdn.net/md/
// @match https://segmentfault.com/write
// @match https://www.jianshu.com/writer*
// @require https://cdn.staticfile.org/jquery/3.4.1/jquery.min.js
// @require https://cdn.bootcss.com/jqueryui/1.12.1/jquery-ui.min.js
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant unsafeWindow
// @grant GM_setClipboard
// @grant window.close
// @grant window.focus
// @grant GM_openInTab
// ==/UserScript==
(function () {
'use strict';

const SF_URL = 'https://segmentfault.com/write'
const CSDN_URL = 'https://editor.csdn.net/md/'

const SF_TITLE = 'sf_title'
const SF_CONTENT = 'sf_content'
const CSDN_TITLE = 'csdn_title'
const CSDN_CONTENT = 'csdn_content'

function saveArticle() {
GM_setValue(CSDN_TITLE, $('._24i7u').val())
GM_setValue(CSDN_CONTENT, $('#arthur-editor').val())
GM_setValue(SF_TITLE, $('._24i7u').val())
GM_setValue(SF_CONTENT, $('#arthur-editor').val())
}

function copyToCsdn() {
var title = GM_getValue(CSDN_TITLE, '')
var content = GM_getValue(CSDN_CONTENT, '')
if (title != '' && content != '') {
$('.article-bar__title').delay(2000).queue(function () {
$('.article-bar__title').val(title)
$('.editor__inner').text(content)
GM_deleteValue(CSDN_TITLE)
GM_deleteValue(CSDN_CONTENT)
$(this).dequeue()
})
}
}

function copyToSegmentFault() {
$(document).ready(function () {
var title = GM_getValue(SF_TITLE, '')
var content = GM_getValue(SF_CONTENT, '')
if (title != '' && content != '') {
$('#title').delay(2000).queue(function () {
$('#title').val(title)
GM_setClipboard(content, 'text')
GM_deleteValue(SF_TITLE)
GM_deleteValue(SF_CONTENT)
$(this).dequeue()
})

}
})

}

function addCopyButton() {
$('body').append('<div id="copyToCS">双击复制到CSDN和思否</div>')
$('#copyToCS').css('width', '200px')
$('#copyToCS').css('position', 'absolute')
$('#copyToCS').css('top', '70px')
$('#copyToCS').css('left', '350px')
$('#copyToCS').css('background-color', '#28a745')
$('#copyToCS').css('color', 'white')
$('#copyToCS').css('font-size', 'large')
$('#copyToCS').css('z-index', 100)
$('#copyToCS').css('border-radius', '25px')
$('#copyToCS').css('text-align', 'center')
$('#copyToCS').dblclick(function () {
saveArticle()
GM_openInTab(SF_URL, true)
GM_openInTab(CSDN_URL, true)
// GM_openInTab(url, options)在新标签页打开URL。options可选的值:
// active :新标签页获得焦点; insert:新标签页在当前页面之后添加; setParent:当新标签页关闭后,焦点给回当前页面 ; incognito: 新标签页在隐身模式或私有模式窗口打开.
// options可以写成{ active: true, insert: true, setParent :true }
// GM_openInTab(url, loadInBackground):loadInBackground 可以是 Boolean 类型,如果是 true,则当前 tab 不变,如果是 false,则当前 tab 变为新打开的 tab. 当前tab就是当前标签页(显示的页面)
})
$('#copyToCS').draggable()
}

$(document).ready(function () {
if (window.location.href.startsWith('https://www.jianshu.com')) {
addCopyButton()
} else if (window.location.href.startsWith(SF_URL)) {
copyToSegmentFault()
} else if (window.location.href.startsWith(CSDN_URL)) {
copyToCsdn()
}
})
})()

▲推荐:油猴脚本编写教程

Author: Mrli

Link: https://nymrli.top/2020/12/01/玩玩油猴脚本/

Copyright: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.

< PreviousPost
Lets learn 设计模式
NextPost >
浙大2020春夏-人工智能习题3——AIforOthello
CATALOG
  1. 1. Greasy Fork
  2. 2. Tampermonkey
    1. 2.1. 新建:
  3. 3.
    1. 3.1. 脚本编写方法
      1. 3.1.1. 注释——功能注释
      2. 3.1.2. 脚本权限
        1. 3.1.2.1. GM_xmlhttpRequest DEMO:
      3. 3.1.3. MyCode
        1. 3.1.3.1. Bilibili倍速
    2. 3.2. 学习baidupan源码
      1. 3.2.1. 程序的逻辑
      2. 3.2.2. PanHelper逻辑
  4. 4. 附录
    1. 4.1. 官方文档
    2. 4.2. Tampermonkey油猴用户脚本API文档-教程
    3. 4.3. 脚本debug建议
      1. 4.3.1. jquery使用
      2. 4.3.2. debug方法:
    4. 4.4. 将文章同步复制到Csdn和思否编辑器的脚本demo: