printf 与 utf-8 编码字符串的兼容性
Compatibility of printf with utf-8 encoded strings
我正在尝试使用 printf 函数在 C 代码 (char *) 中格式化一些 utf-8 编码的字符串。我需要指定格式的长度。当参数字符串中没有多字节字符时一切正常,但是当数据中有一些多字节字符时结果似乎不正确。
我的 glibc 有点旧 (2.17),所以我尝试了一些在线编译器,结果是一样的。
#include <stdlib.h>
#include <locale.h>
int main(void)
{
setlocale( LC_CTYPE, "en_US.UTF-8" );
setlocale( LC_COLLATE, "en_US.UTF-8" );
printf( "'%-4.4s'\n", "elephant" );
printf( "'%-4.4s'\n", "éléphant" );
printf( "'%-20.20s'\n", "éléphant" );
return 0;
}
Result of execution is :
'elep'
'él�'
'éléphant '
第一行正确(输出中有 4 个字符)
第二行显然是错误的(至少从人的角度来看)
最后一行也是错误的:只写了 18 个 unicode 字符而不是 20 个
似乎 printf 函数在 UTF-8 解码之前计算字符数(计算字节数而不是 unicode 字符数)
这是 glibc 中的错误还是 printf 的有据可查的限制?
确实 printf
计算字节数,而不是多字节字符。如果是bug,那么bug是在C标准中,而不是在glibc(通常与gcc结合使用的标准库实现)中。
公平地说,计算字符数也无助于对齐 unicode 输出,因为即使使用 fixed-width 字体,unicode 字符的显示宽度也不完全相同。 (例如,许多代码点的宽度为 0。)
我不会试图争辩这种行为是 "well-documented"。恕我直言,标准 C 的语言环境设施从来都不是特别适合这项任务,而且它们从来没有被特别详细地记录过,部分原因是底层模型试图包含如此多可能的编码,而没有将自己置于一个具体的例子中,这几乎是不可能的解释。 (……长篇大论删了……)
您可以使用 wchar.h
formatted output functions,
以宽字符计。 (这仍然不会为您提供正确的输出对齐方式,但它会按照您期望的方式计算精度。)
让我引用rici:printf 确实计算字节数,而不是多字节字符。如果是bug,那么bug是在C标准中,而不是在glibc(通常与gcc结合使用的标准库实现)中。
但是,不要混淆 wchar_t
和 UTF-8
。见wikipedia,把握前者的意思。相反,UTF-8 几乎可以像处理旧的 ASCII 一样处理。只是避免在字符中间截断。
为了对齐,您需要计算字符数。然后,将字节数传递给 printf。这可以通过使用 *
精度并传递字节数来实现。例如,由于 accented e 占用两个字节:
printf("'-4.*s'\n", 6, "éléphant");
根据 format of UTF-8 characters:
很容易编写计算字节数的函数
static int count_bytes(char const *utf8_string, int length)
{
char const *s = utf8_string;
for (;;)
{
int ch = *(unsigned char *)s++;
if ((ch & 0xc0) == 0xc0) // first byte of a multi-byte UTF-8
while (((ch = *(unsigned char*)s) & 0xc0) == 0x80)
++s;
if (ch == 0)
break;
if (--length <= 0)
break;
}
return s - utf8_string;
}
然而,在这一点上,最终会得到这样的行:
printf("'-4.*s'\n", count_bytes("éléphant", 4), "éléphant");
必须快速重复字符串两次成为维护的噩梦。至少,可以定义一个宏来确保字符串相同。假设上述函数保存在某个 utf8-util.h
文件中,您的程序可以重写如下:
#include <stdio.h>
#include <stdlib.h>
#include <locale.h>
#include "utf8-util.h"
#define INT_STR_PAIR(i, s) count_bytes(s, i), s
int main(void)
{
setlocale( LC_CTYPE, "en_US.UTF-8" );
setlocale( LC_COLLATE, "en_US.UTF-8" );
printf( "'%-4.*s'\n", INT_STR_PAIR(4, "elephant"));
printf( "'%-4.*s'\n", INT_STR_PAIR(4, "éléphant"));
printf( "'%-4.*s'\n", INT_STR_PAIR(4, "ééphant"));
printf( "'%-20.*s'\n", INT_STR_PAIR(20, "éléphant"));
return 0;
}
最后一个测试使用,希腊首音戏剧三百 (U+1016B) 字符。鉴于计数的工作原理,使用连续的非 ASCII 字符进行测试是有意义的。古希腊字符看起来"wide"足矣,可见使用定宽字体需要多少space。输出可能如下所示:
'elep'
'élép'
'éép'
'éléphant '
(在我的终端上,那些 4 个字符的字符串长度相等。)
我正在尝试使用 printf 函数在 C 代码 (char *) 中格式化一些 utf-8 编码的字符串。我需要指定格式的长度。当参数字符串中没有多字节字符时一切正常,但是当数据中有一些多字节字符时结果似乎不正确。
我的 glibc 有点旧 (2.17),所以我尝试了一些在线编译器,结果是一样的。
#include <stdlib.h>
#include <locale.h>
int main(void)
{
setlocale( LC_CTYPE, "en_US.UTF-8" );
setlocale( LC_COLLATE, "en_US.UTF-8" );
printf( "'%-4.4s'\n", "elephant" );
printf( "'%-4.4s'\n", "éléphant" );
printf( "'%-20.20s'\n", "éléphant" );
return 0;
}
Result of execution is :
'elep'
'él�'
'éléphant '
第一行正确(输出中有 4 个字符)
第二行显然是错误的(至少从人的角度来看)
最后一行也是错误的:只写了 18 个 unicode 字符而不是 20 个
似乎 printf 函数在 UTF-8 解码之前计算字符数(计算字节数而不是 unicode 字符数)
这是 glibc 中的错误还是 printf 的有据可查的限制?
确实 printf
计算字节数,而不是多字节字符。如果是bug,那么bug是在C标准中,而不是在glibc(通常与gcc结合使用的标准库实现)中。
公平地说,计算字符数也无助于对齐 unicode 输出,因为即使使用 fixed-width 字体,unicode 字符的显示宽度也不完全相同。 (例如,许多代码点的宽度为 0。)
我不会试图争辩这种行为是 "well-documented"。恕我直言,标准 C 的语言环境设施从来都不是特别适合这项任务,而且它们从来没有被特别详细地记录过,部分原因是底层模型试图包含如此多可能的编码,而没有将自己置于一个具体的例子中,这几乎是不可能的解释。 (……长篇大论删了……)
您可以使用 wchar.h
formatted output functions,
以宽字符计。 (这仍然不会为您提供正确的输出对齐方式,但它会按照您期望的方式计算精度。)
让我引用rici:printf 确实计算字节数,而不是多字节字符。如果是bug,那么bug是在C标准中,而不是在glibc(通常与gcc结合使用的标准库实现)中。
但是,不要混淆 wchar_t
和 UTF-8
。见wikipedia,把握前者的意思。相反,UTF-8 几乎可以像处理旧的 ASCII 一样处理。只是避免在字符中间截断。
为了对齐,您需要计算字符数。然后,将字节数传递给 printf。这可以通过使用 *
精度并传递字节数来实现。例如,由于 accented e 占用两个字节:
printf("'-4.*s'\n", 6, "éléphant");
根据 format of UTF-8 characters:
很容易编写计算字节数的函数 static int count_bytes(char const *utf8_string, int length)
{
char const *s = utf8_string;
for (;;)
{
int ch = *(unsigned char *)s++;
if ((ch & 0xc0) == 0xc0) // first byte of a multi-byte UTF-8
while (((ch = *(unsigned char*)s) & 0xc0) == 0x80)
++s;
if (ch == 0)
break;
if (--length <= 0)
break;
}
return s - utf8_string;
}
然而,在这一点上,最终会得到这样的行:
printf("'-4.*s'\n", count_bytes("éléphant", 4), "éléphant");
必须快速重复字符串两次成为维护的噩梦。至少,可以定义一个宏来确保字符串相同。假设上述函数保存在某个 utf8-util.h
文件中,您的程序可以重写如下:
#include <stdio.h>
#include <stdlib.h>
#include <locale.h>
#include "utf8-util.h"
#define INT_STR_PAIR(i, s) count_bytes(s, i), s
int main(void)
{
setlocale( LC_CTYPE, "en_US.UTF-8" );
setlocale( LC_COLLATE, "en_US.UTF-8" );
printf( "'%-4.*s'\n", INT_STR_PAIR(4, "elephant"));
printf( "'%-4.*s'\n", INT_STR_PAIR(4, "éléphant"));
printf( "'%-4.*s'\n", INT_STR_PAIR(4, "ééphant"));
printf( "'%-20.*s'\n", INT_STR_PAIR(20, "éléphant"));
return 0;
}
最后一个测试使用,希腊首音戏剧三百 (U+1016B) 字符。鉴于计数的工作原理,使用连续的非 ASCII 字符进行测试是有意义的。古希腊字符看起来"wide"足矣,可见使用定宽字体需要多少space。输出可能如下所示:
'elep'
'élép'
'éép'
'éléphant '
(在我的终端上,那些 4 个字符的字符串长度相等。)