手机浏览 RSS 2.0 订阅 膘叔的简单人生 , 腾讯云RDS购买 | 超便宜的Vultr , 注册 | 登陆
浏览模式: 标准 | 列表分类:PHP

iconv编码转换的原理

乔大姐的文章,几年前的了。。不过,还是值得一观,这也是我一朋友在问过,为什么存到文件里的内容不是UTF-8的时候引申出来的事了。

以下内容是乔大姐的转载,主要内容如下【我把C的那部分做了高亮,其余不变】,关于乔大姐是谁,可以从我转载的页面中获知,http://bbs.chinaunix.net/viewthread.php?tid=807393,本人不作介绍,以防被劈。

更广义的字符集转换:iconv
在先前的章节中,我们已见到了两组 mbs 与 wcs 互转的函式,第一组对于有「状态改变」的编码系统 mbs 无法做到字符串的状态控制,故不适合做该 mbs 的转换工作;而第二组则可以直接做字符串的状态控制,故使用范围就更广了。然而,这两组字符串转换函式在某些使用场合下都有很大的限制,广义而言,它们都 属于「字符集转换函式」,然而它们都直接与 I18N、locale 机制绑在一起,也就是说在使用它们之前,程序都必须设好正确的 locale 才行。故在以下的情况使用它们就会很不方便,甚至根本行不通:
•        如果程序中需要做 A 字集编码与 B 字集编码的转换时,不幸的是这两种字集 编码都不是目前程序所处的 locale 所采用的。若使用前述的 wcs 与 mbs 转换函式,唯一的办法只有先呼叫 setlocale(),将程序的 LC_CTYPE locale 先切换到使用字集编码 A 的 locale,把字符串 A 转成 wcs 字符串后,再呼叫一次 setlocale(),将程序的 locale 切换到使用字集编码 B 的 locale,最后才把 wcs 字符串转成字符串 B。万一 都找不到任何 locale 采用字集 A 或字集 B 时,则这一招就没有用了。
•        如果程序中需要同时做多种字集编码的转换时,则这些 wcs 与 mbs 的转换 函式就无法办到了。原因是一旦 setlocale() 设定好程序的 locale 之后,其影响是遍及整个程序每个部分,我们不可能做到在转换字集编码 A 时设好 locale A,又 ``同时'' 设好 locale B 来转字集编码 B。
因此,我们需要更广义的字集转换系统,一个可以与 locale 完全无关的转换系统,才能方便地达到上述的要求。故在 XPG2 标准中另外定义了一组全新的函式接口: iconv。事实上,在 glibc 中,表面上看来那些 wcs 与 mbs 转换函式与 iconv 不太一样,但它们底层的 wcs 与 mbs 转换工作却是由 iconv 来达成的。故 iconv 可以说是字集转换系统中最基底的函式接口。
iconv 字集转换系统只有三个函式,在很多系统中是宣告在 iconv.h 里头,使用上与一般在做档案读写的概念一样,先 ``开启''、之后 ``操作''、完毕后要 ``关闭'',这些函式包括:
•        iconv_t iconv_open(const char *TOENC, const char *FROMENC)
•        size_t iconv (iconv_t CD, const char **INBUF,
size_t *INBYTESLEFT, char **OUTBUF, size_t *OUTBYTESLEFT)
•        int iconv_close(iconv_t CD)
首先 iconv_open() 函式就是做 ``开启'' 动作,也就是当我们要将编码系统 A 转换到编码系统 B 时,必须先呼叫此函式,将 FROMENC 设成编码系统 A 的名字,同时将 TOENC 设成编码系统 B 的名字,这时此函式就会做类似档案开启的动作,传回一个代表此转换管道的数据结构 iconv_t 供后续使用。事实上,在系统的实作中真的是将 iconv_open() 当作 ``开启档案'' 来处理,故它会受到目前系统或同一行程中可开启档案数所限,如果系统或程序的其它部分已开启了太多的档案以至于逼近系统上限,则有可能这边的 iconv_open() 会失败。
如果 iconv_open() 开启失败时,它会传回 (iconv_t)-1 的值,同时设定 errno 全域变量,用以指出开启失败的原因。而开启失败的原因,不外乎就是已开启的档案数已超过上限、或系统内存不足、或系统本身无法做到编码系统 A 与 B 之间的转换。有兴趣的读者可以直接去阅读 info libc, * Character Set Handling:: 一节了解各 errno 的值与其所代表的意义。
第二个 iconv() 函式就是用来做实际编码系统转换工作的,它必须先呼叫过 iconv_open() 并取得 iconv_t 结构后才能工作。只要 iconv_open() 可以开启成功,则理论上它就可以进行转换工作,而且不论其来源编码与目标编码是 mbs 字符串或 wcs 字符串、或二者的混合互转,都没有关系。但要注意的是,如果其中有 wcs 字符串时,就算用来存放 wcs 字符串是使用 wchar_t * 形别,在传入此函式时仍然统一用 char * 来处理。
若要做字符串 A 转换成字符串 B 时,字符串 A 是经由 *INBUF 传入,而 *INBYTESLEFT 则传入数组 A 的长度,一律以字节数来计算。而转换的结果则由 *OUTBUF 传回,同样的 *OUTBYTESLEFT 则为 *OUTBUF 的长度。如果转换成功了,则 *INBUF 最后会设在存放字符串 A 的数组末尾,而 *INBYTESLEFT 会设为目前此数组还剩多少字节可以用。而 *OUTBUF 与 *OUTBYTESLEFT 也是一样。因此我们可以在相同的 A、B 数组中重复呼叫此函式。举个例子来说,如果转换过程中在 A 数组里头遇到了不合法的字符而无法进行转换时,则 *INBUF 与 *OUTBUF 就会停在无法转换的位置上,并将已转换成功的结果传回,则我们可以自行决定看是要跳过那个不合法的字符,继续转换剩余的部分,或做其它的特殊处理 ...等等。
在很多情况下有可能造成 iconv() 的转换失败,例如前面提到的在 A 数组中遇到不合法的字符,或者 A 中某个字符在 B 中找不到可对应的字。其中第二个情况是最有可能发生的,而目前 glibc-2.1.x 系统中遇到这种情况时就当做是转换失败,而不做进一步的处理。当然这并不是最好的处理方式,故未来可能会有所改变。而根据 info libc 的说明,如果转换失败了, iconv() 最后的传回值是 (size_t)-1,并设定 errno 的值说明失败的原因;如果转换成功,则传回值会是已成功转换的字符个数。但我们实际的测试结果,在 glibc-2.1.3 的系统下,如果转换成功后其传回值却永远是 0,与 info libc 上的描述不一样,这点有点奇怪,不晓得是不是目前 glibc 的 bug?
若遇到含「状态改变」的编码系统时,iconv() 也能正确工作,它能在转换的过程中随时记录、更新字符串目前的状态 (应该就是记录在 iconv_t CD 结构中),故就算是采「分期付款」方式将同一字符串切成数分一段段来转换,也不会出问题。但要注意的是,在第一次使用 iconv() 之前,必须先初始化一下 A、B 两字符串的状态,就好像我们在先前所提的 mbsrtowcs() 等函式一样,在使用前也必须先做好状能初始化,如此才能让后续可以正常工作。而对于 iconv() 而言,初始化的方式就是呼叫它时,*INBUF 与 *OUTBUF 都设为 NULL 即可。
最后一个 iconv_close() 函式,就是当整个转换结束后,用来做「关闭档案」用的。
底下我们就写了一个范例程序,用来说明 iconv 函式界面的使用方式:

C++代码
  1. #include <stdio.h>  
  2. #include <string.h>  
  3. #include <iconv.h>  
  4.   
  5. int main(int argc, char **argv)  
  6. {  
  7.   FILE *fin, *fout;  
  8.   char *encFrom, *encTo;  
  9.   char bufin[1024], bufout[1024], *sin, *sout;  
  10.   int mode, lenin, lenout, ret, nline;  
  11.   iconv_t c_pt;  
  12.   
  13.   if (argc != 5) {  
  14.     printf("Usage: a.out <encFrom> <encTo> <fin> <fout>\n");  
  15.     return 0;  
  16.   }  
  17.   encFrom = argv[1];  
  18.   encTo   = argv[2];  
  19.   if ((fin = fopen(argv[3], "rt")) == NULL) {  
  20.     printf("Cannot open file: %s\n", argv[3]);  
  21.     return -1;  
  22.   }  
  23.   if ((fout = fopen(argv[4], "wt")) == NULL) {  
  24.     printf("Cannot open file: %s\n", argv[4]);  
  25.     return -1;  
  26.   }  
  27.   
  28.   if ((c_pt = iconv_open(encTo, encFrom)) == (iconv_t)-1) {  
  29.     printf("iconv_open false: %s ==> %s\n", encFrom, encTo);  
  30.     return -1;  
  31.   }  
  32.   iconv(c_pt, NULL, NULL, NULL, NULL);  
  33.   
  34.   nline = 0;  
  35.   while (fgets(bufin, 1024, fin) != NULL) {  
  36.     nline ++;  
  37.     lenin  = strlen(bufin) + 1;  
  38.     lenout = 1024;  
  39.     sin    = bufin;  
  40.     sout   = bufout;  
  41.     ret = iconv(c_pt, &sin, &lenin, &sout, &lenout);  
  42.     printf("%s -> %s: %d: ret=%d, len_in=%d, len_out=%d\n",  
  43.            encFrom, encTo, nline, ret, lenin, lenout);  
  44.     if (ret == -1) {  
  45.       printf("stop at: %s\n", sin);  
  46.       break;  
  47.     }  
  48.     fprintf(fout, "%s", bufout);  
  49.   }  
  50.   iconv_close(c_pt);  
  51.   fclose(fin);  
  52.   fclose(fout);  
  53.   return 0;  
  54. }  

