abnf java实现_详细讲解如何利用Java实现组合式解析器?

30649b70ca9d56e6c88b5f5b084927c7.png

简介:Ward Cunningham 曾经说过,干净的代码清晰地表达了代码编写者所 想要表达的东西,而优美的代码则更进一步,优美的代码看起来就像是专门为了 要解决的问题而存在的。在本文中,我们将展示一个组合式解析器的设计、实现 过程,最终的代码是优美的,极具扩展性,就像是为了解析特定的语法而存在的 。我们还会选取 H.248 协议中的一个例子,用上述的组合式解析器实现其语法 解析器。读者在这个过程中不仅能体会到代码的美感,还可以学习到函数式编程 以及构建 DSL 的一些知识。

DSL 设计基础

我们在用编程语言(比如:Java 语言)实现某项功能 时,其实就是在指挥计算机去完成这项功能。但是,语言能够带给我们的并不仅 仅局限于此。更重要的是,语言提供了一种用于组织我们的思维,表达计算过程 的框架。这个框架的核心就在于,如何能够把简单的概念组合成更为复杂的概念 ,同时又保持其对于组合方法的封闭,也就是说,组合起来的复杂概念对于组合 手段来说和简单的概念别无二致。引用“Structure and Interpretation of Computer Programs”一书中的话来讲,任何一个强大的语言都是通过 如下三种机制来达成这个目标的:

原子:语言中最简单、最基本的实体;

组合手段:把原子组合起来构 成更复杂实体的方法;

抽象手段:命名复杂实体的方法,命名后的复杂 实体可以和原子一样通过组合手段组合成为更复杂的实体。

像 Java 这 种通用的编程语言,由于其所关注的是解决问题的一般方法。因此,其所提供的 这三种机制也都是通用层面的。在解决特定问题时,这种通用的手段和特定问题 领域中的概念、规则之间是存在着语义鸿沟的,所以某些问题领域中非常清晰、 明确的概念和规则,在实现时就可能会变得不那么清晰。作为程序员来说,用干 净的代码实现出功能仅仅是初级的要求,更重要的是要提升通用语言的层次,构 建出针对特定问题领域的语言(DSL),这个过程中很关键的一点就是寻找并定 义出面向问题领域的 原子概念、组合的方法以及抽象的手段。这个 DSL 不一定 非得像通用语言那样是完备的,其目标在于清晰、直观地表达出问题领域中的概 念和规则,其结果就是把通用的编程语言变成解决特定问题的专用语言。

我们曾经在“基于 Java 的界面布局 DSL 的设计与实现”一 文中,构建了一种用于界面布局的 DSL,其中呈现了上述思想。在本文中,我们 将以解析器的构造为例,来讲述如何构建一种用于字符串解析的 DSL,这种 DSL 具有强大的扩展能力,读者可以根据自己的需要定义出自己的组合手段。此外, 从本文中读者还可以领略到 函数编程的优雅之处。

解析器原子

什么是解析器?最简单的解析器是什么?大家通常都会认为解析器就是判断输入 的字符串是否满足给定的语法规则,在需要时还可以提取出相应的语法单位实例 。从概念上来讲,这种理解没什么问题。不过要想定义出用于解析的 DSL,那么 就需要更为精确的定义,也就是说我们要定义出解析器这个概念的确切类型。在 Java 中,我们用 interface 来定义解析器类型,如下:

interface Parser

{

public Result  parse(String target);

}

其中,target 为要解析的字符 串,Result 是解析的结果,只要满足这个接口语义的对象,我们就称其为一个 解析器实例。Result 的定义如下:

class Result

{

private String recognized;

private String  remaining;

private boolean succeeded;

private Result(String recognized, String remaining,

boolean succeeded) {

this.recognized = recognized;

this.remaining = remaining;

this.succeeded =  succeeded;

}

public boolean is_succeeded()  {

return succeeded;

}

public  String get_recognized() {

return recognized;

}

public String get_remaining() {

return remaining;

}

public static Result  succeed(String recognized,

String remaining) {

return new Result(recognized, remaining, true);

}

public static Result fail() {

return  new Result("", "", false);

}

}

