您的位置:首页 > 其它

2D游戏引擎制作:读取XML文件 3

2018-03-03 21:29 399 查看

读取XML文件 3

前言

  经过前两篇的练习,笔者大概知道应该怎么写,本篇想再写一次能通用的读取XML文件的代码,所以上文代码再次全删。

  这篇文章不是新建的而是将上一篇博客:读取XML文件 2的内容全部替换了(只是想复制上篇内容的格式,结果没有新建就在原来文章上改了),读取XML文件 2的内容已经看不到了(感觉上一篇白写了,笔者的心血啊!虽然没人看,技术水平也不高,但也是辛苦了一天的成果)。

  多说一句:番茄助手不想让注释中出现红色波浪线可以取消此复选框。



正文

笔者知道的编码模式是UNICODE、ANSI,当笔者使用notepad++给文件进行转码时,给出编码选项却是这样:



上网一查才知道UCS-2是什么,不清楚的读者可以参考链接:http://www.fmddlmyy.cn/text6.html

http://blog.csdn.net/ztsinghua/article/details/44277833

笔者希望接下来读取XML文件的方法能够通用一点,所以参考了SlimXML的写法。

关于转码部分参考链接:http://blog.csdn.net/x_iya/article/details/16849765

第三阶段:

首先,要读取文件内容,先判断文件使用什么编码规则。UTF_16大端小端和带BOM的UTF_8比较容易分辨,

至于UTF_8还是ANSI,判断过程可以参考链接:http://blog.csdn.net/bladeandmaster88/article/details/54767487

伪代码如下:

首先新建一个文件用来放读取XML文件的代码。

头文件:

#pragma once
#include <fstream>
#include <string>
#include <codecvt>
#include <unordered_map>
#include <stack>
using namespace std;


定义一个用来存储属性的类:

class LLXMLProperty
{
public:
LLXMLProperty(wstring name);
void SetValue(wstring value);
wstring GetName();
wstring GetValue();
int GetValueInt();
float GetValueFloat();
bool GetValueBool();
private:
wstring name;
wstring value;
};

LLXMLProperty::LLXMLProperty(wstring name)
{
this->name = name;
}

void LLXMLProperty::SetValue(wstring value)
{
this->value = value;
}

wstring LLXMLProperty::GetName()
{
return name;
}

wstring LLXMLProperty::GetValue()
{
return value;
}

int LLXMLProperty::GetValueInt()
{
return _wtoi(value.c_str());
}

float LLXMLProperty::GetValueFloat()
{
return _wtof(value.c_str());
}

bool LLXMLProperty::GetValueBool()
{
return !(value == L""

10cae
|| value == L"0"
|| value == L"false"
|| value == L"False"
|| value == L"FALSE")
;
}


定义一个用来存储节点的类(没有写获得子节点和属性的方法,最简单的方法就是将propertyMap和childNodeList改成public,也可以写一个GetNextNode()之类的方法。):

class LLXMLNode
{
public:
LLXMLNode(wstring name);
void AddProperty(LLXMLProperty* llProperty);
void AddNode(LLXMLNode* llNode);
wstring GetName();
private:
wstring name;
unordered_map<wstring, LLXMLProperty*> propertyMap;
list<LLXMLNode*> childNodeList;
};

LLXMLNode::LLXMLNode(wstring name)
{
this->name = name;
}

void LLXMLNode::AddProperty(LLXMLProperty* llProperty)
{
propertyMap[llProperty->GetName()] = llProperty;
}

void LLXMLNode::AddNode(LLXMLNode* llNode)
{
childNodeList.push_back(llNode);
}

wstring LLXMLNode::GetName()
{
return name;
}


定义常用文件编码:

enum class FileEncode
{
ANSI,//ANSI编码
UTF_8_WITH_BOM,//UTF_8使用BOM标记
UTF_8_NO_BOM,//UTF_8无BOM标记
UTF_16_LITTLE_ENDIAN,//UTF_16小端,低字节在前,高字节在后
UTF_16_BIG_ENDIAN//UTF_16大端,高字节在前,低字节在后
};