这个程序可以从命令列输入来源档案与目的档案的编码系统名称,将来源档案的内容转换成目的档案。各位读者可以注意到,在程序中我们完全没有做 locale 设定等相关动作,原因正是不需要,iconv 函式接口是与 locale 完全无关的。同时,我们的程序只适合拿来做两个 mbs 编码系统间的转换,不适合拿来做其中一个有 wcs 的转换,原因是程序中我们没有特别为 wcs 的情况使用 wchar_t * 数组,同时记得我们以前也提过,wcs 字符串是不能拿来做档案输出的,而我们的程序目前正是直接做档案编码转换的工作。
这样的转码程序倒底「耐不耐用」呢?有兴趣的读者可以跟着我们一起来测试。由于先前我们已介绍过了如何在您的 GNU/Linux 系统中安装 zh_TW.Big5 与 zh_CN.GB2312 locale 环境,如果您都已安装无误的话,理论上您的系统的 Big5 与 GB2312 的 gconv 系统都有不成问题了 (在 glibc 中,gconv 系统可以说是 iconv 的心脏,我们会在下一小节详细说明),故我们就直接以 Big5 与 GB2312 两个编码系统互转为例子,测试一下我们的程序。首先,请先准备一个叫 f-big5 的档案,内容用 Big5 编码打入这样一行的内容:
我是研究生
然后,将我们的范例程序编译好,假设程序执行文件名就是 a.out,接着执行:
a.out BIG5 GB2312 f-big5 output
在 glibc-2.1.x 的系统下,应该可以正确转换,这时您应该会见到如下的程序输出:
BIG5 -> GB2312: 1: ret=0, len_in=0, len_out=1012
其中 ret=0 就表示转换成功,然后,您可以再开一个可以看 GB2312 编码的 terminal 来看 output 档的内容,您就会见到原来 f-big5 档的内容已转换成 GB2312 码了。
看起来似乎很完美,我们再进一步测试。现在请在 f-big5 档中再加一些内容,像这样:
我是研究生
目前在正在做研究测试
按照上头的指令再执行一次,我们得到如下的结果:
BIG5 -> GB2312: 1: ret=0, len_in=0, len_out=1012
BIG5 -> GB2312: 2: ret=-1, len_in=6, len_out=1008
stop at: 测试
第一行没有问题,但第二行失败了,程序指出第二行最后两个字 ``测试'' 无法做转换,为什么会这样呢?
要探讨其原因,就要更深入 iconv 的编码系统转换原理了。我们在之前已多次提过,在做 mbs 与 wcs 之间转换时,其实就是将 mbs 转换成系统的基底字集,而 iconv 的转换原理也是这般。事实上在 glibc 系统中,它在做编码系统 A 与 B 之间的转换时,有两种途径可选择:一是如果 iconv 内部已内建了 A 与 B 的对应表格时,就采用此对应表格来转换,而这也是最可信赖的转换方式。万一它找不到适当的对应表格时,它则会先将 A 转换成基底字集,再由基底字集转换成 B。也就是系统的基底字集在这里扮演了中间媒介的角色。
要注意的是,在其它的 UNIX 系统中,它们可能只提供第一种途径供转换,而 glibc 提供第二种途径的理由是,这样可以确保任意两个字集编码都有机会进行转换,只要这两个字集编码都同时被系统所支持。而且,由于 glibc 所选择的「基底字集」理论上已包含了目前世界上所有正在使用的计算机字集,故使用基底字集做为中间媒介是合理而且可行的。
但各种字集编码的问题是很复杂的玩意儿,有时候这一套也有出槌的时候。就像前面见到的 Big5 的 ``测试'' 无法转成 GB2312 一样,我们再做以下的实验,就会知道原因了。因为 glibc 是以 UCS4 做为其系统的基底字集,而 UCS4 的 mbs 形式为 UTF8 编码系统,故我们现在准备两个档案,一个是 f-big5,内含 Big5 码的 ``测试'' 两字,另一个是 f-gb,内含 GB2312 码的 ``测试'' 两字。现在我们用上头那个范例程序将这两个档案都转成 UTF8 编码:
a.out BIG5 UTF8 f-big5 output1
a.out GB2312 UTF8 f-gb output2
这两个都可以转换成功。然后,我们用 diff 程序比较一下 output1 与 output2 的内容,发现它们的内容不一样!
这就是无法转换成功的原因!因为在中文里头意义一样的两个字,在 Big5 与 GB2312 里头分别被对应到不同的 UCS4 编码里头去了。为什么会如此?这牵涉到更深刻的 UCS4 编码规则与其它编码系统的对应问题,在这里我们就先略过了,以后若有机会的话再回来讨论。现在的问题是,在 glibc 中,如果 A 与 B 的某些字分别被对应到基底字集中不同的编码中,就有可能会无法转换成功。而这往往是很有可能发生的,特别是 glibc 直接采用 UCS4 做为其基底字集时。
而且,在回顾一下我们前面谈的,在不同的 UNIX 系统下,其 iconv 底层的运作模式可能都不太一样,这就造成了更严重的问题:就算系统同时支持 A 与 B 两种编码,我们仍不能保证可以将 A 转成 B。或者,该系统中可以将 A 转成 C,也可以将 C 转成 B,但也不能保证可以将 A 直接 (或间接) 转成 B。
这的确是另人气馁的问题,现在也许读者就会问了,那 iconv 有什么用?我们的认为是,它在小字集与大字集编码间的转换是有用的。例如 Big5 与 GB2312 分别是 UTF8 的子字集,则我们可以很放心地使用 iconv 将 Big5 或 GB2312 转成 UTF8,或反方向地转换回来 (当然,前提是原来的 UTF8 字符串中没有包含 Big5 或 GB2312 所没有的字)。又或者 Big5 是 GBK 的一个子字集,我们一样也可以利用它来做转换。
但如果要直接用 iconv 来做 Big5 与 GB2312 的转换的话,唯一个可靠方法就是在 iconv 系统中加入一个 Big5 与 GB2312 直接互转的表格,并且希望此表格可以成系统标准的一部分 (至少要是 glibc 标准的一部分),如此我们的程序这样写才会有用 (至少在 glibc 的系统下)。因此,现在我们就进入下一小节,来看看 glibc 的 iconv 的核心部分,做为本章的一个结束。

