外叔祖的小园后门外,有一课千年古柏,现在是旅游景点,有标牌有简介。
从小我就听说过很多古柏的神灵故事。说很多年以前,有人想砍下这颗柏树,一斧下去,柏树流出了鲜红的血液,农人头痛欲裂,回家暴毙而亡。柏树的伤口结成了瘤,这个瘤现在还能看到。
后来也有很多人试图砍下这棵树,可是不仅这棵树刀枪不入,而且但凡动过这个念头的人都立刻死了。
柏树成为了乡民的树神,得到大家的供奉和守护,熬过千年,是为古柏。
听起来像是一个守护乡土的日本风格鬼神故事。

然而其实神是没有的。在我小时候,大约是小学时候吧,家门口的孩子过年玩插炮,贪玩,把插炮往树底丢,起了火灾,差点把这棵树烧死。幸而外叔祖拖着年迈的身躯拼命把火灭了。这件事情过后,我就被告知了上面的那些传奇故事,还被警告绝对不要伤害到那棵树,要好好保护他。
所以还是一个守护乡土的故事。

村子里还有一幢 800 年前宋朝时候的房子,旁支亲戚现在还住在里边。房屋很大,很高,夏天很凉快,有漂亮的天井和科学的排水设施。打开室内水道上的盖子,凉风习习,穿孔而出,让人赞叹。门口的石阶已经不大能走路,扭曲折断,还不如普通的小坡易于攀爬,形状就像是麻省理工学院的 Ray and Maria Stata Center。这石阶不是人工的现代艺术,而是大地撕裂的痕迹,真实记录了800年来脚下这片土地的每一次震动,和那棵老树一样,共同记录了文明在自然中的成长和飘摇。

我想,有些人回不到故乡,也许是因为对家乡的故事并不关注。人类文明日新月异,家乡风貌千变万化,故旧的生活自然是再也回不去。家乡不是眼前,家乡是悠久岁月的文明积淀。以色列人可以在两千年后重回耶路撒冷,但如果一个人不知道家乡的故事,那家乡最多与他同岁。这样的故土只能叫做童年,而个人的童年实在没有必要再过一次。故土上的故事,可以解答后人的疑惑,解释后人的现状,满足后人的好奇心,给后人提供认同与信仰。
一个时代的小打小闹,一个人的生老病死,对于故土上繁衍生息的文明而言,只是轻烟。

平时大家在文档排版、印刷排版的时候,不管是应用级的 Adobe inDesign 和 Microsoft Word,还是底层排版引擎 Latex,都会默认在英文字符与汉字之间、阿拉伯数字与汉字之间加一个间隙或是半角空格。不信的话大家可以打开 Word 试一下。而且,如果我们在 Word 的中英文间隔处手动打一个空格,这个间隙不变,我实验过多次。

我今天注意到,腾讯微信的排版引擎没有做这件事情。

中英混排加空格这个排版习惯,当代年轻的传统出版界人士并不了解,倒是 IT 工程师们比较清楚。现在传统媒体都直接用现成的排版软件,这些排版上的事软件都自动做了,导致传统媒体运营者并不知道其中还有这一道关窍。倒是 IT 工程师经常需要自己写原生的朴素文本文档和 HTML 代码,需要不停地手打空格,中英混排加空格这件事情已经深入脑髓。我们的 Windows 操作系统,Mac OS 操作系统,IOS 操作系统,Android 操作系统,但凡涉及中英混排的,都会遵循这个空格习惯。

由于微信排版引擎没有对中英混排做出优化,于是传统媒体发表的微信文章中英混排普遍拥挤。点击可查看大图。

清新时报

财新网

这种拥挤在年号、英文词组中显示得特别明显,英文词组的完整感常因为缺乏两侧空格而割裂。

相反,工程师背景的微信文章多习惯性地加上了空格。点击可查看大图。

亚马逊 AWS 中国

极客公园

我们看到,即便是单个的数字或英文字符,两侧如果与汉字相接,也一定各有一个半角空格。如果英文字符与标点符号相连,则不需要添加空格。

两种效果,大家可以自己对比。

类似于腾讯微信后台这样的排版工具一般也确实没有提供中英智能混排这么专业的功能,在平台没更新的情况下,排版者如果想执行行业习惯,只能自己注意。

这是行业习惯,我搜了一下也找不到文献,只能让大家自己看看自己的手机、电脑、纸质书,看看行业习惯是否如此了。或者有兴趣的同学做个调研。

文章转载自 Google Research,是同名论文的概要说明。原文地址:http://googleresearch.blogspot.com/2014/10/all-news-thats-fit-to-read-study-of.html。译文之后我附了本文原文、初始论文等信息。原文发表于当地时间 2014 年 10 月 9 日。

作者:
Chinmay Kulkarni,斯坦福大学在读博士,前谷歌实习生
Ed H. Chi,Google Research 科学家

翻译:OrangeCLK orangeclk[at]orangeclk.com

新闻是大家日常信息食粮的一道主菜。和互联网上的其他活动一样,在线新闻阅读正在迅速地演变成一种社交体验。今天的互联网使用者可以看到各种各样来源的新闻推荐,报纸网站让读者可以互相分享新闻文章,餐厅点评网站会展示其他食客的推荐,目前一些社交网络也已经集成了社交新闻阅读器[2]。

