免費試用谷歌的翻譯介面

奔跑的Man發表於2019-05-12

引言

最近做個東西,需將各種語言翻譯成中文,看了各家的翻譯效果,還是谷歌的最好。

但谷歌的未提供免費介面,研究了谷歌的翻譯頁面,輸入內容後會觸發ajax請求,請求引數中除了輸入內容,還有個加密引數tk,該加密演算法在壓縮的js程式碼中,我也在網上找到了網友摘出來的程式碼,js格式,一大段,壓縮程式碼翻譯起來很吃力,遂未翻譯,而另闢蹊徑,在生產環境的docker中打包了node環境,業務程式碼通過shell呼叫這段js,得到加密引數後再模擬請求,獲得翻譯結果,用著還挺好。

沒過多久,發現失效了,請求返回403 Forbidden,禁止訪問 ?,估計加密引數演算法又升級了。

介面翻譯固然好用,但隔斷時間就要重新搞一次加密演算法,這個就有點兒難以接受了,每次都要從大量壓縮js程式碼中找出加密演算法,還不一定能完全找對。

至此,我們的主角無頭瀏覽器puppeteer就要登場了,puppeteer一個node類庫,提供了簡潔的API,可以讓使用者操作chrome瀏覽器,基本可以完全模擬人的操作,比如開啟頁面、輸入網址、等待頁面指定內容載入、點選按鈕、甚至滑動也可以,有了這個工具模擬使用者翻譯然後獲取結果完全沒問題。

獲取翻譯結果

通過js獲取

分析了谷歌翻譯頁面的元素,發現使用者輸入內容的時候會觸發某些按鈕變灰,等到翻譯完成,按鈕會再次變亮,這其實是通過新增去除*-disabled類來實現的,所以當我們模擬輸入之後等待該類消失即可

await page.waitForSelector('selector-ele-disabled', {hidden: true});

待元素變亮(去除了*-disabled類),就可以從結果輸入框中獲取到結果了。

但這樣實現起來比較麻煩,也不夠直接,還需要puppeteer呼叫chrome的js執行環境去獲取,獲取的也不是原始的介面返回資料。因此通過查閱文件找到了下面更好的方法?。

通過攔截請求返回獲取

前段時間研究瞭如何爬取手機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是兩個不同的包,如果網路可以FQ,直接部署安裝即可,否則需要手動下載,傳送門,我的網路就不好,因此提前將兩個版本的包放在專案根目錄下的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模式,看效果的圖片
免費試用谷歌的翻譯介面

相關文章