Skip to content

Instantly share code, notes, and snippets.

@ricky9w
Last active September 10, 2026 20:55
Show Gist options
  • Select an option

  • Save ricky9w/93c74743d20645db86a46ce00a190c7f to your computer and use it in GitHub Desktop.

Select an option

Save ricky9w/93c74743d20645db86a46ce00a190c7f to your computer and use it in GitHub Desktop.
URL Trimmer — 清理浏览器地址栏中的跟踪参数 (Tampermonkey 用户脚本)

URL Trimmer

一个轻量的 Tampermonkey 用户脚本,只清理浏览器地址栏里的跟踪参数,让你复制、分享出去的链接干净整洁。

设计思路

  • 只动地址栏,不碰网络请求。 与 ClearURLs 之类的工具不同,本脚本不拦截、不改写 fetch / XMLHttpRequest,也不阻止参数发往服务器。它唯一的目标是:让地址栏中显示、以及你复制出去的 URL 是干净的。因此它不会破坏任何依赖这些参数的页面功能。
  • 黑名单模式。 只删除已知的垃圾 / 跟踪参数,其余参数原样保留。没有白名单,不清理 hash,不解包跳转链接。
  • 两级规则:
    1. 全局规则 —— 对所有网站生效(如 utm_*fbclidgclidmsclkid 等);
    2. 站点规则 —— 仅当域名匹配时生效(如 Bilibili 的 spm、淘宝的 pvid、1688 的 kj_agent_plugin 等)。
  • document-start 阶段尽早运行,先清理初始 URL,再通过劫持 history.pushState / history.replaceState 清理单页应用(SPA)在导航过程中写入的参数。全程不触发页面跳转。

工作原理(简述)

脚本启动时把所有站点域名建成一张 Map,运行时按域名层级向上查找(m.youtube.comyoutube.com),命中后用预编译好的正则逐个匹配参数名并删除。匹配开销与站点数量无关,性能稳定。

已支持的网站

YouTube、Twitter / X、GitHub、Bilibili、小红书、淘宝 / 天猫、1688、TikTok、Reddit、Instagram、LinkedIn、微博、搜狐、微信公众号(mp.weixin.qq.com),以及对所有网站生效的全局 UTM / 点击 ID 规则。

安装

  1. 先安装 Tampermonkey 浏览器扩展。
  2. 点击本 gist 中的 url-trimmer.js 原始(raw)链接,Tampermonkey 会自动弹出安装界面,确认安装即可。

更新

脚本元数据里写了 @updateURL / @downloadURL,指向本 gist 的 raw 链接。Tampermonkey 会按它自己的检查周期(默认约每天一次)自动比对版本号:只要 gist 里的 @version 比你本地的高,就会自动下载更新。 你也可以在 Tampermonkey 的「已安装脚本」里手动点「检查更新」。

因为脚本是自包含的单文件(规则直接写在脚本里,不再依赖外部 @require),所以一次版本号递增就能把代码和规则一起、原子地更新到位,不存在规则滞后的问题。

添加 / 修改规则

规则就在 url-trimmer.js 顶部的 RULES 对象里:

const RULES = {
  globalParams: [ /^utm_[a-z_]+$/i, /* ... */ ],
  providers: [
    {
      name: '1688',
      domains: ['1688.com'],          // 子域名自动匹配
      params: [ /^pvid$/i, /^kj_agent_plugin$/i, /* ... */ ],
    },
    // ...
  ],
};
  • 每个参数规则是一条正则,默认用 ^…$ 全字匹配、带 i 忽略大小写;需要前缀匹配时去掉 $(如 Bilibili 的 /^spm/i)。
  • 新增站点就往 providers[] 里加一项;只清理通用垃圾参数就加到 globalParams
  • 改完记得把 @version 递增,否则已安装的客户端不会触发自动更新。

调试时把脚本顶部的 const DEBUG = false; 改成 true,即可在控制台看到命中的站点和被删除的参数。

