解析INI
文件
为了总结一下本章介绍的内容,我们来看一下如何调用正则表达式来解决问题。假设我们编写一个程序从因特网上获取我们敌人的信息(这里我们实际上不会编写该程序,仅仅编写读取配置文件的那部分代码,对不起)。配置文件如下所示。
searchengine=https://duckduckgo.com/?q=$1
spitefulness=9.7
; comments are preceded by a semicolon...
; each section concerns an individual enemy
[larry]
fullname=Larry Doe
type=kindergarten bully
website=http://www.geocities.com/CapeCanaveral/11451
[davaeorn]
fullname=Davaeorn
type=evil wizard
outputdir=/home/marijn/enemies/davaeorn
该配置文件格式的语法规则如下所示(它是广泛使用的格式,我们通常称之为INI
文件):
忽略空行和以分号起始的行。
使用
[]
包围的行表示一个新的节(section)。如果行中是一个标识符(包含字母和数字),后面跟着一个=字符,则表示向当前节添加选项。
其他的格式都是无效的。
我们的任务是将这样的字符串转换为一个对象,该对象的属性包含没有节的设置的字符串,和节的子对象的字符串,节的子对象也包含节的设置。
由于我们需要逐行处理这种格式的文件,因此预处理时最好将文件分割成一行行文本。我们使用第 6 章中的string.split("\n")
来分割文件内容。但是一些操作系统并非使用换行符来分隔行,而是使用回车符加换行符("\r\n"
)。考虑到这点,我们也可以使用正则表达式作为split
方法的参数,我们使用类似于/\r?\n/
的正则表达式,这样可以同时支持"\n"
和"\r\n"
两种分隔符。
function parseINI(string) {
// Start with an object to hold the top-level fields
let currentSection = {name: null, fields: []};
let categories = [currentSection];
string.split(/\r?\n/).forEach(line => {
let match;
if (match = line.match(/^(\w+)=(.*)$/)) {
section[match[1]] = match[2];
section = result[match[1]] = {};
} else if (!/^\s*(;.*)?$/.test(line)) {
throw new Error("Line '" + line + "' is not valid.");
}
});
return result;
}
console.log(parseINI(`
name=Vasilis
[address]
city=Tessaloniki`));
// → {name: "Vasilis", address: {city: "Tessaloniki"}}
代码遍历文件的行并构建一个对象。 顶部的属性直接存储在该对象中,而在节中找到的属性存储在单独的节对象中。 section
绑定指向当前节的对象。
有两种重要的行 - 节标题或属性行。 当一行是常规属性时,它将存储在当前节中。 当它是一个节标题时,创建一个新的节对象,并设置section
来指向它。
这里需要注意,我们反复使用^
和$
确保表达式匹配整行,而非一行中的一部分。如果不使用这两个符号,大多数情况下程序也可以正常工作,但在处理特定输入时,程序就会出现不合理的行为,我们一般很难发现这个缺陷的问题所在。
if (match = string.match(...))
类似于使用赋值作为while
的条件的技巧。你通常不确定你对match
的调用是否成功,所以你只能在测试它的if
语句中访问结果对象。 为了不打破else if
形式的令人愉快的链条,我们将匹配结果赋给一个绑定,并立即使用该赋值作为if
语句的测试。