油猴子从入门到喵喵喵喵(实例:9/9 完结)

原来喵喵喵喵是这个意思啊.

其实比较希望能看到一个完整的可以使用的脚本的编写流程,函数、语句、关键字很容易就能找到相关的介绍,迷糊的是到底该怎么开始.

原本打算先做完文字校对再放示例的。

但是几万字嗷,自己懒得逐字检查,却发现中文的拼写检查工具没几个好用的,正在乱翻……

你等一下,我这就先上一个例子。

期待中

示例一:获取 B 站视频封面

需求分析:首先在视频列表页复制封面的图片地址,然后进入这个视频的页面,在源码中去搜索这个地址的关键词(就是文件名)。这个操作本身是碰运气,然后发现他把封面图片的地址清晰的写在了网页的头信息中。

<meta data-vue-meta="true" itemprop="image" content="http://i1.hdslb.com/bfs/archive/8ec1b95254e0e67cacad1191e918e66e06fabcbf.jpg">

这个问题就变得非常简单了,只需要获取网页中特定元素的特定属性即可。

那么下一个问题就是获取到的这个地址,如何输出出来。我选择的方法是直接将当前页面的网址修改为这个图片地址,因为这样最简单,而且图片直接显示出来了,如果是在手机上长按图片就可以进行分享。

