MANKA の Blog
研究研究浏览器中的翻译组件

2023-01-15

#Google翻译#微软翻译#Yandex翻译#搜狗翻译#百度翻译

最近为了给 Twitter Monitor 增加几个翻译源,我研究了几个自带翻译功能的浏览器(不出意外全是 Chromium 系的)

Chrome

首先是老大哥 Chrome,自带的翻译源是 Google 翻译,作为我日常使用的浏览器,我觉得这玩意还是很有用的,处理 Google 翻译的 api 唯一的难点就是那个神秘的 tk ,这个值用于校验传入的内容是否有误,出错了直接送400不解释。

这个 tk 值的原理大概就是根据待翻译文本的每一个字符的 charCode 做计算得出一个的数组,再拿这个数组去做一系列的运算,我还是直接放代码吧

其中函数 GoogleTranslateTk() 的传入值 tkk 是动态的,每隔一段时间都会更新,不过历史上出现过的任何一个 tkk 计算出来的值都是合法的,所以可以当作是固定的值

至于dom处理还是很简单粗暴的(还是比较好的了),Google 会对链接和特殊文本进行处理,链接会改成类似 <a id=${index}>${content}</a> 的格式,其中这个 index 就表示那段文字中的第几个链接,别的我忘了,后续再补上

Google 翻译太常用了,已经有大量前人研究过这玩意了,剩下的细节方面大家可以看这篇文章

Microsoft Edge

作为 Chromium 系的搅局者,Windows 系统自带的 Edge 那自然是不得不提的存在,Edge 浏览器自带的翻译源是微软翻译,作为前推文翻译的内容提供商(目前是Google),它的翻译质量还算是可以保障的,不过这家其实很简单,只需要获取一个 jwt 就能用了,这个 token 的有效时长不算很长,大约 9.5 分钟

const jwt = await axios.get('https://edge.microsoft.com/translate/auth')const content = await axios.post(`https://api.cognitive.microsofttranslator.com/translate?from=&to=${target}&api-version=3.0&includeSentenceLength=true`, JSON.stringify(textArray.map(text => ({Text: text}))), {    headers: {        'content-type': 'application/json',        authorization: `Bearer ${jwt}`    }})

这家是用奇奇怪怪的 <b${type}>${content}<b${type}> 来分割 dom,其中不同的 tag 会分配给不同的 type,比如 <a> 分配到的是20<strong>分配到的是10

Yandex Browser

我折腾这家的经历就很好地诠释了 为了这点醋我才包了这顿饺子 这句话,为了这个翻译源我顺便花了一天趟了部署 fasttext 这道坑。

前两家都支持自动判断语言,所以只需要传一个目标语言就可以了,但 Yandex Translate 不支持这玩意,所以需要自己传一个进去,因此就有了我上面的趟坑,即使如此,fasttext传出的语言代码不一定跟 Yandex 使用的相同,这问题可以有两种解决办法,一是直接不支持不同的语言,二是自己做一份映射表……以后我应该会把第二点的坑填上,

至于 dom 处理是跟 Chrome差不多的,可以直接参考 Chrome 的方案

lang = predictFromFastText()//...//from yandex browserconst generateSid = () => {    var t, e, n = Date.now().toString(16)    for (t = 0, e = 16 - n.length; t < e; t++) {        n += Math.floor(16 * Math.random()).toString(16)    }    return n}const supportedLanguageList = ["af","sq","am","ar","hy","az","ba","eu","be","bn","bs","bg","my","ca","ceb","zh","cv","hr","cs","da","nl","sjn","emj","en","eo","et","fi","fr","gl","ka","de","el","gu","ht","he","mrj","hi","hu","is","id","ga","it","ja","jv","kn","kk","kazlat","km","ko","ky","lo","la","lv","lt","lb","mk","mg","ms","ml","mt","mi","mr","mhr","mn","ne","no","pap","fa","pl","pt","pt-BR","pa","ro","ru","gd","sr","si","sk","sl","es","su","sw","sv","tl","tg","ta","tt","te","th","tr","udm","uk","ur","uz","uzbcyr","vi","cy","xh","sah","yi","zu"]let query = new URLSearchParams({    translateMode: 'context',    context_title: 'Twitter Monitor Translator',//自定义的标题,可以自己改    id: `${generateSid()}-0-0`,    srv: 'yabrowser',    lang: `${lang}-${target}`,    format: 'html',    options: 2})const content = await axios.get('https://browser.translate.yandex.net/api/v1/tr.json/translate?' + query.toString() + '&text=' + (textArray.map(text => encodeURIComponent(text)).join('&text=')))