定义一个用来存储XML文件的类:

class LLXMLDocument
{
public:
bool LoadXMLFromFile(wstring filePath);
bool SaveXMLToFile(wstring filePath);
LLXMLNode* GetRootNode();
private:
FileEncode CheckFileEncode(wifstream& file);
bool WCharCanIgnore(wchar_t wc);
bool WCharIsLegalNameStart(wchar_t wc);
bool WCharIsLegalName(wchar_t wc);
bool LoadUnknown(wchar_t*& fileBuffer,int& bufferSize);//不知道接下来的内容,用于判断应该使用哪个方法继续读取。
bool LoadDefine(wchar_t*& fileBuffer, int& bufferSize);//加载声明
bool LoadComment(wchar_t*& fileBuffer, int& bufferSize);//加载注释
bool LoadNode(wchar_t*& fileBuffer, int& bufferSize);//加载节点
bool LoadProperty(wchar_t*& fileBuffer, int& bufferSize);//加载属性
wstring LoadFormatValue(wchar_t*& fileBuffer, int& bufferSize);//加载属性值

LLXMLNode* rootNode;
stack<LLXMLNode*> nodeStack;
};


最关键的就是怎么读取XML文件里的内容:

bool LLXMLDocument::LoadXMLFromFile(wstring filePath)
{
while (!nodeStack.empty())//清空栈
{
nodeStack.pop();
}
wifstream file(filePath, wifstream::binary );//一定要使用二进制读取,否则文件表头会读不到。而且在Windows下,文件中回车是“\n\r”,在获得文件长度时是2,读取却当成一个字符,有可能会影响编码判断。

if (file)
{
FileEncode fe = CheckFileEncode(file);//检查文件编码
int markBufferNum = 0;
int markBufferLength = 0;
switch (fe)
{
case FileEncode::ANSI:
file.imbue(locale(""));
break;
case FileEncode::UTF_8_WITH_BOM:
markBufferNum = 1;
markBufferLength = 3;
case FileEncode::UTF_8_NO_BOM:
file.imbue(locale(locale::empty(), new codecvt_utf8<wchar_t>));
break;
case FileEncode::UTF_16_LITTLE_ENDIAN:
//只有这种编码笔者没有找到可用的编码转换,先空出来,有高手可以帮忙吗?。
markBufferNum = 1;
markBufferLength = 1;
break;
case FileEncode::UTF_16_BIG_ENDIAN:
markBufferNum = 1;
markBufferLength = 1;
file.imbue(locale(locale::empty(), new codecvt_utf16<wchar_t>));
break;
default:
break;
}
file.seekg(0, ios_base::end); // 移动到文件尾。
int fileLength = file.tellg(); // 取得当前位置的指针长度,即文件长度。
file.seekg(0, ios_base::beg);
fileLength++;//需要多一位来存储文件结尾标记。
wchar_t* fileBuffer = new wchar_t[fileLength];
//使用file.read(fileBuffer, fileLength);读取文件时,正文部分正常,结尾处多出一串乱码,不知道原因。
file.get(fileBuffer, fileLength,EOF);// 读取到文件结尾,笔者在网上找到方法都是file.get(fileBuffer, fileLength);而且fileLength不需要加1位,怎么到笔者这儿都有问题?
wchar_t* fileBufferStart = fileBuffer;//标记开头位置,用来delete。
wchar_t* fileBufferEnd = fileBuffer + fileLength;
fileBuffer += markBufferNum;
fileLength -= markBufferLength; //这两行用来移除开头标记,应该移多少是笔者试出来的,有一点需要注意,使用二进制读取后按字符判断和移动可能会有问题,使用read读取出现乱码可能就是这个原因,也不确定。

//开始分析语法。
//秘技:多重判断之术
while (fileBuffer<fileBufferEnd&&fileLength>0&&*fileBuffer!= L'\0')
{
if(WCharCanIgnore(*fileBuffer))//检验是否为可以过滤的字符。
{
fileBuffer++;
fileLength--;
}
else if(!LoadUnknown(fileBuffer, fileLength))//如果需要考虑,再进行判断。
{
delete[] fileBufferStart;
file.close();
return false;
}
}
delete[] fileBufferStart;
//在这儿没有将fileBufferStart==NULL;因为fileBufferStart的作用域结束了,其他地方不会再使用fileBufferStart了,所以应该不会出现野指针的情况吧!
file.close();
return true;
}
else
{
return false;
}
return false;
}
//检查文件编码
FileEncode LLXMLDocument::CheckFileEncode(wifstream& file)
{
FileEncode fileEncode = FileEncode::UTF_8_NO_BOM;//默认UTF_8
wchar_t wch1, wch2;
file.get(wch1);
file.get(wch2);//从文件读取前四个字节
//不同的编码符合不同的规则
if (wch1 == 0xFF && wch2 == 0xFE)
{
fileEncode = FileEncode::UTF_16_LITTLE_ENDIAN;
}
else if(wch1 == 0xFE && wch2 == 0xFF)
{
fileEncode = FileEncode::UTF_16_BIG_ENDIAN;
}
else if (wch1 == 0xEF && wch2 == 0xBB)
{
wchar_t wch3;
file.get(wch3);
if (wch3 == 0xBF)
{
fileEncode = FileEncode::UTF_8_WITH_BOM;
}
}
else
{
//之后就要判断是无BOM型UTF_8还是ANSI,需要将全文遍历找规则,这部分笔者直接拷贝了SlimXML的写法。
//(其实在Window下可以不用考虑无BOM型UTF_8,新建的文件会自动带标志,除非在别处下载的或强行转成无BOM格式,但为了通用性还是需要加上判断。)

file.seekg(0, ios_base::end); // 移动到文件尾。
int fileLength = (int)file.tellg(); // 取得当前位置的指针长度,即文件长度。
fileLength++;//需要多一位来存储文件结尾标记。
file.seekg(0, ios_base::beg); // 移动到文件头。
wchar_t* fileBuffer = new wchar_t[fileLength];
wchar_t* fileBufferStartPos = fileBuffer;
file.get(fileBuffer, fileLength,EOF);
while (fileLength > 0)
{
wchar_t w = *fileBuffer;
if ((w & 0x80) == 0)//0x80代表10000000,该方法判断首位是否为0,以下类似逐位判断。
{
++fileBuffer;
--fileLength;
}
else
{
wchar_t w1 = *(fileBuffer + 1);
if ((w & 0xf0) == 0xe0)
{
if (fileLength < 3)
{
fileEncode = FileEncode::ANSI;
break;
}
if ((w1 & 0xc0) != 0x80 || (*(fileBuffer + 2) & 0xc0) != 0x80)
{
fileEncode = FileEncode::ANSI;
break;
}
fileBuffer += 3;
fileLength -= 3;
}
else if ((w1 & 0xe0) == 0xc0)
{
if (fileLength < 2)
{
fileEncode = FileEncode::ANSI;
break;
}
int a = (w1 & 0xc0);
if (a != 0x80)
{
fileEncode = FileEncode::ANSI;
break;
}
fileBuffer += 2;
fileLength -= 2;
}
else if ((w1 & 0xf8) == 0xf0)
{
if (fileLength < 4)
{
fileEncode = FileEncode::ANSI;
break;
}
if ((w1 & 0xc0) != 0x80 || (*(fileBuffer + 2) & 0xc0) != 0x80 || (*(fileBuffer + 3) & 0xc0) != 0x80)
{
fileEncode = FileEncode::ANSI;
break;
}
fileBuffer += 4;
fileLength -= 4;
}
else
{
fileEncode = FileEncode::ANSI;
break;
}
}
}
delete[] fileBufferStartPos;
file.seekg(0, ios_base::beg);
}
return fileEncode;
}
//可以忽略的字符
bool LLXMLDocument::WCharCanIgnore(wchar_t wc)
{
return (wc == L' ')//半角空格
|| (wc == L' ') //全角空格(输入法快捷键Shift+Space可以切换半角和全角)
|| wc == L'\n' //换行
|| wc == L'\r'//回车(“\r”和“\r\n”编码一样)
|| wc == L'\t'//水平制表符
;
}
//合法的命名开头,只判断首字符。
bool LLXMLDocument::WCharIsLegalNameStart(wchar_t wc)
{
return (L'a' <= wc&&wc <= L'z')
|| (L'A' <= wc&&wc <= L'Z');
}
////合法的命名,除首字符外使用此方法判断。
bool LLXMLDocument::WCharIsLegalName(wchar_t wc)
{
return (L'a' <= wc&&wc <= L'z')
|| (L'A' <= wc&&wc <= L'Z')
|| (L'0' <= wc&&wc <= L'9')
|| (wc == L'_');
}
//在不知道接下来是什么时使用此方法判断,所以叫Unknown。
bool LLXMLDocument::LoadUnknown(wchar_t*& fileBuffer, int& bufferSize)
{
if (*fileBuffer == L'<')//xml的标记。
{
fileBuffer++;
bufferSize--;
if (*fileBuffer==L'?')//声明标志。
{
fileBuffer++;
bufferSize--;
LoadDefine(fileBuffer, bufferSize);
}
else if (*fileBuffer == L'!')//注释标志。
{
if (*(fileBuffer + 1) == L'-'&&*(fileBuffer + 2) == L'-')
{
fileBuffer+=3;
bufferSize-=3;
LoadComment(fileBuffer, bufferSize);
}
else
{
return false;
}
}
else if(*fileBuffer == L'/')//上一个节点结束标志。
{
fileBuffer++;
bufferSize--;
wchar_t* nameStart = fileBuffer;
while (WCharIsLegalName(*fileBuffer))
{
fileBuffer++;
bufferSize--;
if (bufferSize == 0)
{
return false;
}
}
wstring nodeName = wstring(nameStart, fileBuffer - nameStart);
if (nodeStack.top()->GetName() == nodeName&& *fileBuffer==L'>')
{
nodeStack.pop();
fileBuffer++;
bufferSize--;
}
else
{
return false;
}
}
else
{
LoadNode(fileBuffer, bufferSize);//以上情况都不是的情况下开始读取节点。
}
}
else
{
return false;
}
return true;
}
//读取声明和注释的先略过
bool LLXMLDocument::LoadDefine(wchar_t*& fileBuffer, int& bufferSize)
{
while (*fileBuffer != L'>')
{
fileBuffer++;
bufferSize--;
if (bufferSize == 0)
{
return false;
}
}
fileBuffer++;
bufferSize--;
return true;
}

