首页 > 代码库 > 解析器(一):分隔符指导

解析器(一):分隔符指导

 

  又是一个新的系列,那个什么是“解析器”?在我的认知里,大概代表了如下的东西:

  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树。

 

  聪明的读者应该发现我早已偏离了本文的主题:分隔符指导解析。是的,使用分隔符解析,是一种简单且连入门都算不上的解析方式;但其作为一个指引,指向更完善的语法指导,还是有价值的。

  在语法指导解析中,充满了各种树形结构;所以,我们必须了解、知道、精通,一棵树的创建和访问。而前面带有层级的配置解析器,是一个不错的开始。

解析器(一):分隔符指导