apanlin:
看到有大佬分享 APP 兑换码, 但是试了好多, 都是用过的, 即便很多高素质大佬把使用过的贴到了评论区,依然非常难找出一个未使用的兑换码.
于是让 GPT 写了个油猴脚本, 把未使用的兑换码高亮出来方便查找.
当然这个前提是需要大家主动把已经使用的兑换码贴到评论里
高亮显示未使用(绿色)和已使用(红色)兑换码

安装方式
推荐使用 Tampermonkey/Violentmonkey:
- 安装浏览器扩展 Tampermonkey
- 点击 “创建新脚本”,粘贴下面完整脚本
- 保存后访问任意 V2EX 帖子页面,自动生效
使用方法
- 打开 V2EX 帖子页面
- 脚本会自动抓取作者的兑换码 + 评论
- 高亮显示未使用(绿色)和已使用(红色)兑换码
- 页面右下角显示统计信息
完整脚本( v1.6 )
// ==UserScript==
// @name V2EX 兑换码高亮助手 (多页评论)
// @namespace https://v2ex.com/
// @version 1.5
// @description 高亮显示作者发布的兑换码(正文 + 附言),抓取多页评论兑换码,评论中出现的默认已使用。
// @match https://www.v2ex.com/t/*
// @match https://v2ex.com/t/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
const MIN_LEN = 10; // 兑换码最小长度
function extractCodes(text) {
const pattern = new RegExp(`\\b[A-Z0-9]{${MIN_LEN},}\\b`, 'g');
return new Set(text.match(pattern) || []);
}
function extractCodesFromReply(replyNode) {
const codes = new Set();
console.log('[V2EX Code Highlighter] replyNode:', replyNode);
// 遍历 replyNode 的子节点
replyNode.childNodes.forEach(node => {
if (node.nodeType === Node.TEXT_NODE) {
// 文本节点按空格分割
node.textContent.split(/\s+/).forEach(word => {
//console.log('正在解析:', word)
// 全局匹配所有 10 位以上大写字母或数字
const pattern = /\b[A-Z0-9]{10,}\b/g;
const matches = word.match(pattern) || [];
matches.forEach(c => codes.add(c));
});
} else if (node.nodeName === 'BR') {
// <br> 就当作分隔,不需要处理
} else {
// 递归抓取子节点
extractCodesFromReply(node).forEach(c => codes.add(c));
}
});
//console.log('该评论最后得到:', codes)
return codes;
}
function replaceTextNodes(node, callback) {
const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, null, false);
const nodes = [];
let n;
while (n = walker.nextNode()) nodes.push(n);
for (const t of nodes) callback(t);
}
function highlightCodeSpan(code, used) {
const span = document.createElement('span');
span.textContent = code;
span.style.cssText = `
background-color: ${used ? 'red' : 'green'};
color: white;
font-weight: bold;
padding: 2px 4px;
border-radius: 4px;
margin: 0 2px;
font-family: monospace;
`;
span.title = used ? '已用' : '未用';
return span;
}
// 异步抓取评论页内容
async function fetchReplyCodes(url, authorName) {
const commentCodes = new Set();
try {
const res = await fetch(url);
const text = await res.text();
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/html');
const replyNodes = doc.querySelectorAll('.reply_content');
replyNodes.forEach(r => {
const floorNode = r.closest('.cell');
const userLink = floorNode ? floorNode.querySelector('.dark, .username, a[href^="/member/"]') : null;
const userName = userLink ? userLink.textContent.trim() : '';
if (userName === authorName) return; // 跳过作者
extractCodesFromReply(r).forEach(c => commentCodes.add(c));
});
} catch (e) {
console.error('[V2EX Code Highlighter] Fetch page error:', url, e);
}
return commentCodes;
}
async function run() {
const mainPostNode = document.querySelector('#Main .topic_content');
if (!mainPostNode) return;
const authorNode = document.querySelector('#Main .header .fr a[href^="/member/"]');
if (!authorNode) return;
const authorName = authorNode.textContent.trim();
console.log('[V2EX Code Highlighter] Author:', authorName);
const mainCodes = new Set();
const commentCodes = new Set();
// 1️⃣ 抓取作者正文
extractCodes(mainPostNode.innerText).forEach(c => mainCodes.add(c));
// 2️⃣ 抓取作者附言
const subNotes = document.querySelectorAll('#Main .subtle .topic_content');
subNotes.forEach(note => {
extractCodes(note.innerText).forEach(c => mainCodes.add(c));
});
// 输出作者兑换码日志
console.log('[V2EX Code Highlighter] Author codes:', [...mainCodes]);
// 3️⃣ 获取评论页数
const psContainer = document.querySelector('.cell.ps_container');
let totalPages = 1;
if (psContainer) {
const pageLinks = psContainer.querySelectorAll('a.page_current, a.page_normal');
totalPages = Math.max(...Array.from(pageLinks).map(a => parseInt(a.textContent.trim())));
}
console.log('[V2EX Code Highlighter] totalPages:', totalPages);
// 4️⃣ 抓取所有评论页
const currentUrl = window.location.href.split('?')[0];
const pageUrls = [];
for (let p = 1; p <= totalPages; p++) {
pageUrls.push(`${currentUrl}?p=${p}`);
}
for (const url of pageUrls) {
const codes = await fetchReplyCodes(url, authorName);
codes.forEach(c => commentCodes.add(c));
}
console.log('[V2EX Code Highlighter] Comment codes (all pages):', [...commentCodes]);
// 5️⃣ 计算未用
const unusedCodes = [...mainCodes].filter(c => !commentCodes.has(c));
// 6️⃣ 高亮当前页面作者兑换码(正文 + 附言)
const authorContentNodes = [mainPostNode, ...Array.from(subNotes)];
authorContentNodes.forEach(node => {
replaceTextNodes(node, t => {
const text = t.textContent;
const codes = extractCodes(text);
if (!codes.size) return;
const frag = document.createDocumentFragment();
let remaining = text;
codes.forEach(c => {
const parts = remaining.split(c);
frag.appendChild(document.createTextNode(parts.shift()));
const used = commentCodes.has(c);
frag.appendChild(highlightCodeSpan(c, used));
remaining = parts.join(c);
});
frag.appendChild(document.createTextNode(remaining));
t.parentNode.replaceChild(frag, t);
});
});
// 7️⃣ 页面右下角统计
const panel = document.createElement('div');
panel.style.cssText = `
position: fixed;
bottom: 10px;
right: 10px;
background: #222;
color: #fff;
padding: 10px 14px;
border-radius: 8px;
box-shadow: 0 0 6px rgba(0,0,0,0.5);
font-size: 13px;
z-index: 9999;
line-height: 1.5;
`;
panel.innerHTML = `
<b>兑换码统计</b><br>
总数: ${mainCodes.size}<br>
已用: ${commentCodes.size}<br>
可用: ${unusedCodes.length}
`;
document.body.appendChild(panel);
}
window.addEventListener('load', run);
})();