前段时间在学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上发现这个问题的解决办法,babel和flexi-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/),需要的同学可以下载,有什么写得不对的或者不好的欢迎提出来,我是新手需要学习。