[1] All the News That’s Fit to Print 是《纽约时报》老板奥克斯于 1896 年 10 月 25 日提出的口号,于 1897 年 2 月 10 日置于头版左上角,意指《纽约时报》刊印一切值得刊印的新闻。后来纽约时报又针对其网站提出了 All the News That’s Fit to Click。本文标题为 All the News That’s Fit to Read,化用了这一句式。参考来源:http://www.nytco.com/who-we-are/culture/our-history/#1910-1881-timeline http://en.wikipedia.org/wiki/The_New_York_Times http://www.businessinsider.com/2007/10/nyt-all-the-new——译者注。
[2] 例如 Facebook 推出了新闻阅读应用 Paper。——译者注。

新闻文章的推荐信息和赞许信息可以来自计算机与算法、发表与聚合内容的商业公司、朋友、甚至完全陌生的人。这些解释信息(即为什么这些文章会呈现给你,也就是我们所说的“标注”)会怎样影响用户的阅读选择?新闻传播中的在线社群标注已经无所不在,但是我们对标注的理解却惊人地少,用户会怎样响应这些标注,怎样才能把标注富有成效地推荐给用户?

《一切适于阅读的新闻:关于新闻阅读社群标注的研究》发表于 2013 年美国计算机学会人机交互专家协会的计算机系统人类因素研讨会[3]上,是2013年谷歌论文影响力榜单中的重点论文。在这篇论文中,我们发表了两项实验的结果。截至目今,大家普遍认为社交标注是一种提升用户参与度的常见简单方法。但实验显示,社交标注一点也不简单,不同的社交标注说服力有很大差别,提升用户参与度的能力也有很大差别。

[3] ACM 是美国计算机学会;SIGCHI 是人机交互专家协会,即 Special Interest Group on Computer-Human Interaction;译者这里将 Conference on Human Factors in Computing Systems 译作“计算机系统人类因素研讨会”——译者注。

新闻文章的不同社交标注

当用户看到的内容未经个性化定制也不是来自于他们的社交网络时,他们会怎样使用社交标注?这是第一项实验关注的问题。一个典型的情境是用户正在浏览他们尚未登录的社交网络。我们把同样的一组新闻文章展示给参与研究的志愿者,给这些新闻标上来自陌生人、计算机程序、虚构品牌公司的社交标注。此外,我们还告诉志愿者他们的名字是否会出现在他们阅读的文章旁边,作为标注呈现给其他实验参与者(也就是“记录”或“不记录”他们的阅读行为)。

令人吃惊的是,在这个“尚未登录”的情境下,不知名商业公司和计算机的标注要显著地比陌生人的社交标注有说服力。这项实验结果揭示了标注在信息推荐方面的潜力,哪怕这些标注是由用户从不知晓的品牌和推荐算法产生的。实验也表明,在用户尚未登录的情境下,计算机和商业公司的标注很有价值。实验还显示,无论标注是哪种类型,开启“记录”功能后,用户对新闻文章的总点击量会变少。这表明被试者知道他们在社交阅读应用中会展现给其他用户的怎样形象。

第一项实验说明陌生人的标注不如计算机和品牌商有说服力,那么好友的标注效果如何呢?第二项实验以谷歌用户为被试者,考查他们在登录后的情境中对好友的标注有怎样的反应,探究个性化的赞许信息能否帮助人们发现、选择可能更有意思的内容。

可能并不令人多么吃惊,实验结果显示好友标注很有说服力,提升了用户对文章选择的满意度。有趣的是,在实验后的访谈中,我们发现,影响志愿者是否阅读文章的标注主要有三类:第一类,标注者和用户的社交亲密度超过了阈值;第二类,标注者有新闻文章相关领域的专业知识;第三类,标注给被推荐的文章提供了附加信息。这说明社会情境和个性标注共同作用,总体上提升了用户体验。

有待研究的一些问题包括:高亮标注中的专业知识是否能提升用户体验;社交亲密度的阈值是否可以通过算法确定;聚合标注(例如,“110名用户点了赞”)是否能提升用户参与度。我们希望下一步的研究能够解释使得社交推荐能够提供合理解释 为什么用户应该关注 解释标注呈现的更多微妙之处。

原文:All the News that’s Fit to Read: A Study of Social Annotations for News Reading

Posted by Chinmay Kulkarni, Stanford University Ph.D candidate and former Google Intern, and Ed H. Chi, Google Research Scientist

News is one of the most important parts of our collective information diet, and like any other activity on the Web, online news reading is fast becoming a social experience. Internet users today see recommendations for news from a variety of sources; newspaper websites allow readers to recommend news articles to each other, restaurant review sites present other diners’ recommendations, and now several social networks have integrated social news readers.

With news article recommendations and endorsements coming from a combination of computers and algorithms, companies that publish and aggregate content, friends and even complete strangers, how do these explanations (i.e. why the articles are shown to you, which we call “annotations”) affect users’ selections of what to read? Given the ubiquity of online social annotations in news dissemination, it is surprising how little is known about how users respond to these annotations, and how to offer them to users productively.

