Monthly Archives: August 2013

蜜月马尔代夫

  新婚蜜月,目的地早就选好了马尔代夫。各种论坛上一番搜索和比较之后,在携程上订了Banyan Tree Vabbinfaru(瓦宾法鲁悦榕庄)7天5晚的行程.

· 超级漂亮的一个小岛,仿佛是漂浮在蔚蓝清澈的海水中;纯白色的沙滩,脚感很细腻;住的观海别墅也很精致,遮阳伞,小泳池,按摩温泉,户外冲澡,有点小奢侈的享受。
   
  悦榕庄的服务也不错,每个人见到了都会微笑的打声招呼,对于honeymoon还有特别的服务,一个装满各种见过没见过的水果的欢迎果篮;一瓶红酒;还贴心地做了一个心型的花瓣床,给了lp一个惊喜~

· 沙滩螃蟹
  沙滩上有一种白色半透明的小螃蟹,有了保护色有点难发现,不过动的时候还是很容易“暴露”足迹;这种小螃蟹到了晚上还会像萤火虫一样发出光亮,很神奇。

        你能看出螃蟹在哪儿吗?Smile

· 浮潜
  浮潜的装备完全可以不用带,因为可以免费借。刚到岛上我就很兴奋地自己去尝试了,结果喝了好几口海水,咸极了。于是和老婆一起上了节浮潜课,一个昵名为“Jackie Chan”的老师给我们示范,我很快就学会了,老婆有点害怕,跟着老师(或者说被老师推着)去远处的珊瑚礁游了一圈。
  真的有很多鱼!以前真没想过能在身边看到这么多鱼,什么颜色的都有(虽然在海里面颜色没有那么鲜艳),甚至有一大群小鱼从身边穿游而过,这种感觉真是美妙!
  没有穿长袖长裤,就算是涂了防晒霜,还是顶不住那太阳,浮潜一两个小时,顿时就晒黑了。。。不过显得还是蛮健康的哈
  以后再浮潜一定会想要带个水下相机的,这样的美景不拍下来真是浪费啊!

· 飘浮
  当年看到过马尔代夫的一张人漂浮在海面上的照片,当时就觉得我一定也要这么漂一次!岛上有这样的橡胶垫,于是,我们也能这么漂啦~
 

· 星空,银河
  马尔代夫的空气很干净,虽然海拔基本为0,但晚上还是能看到无数的星星,包括牛郎,织女星,以及银河!既然背了三角架过来了,自然不能浪费这样的好机会,正好学学怎么拍星空。
  带了两个镜头,套头18-105mm,50mm 1.8定焦。
  50定焦的镜头虽然光圈大,但能取景的星空范围太小了,拍不出银河的感觉来,不过用ISO1600,曝光10s,效果还是可以的;
  18-105用18mm焦距,F3.5,手动对焦到无穷远,ISO 1250~2500,曝光30s,也能拍出银河的效果来,当然,还是有点糊的。
  没有赤道仪,没有快门线的情况下能拍到这个效果,也算满意了。。。


    俺拍的银河~

· SPA
  瓦宾法鲁悦榕庄的一大特色就是spa,这也是lp一直很期待但又纠结的项目:期待是因为这边的spa据说很好,纠结是因为这边的spa很贵——基本上单人的全身90分钟spa都要$200以上,好多都要300~500刀。然后我们“碰上”了某个特价的时段,5折,双人全身90分钟,一共210刀,lp顿时没了抵抗力^_^
  我以前没做过spa,没法评价,就是觉得很舒服;lp作为“专业人士”,评价是:泡脚时用了鲜橙子;会问要不要精油按摩还是无油按摩;手法,力度都很好;还有新鲜的姜茶和柠檬茶喝。一切都很好很专业,就是贵。。。
 

· 日出日落
  某一天想看日出,google一下得知是6:03日出,于是定好闹钟。到时起床,发现天还是黑的,有点纳闷,走到东边的沙滩,完全没有日出的样子嘛。然后一拍脑袋发现不对,岛上的时间比马累要早一个小时,马累的6:03是岛上的7:03,起早了一个小时,杯具。。。然后睡了一个小时之后再起来看日出,发现了东边都是云,只能看着太阳把云照耀得很灿烂。。。
  另一天坐着船去Vabbinfaru的姐妹岛Ihuru岛(这个岛更小,Ferry2小时一班,开5分钟就到了),正好是傍晚看日落。走到西边的沙滩,夕阳照耀下的云显得特别好看。

  lp情不自禁地对着夕阳跳了起来~正好遇到一个老外mm,主动帮我们拍照,于是就有了这样的照片~

  最后,来几张和lp的合照吧~
   

