日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關咨詢
選擇下列產品馬上在線溝通
服務時間:8:30-17:00
你可能遇到了下面的問題
關閉右側工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
編程是門手藝,手寫解析器:提升編程能力

什么是解析

解析是將 輸入 根據(jù)要求 運算 出 結果。 比如將四則表達式"1 + 2"運算出3。

解析是 很常見的需求,特別是軟件的配置,但很多程序員不會自己去手寫,可能也不知道怎么寫 。 大概是因為現(xiàn)在已經有了一些通用的標準格式,比如ini, json, yaml等,這些常見的格式都有標準庫可供使用。 然而 不管是否需要自己定制,還是用現(xiàn)有的格式,手寫解析文本是一項非常提升編程能力的事情。 這也是本文的目的,通過四則運算的例子,分享如何實現(xiàn)解析器,還有編程經驗和思考。

例子四則運算

麻雀雖小,五臟俱全,這估計是最迷你的解析例子了。我們要做的事情就是將字符串"100 - 20 * 30 + 40 * 50",解析運算出結果:240。這里只支持整數(shù)和加減乘除。 有興趣的同學,可以不看本文的實現(xiàn) ,先自己手寫試一下 。

(四則運算語法表示)

解析通用模式

不管是實現(xiàn)語言,解析配置,解析特定文本需求,解析模式大致一樣,都是如圖所示。只是有些語義更復雜,有些需要考慮性能,在解析和執(zhí)行中間會增加更多的處理,像語法樹,甚至C語言.o文件。

通用術語:

text:文本字符串作為輸入源,文件的話會被讀成(流式)字符串。

run:運行。運行輸入,計算出結果。

parse:解析。

token:詞法單元,比如 "123", " 40", " +", " -" 。

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

unary:一元操作數(shù),比如3, 20, 100。

code:中間碼,由parse產生。

exec:運行中間碼產生結果。

這些術語也會在代碼實現(xiàn)中出現(xiàn)。

運算就是我們要做的事情,要實現(xiàn)解析和執(zhí)行兩個功能。以解析四則為例:

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

(calc是calculator的縮寫)

計算機世界里的四則運算

代碼是人類邏輯的一種表示方式。"1 + 2"是人眼可讀懂的,但對計算機,我們得設計一個它能執(zhí)行的。

1. 借助棧(數(shù)組),解析后的中間碼這樣表示: 。

code = [1, 2, +]

2. 執(zhí)行,遍歷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]。

如何實現(xiàn)新功能

假設這是一個需求,怎么在不用996的情況下,完成并且是高質量的代碼。這是編程能力的綜合體現(xiàn),所以本質上還是要不斷提升編程能力。需要指出的是簡化是一種特別的能力,我們要在編碼中經常使用。

1. 構思整體設計,能清晰的陳述實現(xiàn)邏輯。

2. 編寫測試用例。

我們給這個功能取個名稱:calculator,運算器的意思,并且用它的縮寫作為前綴cacl。

編程經驗:無比重要的命名

善用縮寫作為前綴, 對項目和模塊加個有意義的前綴能讓讀代碼的人清楚它的上下文。 這在團隊協(xié)作里更有價值。

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這樣的函數(shù)。

編程經驗: 追求清晰和簡潔

代碼即邏輯,其它都是表達邏輯的方式。像文件,模塊,函數(shù)和變量等。

不管需要什么樣的命名,變量,功能函數(shù),文件名等,清晰和簡潔是我認為最重要的。在做到清晰的前提下保持簡潔,即一目了然。

比如命名:

nxt_http_request.c ,表示http 的請求處理模塊 ,足夠清晰和 簡潔。

nxt_h1proto.c,表示http的1.1協(xié)議的處理。

nxt_epoll_engine.c,表示epoll模塊,對比下nxt_event_epoll_engine.c。因為epoll已經是個專業(yè)術語,用于處理網(wǎng)絡事件的,這時event就變的多余了,在能表達清晰的前提下,繼續(xù)追求簡潔。

比如邏輯:

good

 
 
 
 
  1. /*
  2.  * 讀數(shù)據(jù)
  3.  * 如果成功,追加數(shù)據(jù)
  4.  * 返回數(shù)據(jù)
  5. */
  6. data = read();
  7. if (data) {
  8.     data = data + "...";
  9. }
  10. return data;

ok

 
 
 
 
  1. /*
  2.  * 讀數(shù)據(jù)
  3.  * 如果失敗,返回空
  4.  * 追加數(shù)據(jù)
  5.  * 返回數(shù)據(jù)
  6. */
  7. data = read();
  8. if (data == null) {
  9.     return null;
  10. }
  11. data = data + "...";
  12. return data;

這是個很小的例子,只是為了說明在做到清晰的前提,做到越簡潔越好。

比如設計:

因為天賦可能是天花板。目前只懂的是保持簡單和通用是好的設計。

前段時間提要實現(xiàn)一個功能,是Unit有關response filter的,但沒有被允許,這種設計目前組里只有nginx作者Igor的設計讓人最放心。其它人都能做,包括我,但是設計出來的東西要做到簡單和通用,還是差點功力。

以前也經歷過這個階段:學會復雜的功能,并覺得是干貨。建議多思考,多原創(chuàng),多看優(yōu)秀的作品,及早突破一些認識的局限。

實現(xiàn)解析邏輯

解析的結果是產生中間碼,引入parser對象方便解析。

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

對那些分散的信息,要考慮用聚集的方式比如對象,放在一起處理。這也是高內聚的一種體現(xiàn)。

前面提到簡化是一種能力。

簡化1: 能從text里找出(所有的)token。實現(xiàn)下next_token()。

 
 
 
 
  1. var TOK_END = 0,
  2.     TOK_NUM = 1;
  3. function next_token(parser) {
  4.     var s = parser.text;
  5.     while (s[parser.pos] == ' ') {
  6.         parser.pos++;
  7.     }
  8.     if (parser.pos == s.length) {
  9.         parser.token.val = TOK_END;
  10.         return;
  11.     }
  12.     var c = s[parser.pos];
  13.     switch (c) {
  14.     case '1': case '2': case '3': case '4':
  15.     case '5': case '6': case '7': case '8':
  16.     case '9': case '0':
  17.         parse_number(parser);
  18.         break;
  19.     default:
  20.         parser.token.val = c;
  21.         parser.pos++;
  22.         break;
  23.     }
  24. }
  25. function parse_number(parser) {
  26.     var s = parser.text;
  27.     var num = 0;
  28.     while (parser.pos < s.length) {
  29.         var c = s[parser.pos];
  30.         if (c >= '0' && c <= '9') {
  31.             num = num * 10 + (c - '0');
  32.             parser.pos++;
  33.             continue;
  34.         }
  35.         break;
  36.     }
  37.     parser.token.val = TOK_NUM;
  38.     parser.token.num = num;
  39. }

每次調用next_token(),就能拿到當前的token,并且解析移動到下一個token的開始位置。

簡化2:可以將運算符*和+當作同一級,但是這里篇幅有限,不貼中間實現(xiàn)過程

簡化3: 分析邏輯,直到能清晰的表達,這也說明你足夠理解它的本質了。

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

我們將整個字符串稱為expression,里面的各塊也是expression。表達式的表示是 expression: expression [op expression]。

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

注意*的優(yōu)先級比+高,因為可以這樣分析:

2 * 3是一個整體,操作數(shù)(2) 操作符(*) 操作數(shù)(3)

1 + 2 * 3也是一個整體,操作數(shù)(1) 操作符(+) 操作數(shù)(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.     if (calc_parse_expr(parser, level - 1)) {
  13.         return -1;
  14.     }
  15.     for (;;) {
  16.         var op = parser.token.val;
  17.         switch (level) {
  18.         case 1:
  19.             switch (op) {
  20.             case '*':
  21.                 var opcode = OP_MUL;
  22.                 break;
  23.             case '/':
  24.                 var opcode = OP_DIV;
  25.                 break;
  26.             default:
  27.                 return 0;
  28.             }
  29.             break;
  30.         case 2:
  31.             switch (op) {
  32.             case '+':
  33.                 var opcode = OP_ADD;
  34.                 break;
  35.             case '-':
  36.                 var opcode = OP_SUB;
  37.                 break;
  38.             default:
  39.                 return 0;
  40.             }
  41.             break;
  42.         }
  43.         next_token(parser);
  44.         if (calc_parse_expr(parser, level - 1)) {
  45.             return -1;
  46.         }
  47.         parser.code.push(opcode);
  48.     }
  49.     return 0;
  50. }
  51. function calc_parse_unary(parser) {
  52.     switch (parser.token.val) {
  53.     case TOK_NUM:
  54.         parser.code.push(OP_NUM);
  55.         parser.code.push(parser.token.num);
  56.         break;
  57.     default:
  58.         return -1;
  59.     }
  60.     next_token(parser);
  61.     return 0;
  62. }

注意:我們是邊解析邊產生中間碼的。

實現(xiàn)之執(zhí)行

執(zhí)行就相對簡單很多了,只要思路清晰。

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

測試用例很重要

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

編程經驗:一致性

每個代碼里有意義的函數(shù)和變量都像人物一樣。之前怎么命名,之后也一樣,同樣的意思不要有多余的表示。而且保持它們的出場順序不變。

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

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

在開源浪潮下,寫好的代碼尤其重要!


當前名稱:編程是門手藝,手寫解析器:提升編程能力
URL鏈接:http://www.5511xx.com/article/cdoejeo.html