微信你的Kindle:记录我的第一个Web项目

一:实现了什么

在任何能够分享到微信的APP,把你正在阅读的文章分享给一个微信机器人好友(非公众号),就能推送到你的Kindle上。

针对微信订阅号,知乎,简书,网易新闻,v2ex,github,stackoverflow 等多个网站校正排版,提供更好的阅读体验和推送速度。

亚马逊官方也有一个服务号提供类似的服务。

相比之下我们的优势:

  1. 支持微信之外,从其他App分享。
  2. 亚马逊官方服务号对非微信公共平台(http://mp.weixin.qq.com)的文章排版支持不好
  3. 排版优化,生成的文档体积更小(平均每篇小三四百K),推送更快。
  4. 图片支持格式更多。如这篇文章 的图片亚马逊官方服务号不支持,我们支持。

二:为什么要做这个

对个人知识获取来说,这是一个最好的时代,通过一台手机你就可以获取到人类几乎所有的知识,但信息爆炸的同时,也造成了信息的贬值。在手机上我只愿意做浏览性的阅读,一条八卦新闻和一篇有深度的文章都只能获取我相同的关注力,超过千字的文章,拇指就会开始有些不耐烦的加快滑动,更遑论停下来思考一下。

kindle是一个伟大阅读工具,e-paper提供了最接近纸张的阅读体验。并且由于功能单一,更能让人专注于阅读。

对我来说一个理想的阅读方式是:

手机(或其他pad)做为一个信息源,快速浏览发现。需要进一步阅读的内容推送到kindle查看。

三:用到的技术

1 关于Scala和Akka

neveread.com有三个模块组成,微信机器人 爬虫 邮件服务,他们通过akka cluster协作,由AKKA提供位置无关性,可以运行在一台服务器上,也可以创建多个实例分布在多台服务器上。

在选择scala+akka之前考察过Erlang,非常喜欢这门语言,特别是它的Pattern MatchingList Comprehensions 让我大开眼界,以及OTP中的Actor概念。但最终放弃是因为几个缺点(我认为的):

  1. 小众语言,生态系统弱。
  2. IDE支持不好。
  3. ErlangVM的性能弱。
  4. 语法简单,但过于简单。
  5. 非类型安全。

Scala/Akka在复制了Erlang的OTP框架之外,正好弥补了这些缺点。

  1. JVM的生态系统
  2. Intellj官方插件
  3. JVM的性能
  4. 语法足够复杂,同时支持OOP和函数式。
  5. 类型安全

作为一个从C语言转过来的人,编程思维方式的转变是个有趣的过程。之前对并发的理解多在用多线程同时解决某个问题,然后在各个线程中疲于同步各种变量的值(加锁,解锁),akka推崇的是状态分离(actor之间只能通过message交换信息),甚至消除可变状态(鼓励用val定义不可变变量,不用var定义可变变量),这些道理一开始的时候都懂,但写出来的代码,回头一看,其实就是裹着actor外衣的线程。

总结出一个结论:akka中一个actor的成本是极低的(内存占用300个字节),远低于一个操作系统线程(几M栈空间),所以,actor和线程的使用模式有一个明显的不同,如果你的系统中始终只有少数几个Actor在包揽所有的工作,那就需要检查一下你对Actor的用法了。

2 关于数据库

ElasticSearch不是严格意义上的数据库,至少拿来做主数据库属于非典型应用。选中它主要是由于:

  1. 在JVM生态系统内。只需添加一行sbt依赖,就能用代码直接起一个ES数据库实例,完全不需要外部依赖,非常方便。
  2. 完善的REST接口。能够接收任意POST过来的Json文档,自动生成对应的scheme,并存储文档。
  3. 文档友好,并且自动支持搜索。
  4. 分布式。

ElasticSearch默认是作为一个独立进程运行在专门供它使用的服务器上,对内存需求很大,在我1g内存的还跑着其他进程的小服务器上,经常会内存不够,引发GC,整个JVM世界都不好了。

针对我的特殊需求单独做了调整:

  1. 数据量小(至少目前),调整ES_HEAP_SIZE到一个较小的值。
  2. 部分数据(web微信 的session)只对本机的进程有用,无需同步到集群。设置session index的shards=1 replicas=0,以减小存储消耗。
  3. 新session开启后,老的session数据就没用了,可以直接close,释放内存。

虽然把embedded的ElasticSearch实例用在生产环境有点让人不太人放心,但embedded ES还有一个额外的好处:
所有的配置都可动态编程配置。比如检测内网IP,自动将es绑定到内网,防止疏忽导致信息泄漏到外网。通过Akka cluster集群的event消息,动态配置ElasticSearch集群。

3 关于爬虫

爬取网页类型

需要采集的网页有两种情形:

  1. 直接返回静态的HTML页面。
  2. 只返回一个HTML页面框架,内容由javascript动态获取后添加。

第一种情形,也是绝大部分网页的情况,只需设置合理的User-Agent和Referer即可直接用Jsoup采集。

第二种情况,如网易客户端,evernotes等,复杂一点,有两种处理方法:

a)用webdriver驱动浏览器执行javascript获取内容,这种方法通用性好,但比较耗资源。

b)分析javascript加载内容的模式,用代码模拟抓取内容。

文档的生成

文档有两种方式生成:

  1. 通过识别网页内容(包括文章主体,用户评论),用jsoup提取出来,插入到一个模板文档中,这种方式生成的文档排版更干净,并且由于不用爬取不必要的图片和内容,生成的体积更小,爬取速度也更快。
  2. 对于内容识别失败的网页,先用jsoup clean一遍(去掉javascript代码,统一UTF8编码等)后,保留原有样式投递。

这两种情况,图片都会被重新编码成base64格式内嵌到网页中,由于base64编码效率比较低,编码后的数据普遍比原图大几倍,目前的规则是超过150K的图片,不重新编码,而是提供一个链接供用户在阅读时点击。

4 关于微信机器人

通过webdriver + phantmjs 上运行web微信实现。

功能:
  1. 接收好友消息,检测内容,并回复。
  2. 提取用户分享的网址
  3. 接收好友验证消息,根据验证码决定是否通过验证.

碰到的一个坑是正好遇上web微信改版,本地测试无论用chrome驱动还是phantomjs驱动都没有问题,deploy到服务器则有时OK,有时失败,没有规律。

现在的代码会检测web微信版本,同时支持目前的两个版本。

稳定性:

微信机器人是最早实现的模块,断断续续跑了几个月,偶尔掉线过几次,为此专门创建了一个状态监控的Actor,一旦检测到掉线就会触发Akka的supervisor策略自动重启,并用Twilio发出电话通知。

压力测试模拟过瞬间收到100条交替不同用户的消息,能够一一回复,只是延时会大一点。为了防止消息发送太快,每条消息间设置了0-1秒的延时,消息队列使用PriorityQueue实现,保证重要消息的优先级。

四: 总结

neveread.com是我第一个web项目,作为一个前BSP驱动码农,处处要学,费力自不必提,但那种看到一个程序从无到有的运行起来,只要拿起手机无时无地都能和你互动的成就感,是在一个大公司编写模块代码无法获取到的。

附:

  1. v2ex讨论链接
  2. http://neveread.com/
  3. 帮助文档
  4. 微信机器人1: 文字鲨(验证码请邮件pm@kindle.pm)

文字鲨