Share

用Calibre下载网页专题内容(非RSS Feed) 以及Referer的处理

神器Calibre有个很强大的Fetch News功能,可以下载RSS Feed然后生成电子书推送给Kindle。关于如何用recipe下载RSS Feed网上已经有不少相关的资料了。

但是如果想下载某一个专题,而这个专题又没有RSS输出怎么办呢?比如说:寻找太阳系的疆界

topic_outer_planets

答案是,recipe还有更逆天的手动parse index功能,正好用来分析一个单独的网页,提取出我们想要的文章。

这篇博客将会简要介绍这个功能。

另外,由于某些网站的图片会要求有Referer的HTTP header,如果没有下载时会403 Forbidden,所以下载图片里还要特别注意处理Referer。

Background:

Calibre内置了Fetch News功能,本质上是python实现的一个继承自 calibre.web.feeds.news.BasicNewsRecipe 的类,并且override了一些成员/函数的实现,以实现定制的功能。

不过一般来说所有的News都是通过抓取feed来实现的,比如说内置的“人民日报”的实现:

class AdvancedUserRecipe1277129332(BasicNewsRecipe):
  title          = u'人民日报'    # 标题
  oldest_article = 2    # 最近几天之前的文章
  max_articles_per_feed = 100    # 最大的文章数
  ...
  feeds          = [
    (u'时政', u'http://www.people.com.cn/rss/politics.xml'),
    (u'国际', u'http://www.people.com.cn/rss/world.xml'),
    (u'经济', u'http://www.people.com.cn/rss/finance.xml'),
    (u'体育', u'http://www.people.com.cn/rss/sports.xml'),
    ...
  ]    # 定义具体的feed名称和url
  keep_only_tags = [...]    # 抓取到的网页里保留的内容
  remove_tags = [...]    # 抓取到的网页里去除的内容
  remove_tags_after = [...]    # 抓取到的网页里去除这个tag之后的内容
  ...

一般来说抄一下Calibre内置的recipe,定义以上的变量就可以搞定绝大部分RSS Feed了。

那么如何搞定像“寻找太阳系的疆界”这样的没有RSS输出的专题文章呢?

Solution:

查看 Calibre的API Document,注意到parse_index()这个函数,它说:

This method should be implemented in recipes that parse a website instead of feeds to generate a list of articles.<br />
...
It must return a list. Each element of the list must be a 2-element tuple of the form ('feed title', list of articles).<br />
Each list of articles must contain dictionaries of the form:
{
 'title'       : article title,
 'url'         : URL of print version,
 'date'        : The publication date of the article as a string,
 'description' : A summary of the article
 'content'     : The full article (can be an empty string). Obsolete
 do not use, instead save the content to a temporary
 file and pass a file:///path/to/temp/file.html as
 the URL.
}

并且提到:

For an example, see the recipe for downloading The Atlantic.

也就是说,可以参考内置的 The Atlantic的recipe,它是parse一个website而不是一个feed。打开这个recipe:

class TheAtlantic(BasicNewsRecipe):
  title      = 'The Atlantic'
  __author__ = 'Kovid Goyal and Sujata Raman'
  description = 'Current affairs and politics focussed on the US'
  INDEX = 'http://www.theatlantic.com/magazine/toc/0/'
  language = 'en'
  remove_tags_before = dict(name='div', id='articleHead')
  remove_tags_after  = dict(id='copyright')
  remove_tags        = [dict(id=['header', 'printAds', 'pageControls'])]
  ...
def parse_index(self):
  ... # Parse the webpage and return feeds

很清楚,主要实现了parse_index这个函数。

那么我们就可以参考这个实现,来写自己parse changhai.org的代码。

Step1 实现parse_index

直接看code吧,注释很容易明白:

def parse_index(self):
  articles = []  # 文章列表
  feeds = []  # 最终要返回的
  soup = self.index_to_soup(self.INDEX)  # 获取网页完整的内容
  feed_title = self.tag_to_string(soup.find('title'))  # 网页的标题
  self.log.debug("Feed Title: " + feed_title)
  for table in soup.findAll('table', attrs={'class':'inside-article-noborder'}):
    # Main articles
    for post in table.findAll('td'):  # 文章列表都在 inside-article-noborder 的 table 里,每一个td是一项
      a = post.find('a', href=True)  # 取出链接
      title = self.tag_to_string(post)  # 文章标题
      url = a['href']
      if "#" not in url:  # 如果url里包含"#",其实是指向了同一个网页的某个anchor,忽略这样的项
        self.log.debug("Article title and url: ")
        self.log.debug(title + ": " + url)
        url = self.INDEX + url  # 生成完整的url
        articles.append((
          {'title':title,
           'url':url,
           'description':'',
           'date':''}))  # 添加到文章列表里
    for post in soup.findAll('p', attrs={'class':'center'}): # 术语简介 | 参考文献 在 class: center的p里面
      for a in post.findAll('a', href=True):  # 同理取出文章的标题和url
        title = self.tag_to_string(a)
        url = a['href']
        self.log.debug("Article title and url: ")
        self.log.debug(title + ": " + url)
        url = self.INDEX + a['href']
        articles.append((
          {'title':title,
           'url':url,
           'description':'',
           'date':''}))
  if articles:
    feeds.append((feed_title, articles))  # 最后生成feed,这里只有一个feed,包含多篇文章
  return feeds

