首页 > 编程语言 >C++使用ranges库解析INI文件

C++使用ranges库解析INI文件

时间:2023-06-20 09:45:08浏览次数:39  
标签:std auto section lines C++ ranges INI entry line

C++使用ranges库解析INI文件

引言

C++20引入了<ranges>头文件,C++23对其进行了完善,本文将使用该头文件提供的adaptor编写一个简单的ini解析器。

ini文件格式介绍


一般的ini文件由section和entry部分,比如

[section]
key=value ;This is entry.

section和entry均独占一行,其中section部分是由一对方括号构成,而entry由key和value俩个部分构成,使用等号隔开,分号后面的部分会被视为注释。


存储结构

一个比较容易想到的做法是使用map<string, entry>来代表section,使用map<string, string>来代表entry。

但是我们可能并不需要有序结构,所以我们可以使用unordered_map来代替map。

同时,大量的string可能会要效率上的损失,我们可以存储整个文件,然后使用string_view来代替string。

std::string context;
std::unordered_map<std::string_view, std::unordered_map<std::string_view, std::string_view>> result;

解析过程

我们可以将ini的解析过程分为如下几步:

  • 读取文本
  • 按行分割
  • 去除该行的注释部分和无效的空白
  • 去除空行
  • 将section和entry进行分组,每组一个section和若干个entry
  • 将entry拆分为键值对
  • 存储分组后的结果

1. 读取本文

