微信你的Kindle:记录我的第一个Web项目
二:为什么要做这个
对个人知识获取来说,这是一个最好的时代,通过一台手机你就可以获取到人类几乎所有的知识,但信息爆炸的同时,也造成了信息的贬值。在手机上我只愿意做浏览性的阅读,一条八卦新闻和一篇有深度的文章都只能获取我相同的关注力,超过千字的文章,拇指就会开始有些不耐烦的加快滑动,更遑论停下来思考一下。
kindle是一个伟大阅读工具,e-paper提供了最接近纸张的阅读体验。并且由于功能单一,更能让人专注于阅读。
对我来说一个理想的阅读方式是:
手机(或其他pad)做为一个信息源,快速浏览发现。需要进一步阅读的内容推送到kindle查看。
三:用到的技术
- 主体架构: Scala & Akka
- 数据库: ElasticSearch
- 爬虫: Jsoup & webdriver + phantomjs
- 邮件服务: AWS Mail & MailGun
- 日志和监控:Logstash+Kibana+ElasticSearch
- 微信机器人:webdriver + phantomjs + web微信
- web前端: github page + 七牛云CDN
1 关于Scala和Akka
neveread.com有三个模块组成,微信机器人
爬虫
邮件服务
,他们通过akka cluster协作,由AKKA提供位置无关性,可以运行在一台服务器上,也可以创建多个实例分布在多台服务器上。
在选择scala+akka之前考察过Erlang,非常喜欢这门语言,特别是它的Pattern Matching,List Comprehensions 让我大开眼界,以及OTP中的Actor概念。但最终放弃是因为几个缺点(我认为的):
- 小众语言,生态系统弱。
- IDE支持不好。
- ErlangVM的性能弱。
- 语法简单,但过于简单。
- 非类型安全。
Scala/Akka在复制了Erlang的OTP框架之外,正好弥补了这些缺点。
- JVM的生态系统
- Intellj官方插件
- JVM的性能
- 语法足够复杂,同时支持OOP和函数式。
- 类型安全
作为一个从C语言转过来的人,编程思维方式的转变是个有趣的过程。之前对并发的理解多在用多线程同时解决某个问题,然后在各个线程中疲于同步各种变量的值(加锁,解锁),akka推崇的是状态分离(actor之间只能通过message交换信息),甚至消除可变状态(鼓励用val定义不可变变量,不用var定义可变变量),这些道理一开始的时候都懂,但写出来的代码,回头一看,其实就是裹着actor外衣的线程。
总结出一个结论:akka中一个actor的成本是极低的(内存占用300个字节),远低于一个操作系统线程(几M栈空间),所以,actor和线程的使用模式有一个明显的不同,如果你的系统中始终只有少数几个Actor在包揽所有的工作,那就需要检查一下你对Actor的用法了。
2 关于数据库
ElasticSearch不是严格意义上的数据库,至少拿来做主数据库属于非典型应用。选中它主要是由于:
- 在JVM生态系统内。只需添加一行sbt依赖,就能用代码直接起一个ES数据库实例,完全不需要外部依赖,非常方便。
- 完善的REST接口。能够接收任意POST过来的Json文档,自动生成对应的scheme,并存储文档。
- 文档友好,并且自动支持搜索。
- 分布式。
ElasticSearch默认是作为一个独立进程运行在专门供它使用的服务器上,对内存需求很大,在我1g内存的还跑着其他进程的小服务器上,经常会内存不够,引发GC,整个JVM世界都不好了。
针对我的特殊需求单独做了调整:
- 数据量小(至少目前),调整ES_HEAP_SIZE到一个较小的值。
- 部分数据(web微信 的session)只对本机的进程有用,无需同步到集群。设置session index的shards=1 replicas=0,以减小存储消耗。
- 新session开启后,老的session数据就没用了,可以直接close,释放内存。
虽然把embedded的ElasticSearch实例用在生产环境有点让人不太人放心,但embedded ES还有一个额外的好处:
所有的配置都可动态编程配置。比如检测内网IP,自动将es绑定到内网,防止疏忽导致信息泄漏到外网。通过Akka cluster集群的event消息,动态配置ElasticSearch集群。
3 关于爬虫
爬取网页类型
需要采集的网页有两种情形:
- 直接返回静态的HTML页面。
- 只返回一个HTML页面框架,内容由javascript动态获取后添加。
第一种情形,也是绝大部分网页的情况,只需设置合理的User-Agent和Referer即可直接用Jsoup采集。
第二种情况,如网易客户端,evernotes等,复杂一点,有两种处理方法:
a)用webdriver驱动浏览器执行javascript获取内容,这种方法通用性好,但比较耗资源。
b)分析javascript加载内容的模式,用代码模拟抓取内容。
文档的生成
文档有两种方式生成:
- 通过识别网页内容(包括文章主体,用户评论),用jsoup提取出来,插入到一个模板文档中,这种方式生成的文档排版更干净,并且由于不用爬取不必要的图片和内容,生成的体积更小,爬取速度也更快。
- 对于内容识别失败的网页,先用jsoup clean一遍(去掉javascript代码,统一UTF8编码等)后,保留原有样式投递。
这两种情况,图片都会被重新编码成base64格式内嵌到网页中,由于base64编码效率比较低,编码后的数据普遍比原图大几倍,目前的规则是超过150K的图片,不重新编码,而是提供一个链接供用户在阅读时点击。
4 关于微信机器人
通过webdriver + phantmjs 上运行web微信实现。
功能:
- 接收好友消息,检测内容,并回复。
- 提取用户分享的网址
- 接收好友验证消息,根据验证码决定是否通过验证.
碰到的一个坑是正好遇上web微信改版,本地测试无论用chrome驱动还是phantomjs驱动都没有问题,deploy到服务器则有时OK,有时失败,没有规律。
现在的代码会检测web微信版本,同时支持目前的两个版本。
稳定性:
微信机器人是最早实现的模块,断断续续跑了几个月,偶尔掉线过几次,为此专门创建了一个状态监控的Actor,一旦检测到掉线就会触发Akka的supervisor策略自动重启,并用Twilio发出电话通知。
压力测试模拟过瞬间收到100条交替不同用户的消息,能够一一回复,只是延时会大一点。为了防止消息发送太快,每条消息间设置了0-1秒的延时,消息队列使用PriorityQueue实现,保证重要消息的优先级。
四: 总结
neveread.com是我第一个web项目,作为一个前BSP驱动码农,处处要学,费力自不必提,但那种看到一个程序从无到有的运行起来,只要拿起手机无时无地都能和你互动的成就感,是在一个大公司编写模块代码无法获取到的。
附:
- v2ex讨论链接
- http://neveread.com/
- 帮助文档
- 微信机器人1: 文字鲨(验证码请邮件pm@kindle.pm)