English
 电子信箱
 加入收藏

  威盾防火墙 >> 新闻中心 >> 业界动态 >> 用状态机实现XML解析器 - C++环境

 

用状态机实现XML解析器 - C++环境

威盾防火墙 2015-01-26

 
这是我3年前写的代码,用C++实现一个XML解析器.现在再翻出来看,觉得还是有些可取之处,尤其是实现XML文本解析时采用的状态切换法 (姑且先这么叫吧,后文有详细解释这个方法的实现)不仅仅可以用来解析XML,几乎所有的文本流都可以用这种方法来解析 (我记得以前上编译原理时,讲到过词法分析器,用状态机 ,方法类似, 看来上课还是要认真听讲,不定什么时候就用上了.) 同时也有一些不足,主要是当时对UNICODE编程还懵懵懂懂,导致接口全是多字节的.所以要把我的代码加到UNICODE环境下还要做一些修改. 还有很重要的一点要事先说明:我对XML标准并没有做太多研究,写这些代码以实用为主,为的就是让我的程序有一个很简单快捷的方式读取,修改,保存XML文件,所以可能有相当一部分的XML特性没有实现,如果只是使你的C++程序可以使用XML文件作为你的配置文件,(INI文件过于简单了)那么我这个XML解析器还是很方便的. XML文档的基本概念 字符存储要面对编码问题,我们在中文环境下,最常碰到的就3种编码方式: GB2312, UTF8 和Unicode. 根据XML标准,XML文件应该在第一行标明编码方式: . 我的做法是:不管它存储为什么编码方式,读到内存后,统统给它转化为宽字符(UNICDOE). 现在就可以把XML文件看作一个宽字符流 ,这点很重要,是我实现解析器的前提. XML文档是一个结构化的文档,一个XML文档对应一棵树.XML树由节点构成,XML里有以下几种节点: enum xmlnode_type { et_none = 0, et_xml, // et_comment, // et_normal, // et_text, // content text et_cdata, // }; 我们以这样一个XML文件作为范例,以方便后面的解说: 小王 小张 一个很重要的概念是: 一棵XML树往往只有2个节点 1. XML节点,就是文件的第一行 2. XML根节点,只是的子节点.而文件的第一行,我们也把它看成一个节点. 这样理解的话,只要我们能解析一个节点,我们就可以解析整棵树. 状态分析法 所谓状态分析法,就是指一个解析函数,它可以根据不同的状态,运行不同的代码.对于解析xml文档,我设计了如下状态: enum xmlnode_state // 分析状态 { st_begin, // 开始 st_tagstart, // /*tag开始 - "<"后的第一个字符*/ st_tagend, // tag结束 - />,>,?> 前的第一个字符 st_attrnamestart, // 属性名开始 - tag后的第一个非空格字符 st_attrnameend, // 属性名结束 - =, ,前的第一个字符 st_attrvaluestart, // 属性值开始 - ',",后的第一个字符 st_attrvalueend, // 属性值结束 - ',",前的第一个字符 st_child, // 开始分析子节点 st_contentstart, // 内容开始 - >后的第一个字符 st_contentend, // 内容结束 - <前的第一个字符 st_commentstart, // 注释开始 前的第一个字符 st_endtagstart, // 结束TAG 开始 前的第一个字符 st_cdatastart, st_cdataend, st_end, // 分析结束 }; 假设pCur指向XML文档的输入流的当前位置, 现在来模拟一下解析过程: 在初始状态st_begin下,一直移动pCur,直到pCur[0] = '<',意味着节点开始了. 此时根据后面字符切换状态: 如果后面连续3个字符时 "!--" 那么说明这是一个注释节点,形如"",把状态切换为st_commentstart并继续运行相应代码; 如果后面一个字符是'?', 那么说明它是这种节点的开始 "" (图1中没有标明CDATA节点的情况,因为作图的时候没有考虑到CDATA节点);如果是其他字符,则说明开始读取节点名, " ..." 根据图1所示,其他的代码都类似:从输入流不断读出字符,根据当前状态,把读到的内容解析为XML文档中不同的项. (CSDN博客上传不了图片.. 上传到我的相册去了.) XML语法分析机(优化) (图1) 特别说明: 我把节点的内容理解为当前节点的一个子节点,比如 "这里是节点内容"这一段XML文本会被解析为一个父节点"company"和一个子节点"这里是节点内容". 这样做是有好处的,看这个例子"这里是节点内容另一段节点内容"如果只是把节点内容作为节点的一个属性,在碰到刚刚这种情况时就束手无策了. 关键代码分析 用C/C++的switch语句,很容易实现状态分析法:每一种状态对应一段case代码. BOOL XMLNode::LoadNode(const wchar_t* pszContent, const wchar_t* &pszEnd) { ResetNode(); const wchar_t* pCur = pszContent; const wchar_t* pBegin = NULL; const wchar_t* pEnd = NULL; xmlnode_state st = st_begin; wstr2wstr s2s; wchar_t chValueFlag; // ' 或者 " 应该成对出现 bool bStop = false; try { while(*pCur != 0 && !bStop) { switch(st) { case st_begin: { if(pCur[0] == L'<') { _trace("########################################", NULL); _trace("开始分析节点", pCur); // 判断节点类型 if(pCur[1] == L'?') { // (1) ""结尾的是CDATA部件 pCur += 8; m_type = et_cdata; st = st_cdatastart; } else { st = st_tagstart; m_type = et_normal; } } else { // 忽略所有'<'之前的字符 if(pCur[0] == L' ' || pCur[0] == L'/r' || pCur[0] == L'/n' || pCur[0] == L'/t') { } else { goto error; } } } break; case st_tagstart: { pBegin = pCur; pEnd = NULL; st = st_tagend; pCur--; } break; case st_tagend: { if(pCur[0] == L' ' || pCur[0] == L'>' || pCur[0] == L'/' && pCur[1] == L'>' && m_type == et_normal || pCur[0] == L'?' && pCur[1] == L'>' && m_type == et_xml ) { pEnd = pCur - 1; st = st_attrnamestart; pCur--; } else { // 非法tag名字符在此判断 if(pCur[0] == L'<' || pCur[0] == L'/') { _trace("tag名中出现了非法的字符", pCur); goto error; } } // 得到节点名称 if(pEnd != NULL) { if(getStr(pBegin, pEnd, m_strName)) { pBegin = NULL; pEnd = NULL; _trace("tag Name", m_strName.c_str()); } else { _trace("非法的tag", pBegin); pCur = pBegin; goto error; } } } break; case st_attrnamestart: { if(L' ' == pCur[0]) { // 跳过属性名前的空格 } else { pBegin = pCur; pEnd = NULL; st = st_attrnameend; pCur--; } } break; case st_attrnameend: { if(L'>' == pCur[0]) { st = st_contentstart; } else if(L'/' == pCur[0] && L'>' == pCur[1] && m_type == et_normal || L'?' == pCur[0] && L'>' == pCur[1] && m_type == et_xml) { st = st_end; pCur++; } else if(L'=' == pCur[0] || L' ' == pCur[0]) { st = st_attrvaluestart; pEnd = pCur - 1; } else { } if(pEnd) { s2s.first = L""; s2s.second = L""; if(getStr(pBegin, pEnd, s2s.first)) { _trace("属性名", s2s.first.c_str()); } else { _trace("非法的属性名", pCur); pCur = pBegin; goto error; } } } break; case st_attrvaluestart: { if(L'/'' == pCur[0] || L'/"' == pCur[0]) { pBegin = pCur + 1; pEnd = NULL; st = st_attrvalueend; chValueFlag = pCur[0]; // 记录'/"要成对出现 } else if(L' ' == pCur[0]) { // 属性名=后的空格过虑掉 } else { _trace("属性名后有非法的字符", pCur); goto error; } } break; case st_attrvalueend: { if((L'/'' == pCur[0] || L'/"' == pCur[0]) && pCur[0] == chValueFlag) { pEnd = pCur - 1; getStr(pBegin, pEnd, s2s.second); _trace("属性值", s2s.second.c_str()); m_AttrList.push_back(s2s); if( L' ' == pCur[1] || L'/' == pCur[1] && L'>' == pCur[2] && m_type == et_normal || L'?' == pCur[1] && L'>' == pCur[2] && m_type == et_xml || L'>' == pCur[1] ) { // 分析下一个属性 st = st_attrnamestart; } else { _trace("属性值/"//'之后发现非法字符", pCur); goto error; } } else { // 非法的属性值字符在此判断 // .. // .. } } break; case st_contentstart: { // 不过虑空格 pBegin = pCur; pEnd = NULL; st = st_contentend; pCur--; } break; case st_contentend: { if(L'<' == pCur[0]) { wstring strText; pEnd = pCur - 1; if(getStr(pBegin, pEnd, strText)) { // 普通文本也作为一个子节点 _trace("content", strText.c_str()); if(isValidText(strText.c_str())) { XMLNode *pNode = new XMLNode; pNode->m_type = et_text; pNode->m_strText = strText; linkChild(pNode); } else { _trace("无效内容文本", strText.c_str()); } } else { _trace("空内容", pBegin); } // 内容结束了,判断下一步操作 if(L'/' == pCur[1] && m_type == et_normal || L'?' ==pCur[1] && m_type == et_xml) { st = st_endtagstart; pCur++; } else { st = st_child; pCur--; // 子节点从"<"开始,所以回退1格 } pBegin = NULL; pEnd = NULL; } else { // 非法的内容字符在此判断 // .. // .. } } break; case st_cdatastart: { pBegin = pCur; pEnd = NULL; st = st_cdataend; pCur--; } break; case st_cdataend: { if(wcsncmp(pCur, L"]]>", 3) == 0) { pEnd = pCur - 1; getStr(pBegin, pEnd, m_strText); // CDATA文本也作为一个子节点 _trace("cdata content", m_strText.c_str()); // cdata结束了,判断下一步操作 pCur += 2; st = st_end; } else { // 非法的内容字符在此判断 // .. // .. } } break; case st_commentstart: { pBegin = pCur; st = st_commentend; pEnd = NULL; pCur--; } break; case st_commentend: { if(L'>' == pCur[0] && L'-' == *(pCur - 2) && L'-' == *(pCur - 1)) { pEnd = pCur - 3; getStr(pBegin, pEnd, m_strText); _trace("comment content", m_strText.c_str()); st = st_end; } else { // 非法的注释字符在此判断 // .. // .. } } break; case st_endtagstart: { pBegin = pCur; pEnd = NULL; st = st_endtagend; pCur--; } break; case st_endtagend: { if(L'>' == pCur[0]) { pEnd = pCur - 1; wstring strTag; getStr(pBegin, pEnd, strTag); _trace("endtagname", strTag.c_str()); if(strTag == m_strName) { st = st_end; } else { pCur = pBegin; goto error; } } else { // } } break; case st_child: { // 递归分析子节点 _trace("开始分析子节点", pCur); XMLNode *pNode = new XMLNode; if(pNode->LoadNode(pCur, pCur)) { linkChild(pNode); pCur--; _trace("继续分析下一段内容(多一个字符)", pCur); st = st_contentstart; } else { delete pNode; goto childerror; } } break; case st_end: { bStop = true; pCur--; } break; default: { } break; } pCur++; } } catch (...) { _trace("捕捉到异常", NULL); goto error; } pszEnd = pCur; return st == st_end || st == st_begin; error: _trace("发生错误, 原始内容", pszContent); _trace("错误位置", pCur); childerror: pszEnd = pCur; return FALSE; } 使用范例 1. 在内存中构建XML文档,并保存. XMLDocument xml; XMLHANDLE hXml = xml.CreateXml("1.0", "gb2312"); hXml = xml.NewNode(NULL, "root"); // 创建根节点 xml.SetAttrValue(hXml, "type", "company"); //添加一个根节点属性 xml.SetAttrValueInt(hXml, "value", 1); XMLHANDLE hContent = xml.NewNode(hXml, NULL, et_text); // 为根节点创建一个子节点(内容) xml.SetText(hContent, "这是内容"); hXml = xml.NewNode(hXml, "software"); // 创建子节点 hXml = xml.NewNode(hXml, "软件部门"); XMLHANDLE hChild = xml.NewNode(hXml, "person"); xml.SetAttrValueInt(hChild, "id", 100020001); hChild = xml.NewNode(hXml, "person"); xml.SetAttrValueInt(hChild, "id", 100020002); xml.SaveXml("C://my.xml"); // 保存文档 运行结果: 这是内容 <软件部门> 说明: XMLHANDLE 是我定义的一个用来标识XML节点的"句柄",任何时候,只要传入句柄,接口就可以操作这个节点. 2. 读取XML文档. XMLDocument xml1; if(xml1.LoadXml(strFilePath)) { XMLHANDLE hRoot = xml1.GetRootNode(); //获取根节点 string strTypeName = xml1.GetAttrValue(hRoot, "type"); // 获取根节点的属性 string strContent = xml1.GetContent(xml1.FirstChild(hRoot)); // 获取根节点的第一段内容 XMLHANDLE hSoftware = xml1.GetChildByName(hRoot, "software"); // 通过节点名定位子节点 XMLHANDLE hSoftware2 = xml1.GetChildByName(hSoftware, "软件部门"); XMLHANDLE hPerson = xml1.GetChildByAttr(hSoftware2, "person", "id", "100020002"); //通过属性值定位子节点 string strId = xml1.GetAttrValue(hPerson, "id"); } 说明:搜索定位接口 GetChildByName() 和 GetChildByAttr() 只搜索指定节点的一级子节点,并不搜索全文.如果要实现全文检索,也不难,用一个栈/队列就可以实现深度优先/广度优先搜索,这是树算法的基本功了. 后记 对于本文介绍的状态分析法 ,画出那张图是关键. 只要能把流程和各个分支想清楚,写代非常容易按部就班就可以写出来.由此也可以看出,用状态分析法来解析XML文档只是这个方法的一次应用.如果你能定义其他的规则,并画出流程,完全可以用这个方法来解析.

相关内容: 最新内容:
什么是SQL注入攻击,有什么危害[2015-01-26]
中小企业安全调查 SQL注入仍是主要威胁[2015-01-26]
黑客对ASP.Net平台网站发起SQL注入攻击[2015-01-26]
人人网分站SQL注入+反射型xss[2015-01-26]
人人网分站SQL注入+反射型xss[2015-01-26]
记一次成功的SQL注入入侵检测附带SQL性能优化[2015-01-26]