其中, recognized 字段表示这个解析器所认识的部分,remaining 表示经过这个解析 器解析后所剩余的部分,succeeded 表示解析是否成功,Result 是一个值对象 。有了解析器的精确定义,接下来我们就可以定义出最简单的解析器。显然,最 简单的解析器就是什么也不解析的解析器,把目标字符串原样返回,我们称其为 Zero,定义如下:

class Zero implements Parser

{

public Result parse(String target) {

return Result.succeed("", target);

}

}

Zero 解析器一定会解析成功,不做任何语法单位识别并直接返 回目标字符串。下面我们来定义另外一个很简单的解析器 Item,只要目标字符 串不为空,Item 就会把目标字符串的第一个字符作为其识别结果,并返回成功 ,如果目标字符串为空,就返回失败,Item 的定义如下:

class  Item implements Parser

{

public Result parse (String target) {

if(target.length() > 0) {

return Result.succeed(target.substring(0,1),

target.substring(1));

}

return  Result.fail();

}

}

Zero 和 Item 是我们解析 器 DSL 中仅有的两个原子,在下一小节中,我们来定义解析器的组合方法。

解析器组合子

我们在上一小节中定义了 Item 解析器,它无条件 的解析出目标字符串中的第一个字符,如果我们希望能够变成有条件的解析,就 可以定义出一个 SAT 组合子,其接收一个条件谓词(predicate)和一个解析器 ,并生成一个复合解析器,该复合解析器能否解析成功取决于原始解析器的解析 结果是否满足给定的条件谓词,条件谓词和 SAT 的定义如下:

interface Predicate

{

public boolean  satisfy(String value);

}

class SAT implements  Parser

{

private Predicate pre;

private  Parser  parser;

public SAT(Predicate predicate,  Parser parser) {

this.pre = predicate;

this.parser = parser;

}

public Result  parse(String target) {

Result r = parser.parse (target);

if(r.is_succeeded() && pre.satisfy (r.get_recognized())) {

return r;

}

return Result.fail();

}

}

如果, 我们想定义一个解析单个数字的解析器,那么就可以定义一个 IsDigit 条件谓 词,并通过 SAT 把该 IsDigit 和 Item 组合起来,代码如下:

class IsDigit implements Predicate

{

public boolean satisfy(String value) {

char c =  value.charAt(0);

return c>='0' &&  c<='9';

}

}

解析单位数字的解析器 digit 定义如下:

Parser digit = new SAT(new IsDigit(), new  Item());

我们可以采用同样的方法组合出单个字母、单个大写 字母、单个小写字母等解析器来。

接下来,我们定义一个 OR 组合子, 其接收两个解析器,并分别用这两个解析器去解析一个目标串,只要有一个解析 成功,就认为解析成功,如果两个都失败,则认为失败,代码定义如下:

class OR implements Parser

{

private  Parser p1;

private Parser p2;

public OR (Parser p1, Parser p2) {

this.p1 = p1;

this.p2 = p2;

}

public Result parse (String target) {

Result r = p1.parse(target);

return r.is_succeeded() ? r : p2.parse(target);

}

}

我们可以定义出一个新的解析器 digit_or_alpha,如 果目标字符是数字或者字母那么该解析器就解析成功,否则就失败。代码如下:

判断是否是字母的条件谓词:

class IsAlpha  implements Predicate

{

public boolean satisfy(String  value) {

char c = value.charAt(0);

return (c>='a' && c<='z') || (c>='A' &&  c<='Z');

}

}

用于解析单个字母的解析器 :

Parser alpha = new SAT(new IsAlpha(), new Item ());

digit_or_alpha 解析器定义:

Parser  digit_or_alpha = new OR(digit, alpha);

下面我们来定义 一个 顺序组合子 SEQ,该组合子接收两个解析器,先把第一个解析器应用到目 标字符串,如果成功,就把第二个解析器应用到第一个解析器识别后剩余的字符 串上,如果这两个解析器都解析成功,那么由 SEQ 组合起来的这个复合解析器 就解析成功,只要有一个失败,复合解析器就解析失败。当解析成功时,其识别 结果由这两个解析器的识别结果连接而成。

为了能够连接两个 Result 中已经识别出来的解析结果,我们在 Result 类中新增一个静态方法:concat, 其定义如下:

public static Result concat(Result r1,  Result r2) {

return new Result(

r1.get_recognized().concat(r2.get_recognized()),

r2.get_remaining(), true);

}

顺序组合子 SEQ 的定义 如下:

class SEQ implements Parser

{

private Parser p1;

private Parser p2;

public SEQ(Parser p1, Parser p2) {

this.p1 =  p1;

this.p2 = p2;

}

public Result  parse(String target) {

Result r1 = p1.parse (target);

if(r1.is_succeeded()) {

Result r2 = p2.parse(r1.get_remaining());

if (r2.is_succeeded()) {

return Result.concat (r1,r2);

}

}

return Result.fail ();

}

}

现在,如果我们想定义一个解析器用以 识别第一个是字母,接下来是一个数字的情况,就可以这样定义:

Parser alpha_before_digit = new SEQ(alpha,  digit);

接下来我们定义本文中的最后一个组合子:OneOrMany。 该组合子接收一个解析器和一个正整数值,其生成的复合解析器会用原始解析器 连续地对目标串进行解析,每一次解析时的输入为上一次解析后剩余的字符串, 解析的最大次数由输入的正整数值决定。如果第一次解析就失败,那么该复合解 析器就解析失败,否则的话,会一直解析到最大次数或者遇到解析失败为止,并 把所有成功的解析的识别结果连接起来作为复合解析器的识别结果,OneOrMany 组合子的定义如下:

class OneOrMany implements Parser

{

private int max;

private Parser  parser;

public OneOrMany(int max, Parser parser)  {

this.max = max;

this.parser =  parser;

}

public Result parse(String target)  {

Result r = parser.parse(target);

return r.is_succeeded() ? parse2(r,1) : Result.fail();

}

private Result parse2(Result pre, int count) {

if(count >= max) return pre;

Result r  = parser.parse(pre.get_remaining());

return  r.is_succeeded() ?

parse2(Result.concat (pre,r),count+1) : pre;

}

}

使用该组合 子,我们可以容易地定义出用于识别由最少一个,最多 10 个字母组成的串的解 析器,如下:

Parser one_to_ten_alpha = new OneOrMany (10,alpha);

本文的组合子就定义到此,不过读者可以根据自己 的需要,用同样的方法容易地定义出符合自己要求其他组合子来。

抽象 的手段

如果在 DSL 的构造中,仅仅提供了一些原子和组合手段,并且组 合的结果无法再次参与组合,那么这个 DSL 的扩展能力和适用性就会大大折扣 。相反,如果我们还能提供出抽象的手段对组合结果进行命名,命名后的复合实 体可以像原子一样参与组合,那么 DSL 的扩展能力就会非常的强大,适用性也 会大大增加。因此,抽象的手段在 DSL 的构造过程中是至关重要的。

敏 锐的读者可能已经发现,对于我们的解析 DSL 来说,其实在前面的小节中已经 使用了抽象的手段。比如,我们在 alpha,digit,digit_or_alpha 以及 alpha_before_digit 等复合解析器的定义中已经使用了抽象的手段来对其进行 命名,然后可以直接使用这个抽象的名字再次参与组合。由于我们的解析器是基 于 Java 语言中的 interface 机制定义的,因此,Java 语言中已有的针对 interface 的抽象支持机制完全适用于我们的解析 DSL。因此,我们就无需定义 自己的特定抽象手段,直接使用 Java 语言中的即可。