bool LLXMLDocument::LoadComment(wchar_t *& fileBuffer, int & bufferSize)
{
while (!((*fileBuffer == L'-')&& (*(fileBuffer+1) == L'-')&&(*(fileBuffer + 2) == L'>')))
{
fileBuffer++;
bufferSize--;
if (bufferSize == 0)
{
return false;
}
}
fileBuffer+=3;
bufferSize-=3;
return true;
}
//加载节点
bool LLXMLDocument::LoadNode(wchar_t*& fileBuffer, int& bufferSize)
{
//直接开始读首字符,笔者使用NodePad++写XML文件时,’<’和命名之间好像不能有空格,其实加上忽略空格的判断也可以。
if (WCharIsLegalNameStart(*fileBuffer))
{
wchar_t* nameStart = fileBuffer;
fileBuffer++;
bufferSize--;
while (WCharIsLegalName(*fileBuffer))
{
fileBuffer++;
bufferSize--;
if (bufferSize == 0)
{
return false;
}
}
wstring nodeName = wstring(nameStart, fileBuffer- nameStart);
LLXMLNode* node = new LLXMLNode(nodeName);
if (!nodeStack.empty())//使用栈来存节点。
{
nodeStack.top()->AddNode(node);
}
else
{
rootNode = node;//空节点。
}
nodeStack.push(node);
while (true)//循环读取属性,是不是不应该使用while true?
{
while (WCharCanIgnore(*fileBuffer))
{
fileBuffer++;
bufferSize--;
if (bufferSize == 0)
{
return false;
}
}
if (*fileBuffer == L'>')
{
fileBuffer ++;
bufferSize --;
return true;
}
else if (*fileBuffer == L'/'&&*(fileBuffer + 1)==L'>')
{
fileBuffer += 2;
bufferSize -= 2;
nodeStack.pop();
return true;
}
else
{
if (!LoadProperty(fileBuffer, bufferSize))//加载属性
{
return false;
}
}
}
}
else
{
return false;
}
return true;
}
//加载属性,和加载节点类似,先加载属性名,之后判断’=’,然后读取属性值。
bool LLXMLDocument::LoadProperty(wchar_t *& fileBuffer, int & bufferSize)
{
if (WCharIsLegalNameStart(*fileBuffer))
{
wchar_t* nameStart = fileBuffer;
fileBuffer++;
bufferSize--;
while (WCharIsLegalName(*fileBuffer))
{
fileBuffer++;
bufferSize--;
if (bufferSize == 0)
{
return false;
}
}
wstring propertyName = wstring(nameStart, fileBuffer - nameStart);
LLXMLProperty* llProperty = new LLXMLProperty(propertyName);
while (*fileBuffer != L'=')
{
fileBuffer++;
bufferSize--;
if (bufferSize == 0)
{
return false;
}
}
fileBuffer++;
bufferSize--;
while (*fileBuffer != L'"')
{
fileBuffer++;
bufferSize--;
if (bufferSize == 0)
{
return false;
}
}
fileBuffer++;
bufferSize--;
llProperty->SetValue(LoadFormatValue(fileBuffer, bufferSize));//将属性值进行格式化。
if (bufferSize == 0)
{
return false;
}
if (!nodeStack.empty())
{
nodeStack.top()->AddProperty(llProperty);
}
else
{
return false;
}
}
else
{
return false;
}
return true;
}
//格式化属性值,暂时将从起点到符号’”’之间的内容当成属性是,转义字符之类的判断应该在此完成,但先不考虑这么多。
wstring LLXMLDocument::LoadFormatValue(wchar_t*& fileBuffer, int& bufferSize)
{
wchar_t* valueStart = fileBuffer;
while (*fileBuffer!=L'"')
{
fileBuffer++;
bufferSize--;
}
wstring value = wstring(valueStart, fileBuffer - valueStart);
fileBuffer++;
bufferSize--;
return value;
}


至此,读取XML格式的文件基本完成。

尚未解决的问题:

1、 在判断编码和进行语法分析时读取了两次文件,可能会影响速度,可是只读一次的话,先进行编码判断后就要自行转码了,笔者不想在代码里出现大段转码的内容,除非必要,像官方提供的file.imbue(locale(“”));直接转成ANSI来读取是最好的。

2、 全文中使用了fileBuffer和bufferSize,但是笔者发现好像bufferSize好像没有用上,判断文件结尾直接使用”\0”就可以了,要不要删掉它?

3、 格式化属性值还没有完成。

4、 UTF_16 LittleEndian还没有完成。

5、 保存等内容也没有完成。

6、 判断可忽略字符使用while(······)和buffer++,size—次数太多,应该可以精简。

结束语

  不删了,绝对不删了,只不过是读取XML文件,太影响进度了,再有需求直接在上文代码基础上改动就好了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: