用go实现Parsers & Lexers

在当今网络应用和REST API的时代,编写解析器似乎是一种垂死的艺术。你可能会认为编写解析器是一个复杂的工作,只保留给编程语言设计师,但我想消除这种观念。在过去几年中,我为JSON,CSS3和数据库查询语言编写了解析器,所写的解析器越多,我越喜欢他们。

基础(The Basics)

让我们从基础开始:什么是词法分析器?什么解析器?当我们分析一种语言(或从技术上讲,一种“正式语法”)时,我们分两个阶段进行。首先我们将一系列的字符分解成tokens。对于类似SQL的语言,这些tokens可能是“whitespace“,”number“,“SELECT“等。这个处理过程叫作lexing(或者tokenizing,或scanning)

以此简单的SQL SELECT语句为例:

SELECT * FROM mytable

当我们标记(tokenize)这个字符串时,我们会得到:

  1. `SELECT` `WS` `ASTERISK`  `WS` `FROM` `WS` `STRING<"mytable">`

这个过程称为词法分析(lexical analysis),与阅读时我们如何分解句子中的单词相似。这些tokens随后被反馈给执行语义分析的解析器。

解析器的任务是理解这些token,并确保它们的顺序正确。这类似于我们如何从句子中组合单词得出意思。我们的解析器将从token序列中构造出一个抽象语法树(AST),而AST是我们的应用程序将使用的。

在SQL SELECT示例中,我们的AST可能如下所示:

  1. type SelectStatement struct {
  1.          Fields []string
  1.          TableName string
  1. }

解析器生成器 (Parser Generators)

许多人使用解析器生成器(Parser Generators)为他们自动写一个解析器(parser)和词法分析器(lexer)。有很多工具可以做到这一点:lex,yacc,ragel。还有一个内置在go 工具链中的用go实现的yacc(goyacc)。

然而,在多次使用解析器生成器后,我发现有些问题。首先,他们涉及到学习一种新的语言来声明你的语言格式,其次,他们很难调试。例如,尝试阅读ruby语言的yacc文件。Eek!

在看完Rob Pike关于lexical scanning的演讲和读完go标准库包的实现后,我意识到手写一个自己的parser和lexer多么简单和容易。让我们用一个简单的例子来演示这个过程。

用Go写一个lexer

定义我们的tokens

我们首先为SQL SELECT语句编写一个简单的解析器和词法分析器。首先,我们需要用定义在我们的语言中允许的标记。我们只允许SQL 语言的一小部分:

  1. // Token represents a lexical token.
  1. type Token int
  1.  
  1. const (
  1.          // Special tokens
  1.          ILLEGAL Token = iota
  1.          EOF
  1.          WS
  1.  
  1.          // Literals
  1.          IDENT // fields, table_name
  1.  
  1.          // Misc characters
  1.          ASTERISK // *
  1.          COMMA    // ,
  1.  
  1.          // Keywords
  1.          SELECT
  1.          FROM
  1. )

我们将使用这些tokens来表示字符序列。例如WS将表示一个或多个空白字符,IDENT将表示一个标识符,例如字段名或表名称。

定义字符类  (Defining character classes)

定义可以检查字符类型的函数很有用。这里我们定义两个函数,一个用于检查一个字符是否为空格,另一个用于检查字符是否是字母。

  1. func isWhitespace(ch rune) bool {
  1.          return ch == ' ' || ch == '\t' || ch == '\n'
  1. }
  1.  
  1. func isLetter(ch rune) bool {
  1.          return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')
  1. }

定义“EOF”也是有用的,以便像任何其他character一样对的EOF:

  1. var eof = rune(0)

Scanning our input

接下来,我们要定义我们的扫描器类型。这个类型将用一个bufio.Reader包装输入阅读器,我们可以从头部取字符。我们还添加帮助函数(helper function),用于从底层Reader读取,取消读取字符。

  1. // Scanner represents a lexical scanner.
  1. type Scanner struct {
  1.          r *bufio.Reader
  1. }
  1.  
  1. // NewScanner returns a new instance of Scanner.
  1. func NewScanner(r io.Reader) *Scanner {
  1.          return &Scanner{r: bufio.NewReader(r)}
  1. }
  1.  
  1. // read reads the next rune from the bufferred reader.
  1. // Returns the rune(0) if an error occurs (or io.EOF is returned).
  1. func (s *Scanner) read() rune {
  1.          ch, _, err := s.r.ReadRune()
  1.          if err != nil {
  1.                  return eof
  1.          }
  1.          return ch
  1. }
  1.  
  1. // unread places the previously read rune back on the reader.
  1. func (s *Scanner) unread() { _ = s.r.UnreadRune() }