相信读者已经从 上一小节中的例子中看到组合、抽象手段的强大威力。在下一小节中,我们将给 出一个更为具体的例子:H.248 协议中 NAME 语法解析器的构造。

一个 H.248 实例

在本小节中,我们将基于前面定义的解析器原子和组合子, 实现用于识别 H.248 协议中 NAME 语法的解析器的构造。

H.248 是一个 通信协议,媒体网关控制器使用该协议来对媒体网关进行控制。H.248 协议是一 个基于 ABNF(扩展 BNF)文法描述的基于文本的协议,协议中定义了 H.248 消 息的组成部分和具体内容。我们仅仅关注其中的 NAME 语法定义,如下:

NAME = ALPHA *63(ALPHA / DIGIT / "_" )

ALPHA =  %x41-5A / %x61-7A  ; A-Z, a-z

DIGIT = %x30-39        ; digits 0 through 9

我们首先来解释一下其中的一些 规则,*63 其实是 n*m 修饰规则的一个实例,表示最少 n 个最多 m 个,当 n 等于 0 时,可以简略写成 *m。因此,*63 表示最少 0 个,最多 63 个。/ 表 示或规则,表示两边的实体可选。()表示其中的实体必须得有一个。- 表示范 围。因此,DIGIT 表示单个数字,ALPHA 表示单个字母(大写或者小写), (ALPHA/ DIGIT/ “_” )表示要么是个字母,要么是个数字,要么是 个下划线。*63(ALPHA/ DIGIT/ “_” )表示,最少 0 个,最多 63 个字母或者数字或者下划线。两个实体顺序写在一起,表示一种顺序关系, ALPHA *63(ALPHA/ DIGIT/ “_” ) 表示,以字母开始,后面最少 0 个,最多 63 个 字母或者数字或者下划线。

根据前面的内容可以很容易 地直接表达出用于解析这个语法规则的解析器来。如下:

class  H248Parsec

{

public static Parser alpha() {

return new SAT(new IsAlpha(), new Item());

}

public static Parser digit() {

return  new SAT(new IsDigit(), new Item());

}

public static Parser underline() {

return new SAT (new IsUnderline(), new Item());

}

public  static Parser digit_or_alpha_or_underline() {

return  new OR(alpha(), new OR(digit(), underline()));

}

public static Parser zero_or_many(int max, Parser parser) {

return new OR(new OneOrMany(max,parser), new Zero ());

}

public static Parser name() {

return new SEQ(alpha(),

zero_or_many(64,

digit_or_alpha_or_underline()));

}

}

可以看出,我们的代码和协议中的语法描述基本上完全一样,我 们通过定义自己的面向解析的 DSL,把 Java 这种通用语言变成了用于 ABNF 语 法解析的专门语言,符合 Ward Cunningham 关于美的代码的定义。最后,我们 用该解析器来做一些关于 NAME 语法识别的实验,如下表所示:

14424bd8292c30e689fdaaf08fbb418d.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/245360.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

充电原理_电动汽车充电桩如何设置?充电桩原理介绍

随着新能源产业的蓬勃发展&#xff0c;电动汽车在生活中变得越来越普遍。比亚迪(BYD)&#xff0c;宝马(BMW)和特斯拉(Tesla)等汽车制造商都已经推出了全电动汽车&#xff0c;而混合动力汽车则更为普遍。为了能够方便地为这些电动汽车的电池充电&#xff0c;必须建立充电桩。充电…

GUI动画演示java_java – 使用JFrame的简单GUI动画

我覆盖了JFrame中的setVisible()函数.它所做的只是不断增长框架或缩小框架,直到它达到我想要的尺寸.这使得一个简单的动画.我的问题是,它是不稳定的,不平滑的,如果移动框架不能很好地工作.有没有办法可以做到更干净,更流畅,更光滑&#xff1f;/*** Sets this window visible an…

python 抽奖器_如何基于python实现年会抽奖工具

用python来实现一个抽奖程序&#xff0c;供大家参考&#xff0c;具体内容如下主要功能有1.从一个csv文件中读入所有员工工号2.将这些工号初始到一个列表中3.用random模块下的choice函数来随机选择列表中的一个工号4.抽到的奖项的工号要从列表中进行删除&#xff0c;以免再次抽到…

java 获取服务器硬件_dell服务器远程获取硬件状态

以dell的R620型号的服务器做的测试登陆上dell服务器ilo的IP地址&#xff0c;首先打开ipmi&#xff0c;ilo2是直接支持ipmi2.0的此框需要点击 “IDRAC设置”->“网络”->“IPMI设置”在”启用LAN上IPMI“后的复选框打钩&#xff0c;才能启动ipmi好像是内置到了ilo2&#x…

简单可行性报告模板_项目可行性报告模板分享!第三章主要内容

项目可行性报告模板分享!第三章主要内容如下&#xff1a;第三章 市场分析与建设规模市场分析在可行性研究中的重要地位在于&#xff0c;任何一个项目&#xff0c;其生产规模的确定、技术的选择、投资估算甚至厂址的选择&#xff0c;都必须在市场需求情况有了充分的了解后才能解…

java外挂源码_2.7 万 Star!Github 项目源码辅助阅读神器

【导语】&#xff1a;一款用于将 Github 项目代码以树形格式展示的浏览器插件。简介大家平时逛 GitHub 是否会觉得查看源代码的体验十分糟糕&#xff1f;项目文件需要一层层点击&#xff0c;返回也要一层层返回。这样不直观&#xff0c;也比较麻烦。Octotree 是一款辅助阅读 Gi…

php把时间戳转换成字符串,php中将时间差转换为字符串提示的实现代码

如微博这看起来更加人性化&#xff0c;好吧&#xff0c;上代码class timeAgo{static $timeagoObject;private $rustle;private $unit;private function __construct(){}private function __clone(){ }public static function getObject(){if(! (self::$timeagoObject instanceo…

php教育网站设计案例_酒店装修,精品酒店设计装修案例,酒店设计网站

酒店设计需要考虑&#xff1a;设计酒店的时候也要顺应市场潮流&#xff0c;不再单一的提供休息、洗漱、睡觉的空间&#xff0c;还要能提供社交、商务等功能&#xff0c;同顾客产生情况共鸣。这样能够引领生活方式的、能够互动&#xff0c;有仪式感的酒店&#xff0c;是很吸引人…

php数据库数组去重复数据库,PHP查询并删除数据库多列重复数据的方法(利用数组函数实现)...

本文实例讲述了php查询并删除数据库多列重复数据的方法。分享给大家供大家参考&#xff0c;具体如下&#xff1a;$con mysql_connect("localhost","root","");if (!$con){die(could not connect: . mysql_error());}$db_selected mysql_selec…

python创建数据库表空间_7.自动化监控多个Oracle表空间

自动化监控多个Oracle表空间上节讲了如何利用Python自动监控Oracle表空间使用率这节会利用一个循环一次性查询多个表空间使用率环境设置Linux系统为 Centos 6.8Python环境为 Python 3.6orale信息文件其次建立一个新的文本文件存放多个数据库的信息注意最后一行不能是空白行文件…

python采用编译型方式执行_Python程序的执行过程 解释型语言和编译型语言

我初学Python时&#xff0c;听到的关于Python的第一句话就是&#xff0c;Python是一门解释性语言&#xff0c;我就这样一直相信下去&#xff0c;直到发现了*.pyc文件的存在。如果是解释型语言&#xff0c;那么生成的*.pyc文件是什么呢&#xff1f;c应该是compiled的缩写才对啊&…

写一个方法判断一个字符串是否对称_判断一个男生是否好色的方法

▾我们店只招黑喵哦?▾其实也不是不能理解为什么男的要跑?▾有什么相见恨晚的小知识&#xff1f;?▾见证奇迹?&#xff1a;是不是穿过去了&#xff01;&#xff1f;▾医学奇迹?▾卧槽流劈&#xff01;?真的超光速了&#xff01;▾细节很到位啊几位少年?▾昨有坟头蹦迪?…

php webserver documentroot,php – 在包含中使用$_SERVER [‘DOCUMENT_ROOT’]是个好主意?...

这是一个好主意吗&#xff1f;require_once($_SERVER[DOCUMENT_ROOT]./include.php);如果在同一台服务器上有两个虚拟主机,一个用于实时,另一个用于开发,具有不同的Apache DocumentRoot,则可避免在包含源未知时包含绝对路径,并且可能位于任何目录中.(注意&#xff1a;下一节中的…

java changestr,java change

VetoableChangeListener (Java Platform SE...2.import java.awt.*; import java.awt.event.*; import javax.swing.*; public class ChangeGUI extends JFrame{ private JButton button; private JLabel ......Java 应用技术实训报告 题 目 万年历记事本 起讫日期 2008 年 6 月…

python读取日志统计ip_如何通过命令行统计和排列访问日志里的ip数

以下的方式速度都会卡在sort上面&#xff0c;数量越多越麻烦。Shell方法:grep -i -o -E -r -e "([0-9]{1,3}\.){3}[0-9]{1,3}" access.log | sort -n | uniq -c | sort -nawk方法:如果是accesslog 格式应该是固定的 会简单些。因为awk做排序也比较的郁闷&#xff0c;…

php斯芬克斯,斯芬克斯之迷——ie私有属性haslayout的困扰

就象神话中的斯芬克斯一样&#xff0c;ie的私有属性haslayout是个神秘且让人困惑的难缠东西&#xff0c;她只游荡于ie(这片沙漠)之下。她无法使用css声明直接创建。即便是对于ie&#xff0c;她也不能说是一个实实在在存在的属性。ie下的元素有些本身拥有haslayout(基本上是一些…

matlab计算联合熵,如何用matlab软件计算一幅图像信息的熵以及两幅图像间的联合熵?...

%计算一副图像的熵%随机生成图像Afloor(rand(8,8).*255);[M,N]size(A);tempzeros(1,256);%对图像的灰度值在[0,255]上做统计for m1:M;for n1:N;if A(m,n)0;i1;elseiA(m,n);endtemp(i)temp(i)1;endendtemptemp./(M*N);%由熵的定义做计算result0;for i1:length(temp)if temp(i)0;…

python多线程写日志_python 应用logging多线程写多个log文件

python 使用logging多线程写多个log文件import threadingimport timeimport loggingimport logging.handlersLEVELS{notset:logging.DEBUG,debug:logging.DEBUG,info:logging.INFO,warning:logging.WARNING,error:logging.ERROR,critical:logging.CRITICAL}LOG_FILENAME test.…

php在数据库中上传图片格式,如何上传图片并在php中保存数据库中的URL

在这个PHP代码中我想自定义图像上传目的地 . 有了这个php文件&#xff0c;我有一个名为uploads的目录 . 我想将我上传的所有图像添加到此目录并在db中存储路径 . 我怎样才能做到这一点&#xff1f;// Assigning value about your server to variables for database connection$…

matlab转python难吗_MATLAB在逐渐被Python淘汰吗?

题主的问题我很适合回答&#xff0c;同机械工程大专业&#xff0c;目前供职某车企研发中心&#xff0c;工作内容里也有产品的控制系统研发部分。Matlab与Python各有其优势项&#xff0c;很难笼统的说孰优孰劣。但是在题主所涉及的机械控制工程、航空航天、嵌入式等领域&#xf…