关于在C程序中处理UTF-8文本的方法详解
UTF-8
互联网的普及,强烈要求出现一种统一的编码方式.UTF-8就是在互联网上使用最广的一种unicode的实现方式.其他实现方式还包括UTF-16和UTF-32,不过在互联网上基本不用.
重复一遍,这里的关系是,UTF-8是Unicode的实现方式之一.
UTF-8最大的一个特点,就是它是一种变长的编码方式.它可以使用1~6个字节表示一个符号,根据不同的符号而变化字节长度.
UTF-8的编码规则
UTF-8的编码规则很简单,只有两条:
1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码.因此对于英语字母,UTF-8编码和ASCII码是相同的.
2)对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10.剩下的没有提及的二进制位,全部为这个符号的unicode码.
如果你对UTF-8编码不是非常了解,就不要试图在C程序中徒手处理UTF-8文本。如果你对UTF-8非常了解,就更没必要这样做。找一个提供了UTF-8文本处理功能并且可以跨平台运行的C库来做这件事吧!
GLib就是这样的库。
从问题出发
下面的这段文本是UTF-8编码的(我之所以如此确定,是因为我用的是Linux系统,系统默认的文本编码是UTF-8):
我的C81每天都在口袋里 @
我需要在C程序中读入这些文本。在读到'@'字符时,我需要判定'@'左侧与之处于同一行的文本是否都是空白字符。
简单起见,我忽略了文件读取的过程,将上述文本表示为C字符串:
gchar*demo_text= "我的C81每天都在口袋里\n" "@";
注:在GLib中,gchar就是char,即typedefchargchar;
下文,当我说『demo_text字符串』时,指的是以demo_text指针的值为基地址的strlen(demo_text)+1个字节的内存空间,这是C语言字符串的基本常识。
UTF-8文本长度与字符定位
为了模拟程序读到'@'字符这一时刻,我需要用一个char*类型的指针对demo_text字符串中的'@'字符进行定位。
'@'字符在demo_text的末尾。我需要一个偏移距离,而这个偏移距离就是demo_text字串在UTF-8编码层次上的长度,通过这个偏移距离,我可以从demo_text字符串的基地址跳到'@'字符的基地址。
GLib提供了g_utf8_strlen函数计算UTF-8字符串长度,因此我可以得到从demo_text字串的基地址到'@'字符基地址的偏移距离:
glongoffset=g_utf8_strlen(demo_text,-1);
结果是38,恰好是demo_text字符串在UTF-8编码层次上的长度(不含字串结尾的null字符,亦即'\0'字符)。
g_utf8_strlen的原型如下:
glongg_utf8_strlen(constgchar*p,gssizemax);
注:glong即long,而gssize即signedlong。
g_utf8_strlen第二个参数max的设定规则如下:
- 如果它是负数,那么就假定字符串是以null结尾的(这是C字符串常识),然后统计UTF-8字符的个数。
- 如果它为0,就是不检测字符串长度……这个值纯粹是出来打酱油的。
- 如果它为正数,表示的是字节数。g_utf8_strlen会按照字节数从字符串中截取字节,然后再统计所截取的字节对应的UTF-8字符的个数。
有了偏移距离,就可以在demo_text中定位'@'字符了,即:
gchar*tail=g_utf8_offset_to_pointer(demo_text,offset-1);
此时tail的值便是'@'字符的基地址。
在UTF-8文本中游走
现在已经获得了'@'的位置,接下来就是从这个位置开始向左(也就是逆序)遍历demo_text字符串的其它字符。GLib为此提供了g_utf8_prev_char函数:
gchar*g_utf8_prev_char(constgchar*str,constgchar*p);
借助g_utf8_prev_char函数可以从str中获得p之前的一个UTF-8字符的基地址(p是当前UTF-8字符的基地址)。如果p与str相同,即p已经指向了字符串的基地址,那么g_utf8_find_prev_char会返回NULL。
对于本文要解决的问题而言,利用这个函数,可以写出从demo_text中的'@'字符所在位置开始逆序遍历'@'之前的所有UTF-8字符的过程:
glongoffset=g_utf8_strlen(demo_text,-1); gchar*viewer=g_utf8_offset_to_pointer(demo_text,offset-1); while(1){ viewer=g_utf8_prev_char(viewer); if(viewer!=demo_text){ /*dosomthinghere*/ }else{ break; } }
GLib还提供了一个g_utf8_next_char,它可以返回当前位置的下一个UTF-8字符的基地址。
提取UTF-8字符
虽然借助g_utf8_prev_char与g_utf8_next_char可以让指针在UTF-8文本中走动,但是只能将一个指针定位到某个UTF-8字符的基地址,如果我们想得到这个UTF-8字符,就不是那么容易了。
例如
viewer=g_utf8_prev_char(viewer);
此时,虽然可以将viewer向前移动一个UTF-8字符宽度的距离,到达了一个新的UTF-8字符的基地址,但是如果我想将这个新的UTF-8字符打印出来,像下面这样做肯定是不行的:
g_print("%s",viewer);
注:g_print函数与C标准库中的printf函数功能基本等价,只不过g_print可以借助g_set_print_handler函数实现输出的『重定向』。
因为g_print要通过viewer打印单个UTF-8字符,前提是这个UTF-8字符之后需要有个'\0',这样就是将一个UTF-8字符作为一个普通的C字符串打印了出来。这个UTF-8字符后面不可能有'\0',除非它是demo_text字符串中的最后一个字符。
要解决这个问题,只能是将viewer所指向的UTF-8字符相应的字节数据提取出来,放到一个字符数组或在堆中为其创建存储空间,然后再打印这个字符数组或堆空间中的数据。例如:
gchar*new_viewer=g_utf8_next_char(viewer); sizt_tn=new_viewer-viewer; gchar*utf8_char=malloc(n+1); memcpy(utf8_char,viewer,n); utf8_char[n]='\0'; g_print("%s",utf8_char); free(utf8_char);
这样显然太繁琐了。不过,这意味着我们应该写一个函数专门做这件事。这个函数可取名为get_utf8_char,定义如下:
staticgchar*get_utf8_char(constgchar*base){ gchar*new_base=g_utf8_next_char(base); gsizen=new_base-base; gchar*utf8_char=g_memdup(base,(n+1)); utf8_char[n]='\0'; returnutf8_char; }
借助这个函数,就可以实现从demo_text的'@'所在位置开始,逆序打印'@'之前的所有UTF-8字符:
glongoffset=g_utf8_strlen(demo_text,-1); gchar*viewer=g_utf8_offset_to_pointer(demo_text,offset-1); while(1){ gcharoutbuf[7]={'\0'}; viewer=g_utf8_prev_char(viewer); if(viewer!=demo_text){ gchar*utf8_char=get_utf8_char(viewer); g_print("%s",utf8_char); g_free(utf8_char); }else{ break; } } g_print("\n");
注:g_memdup等价于C标准库中的malloc+memcpy,而g_free则等价与C标准库中的free。
空白字符比较
现在,假设给定一个UTF-8字符x,怎么判断它与某个UTF-8字符相等?
不要忘记,所谓的一个UTF-8字符,本质上只不过是char*类型的指针引用的一段内存空间。基于这一事实,利用C标准库提供的strcmp函数即可实现UTF-8字符的比较。
下面,我定义了函数is_space,用它判断一个UTF-8字符是否为空白字符。
staticgbooleanis_space(constgchar*s){ gbooleanret=FALSE; char*space_chars_set[]={"","\t",""}; size_tn=sizeof(space_chars_set)/sizeof(space_chars_set[0]); for(size_ti=0;i注:gboolean是GLib定义的布尔类型,其值要么是TRUE,要么是FALSE。
在is_space函数中,我只是判断了三种空白字符类型——英文空格、中文全角空格以及制表符。
虽然回车符与换行符也是空白字符,但是为了解决这篇文章开始时提出的问题,我需要单独为换行符定义一个判断函数:
staticgbooleanis_line_break(constgchar*s){ return(!strcmp(s,"\n")?TRUE:FALSE); }解决问题
现在万事俱备,只欠东风,我们应该着手解决问题了。如果读到此处已经忘记了问题是什么,那么请回顾第一节。
尽管下面这段代码看上去挺丑,但是它能够解决问题。
gbooleanis_right_at_sign=TRUE; glongoffset=g_utf8_strlen(demo_text,-1); gchar*viewer=g_utf8_offset_to_pointer(demo_text,offset-1); while(viewer!=demo_text){ viewer=g_utf8_prev_char(viewer); gchar*utf8_char=get_utf8_char(viewer); if(!is_space(utf8_char)){ if(!is_line_break(utf8_char)){ is_right_at_sign=FALSE; g_free(utf8_char); break; }else{ g_free(utf8_char); break; } } g_free(utf8_char); } if(is_right_at_sign)g_print("Right@!\n");对上述代码略做简化,可得:
gbooleanis_right_at_sign=TRUE; glongoffset=g_utf8_strlen(demo_text,-1); gchar*viewer=g_utf8_offset_to_pointer(demo_text,offset-1); while(viewer!=demo_text){ viewer=g_utf8_prev_char(viewer); gchar*utf8_char=get_utf8_char(viewer); if(!is_space(utf8_char)){ if(!is_line_break(utf8_char))is_right_at_sign=FALSE; g_free(utf8_char); break; } g_free(utf8_char); } if(is_right_at_sign)g_print("Right@!\n");其实,如果将UTF-8字符的提取与内存释放过程置入is_space与is_line_break函数,即:
staticgbooleanis_space(constgchar*c){ gbooleanret=FALSE; gchar*utf8_char=get_utf8_char(c); char*space_chars_set[]={"","\t",""}; size_tn=sizeof(space_chars_set)/sizeof(space_chars_set[0]); for(size_ti=0;i可以得到进一步的简化结果:
gbooleanis_right_at_sign=TRUE; glongoffset=g_utf8_strlen(demo_text,-1); gchar*viewer=g_utf8_offset_to_pointer(demo_text,offset-1); while(viewer!=demo_text){ viewer=g_utf8_prev_char(viewer); if(!is_space(viewer)){ if(!is_line_break(viewer))is_right_at_sign=FALSE; break; } } if(is_right_at_sign)g_print("Right@!\n");附:完整的代码
#include#include gchar*demo_text= "我的C81每天都在口袋里\n" "@"; staticgchar*get_utf8_char(constgchar*base){ gchar*new_base=g_utf8_next_char(base); gsizen=new_base-base; gchar*utf8_char=g_memdup(base,(n+1)); utf8_char[n]='\0'; returnutf8_char; } staticgbooleanis_space(constgchar*c){ gbooleanret=FALSE; gchar*utf8_char=get_utf8_char(c); char*space_chars_set[]={"","\t",""}; size_tn=sizeof(space_chars_set)/sizeof(space_chars_set[0]); for(size_ti=0;i 若是在Bash中使用gcc编译这份代码,可使用以下命令:
$gcc`pkg-config--cflags--libsglib-2.0`utf8-demo.c-outf8-demo总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对毛票票的支持。