在项目程序中,用fgets()读一个文本文件,一行一行的读取,之前测试的所有的文件都没有问题,今天突然拿到一个文件测试时却出问题了,调试发现它并没有一行一行的读,但是用notepad++明明可以看到文件是一行一行的啊,又不是没有回车,纠结一会,发现这个错误的文件是mac格式的,关于unix/mac/dos-windows三种文本文件的格式的行尾区别,以下是搜索到的资料:
不同系统间的行尾表示
UNIX格式,每行的行尾都是用一个0x0a字符(换行字符LF)表示的,
而在WINDOWS/DOS下每行的行尾都是用0x0d 0x0a两个字符(回车字符CR,换行字符LF)表示的,
MAC机,每行的行尾都是0x0d字符表示,即回车字符CR。
(经我用winhex查看,确实如此——yockie注)
在很多计算机语言中, <CR>表示为字符或字符串是\r, 而<LF>表示为字符或字符中是\n.
而在printf这样的函数中, 如果你出现一个\n, 它却代表了一个逻辑上的意义, 即它代表在本地系统上的那种回车换行, 所以它在UNIX上只是<LF>, 但如果你在DOS上用诸如Truboc编程的话它输出的是<CR><LF>
所以不同系统之间传输交换文件的时候,会出现问题;
1)Unix或linux上有个小工具, 专门在两种文件格式之间进行转换, 叫unix2dos, dos2unix.或在
vim中, 如果你想把一个文件存为另一种文件格式, 只需:set fileformat=unix或等号后面是dos, mac即可, 接下来的存盘动作就会自动把你的文件存为指定平台的格式.
2)在UltraEdit的 File->Conversions(有些版本是在Format -> Convert to..)菜单中, 有三个子菜单项就是专供你在各种文本格式之间转换的.
必须先将UNIX文本格式转换为DOS格式的。UltraEdit的FILE菜单中提供了对单个文件进行这种转换的能力,也可以实现批量转换。在UE中选择Search | Search in files菜单,在Find框中填写^n(表示UNIX文件中的回车换行符号),在Replace with中输入^p(表示DOS文件中的回车换行符号),全部替换后,就可以实现批量替换了。
不同系统间ftp使用注意事项
一些ftp的客户端程序会自动进行各个平台的文本文件格式转换. 所以要注意你在ftp中用ls看看到的文件大小未必是你下载下来的文件大小. 上传过程也是一样.
如果你不想ftp这样自作主张只要用bin命令即可. 它会告诉ftp进行逐字节的二进制模式传输.
[以上内容转自:http://hi.baidu.com/dongfangronger/blog/item/a46e56f0fd2129e27709d70f.html]
这就找到原因了,应该是fgets()函数是以读到'\n'为行结束标识(因为windows格式和unix格式的文本文件都能一行一行的读取),所以对于'\r'它并不识别为行结束标识。我是通过将mac格式文件转换为 windows格式或unix格式来解决这个问题的。
当然还有其他的一些方法,例如在http://stackoverflow.com/questions/2061334/fgets-linux-vs-mac这里提到了用getdelim()或getline()函数来解决这个问题,因为在这两个函数中用个参数表示终结符,也就是说自己说明什么字符是终结符。不过我没有尝试。我想如果要做到通用的话,还得判断文本文件是什么格式的文件,也好知道传入哪一个终结符的参数。
关于判断文本文件是什么格式的,上网搜到如下内容,没怎么仔细看:
(以下内容转自http://www.cppblog.com/guogangj/archive/2011/06/08/148257.html)
初一看这个标题,稍微做过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 < 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:
#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:
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方法也可以准确反映出文件指针的位置。