关于在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,即 typedef char gchar;
下文,当我说『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 字串的基地址到 '@' 字符基地址的偏移距离:
glong offset = g_utf8_strlen(demo_text, -1);
结果是 38,恰好是 demo_text 字符串在 UTF-8 编码层次上的长度(不含字串结尾的 null 字符,亦即 '\0' 字符)。
g_utf8_strlen 的原型如下:
glong g_utf8_strlen(const gchar *p, gssize max);
注:glong 即 long,而 gssize 即 signed long。
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(const gchar *str, const gchar *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 字符的过程:
glong offset = 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) { /* do somthing here */ } 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_t n = 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,定义如下:
static gchar * get_utf8_char(const gchar *base) { gchar *new_base = g_utf8_next_char(base); gsize n = new_base - base; gchar *utf8_char = g_memdup(base, (n + 1)); utf8_char[n] = '\0'; return utf8_char; }
借助这个函数,就可以实现从 demo_text 的 '@' 所在位置开始,逆序打印 '@' 之前的所有 UTF-8 字符:
glong offset = g_utf8_strlen(demo_text, -1); gchar *viewer = g_utf8_offset_to_pointer(demo_text, offset - 1); while (1) { gchar outbuf[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 字符是否为空白字符。
static gboolean is_space(const gchar *s) { gboolean ret = FALSE; char *space_chars_set[] = {" ", "\t", " "}; size_t n = sizeof(space_chars_set) / sizeof(space_chars_set[0]); for (size_t i = 0; i < n; i++) { if (!strcmp(s, space_chars_set[i])) { ret = TRUE; break; } } return ret; }
注:gboolean 是 GLib 定义的布尔类型,其值要么是 TRUE,要么是 FALSE。
在 is_space 函数中,我只是判断了三种空白字符类型——英文空格、中文全角空格以及制表符。
虽然回车符与换行符也是空白字符,但是为了解决这篇文章开始时提出的问题,我需要单独为换行符定义一个判断函数:
static gboolean is_line_break(const gchar *s) { return (!strcmp(s, "\n") ? TRUE : FALSE); }
解决问题
现在万事俱备,只欠东风,我们应该着手解决问题了。如果读到此处已经忘记了问题是什么,那么请回顾第一节。
尽管下面这段代码看上去挺丑,但是它能够解决问题。
gboolean is_right_at_sign = TRUE; glong offset = 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");
对上述代码略做简化,可得:
gboolean is_right_at_sign = TRUE; glong offset = 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 函数,即:
static gboolean is_space(const gchar *c) { gboolean ret = FALSE; gchar *utf8_char = get_utf8_char(c); char *space_chars_set[] = {" ", "\t", " "}; size_t n = sizeof(space_chars_set) / sizeof(space_chars_set[0]); for (size_t i = 0; i < n; i++) { if (!strcmp(utf8_char, space_chars_set[i])) { ret = TRUE; break; } } g_free(utf8_char); return ret; } static gboolean is_line_break(const gchar *c) { gboolean ret = FALSE; gchar *utf8_char = get_utf8_char(c); if (!strcmp(utf8_char, "\n")) ret = TRUE; g_free(utf8_char); return ret; }
可以得到进一步的简化结果:
gboolean is_right_at_sign = TRUE; glong offset = 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 <string.h> #include <glib.h> gchar *demo_text = "我的 C81 每天都在口袋里\n" " @"; static gchar * get_utf8_char(const gchar *base) { gchar *new_base = g_utf8_next_char(base); gsize n = new_base - base; gchar *utf8_char = g_memdup(base, (n + 1)); utf8_char[n] = '\0'; return utf8_char; } static gboolean is_space(const gchar *c) { gboolean ret = FALSE; gchar *utf8_char = get_utf8_char(c); char *space_chars_set[] = {" ", "\t", " "}; size_t n = sizeof(space_chars_set) / sizeof(space_chars_set[0]); for (size_t i = 0; i < n; i++) { if (!strcmp(utf8_char, space_chars_set[i])) { ret = TRUE; break; } } g_free(utf8_char); return ret; } static gboolean is_line_break(const gchar *c) { gboolean ret = FALSE; gchar *utf8_char = get_utf8_char(c); if (!strcmp(utf8_char, "\n")) ret = TRUE; g_free(utf8_char); return ret; } int main(void) { gboolean is_right_at_sign = TRUE; glong offset = 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"); return 0; }
若是在 Bash 中使用 gcc 编译这份代码,可使用以下命令:
$ gcc `pkg-config --cflags --libs glib-2.0` utf8-demo.c -o utf8-demo
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对脚本之家的支持。
- C#温故而知新—闲话.Net
- c# IO&&线程 打造 定时打开指定程序
- 前FDIC主席:比特币政策不应打击加密货币发展
- 任何人都不应该控制区块链供应链
- c# IO操作(带进度的文件复制器,读取文本文件的指定行)
- 高科技来了!玩游戏一样开船的时代来了……
- C++库大全
- 人工智能行业前景预测 全球市场或超2700亿元
- Arxiv网络科学论文摘要14篇
- 工信部:网络强国建设2018年重点工作任务
- 刚刚!张小龙再出重磅!微信小程序掀起新零售红利狂潮!
- 无人驾驶系列——深度学习笔记:Tensorflow的安装-windows系统
- 2018年12大顶级云安全威胁
- 缤果盒子为域名意识打call 六位数秒下bingobox.com
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- python-爬取地理坐标
- Python基础第一个案例:猜数字游戏,这个都写不出,那就放弃吧
- 现在听歌要各大平台到处跑,嫌麻烦?制作个人专属的音乐下载器
- 爬取上市公司数据、分析数据,并用可视化现实全国各地区公司数量
- 今天刚上手爬虫,当然要从最简单的开始啦,验证一下所学的知识
- Python数据可视化入门:使用Matplotlib绘图
- 有了音乐下载器,怎么能没有音乐播放器呢,打造自己的音乐播放器
- 七夕节到了,单身狗程序员要对自己好点,用代码送自己点安慰
- 面向对象视角下的前端工程体系
- 使用 Python破解大众点评字体加密(SVG反爬虫)
- Python爬虫练习:爬取高清4K桌面壁纸
- 爬取B站18000条《黑神话:悟空》实机演示弹幕,做成词云
- Python爬虫实战:自动化登录网站,爬取商品数据
- 符合自己的工作难找?取招聘网站数据,让你找到心仪的工作
- 虽然现在有可以去码的软件了,可视频是如何自动跟踪打码的?