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

读取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""
        || 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文件,太影响进度了,再有需求直接在上文代码基础上改动就好了。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值