蜜月马尔代夫

  新婚蜜月,目的地早就选好了马尔代夫。各种论坛上一番搜索和比较之后,在携程上订了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

Kindle Paperwhite 添加中文字体

入手了国行的Kindle Paperwhite,相比Kindle 3有了更好的中文字体支持,原生的“宋体“效果已经相当不错了。
不过以前按照《Kindle 中文阅读终极优化指南》看习惯了“方正准雅宋”,KPW自带的宋体显得有点淡,于是我又开始折腾字体了。。。

The Precondition
首先,KPW添加字体是很简单的一件事情,也不需要越狱:

  1. 用USB线连接KPW到电脑;
  2. 在Kindle的根目录上创建一个空文件 USE_ALT_FONTS (注意不要有扩展名);
  3. 在Kindle的根目录上创建目录 fonts;
  4. 把想要的字体拷到那个fonts目录,如果字体是一个系列的,粗体/斜体按如下命名:
    • Fontname-Regular.ttf     # 普通
    • Fontname-Italic.ttf          # 斜体
    • Fontname-Bold.ttf          # 粗体
    • Fontname-BoldItalic.ttf   # 又粗又斜
  5. 拔掉USB线,重启KPW (菜单->设置->菜单->重启);
  6. 在字体菜单里选择上面添加的字体.

The Problem
但是如果用《优化指南》里提到的这四个字体:

会发现,KPW会把它们认成4个不同的字体,选择“方正准雅宋”之后,也没办法自动使用对应粗体。其实这也很正常,上面的四个字体根本就不是一个Font Family的,KPW自然不认了。

The Solution
解决办法是,用字体编辑器把它们变成同一个Font Family!
在Ubuntu下我试过FontForge, TTFEdit, Ttx这些工具。
FontForge功能太强而且似乎生成的font文件会有问题;
TTFEdit对utf8或者gbk编码的字符串支持有问题;
Ttx其实是一个把TTF转换成xml文件,然后把xml文件转回成ttf的工具。

最后我用ttx搞定了。
1.

ttx -o CJK_Regular.ttx CJK_Regular.ttf  # 把ttf转换成ttx文件(本质就是个xml)

2.

编辑CJK_Regular.ttx
在<name>里,nameID=1是FontFamily,把它改成FZYaSong-GBK
在<OS_2>里,usWeightClass是指字体的Weight,把它改成500 (Medium),顺便把bWeight改成“6”(也许这个改动没有必要)

2.x.

同样的,把另外三个ttx文件里的FontFamily都改成FZYaSong-GBK
CJK_Bold.ttx,CJK_BoldItalic的usWeightClass改成700,bWeight改成8;
CJK_Italic的usWeightClass改成400,bWeight改成6;

3.

ttx -o FZYaSongMod-Regular.ttf CJK_Regular.ttx # 重新生成ttf字体
 另外三个文件同理

这样就生成了FZYaSongMod的四个ttf文件,FontFamily都是FZYaSong-GBK, Weight都是对应的值。然后把这四个文件拷到KPW的fonts文件夹里,重启,搞定!
KPW的显示效果终于和Kindle3一样了,内牛满面啊…

The Last
这四个文件已经打包好,有需求的可以直接用。下载在此: FZYaSongMod.tar.bz2
声明: 更改字体信息仅限学习用,不得作为商业用途。

p.s. 感谢@wzyboy同学《优化指南》 本文修改的字体来源于他的博客。
p.p.s. 推荐越狱之后安装kindlepdfviewer,完美集成于KPW,看PDF效果更好~

Share

婚了

今年5.19 & 5.20,都是好日子。

前者是黄道吉日,办婚礼的人很多,我也在这一天凑热闹;后一天是520,也是南京各大高校的校庆日,我在这一天领了红本本。

如果没有那么多繁文缛节人情世故,结婚是一件很有意思的事情——可以和爱人一起,跑到一个从没去过的地方,旅游结婚。

不过这只是幻想。

实际上是,结婚的前两天先回家,然后忙各种事情:包喜糖盒,买花,理发。。。

然后,然后,就到结婚日了。

妈妈请的婚庆公司不错,布置得很漂亮;司仪是个帅哥,声音不错,不过只是在仪式前跟我们沟通了一下,所以婚礼上基本上是自由发挥了,好在效果还行。

不过婚宴实在太忙了,“表演”完,LP换好造型,就开始敬酒,敬完酒,基本上就已经结束了。

感谢各位同学,跑到我们镇上这么个偏僻的地方来参加我的婚礼,非常感谢哈~

婚后第一天,520,去惠山区民政局领了红本,人果然不少,看来大家确实喜欢这样的日子~

结个婚,很累,不过更多的事情都是老妈在操心,我已经算得上轻松的了,只好在心里对老妈说声感谢了。

总之,感谢大家。俺婚了!

Share

迁移博客到Raspberry Pi上

之前博客host在amazon的服务器上,每个月15~16刀的支出着实不小。