Scanner的入口函数是Scan()方法,它返回下一个token和它所代表的文字字符串。

  1. // Scan returns the next token and literal value.
  1. func (s *Scanner) Scan() (tok Token, lit string) {
  1.          // Read the next rune.
  1.          ch := s.read()
  1.  
  1.          // If we see whitespace then consume all contiguous whitespace.
  1.          // If we see a letter then consume as an ident or reserved word.
  1.          if isWhitespace(ch) {
  1.                  s.unread()
  1.                  return s.scanWhitespace()
  1.          } else if isLetter(ch) {
  1.                  s.unread()
  1.                  return s.scanIdent()
  1.          }
  1.  
  1.          // Otherwise read the individual character.
  1.          switch ch {
  1.          case eof:
  1.                  return EOF, ""
  1.          case '*':
  1.                  return ASTERISK, string(ch)
  1.          case ',':
  1.                  return COMMA, string(ch)
  1.          }
  1.  
  1.          return ILLEGAL, string(ch)
  1. }

该入口函数从读取第一个字符开始。如果字符是whitespace,那么它将与所有连续的whitespace一起使用。如果是一个letter,则被视为identifier和keyword的开始。否则,我们将检查它是否是我们的单字符tokens之一。

扫描连续字符  Scanning contiguous characters

当我们想要连续使用多个字符时,我们可以在一个简单的循环中执行此操作。在scanWhitespace()中,我们假设在碰到一个非空格字符前所有字符都是whitespaces。

  1. // scanWhitespace consumes the current rune and all contiguous whitespace.
  1. func (s *Scanner) scanWhitespace() (tok Token, lit string) {
  1.          // Create a buffer and read the current character into it.
  1.          var buf bytes.Buffer
  1.          buf.WriteRune(s.read())
  1.  
  1.          // Read every subsequent whitespace character into the buffer.
  1.          // Non-whitespace characters and EOF will cause the loop to exit.
  1.          for {
  1.                  if ch := s.read(); ch == eof {
  1.                           break
  1.                  } else if !isWhitespace(ch) {
  1.                           s.unread()
  1.                           break
  1.                  } else {
  1.                           buf.WriteRune(ch)
  1.                  }
  1.          }
  1.  
  1.          return WS, buf.String()
  1. }

相同的逻辑可以应用于扫描identifiers。在scanident()中,我们将读取所有字母和下划线,直到遇到不同的字符:

  1. // scanIdent consumes the current rune and all contiguous ident runes.
  1. func (s *Scanner) scanIdent() (tok Token, lit string) {
  1.          // Create a buffer and read the current character into it.
  1.          var buf bytes.Buffer
  1.          buf.WriteRune(s.read())
  1.  
  1.          // Read every subsequent ident character into the buffer.
  1.          // Non-ident characters and EOF will cause the loop to exit.
  1.          for {
  1.                  if ch := s.read(); ch == eof {
  1.                           break
  1.                  } else if !isLetter(ch) && !isDigit(ch) && ch != '_' {
  1.                           s.unread()
  1.                           break
  1.                  } else {
  1.                           _, _ = buf.WriteRune(ch)
  1.                  }
  1.          }
  1.  
  1.          // If the string matches a keyword then return that keyword.
  1.          switch strings.ToUpper(buf.String()) {
  1.          case "SELECT":
  1.                  return SELECT, buf.String()
  1.          case "FROM":
  1.                  return FROM, buf.String()
  1.          }
  1.  
  1.          // Otherwise return as a regular identifier.
  1.          return IDENT, buf.String()
  1. }

这个函数在后面会检查文字字符串是否是一个保留字,如果是,将返回一个指定的token。

用Go写一个解析器 Writing a Parser in Go

设置解析器

一旦我们准备好lexer,解析SQL语句就变得更加容易了。首先定义我们的parser:

  1. // Parser represents a parser.
  1. type Parser struct {
  1.          s   *Scanner
  1.          buf struct {
  1.                  tok Token  // last read token
  1.                  lit string // last read literal
  1.                  n   int    // buffer size (max=1)
  1.          }
  1. }
  1.  
  1. // NewParser returns a new instance of Parser.
  1. func NewParser(r io.Reader) *Parser {
  1.          return &Parser{s: NewScanner(r)}
  1. }

