专栏首页码神路漫漫Go 译文之词法分析与解析 Part Three

Go 译文之词法分析与解析 Part Three

译者前言

最近发现我的翻译是越来越随性了,刚开始文章翻译的时候比较拘束,现在更多强调可读性,比如有些对文章大意没有什么影响的文字我现在都会选择直接跳过。

这篇文章主要是关于 INI 解释器的 parser 实现,它会从上一节中 Lexer 中接收 Token 解析,最终返回给使用者具有实际意义的结构体。读了这个系列的文章,我相信大家对词法器实现的原理将会有了基本的理解,但如果要真正实践,似乎还有一段距离。有兴趣的话,我们可以实现个自己的 JSON 解释器。要求可以稍微简化,只解析到 JSON 的第一层。

译文如下:


本系列第一篇文章英文原版,我们介绍了词法分析解析的一些基础概念,了解了 INI 文件的基本组成,并在此基础上定义了一些常量和结构体,这对我们接下来实现 INI 文件解析会很有帮助。

第二篇文章英文原版,因主要聚焦在 Lexer 的实现。它完成了将输入文本转化为 Token 的过程。

今天是本系列的最后一篇文章,最终完成我们的解释器。解释器负责从 channel 读取 Token,并最终创建表示 INI 文件内容的结构体实例。解析完成后,我们可以用 JSON 格式将结果打印出来。

结构体

解析器负责启动词法器和从 channel 读取 Token 的组件。接收到 Token 后,解析器需要知道当前 Token 状态,然后将其解析到对应结构中。我们要做的第一件事就是,定义表示 INI 内容的结构体。将主要涉及三个结构体。

第一个表示 Key/Value 的结构体,名称为 IniKeyValue,如下。

model/ini/IniKeyValue.go

package ini

type IniKeyValue struct {
	Key   string `json:"key"`
	Value string `json:"value"`
}

第二个表示 Section 的结构体,名称为 IniSection,如下:

/model/ini/IniSection.go

package ini

type IniSection struct {
	Name          string        `json:"name"`
	KeyValuePairs []IniKeyValue `json:"keyValuePairs"`
}

我们知道,Section 是由 Key/Value 组成的,其中 KeyValuePairs 即是属于这个 Section 的 Key/Value。如果 Key/Value 是不属于任何 Section,将会属于 Name 为空的 Section 中。

最后一个表示整个文件的结构体,名称为 IniFile,如下:

/model/ini/IniFile.go

package ini

type IniFile struct {
	FileName string       `json:"fileName"`
	Sections []IniSection `json:"sections"`
}

IniFile 有两个成员字段组成,分别 FileName 文件名称和一系列 Section。

解析器

解析器的编写,我们要做的第一件事是,创建一个用于存放解析结构的变量,即一个 IniFile 结构体类型变量。如下:

output := ini.IniFile{
   FileName: fileName,
   Sections: make([]ini.IniSection, 0),
}

现在,我们还需要一些变量追踪 Token、 Token 的值,还有解析器当前的状态。例如,当我们在获取 Key/Value 时,我们需要知道当前处于哪个 Section 中。接下来,就可以启动词法器了。

var token lexertoken.Token
var tokenValue string

/* State variables */
key := ""

log.Println("Starting lexer and parser for file", fileName, "...")

l := lexer.BeginLexing(filename, input)

到此,相关变量已经定义完成,并且词法器也成功启动。接下来开始从 channel 接收 Token,如果 Token 类型不是 TOKEN_VALUE,执行 Trim 空格。

