1
用代码的行话来讲,本篇是《键歇》系列的初始化。为了应这个景儿,我也为之选定了一个与“初始化”相关的主题——INI 文件(格式)。
INI 的历史可以追溯到 MS-DOS 及 16 位的 Windows 时代,可谓十分之悠久。它的格式十分简单,只有两个组成元素:节(sections)和属性(properties),这两个元素将 INI 描述成了一个双层级的数据结构,使用者可以将需要保存的配置项按照逻辑整理成不同的大类来进行组织并使之持久化(persistence)。如下例(来自 维基百科)所示:
; last modified 1 April 2001 by John Doe
[owner]
name=John Doe
organization=Acme Widgets Inc.
[database]
; use IP address in case network name resolution is not working
server=192.0.2.62
port=143
file="payroll.dat"
是的,就这么简单。INI 格式极易被人工识读,以至于用语法树来描述它都只会显得繁冗与晦涩。
相应地,为之实现一个解析器也是件很简单的事情:
#include <map>
#include <sstream>
#include <cassert>
template <typename T>
class IniDataT
{
public:
bool GetSection(const T *sec,
std::map< std::basic_string<T>, std::basic_string<T> > *dst) const
{
typename std::map<std::basic_string<T>, SectionData>::const_iterator it
= m_sections.find(sec);
if (it == m_sections.end()) {
return false;
}
*dst = it->second;
return true;
}
bool GetString(const T *sec, const T *key, std::basic_string<T> *dst) const
{
typename std::map<std::basic_string<T>, IniSection>::const_iterator it
= m_sections.find(sec);
if (it == m_sections.end()) {
return false;
}
typename IniSection::const_iterator it2 = it->second.find(key);
if (it2 != it->second.end()) {
dst->assign(it2->second);
return true;
} else {
return false;
}
}
void Parse(const std::basic_string<T> &data)
{
if (data.empty()) {
return;
}
std::basic_stringstream<T> stm(data, std::ios_base::in);
std::basic_string<T> line, sec;
while (!stm.eof()) {
std::getline(stm, line);
Trim(&line);
if (line.empty()) {
continue;
}
if ('[' == line[0] && ']' == line[line.length() - 1]) {
sec = line.substr(1, line.length() - 2);
} else if (!sec.empty()) {
size_t pos = line.find_first_of('=');
if (std::basic_string<T>::npos != pos) {
std::basic_string<T> key = line.substr(0, pos);
std::basic_string<T> value = line.substr(pos + 1);
Trim(&key);
Trim(&value);
m_sections[sec][key] = value;
}
}
}
}
void Reset(void)
{
m_sections.clear();
}
private:
template <typename T>
static void Trim(std::basic_string<T> *str)
{
assert(NULL != str);
if (str->empty())
return;
// Trim begin chars.
typename std::basic_string<T>::iterator it = str->begin();
while (it != str->end()) {
if (!iswspace(*it))
break;
++it;
}
// Trim end chars;
typename std::basic_string<T>::iterator it2 = str->end();
if (it2 == it) {
str->clear();
return;
}
while (it2 != it) {
--it2;
if (!iswspace(*it2))
break;
}
str->assign(it, ++it2);
}
private:
typedef std::map< std::basic_string<T>, std::basic_string<T> > SectionData;
std::map<std::basic_string<T>, SectionData> m_sections;
};
也许你会问了:Windows 不是有 INI 操作的相关 API 吗?有必要另外造一个轮子出来吗?
当然有必要啊少年。因为这些个函数在 Windows CE 里没有,Linux 里没有,OS X 里还没有——估计红旗和那个具有自主知识产权的 COS 里也都够呛能有。因此从可移植的角度来看,这么个平台无关的 INI 解析器还是很有存在的必要的。
抛开跨平台的因素,Win32 的 INI APIs 还有另外的一个硬伤——效率。不知出于何种考虑,GetPrivateProfileXXX 和 WritePrivateProfileXXX 这一系列 API 的实现方式都是无缓存的实时查找与读写。也就是说,每一个 Get/Write 的操作都是按照以下步骤进行的:
- 打开目标 INI 文件;
- 以线性方式来顺序查找目标的节和键名;
- 对文件中与键名相对应的值进行读/写操作;
- 关闭目标文件。
较之上面 IniDataT 所实现的双索引而言,这些个步骤的效率可真是低得让人发指啊。
当然,后来的事情大家都知道了——微软最后放弃了用 INI 来保存配置的方式,转而投向了注册表。在这个问题上,真相可能不止一个,不过我相信效率问题一定会是位列于其中的。
虽然被微软抛弃了,但 INI 的优点却是抹杀不了的,所以它仍然保持着异常活跃的状态:
- 因为 INI 的存储介质通常是文件,所以在其中保存配置是不会像注册表那样在系统中留下垃圾的。于是,INI 成为了绿色软件中最受青睐的配置保存方式。
- 由于极易人工识读,因此编辑 INI 文件并不需要专门的编辑器。所以,对于没有 GUI 而又需要用到一些简易配置的应用程序而言,INI 无疑是最佳的选择。例如 php.ini 和 .gitconfig。
- 虽然 INI 两个层级的纯文本结构极大地限制了它的描述能力,不过它恰恰可以成为国际化多语言文本资源的最佳解决方案:节用于区分不同的界面,每个键-值对则用于标识单词和句子的翻译。除此之外,由于 PHP 原生支持对 INI 的读写,所以使用 PHP 来开发国际化网站时亦可使用 INI 来处理多语言的问题。
既然说到了软件国际化,那么就再多说几句。对于支持切换语言的软件而言,一般都会提供一个类似下面这样的语言选择界面:
![](http://blog.titilima.com/wp-content/uploads/2014/02/rfc-1-1.png)
在上图里,界面语言列表的语言名称都是直接从语言资源的 INI 文件中读取的,这些名称被保存成类似这个样子:
[Global]
Language=简体中文
; 其它字符串 ...
显然,使用类似 IniDataT 这样的解析器来解析整个 INI 的内容并不是个好主意——程序不需要 INI 的完整内容,因此也就压根儿没必要扫描整个文件甚或建立双索引什么的。在这个特定的场景之下,采用线性查找的策略才恰恰是最优的(而且为了使查找速度最快,我还故意将所需要的字符串放置在了文件的开头)——于是,我又重拾起了旧式的 API GetPrivateProfileString,哪怕微软在 MSDN 的每个版本中都不厌其烦地抒写着这么一句哀怨的备注:
This function is provided only for compatibility with 16-bit Windows-based applications. Applications should store initialization information in the registry.
2014 年 2 月 18 日