64、实战:配置解析器(INI/XML格式+正则表达式)

配置解析器,说白了就是让程序能读懂配置文件。你想想看,一个正经的软件总不能把IP地址、端口号这些东西硬编码在代码里吧?那改个配置还得重新编译,太不优雅了。

我个人习惯把配置解析器分成两类:INI格式XML格式。前者轻量、直观,适合小项目;后者结构化强,适合复杂场景。今天我们就用C++和正则表达式,手撸一个能同时解析这两种格式的小工具。

核心知识点:正则表达式分组匹配、INI节区解析、XML嵌套标签解析、容错处理

为什么不用现成库?

我知道你肯定想问:Boost.PropertyTree、libxml2这些不香吗?嗯,确实香。但我在项目中遇到过几次特殊情况——嵌入式环境没有这些库,或者客户要求零依赖。这时候,自己手写一个轻量解析器就很有必要了。

再说了,通过这个实战,你能把正则表达式的分组匹配、回溯引用这些技巧练得滚瓜烂熟。以后遇到日志解析、协议解析,都能举一反三。

INI格式解析

INI文件长这样:

; 这是注释
[server]
host = 192.168.1.1
port = 8080
timeout = 30

[database]
host = localhost
port = 3306
name = mydb

解析思路其实很简单:

  • 用正则匹配 [section] 来识别节区
  • 用正则匹配 key = value 来识别键值对
  • 跳过空行和注释行

我建议用两个正则表达式分开处理,别想着一个正则搞定所有。为什么?因为可读性太差了,三个月后你自己都看不懂。

#include <regex>
#include <string>
#include <map>
#include <fstream>
#include <sstream>

class IniParser {
public:
    bool load(const std::string& filename) {
        std::ifstream file(filename);
        if (!file.is_open()) return false;

        std::string line;
        std::string currentSection;
        std::regex sectionRegex(R"(\[([^\]]+)\])");
        std::regex kvRegex(R"(\s*([^=]+)\s*=\s*(.*)\s*)");
        std::regex commentRegex(R"(^\s*[;#])");

        while (std::getline(file, line)) {
            // 跳过空行和注释
            if (line.empty() || std::regex_match(line, commentRegex))
                continue;

            std::smatch match;
            if (std::regex_match(line, match, sectionRegex)) {
                currentSection = match[1].str();
            } else if (std::regex_match(line, match, kvRegex)) {
                std::string key = match[1].str();
                std::string value = match[2].str();
                data_[currentSection][key] = value;
            }
        }
        return true;
    }

    std::string get(const std::string& section,
                    const std::string& key,
                    const std::string& defaultVal = "") {
        auto it = data_.find(section);
        if (it != data_.end()) {
            auto kv = it->second.find(key);
            if (kv != it->second.end())
                return kv->second;
        }
        return defaultVal;
    }

private:
    std::map<std::string, std::map<std::string, std::string>> data_;
};

小技巧:正则表达式中的 ([^\]]+) 表示匹配除了右中括号以外的任意字符。这样即使节区名里包含空格也能正确处理。

XML格式解析

XML比INI复杂一些,因为有嵌套结构。不过我们只做轻量解析,不搞完整的DOM解析器。目标是把下面这种配置解析成键值对:

<config>
    <server>
        <host>192.168.1.1</host>
        <port>8080</port>
    </server>
    <database>
        <host>localhost</host>
        <port>3306</port>
    </database>
</config>

解析思路:

  • 用正则匹配 <tag>value</tag> 这种模式
  • 用正则匹配 <tag>...</tag> 来识别嵌套结构
  • 用栈来维护当前路径

我曾经在解析一个几百KB的XML配置文件时,发现正则回溯导致性能极差。后来改用逐字符扫描+状态机的方式,速度提升了10倍。所以这里给你两个方案:

方案一:正则表达式法(适合小文件)

class XmlParser {
public:
    bool load(const std::string& filename) {
        std::ifstream file(filename);
        if (!file.is_open()) return false;

        std::string content((std::istreambuf_iterator<char>(file)),
                             std::istreambuf_iterator<char>());

        // 匹配 <tag>value</tag>
        std::regex leafRegex(R"(<([^>/]+)>([^<]*)</\1>)");
        std::smatch match;
        std::string::const_iterator start = content.cbegin();
        while (std::regex_search(start, content.cend(), match, leafRegex)) {
            std::string tag = match[1].str();
            std::string value = match[2].str();
            data_[tag] = value;
            start = match.suffix().first;
        }
        return true;
    }

private:
    std::map<std::string, std::string> data_;
};

