忽略C++中的字节顺序标记,从流中读取

Ignore byte-order marks in C++, reading from a stream

本文关键字:读取 顺序 C++ 字节 忽略      更新时间:2023-10-16

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

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

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

您可以将文件打开为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可以被视为符合标准。)

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

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

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

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

这里有一个简单的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评论。