之前为了学习英语,而想过自己编写一个抓取不会的单词,然后再复习的插件,但是现在已经放弃了。

程序也编了不少了,是用 C++写的,为什么要放弃呢?因为已经没有必要做了,我已经选择了 anki 与 python 相结合的方法。

但是无论是用 C++写抓取单词并且复习的程序,还是用 python 写爬虫爬取 collins 字典上面的英英解释,都有一些踩坑的经验与踩坑的教训,不吐不快啊,便写一篇文章整理一番:

单词抓取程序

一开始,我想写这个程序的动机,完全与考研没有关系,而是基于如下的想法:

  • Emacs 上面有翻译插件,但是没有记录生词的插件,我应该可以做一个出来,顺便练习一下 C++,而且也能用来学习英语。

  • 而如果仅仅是一个单词,也没有那么容易背下来,市面上面的单词书也大多数都是不仅仅有单词,也有对应的例句。

    我可以写一个单词抓取的程序,抓取单词与当前单词所在的句子,然后将它们存储起来,实现查询,复习,等功能。

    这种方法难道不是比买一本单词书,然后照着书上的例句记单词更加有主动性,也更加让人印象深刻吗?

想法是美好的,但是没有考虑到实际,也太过于热衷于规划教堂,而不是去从小木屋做起积累经验,最后规划了非常多的功能,但是一直写,写到现在也还没有写完,并且最终迎来的也不是写完,而是放弃,不得不说,悲惨。

但是我也并非不是完全浪费了时间,我也从这些事情里面得到了一些益处:

  1. 得到了教训?哈哈,确实,但是教训到最后讲。
  2. 我得到了一些写 C++的经验,而且也学会了很多的 C++技巧,很多的学校考研的复试是考 C++上机的,而且很可能数据结构与算法我也需要使用 C++来学习,所以这种练习也是有益的,不是在浪费时间。
  3. 我以此为动力,看完了 C++ primer 这本书,也学习了一些库的使用,还因为这个项目,学会了使用油管看编程的视频,这些习惯或者说这些经验,也都是财富。

我从这件事情里面得到的教训是:

  1. 需求要明确:

    作为一名软件的开发人员,需求在刚刚开始的时候要尽量明确。

    至少要做到将一些自己绝对不可能会扩展的方向规定死,而不是模棱两可,觉得一个船的程序需要考虑到以后飞起来怎么办而留出扩展接口。

  2. 杜绝完美主义:

    We are not a programmer, we are problem solver!

    一个程序,只要能解决了当前的问题,就是好程序。没有必要去考虑那么多的条条框框,把问题解决了,再谈优化。

  3. 没有必要追求一次就写好

    正如好书、好电影,人们想一遍又一遍以求看得更加深入看一样,一段好程序,很难一遍就能够做到最好。

    如果真正想要写好一段程序,那么一开始不妨带着“打草稿”的心态去写第一遍,然后等到能够 run 起来了,并且在几次优化之后,再重新开始写这段程序,不要有心理负担。

  4. stackoverflow 永远的神!

Python 爬虫

最近,从知乎上面看到有人使用英英注释的文档,英语然后考了 70 多分近 80,我想,我亦可以效仿她的这种方法来学习。

遂上某宝搜索英英注释考研词汇,然而发现这个的原理就仅仅是根据考研词汇一个个抓取 collins 字典的意思,然后转成 word,然后再打印出来,我觉得我用 python 也能做到这样,便说干就干了。

然后有一些教训与经验吧:

技术方面

动态一时爽

python 动态语言,各种东西都可以动态地改变,然后这带来了一些蛋疼的特性,这些是写 C++的时候体会不到的。

因为 C++在编译的时候就已经排掉了很多的错误,在编写的时候就已经能够预见运行时的状态了。但是现在 python 的很多错误都将在运行时才爆发出来:

比如如下的这些操作

a.find("div", class_="def").get_text() # a : class Tag
tags.a["href"]

如果是 C++, find 返回的类型是一定的,即使没有找到,那也会返回一个表示空的 Tag 类型,而空的 Tag 类型调用 get_text() ,一般也是会返回空的字符串的。但是在 python 里面, find() 返回的类型是 NullType ,而非 Tag, get_text() 将报错。