In All the News that’s Fit to Read: A Study of Social Annotations for News Reading, presented at the 2013 ACM SIGCHI Conference on Human Factors in Computing Systems and highlighted in the list of influential Google papers from 2013, we reported on results from two experiments with voluntary participants that suggest that social annotations, which have so far been considered as a generic simple method to increase user engagement, are not simple at all; social annotations vary significantly in their degree of persuasiveness, and their ability to change user engagement.

News articles in different annotation conditions

The first experiment looked at how people use annotations when the content they see is not personalized, and the annotations are not from people in their social network, as is the case when a user is not signed into a particular social network. Participants who signed up for the study were suggested the same set of news articles via annotations from strangers, a computer agent, and a fictional branded company. Additionally, they were told whether or not other participants in the experiment would see their name displayed next to articles they read (i.e. “Recorded” or “Not Recorded”).

Surprisingly, annotations by unknown companies and computers were significantly more persuasive than those by strangers in this “signed-out” context. This result implies the potential power of suggestion offered by annotations, even when they’re conferred by brands or recommendation algorithms previously unknown to the users, and that annotations by computers and companies may be valuable in a signed-out context. Furthermore, the experiment showed that with “recording” on, the overall number of articles clicked decreased compared to participants without “recording,” regardless of the type of annotation, suggesting that subjects were cognizant of how they appear to other users in social reading apps.

If annotations by strangers is not as persuasive as those by computers or brands, as the first experiment showed, what about the effects of friend annotations? The second experiment examined the signed-in experience (with Googlers as subjects) and how they reacted to social annotations from friends, investigating whether personalized endorsements help people discover and select what might be more interesting content.

Perhaps not entirely surprising, results showed that friend annotations are persuasive and improve user satisfaction of news article selections. What’s interesting is that, in post-experiment interviews, we found that annotations influenced whether participants read articles primarily in three cases: first, when the annotator was above a threshold of social closeness; second, when the annotator had subject expertise related to the news article; and third, when the annotation provided additional context to the recommended article. This suggests that social context and personalized annotation work together to improve user experience overall.

Some questions for future research include whether or not highlighting expertise in annotations help, if the threshold for social proximity can be algorithmically determined, and if aggregating annotations (e.g. “110 people liked this”) help increases engagement. We look forward to further research that enable social recommenders to offer appropriate explanations for why users should pay attention, and reveal more nuances based on the presentation of annotations.

论文下载地址

http://pan.baidu.com/s/1o6ofZq6

论文 BibTex 代码:

1
2
3
4
5
6
7
8
@inproceedings{41200,
title = {All the news that’s fit to read: a study of social annotations for news reading},
author = {Chinmay Kulkarni and Ed H. Chi},
year = 2013,
URL = {http://dl.acm.org/citation.cfm?id=2481334},
booktitle = {In Proc. of CHI2013},
pages = {2407-2416}
}

近来读叶嘉莹先生的《人间词话七讲》,发觉第六讲中先生对“无奈朝来寒雨晚来风”一句解析有误。叶先生说中国古诗词中但凡朝暮对举,就是朝朝暮暮;风雨对举,就是风风雨雨[1],其实道理并非如此。比如朝秦暮楚,就不是说朝朝暮暮追随秦楚;朝三暮四,也不是说朝朝暮暮三三四四。

“朝来寒雨晚来风”用到了汉语中的一种修辞——互文。百度百科的解释是:“互文,也叫互辞,是古诗文中常采用的一种修辞方法。古文中对它的解释是:‘参互成文,含而见文。’具体地说,它是这样一种互辞形式:上下两句或一句话中的两个部分,看似各说两件事,实则是互相呼应,互相阐发,互相补充,说的是一件事。由上下文义互相交错,互相渗透,互相补充来表达一个完整句子意思的修辞方法。”我认为基本讲得在理。

譬如,“烟笼寒水月笼沙”是说烟与月笼罩水与沙,“秦时明月汉时关”说的是秦汉时的关山,“将军白发征夫泪”说的是将军和征夫的白发与泪。呃写到这里发现我举的例子竟然和百度百科一样,只能说这几句互文过于经典。

为什么要用互文呢?互文可以用精简的句子表达丰富的含义。如果对于互文,我们只理解了字面意思,那很可能是狭隘而说不通的。比如“东市买骏马,西市买鞍鞯,南市买辔头,北市买长鞭。”难道真的那么巧每个超市买一样?其实就是说木兰君到处逛街终于把东西买到了,用对称铺叠的句子写出来,不仅文字精简,而且可以增加诗歌的美感。再比如《项脊轩志》“东犬西吠”,高中语文书上说“东家的狗(听到西家的声音)就对着西家叫。”这脑补得实在有些厉害,也根本讲不通。归有光其实只是说到处都有狗叫而已。《琵琶行》“主人下马客在船”,说的是主人和客人一起下马上船,如果解释成主人下马,客人在船上,那就说不通了。如果是主人送客而主人并没有上船的话,那何来“移船相近邀相见,添酒回灯重开宴”?如果主人是牵马去江边迎客,那又何来序文中“送客湓浦口”一说?不能读出互文,很多作品就理解不了。

新课标人教版中学语文书中对于古诗文的注疏,是有很多问题的。

[1] “中国的诗词凡是在对举的时候,朝暮的对举,就是朝朝暮暮;风雨的对举,就是雨雨风风。”——叶嘉莹《人间词话七讲》第六讲,139页,北京大学出版社。

我中学用过很多 Linux 发行版,在我中学尝试的诸多版本中,印象最好,使用最长的是国人制作的 Magic Linux。

我今天发现,这个版本居然还在维护,而且在今年7 月发出了 6 年来的第一次版本更新:http://www.magiclinux.org/ 这让我非常怀念,也非常开心。

Magic Linux 是一款国人制作的 Linux 发行版,对中国人非常友好。在那个上古年代,Linux 发行版对中文的支持很差,字体错乱,还经常有乱码。那个时候 Red Hat 还没有启用yum,我还没接触 Debian 系,不知道 apt。软件仓库这件事无从谈起,rpm 包也常不兼容,每次安装软件,都会遭遇巨大的痛苦,不仅要自己编译、make,还要读懂各种报错信息,去 sourceforge 下载.lib .so 依赖包,再把它们安装到合适的位置,简直不能再痛苦。那时的国际化 Linux 版本也很少有为中国人订制的软件,办公软件水土不服,不少工具汉化不完整。音频视频播放器这种桌面常见应用用起来也不方便,因为 Linux 的版权限制,MP3 等编码的解析器是不能捆绑在发行版里的,需要用户自己去安装。而那时候大多数系统上并没有软件仓库,安装难度可想而知。当时 rmvb 和 rm 这种古董级格式还非常流行,由于这个格式是 reaplayer 的私家标准,尤其难办。

Magic Linux 把这些问题都改进了,在 2005-2006 年那个年代,它就提供了一个软件管理程序,虽然没有 apt 那么完备,但是已经很有软件仓库的影子。即便我要安装仓库中没有的程序,它也能以更友好的方式向我提示依赖。Magic Linux 提供了 C++写成的 Eva,可以在 Linux 系统下使用 QQ,是我在 Linux 下使用时间最长的第三方QQ客户端。QQ 为了封禁这些第三方客户端,会经常修改通信协议,Eva 坚挺了很久,使用体验也非常好,性能卓越,背后一定有很多贡献者付出了大量的心血。最终 Eva 还是没能坚持开发下来,彻底登不了 QQ 了。音频视频播放器的解码器也有很好的傻瓜式解决方案。令人惊喜的是,它还自带了一个游戏大厅,像腾讯游戏大厅那样可以打牌玩,可惜我登录过几次,里面根本就没有人,码农真是苦逼啊。顺便吐槽一句,QQ 现在越来越慢了,2008 年的那个 QQ 呢?!

国人开发的 Linux 版本也有不少,包括中科院专资开发的红旗 Linux。我用过红旗 4.0 和红旗 5.0,比 Magic Linux 差多了,桌面应用和开发程序都很不方便,倒是外形是我见过的 Linux 中长的最像 Windows 的,连“开始”菜单都 cosplay 了一个,简直哭瞎。现在这个项目已经倒闭,项目员工集体举牌讨薪,我说当年大家如果用点功,也不至于是这个结局…… Magic Linux 要比其他官方赞助的版本优秀多了。

2008 年以后,似乎团队不再维护 Magic Linux,系统从内核到各种库都得不到更新,我也从此改用 Ubuntu8.04 和 Ubuntu8.10,开启了 apt 和“新立得”的幸福生活。

如今“新立得”也早已不见踪迹,Ubuntu 简直变得和 Windows 一样好用。时代真是在进步,当码农越来越容易了。

附 Eva 开源项目 github 地址:https://github.com/MagicGroup/eva

1947 年的铜陵汀洲,是个老师很难生存的地方。

当地家长都很有文化,总嫌弃老师水平差。老师一旦教错,家长就要起哄。

邓老师要靠教书的薪水养足一双儿女,还要天天受家长的学术质疑,内外交困,请了辞。他后来的人生看起来很悲惨,丢了教职以后,一路乘船北上,赶赴妻子的老家天津讨生活。路上又丢了一个孩子。他很怀念故乡,多年以后,回到了铜陵。为了回家,他在路中变卖了所有的行李,回家时已经一无所有,后来怎么样我不知道。

邓老师辞职后留下了一个缺,而外公当时在家赋闲,小学校长听说了,便要请外公去执教。

定教职是需要面试的,校长听课,外公试讲。第一节课讲六年级语文,第二节课讲五年级数学。讲完,午饭,饭中校长就双手捧过了高级教师聘书。在传统的年代,人们的礼数比现在更为繁复,当场录用为高级教师,并且双手捧过聘书,是很大的尊重和肯定。十年前,九十多岁的老校长还给外公送了一幅字,苏轼的《水调歌头》《念奴娇》两首。就一直挂在墙上,外公也不装裱,宣纸已经发黄。我无数次看过这幅字,写得非常漂亮,可惜我手头没有好照片。如今这幅字已经被新的字画盖住,拍起来很麻烦。

高级教师就可以当班主任了,可是这班主任并不好当,因为 1947 年的铜陵汀洲,是个老师很难生存的地方。

有一回外公批国文书法作业,给一位学生的“達”字画了一个圈,以示好评。当时大家还是都用繁体字的,“達”字,是“幸”字下加一横再添走之。可是这位学生却写成了“幸”字加走之,少了一横。家长一见,非常生气,说白字先生乱教书,不仅看不出错字,还给错字画圈。

外公脾气很大,没多久就跟校长说:“我不干了。”校长觉得纳闷,“你不是做得好好的吗?怎么突然就不干了?”外公说:“我给学校丢了人,呆不住了,学生家长不满意。”校长说:“没哪个跟我讲过啊。”外公这才一五一十把事情说了。原来,柳宗元写书法的时候,就给“達”少写了一横,所以书法上少一横不能算错,顶多是个异体字。现代人写简化字写久了不熟悉,在当时这都是广为接受的概念。

于是校长不答应了,他把这件事告诉了乡议员,说这个家长无理取闹。乡议员说这成何体统,要求家长必须检讨,要办酒道歉。那个时候,乡中给老师道歉,要办酒席,放爆竹,让全乡知晓,当众致歉,以示尊师。家长一听乡议员说是他自己搞错了,立马表示道歉,筹备酒席,毫不含糊。那一天外公没去酒席,因为他自认为脾气不好,如果去了一定会教育那位家长,溯清“達”字源流,这样两边都很尴尬。所以当天由校长代行受礼,放爆竹,设酒席,宴同乡,学校名声得以昌隆,乡民对外公也就服气了。

1947 年的铜陵汀洲,是个很热爱知识文化而又不叶公好龙的地方。

V 社大良心,制作了 Steam 这个跨平台游戏中心,提供的 source 引擎也是跨平台的。所以,我们只需要在 Ubuntu 软件仓库中安装 Steam ,就可以玩 Steam 中的游戏啦。

在软件仓库里,我们先要点击“购买”按钮购买 Steam,然后切入 Ubuntu One 的认证页面进行认证,认证完毕,就可以以 0 元的价格买入 Steam 了,此时下载安装,和普通软件包没有区别。

启动 Steam 后,找到 Dota, 下载安装。启动 Dota 后,发现只能连接到世界服,有东南亚区、欧洲区、美国区等等。至于中国区,你懂的。为了取得更快的连接速度和更好的游戏体验,我们需要连接到完美世界代理的国服。在 Steam 中右击 Dota2,点击“属性”,“设置启动选项……”,在弹出的对话框中输入“ -perfectworld steam ”。再启动 Dota2 时,连接的就是完美世界了。我体验了一把,感觉和 Windows7 下运行没什么区别,一样一样的。赞!

由于 expressjs 自带的 body-parser 默认不能解析 ‘text/xml’ 的内容,我曾考虑依照 body-parser 本身的结构自己写一个工业级的 ‘text/xml’ 解析器。后来才发现,其实可以通过调用 body-parser 的 text() 函数作为中间件,对 ‘text/xml’ 解析,只要在赋入的选项中添加 type: ‘text/xml’ 属性即可。我自己写的 ‘text/xml’ 解析器算是个副产品吧,有时间测试后也会发表,比直接调用 bodyParser.text() 在功能上要强一些,流程上也统一一些,安全性也高一些。现有的软件库中我还没有看到具有类似功能的模块。

参考代码 1: expressjs 项目的入口,载入 body-parser 模块并调用 text() 中间件,如果不赋入 ‘text/xml’ 选项,text() 将默认以 ‘text/plain’ 类型执行解析,则会发生类型匹配错误,导致解析无法完成,所以 ‘text/xml’ 选项是必须手动传入的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
* Module dependencies.
*/

var express = require('express');
var routes = require('./routes');
var user = require('./routes/user');
var http = require('http');
var path = require('path');

// 载入 body-parser 模块,expressjs自带的部分没有 text() 函数。
var bodyParser = require ('body-parser');

var app = express();

// all environments
app.set('port', process.env.PORT || 3000);
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.use(express.favicon());
app.use(express.logger('dev'));

// 将请求体中的 xml 解析为字符串。
app.use(bodyParser.text({type: 'text/xml'}));

app.use(express.json());
app.use(express.urlencoded());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(path.join(__dirname, 'public')));