for {
	token = l.NextToken()

	if token.Type != lexertoken.TOKEN_VALUE {
		tokenValue = strings.TrimSpace(token.Value)
	} else {
		tokenValue = token.Value
	}

	...

我们知道,如果词法器遍历到文件结尾,将返回类型为 EOF 的 Token。此时,需要将 Section 和 Key/Value 记录下来,并退出循环。

if isEOF(token) {
	output.Sections = append(output.Sections, section)
	break
}

parser 函数的最后实现具体 Token 的处理,主要有三个 Token 需要关注。

第一个是 Section Token,如果遇到 Section Token,我们首先检查 section 变量中是否已存在 Key/Value,有则将它记录到 output.Sections 中。然后重置 section 变量追踪当前 Section 和接下来 Key/Value。

接着是 Key/Value。如果遇到 TOKEN_KEY,变量 key 用于记录 TOKEN_KEY 的值。遇到 TOKEN_VALUE,我们就可以变量 key 对应的 value,并将其 append 到 section.KeyValuePairs 中。

示例代码如下:

switch token.Type {
   case lexertoken.TOKEN_SECTION:
      /*
* Reset tracking variables
*/
      if len(section.KeyValuePairs) > 0 {
         output.Sections = append(output.Sections, section)
      }

      key = ""

      section.Name = tokenValue
      section.KeyValuePairs = make([]ini.IniKeyValue, 0)

   case lexertoken.TOKEN_KEY:
      key = tokenValue

   case lexertoken.TOKEN_VALUE:
      section.KeyValuePairs = append(section.KeyValuePairs, ini.IniKeyValue{
         Key: key,
         Value: tokenValue,
      })
      key = ""
   }

测试

开发工作已经完成,下面进入测试阶段。Github 上有相应的测试代码,go get 下载好代码,在你的 GOPATH 的 src/github.com/adampresley/sample-ini-parser 目录下的 sampleIniParser.go 即是测试代码。

代码如下:

sampleInput := `
key=abcdefg

[User]
userName=adampresley
keyFile=~/path/to/keyfile

[Servers]
server1=localhost:8080
`

parsedINIFile := parser.Parse("sample.ini", sampleInput)
prettyJSON, err := json.MarshalIndent(parsedINIFile, "", " ")

if err != nil {
   log.Println("Error marshalling JSON:", err.Error())
   return
}

log.Println(string(prettyJSON))

直接通过 go run sampleIniParser.go 执行即可。执行输出如下:

2019/08/01 00:06:33 Starting lexer and parser for file sample.ini ...
2019/08/01 00:06:33 Parser has been shutdown
2019/08/01 00:06:33 {
   "fileName": "sample.ini",
   "sections": [
      {
         "name": "",
         "keyValuePairs": [
            {
               "key": "key",
               "value": "abcdefg"
            }
         ]
      },
      {
         "name": "User",
         "keyValuePairs": [
            {
               "key": "userName",
               "value": "adampresley"
            },
            {
               "key": "keyFile",
               "value": "~/path/to/keyfile"
            }
         ]
      },
      {
         "name": "Servers",
         "keyValuePairs": [
            {
               "key": "server1",
               "value": "localhost:8080"
            }
         ]
      }
   ]
}

总结

这个系列文章是非常有挑战性,但也非常有趣。词法分析与解析是一个非常复杂的话题,有太多内容需要学习。我们可以看到,即使像上面 INI 文件解析这样简单的工作,我们也需要花费一些精力才能完成。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Go 译文之词法分析与解析 Part Three

    解析器负责启动词法器和从 channel 读取 Token 的组件。接收到 Token 后,解析器需要知道当前 Token 状态,然后将其解析到对应结构中。我们...

    波罗学
  • Go 笔记之如何测试你的 Go 代码

    不论是开源项目,还是日常程序的开发,测试都是必不可少的一个环节。今天我们开始进入 Go 测试模块 testing 的介绍。

    波罗学
  • Go 译文之词法分析与解析 - Part One

    一直对词法分析与解析的话题比较感兴趣,最近发现了好几篇相关的优秀文章,准备好好翻译和研究下。我的理解,词法分析与解析的应用还是比较广泛的,无论简单的配置文件、各...

    波罗学
  • php后端跨域header头设置

    93年的老男孩
  • Redis | 事物源码阅读 —— watch

    关于 watch 存在于几个数据结构当中,基本上在 redisServer、redisCient 和 redisDb 当中,它们大致的关系如下:

    码农UP2U
  • 基于Token的登录流程

    要想区分来自不同用户的请求的话,服务端需要根据客户端请求确认其用户身份,即身份验证

    ayqy贾杰
  • Tensorflow教程(十四) 命令行参数tf.flags的使用

    自己查了很多关于tensorflow命令行教程,大多都和实例2相似,对于强迫症的我,忍不了,以后统一成实例1用了。

    致Great
  • 杨老师带你深入研究ArrayList和LinkedList的区别不同

    ArrayList实现了随机访问的接口,LinkedList实现了Deque双向队列的接口,最终继承的是Queue。

    杨校
  • 对希尔排序的一点理解

    希尔排序实际上是一种特殊的插入排序,它是通过对直接插入排序的一些特点的利用,从而达到化简得效果。 具体内容为:

    HUBU生信
  • 单点登录之如何平衡 Token 安全性和用户体验?

    在 《IDaaS 技术解析系列(一)》中,我们介绍了在单点登录中Token认证相对于传统基于Session认证的优势,本文继续介绍一组相关概念:Access T...

    玉符IDaaS

扫码关注云+社区

领取腾讯云代金券