std::string ReadFromFile(const char* filename)
{
    std::ifstream ifs { filename, std::ios::binary };
    return std::string(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
}

context = ReadFromFile(filename);

这种方式比较简洁但是效率不是很理想。

2. 按行分割

lines = context | std::views::split('\n');

split分割完之后会生成若干个std::subrange<std::string::iterator, std::string::iterator, std::ranges::subrange::sized>,前俩个模板参数代表了迭代器的类型,最后一个参数表示这个subrange是知道size的。

3. 去除该行的注释部分和无效的空白

inline constexpr auto TrimBlank = [](std::string_view line) {
    auto first = std::ranges::find_if_not(line, ::isspace);
    auto last = std::ranges::find_if_not(line 
        | std::views::drop(std::ranges::distance(line.begin(), first)) 
        | std::views::reverse, ::isspace).base();
    return std::string_view(first, last);
};

inline constexpr auto GetSectionOrEntryContext = [](auto line) {

    // for empty line or line just with comments, return "".

    auto str = std::string_view(line);

    auto end_of_line = std::ranges::find(str, ';');  // find comment

    auto new_line = std::string_view(std::ranges::begin(str), end_of_line);

    auto line_after_trim = TrimBlank(new_line);

    auto result = std::string_view(line_after_trim.begin(), line_after_trim.end());

    return result;
};

lines = lines | std::views::transform(GetSectionOrEntryContext);

我们对于每一行,首先可以找到注释符号,然后将注释部分去除,再将剩下的部分trim就可以得到有效的内容。

4. 去除空行

lines = lines | std::views::filter(std::ranges::size);

filter会将满足条件的元素留下,空行的size为0,不会被留下。

5. 将section和entry进行分组,每组一个section和若干个entry

由于我们移除了所有的空行,所以现在每一行只可能是section和entry中的一个。我们可以列举所有情况:

  • 同一个section下的两个相邻的entry(entry + entry)
key1=value1
key2=value2
  • 同一个section下的section和entry(section + entry)
[section1]
key1=value1
  • 上一个section的最后一个entry和下一个section(entry + section)
key1=value1
[section1]
  • 一个空的section紧跟下一个section(section + section)
[section1]
[section2]

在以上的四种情况中,只有前面两种应该属于同一个section。我们可以使用adjacent_find来辅助我们分组,而chunk_by内部正好采用的就是adjacent_find。

inline constexpr auto IsSection = [](auto str) {
    return str.front() == '[';
};

inline constexpr auto IsEntry = [](auto str) {
    return !IsSection(str);
};

inline constexpr auto ChunkSectionAndEntries = [](auto l, auto r) {
    return (IsSection(l) && IsEntry(r)) || (IsEntry(l) && IsEntry(r));
};

lines = lines | std::views::chunk_by(ChunkSectionAndEntries); 

6. 将entry拆分为键值对

inline constexpr auto SplitEntryToKeyValue = [](auto line) {

    assert(line.front() != '='); // only allow empty value

    auto kv = line | std::views::split('=');

    auto iter = kv.begin();

    auto key = std::string_view(*iter);

    std::ranges::advance(iter, 1, kv.end());

    return std::make_pair(key, iter == kv.end() ? "" : std::string_view(*iter));

};

我们按照'='进行分割,然后将key和value以pair的形式返回。在这里我们可以进行一些错误检查,比如我们允许value为空,但是不允许key为空的情况:

[section]
key1=   ; ok
=value1 ; error

当然我们还可以在这个函数里面增加一些其他的检查以防止不合法的输入。

7. 存储分组后的结果

到此为止,我们已经将所有的行进行了分组,对于每一组,有若干行,其中第一行为section,剩下的所有行均为entry。我们将他们转换为一个map存储起来。

inline constexpr auto MapEntriesToDict = [](auto lines) {
    std::unordered_map<std::string_view, std::string_view> dict;
    std::ranges::for_each(lines, [&](auto line) {
        auto [k, v] = SplitEntryToKeyValue(line);
        dict[k] = v; // 当一个section下有多个相同的entry时,后面的会覆盖前面的,当然这里也可以利用异常或者断言来组织这样的事情发生。
    });
    return dict;
};

inline constexpr auto ParseOneSectionAndEntriesChunkToResultValueType = [](auto lines) {
    // chuck_by产生若干个chunk(lines),每个chunk第一个元素是section,剩下的部分是entries。
    auto section = *lines.begin(); 
    auto entries = MapEntriesToDict(lines | std::views::drop(1));
    return std::make_pair(
        section.substr(1, section.size() - 2), // 去除section的括号部分
        std::move(entries));
};

lines = lines | std::views::transform(ParseOneSectionAndEntriesChunkToResultValueType);

我们可以在ParseOneSectionAndEntriesChunkToResultValueType中添加相应的检查机制,比如检查一下section是否以'['开头,和以']'结尾。

我们的ini可以出现一些极端的情况:

  • 只包含注释和空行
; Hi, guys!
; Whose life will be better if you learn more C++?
; End of file
  • 只包含section
[section1]
[section2]
[section3]
[section4]
; End of file
  • 只包含entry或者不是以section开头
key1=value1
key2=value2
key3=value3
; End of file

对于第一种情况我们的filter会将所有行都去掉最后生成一个空的map。

对于第二种情况chunk_by生成若干个只包含section不包含entry的chunk。

对于第三种情况chunk_by会将这些entries分到第一个chunk中,这会导致第一个chunk不包含section,不过我们其实并不需要做额外的处理,依旧只需要检查section的格式即可。

auto section = *lines.begin(); 
assert(section.front() == '[' && ...);
// ...

由于entry和section在格式上差距很大,所以检查section格式也可以防止此种情形出现。

到此位置解析和转化已经全部完成,我们最后再把结果放到map中即可,完整的代码点击这里

标签:std,auto,section,lines,C++,ranges,INI,entry,line
From: https://www.cnblogs.com/MasterYan576356467/p/17492801.html

相关文章

  • 京东微前端应用MicroApp,主应用vite-vue3,子应用vite-vue3+pinia
    micro-app官方地址micro-app官方demo地址这篇文章主要是为了记录,本人在使用中遇到的一些问题,供参考资源找不到->本地使用代理,显示nginx转发子应用使用组件插槽或者pinia,路由懒加载报错问题->小项目几个路由不要懒加载,大项目中懒加载的时候不要使用pinia或者组件中不适用......
  • C++四种类型转换
    篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++四种类型转换相关的知识,希望对你有一定的参考价值。const_cast主要用于删除变量的const属性,便于赋值constinta=2;int*p=const_cast<int*>(&a);*p=3;reinterpret_cast仅仅是重新解释类型,没有二进制的......
  • C++ 关键字四种cast类型转换
    1.23四种cast类型转换作用:克服c中强制类型转化带来的风险,C++引入四种更加安全的强制类型转换运算符(明确转换的目的,偏于程序的维护和分析)const_cast://1.去除const属性,将只读变为只读写//2.针对常量指针、常量引用和常量对象constchar*p;char*p1=const_cast<char*>(p......
  • C++ 数据类型转换详解之终极无惑
    程序开发环境:VS2017+Win32+Debug文章目录1.隐式数据类型转换2.显示数据类型转换3.C++新式类型转换3.1const_cast3.2static_cast3.3dynamic_cast3.3.1向下转换3.3.2交叉转换3.4reinterpret_cast4.重载相关类型转换操作符4.1不同类对象的相互转换4.2基本数据类型与类对象......
  • C++面试八股文:什么是智能指针?
    某日二师兄参加XXX科技公司的C++工程师开发岗位第19面:面试官:什么是智能指针?二师兄:智能指针是C++11引入的类模板,用于管理资源,行为类似于指针,但不需要手动申请、释放资源,所以称为智能指针。面试官:C++11引入了哪些智能指针?二师兄:三种,分别是shared_ptr、unique_ptr、和weak_ptr。......
  • C++面试八股文:什么是智能指针?
    某日二师兄参加XXX科技公司的C++工程师开发岗位第19面:面试官:什么是智能指针?二师兄:智能指针是C++11引入的类模板,用于管理资源,行为类似于指针,但不需要手动申请、释放资源,所以称为智能指针。面试官:C++11引入了哪些智能指针?二师兄:三种,分别是shared_ptr、unique_ptr、和weak_ptr。......
  • C++继承和派生
    #继承和派生在C++中,继承和派生是面向对象编程的两个重要概念,用于实现类与类之间的关系。继承是指一个类可以从另一个类中继承属性和方法,并且可以在此基础上扩展出自己的属性和方法。被继承的类称为基类(父类),继承的类称为派生类(子类)。在C++中,可以通过以下方式定义一个派生类:```c++cl......
  • BMZCTF:网鼎杯 2018 minified
    http://bmzclub.cn/challenges#%E7%BD%91%E9%BC%8E%E6%9D%AF%202018%20minifiedflag_enc.png使用stegsolve打开,将每个色道的零通道取出来Alpha0Red0Green0Blue0将Alpha0和Green0进行ImageCombiner,当XOR时出现flagflag{7bb6db9f-d2eb-4e69-8dee-0002ce1e07f9}......
  • pinia-状态管理的修改
    一、直接修改import{storeA}from'../../piniaStore/storeA';letpiniaStore_storeA=storeA();//将piniaAge状态修改为18piniaStore_storeA.piniaAge=18 二、$patch修改piniaStore_storeA.$patch({piniaAge:15,piniaName:'danguner'})//普通修改或piniaS......
  • c++ 2.0 总结
    class内存分配与释放#include<iostream>#include<memory>usingnamespacestd;classPerson{public:Person(){cout<<"personconstructor"<<endl;}~Person(){cout<<"person......