從零開始實現(xiàn)一個終端小詞典

最近花了一點(diǎn)時間寫了一個詞典小工具。復(fù)制你需要查詢的單詞,在終端輸入ss即可得到查詢結(jié)果。查詢過的單詞和結(jié)果會被追加寫入本地的文件,生成生詞本。

coderDic.gif

實際Mac上單詞的查詢非常的簡單:你可以 command + control + d 來自動劃詞查詢,也可以設(shè)置手勢三指輕拍來喚出結(jié)果。我不滿意的地方在于這種查詢方式無法匯總我查詢過的單詞,另外翻譯的結(jié)果很多時候我看起來太過冗余。

mac自帶詞典的查詢結(jié)果

另外一個我動手的原因是:之前的文章一直太過理論了,為了整理這些內(nèi)容花了太多精力在概念的理解上,我希望能夠找一個機(jī)會動手寫一寫代碼。

項目地址:CoderDic
編程語言:python 2.7.10
系統(tǒng)環(huán)境:macOS 10.12.6
依賴庫:參見requirements.txt


要實現(xiàn)這樣一個小工具,第一步需要思考的是翻譯來源。我本身是希望借助已有的翻譯 api來做這件事情,比如百度翻譯和有道翻譯??墒遣榭次臋n以后我覺得不是特別滿意,理由如下:

  • 需要自己去弄一個key
  • 這部分api應(yīng)用場景是給出一個最貼切的翻譯結(jié)果,所以查詢結(jié)果單一不全面。

舉一個簡單的例子,我利用Postman向百度翻譯請求翻譯apple

{
    "from": "en",
    "to": "zh",
    "trans_result": [
        {
            "src": "apple",
            "dst": "蘋果"
        }
    ]
}

作為翻譯api,這樣的返回沒有問題,按照用戶查詢的內(nèi)容給出最可能的翻譯;但是作為詞典,這樣的結(jié)果不太能夠接受。所以我放棄了這種方案,選擇了爬取百度搜索得到的結(jié)果。

比如我需要查詢apple,我可以百度搜索apple 翻譯

apple 翻譯

“單詞” + 翻譯 組合搜索的方式,返回的第一條就是百度翻譯的結(jié)果,附帶音標(biāo),多語義解釋,例句和包括復(fù)數(shù)過去式等其他的解釋。

這樣第一步就明確了,我們模擬向百度發(fā)起一個搜索請求,獲取返回結(jié)果,代碼如下

def searchWord(word):
    # get html text
    request = urllib2.Request('http://www.baidu.com/s?wd='+urllib.quote(word + "翻譯"))
    response = urllib2.urlopen(request)

    #parse html
    soup = BeautifulSoup(response.read(), "lxml")

利用urllib2和BeautifulSoup來獲取百度返回的結(jié)果并進(jìn)行解析。借助Chrome的開發(fā)者工具,我們來分析一下返回界面,確定我們需要的翻譯結(jié)果在哪里。

Chrome 開發(fā)者工具

可以發(fā)現(xiàn)所有的結(jié)果都包含在一個class=op_dict_contentdiv

content = soup.find_all("div", {'class':"op_dict_content"})

    if content:
            print 'get success'
    else:
            print ‘get fail! Please check your word is correct!'

我們嘗試獲取這個div,如果獲取成功繼續(xù)解析,失敗則提示檢查輸入單詞。重復(fù)上述步驟,我們逐個獲取需要的內(nèi)容。

        #get word symbol
        symbol = ''
        symbols_table = soup.find(class_="op_dict_table")
        symbol_trs = symbols_table.find_all("tr")
        for tr in symbol_trs:
            for td in tr.find_all('td'):
                symbol += stringHandle(td.getText()) + ' '
            print symbol

        #get translations
        translations = []
        translation_table = soup.find_all(class_ = re.compile("op_dict3_english_result_table"));
        for tr in translation_table:
            aaa = ''
            for td in tr.find_all('td'):
                temp = stringHandle(td.getText())
                if temp == '[其他]': 
                    temp = '\n' + temp + '\n'
                translations.append(temp);
                aaa += temp + ' '
            print aaa
        print '\n'

音標(biāo)和翻譯都成功獲取,但是在拿例句的時候發(fā)生了一些問題,在返回的網(wǎng)頁源碼當(dāng)中是沒有這部分內(nèi)容的。

原因非常簡單,這部分的內(nèi)容是通過Ajax動態(tài)獲取的。要獲得這部分的內(nèi)容顯然直接抓取不太現(xiàn)實,為了獲取這部分動態(tài)的資源我們可以利用Selenium+PhantomJS來模擬瀏覽器的環(huán)境,從而請求獲取這部分的內(nèi)容。

但是,但是!這個框架太重了!我們只是要例句這么一點(diǎn)內(nèi)容,不至于這么復(fù)雜。讓我們分析一下網(wǎng)頁到底請求了什么,簡單模擬一下就可以了。

我查看了網(wǎng)頁的源碼,確實在一個<script>里找到了相關(guān)的代碼。這部分代碼很長,我格式化以后把有用的部分展示出來。

var cbName = "bd_cb_dict3_" + +new Date;
$.ajax({
            url: _this.data.sensearchUrl + "?wd=" + 
encodeURIComponent(_this.data.wd) + "&cb=" + cbName,
            jsonpCallback: cbName,
            dataType: "jsonp",
            success: function(data) {
                if (!ajaxFinished) if (0 == data.err_no && data.liju_result) {
...

雖然我不太了解js這部分,但是依然可以看出這里發(fā)起了一個Ajax請求。但是url部分的_this.data.sensearchUrl我不確定是什么,讓我們再借助一下Chrome來看看能不能發(fā)現(xiàn)什么。打開Network里我發(fā)現(xiàn)了這樣一個請求

Network

很顯然這就是我們想要的那部分內(nèi)容!讓我們仔細(xì)分析一下這個請求:這是一個GET請求,包含了四個參數(shù)wd,cb,callback和_;請求地址是https://sp1.baidu.com/5b11fzupBgM18t7jm9iCKT-xh_,我不太確定后面隨機(jī)字符串的含義,但這不重要,我們不必關(guān)心。

這里有一個小坑。如果你輸入的是一個錯誤的單詞,百度搜索會聯(lián)想相近的結(jié)果;而這個接口需要準(zhǔn)確的單詞,不能夠聯(lián)想。

讓我們重新回頭查看一下js的代碼,來分析一下各個參數(shù)的含義。

  • wd應(yīng)該是word的縮寫,也就是我們想要查詢的單詞。注意這里的單詞不帶 翻譯 后綴

  • cb的含義結(jié)合js代碼看是固定前綴 'bd_cb_dict3_' 加上當(dāng)前時間戳

  • callback是Ajax請求指定的回調(diào)名稱,和cb參數(shù)一致

  • _是當(dāng)前時間戳

讓我們來模擬一下這部分的請求

base_url = 'https://sp1.baidu.com/5b11fzupBgM18t7jm9iCKT-xh_/sensearch?'

refererStr = ('https://www.baidu.com/s?ie=utf-8&f=8&'
        'rsv_bp=1&'
        'tn=baidu&'
        'wd=well%20%E7%BF%BB%E8%AF%91&' 
       'oq=learn%2520%25E7%25BF%25BB%25E8%25AF%2591&' 
       'rsv_pq=8a7812c70001e773&' 
       'rsv_t=85ae5zPCwmuK3yQhbD%2BYFkooE%2BMpMYpZQ5kot35E%2FTPqoYXS6tHMjVP4%2BYo&' 
       'rqlang=cn&' 
       'rsv_enter=1&' 
       'rsv_sug3=5&' 
       'rsv_sug1=5&' 
       'rsv_sug7=100&' 
       'rsv_sug2=0&' 
       'inputT=1168&rsv_sug4=2102')

headers = {
    'Host': 'sp1.baidu.com',
    'Referer': refererStr,
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36',
    'X-Requested-With': 'XMLHttpRequest',
}

def fetchExampleWord(word):
    cbName = "bd_cb_dict3_" + str(int(time.time()));
    params = {
        'wd': word,
        'cb': cbName,
        'callback': cbName,
        '_':  str(int(time.time()))
    }
    url = base_url + urllib.urlencode(params)
    try:
        response = requests.get(url, headers=headers)
        if response.status_code == 200:
           print 'get success'
    except requests.ConnectionError as e:
        print('Error', e.args)

返回的是一個json字符串,但是并不規(guī)范,格式是/**/bd_cb_dict3_1522221579963(json),我們需要自己對response做一個截斷然后再解析,結(jié)果如下

response json解析結(jié)果
  • err_no是狀態(tài)碼,0是成功

  • err_msg是消息反饋,成功情況下是success

  • liju_result是一個數(shù)組,里面有4個對象:兩個包含了例句信息的數(shù)組,一個數(shù)字和一個字符串。字符串應(yīng)該是例句來源,數(shù)字是ID,這些都不重要,我們可以忽略。

重點(diǎn)放在liju_result里的兩個數(shù)組。第一個數(shù)組包含的是英語例句,第二個數(shù)組是中文翻譯的內(nèi)容。

詞組內(nèi)容

數(shù)組里嵌套的是多個數(shù)組。第一個對象是單詞或者文字;第二個對象是w_x格式的字符串,其中x代表第幾個;第三個字符串的含義不明,沒有找到特別明顯的規(guī)則。

第四個和第五個對象開始我并沒有特別在意,開始我直接就去拼接字符串了,但是出現(xiàn)一個問題:中文拼接每個漢字中間不需要空格,而英文的單詞之間需要。同時英文拼接還有一個問題在于,單詞后面需要加空格,而標(biāo)點(diǎn)符號后面不需要。

回頭來看第四個和第五個對象,當(dāng)?shù)谒膫€對象為0時表示這是一個后面需要拼接空格的部分,通常這個數(shù)組會有第五個對象,也就是一個‘ ’字符串;當(dāng)?shù)谒膫€對象為1時表示這個部分后面不需要額外的拼接,也就沒有第五個對象了。

分析到這里后面的工作就非常好處理了

        if response.status_code == 200:
            str1 = response.text[27:-1]
            
            json1 = json.loads(str1)
            words = json1['liju_result']
            for x in xrange(0,2):
                temp = ''
                word = words[x]
                for char in word:
                    extraStr = ''
                    if len(char) == 5:
                        extraStr = char[4]
                    if char[3] == 1:
                        temp += str(char[0]) + extraStr
                    else:
                        temp += (str(char[0]) + extraStr
                     
                    
                print temp

到這一步基本的內(nèi)容都已經(jīng)獲取成功。我們需要依次把他們打印輸出到終端,單純的黑色太過單調(diào),我們想要用顏色來標(biāo)識不同的部分,為此在打印部分我們分別設(shè)置了一下顏色

print "\033[1;32m%s\033[0m" %('\n' + word + " 查詢成功!")
print "\033[0;32m%s\033[0m" %('===============================')
print "\n"

查詢的結(jié)果我們需要寫入本地,至于寫入的格式其實并不確定,可以自己定義。因為想要后期添加一個后臺管理,所以我仿照POST請求里Body的方式,把每一個查詢結(jié)果的字符串寫入文件,以一個隨機(jī)字符串Boundary來分割。寫入的路徑依據(jù)環(huán)境變量ENV_CODERDIC_PATH來決定,如果為空就按照os.getcwd()寫入當(dāng)前的工作路徑。

def writeCotent(jsonStr):
    path = os.getenv('ENV_CODERDIC_PATH')
    if not path:
        path = os.getcwd()
        os.putenv('ENV_CODERDIC_PATH', path)

    if not os.path.exists(path):
        print "\033[1;31m%s\033[0m" %('Error: Path "' + path + '" is not exist!')
        return

    filePath = os.path.join(path, __CODERDICNAME)

    with open(filePath, 'a+') as f:
        f.write(jsonStr + '\n')
        f.write(__BOUNDARY + '\n')

考慮到每次輸入命令還需要再粘貼一次單詞非常的麻煩,所以我想直接從粘貼板獲取單詞而不必再自己寫入?yún)?shù)了。

if __name__ == '__main__':

    content = pyperclip.paste()

    searchWord(str(content))

終端輸入

ln 文件路徑 \usr\bin\自定義命令

一個簡單的詞典就完成了。

我本身是一名iOS開發(fā),python部分如果寫的不夠好考慮不夠周全,又或者使用發(fā)現(xiàn)了什么問題。歡迎聯(lián)系我改正,謝謝 :)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容