声明在先:纯技术分享,仅供学习交流。
不喜欢请默默关掉,不必喷我,喷了我也不回(手动狗头保命🐶)。
前段时间,看到一篇很有意思的文章,讲的是如何绕过那些“脱裤子放屁”式的外链跳转页面——就是你点开一个链接,不是直接跳走,而是先到一个中转页,提醒你“即将离开本站,请注意安全”的那种。
原文在这里 👉🏻
脱裤子放屁 - 你们讨厌这样的页面吗?一个浏览器插件,用于跳过掘金,知乎,少数派等网站的外链中转站页面。让你的互联网浏览 - 掘金
看完之后我就在想:这种事情,能不能让 AI 帮我写个脚本搞定?
于是,我打开了某 AI 工具,扔给它几句话,它噼里啪啦就给我吐出了一段油猴脚本。
📜 脚本代码(AI 生成版)
// ==UserScript==
// @name Redirect Skipper
// @namespace http://tampermonkey.net/
// @version 1.1
// @description 自动跳过链接中转页面,包含详细调试日志
// @author Original idea from juejin.cn article
// @match *://*/*
// @grant GM_log
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
// 调试开关 - 设置为 true 时在控制台输出详细日志
const DEBUG = false;
function debugLog(...args) {
if (DEBUG) {
console.log('[Redirect Skipper]', new Date().toLocaleTimeString(), ...args);
}
}
function errorLog(...args) {
console.error('[Redirect Skipper] ERROR:', ...args);
}
// 需要处理的网站列表
let hostnames = ["juejin.cn", "sspai.com", "zhihu.com"];
debugLog('脚本开始执行');
debugLog('当前域名:', location.hostname);
debugLog('处理的域名列表:', hostnames);
/**
* 检查当前域名是否在支持列表中
*/
function checkCurrentHostname() {
const currentHost = location.hostname;
const isSupported = hostnames.some(host => {
// 支持子域名匹配,比如 www.zhihu.com 匹配 zhihu.com
return currentHost === host || currentHost.endsWith('.' + host);
});
debugLog('域名检查结果:', {
'当前域名': currentHost,
'是否支持': isSupported,
'匹配规则': isSupported ? hostnames.find(h => currentHost === h || currentHost.endsWith('.' + h)) : '无'
});
return isSupported;
}
/**
* 核心函数:查找并替换带有 ?target= 参数的链接
*/
function findByTarget() {
const isSupported = checkCurrentHostname();
if (!isSupported) {
debugLog('当前域名不在处理列表中,跳过执行');
return;
}
debugLog('开始查找需要处理的链接...');
const linkKeyword = "?target=";
const selector = `a[href*="${linkKeyword}"]:not([data-redirect-skipper])`;
debugLog('使用的选择器:', selector);
try {
const aLinks = document.querySelectorAll(selector);
debugLog('找到的链接数量:', aLinks.length);
if (!aLinks.length) {
debugLog('未找到符合条件的链接');
return;
}
aLinks.forEach((a, index) => {
const originalHref = a.href;
debugLog(`处理链接 ${index + 1}:`, originalHref);
const targetIndex = originalHref.indexOf(linkKeyword);
if (targetIndex !== -1) {
const encodedUrl = originalHref.substring(targetIndex + linkKeyword.length);
const decodedUrl = decodeURIComponent(encodedUrl);
debugLog(` ✓ 解析结果:`, {
'原始URL': originalHref,
'target位置': targetIndex,
'编码后的目标URL': encodedUrl,
'解码后的目标URL': decodedUrl
});
// 验证URL是否有效
if (decodedUrl && decodedUrl.startsWith('http')) {
a.href = decodedUrl;
a.setAttribute("data-redirect-skipper", "true");
debugLog(` ✓ 已替换为:`, decodedUrl);
// 添加点击事件监听,记录用户交互
a.addEventListener('click', function(e) {
debugLog('用户点击了已处理的链接:', decodedUrl);
}, { once: true });
} else {
errorLog('解析出的URL无效:', decodedUrl);
}
} else {
debugLog(` ✗ 未找到 ${linkKeyword} 参数`);
}
});
debugLog('链接处理完成');
} catch (err) {
errorLog('执行过程中发生错误:', err);
}
}
/**
* 监听文档变化(用于处理动态加载的内容)
*/
function observerDocument() {
debugLog('开始监听文档变化...');
const observer = new MutationObserver((mutationsList) => {
debugLog('检测到文档变化,变更数量:', mutationsList.length);
for (const mutation of mutationsList) {
if (mutation.type === "childList" && mutation.addedNodes.length) {
debugLog('发现新增节点,数量:', mutation.addedNodes.length);
debugLog('新增节点:', mutation.addedNodes);
findByTarget();
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
debugLog('MutationObserver 已启动');
return observer;
}
/**
* 扫描页面中的链接并统计信息
*/
function scanPageLinks() {
const allLinks = document.querySelectorAll('a[href]');
const targetLinks = document.querySelectorAll('a[href*="?target="]');
const processedLinks = document.querySelectorAll('a[data-redirect-skipper]');
debugLog('页面链接统计:', {
'总链接数': allLinks.length,
'包含?target=的链接数': targetLinks.length,
'已处理的链接数': processedLinks.length
});
// 列出所有包含?target=的链接
targetLinks.forEach((link, i) => {
debugLog(`链接${i}:`, link.href, '已处理:', link.hasAttribute('data-redirect-skipper'));
});
}
// 主执行流程
function init() {
debugLog('===== 脚本初始化开始 =====');
// 检查油猴环境
if (typeof GM_info !== 'undefined') {
debugLog('油猴脚本信息:', {
'脚本名称': GM_info.script.name,
'版本': GM_info.script.version,
'运行位置': GM_info.scriptRunAt
});
}
// 初始扫描
scanPageLinks();
findByTarget();
// 启动DOM变化监听
const observer = observerDocument();
// 监听页面事件
const events = ['load', 'hashchange', 'popstate'];
events.forEach(event => {
window.addEventListener(event, function() {
debugLog(`事件触发: ${event}`);
if (event === 'load') {
scanPageLinks();
}
findByTarget();
});
});
// 为了调试,将关键函数暴露到全局
if (DEBUG) {
window.RedirectSkipper = {
scanPageLinks,
findByTarget,
checkCurrentHostname,
observer
};
debugLog('调试函数已暴露到 window.RedirectSkipper');
}
debugLog('===== 脚本初始化完成 =====');
}
// 延迟初始化,确保DOM已加载
if (document.readyState === 'loading') {
debugLog('文档还在加载中,等待DOMContentLoaded事件');
document.addEventListener('DOMContentLoaded', init);
} else {
debugLog('文档已就绪,立即初始化');
init();
}
})();
🤖 我是怎么让 AI 写出来的?
其实提示词特别简单,大意是:
“帮我写一个油猴脚本,自动跳过掘金、知乎、少数派等网站的外链中转页,直接跳转到目标链接。”
第一次生成的代码运行后没生效,我就让它“加一下详细的日志,方便我调试”。结果第二次生成的版本直接就能用了,连调试都省了……
不得不说,这年头,善用 AI 真是提效大杀器。
⚙️ 如何自定义支持其他网站?
如果你希望这个脚本也处理其他网站,只需要修改代码开头 hostnames 数组。
比如加一个 example.com:
let hostnames = ["juejin.cn", "sspai.com", "www.zhihu.com", "example.com"];
改完保存,刷新页面即可生效。
🧠 实现原理(一张图看懂)
简单来说,就是:
- 匹配当前域名是否在支持列表里;
- 找出所有带
?target=参数的链接; - 提取
target后面的真实地址,替换掉原链接; - 监听页面变化,动态加载的内容也能处理。
💭 一些随想
为什么这么多网站都要加跳转页?
原文作者列了几点:
- 防止钓鱼攻击
- 增强用户安全意识
- 品牌保护
- 遵守法律法规
- 控制流量去向
不过评论区很多人提到,SEO 可能是更关键的原因。毕竟外链如果出了问题,责任在用户点击,平台可以一定程度上规避风险。
无论初衷如何,最终多出来的那一次点击、多等的那一两秒,都是用户在承担……
温馨提示:本脚本仅供技术交流与学习,使用请遵守相关网站规定,注意网络安全。