当前位置: 首页 > 科技观察

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

时间:2023-03-19 19:45:23 科技观察

什么是解析?解析就是根据要求根据输入计算出结果。比如“1+2”这四个表达式,计算得到3。解析是一个很常见的需求,尤其是软件配置,但是很多程序员不知道怎么手写,也不知道怎么写写下来。可能是因为已经有了一些通用的标准格式,比如ini、json、yaml等,这些通用格式都有标准库。但是,不管是需要自己定制,还是使用已有的格式,手写和解析文本都是一件大大提高编程能力的事情。这也是本文的目的。通过四个算术运算的例子,我们将分享如何实现解析器,以及编程经验和思考。四算术麻雀的例子虽小,五脏六腑却一应俱全。这可能是最小的分析示例。我们要做的就是解析字符串“100-20*30+40*50”,得到结果:240。这里只支持整数和加减乘除。有兴趣的同学可以不看本文的实现,直接手写。(四种算术运算的语法表示)解析常用模式无论是实现语言、解析配置,还是解析具体的文本需求,解析模式都大致相同,如图所示。只是有的语义比较复杂,有的需要考虑性能。在解析和执行之间会加入更多的处理,比如语法树,甚至是C语言的.o文件。一般术语:文本:文本字符串作为输入源,文件的单词将被读取为(流式)字符串。跑跑。运行输入,计算结果。解析:解析。token:词汇单位,如“123”、“40”、“+”、“-”。expression:表达式,如“3”、“1+2”。unary:一元操作数,如3、20、100。code:中间码,由parse生成。exec:运行中间代码产生结果。这些术语也出现在代码实现中。计算是我们要做的,实现解析和执行两个功能。以这四种的解析为例:calc_run(text):code=cacl_parse(text)cacl_exec(code)(calc是calculator的缩写)计算机世界中的四种算术运算代码是人类逻辑的表现。“1+2”是人眼可读的,但是对于计算机来说,我们必须设计一个它可以执行的。1、借助栈(数组),解析出的中间代码表示如下:code=[1,2,+]2.执行,遍历相应处理的代码。loop:c=code[i++];if(cisnum){s.push(c)}else{op1=s.pop()op2=s.pop()r=op1+op2s.push(r)}(s是存放计算值的临时栈)思路是把值压进去,遇到运算符取出运算,把结果压进去,所以最后的结果会是s[0]。如何实现新功能假设这是一个需求,如何在不使用996的情况下,用高质量的代码完成它。这是编程能力的综合体现,所以本质上还是需要不断提高编程能力。需要指出的是,简化是我们在编码中应该经常使用的一种特殊能力。1.构思整体设计,能够清晰表述实现逻辑。2.编写测试用例。我们给这个函数起个名字:calculator,运算符的意思,用它的缩写作为前缀cacl。编程经验:极其重要的命名擅长使用缩写作为前缀。为项目和模块添加有意义的前缀可以帮助阅读代码的人理解其上下文。这在团队合作中更有价值。A。项目:比如nginx源码中有ngx_,nginx单元中有nxt_,njs中有njs_等,这些可以让人清楚的知道这个项目的名称。b.模块:比如nginx中的http模块ngx_http_、日志模块ngx_http_log_、单元中文件服务的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/**readdata*如果成功,appenddata*returndata*/data=read();if(data){data=data+"...";}returndata;ok/**readdata*如果失败,则返回null*追加数据*返回数据*/data=read();if(data==null){returnnull;}data=data+"...";returndata;这是一个小例子,只是为了说明,在清楚的前提下,越简洁越好。比如设计:因为天赋可能是天花板。到目前为止,我所知道的是保持简单和通用是好的设计。前段时间提出要实现一个功能,跟Unit的responsefilter有关,但是没有被允许。目前本组只有nginx作者Igor的设计最让人放心。其他人可以做到,包括我,但是设计简单而通用的东西还是有点技巧的。之前也经历过这个阶段:学习复杂的功能,感觉都是干货。建议多思考,多原创,多看优秀作品,早日突破一些认识的局限。解析逻辑解析的结果是生成中间代码,引入解析器对象方便解析。functioncalc_parse(text){varparser={text:text,pos:0,token:{},code:[]};next_token(parser);if(calc_parse_expr(parser,2)){returnnull;}if(parser.token.val!=TOK_END){returnull;}parser.code.push(OP_END);returnparser.code;}对于那些分散的信息,可以考虑使用对象等聚合的方式,一起处理。这也是高内聚的体现。前面说了,化繁为简是一种能力。简化1:(所有)标记可以从文本中找到。实施next_token()。varTOK_END=0,TOK_NUM=1;functionnext_token(parser){vars=parser.text;while(s[parser.pos]==''){parser.pos++;}if(parser.pos==s.length){parser.token.val=TOK_END;return;}varc=s[parser.pos];switch(c){case'1':case'2':case'3':case'4':case'5':case'6':case'7':case'8':case'9':case'0':parse_number(parser);break;default:parser.token.val=c;parser.pos++;break;}}functionparse_number(parser){vars=parser.text;varnum=0;while(parser.pos='0'&&c<='9'){num=num*10+(c-'0');parser.pos++;continue;}break;}parser.token.val=TOK_NUM;parser.token.num=num;}每次调用next_token(),都可以获得当前token,并且分析移动到下一个标记的开始位置。简化2:操作符*和+可以看成是同一个级别,但是这里限于篇幅,中间的实现过程就不贴出来了。简化三:分析逻辑,直到能够表达清楚,这也说明你对它的本质理解足够。以“1+2*3-4”为例:我们称整个字符串为一个表达式,里面的每一块也是一个表达式。表达式的表示是expression:expression[opexpression]。所以“1+2*3-4”是一个表达式,“2*3”也是一个表达式,“1”和“4”也是表达式。注意*的优先级高于+,因为可以这样分析:2*3是一个整体,操作数(2)运算符(*)操作数(3)1+2*3也是一个整体,操作数(1)运算符(+)操作数(2*3)等。代码如下:varOP_END=0,OP_NUM=1,OP_ADD=2,OP_SUB=3,OP_MUL=4,OP_DIV=5;functioncalc_parse_expr(parser,level){if(level==0){returncalc_parse_unary(parser);}if(calc_parse_expr(parser,level-1)){return-1;}for(;;){varop=parser.token.val;switch(level){case1:switch(op){case'*':varopcode=OP_MUL;break;case'/':varopcode=OP_DIV;break;default:return0;}break;case2:switch(op){case'+':varopcode=OP_ADD;break;case'-':varopcode=OP_SUB;break;default:return0;}break;}next_token(parser);if(calc_parse_expr(parser,level-1)){return-1;}parser.code.push(opcode);}return0;}functioncalc_parse_unary(parser){switch(解析器).token.val){caseTOK_NUM:parser.code.push(OP_NUM);parser.code.push(parser.token.num);break;default:return-1;}next_token(parser);return0;}注意:我们分析时生成中间代码。执行实现比较简单,只要思路清晰即可。functioncalc_exec(code){vari=0;varstack=[];for(;;){opcode=code[i++];switch(opcode){caseOP_END:returnstack[0];caseOP_NUM:varnum=code[i++];堆栈。push(num);break;caseOP_ADD:varop2=stack.pop();varop1=stack.pop();varr=op1+op2;stack.push(r);break;caseOP_SUB:varop2=stack.pop();varop1=stack.pop();varr=op1-op2;stack.push(r);break;caseOP_MUL:varop2=stack.pop();varop1=stack.pop();varr=op1*op2;stack.push(r);break;caseOP_DIV:varop2=stack.pop();varop1=stack.pop();varr=op1/op2;stack.push(r);break;}}}测试用例很重要functioncalc_run(text){varcode=calc_parse(text);if(code==null){returnnull;}returncalc_exec(code);}functionunit_test(tests){for(vari=0;i