存档

2011年12月 的存档

Common Lisp为Babel添加GBK支持

十二月 22nd, {2011 1 条评论 11,231 人阅读过  

前段时间在学Common Lisp,接触新语言我干的第一件事一般是通过HTTP抓取某个web页面,因为对网络编程比较感兴趣,而且平时写的程序也多是网络相关的,所以比较关心这方面的用法,于是用IOLib写了一个简单的小程序尝试着抓取了几个大门户网站的页面代码,关于IOLib的基本用法改天我也写篇日志记录一下,也算是和大家分享一下,毕竟能找到的中文资料比较少,而且文档也不是特别全,就像这篇文章里面说的:”Such is the nature of open source documentation. “,于是大多数的用法都得通过hack源代码来弄明白,言归正传,在写这个小程序的时候我遇到了一些问题,关于字符编码的问题,下面慢慢道来吧。

IOLib的receive-from方法是通过调用recvfrom来进行的,这种带缓存的接收方式很符合其它语言进行编程的套路,但它所接收到的buffer数据是需要存储在一个vector ‘(unsigned-byte 8)中的,虽然字符串在本质上也是向量,但对于字符串的很多操作不能直接应用于vector,而且vector中的元素都是每个字符的unicode编码,而不是确定的字符,于是便需要进行转换,最初我使用的办法:

(map 'string #'code-char buffer)

这个方法简单粗暴,直接在对一个vector上的每一个元素应用code-char函数,然后将输出映射为string,首先需要了解code-char/char-code这对函数的用法,大多数的Common Lisp实现都使用Unicode字符编码,当然,Unicode向下兼容ASCII和ISO-8859-1,所以这一对函数的作用就是在字符和它的Unicode码之间相互转换,如:

CL-USER> (char-code #\中)
20013
CL-USER> (code-char 20013)
#\U4E2D
CL-USER> (format t "~d" #x4E2D)
20013
CL-USER> (format t "~a" #\U4E2D)

字符“中”的Unicode码是20013,用code-char将其转换成字符之后,REPL并不是将可视的字符直接显示出来,而是显示#\U4E2D,表示这是一个Unicode字符,后面的4E2D是该字符的16进程Unicode码,换成10进制也便是20013,将该这了符直接打印出的话便可以得到字符“中”了。

当抓取的页面编码不是Unicode(一般情况下都不是的),直接使用这种办法转换成字符串就会导致除ASCII字符以外的其它字符乱码。

Google搜索下在stackoverflow上发现这个问题的解决办法,babelflexi-streams的octets-to-string函数可以实现由vector向string的转换,而vector必须是(make-array element-length :element-type ‘(unsigned-byte 8))类型,在使用该函数在抓取新浪/网易等门户网站的数据的时候会抛出异常:

代码:

(babel:octets-to-string buffer :encoding :utf-8)

异常:

Illegal :UTF-8 character starting at position 437.
 [Condition of type BABEL-ENCODINGS:INVALID-UTF8-CONTINUATION-BYTE]

起初我对这个问题百思不得其解,后来在水木还有StackOverflow上提出这个问题之后,我才发现原来我的问题是那么弱,也非常感谢大家的热心解答,确实如字面意思,有非法的UTF-8字符,也就是说字符串并非使用UTF-8编码的,而我习惯性地以为这些大网站理应都是UTF-8编码才对,谁知跟我想的正好想反,他们大多都不是UTF-8,有GBK甚至有GB2312,关于GBK和UTF-8我在推特上提起过这个问题,有推友说大网站仍在延用GB2312是为了节省流量,这个我可以接受,也有推友说使用GBK是因为它比UTF-8可能多几个字符,这个我也可以接受,但有的推友却跑过来说GB*是国标,用UTF-8什么的那是崇洋媚外,这个我表示无论如何也接受不了,互联网是没有国界的存在,在这样一个大的开放平台上你搞个国标有个毛用,当然我们的互联网也不开放是真的。

GB2312是GBK的子集,GBK向下兼容GB2312,使用GB2312就意味着能使用的字符数要远小于GBK,这两者相对于UTF-8对于中文的优点就是它们对于汉字的编码是两个字节的,而UTF-8是三个字节的,当然,我说的仅是指汉字,UTF-8是变长编码的,它也支持2字节和4字节甚至更多。

babel是不支持GBK的,sbcl的sb-ext:octets-to-string和Closure的ccl:octets-to-string都支持GBK,我个人比较较真,想给babel写个GBK的patch,以便可以实现平台无关的转码。

可能是我搜索技术不行,搜到的关于GBK的中文正式文档不多,只在维基百科上找到了关于GBK的介绍,虽然没有涉及到具体的GBK到Unicode转码规则,但它提供的关于GBK的介绍也是很有帮助的,发现原来GBK与Unicode的转换并不像UTF-8与Unicode之间的转换那样有固定的规则,而是需要通过查表实现,于是乎我在网上下载到了一个包含所有GBK符号的码表,排列顺序也是按GBK的相关规则来的,解码的过程我参考了这篇文章:从GBK到Unicode的中文字符映射,编码的话就是反其道而行之,原理很简单,参考维基百科上的两个图就可以推算出来。

对于lisp而言,宏是这门语言一个很大的亮点,通过宏甚至可以定制语言的特性,babel写义了几个宏作为编解码的接口,最重要的有下面四个:

define-octet-counter
define-code-point-counter
define-encoder
define-decoder

前两个宏首先对要进行编/解码的数据进行预处理,分别计算出编/解码后的数据所占用的长度,从而预先分配存储空间。后两个宏分别定义对应的编码和解码算法,这两个宏具体的使用方法都是参考了enc-unicode.lisp这个文件里面关于UTF-8的编解码代码。

添加了GBK支持的babel我推送到github上去了(https://github.com/levin108/babel/),需要的同学可以下载,有什么写得不对的或者不好的欢迎提出来,我是新手需要学习。

分类: Lisp 标签: , ,

使用ASDF构建Common Lisp程序包

十二月 13th, {2011 7 条评论 13,963 人阅读过  

在切入正题之前先写点不相关的,工作确定之后便开始忙论文的事,忙里偷闲总想搞点什么以做娱乐,不得不说,腾讯面试官说过的要精通两到三门不同的语言我印象很深刻,自己也想尝试一下新东西,VIM让我审美疲劳了,也想尝试一下Emacs,机缘巧合由田春老师翻译的《实用Common Lisp编程》刚上市不久,Emacs和Lisp也有不少渊源,再加上Lisp作为一门生命持久的元老级别的语言,至今仍然能倍受广大黑客的推崇,我相信它一定有学习的价值,而且Hadoop的MapReduce据说也是受Lisp的map和reduce函数的启发而来,相信对于Lisp的学习肯定不会是浪费时间,尽管将来工作中应用Lisp的机会可能很少,但深入学习的话肯定会对自己有一定的启发和帮助。

于是几乎同一时间我开始尝试使用Emacs并在卓越上订购了中文版的《实用Common Lisp编程》,抽空阅读尝试。总起来说这本书是非常不错的,几乎是面面俱到,但有些我认为也很有用的宏如defstruct,deftype,check-type等书中没有给出相关介绍,另外关于cl的package书中有一章节专门讲了定义的规则,但对于package的管理及安装并没有提及,我个人觉得如果是practical编程的话提一下cl中重要的ASDF包管理工具还是很有必要的,既然书中没有提到就得自己通过其它的渠道去了解学习,这方面中文的资料相对较少,大多数的资料都是在外文网站上查到的,当然也包括到stackoverflow上的提问。

ASDF全称是Another System Definition Facility,asdf这个组合很有意思,正好和左手的键盘基本按键重合,当初还以为是作者很有个性地起了这个随意的名字呢。我个人的理解是ASDF是类似于automake或者cmake的工具,提供一种程序包的管理方法和工具,因此也没什么复杂的地方,只不过有一些基本的规则需要遵守,了解了就没什么了。

程序包中一般会有一个.asd文件,该文件定义了程序包中源码文件的组织方式及依赖关系,类似于cmake中的CMakelists.txt,当然该文的编码方式是使用lisp风格的。

1. asdf工具的安装配置。
目前存在的common lisp实现有很多,在这篇文章中有介绍:Common Lisp Implementations: A Survey,免费的common lisp实现中性能比较好使用也比较简单的应该属sbcl了,《practical common lisp》也推荐了sbcl,作为一个新手我第一选择当然也选了sbcl,在sbcl中已经集成了asdf工具,无需再手动安装,但安装方法也很简单,可以参考ASDF Manual,该手册对asdf有详细的介绍。

2. asdf包的编译加载。
asdf是一个工具集,可以对包进行各种操作,其中包括编译,加载等。asdf对于包的编译和加载等操作都需要基于.asd文件,也就是说编译某个包asdf需要先找到该包对应的.asd文件,该文件一般是存放在源码根目录中的,asdf对于该文件的寻址有两种方式,new style和old style,下面分别简单介绍下这两种方式:

old style是为了兼容旧版本的程序的,目前已经不推荐新程序使用,其方法是将程序包的.asd文件所存在的路径添加到asdf的*central-registry*变量中,可使用如下语句完成该操作:

(push "/home/levin/lisp/spider/" asdf:*central-registry*)

上述操作便可将/home/levin/lisp/spider/(注意结尾的’/')这个路径添加到*central-registry*变量中,这样asdf便可对该包进行寻址,但在REPL中执行的该操作只对当前的会话有效的,REPL重启后需要重新添加路径到该变量中,未免有些繁琐,可以将该语句添加至common lisp实现的启动文件中,如sbcl即为.sbclrc这个文件,这样在REPL启动之后该路径便会自动添加到*central-registry*变量中。

new style是ASDF2所提倡使用的方法,其方法是在~/.config/中创建common-lisp目录用于存放相关的配置文件,和该主题相关的配置文件需要创建一个名为source-registry.conf.d的目录,在该目录下可以创建文件名任意的文件,将下面的语句添加至该文件中即可,文件如:

~/.config/common-lisp/source-registry.conf.d/01-spider-source.conf

(:directory "/home/lisp/lisp/spider/")

如上表示在/home/lisp/lisp/spider/这个目录中查找.asd文件,也可以将:directory替换成:tree,使asdf在目录中递归寻找.asd文件:

(:tree "/home/lisp/lisp/spider/")

完成对asdf包的寻址操作之后,便可以使用asdf对程序包进行编译加载,编译和加载包分别可使用:

CL-USER> (asdf:compile-system :spider)
CL-USER> (asdf:load-system :spider)

也可以使用asdf的operate对包进行编译和加载:

CL-USER> (asdf:oos 'asdf:compile-op :spider)
CL-USER> (asdf:oos 'asdf:load-op :spider)

对于一个未经编译完成的包在编译完成会自动加载,同样,未编译完成的包在加载时也会自动先进行编译。

3. asdf系统的构建:

之前提到的.asd文件是asdf的很重要的文件,该文件最简单的形式如下:

(defpackage :spider-system
  (:use :cl :asdf))
(in-package :spider-system)
 
(defsystem spider
  :name "spider"
  :author "levin li"
  :version "0.0.1"
  :license "MIT"
  :description "A spider program."
  :depends-on (:iolib)
  :components ((:file "package")
               (:file "spider" :depends-on ("package"))))

最开头的包定义是为了防止system名与其它包冲突,定义的包中包含一些基本信息,:depends-on声明了该程序将要依赖的包的,:components定义了包中的组件,即源代码,上述的源码包中只有两个文件,package.lisp和spider.lisp,package.lisp是定义一个cl package,可以理解为其它语言中的namespace,package.lisp定义了一个package,之后的程序如spider.lisp可能都会包含在该包中,因此:depends-on(“package”)这句话就非常重要,它会在编译spider.lisp之前先加载已经编译的好的package.lisp,若未声明spider依赖package,则在一次编译完成后REPL重启并对spider.lisp进行修改,而package.lisp未加改动,则asdf只会编译修改过的spider.lisp,这时候系统会提示spider包未找到,因为asdf发现package.lisp的目标文件是最新的,默认不会对其进行编译和加载,这也是我刚开始遇到的问题之一,在认真读了ASDF Manual后才解决了这个问题,关于该文件的一些详细的定义可以参考ASDF Manual。

package.lisp的内容如下:

(in-package :cl-user)
 
(defpackage :spider
  (:use :cl :iolib)
  (:export :send-request
           :test))

其中定义了包的名称,包的依赖的其它包,及要导出的符号等,关于package的更多介绍还是参考《practical common lisp》等资料。而spider.lisp是代码文件,开头需要有一行:

(in-package :spider)

表明当前程序包在spider包中定义,继而可以在spider.lisp中编写其它的逻辑代码,定义函数,宏或变量,所有的这些定义都被包含在包spider中,其中只有在package.lisp中声明为:export的符号才可以被其它包所引用,如上定义,该包中只有send-request和test两个函数可以被其它外部包引用。

4. asdf包的安装

绝大多数的common lisp包都是使用asdf组织的,可以使用asdf-install工具安装软件包,asdf-install使用之前需要先加载:

CL-USER> (asdf:oos 'asdf:load-op :asdf-install)

之后便可以使用asdf-install安装软件包,一般而言有三种方式:

1. 通过包的名字进行安装:

CL-USER> (asdf-install:install "iolib")

这时asdf-install会自动从cliki.net上下载可用的包并安装,http://www.cliki.net/asdf-install这个页面列出了在线可用的软件包的列表。

2. 通过包的url进行安装:

CL-USER> (asdf-install:install "http://weitz.de/files/cl-ppcre.tar.gz")

3. 通过包的本地路径进行安装:
CL-USER> (asdf-install:install “/home/levin/lisp/iolib.tar.gz”)

更加详细的安装方法可以参考asdf-install turorial.

分类: Lisp 标签: , ,