解析INI文件
为了总结一下本章介绍的内容,我们来看一下如何调用正则表达式来解决问题。假设我们编写一个程序从因特网上获取我们敌人的信息(这里我们实际上不会编写该程序,仅仅编写读取配置文件的那部分代码,对不起)。配置文件如下所示。
searchengine=https://duckduckgo.com/?q=$1spitefulness=9.7; comments are preceded by a semicolon...; each section concerns an individual enemy[larry]fullname=Larry Doetype=kindergarten bullywebsite=http://www.geocities.com/CapeCanaveral/11451[davaeorn]fullname=Davaeorntype=evil wizardoutputdir=/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 fieldslet 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语句的测试。