我们的解析器只是包装了我们的scanner,还为上一个读取token添加了缓冲区。我们定义helper function进行扫描和取消扫描,以便使用这个缓冲区。

  1. // scan returns the next token from the underlying scanner.
  1. // If a token has been unscanned then read that instead.
  1. func (p *Parser) scan() (tok Token, lit string) {
  1.          // If we have a token on the buffer, then return it.
  1.          if p.buf.n != 0 {
  1.                  p.buf.n = 0
  1.                  return p.buf.tok, p.buf.lit
  1.          }
  1.  
  1.          // Otherwise read the next token from the scanner.
  1.          tok, lit = p.s.Scan()
  1.  
  1.          // Save it to the buffer in case we unscan later.
  1.          p.buf.tok, p.buf.lit = tok, lit
  1.  
  1.          return
  1. }
  1.  
  1. // unscan pushes the previously read token back onto the buffer.
  1. func (p *Parser) unscan() { p.buf.n = 1 }

我们的parser此时已经不关心whitespaces了,所以将定义一个helper 函数来查找下一个非空白标记(token)

  1. // scanIgnoreWhitespace scans the next non-whitespace token.
  1. func (p *Parser) scanIgnoreWhitespace() (tok Token, lit string) {
  1.          tok, lit = p.scan()
  1.          if tok == WS {
  1.                  tok, lit = p.scan()
  1.          }
  1.          return
  1. }

解析输入 Parsing the input

我们的解析器的entry function是parse()方法。该函数将从Reader中解析下一个SELECT语句。如果reader中有多个语句,那么我们可以重复调用这个函数。

  1. func (p *Parser) Parse() (*SelectStatement, error)

我们将这个函数分解成几个小部分。首先定义我们要从函数返回的AST结构

  1. stmt := &SelectStatement{}

然后我们要确保有一个SELECT token。如果没有看到我们期望的token,那么将返回一个错误来报告我们我们发现的字符串。

  1. if tok, lit := p.scanIgnoreWhitespace(); tok != SELECT {
  1.          return nil, fmt.Errorf("found %q, expected SELECT", lit)
  1. }

接下来要解析以逗号分隔的字段列表。在我们的解析器中,我们只考虑identifiers和一个星号作为可能的字段:

  1. for {
  1.          // Read a field.
  1.          tok, lit := p.scanIgnoreWhitespace()
  1.          if tok != IDENT && tok != ASTERISK {
  1.                  return nil, fmt.Errorf("found %q, expected field", lit)
  1.          }
  1.          stmt.Fields = append(stmt.Fields, lit)
  1.  
  1.          // If the next token is not a comma then break the loop.
  1.          if tok, _ := p.scanIgnoreWhitespace(); tok != COMMA {
  1.                  p.unscan()
  1.                  break
  1.          }
  1. }

在字段列表后,我们希望看到一个From关键字:

  1. // Next we should see the "FROM" keyword.
  1. if tok, lit := p.scanIgnoreWhitespace(); tok != FROM {
  1.          return nil, fmt.Errorf("found %q, expected FROM", lit)
  1. }

然后我们想要看到选择的表的名称。这应该是标识符token

  1. tok, lit := p.scanIgnoreWhitespace()
  1. if tok != IDENT {
  1.          return nil, fmt.Errorf("found %q, expected table name", lit)
  1. }
  1. stmt.TableName = lit

如果到了这一步,我们已经成功分析了一个简单的SQL SELECT 语句,这样我们就可以返回我们的AST结构:

  1. return stmt, nil

恭喜!你已经建立了一个可以工作的parser

深入了解,你可以在以下位置找到本示例完整的源代码(带有测试)https://github.com/benbjohnson/sql-parser

这个解析器的例子深受InfluxQL解析器的启发。如果您有兴趣深入了解并理解多个语句解析,表达式解析或运算符优先级,那么我建议您查看仓库:

https://github.com/influxdb/influxdb/tree/master/influxql

如果您有任何问题或想聊聊解析器,请在Twitter上@benbjohnson联系我。