方案二:状态机法(适合大文件)

class XmlStateMachineParser {
public:
    bool load(const std::string& filename) {
        std::ifstream file(filename);
        if (!file.is_open()) return false;

        enum State { TEXT, TAG_OPEN, TAG_CLOSE, TAG_NAME };
        State state = TEXT;
        std::string currentTag;
        std::string currentValue;
        std::stack<std::string> tagStack;
        char ch;

        while (file.get(ch)) {
            switch (state) {
                case TEXT:
                    if (ch == '<') state = TAG_OPEN;
                    else if (!std::isspace(ch)) currentValue += ch;
                    break;
                case TAG_OPEN:
                    if (ch == '/') state = TAG_CLOSE;
                    else { currentTag = ch; state = TAG_NAME; }
                    break;
                case TAG_NAME:
                    if (ch == '>') {
                        tagStack.push(currentTag);
                        currentTag.clear();
                        currentValue.clear();
                        state = TEXT;
                    } else {
                        currentTag += ch;
                    }
                    break;
                case TAG_CLOSE:
                    if (ch == '>') {
                        // 弹出栈顶,匹配闭合标签
                        if (!tagStack.empty()) {
                            tagStack.pop();
                        }
                        state = TEXT;
                    }
                    break;
            }
        }
        return true;
    }
};

注意:状态机法虽然性能好,但代码量翻倍。我个人建议:配置文件小于100KB用正则,大于100KB用状态机。别问我100KB这个阈值怎么来的——踩坑踩出来的。

正则表达式进阶技巧

解析配置时,有几个正则技巧特别实用:

场景 正则表达式 说明
匹配键值对 (\w+)\s*=\s*([^;\n]+) 支持值中包含空格,遇到分号或换行停止
匹配带引号的值 "([^"]*)" 匹配双引号包裹的内容
匹配注释 ^\s*[;#] 支持分号和井号两种注释
匹配XML标签 <(\w+)[^>]*>(.*?)</\1> 使用回溯引用 \1 确保标签配对

避坑指南:我曾经在匹配XML标签时用了贪婪模式 .*,结果遇到多个相同标签时,正则直接匹配到最后一个闭合标签。后来改成非贪婪模式 .*? 才解决问题。记住:解析嵌套结构时,非贪婪是你的好朋友。

完整示例:统一配置接口

把INI和XML统一成一个接口,调用方不用关心底层格式:

class ConfigParser {
public:
    enum Format { INI, XML };

    bool load(const std::string& filename, Format fmt) {
        switch (fmt) {
            case INI: return ini_.load(filename);
            case XML: return xml_.load(filename);
        }
        return false;
    }

    std::string get(const std::string& key,
                    const std::string& defaultVal = "") {
        // 先查INI,再查XML
        std::string val = ini_.get("", key, "");
        if (!val.empty()) return val;
        val = xml_.get(key, "");
        return val.empty() ? defaultVal : val;
    }

private:
    IniParser ini_;
    XmlParser xml_;
};

使用方式:

ConfigParser config;
config.load("server.ini", ConfigParser::INI);
std::string host = config.get("host", "127.0.0.1");
int port = std::stoi(config.get("port", "8080"));

知识体系图

下面这张图展示了配置解析器的核心逻辑和决策路径:

配置解析器核心逻辑 配置文件 判断格式 INI / XML INI解析器 XML解析器 正则匹配节区 正则匹配键值对 正则匹配标签 栈维护嵌套层级 统一配置接口 INI路径 XML路径

总结

配置解析器这个实战,说白了就是三件事:读文件、用正则、存数据。INI格式简单直接,适合做应用配置;XML格式结构清晰,适合做系统配置。正则表达式是解析利器,但要注意性能问题——大文件还是状态机靠谱。

嗯,最后提醒一句:如果你在项目中真的需要解析复杂的XML,还是老老实实用libxml2或者pugixml吧。自己手写的解析器,应付80%的场景没问题,但遇到奇葩格式还是会翻车的。我曾经就因为这个被坑过一次,从那以后,我对现成库的态度就变成了「能用就用,除非万不得已才自己写」。


公众号:蓝海资料掘金营,微信deep3321