// development only
if ('development' == app.get('env')) {
app.use(express.errorHandler());
}

app.get('/', routes.index);
app.get('/users', user.list);

http.createServer(app).listen(app.get('port'), function(){
console.log('Express server listening on port ' + app.get('port'));
});

参考代码 2:通过 xml2js 将解析出的 xml 字符串解析为 json ,并存入解析完成后的请求体中供其他代码调用。

1
2
3
4
5
6
7
8
9
10
11
12
{parseString} = require 'xml2js'
exports.index = (req, res) ->
parseString req.body, (err, result) ->
if err
err.status = 400
res.end 'error'
return
else
req.body = result
console.log req.body
res.end 'success'
return

由于需要在 expressjs 中解析 xml ,而 expressjs 默认无法解析 xml 。所以我打算自己造一个工业级轮子,于是参考了 expressjs 中关于 json 解析方面的代码。代码分析如是。

朴素实现是监听 req 的 data 事件,将读到的数据存储在字符串变量中再进行解析。body-parser 的实现与此有不少区别。首先,代码中有很多校验信息,要根据请求头中的内容对请求体做类型、长度、字符编码等校验,以此提高安全性;其次,代码都是通过调用 raw-body 模块中的 getRawBody(stream, options, callback) 来解析请求体的,而不是直接监听 data 事件进行操作。getRawBody 函数会做请求体校验、异常处理、管道卸载等工作,比朴素的做法安全很多。 getRawBody 函数也可以完成解码功能。

