跨平台项目 utf8文本编码在windows上的问题

发布时间 2023-08-30 15:45:20作者: 马肯尼煤牙巴骨

跨平台项目文本编码问题

1.背景

linux系统和mac os默认使用utf8编码,windows系统上通常是跟随系统设置,如果系统选择为中文地区的话,默认为GBK编码。

windows 10 1703开始,支持把windows编码设置为utf8 。

IDE上一般也是默认是这个配置,但是IDE可以选择编码源码保存为什么格式。

2.跨平台项目遇到的问题

由于linux和windows共用一个代码库,所以源码都使用同一编码格式,在我的情况,我把源码保存为utf8,并在windows上把ide配置为 utf8 不带BOM, FL 格式

BOM,windows字符前加上额外字节的信息,用来让windows认识字符串属于哪种编码

FL , 换行符windows上是CRFL, Mac os是CR ,Linux是FL

同时windows上把编译器设置为输出utf8 格式,

qmake使用 CONFIG += utf8_source 配置,它会根据编码器自动加上utf8的编码选项

至此为止,世界如此美好, 一切显示正常,甚至还能用emoji 表情字符

直到打开文件发现打不开, 调用系统命令行无故执行错误 ,控制台显示乱码

程序中的字符和系统发生交互时,终于出现了问题

3.解决方式

3.1 使用windows api转换utf8为gbk

#include <sstream>
#include <locale>
#include <codecvt>


inline std::string utf8_to_gbk(const std::string& str)
{
    std::wstring_convert<std::codecvt_utf8<wchar_t>> conv;
    std::wstring tmp_wstr = conv.from_bytes(str);
    //GBK locale name in windows
    const char* GBK_LOCALE_NAME = ".936";
    std::wstring_convert<std::codecvt_byname<wchar_t, char, mbstate_t>> convert(new std::codecvt_byname<wchar_t, char, mbstate_t>(GBK_LOCALE_NAME));
    return convert.to_bytes(tmp_wstr);
}

inline std::string gbk_to_utf8(const std::string& str)
{
    //GBK locale name in windows
    const char* GBK_LOCALE_NAME = ".936";
    std::wstring_convert<std::codecvt_byname<wchar_t, char, mbstate_t>> convert(new std::codecvt_byname<wchar_t, char, mbstate_t>(GBK_LOCALE_NAME));
    std::wstring tmp_wstr = convert.from_bytes(str);
    std::wstring_convert<std::codecvt_utf8<wchar_t>> cv2;
    return cv2.to_bytes(tmp_wstr);
}

每当需要和系统交互时,先把utf8转换为gbk ,例如 打开文件,调用系统命令行

如果调用系统管道,返回了字符串,分别调用子进程,重定向了字符回显,也需要返回了字符串是gbk,要在程序里转换为utf8

这种方式比较麻烦,但也比较通用,只要是字符串就能转

3.2 使用 c++17 filesystem 解决文件路径

filesystem在gcc 5.4仍然属于实验阶段的功能,所以在gcc 5.4中,它的名称空间和头文件位置多了一级experimental

//gcc 5.4
#include <experimental/filesystem>

int main()
{
    std::experimental::filesystem::path .....
}

如果是gcc 7 则需要去掉experimental这级 名称

#include <filesystem>


void testFilePath(std::string filename)
{
    std::ifstream file(filename);
    if(file.good())
        std::cout << filename << "  存在"<< std::endl;
    else
        std::cout << filename << "  不存在"<< std::endl;
}


int main()
{
    //全是ascii
    testFilePath("d:/privatekey.pem"); 
    
    testFilePath("d:/数据判定.csv"); 

    std::filesystem::path path = std::filesystem::u8path("d:/数据判定.csv");
    std::ifstream file(path);
    if(file.good())
        std::cout << path << "  存在"<< std::endl;
    else
        std::cout << path << "  不存在"<< std::endl;
    
        
}

上面的代码如果使用utf8编译选项,且源码保存为utf8,且在代码页设置为utf8的控制台上运行,你将看到输出

d:/privatekey.pem  存在
d:/数据判定.csv  不存在
"d:/�����ж�.csv"  存在

这种方式可以十分方便的兼容多个平台,但std::filesystem::path只有c++17才有,并且,不能直接和std::string转换,

但是上面的示例代码使用std::cout 输出是正常的,只是被加上了引号, 说明可以使用流的方式写入std::string里,只是可能会被加上引号。

3.3 使用windows api解决 gbk系统环境控制台显示程序输出乱码

这种情况,可以使用3.1中的那种文件转换utf8编码,但是如果不小心把gbk转为gbk, 程序就会崩溃, 打印子进程回显时踩了一次坑

可以使用windows设置程序输出到控制台的代码页

#ifdef _MSC_VER
#define _AMD64_
#include <ConsoleApi2.h>
#include <winnls.h>
#endif


int main(int argc, const char** argv)
{
    
#ifdef _MSC_VER
    SetConsoleOutputCP(CP_UTF8);
    SetConsoleCP(CP_UTF8);
#endif

    
    
}

世界又变更美好了