读取txt文件时经常遇到乱码问题,因为txt的解码方式不正确,这就需要判断txt文件的编码方式。常见的txt文本编码格式有ANSI、UTF-8、Unicode、Unicode big endian,这四种编码就不作详细介绍了。判断编码格式可以以二进制方式读取txt文件的数据,通过文件头或者根据各编码格式的规则判断编码格式。相关知识可以参考下下面几篇文章:
1、ASCII码和ANSI码的区别
2、 unicode,ansi,utf-8,unicode big endian编码区别
3、详解“大端”模式和“小端”模式
4、 晰解读C语言中的比特序、字节序、位域、大小端
一、判断编码格式
txt文件一般都有文件头(除ANSI),可以通过文件头判断编码格式。对于没有文件头的txt文件,可以通过编码规则来进行判断。
1、文件头
UTF-8文件头是EF BB BF,Unicode文件头是FF FE,Unicode big endian文件头是FE FF。
2、编码规则
(1)ANSI(GBK):在简体中文Windows操作系统中,ANSI 编码代表 GBK 编码;在英文Windows操作系统中,ANSI 编码代表 ASCII编码;在繁体中文Windows操作系统中,ANSI编码代表Big5。ANSI用0-127范围的一个字节表示英文字符,值为ASCII码;用二个字节表示一个中文字符,第一个字节首位为1(即1xxxxxxx xxxxxxxx)。
(2)UTF-8:是一种变长的编码方式,1~4个字节表示一个字符。单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码,亦即是ASCII码。对于n字节的符号(1<n<5),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10,剩下的没有提及的二进制位,全部为这个符号的unicode码。
1个字节: 0xxxxxxx
2个字节: 110xxxxx 10xxxxxx
3个字节: 1110xxxx 10xxxxxx 10xxxxxx
4个字节: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
(3)Unicode:2个字节表示一个字符,数值为Unicode码的反序值(例:”严”的Unicode码为4E25,Unicode编码则为25 4E)
(4)Unicode big endian:2个字节表示一个字符,数值为Unicode码(例:”严”的Unicode码为4E25,Unicode big endian编码则为4E 25)
3、注意:
(1)通过位操作判断位值时,一定要先判断位序,再根据位序读取位置,否则读取所得位值数据不符合编码规则
(2)由于有些字符均满足UTF-8和GBK编码规则,即使限定4个连续字符满足UTF-8编码规则判为UTF-8格式,仍有可能有GBK格式被误判为UTF-8格式导致乱码。如果追求精准,可以去掉4个连续字符的限制,遍历所有数据,均满足UTF-8编码规则判为UTF-8格式。但即便如此还是有缺陷无法解决,当字符串过短时,仍有误判导致乱码的问题,如“力挺联通”就会误判编码格式导致乱码。但该方法已能满足日常txt的读取需求。
#include <bitset>
#define GetBit(v, n) ((v) & ((UINT32)1 << (n))) //获取v 的第 n 位
//判断是否是大端位序,例:1的二进制为00000001,大端位序显示为10000000,小端位序显示为00000001
boolean getIsbigendian_bit()
{
char a = 1;
if (GetBit(a, 0) >> 0)
return true;
return false;
}
//判断字符串编码格式
Encode getEncode(uint8_t* data, size_t size, int ChineseNumber=4)
{
//根据文件头判断编码格式
//在简体中文Windows操作系统中,ANSI 编码代表 GBK 编码;在英文Windows操作系统中,ANSI 编码代表 ASCII编码;在繁体中文Windows操作系统中,ANSI编码代表Big5。
if (size > 2 && data[0] == 0xFF && data[1] == 0xFE)
return Encode::UTF16_LE;
else if (size > 2 && data[0] == 0xFE && data[1] == 0xFF)
return Encode::UTF16_BE;
else if (size > 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF)
return Encode::UTF8_BOM;
else//根据编码规则判断编码格式
{
//判断位序大小端
boolean isbigendian = getIsbigendian_bit();
//无文件头根据编码规律来判断编码格式
//UTF-8的编码规则很简单,只有二条:
//1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF - 8编码和ASCII码是相同的。
//2)对于n字节的符号(n>1),第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。
//取第一个字节判断第一位是否为1,以及获取第一位为1时后面位连续为1的数量
int byte = 0;
int utf8number = 0;
//如果是大端位序,从右边开始读取位;如果是小端位序,从左边开始读位
if (isbigendian)
{
for (int i = 0; i < size; i++)
{
if (GetBit(data[i], 7) >> 7 == 1)
{
//取一个字节判断第一位是否为1,以及获取第一位为1时后面位连续为1的数量
int byte = 0;
for (int j = 7; j >= 0; j--)
{
if ((GetBit(data[i], j) >> j) == 1)
byte++;
else
break;
}
//若byte等于0,则非中文,中文数量清零
if (byte ==0)
utf8number = 0;
//如果该字节开头几位连续为1,且数量byte超过1,则判断d该自己后面byte-1个字节是否符合UTF-8编码规则;
//但符合UTF-8编码规则的,也可能与GBK重合,需继续遍历后面作判断;
//UTF-8编码规则来判断后面字节,若是字节数据是UTF-8格式,则都符合UTF-8编码规则;若字节数据是GBK格式,则一定会有不符合UTF-8编码规则的字节段
//此方法下字节数据如果是GBK格式,则遍历到中文字符就会很快出现不符合UTF-8格式的现象;如果是UTF-8格式,则需遍历所有数据
//可以限定四个连续字符满足UTF-8编码规则时,可判为UTF-8,这样就无需遍历所有数据,四个连续字符满足UTF-8编码规则和GBK编码规则的概率较少
//若UTF-8与GBK重合的字符以UTF-8规则编译为中文,且连续为中文的数量为4(ChineseNumber)时,判为UTF-8,否则判为GBK
//如果该字节开头连续为1的数量等于1,则是GBK编码方式
if (byte > 1)
{
bitset <8> head_bit_match[] = { data[i], data[i + 1], data[i + 2], data[i + 3] };
boolean isutf8 = true;
for (int j = 1; j < byte; j++)
{
if (!(head_bit_match[j][7] == 1 && head_bit_match[j][6] == 0))
isutf8 &= false;
}
if (isutf8)
{
utf8number++;
i += (byte - 1);
if (utf8number >= ChineseNumber)
return Encode::UTF8;
}
else
return Encode::ANSI;
}
else if (byte == 1)
return Encode::ANSI;
}
}
}
else
{
for (int i = 0; i < size; i++)
{
if (GetBit(data[i], 0) >> 0 == 1)
{
//取一个字节判断第一位是否为1,以及获取第一位为1时后面位连续为1的数量
int byte = 0;
for (int j = 0; j < 8; j++)
{
if ((GetBit(data[i], j) >> j) == 1)
byte++;
else
break;
}
//若byte等于0,则非中文,中文数量清零
if (byte == 0)
utf8number = 0;
//如果该字节开头几位连续为1,且数量byte超过1,则判断d该自己后面byte-1个字节是否符合UTF-8编码规则;
//但符合UTF-8编码规则的,也可能与GBK重合,需继续遍历后面作判断;
//UTF-8编码规则来判断后面字节,若是字节数据是UTF-8格式,则都符合UTF-8编码规则;若字节数据是GBK格式,则一定会有不符合UTF-8编码规则的字节段
//此方法下字节数据如果是GBK格式,则遍历到中文字符就会很快出现不符合UTF-8格式的现象;如果是UTF-8格式,则需遍历所有数据
//可以限定四个连续字符满足UTF-8编码规则时,可判为UTF-8,这样就无需遍历所有数据,四个连续字符满足UTF-8编码规则和GBK编码规则的概率较少
//若UTF-8与GBK重合的字符以UTF-8规则编译为中文,且连续为中文的数量为4(ChineseNumber)时,判为UTF-8,否则判为GBK
//如果该字节开头连续为1的数量等于1,则是GBK编码方式
if (byte > 1)
{
bitset <8> head_bit_match[] = { data[i], data[i + 1], data[i + 2], data[i + 3] };
boolean isutf8 = true;
for (int j = 1; j < byte; j++)
{
if (!(head_bit_match[j][0] == 1 && head_bit_match[j][1] == 0))
isutf8 &= false;
}
if (isutf8)
{
utf8number++;
i += (byte - 1);
if (utf8number >= ChineseNumber)
return Encode::UTF8;
}
else
return Encode::ANSI;
}
else if (byte == 1)
return Encode::ANSI;
}
}
}
return Encode::ANSI;
}
}
二、读取txt文件
1、txt数据容器ArrayList
由于UTF-8数据和ANSI数据字符的字节数不定,以二进制读取时分段解码需要判断字符完整以及缓存字符字节片段,比较麻烦,且txt文件不会很大,可以用一个容器将所有数据保存下来,可自定义一个简单的可变长数组的类来作容器。
ArrayList.h
#pragma once
#include "stdafx.h"
//可变长数组
template <class T>
class ArrayList
{
public:
T* buf = NULL;
int size = 0;
int datasize = 0;
ArrayList()
{
}
ArrayList(int size)
{
buf =(T*) malloc(size);
this->size = size;
}
~ArrayList()
{
if (buf != NULL)
free(buf);
size = 0;
datasize = 0;
buf == NULL;
}
void add(T*buf,int size)
{
//数组没有分配内存时,分配内存并复制buf数据,否则直接将buf数据复制到数组中
if (this->buf == NULL)
{
this->buf = (T*)malloc(size);
this->size = size;
this->datasize = size;
memcpy(this->buf, buf, size);
}
else
{
//数组内存充足时,直接将buf数据复制到数组中,否则先重新分配内存,再将数据迁移过去并将buf数据复制到数组中
if (this->datasize + size <= this->size)
{
memcpy(this->buf + this->datasize, buf, size);
this->datasize += size;
}
else
{
int addsize = this->datasize + size - this->size;
T* temp = (T*)malloc(this->size + addsize);
memcpy(temp, this->buf, this->datasize);
memcpy(temp + this->datasize, buf, size);
free(this->buf);
this->buf = temp;
this->size += addsize;
this->datasize += size;
}
}
}
};
2、读取txt文件以及转码
判断Unicode、Unicode big endian时,需要先判断字节序,再转码,否则会出现乱码。TypeFormat.h是一个封装好的数据类型转换类,代码中使用到的函数网上搜下类型转换方法替换掉就行。
#include "ArrayList.h"
#include "TypeFormat.h"
enum Encode { ANSI = 1, UTF16_LE, UTF16_BE, UTF8_BOM, UTF8 };
//判断是否是大端字节序,例:大端位序时,wchar_t数据类型1的二进制为00000000 10000000,大端字节序显示为00000000 10000000,小端字节序显示为10000000 00000000
boolean getIsbigendian_byte()
{
wchar_t a = 1;
char* s = (char*)&a;
_cprintf("%x %x\n", s[0], s[1]);
if (s[0] == 1)
return false;
return true;
}
//Unicode big endian 和Unicode little endian互转
wchar_t * Unicode_BL_ByteReverse(char* str, int size, boolean isBOM = true)
{
if (size % 2 == 0)
{
char temp;
int start = 0;
if (isBOM)
start = 2;
for (int i = start; i < size; i += 2)
{
temp = str[i+1];
str[i + 1] = str[i];
str[i] = temp;
}
return (wchar_t *)str;
}
else
return NULL;
}
//对字符串进行转码
wchar_t* Convert(char* str, int targetCodePage)
{
int iunicodeLen = MultiByteToWideChar(targetCodePage, 0, str, -1, NULL, 0);
wchar_t *pUnicode = NULL;
pUnicode = new wchar_t[iunicodeLen + 1];
memset(pUnicode, 0, (iunicodeLen + 1)*sizeof(wchar_t));
MultiByteToWideChar(targetCodePage, 0, str, -1, (LPWSTR)pUnicode, iunicodeLen);//映射一个字符串到一个款字节中
return pUnicode;
}
//MFC窗口按钮单击事件响应函数
void CtestDlg::OnBnClickedButton1()
{
CString file("test.txt");
showdlg->novaltxt = NULL;
char str[1024 * 10];
char end_a = '\0';
wchar_t end_u = '\0';
memset(str, 0, 1024 * 10);
FILE* f = fopen(typeformat.tochar(file), "rb");
int size = filelength(fileno(f));
ArrayList<char> arraylist(size);
int red = 0;
int end_red = 0;
while (red = fread(str, 1, 1024 * 10, f))
{
end_red += red;
arraylist.add(str, red);
memset(str, 0, 1024 * 10);
}
fclose(f);
//转码
int encodelabel[] = { CP_ACP, CP_UTF8, CP_UTF7 };
//判断字节序大小端
boolean isBigendian_byte = getIsbigendian_byte();
wchar_t* buf = NULL;
switch (getEncode((uint8_t*)arraylist.buf, arraylist.size))
{
case ANSI:
arraylist.add(&end_a, 1);//toCString时需要通过str里的'\0'判断字符串结束,否则转换出来的CString末端有野数据
showdlg->novaltxt = typeformat.toCString(arraylist.buf);
break;
case UTF16_LE:
arraylist.add((char*)&end_u, 2);//toCString时需要通过str里的'\0'判断字符串结束,否则转换出来的CString末端有野数据
if (isBigendian_byte)
buf = Unicode_BL_ByteReverse(arraylist.buf, arraylist.size);
else
buf = (wchar_t *)arraylist.buf;
showdlg->novaltxt = typeformat.toCString(buf);
break;
case UTF16_BE:
arraylist.add((char*)&end_u, 2);//toCString时需要通过str里的'\0'判断字符串结束,否则转换出来的CString末端有野数据
if (isBigendian_byte)
buf = (wchar_t *)arraylist.buf;
else
buf = Unicode_BL_ByteReverse(arraylist.buf, arraylist.size);
showdlg->novaltxt = typeformat.toCString(buf);
break;
case UTF8_BOM://有文件头utf-8
arraylist.add(&end_a, 1);//toCString时需要通过str里的'\0'判断字符串结束,否则转换出来的CString末端有野数据
buf = Convert(arraylist.buf, encodelabel[1]);
showdlg->novaltxt = typeformat.toCString(buf);
break;
case UTF8://无文件头utf-8
arraylist.add(&end_a, 1);//toCString时需要通过str里的'\0'判断字符串结束,否则转换出来的CString末端有野数据
buf = Convert(arraylist.buf, encodelabel[1]);
showdlg->novaltxt = typeformat.toCString(buf);
break;
default:
break;
}
}