Handwritten Parsers & Lexers in Go (翻译)的更多相关文章

  1. Handwritten Parsers & Lexers in Go (Gopher Academy Blog)

    Handwritten Parsers & Lexers in Go (原文地址  https://blog.gopheracademy.com/advent-2014/parsers-lex ...

  2. Writing a simple Lexer in PHP/C++/Java

    catalog . Comparison of parser generators . Writing a simple lexer in PHP . phc . JLexPHP: A PHP Lex ...

  3. RAPIDXML 中文手册,根据官方文档完整翻译!

    简介:这个号称是最快的DOM模型XML分析器,在使用它之前我都是用TinyXML的,因为它小巧和容易上手,但真正在项目中使用时才发现如果分析一个比较大的XML时TinyXML还是表现一般,所以我们决定 ...

  4. 《Django By Example》第十二章 中文 翻译 (个人学习,渣翻)

    书籍出处:https://www.packtpub.com/web-development/django-example 原作者:Antonio Melé (译者注:第十二章,全书最后一章,终于到这章 ...

  5. R-CNN论文翻译

    R-CNN论文翻译 Rich feature hierarchies for accurate object detection and semantic segmentation 用于精确物体定位和 ...

  6. AlexNet论文翻译-ImageNet Classification with Deep Convolutional Neural Networks

    ImageNet Classification with Deep Convolutional Neural Networks 深度卷积神经网络的ImageNet分类 Alex Krizhevsky ...

  7. 《Django By Example》第十二章(终章) 中文 翻译 (个人学习,渣翻)

    书籍出处:https://www.packtpub.com/web-development/django-example 原作者:Antonio Melé (译者注:第十二章,全书最后一章,终于到这章 ...

  8. 翻译Lanlet2

    Here is more information on the basic primitives that make up a Lanelet2 map. Read here for a primer ...

  9. 【翻译】Knowledge-Aware Natural Language Understanding(摘要及目录)

    翻译Pradeep Dasigi的一篇长文 Knowledge-Aware Natural Language Understanding 基于知识感知的自然语言理解 摘要 Natural Langua ...

随机推荐

  1. Java之多态

    一.多态 1.含义 一种类型,呈现多种状态.主要关注类多态.方法多态. 2.多态的前提:继承 使用父类引用指向子类对象: Animal a1 = new Cat(): Object a1 = new ...

  2. (译)ABP之依赖注入

    原文地址:https://aspnetboilerplate.com/Pages/Documents/Dependency-Injection 什么是依赖注入 传统方式的问题 解决方案 构造函数注入 ...

  3. 实践作业2:黑盒测试实践——搭建被测web系统Day 4

    1.选择合适的待测web系统 2.安装web系统运行所需工具,配置运行环境 3.成功运行web系统 4.尝试Katalon测试系统

  4. PhpStorm2017版激活方法、汉化方法以及界面配置

    PhpStorm激活和汉化文件下载网址:http://pan.baidu.com/s/1nuHF1St(提取密码:62cg) PHPMailer的介绍 PhpStorm是一个轻量级且便捷的PHP ID ...

  5. SketchMaster 隐私政策

    隐私政策 本应用尊重并保护所有使用服务用户的个人隐私权.为了给您提供更准确.更有个性化的服务,本应用会按照本隐私权政策的规定使用和披露您的个人信息.但本应用将以高度的勤勉.审慎义务对待这些信息.除本隐 ...

  6. post 与get 区别

    刷新/后退按钮 GET后退按钮/刷新无害,POST数据会被重新提交(浏览器应该告知用户数据会被重新提交). 书签 GET书签可收藏,POST为书签不可收藏. 缓存 GET能被缓存 缓存是针对URL来进 ...

  7. Python的控制语句

    1.  控制语句 控制语句是用来改变程序执行的顺序.程序利用控制语句有条件地执行语句,循环地执行语句或者跳转到程序中的其他部分执行语句. Python支持三种不同的控制语句:if,for和while, ...

  8. Effective Java 第三版——3. 使用私有构造方法或枚类实现Singleton属性

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  9. ZED-Board从入门到精通系列(八)——Vivado HLS实现FIR滤波器

    http://www.tuicool.com/articles/eQ7nEn 最终到了HLS部分.HLS是High Level Synthesis的缩写,是一种能够将高级程序设计语言C,C++.Sys ...

  10. PKI(公钥基础设施)基础知识笔记

    数字签名 数字签名(又称公钥数字签名.电子签章)是一种类似写在纸上的普通的物理签名,可是使用了公钥加密领域的技术实现.用于鉴别数字信息的方法. 一套数字签名通常定义两种互补的运算.一个用于签名,还有一 ...