所以,在使用类似上面这些危险操作(可能会导致报错的操作)的时候,头脑要警觉,不要稀里糊涂就使用了危险的操作!而且对于这些危险操作,我们也有一些常见的处理套路:

  • 使用命令的安全版本:

    比如说上面的第二个例子就可以如下转换: tags.a["href"] -> tags.a.get("href")

    但是,请注意,在使用的时候也需要保证安全,比如要考虑到 get()返回 NullType 的情况,如果 get()返回 NullType 而会产生错误的操作不要做,除非已经通过上下环境保证了 get()不会返回 NullType。

  • 用环境来保证操作的安全

    比如下面这段代码:

          # query the subitem
          expanded_defs = []
          for i in definitions:
              if not i.find("div", class_="def"):
                  if i.find("a", class_="xr"):
                      sub_r = requests.get(
                          i.find("a", class_="xr")['href'], headers=headers)
                      sub_soup = BeautifulSoup(sub_r.content, 'lxml')
    
                      # At least give me a definition! If not, remove this item!
                      sub_definition = sub_soup.body.find("div", class_="def")
                      if not sub_definition:
                          continue
    
                      my_sub_grp = sub_soup.new_tag(
                          "span", attrs={"class": "gramGrp"})
                      my_sub_grp.string = "Phrase: " + \
                          i.find("a", class_="xr").string
    
                      # replace gramGrp for better revise.
                      sub_definition.parent.insert(0, my_sub_grp)
    
                      # replace old item "i"(div.hom) with sub definition's hom
                      expanded_defs.append(sub_definition.parent)
    
                  else:
                      continue
              elif not i.find("span", class_="gramGrp"):
                  new_grp = soup.new_tag("span", attrs={"class": "gramGrp"})
                  new_grp.string = "None"
                  i.insert(0, new_grp)
                  expanded_defs.append(i)
              else:
                  expanded_defs.append(i)
    

    可以看到,我用几个 if-else 语句,保证了 append 进入 expanded_defs 的元素,都具有"div.defs" 与 “span.gramGrp” 这两个标签,从而保证了后面的程序如下调用的安全性:

      def parse_meaning(definition):
          """Parsing the definition of a word.
    
      replace all "<a href='#' >TEXT<a>" with TEXT.
      store all definitions, all examples, all synonyms of a word.
      """
          context = {}
          # some sub_definitions also have no gramGrp, such as out-of-print
          context['group'] = definition.find('span', class_="gramGrp").get_text()
          context['def'] = definition.find('div', class_="def").get_text().replace(
              " \n", " ").replace("\n", " ")  # 如果不这么写,有的单词会连在一起
    
          context['examples'] = []
          try:
              for i in definition.find_all('div', class_="type-example"):
                  context["examples"].append(
                      i.span.get_text().replace(" \n", " ").replace("\n", " "))
          except:
              pass
    
          if definition.find("div", class_="thes"):
              context['synonyms'] = definition.find(
                  'div', class_="thes").get_text().replace(" \n", " ").replace("\n", " ")
              context['synonyms'] = re.sub(re_syn, "", re.sub(
                  re_more, "", context['synonyms'])).strip()
    
          return context
    

    用了一个 try-except,但是我不记得为什么了,这就可见注释是多么重要了。言归正传,上面我对 find 的结果直接使用了 get_text() 方法,因为我之前已经保证过这样是没有问题的了。

其实这些事情,都不过是在模仿编译器的工作而已,这些在静态语言中不算折磨的事情,在动态语言里面就有一些折磨了。不过,我不是说动态不好,静态语言虽然检查还算给力,但是写起来却折磨得人欲仙欲死。

程序的思路总结

考研词汇我是从这里得到的,然后它的使用方法里面就告诉了我们如何提取单词列表了。

使用 requests, beautifulsoup 来访问网页,因为 collins 查单词的 url 很简单,比如查"challenge"这个单词,它的 url 就是"https://www.collinsdictionary.com/dictionary/english/challenge" 自己感受一下。

要注意的是,要定义 user-agent 来访问服务器,不然就会 403 Forbidden。requests 设置 user-agent 的方法可以自行百度。

然后就是解析 html 得到信息了,所幸还是比较简单的,思路都在代码里面。

最后就是写成 anki 的包还有写入 json 文件,写入 anki,我使用的是 genanki, 写入 json 就用 python 自带的 json 库就好了。

非技术方面

错误稍后再处理,保留数据的备份

这个也没有什么好说的了,爬虫爬下来,先将东西保存成 json,不然出了错又得重新再爬一遍!

错误之后再处理,先将主要的部分罩上一个 try-except,然后再在 except 里面将发生错误的词写到一个文件里面,然后之后再针对这个文件里面的单词单独处理就好了。

明白自己的需求,程序可以写多遍

跟 c++写单词抓取程序一个道理,我在写爬虫的时候,也没有好好分析自己的需求,collins 的单词页面上面的内容,哪些是我需要的,哪些可以舍弃?

上面说的这些,哪些想要,哪些不要,这些问题所幸我一开始就明白了,但是,还有的更加具体的需求问题,我没有去深入了,这让我写出来的东西总需要修修改改。这里就不举例说明了,不太好说明。

一开始,我不是非常熟悉 beautifulsoup,然后走了一点弯路,但是前面因为不熟悉写的丑代码也没有改了,因为我不是非常想在写爬虫这件事情上面去追求完美,但是如果让我再写一遍的话,我是可以写得更加简洁的。

不过,我有理由怀疑,如果我真的再写一遍的话,可能我就会上多线程这个花活了,并且还会遇到什么锁之类的东西。

也许算是个总结吧

到这里已经写了两个小时了,时间也已经来到了 4 点,该歇了。。。明天可能得 12 点起了。。。

我的程序从 2 点开始跑,跑到现在也没有完成,看来以后写爬虫还是需要一点异步或者多线程的魔法啊。。。

现在那个单词抓取程序已经被我放弃了,那么之后的编程时间我用来干啥呢?不如用来看算法导论。。。

考研真的累人啊。