【问题标题】:Accessing foreign-language string character by character逐个字符访问外语字符串
【发布时间】:2017-05-23 06:37:43
【问题描述】:

我知道这个问题可能非常初级。如果这是显而易见的事情,请原谅。 考虑以下程序:

#include <stdio.h>

int main(void) {
   // this is a string in English
   char * str_1 = "This is a string.";
   // this is a string in Russian
   char * str_2 = "Это строковая константа.";
   // iterator
   int i;
   // print English string as a string
   printf("%s\n", str_1);
   // print English string byte by byte
   for(i = 0; str_1[i] != '\0'; i++) {
      printf(" %c  ",(char) str_1[i]);
   }
   printf("\n");
   // print numerical values of English string byte by byte
   for(i = 0; str_1[i] != '\0'; i++) {
      printf("%03d ",(int) str_1[i]);
   }
   printf("\n");
   // print Russian string as a string
   printf("%s\n", str_2);
   // print Russian string byte by byte
   for(i = 0; str_2[i] != '\0'; i++) {
      printf(" %c  ",(char) str_2[i]);
   }
   printf("\n");
   // print numerical values of Russian string byte by byte
   for(i = 0; str_2[i] != '\0'; i++) {
      printf("%03d ",(int) str_2[i]);
   }
   printf("\n");
   return(0);
}

输出:

This is a string.
 T   h   i   s       i   s       a       s   t   r   i   n   g   .
084 104 105 115 032 105 115 032 097 032 115 116 114 105 110 103 046
Это строковая константа.
 ▒   ▒   ▒   ▒   ▒   ▒       ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒       ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   .
-48 -83 -47 -126 -48 -66 032 -47 -127 -47 -126 -47 -128 -48 -66 -48 -70 -48 -66 -48 -78 -48 -80 -47 -113 032 -48 -70 -48 -66 -48 -67 -47 -127 -47 -126 -48 -80 -48 -67 -47 -126 -48 -80 046

可以看出,英文(ASCII)字符串可以打印为字符串或使用数组索引访问并逐字符(逐字节)打印,但是俄语字符串(我相信编码为 UTF-8)可以以字符串形式打印,但不能逐字符访问。

我知道原因是在这种情况下,俄语字符使用两个字节而不是一个字节进行编码。

我想知道是否有任何简单的方法可以通过正确声明数据类型或通过以某种方式标记字符串来使用标准 C 库函数逐个字符(在本例中为两个字节乘两个字节)打印 Unicode 字符串或通过设置语言环境或其他方式。

我尝试在俄语字符串前面加上“u8”,即char * str_2 = u8"...",但这不会改变行为。我想避免使用宽字符来假设所使用的语言,例如每个字符恰好两个字节。任何建议将不胜感激。

【问题讨论】:

  • 它没有什么基本的东西——这类问题与国际化代码非常相关,而且一点也不明显。在 Web 浏览器的另一端,我们面临的一个问题是字符串几乎不可避免地会以 UTF-8 编码。但是,如果您使用不同的代码集(例如 ISO 8859-5 等西里尔文代码集),您磁盘上的内容可能与我们在网络上看到的不同。假设您在磁盘上确实有 UTF-8,最好确保以十六进制打印“无符号字符”值(显然,在您的机器上签名了普通的 char)。
  • 可能有它的库,但我不知道标准 C 中的方法(除了编写自己的 UTF-8 解析器)。

标签: c string unicode-string


【解决方案1】:

我认为来自&lt;stdlib.h&gt;mblen()mbtowc()wctomb()mbstowcs()wcstombs() 函数是部分相关的。例如,您可以使用mblen() 找出字符串中每个字符由多少字节组成。

另一个很少使用的标题和函数是&lt;locale.h&gt;setlocale()

这是对您的代码的改编:

#include <assert.h>
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static inline void ntbs_hex_dump(const char *pc_ntbs)
{
    unsigned char *ntbs = (unsigned char *)pc_ntbs;
    for (int i = 0; ntbs[i] != '\0'; i++)
        printf(" %.2X ", ntbs[i]);
    putchar('\n');
}

static inline void ntbs_chr_dump(const char *pc_ntbs)
{
    unsigned char *ntbs = (unsigned char *)pc_ntbs;
    for (int i = 0; ntbs[i] != '\0'; i++)
        printf(" %c  ", ntbs[i]);
    putchar('\n');
}

int main(void)
{
    char *loc = setlocale(LC_ALL, "");
    printf("Locale: %s\n", loc);

    char *str_1 = "This is a string.";
    char *str_2 = "Это строковая константа.";

    printf("English:\n");
    printf("%s\n", str_1);
    ntbs_chr_dump(str_1);
    ntbs_hex_dump(str_1);

    printf("Russian:\n");
    printf("%s\n", str_2);
    ntbs_chr_dump(str_2);
    ntbs_hex_dump(str_2);

    char *mbp = str_2;
    while (*mbp != '\0')
    {
        enum { MBS_LEN = 10 };
        int mbl = mblen(mbp, strlen(mbp));
        char mbs[MBS_LEN];
        assert(mbl < MBS_LEN - 1 && mbl > 0);
        // printf("mbl = %d\n", mbl);
        memmove(mbs, mbp, mbl);
        mbs[mbl] = '\0';
        printf(" %s ", mbs);
        mbp += mbl;
    }
    putchar('\n');

    return(0);
}

setlocale() 很重要,至少在 macOS Sierra 10.12.2(带有 GCC 6.3.0)上是我开发和测试它的地方。没有它,mblen() 总是返回 1,并且代码没有任何好处。

我从中得到的输出是:

Locale: en_US.UTF-8
English:
This is a string.
 T   h   i   s       i   s       a       s   t   r   i   n   g   .  
 54  68  69  73  20  69  73  20  61  20  73  74  72  69  6E  67  2E 
Russian:
Это строковая константа.
 ?   ?   ?   ?   ?   ?       ?   ?   ?   ?   ?   ?   ?   ?   ?   ?   ?   ?   ?   ?   ?   ?   ?   ?       ?   ?   ?   ?   ?   ?   ?   ?   ?   ?   ?   ?   ?   ?   ?   ?   ?   ?   .  
 D0  AD  D1  82  D0  BE  20  D1  81  D1  82  D1  80  D0  BE  D0  BA  D0  BE  D0  B2  D0  B0  D1  8F  20  D0  BA  D0  BE  D0  BD  D1  81  D1  82  D0  B0  D0  BD  D1  82  D0  B0  2E 
 Э  т  о     с  т  р  о  к  о  в  а  я     к  о  н  с  т  а  н  т  а  . 

稍加努力,代码可以更紧密地打印 UTF-8 数据的字节对。 D0 和 D1 前导字节对于 BMP(基本多语言平面)中的西里尔代码块 U+0400 .. U+04FF 的 UTF-8 编码是正确的。

只是为了您的娱乐价值:BSD sed 拒绝处理输出,因为那些问号代表无效代码:sed: RE error: illegal byte sequence

【讨论】:

    【解决方案2】:

    这是一个使用sscanf 函数的简单解决方案。 C99 要求 printfscanf(和朋友)都理解 l 大小限定符到 %s%c 字符代码,导致它们在多字节(即 UTF-8)表示和宽字符串之间转换/character(即wchar_t,它是一个大到足以包含代码点的整数类型)。这意味着您可以使用它一次将一个字符串分开一个(多字节)字符,而不必担心该序列是否只是七位字符(英文)。 (如果这听起来很复杂,请查看代码。本质上,它只是在格式字符串中添加了一个 l 限定符。)

    这确实使用了wchar_t,在某些平台(Windows、咳嗽、咳嗽)上可能限制为 16 位。我怀疑如果你在 Windows 上使用星体平面字符,你最终会得到代理字符,这可能会让你感到悲伤,但代码在 Linux 和 Mac 上都可以正常工作,至少在不太古老的版本中是这样。

    注意程序开始时对setlocale 的调用。这是任何宽字符功能正常工作所必需的。它将执行语言环境设置为默认系统语言环境,这通常是多字节字符为 UTF-8 的语言环境。 (但是,下面的代码并不关心。它只要求函数的输入采用当前语言环境指定的多字节表示形式。)

    它可能不是这个问题的最快解决方案,但它的优点是编写起来相当简单,至少在我看来。

    以下基于原始代码,但为了简单起见,我将输出重构为单个函数。我还将数值输出更改为十六进制(因为使用代码图表更容易验证)。

    #include <locale.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <wchar.h>
    
    /* Print the string three ways */
    void print3(const char* s);
    
    void print3(const char* s) {
       wchar_t wch;
       int n;
       // print as a string
       printf("%s\n", s);
       // print char by char
       for (int i = 0; s[i] != '\0'; i += n) {
          sscanf(s+i, "%lc%n", &wch, &n);
          printf("   %lc   ", wch);
       }
       putchar('\n');
       // print numerical values char by char 
       for (int i = 0; s[i] != '\0'; i += n) {
          sscanf(s+i, "%lc%n", &wch, &n);
          printf(" %05lx ", (unsigned long)wch);
       }
       putchar('\n');
    }
    
    int main(void) {
       setlocale(LC_ALL, "");
    
       char *str_1 = "This is a string.";
       char *str_2 = "Это строковая константа.";
       char *str_3 = u8"\U0001d7d8\U0001d7d9\U0001f638 in the astral plane";
    
       print3(str_1);
       print3(str_2);
       print3(str_3);
       return 0;
    }
    

    以上尝试模仿 OP 中的代码。我实际上更喜欢使用指针而不是索引来编写循环,并检查 sscanf 的返回码作为终止条件:

    /* Print the string three ways */
    void print3(const char* s) {
       wchar_t wch;
       int n;
       // print as a string
       printf("%s\n", s);
       // print char by char
       for (const char* p = s;
            sscanf(p, "%lc%n", &wch, &n) > 0;
            p += n) {
               printf("   %lc   ", wch);
       }
       putchar('\n');
       for (const char* p = s;
            sscanf(p, "%lc%n", &wch, &n) > 0;
            p += n) {
               printf(" %5.4lx ", (unsigned long)wch);
       }
       putchar('\n');
    }
    

    最好确保sscanf 没有返回错误,表明存在无效的多字节序列。

    这是我系统上的输出:

    This is a string.
       T      h      i      s             i      s             a             s      t      r      i      n      g      .   
      0054   0068   0069   0073   0020   0069   0073   0020   0061   0020   0073   0074   0072   0069   006e   0067   002e 
    Это строковая константа.
       Э      т      о             с      т      р      о      к      о      в      а      я             к      о      н      с      т      а      н      т      а      .   
      042d   0442   043e   0020   0441   0442   0440   043e   043a   043e   0432   0430   044f   0020   043a   043e   043d   0441   0442   0430   043d   0442   0430   002e 
    ??? in the astral plane
       ?      ?      ?             i      n             t      h      e             a      s      t      r      a      l             p      l      a      n      e   
     1d7d8  1d7d9  1f638   0020   0069   006e   0020   0074   0068   0065   0020   0061   0073   0074   0072   0061   006c   0020   0070   006c   0061   006e   0065
    

    【讨论】:

    • 它适用于带有 GCC 6.3.0 的 macOS Sierra 10.12.2。 (使用我的默认编译选项,我添加了print3() 的声明并在loc 中打印了语言环境信息(否则它是一个未使用的变量)——但这是我非常挑剔的默认编译选项。)
    • 这个问题确实说“我不想使用宽字符来假设正在使用什么语言”。您使用的是宽字符,但我不确定它们是否仅限于 U+0000 .. U+07FF(1 字节和 2 字节 UTF-8 编码范围)。我可以很容易地相信他们处理 BMP(基本的多语言计划,又名 U+0000 .. U+FFFF),但是如何处理超出该范围的字符(例如大多数表情符号)?作为 2 个 16 位 wchar_t 值(一个高代理和一个低代理)还是它们实际上是 32 位值并将它们作为 UTF-32 处理?
    • 顺便提一下,新的表情符号在 U+1F600 .. U+1F64F 范围内。
    • @jonathan:在 linux 上,我相信 mac,wchar_t 是 32 位的。星光层代码点已经使用了很长时间,现在(相当早于表情符号)。在 Windows 上,我认为 wchar_t 是 16 位的,这就是为什么我说这在那个平台上会是一个问题。
    【解决方案3】:

    正确地建议您编写自己的 UTF-8 解析器,实际上是 quite easy to do。这是一个示例实现:

    int utf8decode(unsigned char *utf8, unsigned *code) {
      while(*utf8) { /* Scan the whole string */
        if ((utf8[0] & 128) == 0) { /* Handle single-byte characters */
          *code = utf8[0];
          utf8++;
        } else { /* Looks like it's a 2-byte character; is it? */
          if ((utf8[0] >> 5) != 6 || (utf8[1] >> 6) != 2)
            return 1;
          /* Yes, it is; do bit magic */
          *code = ((utf8[0] & 31) << 6) + (utf8[1] & 63);
          utf8 += 2;
        }
        code++;
      }
      *code = 0;
      return 0; /* We got it! */
    }
    

    让我们做一些测试:

    int main(void) {
      int i = 0;
      unsigned char *str = "Это строковая константа.";
      unsigned codes[1024]; /* Hope it's long enough */ 
    
      if (utf8decode(str, codes) == 1) /* Decode */
        return 1;
      while(codes[i]) /* Print the result */
        printf("%u ", codes[i++]); 
    
      puts(""); /* Final newline */
      return 0;
    }
    

    1069 1090 1086 32 1089 1090 1088 1086 1082 1086 1074 1072 1103 32 1082 1086 1085 1089 1090 1072 1085 1090 1072 46

    【讨论】:

    • 我可以看到 UTF8 解码器适用于格式良好的 UTF8,它不需要超过 2 个字节(所以 U+0000 到 U+07FF),但它不适用于 3-字节或 4 字节 UTF8 序列(它只是拒绝它们)。对于手头的问题,这就足够了(只是)。
    猜你喜欢
    • 2020-01-20
    • 1970-01-01
    • 2013-01-18
    • 1970-01-01
    • 1970-01-01
    • 2012-04-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多