关于在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

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • C++实现判断一个字符串是否为UTF8或GBK格式的方法

    本文实例讲述了C++实现判断一个字符串是否为UTF8或GBK格式的方法.分享给大家供大家参考,具体如下: 在处理外部数据的时候,很可能因为数据格式不一样而导致乱码,甚至导致某些程序挂掉.鉴于对多数系统来说,使用是更被广泛使用的utf8,所以判断是不是utf8格式显得很重要了. 下面是一个判断字符串是否为utf8的函数: bool is_str_utf8(const char* str) { unsigned int nBytes = 0;//UFT8可用1-6个字节编码,ASCII用一个字节 u

  • C++中可正确获取UTF-8字符长度的函数分享

    在C++的char*以及string中,使用的是字节流编码,即sizeof(char) == 1. 也就是说,C++是不区分字符的编码的. 而一个合法UTF8的字符长度可能为1-4位. 现在假设一串输入为UTF8编码,如何能准确的定位到每个UTF8字符的"CharPoint",而不会错误的分割字符呢? 参考这个页面:http://www.nubaria.com/en/blog/?p=289 可以改造出下面的函数: const unsigned char kFirstBitMask =

  • C语言中判断一个char*是不是utf8编码

    C语言中判断一个char*是不是utf8编码 里我修改了一下, 纯ASCII编码的字符串也返回true, 因为UTF8和ASCII兼容 实例代码: int utf8_check(const char* str, size_t length) { size_t i; int nBytes; unsigned char chr; i = 0; nBytes = 0; while (i < length) { chr = *(str + i); if (nBytes == 0) { //计算字节数 i

  • 如何在程序中判断VS的版本(实现方法详解)

    代码如下所示: #include<iostream> using namespace std; int main() { cout << _MSC_VER << endl; return 0; } 在VC6.0中结果为:1200 在VC10.0(VS2010)中结果为:1600 _MSC_VER实际就是 Microsoft visual c++ version(是微软的预定义宏). 具体对应如下: MS VC++ 14.0 _MSC_VER = 1900(VS2015)

  • Java程序中实现调用Python脚本的方法详解

    本文实例讲述了Java程序中实现调用Python脚本的方法.分享给大家供大家参考,具体如下: 在程序开发中,有时候需要Java程序中调用相关Python脚本,以下内容记录了先关步骤和可能出现问题的解决办法. 1.在Eclipse中新建Maven工程: 2.pom.xml文件中添加如下依赖包之后update maven工程: <dependency> <groupId>org.python</groupId> <artifactId>jython</ar

  • 微信小程序中的列表切换功能实例代码详解

    感觉这列表切换有点类似于轮播图,而且感觉这代码直接可以拿来用,稍微改一改样式什么的就OK了,列表切换也是用到的地方也很多 wxml中的代码如下: <!-- 标签页面标题 --> <view class="tab"> <view class="tab-item {{tab==0?'active':''}}" bindtap="changeItem" data-item="0">音乐推荐<

  • C++中new和delete的使用方法详解

    C++中new和delete的使用方法详解 new和delete运算符用于动态分配和撤销内存的运算符 new用法:           1.     开辟单变量地址空间 1)new int;  //开辟一个存放数组的存储空间,返回一个指向该存储空间的地址.int *a = new int 即为将一个int类型的地址赋值给整型指针a. 2)int *a = new int(5) 作用同上,但是同时将整数赋值为5           2.     开辟数组空间 一维: int *a = new in

  • 对python 中class与变量的使用方法详解

    python中的变量定义是很灵活的,很容易搞混淆,特别是对于class的变量的定义,如何定义使用类里的变量是我们维护代码和保证代码稳定性的关键. #!/usr/bin/python #encoding:utf-8 global_variable_1 = 'global_variable' class MyClass(): class_var_1 = 'class_val_1' # define class variable here def __init__(self, param): self

  • Android中实现ping功能的多种方法详解

    使用java来实现ping功能. 并写入文件.为了使用java来实现ping的功能,有人推荐使用java的 Runtime.exec()方法来直接调用系统的Ping命令,也有人完成了纯Java实现Ping的程序,使用的是Java的NIO包(native io, 高效IO包).但是设备检测只是想测试一个远程主机是否可用.所以,可以使用以下三种方式来实现: 1. Jdk1.5的InetAddresss方式 自从Java 1.5,java.net包中就实现了ICMP ping的功能. 使用时应注意,如

  • Spring-IOC容器中的常用注解与使用方法详解

    Spring是什么? Spring是一个轻量级Java开发框架,最早有Rod Johnson创建,目的是为了解决企业级应用开发的业务逻辑层和其他各层的耦合问题.它是一个分层的JavaSE/JavaEE full-stack(一站式)轻量级开源框架,为开发Java应用程序提供全面的基础架构支持.Spring负责基础架构,因此Java开发者可以专注于应用程序的开发. 体系结构 核心容器(Core Container):Spring的核心容器是其他模块建立的基础,有Spring-core.Spring

  • C#给Word中的字符添加着重号的方法详解

    目录 前言 引入dll 方法1 方法2 添加强调符号 C# vb.net 前言 在Word中添加着重号,即强调符号,可以在选中字符后,鼠标右键点击,选择“字体”,在窗口中可直接选择“着重号”添加到文字,用以对重要文字内容起加强提醒的目的,如下图: 通过C#,我们可以查找到需要添加着重号的字符串,然后通过字符串格式的属性值来添加符号.下面,将对此做详细介绍. 引入dll 方法1 手动引入 将 Free Spire.Doc for .NET 下载到本地,解压,安装.安装完成后,找到安装路径下BIN文

  • 微信小程序全屏滚动字幕的实现方法详解

    目录 一.实现背景 二.实现代码 三.滚动速度 四.后续优化 实现效果 一.实现背景 无意中在某音上看到用手机横屏作为广告屏的视频,大部分都是用第三方软件实现的: 以及在汽车后挡风玻璃放置提醒字样的视频,这种基本是要花钱买屏幕,通过手机控制屏幕内容: 遂想实现这种效果 二.实现代码 1,滚动字幕 zimu.wxml,界面布局,很简单,没啥特别的,顶部一个返回按钮,为了不影响整体效果,可以把这个按钮做成透明的图片放上去:除了那个按钮剩下的就是滚动的字幕组件了 <!--pages/zimu/zimu

  • MongoDB 中Limit与Skip的使用方法详解

    MongoDB 中Limit与Skip的使用方法详解 一 MongoDB Limit() 方法 如果你需要在MongoDB中读取指定数量的数据记录,可以使用MongoDB的Limit方法,limit()方法接受一个数字参数,该参数指定从MongoDB中读取的记录条数. 语法 limit()方法基本语法如下所示: >db.COLLECTION_NAME.find().limit(NUMBER) 实例 > db.col.find({},{"title":1,_id:0}).li

随机推荐