8.4.1 INI文件简介
以下是一个INI例子文件的内容:
[DISPLAY_SETTING]
#是否显示启动窗口:
will_show_splash_window = yes
default_title = welcom...
[NETWORK_SETTING]
svc_host = www.d2school.com
svc_port = 80
带中括号的行代表一个“配置段(section)”的开始,该段所含内容直到碰上下一段或文件结束。
段下的内容是注释或配置项,注释以#开始。配置项一行为一项,使用“=”分为左边的“Key(键)”和右边的“Value(值)”。
例子中有两个配置段:[DISPLAY_SETTING]和[NETWORK_SETTING],正好每个配置段又有两行配置项。
程序需实现读指定段、指定键的配置值,比如指定“NETWORK_SETTING”和“svc_host”,得到“www.d2school.com”。还需实现对指定段、指定键的配置值的修改,并写回INI文件。
如果指定段或指定键不存在,则自动添加一项。
本例所示并非标准的INI语法,几点格式要求会影响程序的实现,特别说明如下:
(1)所有段名、键名、值的内容,均区分大小写;
(2)键名称不能以“#”开头;
(3)配置项的“键”和“值”,前后可以存在空格,实际操作时将被忽略。如“svc_port=80”,程序将自动去除等号前后的空格;
(4)“段”名称则指[ ]的所有内容,不作去空格处理。意思是[ ABC ]和[ABC]是不同的两个段。不过所在行首、行末的空格同样会被去除;
(5)注释应独占一行或多行,不能和配置项同行;
(6)修改配置项并回写文件后,原有注释内容应能保留,同时确保原文件各行次序不被改变。”
8.4.2 面向过程的设计
“读”和“写”分别是两个独立的过程,采用函数表示是:
//读INI文件指定段,指定键的值,value以string类型返回
//如果指定配置项不存在,则返回default_value
string ReadINIvalue(string const& filename
, string const& section
, string const& key
, string const& default_value);
//写INI文件指定段,指定键的值,value以string类型传入
//返回是否写成功。
string WriteINIvalue(string const& filename
, string const& section
, string const& key
, string const& default_value);
ReadINIValue()实现过程又可分解为:打开文件,然后,在文件中前进到指定“段(section)”,再在该段内,找到指定“键(key)”,找到则读出其值,否则返回“默认值(default_value)”。前进到指定“段”的函数命名为“GotoSection()”;查找指定“键”的函数命名为“FindValue()”:
//读INI文件指定段,指定键的值,value以string类型返回
//如果指定配置项不存在,则返回default_value
string ReadINIvalue(string const& filename
, string const& section
, string const& key
, string const& default_value)
{
assert(!key.empty() && !section.empty());
//打开配置文件
ifstream file(filename);
if(!file)
{
return default_value; //配置文件不存在?也返回默认值
}
//在"文件/file"中前进到指定“段/section”
if(!GotoSection(file, section))
{
return default_value;
}
string value;
//在"文件/file"中读取指定"键/key"的值"值/value"
if(!FindValue(file, key, value))
{
return default_value; //找不到指定"键",返回默认值
}
return value; //如果找到了,则返回value
}
“前进到指定段(GotoSection)”子过程的实现很简单:循环读取文件的每一行,然后和加了中括号的段名称比较,相等就是找到了:
//循环读取文件的每一行,
//然后和加了中括号的段名称比较,相等就是找到了
bool GotoSection(ifstream& file, string const& section)
{
//比如待找的段是"NETWORK_SETTING",则参与比较的应是
//"[NETWORK_SETTING]"
string secion_line = "[" + section + "]";
while(!file.eof()) //文件未结束..
{
string line;
getline(file, line); //从文件中读出一行
Trim(line); //去除行首尾的空格
// cout << line << endl;
if(line == secion_line)
{
//找到了。
//注意!此时ifstream的位置在段名的下一行
return true;
}
}
return false; //找不到
}
使用“Trim”函数将入参的字符串去除其前后的空格(‘ ’、‘\t’),避免配置文件中的某一行前后有意无意地有个空格,造成找不到。Trim函数也由我们手动打造
//去掉字符串两端的空格
void Trim(string& str)
{
//注意“\t”后有一空格
str.erase(0, str.find_first_not_of("\t "));
str.erase(str.find_last_not_of("\t ") + 1); //同上
}
再来考虑“FindValue”的实现。刚才“GotoSection()”返回本段第一行的位置,FindValue就在本段中找有指定“键”的行。在“本段中”的意思是:只要碰上有新的一段就结束查找。那么查找指定键呢 ?我们暂时将该操作命名为“SplitKeyValue()”,则“FindValue()”的实现如下:
//将从file中读到的值,写入value
bool FindValue(ifstream& file, string const& key, string& value)
{
while(!file.eof()) //文件未结束..
{
string line;
getline(file, line); //从文件中读出一行
Trim(line);//去除行首行尾空格
if(line.empty())
{
continue; //空行?跳过...
}
if(line[0] == '[') //碰上新一段了呀,放弃查找
{
break; //碰上新一段,放弃查找
}
//按"="拆成左键右值
KeyValue kv = SplitKeyValue(line);
if(kv.key == key) //是要找的键...
{
value = kv.value;
return true; //找到了
}
}
return false; //找不到
}
“SplitKeyValue()”的逻辑是:查找给定字符串中,包含有第一个“=”符号的位置,再以该位置将字符串拆成两段,分别去除前后空格,就得到key和value。KeyValue是一个结构,二者定义如下:
struct KeyValue
{
string key;
string value;
};
//将字符串分解成KeyValue结构体
KeyValue SplitKeyValue(string const& str)
{
KeyValue result;
//提醒:= 前后并无空格
string::size_type pos = str.find("=");
//find()返回值是字母在母串中的位置(下标记录),
//如果没有找到,就会返回一个特别的
//标记npos,也就是-1. 返回值可以看成是一个int类型的数
if(pos == string::npos)
{
return result;
}
//取子串:从位置0开始,长度pos
result.key = str.substr(0, pos);
//取子串;从位置pos + 1到结束
result.value = str.substr(pos + 1);
Trim(result.key);
Trim(result.value);
return result;
}
有关“ReadINIValue()”操作的所有子过程都实现了。将以上代码按正确的依赖次序,组织到同一文件中,就可以进行读INI文件的测试了:
#include <iostream>
#include <list>
#include <fstream>
#include <cassert>
.../* 以上代码略 */
int main()
{
string const& filename = "demo.ini";
string value = ReadINIvalue(filename
, "NETWORK_SETTING" //section
, "svc_host" //key
, "127.0.0.1"); //default_value
cout << value << endl; //www.d2school.com
return 0;
}
接下来实现“WriteINIValue()”操作。
是否只需把“ifstream”换成“ofstream”,再改改一些小逻辑就能解决?常规的文件改写操作,并不能直接在文件流上处理。假设某文件中有一行内容:abcdefg,然后要将其中的‘c’改成“CCC”,并不能先定位到‘c’的位置,然后直接写上“CCC”了事,那样后面的内容会丢失。
正确的做法是将全部内容读入,然后在程序中(内存中)将“abcdefg”替换成“abcCCCdefg”,再整串输出到源文件中。所以,变化很大:我们会将文件所有内容读入到一个list再做处理。
下面的代码,增加了修改操作所需添加的代码,
#include <list>
//去除字符串前后空白,但并不是在原字符串身上操作
//而是使用一个复制品,在复制品上去除,并返回该复制品。
string TrimCopy(string const& str)
{
string str_copy = str;
Trim(str_copy);
return str_copy;
}
//又一个GotoSection,但这个是在list <string> 上操作,
//返回迭代器位置,也是执行指定section的下一行
list <string> :: iterator
GotoSection(list <string>& lines, string const& section)
{
string section_line = "[" + section + "]";
for(auto it = lines.begin(); it != lines.end(); ++it)
{
//去除空格只是为了比较,并不会真正修改原有内容
string line = TrimCopy(* it);
if(line == section_line)
{
++ it;
return it;
}
}
//没找到指定段,添加之
lines.push_back(section_line);
return lines.end();
}
//beg是之前GotoSection()返回的位置,在这个位置开始,向后查找
//含有指定键的行。如果找不到会在本段结束位置插入一行
void ChangeValue(list <string>::iterator beg
,list <string> & lines
,string const& key
,string const& value)
{
auto it = beg;
for(it; it != lines.end(); ++ it)
{
string line_trim = TrimCopy(*it);
if(line_trim.empty())
{
continue;
}
if(line_trim[0] == '[') //碰到下一段了?放弃查找,跳出循环...
{
break;
}
//line_trim既不是空,又不是段名,将期分解为键值对
KeyValue kv = SplitKeyValue(line_trim);
if(kv.key == key)
{
if(kv.value != value)//值不相等才修改
{
string line_with_new_value = (key + "=" +value);
* it = line_with_new_value; //在list中改掉这行的内容
}
return;
}
}
//如果lines中找不到指定的键值对,
string new_line = (key + "=" +value);
lines.insert(it, new_line);
}
//把文件所有行,读入到内存中的list
list <string> ReadLines(ifstream& ifs)
{
list <string> lines;
while(!ifs.eof())
{
string line;
getline(ifs, line); //将line读入到ifs中
lines.push_back(line);
}
return lines;
}
//对应ReadLines, 将list中的所有行,写入文件
void WriteLines(ofstream& ofs, list <string> const& lines)
{
for(auto it = lines.begin(); it != lines.end(); ++ it)
{
ofs << *it;
if(it != --lines.end()) //避免每次写,都多产生一个空行
{
ofs << endl;
}
}
}
//修改指定INI文件,指定段,指定配置项的值
bool WriteINIvalue(string const& filename
, string const& section
, string const& key
, string const& value)
{
assert(!key.empty() && !section.empty());
list <string> lines;
//打开指定INI文件
//注意,如果源文件不存在也不要紧,后面ofstream会创建它
//将文件读入一个输入流中,文件名为filename
ifstream ifs(filename);
//如果打开成功,就读入原文件所有行
if(ifs.is_open())
{
lines = ReadLines(ifs);
ifs.close();
}
//尝试找指定section在list中的位置(返回该位置下一行)
//前进到指定的section的下一行
list <string> :: iterator it = GotoSection(lines, section);
//在指定段中修改指定配置项的值
ChangeValue(it, lines, key, value); //改变这一行的键和值
//输出:
ofstream ofs(filename);
if(!ofs)
{
return false; //输出文件打不开,只能返回错误
}
//将lines内容输出到输出流,进而写到文件中
WriteLines(ofs, lines);
return true;
}
以下是完整的读写INI测试的代码:
int main(int argc, char** argv)
{
//新文件,第一次运行时不存在
string const& filename = "demo_new.ini";
string value = ReadINIvalue(filename
, "NETWORK_SETTING" //section
, "svc_host" //key
, "127.0.0.1"); //default_value
cout << value << endl; //127.0.0.1
value = "www.d2school.com";
WriteINIvalue(filename, "NETWORK_SETTING", "svc_host", value);
WriteINIvalue(filename, "USER_INFO", "name", "Tom");
return 0;
}
新需求:
在保留现有功能的同时,要求采集操作过程中的出错信息。在现有代码的基础上要扩展此功能,简单粗暴的做法是为函数再增一个列表参数,以读为例:
string ReadINIvalue(string const& filename
, string const& section
, string const& key
, string const& default_value
, list <string>& warning_list
);