接下来的问题是如何触发。如果这个功能在网页打开之后就立刻执行,得到的结果是打开一个视频页面,结果显示出来的是它的封面图。(那我以后还怎么摸鱼呀!!!

所以我选择给这个脚本添加了一个菜单,当点击这个菜单的时候才执行这个动作。


好了,问题分析完了,下面来看代码比解释少系列:

// ==UserScript==
// @name        获取 B 站视频封面
// @namespace   Get bilibili video cover
// @match       *://www.bilibili.com/video/*
// @match       *://m.bilibili.com/video/*
// @grant       GM_registerMenuCommand
// @version     1.0
// @author      -
// @description 2020/7/15 上午7:27:34 
// ==/UserScript==

GM_registerMenuCommand('获取此视频封面', ()=>{
  window.location.href = document.querySelector('meta[itemprop="image"]').getAttribute('content')
})

代码中相比我讲到的知识有两处超纲:

  • 这个选择器是稍微复杂一点的属性选择器
  • 这个元素的 content 属性似乎无法直接获得,于是用了 getAttribute 方法

但在操作思路上没有超纲的地方

示例二:论坛高亮楼主头像

有时候楼层比较高,看着看着就忘记了谁是楼主。本论坛看过的帖子再点进去或直接从你上次阅读的地方开始,就更容易遇到,看回复时不知道楼主是谁的尴尬。这会很影响我对回复内容的理解。所以就做一下高亮,让我能够识别出来。

本来也想像 V2EX 家插件那样直接对楼层进行高亮,但试了试,在本论坛的显示效果不好。那就高亮头像吧。然后我也记不清是从哪里复制了一下样式,反正挺简单的两行 CSS。

那怎么确定谁是楼主呢?这个问题我也挺犯愁的,因为如果直接打开是最先回复的位置,那么有可能顶楼并没有被加载出来,这就不太容易判断。但我觉得程序不会把这么重要的信息直接忽略掉,于是在代码中观察了一下,发现凡是楼主所在的楼层都会有这样的类 topic-owner。那问题就很简单了,就是给对应的元素添加上样式就好。

看下面代码,和我们讲添加样式的接口时所举的例子已经十分一致了。

// ==UserScript==
// @name        论坛细节优化
// @namespace   Appinn Forum Details Optimization
// @match       *://meta.appinn.net/*
// @grant       GM_addStyle
// @version     1.0
// @author      -
// @description 2020/7/13 下午2:05:15
// ==/UserScript==

GM_addStyle(`
  .topic-owner .topic-avatar img {
    box-shadow: 0 0 3px 1px #81c3e4;
    border: 2px solid #85c2e0;
  }
`)

示例三:链接地址洗白白(简化)

地址简化,主要就是这样几种情况:

  • 地址中的 get 参数可以清理,比如淘宝的搜索结果,只保留查询的关键词即可
  • 地址中的路径可以被出新组合,比如本论坛的帖子地址

对于第 2 种,可以利用正则替换来修改网址,简单理解就是查找替换。对于第 1 种,因为查询的参数前后顺序是不固定的,用正则难以处理这种顺序不固定的情况,所以先提取出查询参数,然后重组成自己需要的格式。

针对已知的网站设定规则,然后根据当前网站使用对应的规则进行处理。为了适配更多的网站,所以设置了一些常用的查询参数,对于为准确是配的网站,尝试只保留这些查询参数。

// ==UserScript==
// @name 链接地址洗白白(精简示例版)
// @namespace Daomouse Link Cleaner
// @version 0.0.1
// @author 稻米鼠
// @description 把链接地址缩减至最短可用状态
// @match *://*/*
// @grant GM_setClipboard
// @grant GM_registerMenuCommand
// @noframes
// ==/UserScript==
const cleanIt = ()=>{
  const url=window.location.href
  const hash = url.replace(/^[^#]*(#.*)?$/, '$1')
  const base = url.replace(/(\?|#).*$/, '')
  let pureUrl = url
  const getQueryString = function(key) {
    let ret = url.match(new RegExp('(?:\\?|&)(' + key + '=[^?#&]*)', 'i'))
    return ret === null ? '' : ret[1]
  }
  const pureUrl = base + '?q=' + getQueryString('q')
  GM_setClipboard(pureUrl)
}
GM_registerMenuCommand('链接净化', cleanIt)

大概就是这个样子,上面代码实现了一下对淘宝搜索结果链接的净化。其中正则表达式应用的很多,但是前面我们没有讲。

总之就是注册一个菜单,当我们点击这个菜单的时候,运行上面已经写好的函数。函数里面对当前网址一通操作,然后把修改完的地址放入剪切板。

只要你会使用正则,并且对网址的结构有基础的了解,做这样一个脚本并不困难。真正麻烦的是对规则的收集和维护。

不过这种方法并不能够解决所有网址净化的问题,不过那不在我们这次的讨论范围之内了。

示例四:自动展开全文(简化)

这个更简单,就做两件事情,让那个按钮不显示,然后去掉对内容部分的限制。

这两个问题都可以通过 CSS 解决,所以事实上就是对页面添加几句 CSS。麻烦的就是对每一个网站的适配确定这个网站中哪个元素是按钮,哪个元素是文章内容。

这个我就不放代码了,因为精简到只针对一个网站的话,和前面我们写的为页面加入 CSS 是一样的。

针对多个网站,就是遍历已有的规则,验证网址是否符合,如果符合就按照这个规则中记录的元素去设定相应的 CSS。

(代码不复杂,维护规则就……

示例五:网易云音乐歌词放大

没啥解释的,就是把当前(那一句)歌词的字号设置大一点,方便看歌词。

// ==UserScript==
// @name        网易云音乐歌词放大
// @namespace   Violentmonkey Scripts
// @match       https://music.163.com/*
// @grant       GM_addStyle
// @version     1.0
// @author      -
// @description 2020/5/24 下午6:18:58
// ==/UserScript==

GM_addStyle(`
  .m-playbar .listlyric p.z-sel {
    font-size: 22px;
  }
`)

这个例子就是告诉大家用已有的知识去解决小问题是可行的。我也确实在用这个脚本。

如果愿意折腾,把歌词弄到网页标题里也可以,这样切到别的页面也能看歌词。不知道如果动用浏览器的 Picture in picture 功能的话,能否实现桌面浮动歌词。我自己需求不大,就懒得折腾。

示例六:假装水墨屏

代码看这里吧: https://greasyfork.org/zh-CN/scripts/407641/code

原理很简单,为页面插入一段 CSS,其中有四段内容:

  • 页面滤镜,让网页变成黑白色调,在各种纪念日,网站变黑白用的就是这个方法。我这里增加了一层亮度调节,这样可以降低深色元素的浓度,毕竟水墨屏的纯黑也不是特别黑,而手机屏现在可以显示纯黑的,这差别很大。
  • 定义一个类,具有浅灰色背景,来模拟水墨屏本身的灰色(随着发展,现在的水墨屏越来越白了,但依旧不算纯白,加上手机电脑的屏幕本身比较亮,要灰一点才像
  • 定义一个类,具有深灰色文字,页面中深灰色的文字,全都换成这个值,让显示效果更好控制。
  • 让所有元素的文字具有阴影,借此模拟水墨屏文字发虚(现在高 PPI 设备基本不虚了),和屏幕残影造成的效果。

然后做了两个判断:

  • 每一个背景色为浅色的元素,设置为上面的浅灰色背景
  • 每一个文字颜色为深色的元素,调整我设定的那个颜色

其实直接把前面样式全局应用就可以兼容大多数小说站了,加入判断是为了更加严谨。


高级技法:

瀑布流和延迟加载的页面效果会很差。因为我们上面讲的这些,只是在页面载入后执行一次,后面新增加的元素并不会受到影响,背景和文字的颜色就不会被优化到。

所以加入和页面变化监控,当有元素变化时,对元素进行上述两个判断,按需要修改。这就解决问题了。

但是下一个问题又来了:当新元素需要修改时,我们修改了。这个元素不也发生变化了么,那监控元素变化的部分就会再通知我们这个元素变化了……然后陷入死循环(可能)。所以,当我们对元素进行修改时,要先停止对元素变化的监控,修改后再重新开启监控。

这部分比较比较复杂,不推荐新手现在啃。就是先了解些思路。这里算是展示一个看起来很简单,但其实挺复杂的脚本。

为了尽可能提高自己脚本的兼容性,所以十分诚心诚意的去尝试手机上那些号称支持脚本的浏览器。

以前我对他们的认知只限于他们介绍中的说明,并未实际使用过。现在体验下来大跌眼镜,我是真的用来。

如果支持从市场中直接安装脚本的体验,还算好一丢丢,但是可能浏览器自建的市场中收入的脚本非常少,并且收入之后不再及时更新。如果自己新建复制粘贴进去,因为好多脚本的代码比较长,在手机上的操作体验是非常差的,甚至多次操作才能成功。

然后自己新建的话一般匹配网址部分需要自己单独额外输入,如果匹配多个网址,在手机上的输入体验,依然让人抓狂。

既然在输入格式上都不能脚好的去适配油猴规范,就很难指望他们能够提供对应的接口。前面我们说过这些接口是给开发者带来便利的,所以在有需要的情况下,开发者肯定会优先选择使用这些接口。这就导致这些浏览器对脚本的兼容性必然很差。

感觉他们对脚本的支持,都处于小书签的水平。都怀疑是用 window.location.href = 脚本代码 来实现的。这种情况下,想对它们进行兼容十分困难。

可以的,谢谢

原来可以用 window.location.href = 脚本代码 这种方式自动调用小书签啊

学到了

好多知识冷的叫人发抖。真的可能掌握以后直到这技术淘汰都用不上。

但是玩油猴子这种,就免不了时常需要一些冷知识,也挺有趣的。

示例七:加入统计代码

建议先阅读:【老鼠讲故事系列 004】油猴子加入统计代码

第一步:判断用户设置

如果用户拒绝追踪,那就不要插入统计代码。

if (!navigator.doNotTrack) {
    // 这里是后续的代码
}

如果 navigator.doNotTrack 结果为 1,则禁止跟踪。0 为允许,未设置为 null

第二步:插入一个框架

这是因为我们可以对框架进行更好的操作,比如修改网址和标题,而不会影响到用户对页面浏览的体验。而这些修改可以让统计携带回更多有效的数据。

这并不是打算做什么坏事,是因为我们要跟踪的是脚本的使用情况,而脚本相关的一些数据并不能够通过普通的统计来获得。

框架中的页面最好是一个十分分简单,并且访问速度非常快的页面,比较好的选择就是一些网站的 404 页面,我使用了,提供统计服务的网站的某个页面。这样如果这个页面都无法访问,那么统计代码大概率也无法生效。

const href = 'https://c.statcounter.com/' + window.location.hostname + '/';
const iframe = document.createElement('iframe');
iframe.src = href;
iframe.style = 'display: none !important; width: 0; height: 0;';
document.body.appendChild(iframe);

就是很简单的在页面中追加一个元素,在加入之前对元素的一些属性进行必要的设置。这里我将当前页面的域名部分附加在了这个页面网址之后,我们可以以类似的方式附加一些有效信息。当然要确保最后这个网址所获得的页面符合我们的预期。

记得设置这个框架隐藏起来,不要显示。

第三步:对这个页面进行修改

首先我们要把上一个页面放入脚本的匹配( @match )之中。然后我们的脚本要可以在框架下运行。

如果发现当前页面的网址和我们预设的这个页面相匹配。就去修改这个页面的标题,让它携带更多信息,比如我让他携带了脚本管理器的名称、版本、当前脚本的版本号等。

if (/^https:\/\/c\.statcounter\.com\//i.test(window.location.href)) {
        // 在标题中放入脚本相关信息
        document.title = GM_info.scriptHandler+
            '-' + GM_info.version +
            '-' + GM_info.script.version
}

第四步:加入统计代码

这里要注意,不能直接通过修改 html 代码来插入,因为可能导致脚本没有正确的运行,所以还是要像上面那样,通过追加元素的方式进行插入,但在这之前可以先简单清空一下 body 元素中的内容。下面代码中项目编号和加密的识别码,我都用变量替代了。

document.body.innerHTML = '';
const scriptA = document.createElement('script');
scriptA.type = 'text/javascript';
scriptA.innerHTML =
  `var sc_project=` + sc_project + `;
  var sc_invisible=1;
  var sc_security="` + sc_security + `";
  var sc_https=1;
  var sc_remove_link=1;
`;
document.body.append(scriptA);

第五步:优化兼容性

一般统计代码在脚本无法正确运行的时候,会采取备用方式,就是插入一张图片。这里我们准备好这张图片,当上面的代码没有能够正确注入时,就插入这一张图片。同样的,代码中项目编号和加密的识别码,我都用变量替代了。

scriptB.type = 'text/javascript';
scriptB.src = 'https://www.statcounter.com/counter/counter.js';
document.body.append(scriptB);
scriptB.onerror = () => {
  const img = document.createElement('img');
  img.src =
    'https://c.statcounter.com/' +
    sc_project +
    '/0/' +
    sc_security +
    '/1/#' +
    Number(new Date());
  img.style = 'display: none';
  document.body.append(img);

第六步:降低干扰

这个干扰是双方向的,一方面在页面打开时就插入统计代码,虽然影响本身是很小的,但终归会让页面打开的速度稍微慢一丢丢。如果等页面彻底打开之后,再去加入统计代码,这样对用户的影响会更小。

同时如果用户还没有等这个页面彻底打开,就将页面关闭了,那么对这个页面进行统计也没有什么必要。所以我将插入框架的时间进行了一个延迟。

第七步:控制次数

但是我只是希望了解用户使用的脚本管理器和一些版本号的情况。就没有必要用户访问,每一个页面都进行统计。所以我设置了每天只统计一次。但这一点需要脚本管理器提供数据存储的接口才能够实现。

首先我获取当前的时间,然后除以一天的好秒数,将结果取整数作为一个标记。

然后查看脚本存取的数据里的标记和这个标记是否相同,如果相同那么意味着今天已经做过统计了,就无需再插入统计代码。如果不同,那么插入统计代码,同时更新储存的这个标记为当前标记。


以上便是在脚本中放入统计代码的思路分析,都不复杂,但是这些细节我也确实思考了好久,尽自己的可能去降低对用户的影响。

下面放出相对完整的代码:

// 如果用户拒绝追踪,则不添加统计代码
if (!navigator.doNotTrack) {
  // 如果是预定页面,则进行改造
  if (/^https:\/\/c\.statcounter\.com\//i.test(window.location.href)) {
    // 在标题中放入脚本相关信息
    document.title = GM_info
      ? GM_info.scriptHandler.replace(/monkey/i, '') +
        '-' +
        GM_info.version +
        '-' +
        GM_info.script.version
      : navigator.userAgent + '-' + scriptVersion;
    // 插入统计代码
    document.body.innerHTML = '';
    const scriptA = document.createElement('script');
    scriptA.type = 'text/javascript';
    scriptA.innerHTML =
      `var sc_project=` + sc_project + `;
      var sc_invisible=1;
      var sc_security="` + sc_security + `";
      var sc_https=1;
      var sc_remove_link=1;
    `;
    document.body.append(scriptA);
    const scriptB = document.createElement('script');
    scriptB.type = 'text/javascript';
    scriptB.src = 'https://www.statcounter.com/counter/counter.js';
    document.body.append(scriptB);
    scriptB.onerror = () => {
      const img = document.createElement('img');
      img.src =
        'https://c.statcounter.com/' +
        sc_project +
        '/0/' +
        sc_security +
        '/1/#' +
        Number(new Date());
      img.style = 'display: none';
      document.body.append(img);
    };
  } else {
    // 非预定页面则插入统计代码
    setTimeout(() => {
      const todayMark = Math.floor(+new Date() / 864e5); // 获取今日时间标记
      const recordMark = GM_getValue('timeMark', 0); // 获取记录中时间标记
      if (todayMark !== recordMark) {
        // 时间标记不同则插入统计代码
        const href =
          'https://c.statcounter.com/' + window.location.hostname + '/';
        const iframe = document.createElement('iframe');
        iframe.src = href;
        iframe.style = 'display: none !important; width: 0; height: 0;';
        document.body.appendChild(iframe);
        GM_setValue('timeMark', todayMark); // 将新的时间标记存入记录
      }
    }, delay);
  }
}

工具类

因为自己的需求,写了一个通用的工具类,用来把一些可复用的操作放在一起。这个事情设想起来很简单,实际动手才发现并不容易,因为一个操作如果只是用在某个地方,那实现功能就好了;而如果想让它能在许多类似的情况下都良好的运行,需要考虑的问题就非常多了。

前前后后写了一个星期,扭头一看,写了一堆语法糖……唔,大概是这么称呼吧,我不擅长这些名词。反正总有点可有可无的意思。

不过作为示例也好,作为工具也罢,放出来供大家探讨吧。我自己现在是有在使用的,未来也会随着需求不断向其中加入新的方法。

说明文档在这里( https://dmscode.github.io/DMS-UserScripts-Toolkit/ ),里面有比较详细的说明,不过依然需要一点 JS 基础才能比较好的理解和使用。

文本替换小工具

这个需要首先了解 Node.js 的工具链,就是 npm 安装使用依赖那一套。

这个工具就是为了解决我喜欢到处写版本号的问题,看起来好像帅帅的,但修改版本号要哭啊!所以写个工具,一处修改,替换所有。

不止替换关键字,还能用某个文件的内容来替换特定关键字,用图片文件的 base64 编码替换关键字等。

详细使用说明在如下地址:

这个不算实例,就是一个小工具,或许有用吧,希望如此。


今天自己安装时发现名称不对,一检查,日常拼写出错,尴尬死,发布以后这名称也不好改了,就……就这么滴吧。

太强了!能不能整理成一个完整的文档或者独立的网页呀?我挺想支持,但是目前浏览起来真的很费劲…

倒不是不可以,但不如这边修改、更新、交流、讨论方便些。而且一楼有详细的目录哦,点击链接可以快速定位的。

收到了,今天晚上应该会上第一波内容,小老鼠会很认真很努力滴~~

还有,十分感谢支持~(这真的会给小老鼠好多信心的说!!

大环境肯定会越来越好的。疫情和国际形势也不会一直差的。

希望