【问题标题】:Ignore byte-order marks in C++, reading from a stream忽略 C++ 中的字节顺序标记,从流中读取
【发布时间】:2012-01-16 13:17:13
【问题描述】:

我有一个函数可以读取 ifstream 中单行上的一个变量(整数、双精度或布尔值)的值:

template <typename Type>
void readFromFile (ifstream &in, Type &val)
{
  string str;
  getline (in, str);
  stringstream ss(str);
  ss >> val;
}

但是,它在使用编辑器在第一行开头插入 BOM (byte order mark) 创建的文本文件上失败,不幸的是其中包括 {Note,Word}pad。如果存在于str 开头的字节顺序标记,如何修改此函数以忽略?

【问题讨论】:

  • 你的意思是 UTF-8 BOM?这太神秘了……
  • 咳咳.. UTF8 BOM 不是 FEFF EF BB BF,它也应该与字节序无关。顺便说一句,UTF8 BOM 被 unicode 联盟搞砸了。

标签: c++ unicode


【解决方案1】:

(我假设您使用的是 Windows,因为在 UTF-8 文件中使用 U+FEFF 作为签名主要是 Windows 的事情,应该在其他地方避免使用)

您可以将文件作为 UTF-8 文件打开,然后检查第一个字符是否为 U+FEFF。您可以通过打开一个普通的基于字符的 fstream 来做到这一点,然后使用 wbuffer_convert 将其视为另一种编码中的一系列代码单元。 VS2010 对 char32_t 还没有很好的支持,所以下面在 wchar_t 中使用 UTF-16。

std::fstream fs(filename);
std::wbuffer_convert<std::codecvt_utf8_utf16<wchar_t>,wchar_t> wb(fs.rdbuf());
std::wistream is(&wb);
// if you don't do this on the stack remember to destroy the objects in reverse order of creation. is, then wb, then fs.
std::wistream::int_type ch = is.get();
const std::wistream::int_type ZERO_WIDTH_NO_BREAK_SPACE = 0xFEFF
if(ZERO_WIDTH_NO_BREAK_SPACE != ch)
    is.putback(ch);

// now the stream can be passed around and used without worrying about the extra character in the stream.

int i;
readFromStream<int>(is,i);

请记住,这应该在整个文件流上完成,而不是在您的字符串流上的 readFromFile 中完成,因为只有当它是整个文件中的第一个字符(如果有的话)时,才应该忽略 U+FEFF。它不应该在其他任何地方进行。

另一方面,如果您喜欢使用基于字符的流并且只想跳过 U+FEFF(如果存在),那么 James Kanze 的建议似乎不错,所以这里有一个实现:

std::fstream fs(filename);
char a,b,c;
a = fs.get();
b = fs.get();
c = fs.get();
if (a != (char)0xEF || b != (char)0xBB || c != (char)0xBF) {
    fs.seekg(0);
} else {
    std::cerr << "Warning: file contains the so-called 'UTF-8 signature'\n";
}

此外,如果您想在内部使用 wchar_tcodecvt_utf8_utf16codecvt_utf8 构面具有可以为您消耗“BOM”的模式。唯一的问题是,wchar_t 现在被广泛认为毫无价值*,因此您可能不应该这样做。

std::wifstream fin(filename);
fin.imbue(std::locale(fin.getloc(), new std::codecvt_utf8_utf16<wchar_t, 0x10FFFF, std::consume_header));

* wchar_t 毫无价值,因为它被指定只做一件事;提供一个固定大小的数据类型,可以表示区域设置字符库中的任何代码点。它不提供 语言环境之间的通用表示(即,相同的wchar_t 值在不同的语言环境中可以是不同的字符,因此您不一定要转换为wchar_t,切换到另一个语言环境,然后再转换回到char 以进行iconv 类似的编码转换。)

固定大小的表示本身没有价值,有两个原因;首先,许多代码点具有语义含义,因此理解文本意味着您无论如何都必须处理多个代码点。其次,某些平台(例如 Windows)使用 UTF-16 作为 wchar_t 编码,这意味着单个 wchar_t 甚至不一定是代码点值。 (以这种方式使用 UTF-16 是否符合标准是模棱两可的。标准要求语言环境支持的每个字符都可以表示为单个 wchar_t 值;如果没有语言环境支持 BMP 之外的任何字符,则 UTF-16可以看作是一致的。)

