初一看这个标题,稍微做过C++编程的人都不认为这是什么困难的事情,但细细想起来还是有些需要注意的。其中我想最主要的就是文本文件的格式了,我们目前常见的有下面这四种,也就是Windows的记事本所能保存的那四种格式了:
1,ANSI,也是我们最最常见的文本文件格式,在中文操作系统中,使用默认的GBK编码,而港台用的繁体中文操作系统则默认使用Big5码,简体中文操作系统打开Big5码的文本文件,会显示乱码,反之亦然,ANSI是ASCII的超集,所以英文总是能正常显示,英文占一个字节,中文占两个字节,所以光凭文件大小是不知道字符数的。
2,Unicode(Little endian,LE),这是Windows默认的Unicode编码,每个字符都是占据两个字节,全球统一,所以Unicode编码的文本文件都不会显示乱码,只可能由于缺乏字体的支持而显示出小方块(默认是小方块,也可能是别的)。准确说Unicode的字符并不一定是占据两个字节,但在Windows环境下这么认为是没有任何问题的。
3,Unicode(Big endian,BE),同上,唯一不同的是字节序,貌似这是Mac系统默认的编码格式。比如“中”字的Unicode(LE)编码是“2D 4E”,而Unicode(BE)的编码这是“4E 2D”。
4,UTF-8,和Unicode编码是一一对应的关系,并且兼容ASCII,所以UTF-8编码的文本文件同ANSI编码的那样,英文总是能正常显示,而它每个字符所占据的字节也是不确定的,可能占据一到六个字节,和Unicode不同,UTF-8并没有字节序一说,所以它往往被用作文本传输的标准格式,实现文本的跨平台传输。
别的我知道的还有UTF-16等格式,由于用得少,就不提了。另外,对于以上各类格式,如果有必要,还要区分Windows版,Unix版和Mac版,它们的关键区别在于对换行的理解,Windows版的换行其实是“回车字符”+“换行字符”,也就是0x0D+0x0A,而Unix版的只有“换行符”0x0A,Mac版的只有“回车符”0x0D,真是有趣极了。
那么,当你试图打开一个文本文件的时候,你会以哪种格式去“阅读”它呢?这让我想起以前我曾经工作过的一家公司,把一个任务交给我,就是做文件分类,其中有一个类型就是文本文件,这十分让我头疼,因为文本文件没有固定的格式,所以只好根据一些字符来判断,这个是不一定准的。通常,为了区分文本文件的格式,文本文件编辑器通常都会给文本文件添加一个叫BOM的标志,BOM是Byte Order Marked的缩写:
ANSI:没有BOM,直接是内容。
UNICODE(LE):FF FE
UNICODE(BE):FE FF
UTF-8:EF BB BF
所以我们可以根据这些特征来判断一个文本文件的编码格式。
那如果一个Unicode文件没有BOM,我们把它判定为ANSI格式的,岂不是乱了套?那是肯定的,但有些比较高级的文本编辑器,如UltraEdit,就有智能识别文本格式的功能,即便文本文件缺乏BOM,但这个我们就不讨论了。
也许你要问了:“有那么繁琐么?我只是想用C运行库获取文本内容。”在Windows环境下,目前还算比较简单了,VC++2005的运行库已经支持读取Unicode和UTF-8格式的文本文件,而下面我给出一个简单的例子,是读取一个UTF-8格式的文本文件的。
#include "stdafx.h"
#include <windows.h>
//test_utf8.txt的内容是四个汉字:“中文测试”
//一共占据15个字节,分别是:
//EF BB BF E4 B8 AD E6 96 87 E6 B5 8B E8 AF 95
//其中“EF BB BF”为BOM(Byte Order Mark),之后每个汉字占3个字节
int _tmain(int argc, _TCHAR* argv[])
{
WCHAR szDataAll[64];
FILE* pf = _wfopen(L"test_utf8.txt", L"r,ccs=utf-8");
if (pf!=NULL)
{
long pos = ftell(pf); //3
ZeroMemory(szDataAll, sizeof(szDataAll));
fread(szDataAll, 2, 1, pf);
pos = ftell(pf); //9
OutputDebugStringW(szDataAll); //中
ZeroMemory(szDataAll, sizeof(szDataAll));
fread(szDataAll, 2, 1, pf);
pos = ftell(pf); //11
OutputDebugStringW(szDataAll); //文
ZeroMemory(szDataAll, sizeof(szDataAll));
fread(szDataAll, 2, 1, pf);
pos = ftell(pf); //13
OutputDebugStringW(szDataAll); //测
ZeroMemory(szDataAll, sizeof(szDataAll));
fread(szDataAll, 2, 1, pf);
pos = ftell(pf); //15
OutputDebugStringW(szDataAll); //试
fclose(pf);
}
return 0;
}
需要注意的是,使用fopen的时候,记得使用其宽字符版_wfopen,另外,注意fopen的第二个参数“ccs=utf-8”,是“ccs”而不是“css”,写错的话是无效的,这样就能直接把UTF-8的文本读进来,而不用管BOM,也不需要额外的转换,直接就已经是Unicode编码了。
注意上面我使用了ftell来测试文件指针的位置,看起来文件指针的行为确实有些怪异,貌似ftell使用起来不灵了,这个时候,这是我们要注意的一个地方;另一个要注意的地方就是fread的第二个参数,我写了2,其实指的是读进来的Unicode编码的字节数,要读一个字符,那就写2,读两个字符,那就写4,而不是UTF-8的3个字节一个汉字的这种长度。
如果你要读取一个Unicode(LE)的文本文件,将fopen的“ccs=utf-8”参数改为“ccs=unicode”即可。
这都是你已经知道了文件格式的前提下,所使用的方法,如果文件格式未知,你还得手工判断一下,先用“_wfopen(L"abc.txt", L"rb")”这种方式打开文件,再读取头几个字节来分析。
遗憾的是,“ccs=utf-8”这种参数并不是C的标准,这是Microsoft VC++的功能,并且我发觉Windows Mobile平台不能这样用,so,下面我就只好完全自己动手丰衣足食了,总的思路就是:判断文件格式,根据格式类型和该格式类型的标准,读取一定字符数目(究竟读取多少字节,要计算),然后利用Windows的API,MultiByToWideChar将其转为Unicode,当然了,如果文件就是Unicode(LE)的话,处理掉BOM就可以直接读取了,如果是Unicode(BE)的话,得倒一下字节序。
下面给出我实现的类的代码。
这是头文件TxtReader.h:
#pragma once
#include <windows.h>
#include <stdio.h>
enum
{
TXT_TYPE_NONE = 0,
TXT_TYPE_ANSI,
TXT_TYPE_UNICODE_LE,
TXT_TYPE_UNICODE_BE,
TXT_TYPE_UTF8
};
class CTxtReader
{
public:
CTxtReader(void);
~CTxtReader(void);
BOOL Open(WCHAR* pFileName);
void Close();
BOOL Read(WCHAR* pBuff, DWORD dwToRead, DWORD& dwRead);
LONG Tell();
protected:
FILE* m_pFile;
INT m_iType;
CPINFO m_codepage;
INT m_iMaxLeadBytePairNum;
BOOL NeedNextByte(BYTE byFirstByte);
};
这是CPP文件TxtReader.cpp:
#include "TxtReader.h"
CTxtReader::CTxtReader(void)
{
m_pFile = NULL;
m_iType = TXT_TYPE_NONE;
GetCPInfo(CP_ACP, &m_codepage);
m_iMaxLeadBytePairNum = 0;
int i;
for(i=0; i<5; i++)
{
if(m_codepage.LeadByte[i*2]==0 && m_codepage.LeadByte[i*2+1]==0)
break;
++m_iMaxLeadBytePairNum;
}
}
CTxtReader::~CTxtReader(void)
{
Close();
}
BOOL CTxtReader::Open(WCHAR* pFileName)
{
Close();
m_pFile = _wfopen(pFileName, L"rb");
if (m_pFile==NULL)
return FALSE;
BYTE byBOM[3];
size_t stRead = fread(byBOM, 1, 3, m_pFile);
if (stRead==3 && byBOM[0]==0xEF && byBOM[1]==0xBB && byBOM[2]==0xBF)
m_iType = TXT_TYPE_UTF8;
else if (stRead>=2 && byBOM[0]==0xFF && byBOM[1]==0xFE)
{
m_iType = TXT_TYPE_UNICODE_LE;
fseek(m_pFile, 2, SEEK_SET);
}
else if (stRead>=2 && byBOM[0]==0xFE && byBOM[1]==0xFF)
{
m_iType = TXT_TYPE_UNICODE_BE;
fseek(m_pFile, 2, SEEK_SET);
}
else
{
m_iType = TXT_TYPE_ANSI;
fseek(m_pFile, 0, SEEK_SET);
}
return TRUE;
}
void CTxtReader::Close()
{
if (m_pFile!=NULL)
{
fclose(m_pFile);
m_pFile = NULL;
}
m_iType = TXT_TYPE_NONE;
}
BOOL CTxtReader::Read(WCHAR* pBuff, DWORD dwToRead, DWORD& dwRead)
{
if (dwToRead==0)
return FALSE;
INT iBuffSize;
DWORD dwReadBytes;
DWORD i;
switch (m_iType)
{
case TXT_TYPE_ANSI:
iBuffSize = dwToRead*2; //ANSI's max bytes number of one char is 2;
break;
case TXT_TYPE_UNICODE_LE:
dwReadBytes = fread(pBuff, 1, dwToRead*2, m_pFile); //Each unicode char has two bytes.
if(dwReadBytes>0)
{
dwRead = dwReadBytes/2;
return TRUE;
}
else
return FALSE;
case TXT_TYPE_UNICODE_BE:
dwReadBytes = fread(pBuff, 1, dwToRead*2, m_pFile); //Each unicode char has two bytes.
if (dwReadBytes>0)
{
dwRead = dwReadBytes/2;
for (i=0; i<dwRead; i++)
pBuff[i] = ((pBuff[i]&0xFF)<<8) + ((pBuff[i]>>8)&0xFF);
return TRUE;
}
else
return FALSE;
break;
case TXT_TYPE_UTF8:
iBuffSize = dwToRead*6; //UTF-8's max bytes number of one char is 6
break;
}
BYTE *pByBuff = new BYTE[iBuffSize];
DWORD dwWcharRead = 0; //Read chars(in wide char)
BYTE *pCurrPos = pByBuff;
while (dwWcharRead<dwToRead)
{
if(0==fread(pCurrPos, 1, 1, m_pFile))
break;
BYTE byFirst = *pCurrPos;
++pCurrPos;
switch (m_iType)
{
case TXT_TYPE_ANSI:
if(NeedNextByte(byFirst))
{
fread(pCurrPos, 1, 1,m_pFile);
++pCurrPos;
}
++dwWcharRead;
break;
case TXT_TYPE_UTF8:
if((byFirst|0xDF)==0xDF && (byFirst&0xC0)==0xC0) // 110X XXXX : Two bytes.
{
fread(pCurrPos, 1, 1, m_pFile);
++pCurrPos;
}
else if((byFirst|0xEF)==0xEF && (byFirst&0xE0)==0xE0) //1110 XXXX : Three bytes.
{
fread(pCurrPos, 1, 2, m_pFile);
pCurrPos+=2;
}
else if((byFirst|0xF7)==0xF7 && (byFirst&0xF0)==0xF0) //1111 0XXX : Four bytes.
{
fread(pCurrPos, 1, 3, m_pFile);
pCurrPos+=3;
}
else if((byFirst|0xFB)==0xFB && (byFirst&0xF8)==0xF8) //1111 10XX : Five bytes.
{
fread(pCurrPos, 1, 4, m_pFile);
pCurrPos+=4;
}
else if((byFirst|0xFD)==0xFD && (byFirst&0xFC)==0xFC) //1111 10XX : Six bytes.
{
fread(pCurrPos, 1, 5, m_pFile);
pCurrPos+=5;
}
++dwWcharRead;
break;
}
}
INT iConvertedNum;
BOOL bSucceeded = FALSE;
if(pCurrPos-pByBuff>0)
{
UINT iCP;
switch (m_iType)
{
case TXT_TYPE_ANSI:
iCP = CP_ACP;
break;
case TXT_TYPE_UTF8:
iCP = CP_UTF8;
break;
}
iConvertedNum = MultiByteToWideChar(iCP, 0, (LPCSTR)pByBuff, pCurrPos-pByBuff, pBuff, dwToRead);
if (iConvertedNum>0)
{
dwRead = iConvertedNum;
bSucceeded = TRUE;
}
}
delete[] pByBuff;
return bSucceeded;
}
LONG CTxtReader::Tell()
{
if (m_pFile!=NULL)
return ftell(m_pFile);
return 0;
}
BOOL CTxtReader::NeedNextByte(BYTE byFirstByte)
{
int i;
for(i=0; i<m_iMaxLeadBytePairNum; i++)
{
if(byFirstByte>=m_codepage.LeadByte[i*2] && byFirstByte<=m_codepage.LeadByte[i*2+1])
return TRUE;
}
return FALSE;
}
其中需要特别说明的是GetCPInfo这个API,我用它来获取相关的信息来确定:ANSI的格式下,什么字符需要读取两个字节。我不知道Linux环境下对应的函数是什么,但我想应该会有类似的函数的。
利用这个CTxtReader类,我们就能轻松从四种格式的文本文件里获取到我们指定字符数目的字符串了,而且,Tell方法也可以准确反映出文件指针的位置。