编程是门手艺,手写解析器:提升编程能力

开发 前端
解析是 很常见的需求,特别是软件的配置,但很多程序员不会自己去手写,可能也不知道怎么写 。

什么是解析

解析是将 输入 根据要求 运算 出 结果。 比如将四则表达式"1 + 2"运算出3。

解析是 很常见的需求,特别是软件的配置,但很多程序员不会自己去手写,可能也不知道怎么写 。 大概是因为现在已经有了一些通用的标准格式,比如ini, json, yaml等,这些常见的格式都有标准库可供使用。 然而 不管是否需要自己定制,还是用现有的格式,手写解析文本是一项非常提升编程能力的事情。 这也是本文的目的,通过四则运算的例子,分享如何实现解析器,还有编程经验和思考。

例子四则运算

麻雀虽小,五脏俱全,这估计是最迷你的解析例子了。我们要做的事情就是将字符串"100 - 20 * 30 + 40 * 50",解析运算出结果:240。这里只支持整数和加减乘除。 有兴趣的同学,可以不看本文的实现 ,先自己手写试一下 。

(四则运算语法表示)

解析通用模式

不管是实现语言,解析配置,解析特定文本需求,解析模式大致一样,都是如图所示。只是有些语义更复杂,有些需要考虑性能,在解析和执行中间会增加更多的处理,像语法树,甚至C语言.o文件。

通用术语:

text:文本字符串作为输入源,文件的话会被读成(流式)字符串。

run:运行。运行输入,计算出结果。

parse:解析。

token:词法单元,比如 "123", " 40", " +", " -" 。

expression:表达式,比如" 3", "1 + 2" 。

unary:一元操作数,比如3, 20, 100。

code:中间码,由parse产生。

exec:运行中间码产生结果。

这些术语也会在代码实现中出现。

运算就是我们要做的事情,要实现解析和执行两个功能。以解析四则为例:

  1. calc_run(text): 
  2.     code = cacl_parse(text) 
  3.     cacl_exec(code) 

(calc是calculator的缩写)

计算机世界里的四则运算

代码是人类逻辑的一种表示方式。"1 + 2"是人眼可读懂的,但对计算机,我们得设计一个它能执行的。

1. 借助栈(数组),解析后的中间码这样表示: 。

code = [1, 2, +]

2. 执行,遍历code作相应的处理。

lo op:

c = code[i++];

if (c is num) {
s.push(c)
} else {
op1 = s.pop()
op2 = s.pop()
r = op1 + op2
s.push(r)

}

(s是临时的栈用于存放运算值)

思路就是压值进去,碰到操作符取出来运算,并且将结果压进去, 所以最后 的结果 会是s[0]。

如何实现新功能

假设这是一个需求,怎么在不用996的情况下,完成并且是高质量的代码。这是编程能力的综合体现,所以本质上还是要不断提升编程能力。需要指出的是简化是一种特别的能力,我们要在编码中经常使用。

1. 构思整体设计,能清晰的陈述实现逻辑。

2. 编写测试用例。

我们给这个功能取个名称:calculator,运算器的意思,并且用它的缩写作为前缀cacl。

编程经验:无比重要的命名

善用缩写作为前缀, 对项目和模块加个有意义的前缀能让读代码的人清楚它的上下文。 这在团队协作里更有价值。

a. 项目:比如nginx的源码里都有ngx_,nginx unit里的nxt_,njs里的njs_等。这些都可以让人清楚的知道这个项目的名称。

b. 模块:比如nginx里的http模块ngx_http_,日志模块ngx_http_log_,unit里文件服务的nxt_http_static_等。注意模块是可以包含子模块的。

所以我们将用cacl_作为四则运算解析的前缀,会有cacl_run, cacl_parse, cacl_exec这样的函数。

编程经验: 追求清晰和简洁

代码即逻辑,其它都是表达逻辑的方式。像文件,模块,函数和变量等。

不管需要什么样的命名,变量,功能函数,文件名等,清晰和简洁是我认为最重要的。在做到清晰的前提下保持简洁,即一目了然。

比如命名:

nxt_http_request.c ,表示http 的请求处理模块 ,足够清晰和 简洁。

nxt_h1proto.c,表示http的1.1协议的处理。