【讨论】:

    【解决方案2】:

    您必须从读取流的第一个或两个字节开始,然后 决定它是否是 BOM 的一部分。有点痛, 因为你只能putback 一个字节,而你通常会 想读四。最简单的解决办法是打开文件,读取 初始字节,记住需要跳过的字节数,然后返回 开始并跳过它们。

    【讨论】:

    • UTF8 BOM 的长度为 三个 字节。我假设流是字节大小的,因为它是char-stream,所以它不可能是 UTF16 或 UTF32。
    • @KerrekSB 您可以将 UTF-16 和 UTF-32 读取为 char 流,前提是您具有适当的语言环境。另一方面,我不知道他们会用 BOM 做什么。 (恕我直言,BOM 确实应该是流的责任。或者更确切地说是它使用的 codecvt 方面。)
    • 我忘记了语言环境。您必须自己编写,还是标准中有 UTF-16 的?
    • @KerrekSB 标准中唯一的本地是“C”。其余的,这一切都取决于实现。对于 Linux,您可以通过列出 /usr/lib/locale 查看可用的语言环境。但是,我不知道 Windows 有什么等价物。
    【解决方案3】:

    使用不太干净的解决方案,我通过删除非打印字符来解决:

    bool isNotAlnum(unsigned char c)
    {
        return (c < ' ' || c > '~');
    }
    

    ...

    str.erase(remove_if(str.begin(), str.end(), isNotAlnum), str.end());
    

    【讨论】:

      【解决方案4】:

      这是一个简单的 C++ 函数,用于在 Windows 上跳过输入流的 BOM。这假定字节大小的数据,如 UTF-8:

      // skip BOM for UTF-8 on Windows
      void skip_bom(auto& fs) {
          const unsigned char boms[]{ 0xef, 0xbb, 0xbf };
          bool have_bom{ true };
          for(const auto& c : boms) {
              if((unsigned char)fs.get() != c) have_bom = false; 
          }
          if(!have_bom) fs.seekg(0);
          return;
      }
      

      它只是检查 UTF-8 BOM 签名的前三个字节,如果它们都匹配则跳过它们。没有BOM就没有坏处。

      编辑:这适用于文件流,但不适用于cin。我发现它确实可以在带有 GCC-11 的 Linux 上与 cin 一起使用,但这显然不是可移植的。请参阅下面的@Dúthomhas 评论。

      【讨论】:

      • 这个问题已有 10 年历史了,您的解决方案假设一个可搜索的流提供字节大小的数据。例如,std::cin 是not 可搜索的,这意味着如果第一个字符不是 UTF-8 BOM,则 skip_bom() 使输入状态与程序想要获得的状态不同步。 — 正确方法是将流作为字节流打开以识别其类型,关闭它,然后返回一个new文件对象读取特定的流类型并返回 代码点,而不是字节字符。
      • @Dúthomhas - 这是有道理的 - 谢谢。有趣的是,这适用于具有 GCC 11 的 Linux,并且在 seekg() 调用之后,rdstate 为 0。但它似乎不适用于 macOS 上的 Clang。我会采纳你的建议并重新考虑我的方法。
      • 如果您暂时仅出于测试BOM的唯一目的而打开文件,您的方法就可以正常工作。 (这实际上是识别文件类型和格式的常用方法。一旦识别出文件类型,调用者就可以选择如何处理它。)因此,函数的调用者可以确定文件是有效的 UTF-8 字节流或没有完整的 BOM。 (部分 BOM 表示它不是 UTF-8。)
      • AFAIK,回溯 cin 是一种与编译器 + 操作系统 + 流源相关的实现能力。但是,如果cin 是键盘上的人,那么我所知道的操作系统不支持任何类型的向后搜索。它可能不会报告失败,但输入指针实际上并没有改变。
      • @Dúthomhas - 我最初的测试是使用管道文件。 GCC/Linux 必须将其作为文件流处理。无论如何,我已经更新了答案以解释它不适用于 cin。再次感谢。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-02-24
      • 1970-01-01
      • 1970-01-01
      • 2010-12-22
      相关资源
      最近更新 更多