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

實際Mac上單詞的查詢非常的簡單:你可以 command + control + d 來自動劃詞查詢,也可以設(shè)置手勢三指輕拍來喚出結(jié)果。我不滿意的地方在于這種查詢方式無法匯總我查詢過的單詞,另外翻譯的結(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 翻譯

“單詞” + 翻譯 組合搜索的方式,返回的第一條就是百度翻譯的結(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é)果在哪里。

可以發(fā)現(xiàn)所有的結(jié)果都包含在一個class=op_dict_content的div里
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)了這樣一個請求

很顯然這就是我們想要的那部分內(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é)果如下

err_no是狀態(tài)碼,0是成功
err_msg是消息反饋,成功情況下是
successliju_result是一個數(shù)組,里面有4個對象:兩個包含了例句信息的數(shù)組,一個數(shù)字和一個字符串。字符串應(yīng)該是例句來源,數(shù)字是ID,這些都不重要,我們可以忽略。
重點(diǎn)放在liju_result里的兩個數(shù)組。第一個數(shù)組包含的是英語例句,第二個數(shù)組是中文翻譯的內(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)系我改正,謝謝 :)