免費使用谷歌的翻譯介面
引言
最近做個東西,需將各種語言翻譯成中文,看了各家的翻譯效果,還是谷歌的最好。
但谷歌的未提供免費介面,研究了谷歌的 翻譯頁面 ,輸入內容後會觸發ajax請求,請求引數中除了輸入內容,還有個加密引數 tk
,該加密演算法在壓縮的js程式碼中,我也在網上找到了網友摘出來的程式碼,js格式,一大段,壓縮程式碼翻譯起來很吃力,遂未翻譯,而另闢蹊徑,在生產環境的docker中打包了node環境,業務程式碼通過shell呼叫這段js,得到加密引數後再模擬請求,獲得翻譯結果,用著還挺好。
沒過多久,發現失效了,請求返回403 Forbidden,禁止訪問 :no_entry_sign:,估計加密引數演算法又升級了。
介面翻譯固然好用,但隔斷時間就要重新搞一次加密演算法,這個就有點兒難以接受了,每次都要從大量壓縮js程式碼中找出加密演算法,還不一定能完全找對。
至此,我們的主角無頭瀏覽器 puppeteer 就要登場了,puppeteer一個node類庫,提供了簡潔的API,可以讓使用者操作chrome瀏覽器,基本可以完全模擬人的操作,比如開啟頁面、輸入網址、等待頁面指定內容載入、點選按鈕、甚至滑動也可以,有了這個工具模擬使用者翻譯然後獲取結果完全沒問題。
獲取翻譯結果
通過js獲取
分析了谷歌翻譯頁面的元素,發現使用者輸入內容的時候會觸發某些按鈕變灰,等到翻譯完成,按鈕會再次變亮,這其實是通過新增去除 *-disabled
類來實現的,所以當我們模擬輸入之後等待該類消失即可
await page.waitForSelector('selector-ele-disabled', {hidden: true});
待元素變亮(去除了 *-disabled
類),就可以從結果輸入框中獲取到結果了。
但這樣實現起來比較麻煩,也不夠直接,還需要puppeteer呼叫chrome的js執行環境去獲取,獲取的也不是原始的介面返回資料。因此通過查閱文件找到了下面更好的方法:point_down:。
通過攔截請求返回獲取
前段時間研究瞭如何爬取手機app中的資料,裡面用到了中間人代理攻擊,中間人代理轉發請求、返回,轉發的時候就可以對請求進行攔截處理,我就想puppeteer應該也有,果然查到了 event-response ,他是 Page 例項的一個鉤子,如果我們設定了 "response": function callback(response){}
,當chrome發出的任何一個請求返回的時候,都會觸發他,並將類 Response 的一個例項傳給回撥函式,裡面包含請求url、請求結果、請求結果狀態等資訊,這樣我們就可以檢測我們的翻譯介面了
let browser = await puppeteer.launch() let page = await browser.newPage() page.on('response', async response => { const url = response.url() if (url.indexOf("檢測的介面地址") != -1) { let text = await response.text() // text就是介面返回的結果,拿到介面原始資料,接下來就任你處理了 } })
設計
大體流程如下圖所示,初始化例項,等待請求,請求到達之後模擬輸入,然後返回結果,再次進入等待請求狀態。
此文的最終目的是可以為呼叫者提供一個簡潔的介面,請求該介面返回,返回為中文的結果,介面的響應時間儘可能的短,可支援併發。
響應時間沒多少可以優化的地方,主要依賴網路環境,以及谷歌的介面響應時間,我們只能做到當谷歌介面返回的時候我們也第一時間返回給呼叫者。
併發這裡可以做優化,一個puppeteer同一時間只能處理一個翻譯請求,如果做個例項池,維護多個puppeteer例項,這樣就可以提升翻譯介面的併發能力了。
例項池
如下圖所示,虛線框內表示一個例項池,例項池中有多個puppeteer例項,他們之間互相獨立,當請求來的時候,隨機從池子中拿出一個例項,處理請求,等待請求處理完畢之後,再次將改例項放回池子中。
為了減少意外情況,池子中的每個例項處理100個翻譯之後推出,重新啟動一個新的額例項補充進來,池子中的例項總量保持不變,如果需要甚至可以搞成動態的,像php-fpm一樣,請求多的時候動態增加例項池中的例項,空閒的時候,清理推出一些例項。
如何將請求和結果聯絡起來
一個請求可以分為兩個流程,一個 請求流程
,一個 ajax成功回撥流程
,請求時候輸入翻譯原始內容,例項內部在請求谷歌ajax介面成功的時候呼叫預先註冊好的回撥函式,這兩個流程沒有辦法直接聯絡起來,但他們都會接觸到同一個例項,所以用這個例項將他們倆聯絡起來,ajax流程成功之後寫入一個變數到例項物件上,請求流程中監測該例項上的變數,有資料說明請求成功,返回資料,清空該變數,原理可以看下面的簡化程式碼
let obj = {} setTimeout(() => { obj.result = "this is async result" }, 2000) async function sleep(duration) { return new Promise(resolve => { setTimeout(() => { resolve() }, duration) }) } async function getRet() { let times = 1 while(times <= 100) { if (obj.result) { return Promise.resolve(obj.result) } else { await sleep(200) } times++ } } (async () => { let ret = await getRet() console.log(ret) console.log("now i can do something") })()
實現
我將這個功能包裝成了一個類庫,上傳到了npm, google-trans-api ,順便也熟悉了整個打包流程以及typescript的使用,不得不說typescript真是不錯,可以防止很多誤寫的錯誤,還有自動提示的功能,用起來不要太爽。這裡是原始碼地址 aizuyan/google-trans-api 。
使用
下載Chromium
linux、mac下面的Chromium是兩個不同的包,如果網路可以翻牆,直接部署安裝即可,否則需要手動下載, 傳送門 ,我的網路就不好,因此提前將兩個版本的包放在專案根目錄下的 Chromium
目錄下,開發環境使用 darwin
目錄下的包,生產環境使用linux下的包
. ├── Chromium │├── darwin │└── linux
配合下面的程式碼,可以自動根據環境選擇使用的包路徑,並傳入例項的 executablePath
引數中
"use strict" const path = require("path") const os = require("os") const platform = os.platform() let ret = path.join(__dirname, "..", "Chromium", platform) switch (platform) { case "linux": ret = path.join(ret, "chrome") break case "darwin": ret = path.join(ret, "Chromium.app/Contents/MacOS/Chromium") break } module.exports = ret
代理
如果網路不好,可能需要安裝代理,可以使用shadowsocks,支援所有環境,預設沒有代理,如果需要,可以在初始化的時候傳入 proxyServer: '--proxy-server=socks5://127.0.0.1:1080'
引數。
完整的試用版本
const koa = require('koa') const app = new koa() const router = require('koa-router')(); const GoogleTrans = require('google-trans-api').default // 呼叫的時候改為你自己的 const chromePath = '/path/to/puppeteer Chromium'; (async () => { let instance = new GoogleTrans({ handles: false, worker:3, executablePath: chromePath, initPageTimeout: 0, //proxyServer: '--proxy-server=socks5://127.0.0.1:1080', regExpIncludeUrl: url => { const reg = new RegExp("translate.google.cn/translate_a/single.*?q=.*") return reg.test(url) }, responseCb: async response => { const url = response.url() console.log(url) try { const text = await response.text() const status = response.status() let ret = JSON.parse(text) ret = ret[0] let data = "" for (let i = 0; i < ret.length; i++) { if (ret[i][0]) { data += ret[i][0] } } return Promise.resolve(data) } catch (err) { console.error(`Failed getting data from: ${url}`) console.error(err); } } }) let flag = await instance.init() router.get('/trans-auto', async ctx => { try { let msg = decodeURIComponent(ctx.query.msg) let ret = await instance.trans(msg) ctx.response.body = ret } catch (e) { console.log(`[error] when trans ${e.message}`) ctx.response.body = "" } }) app .use(router.routes()) .use(router.allowedMethods()) app.listen(3000, () => { console.log('server is running at http://localhost:3000') }) })()
下面是我開啟GUI模式,看效果的圖片