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格式结构清晰,适合做系统配置。正则表达式是解析利器,但要注意性能问题——大文件还是状态机靠谱。
嗯,最后提醒一句:如果你在项目中真的需要解析复杂的XML,还是老老实实用libxml2或者pugixml吧。自己手写的解析器,应付80%的场景没问题,但遇到奇葩格式还是会翻车的。我曾经就因为这个被坑过一次,从那以后,我对现成库的态度就变成了「能用就用,除非万不得已才自己写」。
公众号:蓝海资料掘金营,微信deep3321