// ==UserScript==
// @name URL Trimmer
// @namespace https://gist.github.com/ricky9w/93c74743d20645db86a46ce00a190c7f
// @version 0.3.8
// @description Clean tracking parameters from the browser address bar
// @author ricky9w
// @match *://*/*
// @run-at document-start
// @inject-into content
// @grant none
// @updateURL https://gist.githubusercontent.com/ricky9w/93c74743d20645db86a46ce00a190c7f/raw/url-trimmer.user.js
// @downloadURL https://gist.githubusercontent.com/ricky9w/93c74743d20645db86a46ce00a190c7f/raw/url-trimmer.user.js
// ==/UserScript==
'use strict';
(function () {
// Set to true to log matched providers and removed params to the console.
const DEBUG = false;
const TAG = '[URL Trimmer]';
const log = DEBUG ? (...a) => console.log(TAG, ...a) : () => {};
// --- Rules ---
// globalParams: applied to every URL.
// providers[].params: applied only when hostname matches one of `domains`
// (subdomains match automatically via the hostname walk-up).
// Each pattern is anchored with ^…$ for a full-key match unless noted.
const RULES = {
globalParams: [
/^utm_[a-z_]+$/i,
/^mtm_[a-z_]*$/i,
/^gclid$/i,
/^fbclid$/i,
/^msclkid$/i,
],
providers: [
{
name: 'YouTube',
domains: ['youtube.com', 'youtu.be'],
params: [/^feature$/i, /^kw$/i, /^si$/i, /^pp$/i],
},
{
name: 'Twitter',
domains: ['twitter.com', 'x.com'],
params: [/^s$/i, /^t$/i, /^ref_src$/i, /^ref_url$/i],
},
{
name: 'GitHub',
domains: ['github.com'],
params: [/^email_source$/i, /^email_token$/i],
},
{
name: 'Bilibili',
domains: ['bilibili.com'],
params: [
/^buvid$/i, /^is_story_h5$/i, /^launch_id$/i, /^live_from$/i,
/^mid$/i, /^session_id$/i, /^timestamp$/i, /^up_id$/i,
/^vd_source$/i, /^share/i, /^spm/i,
/^callback$/i, /^from_source$/i, /^from$/i, /^seid$/i,
/^from_spmid$/i, /^referfrom$/i, /^bilifeed_channel$/i,
/^unique_k$/i,
/^msource$/i, /^refer_from$/i, /^plat_id$/i, /^bbid$/i,
/^ts$/i, /^visit_id$/i, /^broadcast_type$/i, /^is_room_feed$/i,
/^trackid$/i, /^track_id$/i,
/^caid$/i, /^resource_id$/i, /^source_id$/i, /^request_id$/i,
/^creative_id$/i, /^linked_creative_id$/i,
],
},
{
name: 'Xiaohongshu',
domains: ['xiaohongshu.com', 'xhslink.com'],
params: [
/^xhsshare$/i, /^appuid$/i, /^apptime$/i,
/^share_id$/i, /^xsec_source$/i, /^xsec_token$/i,
],
},
{
name: 'Taobao / Tmall',
domains: ['taobao.com', 'tmall.com'],
params: [
/^spm$/i, /^pvid$/i, /^scm$/i, /^ali_refid$/i, /^ali_trackid$/i,
/^utparam$/i, /^ns$/i, /^abbucket$/i, /^acm$/i,
/^user_number_id$/i, /^activity_id$/i,
/^mi_id$/i, /^xxc$/i, /^priceTId$/i, /^mm_sceneid$/i,
/^from$/i, /^item_type$/i,
],
},
{
name: '1688',
domains: ['1688.com'],
params: [
/^spm$/i, /^pvid$/i, /^scm$/i, /^ali_refid$/i, /^ali_trackid$/i,
/^utparam$/i, /^ns$/i, /^abbucket$/i, /^acm$/i,
/^offerId$/i, /^skuId$/i, /^hotSaleSkuId$/i, /^scene$/i,
/^from$/i, /^trace_log$/i, /^tracelog$/i, /^uuid$/i,
/^sessionid$/i, /^pageId$/i, /^abBizDataType$/i,
/^pcLoginAbJumpBeforeTimeStr$/i, /^cosite$/i,
/^_p_isad$/i, /^clickid$/i,
/^sortType$/i, /^forcePC$/i,
/^kj_agent_plugin$/i,
/^fromkv$/i, /^source$/i, /^amug_biz$/i, /^amug_fl_src$/i,
/^kjSource$/i, /^sourceId$/i, /^td_page_id$/i,
],
},
{
name: 'TikTok',
domains: ['tiktok.com'],
params: [
/^_d$/i, /^_r$/i, /^_t$/i, /^checksum$/i, /^is_copy_url$/i,
/^is_from_webapp$/i, /^sec_uid$/i, /^sec_user_id$/i,
/^share_app_id$/i, /^share_link_id$/i,
],
},
{
name: 'Reddit',
domains: ['reddit.com'],
params: [
/^\$deep_link$/i, /^\$android_deeplink$/i, /^\$ios_deeplink$/i,
/^utm_name$/i, /^share_id$/i, /^ref$/i, /^ref_source$/i,
/^correlation_id$/i, /^rdt$/i,
],
},
{
name: 'Instagram',
domains: ['instagram.com'],
params: [/^igshid$/i, /^igsh$/i],
},
{
name: 'LinkedIn',
domains: ['linkedin.com'],
params: [/^trackingId$/i, /^refId$/i, /^trk$/i, /^midToken$/i],
},
{
name: 'Weibo',
domains: ['weibo.com', 'weibo.cn'],
params: [/^sudaref$/i, /^display$/i],
},
{
name: 'Sohu',
domains: ['sohu.com'],
params: [/^spm$/i, /^pvid$/i],
},
{
name: 'WeChat MP',
domains: ['mp.weixin.qq.com'],
params: [
/^scene$/i, /^from_masonry$/i,
/^sharer_shareinfo$/i, /^sharer_shareinfo_first$/i,
/^poc_token$/i, /^chksm$/i, /^version$/i, /^ascene$/i,
/^mpshare$/i, /^srcid$/i,
],
},
],
};
// --- Domain Map ---
const domainMap = new Map();
for (const p of RULES.providers)
for (const d of p.domains) domainMap.set(d, p);
log(`loaded ${RULES.providers.length} providers, ${RULES.globalParams.length} global rules`);
// --- Provider Lookup ---
function findProvider(hostname) {
let h = hostname;
while (h) {
const p = domainMap.get(h);
if (p) return p;
const dot = h.indexOf('.');
if (dot === -1) break;
h = h.substring(dot + 1);
}
return null;
}
// --- URL Cleaning ---
// Operate on the raw query string so kept params are preserved byte-for-byte.
// We must NOT touch urlObj.searchParams: reading it re-serializes the whole
// query as UTF-8, which corrupts legacy charsets (e.g. 1688 search uses GBK,
// so keywords=%C7%D0... would turn into %EF%BF%BD garbage).
function cleanUrl(url) {
if (!url) return url;
try {
const urlObj = new URL(url, location.href);
if (!urlObj.search) return url;
const provider = findProvider(urlObj.hostname);
const patterns = provider
? RULES.globalParams.concat(provider.params)
: RULES.globalParams;
if (provider) log(`matched provider: ${provider.name} (${urlObj.hostname})`);
const removed = [];
const kept = [];
for (const seg of urlObj.search.slice(1).split('&')) {
const eq = seg.indexOf('=');
const rawKey = eq === -1 ? seg : seg.slice(0, eq);
let key = rawKey;
try { key = decodeURIComponent(rawKey); } catch { /* keep raw key */ }
if (patterns.some((re) => re.test(key))) {
removed.push(key);
} else {
kept.push(seg);
}
}
if (!removed.length) return url;
const query = kept.length ? '?' + kept.join('&') : '';
log(`removed params: [${removed.join(', ')}]`);
log(`cleaned: ${urlObj.pathname}${query}`);
const base = url.startsWith('/') && !url.startsWith('//')
? urlObj.pathname
: urlObj.protocol + '//' + urlObj.host + urlObj.pathname;
return base + query + urlObj.hash;
} catch {
return url;
}
}
// --- History Patching ---
const _pushState = history.pushState.bind(history);
history.pushState = function (state, unused, url) {
return _pushState(state, unused, cleanUrl(url));
};
const _replaceState = history.replaceState.bind(history);
history.replaceState = function (state, unused, url) {
return _replaceState(state, unused, cleanUrl(url));
};
_replaceState(history.state, '', cleanUrl(location.href));
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment