【问题标题】:Easy way to read UTF-8 characters from a binary file?从二进制文件中读取 UTF-8 字符的简单方法?
【发布时间】:2021-09-19 02:12:47
【问题描述】:

这是我的问题:我必须读取“二进制”文件,即具有不同“记录”大小并且可能包含二进制数据以及 UTF-8 编码的文本字段的文件。

从输入文件中读取给定数量的字节是微不足道的,但我想知道是否有函数可以轻松地从文件中读取给定数量的字符(不是字节)?比如,如果我知道我需要读取一个 10 个字符的字段(以 UTF-8 编码,它会至少 10 个字节长,但如果我们是谈论“高”代码点)。

我强调我正在读取一个“混合”文件,也就是说,我不能将其全部作为 UTF-8 处理,因为必须读取二进制字段而不被解释为 UTF-8 字符。

因此,虽然手工操作非常简单(逐字节、幼稚的方法并不难实现——尽管我对效率持怀疑态度),但我想知道是否有更好的方法那里的替代品。如果可能的话,在标准库中,但我也对第 3 方代码持开放态度 - 如果我的组织验证了它的使用。

【问题讨论】:

  • 确实需要更详细的二进制文件格式。文本字段是固定宽度还是可变长度,带有长度指示符或空终止符?我认为让二进制文件在 Unicode 代码点中指示宽度是不好的设计。
  • 你为什么不mmap文件?然后,您可以轻松地将 UTF8 的 byte-at-a-time 和以后的二进制交换 这是读取文件的最快方式。看我的回答:stackoverflow.com/questions/33616284/…
  • This old question(在右侧边栏中的“相关”下建议)看起来正是您想要做的。
  • 请注意,使用 MSVC 时 getwc 无法返回完整的 Unicode 代码点。 MSVC 的 wint_t 是一个无符号短字符,因此如果您的数据实际上包含非 BMP 字符,那么您将得到最好的代理(我还没有确认这是否真的有效,或者它是否会产生某种错误)。跨度>
  • “二进制字段”是什么意思?是什么界定了这些领域? UTF-8 有一个相当固定的模式:任何具有 C/D/E/F 高 4 位的“新”字节都是 2/3/4 字节字符序列的第一个字节,等等。现在未知的是你的“二进制”数据。

标签: c unicode utf-8


【解决方案1】:

这里有两种可能性:

(1) 如果(但通常仅当)您的语言环境设置为处理 UTF-8,getwc 函数应该准确读取一个 UTF 编码的 Unicode 字符,即使它是多个字节长。所以你可以做类似的事情

setlocale(LC_CTYPE, "UTF-8");
wint_t c;

for(i = 0; i < 10; i++) {
    c = getwc(ifp);
    /* do something with c */
}

现在,c 将是一个包含 Unicode 代码点的整数,不是 UTF-8 多字节序列。如果(很可能)您想在内存数据结构中存储 UTF-8 字符串,则必须转换回 UTF-8,可能使用wctomb

(2) 您可以从输入中读取 N 个字节,然后使用 mbstowcs 将它们转换为宽字符流。这也不是完美的,因为很难知道 N 应该是什么,而且mbstowcs 给你的宽字符串可能不是你想要的。

但在探索这两种方法之前,真正的问题是,您输入的格式是什么?那些 UTF 编码的文本片段,它们是固定大小的,还是文件格式包含一个明确的计数来说明它们有多大?无论哪种情况,它们的大小是以字节还是字符为单位指定的?希望它以字节为单位,在这种情况下,您不需要对 UTF-8 进行任何转换,您可以使用 fread 读取 N 个字符。如果计数是按字符指定的(根据我的经验,这有点奇怪),您可能不得不使用类似于我上面的方法 (1) 的方法。

除了上面 (1) 中的循环之外,我不知道有一种简单的封装方式来执行相当于“读取 N 个 UTF-8 字符,无论需要多少字节”的操作。

【讨论】:

  • 据我所知,有不同类型的“记录”,每条记录都有自己的格式。一个可能是 10 个 UTF-8 字符,然后是 30 个字节的二进制数据,然后是 25 个 UTF-8 字符,另一个可能有完全不同的布局......我只有在阅读给定的“魔术”部分后才会知道记录,让我知道要读取哪种记录格式。而且“神奇”部分甚至不在给定记录的第一个字节......
  • 我想我现在会尝试这些答案(你的,以及 cmets 中提到的其他线索)。如果有必要,我会回来更多。感谢回答的人!
  • @Kzwix 如果您对这种格式的设计有任何意见,并且如果有任何更改的可能性,您应该敦促他们定义像 10 和 30 这样的计数作为字节,而不是字符。它将使大多数处理任务变得更加容易。 (然而,它会给构造记录的代码带来一些负担,因为它不能盲目地截断 N 个字节,但如果下一个字符恰好是不适合的 4 字节 UTF-8 字符。)
  • Re "如果您对这种格式的设计有任何意见",或者添加一个以字节为单位的长度前缀。您仍然可以将长度限制为 10 个字符或要求 10 个字符,但在文件中添加这些字符的编码长度。
  • @Kzwix 好吧,你有我的同情,因为我不得不处理明显不是为便于阅读而设计的格式。 (正如that older thread 的评论者所说,“哇,这将是一个非常糟糕的格式。”)
【解决方案2】:

你也可以这样使用:

