仿真Burning Vocabulary,带翻译功能,在经济学人、彭博社网站上对单词进行标注并显示中文翻译(智能识别多个相关单词,只标注选中部分)
// ==UserScript==
// @name Burning Vocabulary类似版
// @namespace http://tampermonkey.net/
// @version 70.1
// @description 仿真Burning Vocabulary,带翻译功能,在经济学人、彭博社网站上对单词进行标注并显示中文翻译(智能识别多个相关单词,只标注选中部分)
// @author TCH
// @match *://www.economist.com
// @match *://www.bloomberg.com
// @include *://*economist.com/*
// @include *://*bloomberg.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_listValues
// @grant GM_xmlhttpRequest
// @connect dashscope.aliyuncs.com
// @require https://scriptcat.org/lib/513/2.1.0/ElementGetter.js#sha256=aQF7JFfhQ7Hi+weLrBlOsY24Z2ORjaxgZNoni7pAz5U=
// @license tangchuanhui
// ==/UserScript==
(function() {
// ==================== 千问API配置 ====================
const QIANWEN_API_KEY = 'sk-ee2c525aaba8427aa01049c0d90f7c9a'; // 请替换为你的千问API Key
const QIANWEN_API_URL = 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation';
// ==================== 辅助函数 ====================
// 从全文中查找所有以部分单词开头的完整单词(去重)
function findAllCompleteWords(partialWord, fullText) {
if (!partialWord || !partialWord.trim()) {
return [];
}
partialWord = partialWord.trim().toLowerCase();
const wordBoundaryRegex = /[\s\.,;:!?\-—()\[\]{}"'⁰¹²³⁴⁵⁶⁷⁸⁹]/;
// 清理文本:去除上标数字和翻译标记
const cleanText = fullText.replace(/[⁰¹²³⁴⁵⁶⁷⁸⁹]/g, '').replace(/\[[^\]]+\]/g, '');
const completeWords = new Set(); // 使用Set自动去重
let currentPos = 0;
// 遍历全文查找所有匹配
while (currentPos < cleanText.length) {
// 查找部分单词的下一个出现位置
let foundPos = cleanText.toLowerCase().indexOf(partialWord, currentPos);
if (foundPos === -1) {
break; // 没有更多匹配
}
// 检查是否是单词开头(前面是空格或文本开头)
const isWordStart = foundPos === 0 || wordBoundaryRegex.test(cleanText[foundPos - 1]);
if (isWordStart) {
// 向后拓展获取完整单词
let endPos = foundPos + partialWord.length;
while (endPos < cleanText.length && !wordBoundaryRegex.test(cleanText[endPos])) {
endPos++;
}
const completeWord = cleanText.substring(foundPos, endPos).trim();
if (completeWord.length > 0) {
completeWords.add(completeWord);
}
}
currentPos = foundPos + 1;
}
return Array.from(completeWords);
}
// 从文本中查找部分单词并拓展为完整单词(单个)
function findAndExpandWord(partialWord, text) {
// 如果部分单词为空,返回null
if (!partialWord || !partialWord.trim()) {
return null;
}
partialWord = partialWord.trim();
const wordBoundaryRegex = /[\s\.,;:!?\-—()\[\]{}"']/;
// 在文本中查找部分单词(带空格前缀,确保是单词边界)
const searchPattern = ' ' + partialWord;
let startIndex = text.indexOf(searchPattern);
// 如果在开头找不到(可能是句首),尝试不带空格查找
if (startIndex === -1) {
startIndex = 0;
if (!text.startsWith(partialWord)) {
return partialWord; // 找不到,返回原始单词
}
} else {
startIndex++; // 跳过前面的空格
}
// 向后拓展直到遇到单词边界
let endIndex = startIndex + partialWord.length;
while (endIndex < text.length && !wordBoundaryRegex.test(text[endIndex])) {
endIndex++;
}
const completeWord = text.substring(startIndex, endIndex).trim();
return completeWord;
}
// 提取单词所在的完整句子
function extractSentence(word, fullText) {
// 过滤掉文本中的上标数字(⁰¹²³⁴⁵⁶⁷⁸⁹)
const cleanText = fullText.replace(/[⁰¹²³⁴⁵⁶⁷⁸⁹]/g, '');
// 查找单词在文本中的位置
word = " " + word;
const wordIndex = cleanText.indexOf(word);
if (wordIndex === -1) {
return null; // 如果找不到,返回null
}
// 向前查找句子开始(句号、问号、感叹号或文本开头)
let sentenceStart = wordIndex;
const sentenceStartRegex = /[.!?]\s+/;
while (sentenceStart > 0) {
if (sentenceStartRegex.test(cleanText.substring(sentenceStart - 2, sentenceStart + 1))) {
break;
}
sentenceStart--;
}
// 向后查找句子结束
let sentenceEnd = wordIndex + word.length;
while (sentenceEnd < cleanText.length) {
if (sentenceStartRegex.test(cleanText.substring(sentenceEnd, sentenceEnd + 2))) {
sentenceEnd++;
break;
}
sentenceEnd++;
}
// 返回清理后的句子,并进一步清理可能残留的标注标记
const sentence = cleanText.substring(sentenceStart, sentenceEnd).trim();
// 也过滤掉中文翻译标记 [xxx]
return sentence.replace(/\[[^\]]+\]/g, '');
}
// 调用千问API获取翻译
function getTranslationFromQianwen(word, sentence) {
return new Promise((resolve, reject) => {
if (!QIANWEN_API_KEY || QIANWEN_API_KEY === 'YOUR_API_KEY_HERE') {
console.warn('未配置千问API Key,使用默认翻译');
resolve('翻译');
return;
}
// 如果句子为空,直接返回
if (!sentence || sentence.trim() === '') {
console.warn('句子为空,无法翻译');
resolve('翻译');
return;
}
const prompt = `请翻译句子"${sentence}"中的单词"${word}"在这个语境下的中文意思。只需要返回1-6个汉字的简短翻译,不要解释。`;
GM_xmlhttpRequest({
method: 'POST',
url: QIANWEN_API_URL,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${QIANWEN_API_KEY}`
},
data: JSON.stringify({
model: "qwen-turbo",
input: {
messages: [{
role: "system",
content: "你是一个专业的英语翻译助手,只返回简短的中文翻译,不要任何解释。"
},
{
role: "user",
content: prompt
}
]
},
parameters: {
max_tokens: 50,
temperature: 0.3,
result_format: "message"
}
}),
onload: function(response) {
try {
console.log('千问API响应状态:', response.status);
console.log('千问API响应内容:', response.responseText);
const result = JSON.parse(response.responseText);
// 检查是否有错误
if (result.code || result.error) {
console.error('千问API返回错误:', result.message || result.error);
resolve('翻译');
return;
}
// 尝试多种可能的响应格式
let translation = null;
// 格式1: result.output.choices[0].message.content
if (result.output && result.output.choices && result.output.choices[0] && result.output.choices[0].message) {
translation = result.output.choices[0].message.content;
}
// 格式2: result.output.text
else if (result.output && result.output.text) {
translation = result.output.text;
}
// 格式3: result.choices[0].message.content
else if (result.choices && result.choices[0] && result.choices[0].message) {
translation = result.choices[0].message.content;
}
// 格式4: result.text
else if (result.text) {
translation = result.text;
}
if (translation) {
resolve(translation.trim());
} else {
console.error('无法从响应中提取翻译,响应结构:', result);
resolve('翻译');
}
} catch (error) {
console.error('解析千问API响应失败:', error);
console.error('原始响应:', response.responseText);
resolve('翻译');
}
},
onerror: function(error) {
console.error('千问API调用失败:', error);
resolve('翻译');
}
});
});
}
// 统一的渲染函数,接受容器选择器作为参数
function rendering(containerSelector) {
// 根据选择器获取容器
const container = containerSelector === '.body-content'
? document.querySelector('.body-content')
: document.getElementsByTagName("body")[0];
var allsText = container.innerHTML;
function makeTranslate(completeWord, translation) {
// 如果有翻译,添加到显示中
const translationText = translation ? `[${translation}]` : '';
var num = -1;
var rHtml = new RegExp("\<.*?\>", "ig"); //匹配html元素
var aHtml = allsText.match(rHtml); //存放html元素的数组
allsText = allsText.replace(rHtml, '{~}'); //替换html标签
// 使用正则表达式 + g 标志替换所有匹配
var rWord = new RegExp(completeWord.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), "g");
allsText = allsText.replace(rWord, completeWord + translationText); //替换翻译
allsText = allsText.replace(/{~}/g, function() { //恢复html标签
num++;
return aHtml[num];
});
}
function makeOnceColor(searchVal, nColor, tot) {
searchVal = " " + searchVal;
var sKey = "<span name='addSpan' style='color:" + nColor + ";'><sup style='vertical-align: super; font-size: 0.75em;'>" + tot + "</sup>" + searchVal + "</span>";
var num = -1;
var rStr = new RegExp(searchVal, "g");
var rHtml = new RegExp("\<.*?\>", "ig"); //匹配html元素
var aHtml = allsText.match(rHtml); //存放html元素的数组
allsText = allsText.replace(rHtml, '{~}'); //替换html标签
allsText = allsText.replace(rStr, sKey); //替换key
allsText = allsText.replace(/{~}/g, function() { //恢复html标签
num++;
return aHtml[num];
});
}
function makecolor(searchVal, completeWord, nColor, tot, translation) {
var oDiv = containerSelector === '.body-content'
? document.querySelector('.body-content')
: document.getElementsByTagName("body")[0];
var sText = oDiv.innerHTML;
// 如果有翻译,添加到显示中
const translationText = translation ? `[${translation}]` : '';
var sKey = "<span name='addSpan' style='color:" + nColor + ";'><sup style='vertical-align: super; font-size: 0.75em;'>" + tot + "</sup>" + searchVal + "</span>";
searchVal = " " + searchVal;
var num = -1;
var rStr = new RegExp(searchVal, "g");
var rHtml = new RegExp("\<.*?\>", "ig"); //匹配html元素
var aHtml = sText.match(rHtml); //存放html元素的数组
sText = sText.replace(rHtml, '{~}'); //替换html标签
sText = sText.replace(completeWord, completeWord + translationText); //替换翻译
sText = sText.replace(rStr, sKey); //替换key
sText = sText.replace(/{~}/g, function() { //恢复html标签
num++;
return aHtml[num];
});
oDiv.innerHTML = sText;
}
//alert("开始整体染色1");
let list_value = GM_listValues();
//alert("开始渲染按钮");
let div = document.createElement("div");
div.style = "position:fixed; z-index:90;bottom:20px; left: 0; margin: auto; right: 0;text-align:center;"
div.innerHTML = '<span id="biaozhubiaozhu"style="width:150rpx;z-index:100;margin:15px;background-color: red;font-size: 30px;border-color: red;border-radius: 5px;" >标注</span><span id="quxiaobiaozhu" style="width:150rpx;margin:15px;background-color: black;font-size: 30px;color: white;border-radius: 5px;">取消</span>';
// 异步加载所有翻译
(async function() {
const fullText = container.innerText;
for (var i = 0; i < list_value.length; i++) {
let tot = GM_getValue(list_value[i], 0);
// 从全文中找到所有以list_value[i]开头的完整单词
const completeWords = findAllCompleteWords(list_value[i], fullText);
console.log(`部分单词 "${list_value[i]}" 在全文中找到 ${completeWords.length} 个完整单词:`, completeWords);
// 为每个完整单词分别获取翻译
for (const completeWord of completeWords) {
// 提取包含该完整单词的句子
const sentence = extractSentence(completeWord, fullText);
if (sentence === null) {
console.warn(`无法为单词 "${completeWord}" 提取句子`);
continue;
}
// 使用完整单词获取翻译
const translation = await getTranslationFromQianwen(completeWord, sentence);
console.log(`完整单词 "${completeWord}" 的翻译: ${translation}`);
// 先把所有的翻译加上
makeTranslate(completeWord, translation);
}
makeOnceColor(list_value[i], "red", tot);
}
container.innerHTML = allsText;
// 重新添加按钮(因为上面的 innerHTML 替换会删除按钮)
document.body.append(div);
})();
//监听选择文本的动作
var selectionFirst = null;
var selectionSecond = null;
document.addEventListener("selectionchange", () => {
selectionFirst = selectionSecond;
selectionSecond = document.getSelection()
.toString();
});
//alert("整体染色结束1");
document.onclick = async function(event) {
if (event.target.id == "biaozhubiaozhu") {
selectionFirst = selectionSecond; //在有些浏览器,需要把这句去除
if (selectionFirst !== null && selectionFirst !== void 0 && selectionFirst.toString()) {
// 获取完整页面文本
const bodyContent = document.querySelector('.body-content') || document.getElementsByTagName("body")[0];
const fullText = bodyContent.innerText;
let selectedText = selectionFirst.trim();
let completeWord;
// 清理选中文本,得到完整单词(去除前后的标点符号)
selectedText = selectedText.replace(/^[^\w]+|[^\w]+$/g, '');
// 提取包含该单词的句子
const sentence = extractSentence(selectedText, fullText);
// 获取存储的计数
let tot = GM_getValue(selectedText, 0);
// 如果是第一次标注,获取翻译并高亮显示
if (tot === 0) {
try {
// 从全文中找到所有以selectedText开头的完整单词
const completeWords = findAllCompleteWords(selectedText, fullText);
console.log(`部分单词 "${selectedText}" 在全文中找到 ${completeWords.length} 个完整单词:`, completeWords);
// 为每个完整单词分别获取翻译
for (completeWord of completeWords) {
// 提取包含该完整单词的句子
const sentence = extractSentence(completeWord, fullText);
if (sentence === null) {
console.warn(`无法为单词 "${completeWord}" 提取句子`);
continue;
}
// 使用完整单词获取翻译
const translation = await getTranslationFromQianwen(completeWord, sentence);
console.log(`完整单词 "${completeWord}" 的翻译: ${translation}`);
// 但只标注用户实际选中的部分(list_value[i])
makecolor(selectedText, completeWord, "red", 1, translation);
}
} catch (error) {
console.error('获取翻译失败:', error);
makecolor(selectedText, completeWord, "red", 1, "翻译");
}
}
// 只保存计数,不保存翻译
GM_setValue(selectedText, tot + 1);
}
} else if (event.target.id == "quxiaobiaozhu") {
selectionFirst = selectionSecond; //在有些浏览器,需要把这句去除
if (selectionFirst !== null && selectionFirst !== void 0 && selectionFirst.toString()) {
// 清理选中文本
let completeWord = selectionFirst.trim().replace(/^[^\w]+|[^\w]+$/g, '');
GM_deleteValue(completeWord);
}
}
};
document.body.append(div);
}
// ==================== ElementGetter 已通过 @require 引入 ====================
// ElementGetter 官方库地址: https://bbs.tampermonkey.net.cn/thread-2726-1-1.html
// 使用 elmGetter 对象进行异步元素获取
elmGetter.get('p[class="ArticleBodyText_articleBodyContent__17wqE typography_articleBody__3UcBa paywall"]').then(div1 => {
////删除广告
document.querySelector('.media-ui-FullWidthAd_fullWidthAdWrapper-fClHZteIk3k-').style.display = 'none';
//删除浏览器版本低的提示
const targetElement1 = document.querySelector('.unsupported-browser-notification-container');
if (targetElement1) {
targetElement1.style.display = 'none';
targetElement1.style.visibility = 'hidden';
} else {
//alert('未找到浏览器版本低的提示');
}
rendering('.body-content');
});
elmGetter.get('main[style]').then(div1 => {
rendering('body');
});
})();