Tags: iconv, mb_string

PHP里simplexml_load_string函数一个容易犯晕的地方

以下内容均来自老王。说实话,simplexml的这些问题我以前遇到过,而且还是非常强烈的遇到了。
那时候是08年的事了,项目中与SOAP在交互,返回的都是XML,所以就直接采用了simplexml_load_string了。这时候就会遇到这种问题,比如我拉一个列表,但会有三种情况:1、没数据,2、1条数据,3、正常数据
1、没数据的时候,返回一个空的simplexml Object,
2、一条数据的时候,返回的也是一个simplexml Object,结果集成了这个object的元素
3、数据列表,这时候返回的却是一个Array,每一个元素是simplexml Object,等同于2。
后来我是没办法,就多写了一个函数判断,如果是object,并且不为空,直接把object强制转为array,因为,如果是空的simplexml Object,强制转的话,还是Object,转不了。。。

当时还有一个可恨的事情,我以前可能介绍过,xml在返回的时候,他的形式可能是这样的<test-name>gouki</test-name>,在返回这样的形式时,生成object时,却是test_name,在没有print_r出来时,我死活搞不定。。。后来才发现这个问题,郁闷啊

好了,说了半天,该说说老王的内容了:
这个问题遇到好几次了,今天翻看以前代码的时候看到,便记下来,先用一段代码重现一下问题:

PHP代码
  1. <pre>  
  2. <?php  
  3. $string = <<<EOF  
  4. <data>  
  5. <foo><bar>hello</bar></foo>  
  6. <foo><bar>world</bar></foo>  
  7. </data>  
  8. EOF;  
  9.   
  10. $data = simplexml_load_string($string);  
  11.   
  12. print_r($data);  
  13. print_r($data->foo);  
  14. ?>  
  15. </pre>   

乍一看,结果很让人费解:

XML/HTML代码
  1. SimpleXMLElement Object  
  2. (  
  3.     [foo] => Array  
  4.         (  
  5.             [0] => SimpleXMLElement Object  
  6.                 (  
  7.                     [bar] => hello  
  8.                 )  
  9.             [1] => SimpleXMLElement Object  
  10.                 (  
  11.                     [bar] => world  
  12.                 )  
  13.         )  
  14. )  
  15. SimpleXMLElement Object  
  16. (  
  17.     [bar] => hello  
  18. )  

明明print_r显示foo是一个有两个bar元素的数组,但是最后却仅仅显示了一个bar元素!

原因其实很简单,在如上所示simplexml_load_string的结果里,foo并不是数组,而是一个迭代对象!

可以这样确认:

PHP代码
  1. foreach ($data->foo as $v) print_r($v);  
  2. foreach ($data->children() as $v) print_r($v);  
看来,print_r或者var_dump之类的表象并不完全可信,自己多留心吧。
原文来自:http://hi.baidu.com/thinkinginlamp/blog/item/9dbb15ce7a621632b700c86a.html

Tags: simplexml, 老王

轻轻松松统计代码行数

前段时间有人问,如何统计自己的项目一共写了多少行代码,当时我说的是,遍历目录,然后用files函数读取每个要统计的文件【根据后缀名过滤】,并去掉空行,最后就把数字累加起来就可以了。

后来,他是google到一个专门统计代码的程序后搞定了。

刚才,看到cnitblog,有人这样写道:

XML/HTML代码
  1. find . -name "*.代码文件后缀" | xargs wc -l  
当然,这是在linux下面。。如果有SVN,还需要加上:
XML/HTML代码
  1. $find . -name "*.代码文件后缀" | grep -v "./svn" | xargs wc -l  
一切都是这么的自然。。。

linux下面的方法来自:http://www.cnblogs.com/maczpc/archive/2010/01/28/1658371.html

不同浏览器对回车提交表单的处理办法

不知道你是否注意过,在firefox或者Chrome下面,即使没有定义input type=submit,只要点击任何一个button,都能触发form 的submit事件,这是为什么呢?
看看这篇文章吧,介绍的有点详细,把它留下来是因为我一向属于知其然不知其所以然,只知道问题所在,和如何解决,从来不深究它的内涵,这也是我为什么水平上不去的原因。。。所以,不要学我,还是看完这篇文章吧。。。虽然不一定详细,但毕竟知其一部分所以然了。

原文来自:不同浏 览器对回车提交表单的处理办法
内容如下:

  在浏览器中填写表单的时,可以直接在“文本框”中敲击“Enter”来提交表单,很是方便。条件是:

  • 在IE和Firefox下

1、<form>属性的“action”字段必填;

2、有一个type="submit"的“input”。

  • 在Chrome和Safari下 满足第一条即可。(注释1)

  所以,如果要控制提交行为的话(比如,提交前检查必填项是否已填),可以在type="submit"的“input”后添加 Javascript“onXXX(比如onClick)”事件。而如果需要用异步交互来检测相关字段,则无效了。因为return false是作为onreadystate=的子函数,并不能控制全局。这时就需要用到<form>的 “onsubmit”属性(注释2)。比如:

HTML

<form name="form1" id="form1" action="act.php" method="post" onsubmit="myfn();return false;">
  •  myfn()是自己定义的函数:无论通过什么办法 (比如敲击“enter”或点击type="submit"(而不是type="button")的“input”按钮)提交,都会触发这个这个函数。于 是,大可不必再在type="submit"的“input”后添加Javascript事件“onXXX”了。直接在“onsubmit”中控制,更直 观与统一,除非有意控制提交方式(是onClick还是什么)。
  • return false位于末尾,表示不提交本表单。提交表单的字句document.form1.submit()可以作为myfn()的一个选择条件的分支。

总结:

  这样,<form>有“action”和“onsubmit”属性,有type="submit"的“input”的提交按钮后, 就可以在任何浏览器下(不行吗?请给我反馈)实现使用“Enter”键和鼠标都可以对表单的相关字段进行异步(Ajax)和同步(单纯的 Javascript)检查并予以提交。

其他:

1、如果使用type="button"来异步检测相关字段呢?

  这样,这个“input”必须得有一个事件触发器。在IE和FF下,无法直接在文本框中按下“Enter”来提交表单。如果要的话,需要使用额 外的函数来监听用户按下了什么,给出一个怎样的反映(注释3)。更糟糕的是,在Ch和Sa下,会忽略是否有type="submit"按钮,而直接根据 “action”属性来提交表单,如此以来,便无法实现预期的检测,可能还有更麻烦的后果。

2、如果不写“action”属性,直接异步提交表单呢?

  这样,通过异步的方式提交表单,而不仅仅是检测,这个属性则没有必要。而且还是多余。假如这样能提供更好的用户体验,何乐而不为呢?呵呵。

【参考】

1 在IE8,FF,Ch中测试有效。Opera和Sa没有测试。参考了这篇文章(http://tieba.baidu.com/f?kz=669494938

2 这篇文章(http://blog.csdn.net/xiewhenwe/archive/2009/11/08/4785469.aspx) 给了我借鉴

3 参考我以前的文章(http://www.cnblogs.com/lamper/archive/2010/02/06/1665117.html

 

Tags: form, firefox, chrome, ie, submit

PHP 中如何处理时区

小记录,一般情况而言,默认安装好的lamp中,时间都是标准GMT时间,基本上,我们中国时间都应该是+8才对,但很多人往往不设置。

因此,为了时间正确,建议你还是设置一下默认时区,否则出现这8小时的误差也很郁闷的。

那么如何知道自己的时区是否正确呢?在不知道一些系统函数的情况下,直接输出 date('Y-m-d H:i:s', now()) ,就可以了解了。。

那么,我在知道这样的问题后,怎么能够知道当前设定的时区是哪里呢?这时可以通过:date_default_timezone_get()函数来获取,理所当然,设置时区就是date_default_timezone_set()了。

手册上也说明,在PHP 5.1之后,如果时区不正确,那每次在调用Date或者time函数时,都会有一个warning 或者 notice出现:

XML/HTML代码
  1. Note: Since PHP 5.1.0 (when the date/time functions were rewritten), every call to a date/time function will generate a E_NOTICE if the timezone isn't valid, and/or a E_STRICT message if using the system settings or the TZ environment variable.   

当然,如果你觉得这样设置很麻烦,你可以更改php.ini文件中的 ;date.timezone = ,添加自己需要的时区,例如 date.timezone = Asia/Chongqing,然后重启服务器就可以了。PHP支持的 timezone 的字符串在手册中能够查到。

手册中的:Appendix J. List of Supported Timezones,可以查看所有支持的时区。

Note: The latest version of the timezone database can be installed via PECL's timezonedb. For Windows users, a pre-compiled DLL can be downloaded from the PECL4Win site: php_timezonedb.dll.

Tags: 时区