QQ 浏览器

作为国产浏览器的老大哥之一,QQ 浏览器的使用者还是挺多的,所以我拿它来作为最后一个研究的浏览器。

跟前面三家不一样,QQ 浏览器是用一个搜狗翻译的插件完成这个功能的,这个插件疑似调用了一些 QQ 浏览器的私有 api 或者是过时的 Chromium 的 api,反正我尝试在最新版本的 Chrome (Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36) 上加载解压后的拓展程序是用不了的,而 QQ 浏览器没直接在拓展程序页面提供调试插件的办法(难道他们自己都不用?),我懒得研究各种奇技淫巧就直接拆包了

反正没多复杂,很简单就解决了

let body = JSON.stringify({    from_lang: "auto",    to_lang: target,    trans_frag: textArray.map(text => ({text}))})const content = axios.post('https://go.ie.sogou.com/qbpc/translate', `S-Param=${body}`)

我研究了一番发现它不会翻译 # ,所以理论上只需要用两个 # 包裹编号的格式就可以解决 dom 的问题了,大概就是 #${index}#

百度翻译

研究之余我还有一个意外发现,百度翻译不再需要一顿复杂的拿 token 的流程了,鉴于百度 sign 的原理跟 Google 基本是一样的,只不过百度遇到长度大于30的字符串时会切割前中后各10个字符组成总长30的新字符串

const gtk = [320305, 131321201]//应该跟Google差不多,是永久有效的,最好还是实时获取啦const baiduPrefix = (text) => {    let textArray = [...text]    if (textArray.length > 30) {        return textArray.slice(0, 10).join("") + textArray.slice(Math.floor(textArray.length / 2) - 5, Math.floor(textArray.length / 2) + 5).join("") + textArray.slice(-10).join("")    }    return text}const sign = GoogleTranslateTk(baiduPrefix(text), gtk)

别的还是自己去抓包啦,反正也不难

DeepL

其实我还是有研究了一番的,不过一直报 429 我就不想研究了,这家风控挺烦人的,我懒得折腾了

2023-02-20 更新

其实这个的原理还是挺好分析的,以前还要逆向客户端,现在看看chrome插件源码就好了,细节方面我不敢说,免得这篇文章被 DeepL 干掉。不过这些细节在代码层面都是直接拍脸上的,所以不难,但不要想当然,所见的未必是原本的意思

碎碎念

  • 腾讯系的翻译网站有三个!三个!我发现这个情况时还是很震撼的,这三个分别是 搜狗翻译腾讯翻译君以及腾讯交互翻译
  • 各家使用的语言代码都有自己的花样,不过一般都比较遵守 ISO 639 的,百度比较特立独行搞了一堆乱七八糟的代码,不加预处理应该是没法跟其他源共用一套来源的
  • 其实我还研究了 Safari,不过实在搞不来 iOS 的抓包外加后面查到有前人提到这个接口一过代理就会访问失败,最后只抓到一个 url: https://sequoia.apple.com
  • 你说我为啥不研究研究360翻译?emmm自己看吧
    我也不用!

    我也不用!


    不过他们好像自己套了一层代理,还挺有意思的
    'https://elephant.browser.360.cn/?t=translate&m=google&anno=3&client=te_lib&format=html&v=1.0&key=AIzaSyBOti4mM-6x9WDnZIjIeyEU21OpBXqWBgw&logld=vTE_20200506_00&sl=en&tl=zh-CN&sp=nmt&tc=1&sr=1&tk=669631.848623&mode=1'# body我懒得写了,反正就只是套了一层代理,别的格式是一样的
  • 我写了两段正则表达式来判断简中还是繁中……虽然感觉很鸡肋,不过写都写了别浪费了
    const isChs = (lang = 'zh') => /^zh(?:_|\-)(?:cn|sg|my|chs)|zh|chs|zho$/.test(lang.toLowerCase())const isCht = (lang = 'zh_tw') => /^zh(?:_|\-)(?:tw|hk|mo|cht)|cht$/.test(lang.toLowerCase())

参考


评论区