body-parser 的文档和代码见: https://github.com/expressjs/body-parser 。中文注释为我的注析。这里只解析 index.js 及其调用的内容, lib/types/ 下的其他文件结构类似,忽略不析。

index.jslink
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
/*!
* body-parser
* Copyright(c) 2014 Douglas Christopher Wilson
* MIT Licensed
*/

/**
* Module dependencies.
*/

// deprecate 是一个用来显示“不建议”消息的工具,可以用来警告用户不要使用
// 那些不建议使用的函数或模块。详见:
// https://github.com/dougwilson/nodejs-depd
var deprecate = require('depd')('body-parser')
var fs = require('fs') // 载入文件处理模块,nodejs 核心模块。
// 载入路径处理模块,nodejs 核心模块,参考:
// http://nodejs.org/api/path.html
var path = require('path')

/**
* Module exports.
*/

exports = module.exports = deprecate.function(bodyParser,
'bodyParser: use individual json/urlencoded middlewares')

/**
* Path to the parser modules.
*/

var parsersDir = path.join(__dirname, 'lib', 'types')

/**
* Auto-load bundled parsers with getters.
*/

//遍历 /lib/types 目录下的文件。
fs.readdirSync(parsersDir).forEach(function onfilename(filename) {
if (!/\.js$/.test(filename)) return //如果文件不是 .js 文件,返回。

var loc = path.resolve(parsersDir, filename) // 提取文件的绝对位置。
var mod
var name = path.basename(filename, '.js') // 提取文件的基础名。

function load() {
if (mod) {
return mod
}

// 载入位于loc位置的文件,由于load函数可能在其他地方被调用,所以这里
// loc 记录的是绝对位置,增强代码的可移植性。
return mod = require(loc)
}

// 给 exports 添加一个新的属性 name,代码还设置了 name 属性的三个属性
// 描述符。关于 Object.defineProperty ,详见:
// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
Object.defineProperty(exports, name, {
configurable: true, // 使 name 属性可以被改变。
enumerable: true, // 使 name 属性可枚举。
get: load // 使 name 属性有 getter 方法。
})
})

/**
* Create a middleware to parse json and urlencoded bodies.
*
* @param {object} [options]
* @return {function}
* @deprecated
* @api public
*/

function bodyParser(options){
var opts = {}

options = options || {} // 初始化 options 。

// exclude type option
for (var prop in options) {
if ('type' !== prop) {
opts[prop] = options[prop]
}
}

var _urlencoded = exports.urlencoded(opts)
var _json = exports.json(opts)

return function bodyParser(req, res, next) {
_json(req, res, function(err){ // 将请求体解析为 json 。
if (err) return next(err);
// 如果请求体不是 json ,则将其解析为 urlencoded 内容。
_urlencoded(req, res, next);
});
}
}

lib/types/json.jslink
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
/*!
* body-parser
* Copyright(c) 2014 Jonathan Ong
* Copyright(c) 2014 Douglas Christopher Wilson
* MIT Licensed
*/

/**
* Module dependencies.
*/

// bytes 模块可以在字节度量的数字表示和字符串表示之
// 间互相转换。例如:
// bytes('1kb') 结果为 1024 ;
// bytes('2mb') 结果为 2097152 ;
// bytes('1gb') 结果为 1073741824 ;
// bytes(1073741824) 结果为 1gb ;
// bytes(1099511627776) 结果为 1tb 。
// 详见 github 上的 visionmedia/bytes.js 项目:
// https://github.com/visionmedia/bytes.js
var bytes = require('bytes')
var read = require('../read')
// expressjs 自带的媒体解析模块,详见:
// https://github.com/expressjs/media-typer
var typer = require('media-typer')
// type-is 是 expressjs 自带的类型判断模块,详见 github 上的 expressjs/
// type-is 项目: https://github.com/expressjs/type-is
var typeis = require('type-is')

/**
* Module exports.
*/

module.exports = json

/**
* RegExp to match the first non-space in a string.
*/

var firstcharRegExp = /^\s*(.)/

/**
* Create a middleware to parse JSON bodies.
*
* @param {object} [options]
* @return {function}
* @api public
*/

