总是给其他人布置作业,终于遭了报应。被94smart点了名,要做遗憾接龙的游戏,得交作业,写一到五个遗憾。
按要求,这个游戏还需要传下去,我就不传了,实在不好意思,坏了规矩呵。
经过一阵的开发、出差和来回折腾,发现自己竟然有一个多月没有看一本完整的厚书了(paragmatic系列的小书和文档是不算的)。不禁觉得自己在技术上似乎落后了,唉,学习的时间是靠挤出来的。然而,如此一想,我心里反而觉得庆幸,虽然这两年没能赚钱,但是这两年却给我大量的时间阅读了无数的技术书籍,看了无数的技术八卦和业界文章,这对于每天工作八九个小时回家就筋疲力尽的程序员来说是不可想象的。我感觉这种大量阅读与积累比天天写程序做项目的收获肯定是多的(当然做项目是必不可少的,光看书不做项目的人是不可能取得进步的)。因为阅读和看牛人的采访blog使我的眼界变高,使我可能站在更高的角度用更接近牛人的思维看问题,这种影响是潜移默化的。还记得两年前的我似乎根本不会写文章,敲破脑袋也写不了几行,但是现在写起东西来一堆一堆的。比如说对一个项技术分析优劣,我几乎不用太多思考就能想出个三五点,而且是一气呵成就把东西给写出来了。
然而最近确实很累,不知道是处于疲倦期还是程序写得太多了。在这样的状态下,看太深的书似乎进不了状态,不过我终于发现桌上居然还有一本中文版的厚书,而且这本书的内容在这种状态下看比较合适,这就是Java Development with Ant。得到这本书纯属偶然,然而我把这本书搁在书架上这么久让我产生了罪恶感,而这确实是一本好书。
总的来说,我似乎更喜欢看深入问题本质的书,象设计模式、J2EE without EJB里的深入透彻的分析会使我进入更兴奋的状态。而似乎对操作性的书并不在意,比如说Subversion,我现在用它仍然是一知半解的,反正几个常用的命令checkout, commit, update之类的会就行了。Ant也是其中一例,我对Ant唯一会干的事就是copy and paste,从别人的脚本里拿过来改一下就OK了。
然而一知半解始终不是一个好习惯,如果有时间的话,从头而深入地对一种工具进行研究会让我们的水平更上一层楼。更何况ANT是如此常用又优秀的工具(到目前为止我仍然没有成为maven的fan,1.0版的不稳定,和2.0版的与旧版不兼容总是让人不太放心)。而Java development with Ant正是一本让我们深入了解Ant的一本书,这本书的质量不用怀疑,这么多优秀的评语是不会看错的,作者之一Erik Hatcher也是lucene in action的作者,同时他们也都是ant的commitor 。这本书的翻译质量也相当令人惊喜,通顺而流畅的文笔,让人看起来有飞快又很爽的感觉。
这本书最大的特点是它的全面性,它将ant的基本特性进行了详尽而细致的分析,对于我这种copy and paste的用户,感觉好象以前的东西全部连在了一块,以前的一些疑问和细节的问题都得到了解答,真是很爽的感觉。当然除了Ant的基本特性外,还有很多的实际与项目开发相关的技术都涉及了,单无测试、XDoclet、Web服务、持续集成、成品部署,这本书提供了大量有益的实践技巧。
当然,毕竟这本书还是有些早了,它当时讲的还是Ant 1.5,虽然Ant本身的特性基本上没有过时,但是与Ant相关的一些技术确实还是有些过时了,比如说XDoclet已经被Annotation代替了,比如说持续集成里的CuiseControl,现在的版本和以前的区别还是挺大的。但是这本书仍然有很大的价值,甚至拿来当参考书也是很不错的。
偶对Acegi Security System景仰已久,它利用了Spring提供的IOC和AOP的机制,实现了一个安全架构,让我们的业务逻辑和安全检查逻辑完全解耦,所有的安全逻辑只要通过Spring的标准配置文件的定义就可实现了。不过说得简单,真的每次拿起Acegi Security System来看的时候,感觉就象云里雾里,如果不仔细看它的文档,再深入研究的话,根本就看不出头绪。某种程度上来说,学习Acegi Security System似乎比学习Spring还要难。
最近偶沉下心来,花了好多时间,看了文档和主要的源代码,终于摸出了点头绪。为什么Acegi Security System会这么难?我觉得原因有以下几个:
一、Acegi Security System的配置文件是用的是Spring的标准配置。某种程度上,它的配置是针对Acegi的设计和实现都很了解的人,而不是针对最终用户的。配置文件里那么多的类,那么复杂的对象关联,如果没有对Acegi的设计有深入了解,只能看得云里雾里。也许我们能在Acegi Security上包一层,定义自己的配置文件来简化Acegi Security。但是仔细想想,Acegi Security的设计是有道理的,Security本来就不是一件简单的事,我们不能指望一个新手来完成所有Security的配置。而利用Spring的配置文件却能达到最大的灵活性,利用Spring的IOC机制,所有的配置都可以提供我们的自定义的实现。一旦当我们对Acegi有了深入了解之后,所有的这些配置就会变得很自然了。
二、Acegi的文档相当的好,它将Acegi的内部实现、类之间的关联,实现的目标进行了很深入的分析。但是却忽略了一点,它的整个文档是从设计出发,而不是从需求出发的,整个文档似乎更关注于Acegi Security是怎么设计的,而不是怎么用的。当然我承认如果要深入了解Acegi,我们必须要知道它的设计和实现,但是对于一个新手来说,假如我们能马上知道Acegi的安全认证执行流程是怎么样的,再通过个认证流程展开说明authentication和authorization所涉及到的东西岂不更好?
今天先写到这里啦,过两天再写一下Acegi Security的认证流程吧,还是蛮好玩的,虽然写起来可能会有点累。
我发现我给自己设了个陷阱,对于Acegi Security System的解析并不是光写个安全认证的流程就能说清楚的。虽然看起来Acegi的文档确实挺累,但是当我动笔写时却发现要写得比这个文档更好还是挺难的。毕竟我们不能让本来很难的事情一下子就变得很简单了,我也是昨天重要看了一遍了Acegi Security System的源代码,才发现我自己有明白了好多不明白的东西。但是我仍然希望我写的东西能够帮助那些正在学习和研究Acegi Security System的人们,所以我又开始动笔了,呵呵!
虽然我不想介绍太多与安全认证流程无关的东西,但是有些东西的讲解却是必不可少的,因为没有这些基本的概念和类,后面的东西就没法说清了。不过对于具体的类、类图和它们之间的关联,我还是推荐去看Professional Java Development with the Spring Framework里关于Acegi的那一章,对于想读Acegi的源代码和了解Acegi内部设计的人来说,这一章真是太有用了。
本来不想贴这幅类图的,毕竟有盗版的嫌疑,但是发现有些东西不贴出来又说不清楚。整个认证功能的核心是Authentication接口,我们只把Authentication想象成一个包含用户基本信息的类就行了,它里面放了用户名、密码、这个用户的具体权限有哪些(当然具体的东西是由它的子类UsernamePasswordAuthenticationToken实现的,其它类的存放的信息稍有不同,毕竟验证的方式还是多种多样的,我这里描述的所有东西Acegi最常用的实现方式,而不考虑其它的东西,否则只会分散注意力,看了之后一头雾水)。Authentication里还包含了一个GrantedAuthority接口,今天暂且不讨论Authorization的问题了,毕竟它与验证的流程是不相关的,而具体的细节又极复杂。
我们通过AuthenticationManager这个接口来验证这个Authtication对象的合法性,真正的验证过程看上去很悬,其实最后的实现无非是去数据库搜索一下是否存在这个用户,密码是否匹配(说的仍然是最常用的实现方式,呵呵),只是它设计的时候对象的关联比较巧妙,类图看熟了就会觉得没什么,真正查数据库的那个类是DaoAuthenticationProvider。这个接口真正巧妙的地方是它执行后返回的结果是一个Authentication,而不是用一个布尔值来表示验证成功或失败。Why? 记得当年在JavaEye上有个讨论Exception的贴子,robbin认为用户安全认证应该用Checked Exception来控制流程,更多的人认为密码错误是正常的事件流,返回布尔值更为恰当,这里不讨论这两种观点的对错,毕竟每个人站的角度不同,具体的情况也不同。
但是如果要实现认证的透明性,我们要用到的却是unchecked exception,这个Exception叫做AuthenticationException(如果是authorization会抛出AccessDeniedException,不过道理类似),这真是奇妙的事,因为Exeception是可以传递的,如果在本类里面处理不了这个Exception,我们就会将这个Exception抛给调用这个类的类,如此循环,直到有一个类可以处理它为止(对于Web来说应该是在页面上提示登录出错信息)。没想到Exception的这个种特性用在安全认证里如此的合适,权限不够?用户密码不对?我才不管呢,只要抛出个异常,最后会有人把它接住处理掉的。当然这里的另一个条件是Unchecked,只有unchecked才不会导致接口的污染,才能达到完全的透明性。
有了前面的基础接口,我们要提出下一个问题了,这个Authentication对象应该存放在哪里?几乎每个做过Web应用的人告诉我:HttpSession。Acegi也不例外,虽然还有其它的存放地点(要跟具体的Web容器结合,会导致不兼容性,一般不提倡用)。但是我们马上会问下一个问题:我们怎么得到Authentication对象?当然我们可以去HttpSession里去取,但是很多时候我们在验证的是与Web层无关的(比如要用户调用Service层的权限或Domain Object的权限)。我们必须用与Web无关的API来获取Authentication。这让我们想起了什么?对,是Webwork,Webwork的Action是完全与Web API无关的,它的Request里的参数自动populate成了Action的属性,但是我们仍然可以通过ActionContext来获取这些信息。它是怎么做到的?是Threadlocal,因为每个Web Request都会使Web容器生成一个新的线程来处理它的这个特性使我们可以将这些Web的数据一股脑塞给Threadlocal。这个存放Authentication的对象叫做SecurityContext,而把SecurityContext放入Threadloal或取出的则是SecurityContextHolder,下面就是它的类图:
讲完这些基础设施,我们就可以看具体的认证流程啦,真正的认证是一串的Filter(对Servlet容器熟悉的人应该都不要解释了)。只不过Acegi在这些Filter上稍微玩的点花招,因为一般的Filter是不能定义在Spring的ApplicationContext里的,所以这用了一个代理的Filter对象FilterToBeanProxy将Filter操作Delegate给定义在ApplicationContext里的Filter。这个似乎跟主题无关,不过如果以后真有类似的需求的话,这倒是蛮管用的一招。当然还有FilterChainProxy,它把一串的Filter全部定义在一个bean里,使配置简化了好多,呵呵。
我们来看看Filter的一头一尾。头是httpSessionContextIntegrationFilter,其实它的功能前面已经讨论过了,在执行前把HttpSession里的Authentication取出来放到SessionContextHolder(Threadlocal)里,在执行完毕后,把Authentication塞回到HttpSession。真正的实现代码有一堆,不过核心的代码无非就这么几行:
Object contextFromSessionObject = httpSession.getAttribute(ACEGI_SECURITY_CONTEXT_KEY);
SecurityContextHolder.setContext((SecurityContext) contextFromSessionObject);
chain.doFilter(request, response);
httpSession.setAttribute(ACEGI_SECURITY_CONTEXT_KEY,,SecurityContextHolder.getContext());
Filter的尾是securityEnforcementFilter,它的工作就是进真正的用户认证的流程控制了,具体的认证工作它会delegate给FilterSecurityInterceptor,但是不管怎么认证,结果无非是认证成功或失败,securityEnforcementFilter只要管抓住异常再转到特定的页面就行了。下面就是这个类的信心代码:
try {
filterSecurityInterceptor.invoke(fi);
}
} catch (AuthenticationException authentication) {
sendStartAuthentication(fi, authentication);
} catch (AccessDeniedException accessDenied) {
sendAccessDeniedError(fi, accessDenied);
}
我们再来看看用户登录是怎么干的吧。Acegi的用户登录很有意思,为了不让用户写任何这方面的代码,它也直接把这个功能放到Filter里了,这个Filter叫做authenticationProcessingFilter。这个Filter的要求是页面上的form的Action名字,登录名、密码的字段名都是定死的。一个简单的页面就这些啦:
<form action="<c:url value=' j_acegi_security_check '/>" method="POST">
<input type='text' name='j_username'>
<input type='password' name='j_password'>
<input name="submit" type="submit">
</form>
记住action必须叫j_acegi_security_check,用户名必须叫j_username,密码必须叫j_password。呵呵,代码就不写了,无非就是判断只要Action名字对,就把用户名、密码取出来认证一把,最后把认证成功的Authetication对象填到SecurityContextHolder里再导到相应页面,认证失败就导到出错页面。
呵呵,好了,认证的核心过程其实就这些了,虽然还有其它的好多的Filter和关联,但是当我们把核心的内容分析清楚之后,其它的都不难了。(Authorization是例外,有些部分我还没看明白)。
一个突如其来的项目需求,再加上自己的懒惰,使我的blog又好久更新了。不过仅用了一周不到时间做出一个基于语义的海量数据库全文搜索系统确实还是一件挺有成就感的事,当然看资料和研究lucene的时间也有一周多,我比较喜欢在做开发之前狂看书,把东西了解透了然后再狂写代码,可能跟一些人一边看书一边写代码的风格稍有不同,呵呵。
不过lucene确实是好东西,短短不到2万行的代码实现了如此强大全文搜索功能,确实让人汗颜。在玩完lucene,再联想起前段找工作的经历,不由的想起了算法的重要性。算法真是奇妙的东西,一个突发灵感可能会让程序的效率有数百倍的提高,可能解决以前根本解决不了的问题。看看lucene,再看看google, ms, baidu,心中不免有了狂学算法的冲动。毕竟算法在找工作中的作用也是出乎想象的,不说google、ms、baidu,光杭州的几家公司如trilogy、askJeeve所给出年薪都在12万以上。即使不做算法的公司,它的笔试题里都包含了大量的算法题。
然而仔细想想,算法并不是开发的全部,在程序员这个行业里我们需要知道的东西远远不只算法,我不知道我这样的归纳是否准确:算法、语言、底层和solution。也许当中的概念会有些重叠,也许这样的分法很粗糙,不过似乎这几个词是大家平时聊天时比较会经常用到的。如果我们对其中的某一项掌握到一定的层次,我们都可能被认为是大牛了,只可惜所有的这些东西都有一个特征:入门容易,精通难。
算法的问题我不想多谈,谈起这个来我隐隐觉得惭愧,毕竟感觉自己太初级了。除了大学里的数据结构和近来看的算法导论,我对算法理解还是很初级的,看着周围的好几个同学以18万以上的年薪进了google,心中也不免后悔当初没有努力。但是仔细回想,也许当初的选择其实是对的,算法要求的天赋和思维的跳跃,它很大一部分是需要天分的。象我这种从小学到高中从没有在数据竞赛或其它竞赛中获奖的人就算是拼命地学算法,也许最后的结果只是将书的算法烂熟于胸,而很难提出创新的点子。
语言的重要性是不言而喻的。我只想提一下前段时间在吴山和云风聊以及对云风的blog上这篇文章的看法。不要误会哦,我对云风是非常佩服的,他的技术水平是吾辈望尘莫及的,而他新写的那本书在国内也有巨大的影响力。而且感觉他好能说、又幽默,个子高,又能运动,长得又帅(呵呵,不要误会哦,我不是gay)。更重要的是明年3月份我就要跟他成为同事啦。不过这篇文章和他对Java的观点是我唯一不能同意的,云风的错误在于,将自己开发游戏的思路运用到开发其它solution的上,虽然他对这些领域不是太熟悉(对自己不熟悉的领域妄加评论通常是不恰当的,我也犯过样的错误)。
java 封装和统一了太多东西,导致程序员的智慧无初发挥,只能集中到设计模式上发泄了 :) 而 C++ ,却有无数的方法证明你的聪明。
Java程序员没有创造力吗?Java程序员的智慧没法发挥吗?我的看法是解决不同的问题需要的是不同的智慧,在用C++开发游戏时我们需要深厚的算法、底层知识。在用Java开发solution时,我们需要的是站在更高的层次,对系统的设计有整体的把握(设计模式仅仅是其中的一个方面)。我们需要掌握从现实世界的事物、逻辑到计算机语言的抽象,这就是设计与建模,任何架构师都会告诉你建模不是简单的事。我们需要开发framework把开发中遇到共性的东西抽象出来,开发framework的难度在于抽象和面向对象设计的深层掌握。我们需要掌握敏捷开发、测试驱动(不要以为测试驱动是很简单的事,测试驱动的关键是我们设计的代码的可测试性和测试条件的准备,而不是测试本身)、各种不同的framework、语言上的细节(Java比C++简单,但是它仍有很多深层次的复杂性)。当然还有企业级应用本身的特殊要求…
开发一个大的企业级应用的复杂性远远超出了外人的想象。很多人的观点是越底层的东西开发起来越难,于是出现了很多的底层崇拜者。产生这种现象的原因是底层的神秘感,因为站在外面的人无法底层的东西是怎么实现的,所以就觉得它很难实现。从入门的角度来说也是,一个几个月开发经验的程序就能做出一个简单的Web应用,而要开发底层的东西也许要经过一两年的学习。
但是我们要记住任何东西深入以后都是难的,还记得Rod Johnson在今年TheServerSide上Why J2EE project failed的讲座,原来老外也一样,大概有一半的J2EE项目竟然是以失败而告终的,投入了几亿欧元的J2EE项目最后竟然流产!当企业级应用的规模大到一定的程序时,所有的问题都会暴露出来,复杂的业务逻辑、concurrent、scalability、需求的变化和evolution,每一项都不这么容易解决的,仅仅这几点就已经足够架构师们绞尽脑汁了。
洋洋洒洒说了一堆,最后总结出我日后的发展规划:solution为主,算法为辅。虽然我对算法没有太多天赋,但最近觉得搜索确实是一件很有趣事。我觉得每个人都应该对自己的日后发展做出一个规划。
尽管最大匹配法分词是常用的解决的方案,但是无疑它存在很多明显的缺陷,这些缺陷也限制了最大匹配法在大型搜索系统中的使用频率。最大匹配法的问题有以下几点:
一、长度限制
由于最大匹配法必须首先设定一个匹配词长的初始值,这个长度限制是最大匹配法在效率与词长之间的一种妥协。我们来看一下以下两种情况:
(1)词长过短,长词就会被切错。例如当词长被设成5时,也就意味着它只能分出长度为5以下词,例如当这个词为“中华人民共和国”长度为7的词时,我们只能取出其中的5个字去词库里匹配,例如“中华人民共”,显然词库里是不可能有这样的词存在的。因此我们无法下确的划分出“中华人民共和国”这样的词长大于5的词。
(2)词长过长,效率就比较低。也许有人会认为既然5个字无法满足我们的分词要求,何不将词长加大,例如加到10或者100,毕竟这个世界超过100个字长的词还是很少见的,我们的词长问题不就解决了?然而当词长过长时,我们却要付出另一方面的代价:效率。效率是分词算法、甚至是整个算法理论体系的关键,毕竟算法书里所有的高深的查询或排序算法都是从效率出发的,否则任何笨办法都可以解决分词效率低的问题。设想到我们把字长设成100个词时,我们必须将词从100开始一直往下匹配直到找到要查的字为止,而我们大多数词的字长却只有两三个字,这意味着前97次的匹配算法是徒劳的。
因此我们必须要在词长与效率之间进行妥协,既要求分词尽量准确,又要求我们的词长不能太长。尽管我们可能找到这样一个比较优化的字长值使两者都达到比较满足的状态,但是毕竟不管我们怎么设定,总会有些太长词分出来,或者带来效率问题。
二、效率低
效率低是最大匹配法分词必然会来的问题。即使我们可以将字长设成相当短,例如5(注意,我们不能再缩短字长了,毕竟字长为5以上的词太多了,我们不能牺牲分词的准确),然而当我们的大数词长为2时,至少有3次的匹配算法是浪费掉的。回想一下算法书里提到的最简单的字符匹配与KMP算法之间天差地别的效率,我们知道通过某种方法,这些浪费的掉的匹配时间是可以补回来的。
三、掩盖分词歧义
中文是如此复杂的语言,它的表达方式如此之多,语法文法如此精妙,机械的电脑是很难理解这么复杂的语言,因此它必然会带来歧意性,以下是两个简单的例子:
A.“有意见分歧” (正向最大匹配和逆向最大匹配结果不同)
有意/ 见/ 分歧/
有/ 意见/ 分歧/
B.“结合成分子时” (正向最大匹配和逆向最大匹配结果相同)
结合/ 成分/ 子时/由于词的歧义性使我们在使用最大匹配法分词会产生错误的结果,而且使用正向分词与逆向分词往往会产生截然不同的结果。尽管使用回溯法或计算计算词的使用频率,可以使出现歧义的可能性减少,但是我们知道,这样的结果是不可避免的,因为中文的变化实在太多了。
四、最大匹配的并不一定是想要的分词方式
最大匹配法基于的理念是找到最大的匹配词,但有的时候除了最大匹配词外,我们也可能只需要这个词的一部分。例如“感冒解毒胶囊”是一个完整的词,按照最大匹配法我们无法对它进行拆分了,这样我们输入“感冒”的时候就根本搜不到我们需要的词。这是我们需要的吗?做为生产这种药的厂商,它肯定希望用户输入“感冒”甚至“解毒”,我们都能查到对应的内容。
基于对分词算法的理解和对最大匹配法分词的分析,我们知道我们必须提出不同的解决方案,使分词算法的效率、分词的长度限制甚至歧义处理上得到提高。因此我们提出了如下的设计目标:
一、 高效
中文分词算法必须要高效,毕竟效率对于搜索引擎的重要性是不言而喻的。而且我们面对的是海量的数据,而不是一篇几百字或几千字的文章,效率的差别的影响可能会使最后运行效率差几个小时甚至几天。因此我希望我们设计的算法一定要比最大匹配法高,毕竟我们已经常看到最大匹配法的很多次匹配都是浪费在无用功上了,肯定有办法把这些浪费的时间节省回来。
二、无长度限制
最大匹配法的长度限制真是很讨厌的事,我们很难找到词长与效率的之间的平衡。为什么我们需要长度的限制?为什么我们不能设计出任何词长的词(只要词库中存在)都可以分出来?
三、歧义包容
我们相信长度限制的问题总是可以解决的,因为虽然长度限制这个问题很难,但是它是有规律可循的,它是严谨的科学。但是当我们碰到中文歧义时,我知道不管我们怎么努力,它仍然是不可能彻底解决的。因为中文实在太博大精深了,即使有极强的人工智能和机器学习功能,这样的错误仍然是难以避免。既然无法避免?我们为什么不换一个角度去考虑?我们为什么不可以将出现歧义的各种可能性都包含进去,作为分词的参考。例如上述的“有意见分歧”的两种分词方法:
有意/ 见/ 分歧/
有/ 意见/ 分歧/
为什么我们不能把这样两种结果都拿来分词呢?毕竟分词的目的是为了搜索,而不是为了教小孩读出。如果把两种分词的可能性都告诉搜索引擎,搜索引擎会很高兴的,因为这下不管是“有意”还是“意见”,它都可以搜到了。这就是我提出来另一个目标:歧义包容。
虽然我们的目标已经确定下来了,但是要想出一个更好的算法却是非常难的事。毕竟算法需要的是灵感与突发奇想,这与系统的架构设计和面向对象的设计与编者编码刚好是相反的,象设计模式或重构这样的东西我们需要的实践、总结、再实践。而算法需要的却是当我们在山重水复疑无路的时候会换个角度思考。
但是分词算法的突破口在哪里呢?我们必须要有一个词库,我们必须将全文中的词与词库去匹配,这一切都是不可避免的。
真正要改善是就是我们的匹配过程,我们要减少匹配过程中的浪费,我们要解决匹配中的词长限制。但是我们有什么办法呢?每次的匹配我们必须要去词库中查找一次。怎么改善这样的做法?
我们总是把优化的思路定格在更好的匹配算法,更好地处理词条和全文。但是真正束缚我们的却是词库!是基于关系数据库的词库,我们需要的对词库的改造,我们要让我们的词库更适合用于匹配与分词!
这是几十年来关系数据库带给我们的思维:我们查找的词是数据库的某条记录,通过表格与关系代数,我们总能找到这个词。但是正是关系数据库的这种思维束缚着我们,关系数据库让我们的数据结构及关联表达得清楚又简单,并使某些查询的效率变得很高。但是这不适用于中文分词,有的时候退到几十年前流行的数据库模型也许更适合。这就是层次数据库。
我们要做的是将关系数据库的词按字打散,并存放到层次数据库中。以下就是一个示例:
红色的字表示树上面的字串是可以单独组成一个词的,例如“感冒”它本身是词库里可以找到的词,所有红色的表示的是终止符。而黄色则表示树上面的字串是无法单独成词的,例如“感冒解”是不存在的词。
真的很奇妙,词库经过这样的改装后,所有的匹配的思维都变掉了。任何一个句子都会打散成单字去与树状结构的单字去匹配,词的长度变成了树的高度,每一次的匹配变成了树的遍历,而这种遍历的效率竟然都是线性的!
有了以上的中文词库后,我们分词算法设计就水到渠成的。首先我们来看一下分词的步骤:
(1)首先将要分的全文按标点符号打散成一个一个的句子。这算是预处理的一个步骤,目的是让我们处理的句子短,效率更高。毕竟中间有标点符号的词是不存在的。(注:真正实现时我们是基于lucene的SimpleAnalyzer来做的,因为SimpleAnalyzer本身就是为了将全文打散成句子,因此我们没必要耗费体力去实现这一步)。
(2)我们开始将要处理的句子在树状结构中遍历,如果找到匹配的就继续,如果遇到红色的终止符,我们就发现这个词是一个完整的词了,这样我们就可以把这个词作为一个一个分词了。
(3)从分词后的下一字开始继续做步骤2这样的遍历,如此循环往复就将词分完了。
可以看到,我们字符匹配效率几乎是线性的!我们所要做的只是取出每一个字去树上找到相应的匹配,每次的匹配代价都是O(1)(如果词库用Hash表的话),这样匹配下来的时间复杂度就是字符串本身的长度!对于一个长度为n的字符串来说,它的分词复杂度是O(n)。而最大匹配的平均复杂度是O(n2)。
当然我们这里没有考虑歧义包容与分支处理等情况,但即使加上这些我们复杂度仍然是有限的。
一、建立词库
有了改装词库的基本思想后,建立词库的步骤变得很简单,但是仍然会有好多的细节需要注意。
首先是词库的保存格式。现在最常用的保存数据的方式当然是关系数据库,其次是文件系统中的二进制文件。显然关系数据库对于我们并不适用,而自定义的二进制文件则实现起来比较困难,而且读写的效率也会有问题。因为我们想到了最简单的方法是利用java的serialization的功能,把整个内存中的树状结构直接序列化成磁盘的文本文件是最方便的!而且读写的效率也会相当的高。
第二个问题是树的父子节点的导航。我们的树并不是一颗二叉树,父亲的子节点会有好多!尤其是第一层,我们会把词库中所有的首字都取出来作为根节点的子节点,这意味着如果首字有4000个的话,根节点就有4000个儿子。当然随着树层数的增多,节点的儿子数也会减少,毕竟以“感冒”开头的词在整个词库也只有四十多个,而以“感冒清”开头的词则只有两三个了。这意味着如果设计得不合理,我们树的匹配遍历过程并不完全是线性的。最坏的查找算法是O(N)(N代表儿子数)。当然如果我们建词库时将儿子有序排列,再按照二分查找的方法,则我们的复杂度会减到O(lgN),这样的复杂度已经可以接受了。但是还有更简单又更快的存储方式,为什么不使用呢?那就是HashMap,毕竟在HashMap里查找东西时它的效率几乎是线性的,而且实现起来要比二分查询简单得多。当然用HashMap要付出存储空间变大的代价,但这样的代价来换取速度与简单性也是的。
第三个问题是找到有终结符的字后,我们必须要将它建成一个完整的词。这时我们必须能从字个往上回溯,直到找到根结点。因此我们在每个节点里都保存了父节点的指针。
有了以上的设计思想,我们就动手建立了我们的词库,词库的来源是中医药数据库的词汇表,因为我们应用一直是围绕中医药的。我们找到了两个最重要的表,这两个表几乎包含了中医药的全部词库:
一体化语言系统词库 92112个词
疾病大全、症状、证候 20879个词
最后生成的词库是java serialization的一个文件,文件的大小是16M 。当然这跟我们采用HashMap存放父子关联有关,也跟java的对象所占空间有关,虽然将词库按这种方式存放实际上也对词库进行了压缩(以“感”开头的字有数十个,关系数据库里就要保存数十个,但我们在词库只保存了一个“感”)。但文件仍然偏大,因此用oracle将这两个表导出后生成的文件大小是4M。不过这个大小仍然是可以接受的,毕竟效率才是关键。
二、 分词查询
虽然刚才对分词算法进行了描述,但实际上实现的时候我们还会碰到很多问题。
1、分支处理。
这是分词算法时歧义包容所必然碰到的问题。为了歧义包容,我们采用了与最大分词法完全不同的理念,我们的理念是将词库中存在词全部收入囊中!而且会发生重叠。例如“感冒解毒胶囊”,由于词库里存在“感冒”、“解毒”和“感冒解毒胶囊”这三个词,因此在分词的时候,我们会分别分出这三个词,这样用户无论输入“感冒”、“解毒”或“感冒解毒胶囊”搜索引擎都会找到相应的结果。
因此当遇到分支时,我们会分解成两条路线!例如当我们匹配到“感冒”的“冒”时,我们会发现一个终止符,代表“感冒”是一个完整的字,将它收录到分词中。接下来我们会分成两支,一支是继续往下走,匹配树的下一层,因为“冒”不是树的叶子,往下走可能会碰到更大的匹配词,例如“感冒解毒胶囊”。而另一支则从根开始,直接用“解”去匹配树的第一层节点,最后发现了“解毒”也是其中的一个词。
2、动态规划法
分支虽然使我们可以消除很多的歧义,但是显然它会带来副作用:导致分词的复杂度变大。如果一个句子很长时,分词的变化也许会呈指数级的增长,从一开始的两个分支变成四个、八个甚至更多。我们会发现很多句子虽然会有很多分支,但是这些分支又经常会汇聚到一个点,变成一个分支。例如:“感冒解毒胶囊可以治感冒”,我们在分词的时候可能会出现“感冒”,“解毒”,“感冒解毒”,“感冒解毒胶囊”等多个分支,但是当我们到达“囊”这个点的时候,所有的分支又会汇集到一起,因为大家接下来要处理的都是“可以治感冒”这个字符串。如果有办法让我们在汇聚以后只处理一个分支,那么算法的时间复杂度就不会象原来想象的那么坏。
而这刚好是动态规划法发挥威力的时候,动态规划要解决的问题是Overlapping sub-problem。它的处理方法就是将所有的子问题记录在公有的变量里(这里指的是类变量,它相对于某个method来说是公有变量,而不是真的全局变量)。当我遇到的子问题已经被处理过一次了,就直接跳过。这样节约的结果可以使算法复杂度得到质的改变,当然由于中文的变化多端,我们无法精确估计使用动态规划法后算法复杂度得到了多大的提高。
实际上的动态规划法的实现起来比说起来反而简单,我们只是简单地放了一个HashSet来存放已经分词过的位置:
然后判断的函数也相当的简单:
最后在分词的递归函数中加入这一句判断:
当这个位置已经被处理过了就直接返回了。
3、词库预load
在使用基于词库的方法时,我们必须要面临的一个问题是:必要将词库读到内存中,而这通常会耗费很长的时间,幸运的是这样的工作我们只需要做一次,当我们将词库load进来以后,所有的工作都会在内存中进行,分词的速度会得到极速提升。我们选择的词库预load时机是我们第一次进行分词时,这相当于lazy load,只有用到的时候我们才去初始化。
讲完算法,我们来看看分词部分的实现代码,实际上这部分的内容实现起来远比想中简单。在处理的过程中,我们对给每个句子(实际是lucene里用SimpleAnalyzer分词后的一个个Token)都新建一个ChineseSplitter,这是更加面向对象的做法,使我们处理起来更加方便简洁,因为我们会发现如果用一个Singleton的ChineseSplitter时,它的变量无法共享会导致整个Splitter里的递归方法跟上一堆的参数,容易出错,而且无法调试。代码如下:
代码简单明了,只是做了词库预load的工作后就将实际的分词工作交给了ChineseSplitter。
ChineseSplitter的核心功能实际上将句子中词典中能找到的词放到一个队列中,这中队列里提供了分词以后的所有词的信息:
下面是分词的核心算法:
它是一个递归的过程,初始时我们调用的参数里pos为0,这样它就会一级一级递归下去并将所有可能的分词放入到tokenQueue里。
1、实验1――短文分词
在第一个实验中我们选了一篇2000字的文章(是关于中医药的专业论文)。然后用三种Analyzer对它进行处理,以下是实验结果:
Analyzer | 分词算法 | 耗时 |
SimpleAnalyzer | 将文章按标点符号隔开成句子 | 47ms |
StandardAnalyzer | 将文章的中文字分成一个一个的单字 | 250ms |
ChineseAnalyzer | 我们的分词算法 | 词库没preload: 13359ms , 词库preload: 63ms |
我们没有找到最大匹配法分词可用的开源代码,因此只能用SimpleAnalyzer和StandardAnalyzer与之比较。这两种算法事实上是根本没有去查词库的,因此也不会按任何语义去分词,SimpleAnalyzer只是简单地将文章按标点符号隔开成句子,而StandardAnalyzer则只是简单地将文章的中文字分成一个一个的单字。结果确实让人惊讶,当词库preload以后,我们的分词速度竟然远超不需要查任何词库的StandardAnalyzer算法!
2、实验2――建索引
这是将分词算法应用到我们的索引系统后的效果比较,我们的数据源是来自中医药数据库的几十张表,一共有九十万条记录:
Analyzer | 分词算法 | 耗时 |
StandardAnalyzer | 将文章的中文字分成一个一个的单字 | 35分钟 |
ChineseAnalyzer | 我们的分词算法 | 31分钟 |
由于建索引时数据库的查询操作会耗费很多的时间,因此两者的差别不是太明显,但结果至少说明了我们的分词效率确实是很高。
前几天因为好久没发blog了,就拿我毕设中的一段算法凑数,没想到引起很多人的兴趣。因此就把我的分词算法单独拎出来做了一个项目叫作DartSplitter。暂时把分词算法的名称叫做树状词库分词法。
刚刚统计了一下源代码,一共也就950多行代码,加上测试用例共1200行代码。看来确实还是想法比实现重要。说明如下:
1、由于不能用原来的专业词库,因此我特地去网上找了个Access的词库,一共有一万条记录左右,还有很多单字,因此分词的效果不会太理想。不过我这里只是为了演示一下功能,幸好从数据库转成我现的词库并不复杂,我的演示程序里提供了例子,后面还会有说明。而且,真正好的词库可能还要加入机器学习等功能,真正全面的分词可能还需要将基于词库的分词与无意义的分词结合,不过这些功能都不是那么简单的啦。
2、由于测试对词库的依赖性太强了,因此我的测试用例里没有用太多的assert,只是简单地log一下结果。而且考虑到大家用TestNG的还比较少,因此我把测试用例都改成JUnit了。测试用例与外部资源的依赖一直是困扰着我的问题,不知大家有何良策?
3、由于我现在写程序已经对Spring产生依赖症了,因此虽然我希望我程序依赖的包越少越好,但还是用了Spring,这样的好处是所有接口与关联都是可配置的。因此如果要替换掉某一部分实现也会比较简单,例如从关系数据库的词库取词的接口肯定是要重写,只要配置文件里修改一行就可以了,这个在后面说明。
4、为了方便大家使用我特意写了示例splitterTest,里面提供了两个main,一个是建词库(DictSerializationMain),另一个是对一篇文章的analysis(AnalysisTest),用了SimpleAnalyzer,StandardAnalyzer和我的TreeDictAnalyzer进行对比。
5、系统在设计的时候就是与lucene紧耦合,分词的单位也是lucene中token。这是通用性与效率平衡的结果,最后我选择了效率,而且毕竟lucene是大家用得最多的全文检索引擎包。
下面讲一下使用说明:
1、如果不需要修改源代码的话,只要下载dartsplitter-0.3.jar就可以了。
2、需要在新建项目的source的etc下放入以下配置文件(示例项目里都有,只要copy就行了):dartSplitter.properties, dictJdbc.properties, dartSplitterContext.xml。
dartSplitter.properties的大概内容如下:
splitter.dictDir=f:/WebDict (指定了词典的路径,主要用于lazy load,目前还没用到)
splitter.dictFile=f:/WebDict/common.dict (词典的文件名,只要将词典文件与配置对就行了)
splitter.maxWordLength=20 (放入词库的最大词长,load之后相当于树的高度)
演示的字典文件名位于dict文件夹下:common.dict。 commonDict.mdb则是当时找来的access文件。
dictJdbc.properties的内容如下:
dict.jdbc.driverClassName=sun.jdbc.odbc.JdbcOdbcDriver
dict.jdbc.url=jdbc:odbc:commonDict
dict.jdbc.username=
dict.jdbc.password=
其实就是词库文件对应的Jdbc链接啦。
dartSplitterContext.xml是Spring的配置文件,除了建词库时访问关系数据库的DAO配置要改动外,其它都不要去动。
3、建自己的词库
A、自己implements一下DictDAO接口,提供自己的实现,DictDAO的接口定义很简单,只要实现两个方法就行了,可参考CommonDictDAO的实现:
public interface DictDAO {
/**
* @param strPrefix 词的首个字
* @return 以这个字为首字的词对象(@see cn.edu.zju.dartsplitter.data.DictValue)的列表
*/
public List<DictValue> getDictValues(String strPrefix);
/**
* @return 词库中所有词的首字列表
*/
public List<String> getAllPrefixes();
}
B、修改dartSplitterContext.xml的配置:
<bean id="dictTree" class="cn.edu.zju.dartsplitter.impl.DictTreeImpl">
<property name="rebuild"><value>false</value></property>
<property name="maxWordLength"><value>${splitter.maxWordLength}</value></property>
<property name="fileName"><value>${splitter.dictFile}</value></property>
<property name="dictDAOList">
<list>
<ref local="commonDictDAO "/>
</list>
</property>
</bean>
只要在以下这段里将替换commonDictDAO为自己的DAO就行了,也可以加入新的DAO,因为我们考虑到有多个数据来源的情况,因此可以把多个DAO实现一起放入List里。
C、执行一下包里或者示例程序里的DictSerializationMain就OK了
最后感谢要一下blueGuitar,如果没有当时与他讨论时的灵感,就不会有现在的算法。还要谢谢车东,是他引导我使我对中文分词感兴趣的。
以下是项目的地址: http://ccnt.zju.edu.cn/projects/DartSplitter
我的blog地址: http://blog.itpub.net/xiecc
发现我的思想越来越保守了,对好多的新技术都没法马上接受。比如说ruby on rails,这一年来炒的沸沸扬扬,我也不过是冷眼旁观而已。老听人说ruby on rails开发效率如何如何高之类的,甚至有人吹嘘比J2EE高十倍,对此我只能深感怀疑。Ruby on rails在哪些方面会带来生产效率的突变?我想无外乎以下几方面:一、ruby语言本身比java有了很大的简化;二、用reflection和默认的文件夹名等消灭了配置文件;三、用Active Record、MVC和代码生成等功能使开发CRUD的程序异常简单。
但是仔细想想这些观点似乎不一定能站得住脚。
一、除了closure之外,我不认为其它的语法调整会给开发效率带来特别的变化。动态数据类型是典型的双刃剑,它使一些操作变得很简单(比如对XML的操作),但是它也带来了很多问题,例如现代IDE的IntelliSense statement completion,使用动态数据类型里根本没法做到的,而这恰恰是现代IDE生产效率的提高的重要因素。当然动态数据类型还会带来代码的安全性问题。虽然Dave Thomas、 Jon Tirsen们一直在鼓吹we need don’t need strong type language, we need strong test,我仍然表示怀疑,当我们在定义的时候就能知道数据类型后对我们了解程序的结构和设计思想绝对是有好处的。
二、当然,如果把我们定义配置文件的麻烦都去掉是一件很爽的事。但是我仍然在怀疑,就象爱因斯坦说的,”we should make the simple thing simpler, and difficult things possible”,或者另一句名言,”simple, but not simpler”。去掉配置让我们做简单的事情变得更简单,但是当碰到一些复杂的任务呢?我想不出在不需要元数据的O/R Mapping framework里如何实现lazy load与eager load的区别,如何实现second level cache的配置。
三、是的,当我们用ruby on rails开发简单的CRUD应用的Web程序时,我们的开发效率可能不只比J2EE高十倍。所有的东西都已经默认生成了,或者我们可以执行一个命令所有的代码都帮我们生成好了。可是我们都知道,企业级应用并不都象CRUD这种小儿科的游戏那样简单。它包含的业务逻辑,它要考虑的问题远比CRUD复杂数千倍。而做过几个应用的人都知道代码生成这种玩意是没意义的,在J2EE without EJB,甚至在极力推崇ruby的Dave Thomas写的pragmatic programmer里都对代码生成进行了批判。
当然,在没有深入了解一样技术的时候就妄加评论是很愚蠢的,如果我有时间的话也许会看一下agile web development with rails,也许会用ruby on rails做几个应用,只是我现在要学的东西太多了,我宁愿相信AOP和AJAX会在将来的应用开发中越来越重要,也不愿相信ruby on rails,所以我选择了冷眼旁观。也许有一天当我用ruby on rails开发了几个系统并发现它确实不仅仅是玩具,并带来强大的开发效率的时候,我也会改变我的态度。我只是希望那些嚷着ruby on rails要取代java EE的人不是因为跟风,而是都通过自己的辨别力来得出这个结论的。
本来上面这部分内容是要在“评AJAX in action”前写个序言,没想到罗罗嗦嗦写了一堆,就单独作为一篇文章发了,呵呵!
还记得几个月前我曾经说只有当我闲到无聊的时候才会去学学ruby,我说谎了。在我毕设答辩最忙的时候,我在偷偷地看programming ruby。在我工作的间歇,我在偷偷地看agile web development with rails。在我有机会写一些小应用的时候,我拼命地抓住机会用ruby。人的好奇心真是奇怪的东西,当ruby和ruby on rails整个community如此火热的时候,我没有挡住这种诱惑。
尽管我现在写过的ruby代码行数可能不到500行,尽管我甚至没有用rails写过web应用,但是我必须承认我当时的想法错了。ruby是那种第一眼看上去并不怎么样,但越用就越爱不释手的东西,有些东西只有用过之后才会有感觉。
很难通过哪一个点说出ruby比其它语言(尤其是Java优秀),有的时候一些语法的改进似乎是那么的不起眼,但是当这些细小的东西组合在一起的时候,那么种感觉是Oh my god!,真是太爽了。记得当时JDK 5引入了一个for each循环的时候,感觉只是一点点语法的改动,但是真正写起代码时才感觉到真是太方便了,而ruby远不止是for each循环这样的语法糖,当好多这些组合在一起的时候,给人的感觉是写程序竟然会那么easy,那么有趣。
Closure、正则表达式、range所有这些特性给人的感觉是写ruby的程序就象写诗一样,它是比Java更高级的语言,或者抽象程度更高的语言。ruby is beautiful。
然而,当全世界都在喧嚷着Java将会被ruby或被动态脚本语言所替代时,我不得不保持我的理智,语言的美丽并不能保证它会被大众所接受。很多人认为ruby很简单,因为它是比Java更高级的语言,因此它比Java更容易。他们错了。也许从语法上来说,他们是对的,尽管ruby的语法更具有灵活性,但是这点语法灵活性不会给ruby带来太大的学习门槛。也许因为类型的动态性,使IDE的IntelliSense statement completion不能用会造成很多人的不适应。也许因为ruby的动态类型,使很多的重构功能都不能使用会给人带来不变。
然而真正的问题是,ruby的动态数据类型会给人带来思维上的转变,而这种转变使得ruby的使用者所遵守的协议变得松散。
1、ruby的duck type使我们的面向对象思维颠覆,也使我们需要重新思考所有现有设计模式,因为我们不需要interface了,这曾经是我们面向对象设计的精华。类的核心是对象的行为,而不是类型,这就是duck type的基本理论,只要你飞得象鸭子、叫得象鸭子,那你就是鸭子。无需求任何abstract class或interface的约束,我们就可以完成任何事情!这真是太爽了, 太灵活了, 但这种灵活性使开发者无需去遵守interface所规定的协议,因为我们根本不需要interface的类型规定。但是同时这意味着危险性,这真的是一把可怕的双刃剑。使用不当,我们的程序将变成一堆散沙,很难追踪它的线索和思路。
2、method missing,动态数据类型、class的随处扩展使程序的追踪和可读性变得很困难。我们经常查了半天搞不清楚别人的方法到底是在哪里定义的,方法的返回值到底在哪里。
DHH用ruby写出了rails这样的framework,Dave Thomas可以在两个多月里写出5万行的ruby代码。ruby的这些特性对于Dave Thomas来说只有power,而绝没有上述这些困挠。但是come on, 全世界有多少个Dave Thomas?现在大部分的程序员是希望早上九点上班、晚上五点回家,过着幸福的小日子的年轻人,让他们用ruby开发大项目的后果会是怎么的? 我不敢想象。
ruby会成为软件工匠最好的利器,会使很多人有超爽的感觉,但是它究竟能达到什么地步?能不能取代java? 我只能拭目以待了。
快毕业前被拉回实验室做了个spring讲座。其实好久没深入研究过spring了,好多概念都记不太清,又没充分时间做准备,就顺势粘贴了好多了的PPT,聊以凑数。
由于实验室里水平参差不齐,这个讲座真是难讲死了,听到后来好多人昏昏欲睡,不过也有几个人说挺有收获的。
反正这几天又没写blog,就把讲座的MP3和PPT拉出来凑凑数。感觉自己听自己的声音都怪怪的,不管了,是骡子是马,大家自己看吧。
下面是下载地址: http://ccnt.zju.edu.cn/lectures/xcc_spring.rar
终于搞完了答辩和毕业的一些杂事,可以安心坐下来写写blog了。想起半年多之前刚启动我的blog时,我的blog上只有人物八卦,那七篇TheServerSide人物谱也引起了好多人的兴趣,甚至gigix还请我去当记者,还有人把我当成了专挖人老底的狗仔队,真是逗死了。其实我唯一做的只是比较喜欢看或听那些人的采访和他们的blog,其中会发现很多很有趣而又在书上找不到的东西。事隔近一年,我决定重新祭起我的八卦刀,呵呵,支持头文字8,将八卦进行到底。
丹麦的哥本哈根,一个诞生软件天才的地方(北欧的其它的城市也同样诞生了好多天才)。20年前,一位天才在这里开发了自己的pascal编译器(后来这个编译器成了turbo pascal的前身),随后这位天才在美国开发出了turbo pascal、delphi、C#这样的重量级产品。快20年后的2003年,同样在丹麦的哥本哈根,历史似乎正要在重演。
然而,如果有人在2003年前看到这个小伙子,也许没人想到他会是天才。他的高中数学成绩考过F,他当过丹麦一个著名游戏网站的记者,他到了21岁才进入哥本哈根商学院读大学本科,他甚至在20岁前没有写过程序。这一切的一切似乎没有任何地方会将他与程序天才挂起钩来。
不过现在,他显得很兴奋,因为他刚接到遥远的大洋彼岸―美国的芝加哥,他两年多的合作伙伴37signals的电话。37signals是一家世界级的小公司(将世界级与小联系在一起真件有趣的事,到了2006年整个公司只有7个人),他们给他们的客户开发好多的Web应用,但是现在他们决定要拥有自己的产品了。这个产品的名字叫basecamp,这是一个小型的项目管理和交流软件,他们有两位很好的设计师,但是他们却只有一个程序员――来自大洋彼岸的还在哥本哈根商学院读大三的David Heinemeier Hansson。
David Heinemeier Hansson显得很兴奋,因为这是一个很有挑战性的项目,尽管他的PHP经验只有两年多,尽管他只在学校的毕设项目里用过J2EE,但是他显得很自信,他知道也许自己没有数学天赋,也许没有能力解决的难题,但是他对他的开发实力和理解力很自信,因为他知道他有另外一种能力――他能将简单的事情变得更简化。在使用了PHP的时候,他就开发了一套自己的framework,使PHP的开发变得更简单。
然而真正令David Heinemeier Hansson兴奋的原因却不仅在这里,他决定使用一种新的语言―ruby。事实上他对于ruby的经验只有几天,他只是觉得PHP的语法和设计令他无法忍受了,尽管PHP的开发速度很快,尽管PHP存在着好多的优点,但是语言的天生缺陷令他决定放弃PHP,他在朋友的怂恿下开始看ruby了,pragmatic programmer一直是他的偶象和目标,而由pragmatic programmer所写的programming ruby也确实令人兴奋,尽管受过些挫折,但是他觉得应该用ruby试试,于是他开始写一套以前用PHP写过的framework。
一周以后,事情的发展变得令人吃惊,Oops,ruby的开发效率实在是太惊人了,而且更重要的是的它的语法是如此的美丽优雅,David Heinemeier Hansson看着他自己一周之内开发出了以前用PHP要一个多月的东西,再加上它把J2EE开发中的学到的一些东西用上去,一切竟会如此简单。他兴奋地报告了美国总部:我要用ruby开发basecamp。与任何大型、官僚的公司与机构不同,37signals甚至没有做任何考虑就答应了。
两个月后,David Heinemeier Hansson开发出了自己的framework,再过了两个月,整个BaseCamp的产品竟然已经完成了。David Heinemeier Hansson看着自己写的代码兴奋异常,然而更兴奋的事还在后面,BaseCamp一发布就引起了轰动,全世界40多个国家的人值得开始使用,有人认为它是世界是最好的Web应用程序。
然而更令人轰动的则是架构BaseCamp的framework,David决定将这个framework从basecamp中剥离出,并取名叫ruby on rails,他觉得既然rails能让自己这么兴奋,开发的效率如此之高,那么rails也应该让别人感到快乐,也许会引起轰动。
2004年7月,rails终于发布了,David Heinemeier Hansson盯着下载的流量,第一周2000次,这是一个不错的成绩,第二周下载量翻了好几倍,一个月、两个月整个社区似乎都在为ruby on rails的诞生而兴奋!随后,他收到了他的偶像pragmatic programmer之一的Dave Thomas的信,Dave决定写一本关于ruby on rails的书,David Heinemeier Hansson也被邀请作为第二作者完成了其中的一章和很多脚注,当2005年这本agile web development with rails诞生后的几周,它登上amazon书店计算机书籍排行榜榜首。甚至反过来,rails也大大影响了ruby的地位,ruby让rails成功,rails使ruby书籍的销售量比2004年翻了10几倍,使ruby成为2005年最受观注的语言,amazing。
ruby on rails的成功让全世界都震惊了,很多人对它喜欢狂热,很多人怀疑,很多人恐慌,不管ruby on rails能走多远,不管ruby on rails会不会代替J2EE,他的创新精神和他的简化开发的思想都将永存。他被评为Google/O'Reilly's Best Hacker of '05,他甚至成为OSCON和好多会议的keynote speaker。
2005年10月,David在众多大牌(Tim O'Reilly, Martin Fowler, Sam Ruby, Bruce Perens, Jeffrey Zeldman, Richard Bird)的推荐下,拿到绿卡正式移居美国芝加哥,与37signals总部的同志们会合了。David是一个很帅的小伙子,下面是他的照片,在他在blog上还有很多他的还有他的女朋友的照片。在ruby on rails的网站上还有他所做讲座的视频。
Skype网站开设了一个“分享频道”(http://Share.Skype.com),这个频道不是产品功能介绍网页,而是教用户如何将Skype分享给好朋友:)实际上,这就是Skype的“在线宣传推广中心”。他们用了很动人的一句话:“分享好的,而非坏的。”(Sharing good, not sharing bad),这让人联想起那句老话:“好东西要和好朋友分享。”
在这个Skype的“在线宣传推广中心网页”里,有一个小地方很是贴心,“事实与数据”(Facts and figures)这里提供了RSS订阅,让用户订阅Skype的用户数据,包括下载的总次数和在线用户人数,这些数据每隔几分钟更新一次,小容现在写这个贴子时,Skype的在线用户数量是5,004,056人。
Skype的首页现在已经看不到用户数据了,实际上早些时候的Skype网站是在网站首页的标志的下方展示Skype的用户数据。通过Internet Archive(网络档案馆,介绍见本文末尾附注)的WayBack Machine这个频道,我们可以找出Skype的历史网页,并且找出Skype的用户数据。
小容在这几天忙里偷闲通过WayBack Machine整理了一部分时间点的Skype用户数据,从2003年到2005年,列出来在这里供大家参考。