nxt_epoll_engine.c,表示epoll模块,对比下nxt_event_epoll_engine.c。因为epoll已经是个专业术语,用于处理网络事件的,这时event就变的多余了,在能表达清晰的前提下,继续追求简洁。

比如逻辑:

good

  1. /* 
  2.  * 读数据 
  3.  * 如果成功,追加数据 
  4.  * 返回数据 
  5. */ 
  6. data = read(); 
  7. if (data) { 
  8.     data = data + "..."
  9. return data; 

ok

  1. /* 
  2.  * 读数据 
  3.  * 如果失败,返回空 
  4.  * 追加数据 
  5.  * 返回数据 
  6. */ 
  7. data = read(); 
  8. if (data == null) { 
  9.     return null
  10. data = data + "..."
  11. return data; 

这是个很小的例子,只是为了说明在做到清晰的前提,做到越简洁越好。

比如设计:

因为天赋可能是天花板。目前只懂的是保持简单和通用是好的设计。

前段时间提要实现一个功能,是Unit有关response filter的,但没有被允许,这种设计目前组里只有nginx作者Igor的设计让人最放心。其它人都能做,包括我,但是设计出来的东西要做到简单和通用,还是差点功力。

以前也经历过这个阶段:学会复杂的功能,并觉得是干货。建议多思考,多原创,多看优秀的作品,及早突破一些认识的局限。

实现解析逻辑

解析的结果是产生中间码,引入parser对象方便解析。

  1. function calc_parse(text) { 
  2.     var parser = { 
  3.         text: text, 
  4.         pos: 0
  5.         token: {}, 
  6.         code: [] 
  7.     }; 
  8.  
  9.  
  10.     next_token(parser); 
  11.  
  12.  
  13.     if (calc_parse_expr(parser, 2)) { 
  14.         return null
  15.     } 
  16.  
  17.  
  18.     if (parser.token.val != TOK_END) { 
  19.         return null
  20.     } 
  21.  
  22.  
  23.     parser.code.push(OP_END); 
  24.  
  25.  
  26.     return parser.code; 

对那些分散的信息,要考虑用聚集的方式比如对象,放在一起处理。这也是高内聚的一种体现。

前面提到简化是一种能力。

简化1: 能从text里找出(所有的)token。实现下next_token()。

  1. var TOK_END = 0
  2.     TOK_NUM = 1
  3. function next_token(parser) { 
  4.     var s = parser.text; 
  5.  
  6.  
  7.     while (s[parser.pos] == ' ') { 
  8.         parser.pos++; 
  9.     } 
  10.  
  11.  
  12.     if (parser.pos == s.length) { 
  13.         parser.token.val = TOK_END; 
  14.         return
  15.     } 
  16.  
  17.  
  18.     var c = s[parser.pos]; 
  19.  
  20.  
  21.     switch (c) { 
  22.     case '1'case '2'case '3'case '4'
  23.     case '5'case '6'case '7'case '8'
  24.     case '9'case '0'
  25.         parse_number(parser); 
  26.         break
  27.  
  28.  
  29.     default
  30.         parser.token.val = c; 
  31.         parser.pos++; 
  32.         break
  33.     } 
  34.  
  35.  
  36. function parse_number(parser) { 
  37.     var s = parser.text; 
  38.     var num = 0
  39.  
  40.  
  41.     while (parser.pos < s.length) { 
  42.         var c = s[parser.pos]; 
  43.  
  44.  
  45.         if (c >= '0' && c <= '9') { 
  46.             num = num * 10 + (c - '0'); 
  47.             parser.pos++; 
  48.             continue
  49.         } 
  50.  
  51.  
  52.         break
  53.     } 
  54.  
  55.  
  56.     parser.token.val = TOK_NUM; 
  57.     parser.token.num = num; 

每次调用next_token(),就能拿到当前的token,并且解析移动到下一个token的开始位置。

简化2:可以将运算符*和+当作同一级,但是这里篇幅有限,不贴中间实现过程

简化3: 分析逻辑,直到能清晰的表达,这也说明你足够理解它的本质了。

以"1 + 2 * 3 - 4"为例:

我们将整个字符串称为expression,里面的各块也是expression。表达式的表示是 expression: expression [op expression]。

因此 "1 + 2 * 3 - 4"是表达式,"2 * 3"也是表达式, "1"和"4"也是表达式。

注意*的优先级比+高,因为可以这样分析:

2 * 3是一个整体,操作数(2) 操作符(*) 操作数(3)

1 + 2 * 3也是一个整体,操作数(1) 操作符(+) 操作数(2 * 3)

依此类推。代码如下:

  1. var OP_END = 0
  2.     OP_NUM = 1
  3.     OP_ADD = 2
  4.     OP_SUB = 3
  5.     OP_MUL = 4
  6.     OP_DIV = 5
  7.      
  8. function calc_parse_expr(parser, level) { 
  9.     if (level == 0) { 
  10.         return calc_parse_unary(parser); 
  11.     } 
  12.  
  13.  
  14.     if (calc_parse_expr(parser, level - 1)) { 
  15.         return -1
  16.     } 
  17.  
  18.  
  19.     for (;;) { 
  20.         var op = parser.token.val; 
  21.  
  22.  
  23.         switch (level) { 
  24.         case 1
  25.             switch (op) { 
  26.             case '*'
  27.                 var opcode = OP_MUL; 
  28.                 break
  29.  
  30.  
  31.             case '/'
  32.                 var opcode = OP_DIV; 
  33.                 break
  34.  
  35.  
  36.             default
  37.                 return 0
  38.             } 
  39.  
  40.  
  41.             break
  42.  
  43.  
  44.         case 2
  45.             switch (op) { 
  46.             case '+'
  47.                 var opcode = OP_ADD; 
  48.                 break
  49.  
  50.  
  51.             case '-'
  52.                 var opcode = OP_SUB; 
  53.                 break
  54.  
  55.  
  56.             default
  57.                 return 0
  58.             } 
  59.  
  60.  
  61.             break
  62.         } 
  63.  
  64.  
  65.         next_token(parser); 
  66.  
  67.  
  68.         if (calc_parse_expr(parser, level - 1)) { 
  69.             return -1
  70.         } 
  71.  
  72.  
  73.         parser.code.push(opcode); 
  74.     } 
  75.  
  76.  
  77.     return 0
  78.  
  79.  
  80. function calc_parse_unary(parser) { 
  81.     switch (parser.token.val) { 
  82.     case TOK_NUM: 
  83.         parser.code.push(OP_NUM); 
  84.         parser.code.push(parser.token.num); 
  85.         break
  86.  
  87.  
  88.     default
  89.         return -1
  90.     } 
  91.  
  92.  
  93.     next_token(parser); 
  94.  
  95.  
  96.     return 0

注意:我们是边解析边产生中间码的。

实现之执行

执行就相对简单很多了,只要思路清晰。

  1. function calc_exec(code) { 
  2.     var i = 0
  3.     var stack = []; 
  4.  
  5.  
  6.     for (;;) { 
  7.         opcode = code[i++]; 
  8.  
  9.  
  10.         switch (opcode) { 
  11.         case OP_END: 
  12.             return stack[0]; 
  13.  
  14.  
  15.         case OP_NUM: 
  16.             var num = code[i++]; 
  17.             stack.push(num); 
  18.             break
  19.  
  20.  
  21.         case OP_ADD: 
  22.             var op2 = stack.pop(); 
  23.             var op1 = stack.pop(); 
  24.             var r = op1 + op2; 
  25.             stack.push(r); 
  26.             break
  27.  
  28.  
  29.         case OP_SUB: 
  30.             var op2 = stack.pop(); 
  31.             var op1 = stack.pop(); 
  32.             var r = op1 - op2; 
  33.             stack.push(r); 
  34.             break
  35.  
  36.  
  37.         case OP_MUL: 
  38.             var op2 = stack.pop(); 
  39.             var op1 = stack.pop(); 
  40.             var r = op1 * op2; 
  41.             stack.push(r); 
  42.             break
  43.  
  44.  
  45.         case OP_DIV: 
  46.             var op2 = stack.pop(); 
  47.             var op1 = stack.pop(); 
  48.             var r = op1 / op2; 
  49.             stack.push(r); 
  50.             break
  51.         } 
  52.     } 

测试用例很重要

  1. function calc_run(text) { 
  2.     var code = calc_parse(text); 
  3.     if (code == null) { 
  4.         return null
  5.     } 
  6.  
  7.  
  8.     return calc_exec(code); 
  9.  
  10.  
  11. function unit_test(tests) { 
  12.     for (var i = 0; i < tests.length; i++) { 
  13.         var test = tests[i]; 
  14.         var ret = calc_run(test.text); 
  15.  
  16.  
  17.  
  18.  
  19.         if (ret != test.expect) { 
  20.             console.log("\"" + test.text + "\" failed,"
  21.                         "expect \"" + test.expect + "\","
  22.                         "got \"" + ret + "\""); 
  23.         } 
  24.     } 
  25.  
  26.  
  27.  
  28.  
  29. var tests = [ 
  30.     { 
  31.         text: "123"
  32.         expect: 123 
  33.     }, 
  34.     { 
  35.         text: "1 + 2 + 3"
  36.         expect: 6 
  37.     }, 
  38.     { 
  39.         text: "10 - 1 - 11"
  40.         expect: -2 
  41.     }, 
  42.     { 
  43.         text: "1 + 2 * 3 - 4"
  44.         expect: 3 
  45.     }, 
  46.      
  47.         text: "1 + 2 * 3 - 4 / 2"
  48.         expect: 5 
  49.     }, 
  50.     { 
  51.         text: ""
  52.         expect: null 
  53.     }, 
  54.     { 
  55.         text: "a"
  56.         expect: null 
  57.     }, 
  58.     { 
  59.         text: "10 a "
  60.         expect: null 
  61.     }, 
  62.     { 
  63.         text: "10 + "
  64.         expect: null 
  65.     }, 
  66.     { 
  67.         text: " + 2"
  68.         expect: null 
  69.     }, 
  70. ]; 
  71.  
  72.  
  73. unit_test(tests); 

编程经验:一致性

每个代码里有意义的函数和变量都像人物一样。之前怎么命名,之后也一样,同样的意思不要有多余的表示。而且保持它们的出场顺序不变。

  1. var TOK_END = 0
  2.     TOK_NUM = 1
  3.  
  4.  
  5. var OP_END = 0
  6.     OP_NUM = 1
  7.     OP_ADD = 2
  8.     OP_SUB = 3
  9.     OP_MUL = 4
  10.     OP_DIV = 5
  11.  
  12.  
  13. function calc_run(text) { 
  14.  
  15.  
  16. function calc_parse(text) { 
  17.  
  18.  
  19. function calc_parse_expr(parser, level) { 
  20.  
  21.  
  22. function calc_parse_unary(parser) { 
  23.  
  24.  
  25. function next_token(parser) { 
  26.  
  27.  
  28. function parse_number(parser) { 
  29.  
  30.  
  31. function calc_exec(code) { 
  32.  
  33.  
  34. function unit_test(tests) { 
  35.  
  36.  
  37. var tests = [ 
  38. ]; 
  39.  
  40.  
  41. unit_test(tests); 

https://github.com/hongzhidao/the-craft-of-programming

在开源浪潮下,写好的代码尤其重要!

 

责任编辑:张燕妮 来源: 程序员洪志道
相关推荐

2017-12-28 10:39:23

编程网站编辑

2020-07-23 07:27:50

编程学习技术

2020-09-27 15:52:02

编程语言C 语言Python

2022-09-27 08:01:48

递归函数GScript

2015-07-23 09:38:38

2015-03-13 11:23:21

编程编程超能力编程能力

2022-09-24 19:38:40

开源C 语言

2021-12-14 10:08:57

编程语言PythonJava

2013-11-14 10:05:25

程序员职业转型

2023-03-27 18:18:47

GPT-4AI

2019-09-22 21:05:51

编程语言开发

2009-03-19 09:26:05

RSS解析器MagpieRSS

2020-11-12 07:00:50

JavaScript前端编程语言

2019-11-15 14:48:26

编程语言开发者分析

2010-02-22 13:38:50

Python解析器

2010-02-22 16:51:03

Python 解析器

2011-07-05 17:19:47

元编程

2022-02-27 14:45:16

编程语言JavaC#

2012-09-04 11:20:31

2020-12-02 10:13:45

JacksonJDK解析器
点赞
收藏

51CTO技术栈公众号