从零开始实现一个终端小词典

最近花了一点时间写了一个词典小工具。复制你需要查询的单词,在终端输入ss即可得到查询结果。查询过的单词和结果会被追加写入本地的文件,生成生词本。

coderDic.gif

实际Mac上单词的查询非常的简单:你可以 command + control + d 来自动划词查询,也可以设置手势三指轻拍来唤出结果。我不满意的地方在于这种查询方式无法汇总我查询过的单词,另外翻译的结果很多时候我看起来太过冗余。

mac自带词典的查询结果

另外一个我动手的原因是:之前的文章一直太过理论了,为了整理这些内容花了太多精力在概念的理解上,我希望能够找一个机会动手写一写代码。

项目地址:CoderDic
编程语言:python 2.7.10
系统环境:macOS 10.12.6
依赖库:参见requirements.txt


要实现这样一个小工具,第一步需要思考的是翻译来源。我本身是希望借助已有的翻译 api来做这件事情,比如百度翻译和有道翻译。可是查看文档以后我觉得不是特别满意,理由如下:

  • 需要自己去弄一个key
  • 这部分api应用场景是给出一个最贴切的翻译结果,所以查询结果单一不全面。

举一个简单的例子,我利用Postman向百度翻译请求翻译apple

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

作为翻译api,这样的返回没有问题,按照用户查询的内容给出最可能的翻译;但是作为词典,这样的结果不太能够接受。所以我放弃了这种方案,选择了爬取百度搜索得到的结果。

比如我需要查询apple,我可以百度搜索apple 翻译

apple 翻译

“单词” + 翻译 组合搜索的方式,返回的第一条就是百度翻译的结果,附带音标,多语义解释,例句和包括复数过去式等其他的解释。

这样第一步就明确了,我们模拟向百度发起一个搜索请求,获取返回结果,代码如下

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来获取百度返回的结果并进行解析。借助Chrome的开发者工具,我们来分析一下返回界面,确定我们需要的翻译结果在哪里。

Chrome 开发者工具

可以发现所有的结果都包含在一个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,如果获取成功继续解析,失败则提示检查输入单词。重复上述步骤,我们逐个获取需要的内容。

        #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'

音标和翻译都成功获取,但是在拿例句的时候发生了一些问题,在返回的网页源码当中是没有这部分内容的。

原因非常简单,这部分的内容是通过Ajax动态获取的。要获得这部分的内容显然直接抓取不太现实,为了获取这部分动态的资源我们可以利用Selenium+PhantomJS来模拟浏览器的环境,从而请求获取这部分的内容。

但是,但是!这个框架太重了!我们只是要例句这么一点内容,不至于这么复杂。让我们分析一下网页到底请求了什么,简单模拟一下就可以了。

我查看了网页的源码,确实在一个<script>里找到了相关的代码。这部分代码很长,我格式化以后把有用的部分展示出来。

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这部分,但是依然可以看出这里发起了一个Ajax请求。但是url部分的_this.data.sensearchUrl我不确定是什么,让我们再借助一下Chrome来看看能不能发现什么。打开Network里我发现了这样一个请求

Network

很显然这就是我们想要的那部分内容!让我们仔细分析一下这个请求:这是一个GET请求,包含了四个参数wd,cb,callback和_;请求地址是https://sp1.baidu.com/5b11fzupBgM18t7jm9iCKT-xh_,我不太确定后面随机字符串的含义,但这不重要,我们不必关心。

这里有一个小坑。如果你输入的是一个错误的单词,百度搜索会联想相近的结果;而这个接口需要准确的单词,不能够联想。

让我们重新回头查看一下js的代码,来分析一下各个参数的含义。

  • wd应该是word的缩写,也就是我们想要查询的单词。注意这里的单词不带 翻译 后缀

  • cb的含义结合js代码看是固定前缀 'bd_cb_dict3_' 加上当前时间戳

  • callback是Ajax请求指定的回调名称,和cb参数一致

  • _是当前时间戳

让我们来模拟一下这部分的请求

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字符串,但是并不规范,格式是/**/bd_cb_dict3_1522221579963(json),我们需要自己对response做一个截断然后再解析,结果如下

response json解析结果
  • err_no是状态码,0是成功

  • err_msg是消息反馈,成功情况下是success

  • liju_result是一个数组,里面有4个对象:两个包含了例句信息的数组,一个数字和一个字符串。字符串应该是例句来源,数字是ID,这些都不重要,我们可以忽略。

重点放在liju_result里的两个数组。第一个数组包含的是英语例句,第二个数组是中文翻译的内容。

词组内容

数组里嵌套的是多个数组。第一个对象是单词或者文字;第二个对象是w_x格式的字符串,其中x代表第几个;第三个字符串的含义不明,没有找到特别明显的规则。

第四个和第五个对象开始我并没有特别在意,开始我直接就去拼接字符串了,但是出现一个问题:中文拼接每个汉字中间不需要空格,而英文的单词之间需要。同时英文拼接还有一个问题在于,单词后面需要加空格,而标点符号后面不需要。

回头来看第四个和第五个对象,当第四个对象为0时表示这是一个后面需要拼接空格的部分,通常这个数组会有第五个对象,也就是一个‘ ’字符串;当第四个对象为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

到这一步基本的内容都已经获取成功。我们需要依次把他们打印输出到终端,单纯的黑色太过单调,我们想要用颜色来标识不同的部分,为此在打印部分我们分别设置了一下颜色

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

查询的结果我们需要写入本地,至于写入的格式其实并不确定,可以自己定义。因为想要后期添加一个后台管理,所以我仿照POST请求里Body的方式,把每一个查询结果的字符串写入文件,以一个随机字符串Boundary来分割。写入的路径依据环境变量ENV_CODERDIC_PATH来决定,如果为空就按照os.getcwd()写入当前的工作路径。

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')

考虑到每次输入命令还需要再粘贴一次单词非常的麻烦,所以我想直接从粘贴板获取单词而不必再自己写入参数了。

if __name__ == '__main__':

    content = pyperclip.paste()

    searchWord(str(content))

终端输入

ln 文件路径 \usr\bin\自定义命令

一个简单的词典就完成了。

我本身是一名iOS开发,python部分如果写的不够好考虑不够周全,又或者使用发现了什么问题。欢迎联系我改正,谢谢 :)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容