读取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;
	}
}
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