首页 > 代码库 > 解析器(一):分隔符指导
解析器(一):分隔符指导
又是一个新的系列,那个什么是“解析器”?在我的认知里,大概代表了如下的东西:
1、格式解析,将固定格式的字符串内容,翻译成我们能够简单获取和处理的结构;如配置文件(.ini)、XML。
2、编译器+虚拟机,其实就是某一门语言的前端+后端+运行时,当然也可以解释执行;如本人的Xmas,轻量的有正则表达式(Regex)。
所以,本系列是打算讲述:配置文件(或许有Xml乱入)---正则表达式---Xmas(目前个人实现的最复杂的部分)。所以,接下来,回到本文的主题:
分隔符指导解析:使用固定字符进行语法分割的简单的一种解析方式。
比如配置文件:
[GC.Mark] MajorAge = 5 ElevateRatio = 5 MinorBytes = 2097152 MajorBytes = 104857600 [GC.Generation] MajorAge = 5 SurvivorRatio = 5 MinorBytes = 2097152 MajorBytes = 10485760 FixedBytes = 1048576
上面就是最典型的配置文件的一部分。其中只有4个我们需要注意的【分隔字符】:
1、【[】,代表了一个条目的名字的开始
2、【]】,一个条目名字的结束,其后所有的内容都是该条目的键值对,直到遇到一个新的条目
3、【=】,键值对的分隔符,其前面的是键(key),后面的则是值(value)
4、【;】,注释
当然还有一个隐式的分隔符:【\n】换行符,所有的内容都以它作为结束;这一点至关重要:解析工作,可以以行为单位。
std::string context; Files.read("config.ini", context); std::vector<std::string> lines; Strings.split(context.c_str(), ‘\n‘, lines); for(auto& line, lines){ parseLine(line); }
这样,我们的工作对象,就将整个字符串减少到一行。
在这里,我们可以预见:解析配置文件,是简单而枯燥无味的;是的,的确如此:
void parseLine(const std::string& line) { if(line == ""){ return; } if(line[0] == ‘[‘){ std::string item(&line[1], &line[line.length() - 1]); setCurrentItem(item);//设置当前所处的条目环境 return; } if(line[0] == ‘;‘){ return; } int offset = line.find(‘=‘); if(offset < 0){ return; } std::string key(&line[0], &line[offset]); std::string value(&line[offset + 1]; addValue(key, value); }
如此简单地我们就完成了解析任务的最核心的部分;可惜的是,丝毫没有任何难度和挑战。当然,我们可以做一些优化工作:
1、考虑【]】的匹配,前面我们并没有考虑它;只是默认,条目所在行的最后字符一定就是【]】;当然,这样也才是正确的配置格式;但是,我们可以容错,至于需要容许怎样的错误,由自己喜好。
2、去空隔,特别是键值对,其中【=】的前后可能会有空格;我们有必要去掉它。
3、更多的注释支持,如:不止使用【;】、其位置可以不是每行的开始。
下一个问题,就是:这些解析出的信息,放在哪里?
作为一个没有难度和特色的ini解析器,我们可以这样:
void setCurrentItem(const std::string& item) { mItem = item; } void addValue(const std::string& key, const std::string& value) { mValues[mItem + "." + key] = value;//注意这个“.”的作用 }
我们使用一个Map来存放键值对,而配置文件只有键值对。到此,我们基本上完成了所有的工作;只剩对外的查询接口了。
我们支持怎样的查询?
来点有意思的如何:
String value(const String& name)const; ItemList items(const String& name)const; ValueList values(const String& name)const;
我个人的解析器,支持层级查询:类似于文件目录结构,一个层级内部有多个下一级层级和多个值(对应一个文件夹内部有多个文件夹和多个文件)。要仔细分析这句话:
1、有目录结构,即:一个条目长这样“GC.Mark”,意味着其有两级对应的层次“GC”和它的下一级“Mark”(使用“.”作为层分隔)
2、有多个层级,即:我们的配置文件,其实是一棵树(在当前定义下)
3、当前层级有多个值,而非键值对。
这样,我们将支持这样的格式:
[Xmas.Source] H:\Xmas\Xmas.xmas H:\Xmas\Tools.xmas
一个列表。当我使用如下代码时:
sourceList = config.values("Xmas.Source");
它将返回一个字符串列表 ["H:\Xmas\Xmas.xmas", "H:\Xmas\Tools.xmas"]。
当我使用如下代码时:
itemList = config.items("GC");
它将返回两个条目:“Mark”、“Generation”。然后,沿着条目,我可以继续向下遍历:
class Item{ public: String name()const; String value()const; String value(const String& name)const; public: ItemList items()const; ValueList values()const; };
通过该条目,我便可以像访问一颗树一样。当然,这时该解析器内部正是构建了一个真正的DOM树。
聪明的读者应该发现我早已偏离了本文的主题:分隔符指导解析。是的,使用分隔符解析,是一种简单且连入门都算不上的解析方式;但其作为一个指引,指向更完善的语法指导,还是有价值的。
在语法指导解析中,充满了各种树形结构;所以,我们必须了解、知道、精通,一棵树的创建和访问。而前面带有层级的配置解析器,是一个不错的开始。
解析器(一):分隔符指导