function json(options) {
options = options || {}

var limit = typeof options.limit !== 'number'
? bytes(options.limit || '100kb')
: options.limit // 设置解析请求体长度上限。
var inflate = options.inflate !== false // 设置是否要将压缩的请求体解压。
var reviver = options.reviver // 传递给 JSON.parse() 的参数。
var strict = options.strict !== false // 设置是否只解析对象和数组。
var type = options.type || 'json' // 设置解析的请求体内容类型。
var verify = options.verify || false // 设置请求体内容验证函数。

if (verify !== false && typeof verify !== 'function') {
throw new TypeError('option verify must be function')
} // 请求体的内容验证函数必须是一个函数。

function parse(body) {
if (0 === body.length) {
throw new Error('invalid json, empty body')
}

if (strict) {
var first = firstchar(body) // firstchar 函数的定义在116行。

if (first !== '{' && first !== '[') {
throw new Error('invalid json')
}
}

// 解析 body ,详见:
// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse
return JSON.parse(body, reviver)
}

// 返回解析函数作为 expressjs 的中间件。
return function jsonParser(req, res, next) {
// req._body 标记着请求体是否已被解析,若 req._body 为 true ,则请求体
// 已被解析。
if (req._body) return next()
req.body = req.body || {}

// 检测请求体是否与 type 类型匹配。
if (!typeis(req, type)) return next()

// RFC 7159 sec 8.1
var charset = typer.parse(req).parameters.charset || 'utf-8'
if (charset.substr(0, 4).toLowerCase() !== 'utf-') {
var err = new Error('unsupported charset')
err.status = 415
next(err)
return
}

// read
read(req, res, next, parse, {
encoding: charset,
inflate: inflate,
limit: limit,
verify: verify
})
}
}

/**
* Get the first non-whitespace character in a string.
*
* @param {string} str
* @return {function}
* @api public
*/


function firstchar(str) {
if (!str) return ''
// 见第 40 行,匹配字符串中第一个非空字符。
var match = firstcharRegExp.exec(str)
return match ? match[1] : ''
}
lib/read.jslink
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
/*!
* body-parser
* Copyright(c) 2014 Douglas Christopher Wilson
* MIT Licensed
*/

/**
* Module dependencies.
*/

// 将流中的所有内容载入为 buffer 或字符串。参考:
// https://github.com/stream-utils/raw-body
var getBody = require('raw-body')

// 互相转换 buffer 与 js 字符串。参考 github:
// https://github.com/ashtuchkin/iconv-lite
var iconv = require('iconv-lite')

// 使得程序可以在退出时执行一个回调函数。参考:
// https://github.com/jshttp/on-finished
var onFinished = require('on-finished')

// expressjs 自带的媒体解析模块,详见 github:
// https://github.com/expressjs/media-typer
var typer = require('media-typer')

// nodejs 核心模块,提供数据压缩和解压功能。参考:
// http://nodejs.org/api/zlib.html
var zlib = require('zlib')

/**
* Module exports.
*/

module.exports = read

/**
* Read a request into a buffer and parse.
*
* @param {object} req
* @param {object} res
* @param {function} next
* @param {function} parse
* @param {object} options
* @api private
*/

function read(req, res, next, parse, options) {
var length
var stream

// flag as parsed
req._body = true

try {
stream = contentstream(req, options.inflate) // 见第 129 行。
length = stream.length
delete stream.length
} catch (err) {
return next(err)
}

options = options || {} // 初始化 options 。
options.length = length

var encoding = options.encoding !== null
? options.encoding || 'utf-8'
: null
var verify = options.verify

options.encoding = verify
? null
: encoding

// read body
getBody(stream, options, function (err, body) {
if (err) {
if (!err.status) {
err.status = 400
}

// read off entire request
stream.resume()
onFinished(req, function onfinished() {
next(err)
})
return
}

// verify
if (verify) {
try {
verify(req, res, body, encoding)
} catch (err) {
if (!err.status) err.status = 403
return next(err)
}
}

// parse
try {
body = typeof body !== 'string' && encoding !== null
? iconv.decode(body, encoding) // 将请求体解码为 js 字符串。
: body
req.body = parse(body)
} catch (err) {
if (!err.status) {
err.body = body
err.status = 400
}
return next(err)
}

next()
})
}

/**
* Get the content stream of the request.
*
* @param {object} req
* @param {boolean} [inflate=true]
* @return {object}
* @api private
*/

// inflate 表示是否给数据解压缩。
function contentstream(req, inflate) {
// req.headers 是 http 请求的请求头,详细参数见:
// http://www.w3cschool.cc/http/http-header-fields.html

// identity 代表没有压缩编码,见 RFC 7231 , sec 3.1.2.2 。
var encoding = req.headers['content-encoding'] || 'identity'
var err
var length = req.headers['content-length'] // 见 RFC 7230 , sec 3.3.2 。
var stream

if (inflate === false && encoding !== 'identity') {
err = new Error('content encoding unsupported')
err.status = 415
throw err
}

// 参考 zlib 文档: http://nodejs.org/api/zlib.html 。
switch (encoding) {
case 'deflate':
stream = zlib.createInflate()
req.pipe(stream)
break
case 'gzip':
stream = zlib.createGunzip()
req.pipe(stream)
break
case 'identity':
stream = req
stream.length = length
break
default:
err = new Error('unsupported content encoding')
err.status = 415
throw err
}

return stream
}
lib/types/urlencoded.jslink
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
/*!
* body-parser
* Copyright(c) 2014 Jonathan Ong
* Copyright(c) 2014 Douglas Christopher Wilson
* MIT Licensed
*/

/**
* Module dependencies.
*/

var bytes = require('bytes')
var deprecate = require('depd')('body-parser')
var read = require('../read')
var typer = require('media-typer')
var typeis = require('type-is')

/**
* Module exports.
*/

module.exports = urlencoded

/**
* Cache of parser modules.
*/

var parsers = Object.create(null)

/**
* Create a middleware to parse urlencoded bodies.
*
* @param {object} [options]
* @return {function}
* @api public
*/

// 详见: https://github.com/expressjs/body-parser#bodyparserurlencodedoptions 。
function urlencoded(options){
options = options || {};

// notice because option default will flip in next major
if (options.extended === undefined) {
deprecate('undefined extended: provide extended option')
}

var extended = options.extended !== false // 是否采用 qs 模块解析 url 编码。
var inflate = options.inflate !== false
var limit = typeof options.limit !== 'number'
? bytes(options.limit || '100kb')
: options.limit
var type = options.type || 'urlencoded'
var verify = options.verify || false

if (verify !== false && typeof verify !== 'function') {
throw new TypeError('option verify must be function')
}

// 选择解析器。
var queryparse = extended
? extendedparser(options)
: simpleparser(options)

function parse(body) {
return body.length
? queryparse(body)
: {}
}

return function urlencodedParser(req, res, next) {
if (req._body) return next();
req.body = req.body || {}

if (!typeis(req, type)) return next();

var charset = typer.parse(req).parameters.charset || 'utf-8'
if (charset.toLowerCase() !== 'utf-8') {
var err = new Error('unsupported charset')
err.status = 415
next(err)
return
}

// read
read(req, res, next, parse, {
encoding: charset,
inflate: inflate,
limit: limit,
verify: verify
})
}
}

/**
* Get the extended query parser.
*
* @param {object} options
*/

// 利用 qs 模块解析 url ,详见: https://github.com/hapijs/qs 。
function extendedparser(options) {
var parameterLimit = options.parameterLimit !== undefined
? options.parameterLimit
: 1000
var parse = parser('qs')

if (isNaN(parameterLimit) || parameterLimit < 1) {
throw new TypeError('option parameterLimit must be a positive number')
}

if (isFinite(parameterLimit)) {
parameterLimit = parameterLimit | 0
}

return function queryparse(body) {
if (overlimit(body, parameterLimit)) {
var err = new Error('too many parameters')
err.status = 413
throw err
}

return parse(body, {parameterLimit: parameterLimit})
}
}

/**
* Determine if the parameter count is over the limit.
*
* @param {string} body
* @param {number} limit
* @api private
*/

function overlimit(body, limit) {
if (limit === Infinity) {
return false
}

var count = 0
var index = 0

while ((index = body.indexOf('&', index)) !== -1) {
count++
index++

if (count === limit) {
return true
}
}

return false
}

/**
* Get parser for module name dynamically.
*
* @param {string} name
* @return {function}
* @api private
*/

function parser(name) {
var mod = parsers[name]

if (mod) {
return mod.parse
}

// load module
mod = parsers[name] = require(name)

return mod.parse
}

/**
* Get the simple query parser.
*
* @param {object} options
*/

function simpleparser(options) {
var parameterLimit = options.parameterLimit !== undefined
? options.parameterLimit
: 1000
// 调用 nodejs 核心模块 querystring 解析 url 。详见:
// http://nodejs.org/api/querystring.html
var parse = parser('querystring')

if (isNaN(parameterLimit) || parameterLimit < 1) {
throw new TypeError('option parameterLimit must be a positive number')
}

if (isFinite(parameterLimit)) {
parameterLimit = parameterLimit | 0
}

return function queryparse(body) {
if (overlimit(body, parameterLimit)) {
var err = new Error('too many parameters')
err.status = 413
throw err
}

return parse(body, undefined, undefined, {maxKeys: parameterLimit})
}
}

由于甲骨文公司的版权限制,Linux 发行版不能包含 Oracle Java,Ubuntu 也只提供了开源的 OpenJDK。OpenJDK 会带来各种各样的兼容性问题,不推荐部署 Java 开发。甲骨文 Oracle Java 本身既包含 JRE(Java Runtime Environment Java 运行环境)又包含 JDK(Java Development Kit Java 开发套件),我们只需要安装一份 Oracle Java 8,即可满足需要。

对于 Ubuntu 及其衍生版,使用 PPA 软件仓库安装 Oracle Java 8 是最快捷的,在终端中输入命令如下即可完成安装:

1
2
3
sudo apt-get repository ppa : webupd8team/java
sudo apt-get update
sudo apt-get install oracle-java8-installer

下面这条命令可以设置环境变量,将 Oracle Java 8 设置为系统的默认 Java 虚拟机:

1
sudo apt-get install oracle-java8-set-default

完成以上步骤之后你可以通过下面这行命令检验是否安装成功:

1
java –version

如果安装成功,终端会得到如下提示:

1
2
3
java version “1.8.0”
Java (TM) SE Runtime Environment (build 1.8.0–b132)
Java HotSpot (TM) 64-Bit Server VM (build 25.0–b70, mixed mode)

具体显示的版本号可能会略有不同,例如,32 位机器上会显示 32 位虚拟机的相关信息。