现在有了Raspberry Pi了,基本上24小时开机,可以当一个小server用,所以想着把博客迁移到Pi上,反正访问量不大,肯定能承受得住。

Apache有点大,想想还是用nginx来跑wordpress吧。google一下,找到这篇文章:http://www.cnx-software.com/2012/08/03/wordpress-for-raspberry-pi-using-nginx-and-mysql/
基本上把在Pi上安装http server和部署wordpress的要点写清楚了。

不过我要的不是一个新的wordpress,而是迁移原来的博客,所以也在这里记录一下。因为是事后记录,可能有遗漏或者错误,仅供参考。

1. 安装nginx, php,mysql等

sudo apt-get update
sudo apt-get install nginx php5-fpm php5-cli php5-curl php5-gd php5-mcrypt php5-mysql php5-cgi mysql-server

2. 参考上面的link,把nginx和php的配置文件配好,因为我还要用https(防GFW),所以相比那篇文章里的配置,多了ssl的配置

# Upstream to abstract backend connection(s) for php
upstream php {
server unix:/var/run/php5-fpm.sock;
}
server {
## Your only path reference.
root /srv/www/wordpress;
listen          80;
listen          443 default_server ssl;
## Your website name goes here. Change to domain.ltd in VPS
server_name     mine260309.me;

ssl_certificate     /home/pi/cacert/mine260309.me.StartCom.cert;
ssl_certificate_key /home/pi/cacert/mine260309.me.key.pem;
ssl_protocols       SSLv3 TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers         HIGH:!aNULL:!MD5;
access_log      /srv/www/wordpress/logs/access.log;
error_log       /srv/www/wordpress/logs/error.log;

## This should be in your http block and if it is, it's not needed here.
index index.php;

location = /favicon.ico {
log_not_found off;
access_log off;
}
location = /robots.txt {
allow all;
log_not_found off;
access_log off;
}
location / {
# This is cool because no php is touched for static content
try_files $uri $uri/ /index.php;
}
location ~ \.php$ {
#NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini
include fastcgi_params;
fastcgi_intercept_errors on;
fastcgi_pass php;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
expires max;
log_not_found off;
}
}

记得创建对应的文件夹,否则nginx会访问失败:

sudo mkdir -p /srv/www/wordpress/
sudo mkdir -p /srv/www/wordpress/logs

3. 把备份的文件拷过来,恢复wordpress文件夹和数据库。

3.1 我的备份是这样的一个脚本,crontab每周跑一次,备份过的文件会自动被dropbox同步。(注意用户名和密码是自己设的mysql的用户名、密码)

#!/bin/sh
# Back my web with clear password!
mysqldump --add-drop-table -h localhost -u  --password= minewpdb | bzip2 -c > ~/Dropbox/MyWebs/blog.bak.sql.bz2
tar -czf ~/Dropbox/MyWebs/blog.bak.html.tar.gz /var/www/html

3.2 恢复的过程是:
a) 恢复mysql数据库

$ mysql -u root -p
mysql> CREATE DATABASE minewpdb;  #创建database,注意名字和之前用的一样
mysql> GRANT ALL ON minewpdb.* TO @localhost IDENTIFIED BY '';  # 创建一个myssql用户,方便起见,也和以前一样好了
$ mysql -u ec2-user -p minewpdb < /media/ent/blog.bak.sql # 恢复数据库

b) 恢复html文件夹,这个很简单,就是把blog.bak.html.tar.gz解压出来,放到/srv/www/wordpress目录就行了
然后就可以先在本地测试了,确认wordpress没有问题之后,就是域名了。

4. 域名DDNS

这一块还没玩过,只能做尝试了。

先测试了url forwarding,在dyndns.org有个免费域名mine260309.dyndns-at-home.com,这个是可以DDNS的,然后在GoDaddy里设置了forwarding,然而无论是设置成http://的还是https://的,ssl都有问题(我的博客用了WordPress HTTPS),上推去问,@wzyboy同学说可以用he.net的服务,去google了一下,果然是个好东西。参考http://www.bidon.ca/en/random/2011-06-16-using-dynamic-dns-feature-dnshenet

4.1 上dns.he.net,注册后,可以免费设置50个域名。
1) 添加mine260309.me;
2) Edit Zone -> New A -> Enable entry for dynamic dns
3) 点击”refresh”图标 -> Generate Key,记住这个key,以后给ddclient用
4.2 然后在pi上下载ddclient

apt-get install ddclient

#安装的过程中会有preconfig:

remote server: dyn.dns.he.net
username: mine260309.me
password:
domain: mine260309.me

service ddclient status

确认ddclient工作正常。

4.3 在GoDaddy的设置里把Forwarding关掉,然后手动设置Nameserver为ns1.he.net, ns2.he.net, …

最后测试,一切正常!

不过因为在墙内了,发完博客自动发推的功能木有了,当然这个可以用别的服务搞定就是了,以后再说~

Share