本文主要介绍用C语言实现xml解析器的详细说明。有需要的朋友可以借鉴一下,希望能有所帮助。祝大家进步很大,早日升职加薪。
目录
xml格式简介xml格式解析过程简单分析代码实现类——元素关键代码13354实现整体解析关键代码23354解析所有元素开发技巧C优化补充说明
xml格式简单介绍
?xml版本='1.0 '?
!-这是评论-
工作流程
工作名称='1 '开关='开'
plugin name=' echo plugin . so ' switch=' on '/
/工作
/工作流程
让我们简单看一下上面的xml文件。xml格式非常类似于html格式,它通常用于存储需要属性或多个嵌套关系的配置。
通常,xml用于项目的配置文件。与其他ini格式或yaml格式相比,它的优点是一个标签可以有多个属性。比如上面提到的xml文件格式用来配置工作流,包括name属性和switch属性,plugin标签嵌套在work标签中,比其他配置文件格式灵活很多。
有很多具体的应用场景。比如在Java中使用过Mybatis的同学应该知道,Mybatis的配置文件是xml格式的,sql语句也可以用xml格式编写。同样,Java中maven项目的配置文件也是在xml文件中配置的。
我为什么要编写xml解析器呢?显然我以后写的C工程需要用到。
xml格式解析过程浅析
回到前面的代码,我们实际上已经列出了不同的xml文件格式。
总的来说,所有的xml标签都分为:
Xml声明(包括版本、编码等信息)标注xml元素:1。单标签元素。2.成对的标签元素。
其中不需要xml声明和注释。xml元素至少需要一个成对的tag元素,最外层只有一个,就是根元素。
从xml元素的角度来看,可以分为:
名称内容子节点
根据前面的例子,很明显,名称是必要的和唯一的,而其他内容是可选的。根据元素的结束形式,我们将元素分为单标签元素和双标签元素。
代码实现
完整的代码库:xml解析器
实现存储解析数据的类——Element
代码如下:
命名空间xml
{
使用STD:vector;
使用STD:map;
使用STD:string _ view;
使用STD:string;
类元素
{
公共:
使用children _ t=vectorElement
使用attrs_t=mapstring,string
使用iterator=vector element:iterator;
使用const _ iterator=vector element:const _ iterator;
字符串名称()
{
返回m _ name
}
字符串文本()
{
返回m _ text
}
//迭代器方便遍历子节点
迭代器begin()
{
返回m _ children . begin();
}
[[nodiscard]]常量_迭代器begin()常量
{
返回m _ children . begin();
}
迭代器结束()
{
返回m _ children . end();
}
[[nodiscard]]常量_迭代器end()常量
{
返回m _ children . end();
}
void push _ back(element const element)//方便子节点的存储
{
m _ children . push _ back(element);
}
string operator[](string const key)//方便访问键值
{
return m _ attrs[key];
}
字符串到字符串()
{
return _ to _ string();
}
私人:
string _to_string()。
私人:
字符串m _ name
字符串m _ text
儿童_ t m _儿童;
attrs _ t m _ attrs
};
}
对于上面的代码,我们主要看成员变量。
我们使用字符串类型表示元素的名称和文本,向量嵌套表示子节点,map表示键-值对的属性。
其余的方法要么是Getter/Setter,要么便于操作子节点和属性。当然还有一个to_string()方法,后面会讲到。
关键代码1——实现整体的解析
关于整体结构我们分解为下面的情形:
代码如下:
元素xml:Parser:Parse()
{
而(真)
{
char t=_ get _ next _ token();
如果(t!='')
{
THROW_ERROR('无效格式,m_str.substr(m_idx,detail _ len));
}
//解析版本号
if(m _ idx 4m _ str。size()m _ str。compare(m _ idx,5,'?xml')==0)
{
如果(!_parse_version())
{
THROW_ERROR('版本解析错误,m_str.substr(m_idx,detail _ len));
}
继续;
}
//解析注释
if(m _ idx 3m _ str。size()m _ str。比较(m _ idx,4,'!- ')==0)
{
如果(!_parse_comment())
{
THROW_ERROR('注释解析错误,m_str.substr(m_idx,detail _ len));
}
继续;
}
//解析元素
if(m _ idx 1m _ str。size()(is alpha(m _ str[m _ idx 1])| | m _ str[m _ idx 1]==' _ '))
{
return _parse_element().
}
//出现未定义情况直接抛出异常
THROW_ERROR('错误格式,m_str.substr(m_idx,detail _ len));
}
}
上述代码我们用在…期间循环进行嵌套的原因在于注释可能有多个。
关键代码2——解析所有元素
对应代码:
元素xml:解析器:_parse_element()
{
元素元素;
auto pre _ pos=m _ idx//过掉
//判断名字首字符合法性
如果(!(m _ idx m _ str。size()(STD:is alpha(m _ str[m _ idx])| | m _ str[m _ idx]==' _ '))
{
THROW_ERROR('解析名中出现错误,m_str.substr(m_idx,detail _ len));
}
//解析名字
while(m _ idx m _ str。size()(is alpha(m _ str[m _ idx])| | m _ str[m _ idx]==':' | |
m _ str[m _ idx]=='-' | | m _ str[m _ idx]==' _ ' | | m _ str[m _ idx]==','))
{
m _ idx
}
if (m_idx=m_str.size())
THROW_ERROR('解析名中出现错误,m_str.substr(m_idx,detail _ len));
元素. Name()=m_str.substr(pre_pos,m _ idx-pre _ pos);
//正式解析内部
while (m_idx m_str.size())
{
char token=_ get _ next _ token();
if (token=='/') //1 .单元素,直接解析后结束
{
if (m_str[m_idx 1]==' ')
{
m _ idx=2;
返回元素;
}否则
{
THROW_ERROR('解析单一元素失败,m_str.substr(m_idx,detail _ len));
}
}
if (token=='')//2 .对应三种情况:结束符、注释、下个子节点
{
//结束符
if (m_str[m_idx 1]=='/')
{
if (m_str.compare(m_idx 2,element .名称()。大小(),元素. Name())!=0)
{
THROW_ERROR('解析结束标记错误,m_str.substr(m_idx,detail _ len));
}
m_idx=2个元素。名称()。size();
char x=_ get _ next _ token();
如果(x!='')
{
THROW_ERROR('解析结束标记错误,m_str.substr(m_idx,detail _ len));
}
m _ idx//千万注意把'' 过掉,防止下次解析被识别为初始的标签结束,实际上这个元素已经解析完成
返回元素;
}
//是注释的情况
if(m _ idx 3m _ str。size()m _ str。比较(m _ idx,4,'!- ')==0)
{
如果(!_parse_comment())
{
THROW_ERROR('解析注释错误,m_str.substr(m_idx,detail _ len));
}
继续;
}
//其余情况可能是注释或子元素,直接调用从语法上分析进行解析得到即可
元素。push _ back(Parse());
继续;
}
if (token=='') //3 .对应两种情况:该标签的文本内容,下个标签的开始或者注释(直接继续跳到到下次循环即可
{
m _ idx
//判断下一个令牌是否是文本,如果不是,继续
char x=_ get _ next _ token();
If (x=='')//不能是终止符,因为xml元素不能是空体。如果直接出现这种情况,中间可能会有评论。
{
继续;
}
//分析文本,然后分析子级
auto pos=m_str.find(',m _ idx);
if (pos==string:npos)
THROW_ERROR('解析文本错误',m_str.substr(m_idx,detail _ len));
元素。Text()=m_str.substr(m_idx,pos-m _ idx);
m _ idx=pos
//注意:有可能直接打到终止符,需要继续,让元素里的逻辑判断。
继续;
}
//4.剩下的情况都是属性解析。
auto key=_ parse _ attr _ key();
auto x=_ get _ next _ token();
如果(x!='=')
{
THROW_ERROR('parse attrs error ',m_str.substr(m_idx,detail _ len));
}
m _ idx
auto value=_ parse _ attr _ value();
element[key]=值;
}
THROW_ERROR('解析元素错误',m_str.substr(m_idx,detail _ len));
}
开发技巧
无论是C开发还是其他各种语言的造轮,在这个造轮的过程中,都不可能一帆风顺。它需要不断的调试,然后测试,然后调试。其实解析这种格式,简单调试程序的效率是很低的!
尤其是你用的语言是C的话,如果出现意外停机,debug几乎不可能简单的找出原因。所以为了方便调试或者意外停机,我们最好多做一些错误和异常处理。
例如,在上面的代码中,我们大量使用了宏THROW_ERROR。事实上,这个宏的输出很容易调试和快速定位。具体代码如下:
//用于返回更详细的错误信息,便于错误跟踪。
#define THROW_ERROR(错误信息,错误详细信息)\
做{ \
字符串信息=“中的分析错误”;\
string file _ pos=_ _ FILE _ _\
file _ pos . append(':');\
file _ pos . append(STD:to _ string(_ _ LINE _ _);\
info=file _ pos\
info=',';\
info=(error _ info);\
信息=' \详细信息:';\
info=(error _ detail);\
抛出STD:logic _ error(info);\
}while(false)
如果发生错误,此异常将包含以下信息:
印刷了两条关键信息:
的内部C代码解析所引发的异常的位置
解析有错误的字符串。
理论上这些信息应该是用日志记录的,但是因为这个项目比较小,直接用日常信息做日志也不错,方便调试。
有关C++的优化
众所周知,在C中,一个类有八个默认函数:
默认构造函数默认复制构造函数默认析构函数默认重载赋值运算符函数默认重载地址运算符函数默认重载地址运算符常量函数默认移动构造函数(C 11)默认重载移动赋值运算符函数(C 11)
一般来说,我们需要注意以下三种类型的构造函数和赋值函数:
复制构造。移动结构。析构函数。
以下面的代码为例来说明默认行为:
类别数据{
.
}
类别测试{
pvivate:
数据m _ data
}
额外注意
违约情况模拟
类别数据{
.
}
类别测试{
公共:
//复制构造
test(test consts RC)=default;//相当于下面的代码
//test(test const src):m _ data(src . m _ data){ }
//移动构造
test(test src)=默认;//相当于下面的代码
//tset(test src):m _ data(STD:move(src . m _ data)){ }
pvivate:
数据m _ data
}
从上面的情况可以看出,如果一个类的数据成员包含原始指针数据,那么复制构造和移动构造都需要定制。如果所有成员都使用标准库中的东西,那么我们就使用默认的,因为标准库的所有成员都自己实现了复制移动构造!例如,我们当前的所有元素都应该使用默认元素。
需要特别注意的几点
显式定义一个构造函数或赋值函数,那么相应的其他构造函数或赋值函数将被删除。默认情况下,需要再次显式定义它。举个例子:比如我显式定义了移动构造(关于显式定义,手动创建是显式的,手动写default也是显示),那么所有的默认副本(副本构造和副本赋值)都会被删除。反之,移动赋值的显式定义类似,删除默认的复制行为。复制对于显式默认行为也是一样的。如果要使用默认的构造函数/赋值函数,相应的成员也必须支持它。例如,下面的代码:
类别数据{
.
}
类别测试{
pvivate:
数据m _ data
}
因为测试类不写任何构造函数,所以这八个默认构造函数理论上都是可用的。但是如果由于一些显式定义删除了数据类中的复制构造,测试类将不再支持复制构造(对我们的影响是不能直接用等号初始化)。
最后,通过上面的规则,我们发现,如果要通过默认构造函数偷懒,那么首先你的成员要支持对应的构造函数,不要画蛇添足:比如我本来什么都不用写,它自动生成了八个默认函数,然后你手写了一个复制构造=default。嗯,你的默认功能从那以后就没了,要手动补!
所以,如果成员变量同时支持移动和复制行为,那你就千万不要再画蛇添足了,除非你需要自定义一些特殊的行为(比如日志什么的)。如果成员变量包含原始指针,则必须手动编写移动和复制行为。如果成员变量部分支持复制和移动行为,就需要根据你的使用情况手动补充这些行为!
以上是C实现xml解析器例子的详细内容。关于C xml解析器的更多信息,请关注我们的其他相关文章!
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。