static unsigned char num_most_significant_ones[] = {
    /* 80 */   1, 1, 1, 1, 1, 1, 1, 1,   1, 1, 1, 1, 1, 1, 1, 1,
    /* 90 */   1, 1, 1, 1, 1, 1, 1, 1,   1, 1, 1, 1, 1, 1, 1, 1,
    /* A0 */   1, 1, 1, 1, 1, 1, 1, 1,   1, 1, 1, 1, 1, 1, 1, 1,
    /* B0 */   1, 1, 1, 1, 1, 1, 1, 1,   1, 1, 1, 1, 1, 1, 1, 1,
    /* C0 */   2, 2, 2, 2, 2, 2, 2, 2,   2, 2, 2, 2, 2, 2, 2, 2,
    /* D0 */   2, 2, 2, 2, 2, 2, 2, 2,   2, 2, 2, 2, 2, 2, 2, 2,
    /* E0 */   3, 3, 3, 3, 3, 3, 3, 3,   3, 3, 3, 3, 3, 3, 3, 3,
    /* F0 */   4, 4, 4, 4, 4, 4, 4, 4,   5, 5, 5, 5, 6, 6, 7, 8
};

static unsigned char lead_byte_data_mask[] = {
   0x7F, 0, 0x1F, 0x0F, 0x07, 0x03, 0x01
};

static int32_t min_by_len[] = {
   -1, 0x00, 0x80, 0x800, 0x10000ULL
}

// buf must be capable of accommodating at least 4 bytes.
// Returns 0 on EOF or read error.
size_t read_one_utf8_char(FILE* stream, char* buf) {
   int lead = getc(stream);
   if (lead == EOF)
      return 0;

   buf[0] = lead;
   if (lead < 0x80)
      return 1;

   unsigned len = num_most_significant_ones[ lead - 0x80 ];
   if (len == 1 || len > 6)
      goto ERROR;

   unsigned char mask = lead_byte_data_mask[len];
   uint32_t cp = lead & mask;
   for (int i=1; i<len; ++i) {
      int ch = getc(stream);  // Premature EOF or error.
      if (ch == EOF)
         goto ERROR;
      if ((ch & 0xC0) != 0x80) {  // Premature end of character.
         ungetc(ch, stream);
         goto ERROR;
      }
      cp = (cp << 6) | (ch & 0x3F);
      if (i < 4)
         buf[i] = ch;
   }

   if (len > 4 || cp < min_by_len[len] || ( cp >= 0xD800 && cp < 0xE000 ) || cp >= 0x110000)
      goto ERROR;

   return len;

ERROR:
   // Return U+FFFD.
   buf[0] = 0xEF;
   buf[1] = 0xBF;
   buf[2] = 0xBD;
   return 3;
}

getwc 不同,它返回 UTF-8。

此外,它还会验证,用 U+FFFD 替换非法序列。 (它不会替换非字符。[1][2])我不知道getwc 是否这样做。

未经测试。

【讨论】:

  • 验证是一个非常复杂的问题。你甚至不能说getwc 是否这样做,因为它取决于 (a) 实现,也许还取决于 (b) 当前语言环境。超长编码只是您需要检查的事情之一:还有 (1) 完全非法的 5 和 6 字节序列(它们不是当前标准 UTF-8 定义的一部分),(2) 非法的 4 字节序列(0x110000 以上)和(3)代理(0xD800-0xF8FF)。
  • @Steve Summit,关于“完全非法的 5 和 6 字节序列”,已处理。 >6 的检查被意外删除,但重新添加。
  • @Steve Summit,Re“非法的 4 字节序列(0x110000 以上的那些),和(3)代理(0xD800-0xF8FF)。”,你指的是@ 987654323@。你错过了一些,它们实际上并不违法。见Corrigendum #9。 “它们在交换中不是非法的,也不会导致格式不正确的 Unicode 文本。”现在由答案链接。
  • 关键是这一切都足够复杂,以至于有人(比如 Kzwix)可能会合理地决定他们需要使用某种标准库实现,而不是冒险自己动手。 (但不要误会我的意思——我已经在这里推出了几次。)
  • @Steve Summit,是的,如果可能的话。
【解决方案3】:

好吧,现在,我决定创建一个函数来分配大小为 4 * numberOfCharactersToRead + 1 的缓冲区(因为 UTF-8 字符最多编码为 4 个字节)。

然后我 fread() 那么多(或者尽可能多,如果我点击 EOF)。然后我只测试高位以了解我是否击中了 1 字节、2 字节、3 字节或 4 字节字符。我根据需要检查以下字节,并注意它放在我的位置。

在读取所需的字符数后,我会记下它实际占用的字节数,然后如果我读取的字符数超出了需要,我会调整回文件指针。我还重新分配()缓冲区以将其缩小到所需的大小。

我很确定它比在将 wchar_t 转换回 UTF-8 之前重复调用 getwc() 更有效(因为最后,我需要将它保存为 UTF-8 序列,因为我正在存储Perl 标量中的数据,这就是 Perl 内部的处理方式)。

我以 0 结束读取的 UTF-8“字符串”(因此是额外的字节),以便能够使用标准 C 函数打印它,就是这样。

此外,为了将“原始二进制”与 UTF-8 编码文本一起存储,当我连接它们时,我只是将二进制字节编码为 UTF-8 代码点。这样,在 Perl 下,我可以将字符或“原始字节”视为 UTF-8 字符。当我需要处理伪装成字符的原始字节时,我只需要取回“代码点”值。

我知道我没有在标签中提到 Perl,但这对于这个问题并不重要,所以我只是为了提供一些背景信息来说明我为什么这样做。

感谢所有发布有用建议的人:)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-10-26
    • 2016-07-01
    • 1970-01-01
    • 2011-01-07
    • 1970-01-01
    • 2018-06-18
    • 2010-12-25
    • 2014-06-18
    相关资源
    最近更新 更多