这样每篇文章都能被download下来了,下面要处理每篇文章的内容。

Step 2 处理正文内容

分析正文的网页内容,会发现:

正文内容都在 main-body 的table里,所以

keep_only_tags = [
  dict(name='table', attrs={'id':['main-body']}),
]

“返回目录 | 下一篇”这样的link可以忽略,都在 class left 和right的p里面

remove_tags = [
  dict(name='p', attrs={'class':['left', 'right']}),
]

“站长近期发表的作品”及以后的内容应该忽略,它位于article-data-claim的后面,所以

remove_tags_after = [
  dict(name='p', attrs = {'class' : ['article-date-claim']}),
]

好了,到这一步,文章的内容也能完美提取出来了。

但是,正文里的图片还是没有显示,原因在calibre的log里:

Could not fetch image  http://www.changhai.org/articles/science/astronomy/outer_planets/images/kepler_platonic.png
reply: 'HTTP/1.1 403 Forbidden\r\n'

webserver直接返回403错误了,这一般是因为图片下载时没有Referer的HTTP header,算是一算防盗链的功能吧。

所以我们还需要处理Referer。

Step3 在HTTP Request里添加Referer

通过Google,发现可以通过自定义browser.open_novisit这个函数来实现添加HTTP header

def get_browser(self):
  br = BasicNewsRecipe.get_browser(self)
  orig_open_novisit = br.open_novisit
  def my_open_no_visit(url, **kwargs):
    req = mechanize.Request( url, headers = { 'Referer': self.INDEX, }) # 添加Referer的header
    return orig_open_novisit(req)
  br.open_novisit = my_open_no_visit # 把open_novisit这个函数重新定义成自己的函数
  return br

理论上这样就OK了,然后实际运行的时候会发现,下载文章和图片的时候,根本就没有调用到my_open_no_visit(),图片仍然下载失败。

这下就只能看Calibre的源代码了——通过代码和打log,注意到_fetch_article()里面会调用clone_browser(),而这个clone完的browser的open_no_visit函数还是原生的,并不是自定义的函数。假如这个不是bug,就说明俺的recipe是有点exotic了:

Clone the browser br. Cloned browsers are used for multi-threaded downloads, since mechanize is not thread safe. The default cloning routines should capture most browser customization, but if you do something exotic in your recipe, you should override this method in your recipe and clone manually.

好吧,那就自己实现一个clone_browser()吧,同时也更新自己的get_browser():

_cachedBrowser = None  # 自己的browser
def get_browser(self):
  if self._cachedBrowser is None:
    br = BasicNewsRecipe.get_browser(self)
    orig_open_novisit = br.open_novisit
    def my_open_no_visit(url, **kwargs):
      req = mechanize.Request( url, headers = { 'Referer': self.INDEX, })
      return orig_open_novisit(req)
    br.open_novisit = my_open_no_visit
    self._cachedBrowser = br  # 保存browser
  return self._cachedBrowser
def clone_browser(self, br):
  if self._cachedBrowser is None:
    raise Exception("No browser")
  br = BasicNewsRecipe.clone_browser(self, br)
  br.open_novisit = self._cachedBrowser.open_novisit  # 设置clone出来的browser的open_novisit函数
  return br

OK, 这下Referer的问题也解决了,生成的文章里也完美包含了图片。

kindle_outer_planets kindle_outer_planets_2

Finally:

完整的code放在github(见calibre_recipes)上了,当然code还有待完善,比如说可以合并common的code,以后再慢慢改了。

目前实现了3个专题的recipe:

Q.E.D.

p.s. 顺便推荐一下卢昌海老师的书吧,上面的这些文章都已经出版了,并且有的也有Kindle的版本,推荐购买 🙂
p.p.s 在微博上发现@敲代码的张洋已经有了一些抓取网页的Recipe,包括著名的MIT的SICP,以及O’Reilly的开放书籍,很不错。代码也在Github

Share