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

推荐使用 Tampermonkey/Violentmonkey:
// ==UserScript== // @name V2EX 兑换码高亮助手 (多页评论) // @namespace https://v2ex.com/ // @version 1.5 // @description 高亮显示作者发布的兑换码(正文 + 附言),抓取多页评论兑换码,评论中出现的默认已使用。 // @match 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); })(); 1 korvin 16 天前 哈哈,和我之前写的差不多 /t/1127520 |
2 saimax 16 天前 想法是好的,但实际情况兑换了回复的不足 1 成。所以没啥用 |
3 HMYang33 16 天前 基数不够大,如果是腾讯或谷歌做的,估计有点用 |
5 直接复制全文丢到 AI 里 |
6 callv 16 天前 我写的这个社区可以直接记录兑换,你们看看效果是不是更好一些。https://2libra.com/post/festival-things/IpsWhjF |
9 deplives 16 天前 实际上没啥用,用码后回复的我觉得不到 1/10 |