某小伙的Antlr4学习笔记

概览

作为一款语言识别工具,

它可以解析(自定义)规则的语句,生成执行树

分有几个阶段

1.词法分析阶段 (lexical analysis)

根据我们定义的词法

解析出我们对应的关键词出来

2.解析阶段

根据我们定义的语法

对解析出来的词进行构建,生成一个语法树

应用场景

1.定制特定领域语言(DSL)

类似hibernate中的HQL,用DSL来定义要执行操作的高层语法,这种语法接近人可理解的语言,由DSL到计算机语言的翻译则通过ANTLR来做,可在ANTLR的结构语言中定义DSL命令具体要执行何种操作。

spark,hive中的解析语法也是用的antlr4

2.文本解析 可利用ANTLR解析JSON,HTML,XML,EDIFACT,或自定义的报文格式。解析出来的信息需要做什么处理也可以在结构文件中定义。

安装

antlr4在使用的时候需要自己定义一个xx.g4文件,然后通过antlr4程序对其进行代码自动生成(SimpleTemplate),当然也可以自定义

代码生成出几个文件

XXXBaseListener.java

XXXLexer

XXXListener(监听模式)

XXXParser

以及一些语法文件

根据需要还可以生成XXXBaseVisitor.java(访问者模式)

这里介绍两种途径进行生成代码

  1. 依赖idea的插件进行配置生成
  2. 通过antlr4的jar包(https://www.antlr.org/download/)antlr-4.0-complete.jar

使用idea的插件的方法的话,对应的生成的代码的版本受到idea本身插件兼容的版本的限制,想要自己生成指定版本的代码并运行比较麻烦

而antlr-4.0-complete.jar 可以根据自己需要的版本进行下载然后构建比较灵活

IDEA安装antlr

在这里插入图片描述

由于我已经安装好了,安装后重启,就会发现下标栏出现了对应的工具

在这里插入图片描述

新建maven项目,引入jar包

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>it.luke</groupId>
    <artifactId>Antlr_pro</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>

        <dependency>
            <groupId>org.antlr</groupId>
            <artifactId>antlr4-runtime</artifactId>
            <version>4.8</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.antlr</groupId>
                <artifactId>antlr4-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <id>antlr</id>
                        <goals>
                            <goal>antlr4</goal>
                        </goals>
                        <phase>none</phase>
                    </execution>
                </executions>
                <configuration>
                    <outputDirectory>src/test/java</outputDirectory>
                    <listener>true</listener>
                    <treatWarningsAsErrors>true</treatWarningsAsErrors>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

新建一个g4文件,并编写自己的规则,这里引用一个四则运算的规则

grammar Demo;

//parser
prog:stat
;
stat:expr|NEWLINE
;

expr:multExpr(('+'|'-')multExpr)*
;
multExpr:atom(('*'|'/')atom)*
;
atom:'('expr')'
    |INT
    |ID
;

//lexer
ID:('a'..'z'|'A'..'Z')+;
INT:'0'..'9'+;
NEWLINE:'\r'?'\n';
WS:(' '|'\t'|'\n'|'\r')+{skip();};

编写成功后可以通过安装antlr的插件进行规则预览

选择语法文件,对其中某个语法规则进行测试,可以看到有视图弹出

在这里插入图片描述

在这里插入图片描述

便可以测试你的规则的效果了

待你认为规则没问题后,你便可以进行配置,然后生成代码了,主要就算指定一些代码的生成路径,包名,解析成那种程序语言,默认java

在这里插入图片描述

在这里插入图片描述

antlr工具包生成

到上方的网址选择自己想要的antlr版本之后,根据自己的g4文件开始构建

在这里插入图片描述

java -jar antlr-4.8-complete.jar

可以看到他提供了很多配置参数可以用

在这里插入图片描述

这里我们默认执行

java -jar antlr-4.8-complete.jar Demo.g4

在这里插入图片描述

可以看到它在默认的当前目录下生成了文件,这个时候你可以选择在当前目录构建按一个java项目,也可以将这些文件拷贝到现有的java项目之下,然后选择的antlr4-runtime包需要和你的complete的包的版本一致,不然会导致一些编译问题

使用方法

g4文件的构造

规则识别文件在antlr4的github官网里面有很多别人的样例可供学习,你也可以定制一套属于自己的规则

g4构造分有几个部分

  1. grammar Name (包括了词法和语法的声明写法,有的为了重用性和解耦,可以将词法和语法进行分开,也就是 lexer grammar Name和parser grammar Name 进行声明)

  2. options (通过这个声明一些配置信息,可选)

  3. import (如果你不是合并式的写法,词法和语法是分开写的,就可以在语法声明文件中通过import,导入对应的词法文件)

  4. actionName (用来定义一些文件头…之类的)

  5. rule (主要的语法规则,可以是多个)

    这是核心,表示规则,以 “:” 开始, “;” 结束, 多规则以 “|” 分隔。

    ID : [a-zA-Z0-9|'_']+ ;    //数字 
    STR:'\'' ('\'\'' | ~('\''))* '\''; 
    WS: [ \t\n\r]+ -> skip ; // 系统级规则 ,即忽略换行与空格
    
    sqlStatement
        : ddlStatement 
        | dmlStatement     | transactionStatement
        | replicationStatement     | preparedStatement
        | administrationStatement     | utilityStatement
        ;
    

和前文说的一样,g4文件主要处理了两件事,

一个是解析出你要的词法:

​ 比如我定义了规则里面的INT就只能是数字

INT:'0'..'9'+;

​ 比如我定义了ID只能是英文

ID:('a'..'z'|'A'..'Z')+;

一个是根据你的词法指定的规则

​ 比如我要指定元素之间的加减乘除

prog:stat
;
stat:expr|NEWLINE
;

expr:multExpr(('+'|'-')multExpr)*
;
multExpr:atom(('*'|'/')atom)*
;
atom:'('expr')'
    |INT
    |ID
;

同一个语法里面可以通过|进行多个结果的匹配,相当于或

语法树的遍历使用

语法树的遍历使用分有两种,一种是监听者模式,一种是访问者模式

使用监听者模式,主要借助了ParseTreeWalker 这样一个类,相当于是一个hook

每经过一个树的节点,便会触发对应节点的方法.好处就算是比较方便,但是灵活性不够,不能够自主性的调用任意节点进行使用

使用访问者模式,将每个数据的节点类型高度抽象出来够,根据你传入的上下文类型来判断你想要访问的是哪个节点,触发对应的方法

这边通过对刚刚的四则运算语法进行简单的测试

grammar Demo;

//parser
prog:stat
;
stat:expr|NEWLINE
;

expr:multExpr(('+'|'-')multExpr)*
;
multExpr:atom(('*'|'/')atom)*
;
atom:'('expr')'
    |INT
    |ID
;

//lexer
ID:('a'..'z'|'A'..'Z')+;
INT:'0'..'9'+;
NEWLINE:'\r'?'\n';
WS:(' '|'\t'|'\n'|'\r')+{skip();};

测试的规则语法树如图:(a+b)+2*3

在这里插入图片描述

监听者模式

在这里插入图片描述

在生成的代码结构里面,新建一个Main类进行调用测试

public static void runListener() throws Exception{

        //对每一个输入的字符串,构造一个 ANTLRStringStream 流 in
        ANTLRInputStream in = new ANTLRInputStream("(a+b)+2*3\n");

        //用 in 构造词法分析器 lexer,词法分析的作用是产生记号
        DemoLexer lexer = new DemoLexer(in);

        //用词法分析器 lexer 构造一个记号流 tokens
        CommonTokenStream tokens = new CommonTokenStream(lexer);

        //再使用 tokens 构造语法分析器 parser,至此已经完成词法分析和语法分析的准备工作
        DemoParser parser = new DemoParser(tokens);
        DemoParser.StatContext stat = parser.stat();

        //调用lister
        DemoBaseListener demoBaseListener = new DemoBaseListener();
		
        ParseTreeWalker parseTreeWalker = new ParseTreeWalker();
        parseTreeWalker.walk(demoBaseListener,stat );

        System.out.println("end");
    }

官网说的监听器的遍历规则是从左往右的深度优先遍历,眼见为虚,实践为实,这边重写一下生成的DemoBaseListener,简单的打印当前节点的信息,观察一下是否符合

// Generated from D:/GitPro/My_pro/Antlr_pro/src/test/antlr\Demo.g4 by ANTLR 4.8
package Demo;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.tree.ErrorNode;
import org.antlr.v4.runtime.tree.TerminalNode;


public class DemoBaseListener implements DemoListener {

	@Override public void enterProg(DemoParser.ProgContext ctx) {
		System.out.println("enterProg:"+ctx.getText());
	}

	@Override public void exitProg(DemoParser.ProgContext ctx) {
//		System.out.println("exitProg: " +ctx.getText());
	}

	@Override public void enterStat(DemoParser.StatContext ctx) {
		System.out.println("enterStat:"+ctx.getText());
	}

	@Override public void exitStat(DemoParser.StatContext ctx) {
//		System.out.println("exitStat:"+ctx.getText());
	}

	@Override public void enterExpr(DemoParser.ExprContext ctx) {
		System.out.println("enterExpr:"+ctx.getText());
	}

	@Override public void exitExpr(DemoParser.ExprContext ctx) {
//		System.out.println("exitExpr: "+ctx.getText());
	}

	@Override public void enterMultExpr(DemoParser.MultExprContext ctx) {
		System.out.println("enterMultExpr:"+ctx.getText());
	}

	@Override public void exitMultExpr(DemoParser.MultExprContext ctx) {
//		System.out.println("exitMultExpr:"+ctx.getText());
	}

	@Override public void enterAtom(DemoParser.AtomContext ctx) {
		System.out.println("enterAtom:"+ctx.getText());
	}

	@Override public void exitAtom(DemoParser.AtomContext ctx) {
//		System.out.println("exitAtom:"+ctx.getText());
	}


	@Override public void enterEveryRule(ParserRuleContext ctx) {
//		System.out.println("enterEveryRule:"+ctx.getText());
	}

	@Override public void exitEveryRule(ParserRuleContext ctx) {
//		System.out.println("exitEveryRule:"+ctx.getText());
	}

	@Override public void visitTerminal(TerminalNode node) {
		System.out.println("visitTerminal:"+node.getText());
	}

	@Override public void visitErrorNode(ErrorNode node) {
		System.out.println("visitErrorNode:"+node.getText());
	}
}

执行调用我们的main方法

在这里插入图片描述

对比我们解析的语法树,验证了这个遍历顺序

访问者模式

新写一个访问者的方法

public static void runVistor(){

        //新建输入流
        ANTLRInputStream in = new ANTLRInputStream("(a+b)+2*3\n");

        //新建词法解析器
        DemoLexer vistorLexer = new DemoLexer(in);
        //对解析token进行缓存
        CommonTokenStream vistorToken = new CommonTokenStream(vistorLexer);
        //新建解析器进行解析
        DemoParser vistorParser = new DemoParser(vistorToken);

        //通过访问者的模式进行访问
        DemoBaseVisitor<String> DemoBaseVisitor = new DemoBaseVisitor<String>();

        //传入需要解析的节点
        DemoBaseVisitor.visit(vistorParser.prog());
        DemoBaseVisitor.visit(vistorParser.stat());
        DemoBaseVisitor.visit(vistorParser.multExpr());


    }
    public static void main(String[] args) throws Exception{
//        runListener();
        runVistor();
    }

本次我们可以通过传入我们需要访问的节点:prog,stat,multExpr

并重写访问者的对应节点的代码

@Override public T visitProg(DemoParser.ProgContext ctx) {
		System.out.println("visitProg:"+ctx.getText());
		return visitChildren(ctx);
	}
...
@Override public T visitStat(DemoParser.StatContext ctx) {
		System.out.println("visitStat:"+ctx.getText());
		return visitChildren(ctx); }
...

@Override public T visitMultExpr(DemoParser.MultExprContext ctx) {
		System.out.println("visitMultExpr:"+ctx.getText());
		return visitChildren(ctx); }

查看结果确实可以根据你的需要访问到对应的节点,如果多个节点的名字相同,则会按照从左到右,深度优先的顺序依次触发

在这里插入图片描述

样例

在这个基础上,我们尝试着解析一下sql的语法,为了简单测试,只测试查询语法,这边在git上拉了一份g4文件用来测试

 grammar MysqlQuery;
 
// @header{package com.antlr.mysql.query;}
    
AS                              : A S;
SELECT                       : S E L E C T;
FROM                        : F R O M;
TABLE                        : T A B L E;
MAX                         : M A X;
SUM                         : S U M;
AVG                          : A V G;
MIN                          : M I N;
COUNT                     : C O U N T;
ALL                            : A L L;
DISTINCT                  : D I S T I N C T;
WHERE                     : W H E R E;
GROUP                    : G R O U P;
BY                             : B Y ;
ORDER                     : O R D E R; 
HAVING                   : H A V I N G;
NOT                          : N O T;
IS                               :  I S ;
TRUE                         : T R U E;
FALSE                        : F A L S E;
UNKNOWN               : U N K N O W N;
 BETWEEN                  : B E T W E E N;
 AND                           :  A N D;
 IN                                :   I N;
 NULL                           : N U L L;
 OR                             : O R ;
 ASC                          : A S C;
 DESC                       : D E S C;
 LIMIT                      : L I M I T ;
 OFFSET                    : O F F S E T;    
     
fragment A      : [aA];
fragment B      : [bB];
fragment C      : [cC];
fragment D      : [dD];
fragment E      : [eE];
fragment F      : [fF];
fragment G      : [gG];
fragment H      : [hH];
fragment I      : [iI];
fragment J      : [jJ];
fragment K      : [kK];
fragment L      : [lL];
fragment M      : [mM];
fragment N      : [nN];
fragment O      : [oO];
fragment P      : [pP];
fragment Q      : [qQ];
fragment R      : [rR];
fragment S      : [sS];
fragment T      : [tT];
fragment U      : [uU];
fragment V      : [vV];
fragment W      : [wW];
fragment X      : [xX];
fragment Y      : [yY];
fragment Z      : [zZ];
fragment HEX_DIGIT:                  [0-9A-F];
fragment DEC_DIGIT:                  [0-9];
fragment LETTER:                         [a-zA-Z];

 
ID:    ( 'A'..'Z' | 'a'..'z' | '_' | '$') ( 'A'..'Z' | 'a'..'z' | '_' | '$' | '0'..'9' )*;
TEXT_STRING :    (  '\'' ( ('\\' '\\') | ('\'' '\'') | ('\\' '\'') | ~('\'') )* '\''  );
ID_LITERAL:   '*'|('@'|'_'|LETTER)(LETTER|DEC_DIGIT|'_')*; 
REVERSE_QUOTE_ID :   '`' ~'`'+ '`';
DECIMAL_LITERAL:     DEC_DIGIT+; 

    
tableName            : tmpName=ID;
column_name            :ID;
function_name            : tmpName=ID ;
 
 selectStatement:
       SELECT        
        selectElements            
    ( 
        FROM tableSources         
        ( whereClause )? 
        ( groupByCaluse )?
        ( havingCaluse )?
    ) ?    
    ( orderByClause )?
    ( limitClause )?
;  

 
 selectElements
    : (star='*' | selectElement ) (',' selectElement)*
    ;  
    
  
tableSources
    : tableName (',' tableName)*
    ;
      
whereClause 
    : WHERE    logicExpression
    ;
    
 logicExpression
     : logicExpression logicalOperator logicExpression
     | fullColumnName comparisonOperator value
     | fullColumnName BETWEEN value AND value
     | fullColumnName NOT? IN '(' value (',' value)*  ')' 
     | '(' logicExpression ')'
     ;
      

groupByCaluse
    :   GROUP BY   groupByItem (',' groupByItem)*   
    ;
havingCaluse
    :    HAVING  logicExpression
   ;
   
 orderByClause
    : ORDER BY orderByExpression (',' orderByExpression)*
    ;       
    
 limitClause
    : LIMIT
    (
      (offset=decimalLiteral ',')? limit=decimalLiteral
      | limit=decimalLiteral OFFSET offset=decimalLiteral
    )
    ;
    
orderByExpression
    : fullColumnName order=(ASC | DESC)?
    ; 

    
groupByItem
    : fullColumnName order=(ASC | DESC)?
    ;
    
logicalOperator
    : AND | '&' '&'  | OR | '|' '|'
    ;    
    
comparisonOperator
    : '=' | '>' | '<' | '<' '=' | '>' '='
    | '<' '>' | '!' '=' | '<' '=' '>'
    ;  
    
    
value 
    : uid
    | textLiteral
    | decimalLiteral 
    ;
 
decimalLiteral
    : DECIMAL_LITERAL
    ;  
textLiteral
    : TEXT_STRING
    ;      
 
selectElement
    : fullColumnName (AS? uid)?      #selectColumnElement
    | functionCall (AS? uid)?               #selectFunctionElement   
    ; 

fullColumnName
    : column_name  
    ;
    
functionCall
   :  aggregateWindowedFunction     #aggregateFunctionCall  
    ;   

aggregateWindowedFunction
    : (AVG | MAX | MIN | SUM) '(' functionArg ')'
    | COUNT '(' (starArg='*' |  functionArg?) ')'
    | COUNT '(' aggregator=DISTINCT functionArgs ')'  
    ;

functionArg
    :  column_name 
    ;

functionArgs 
    : column_name (',' column_name)*
    ;

uid
    : ID  
    ;


 WS : [ \t\r\n]+ -> skip ; // skip spaces, tabs, newlines

可以看到,当是简单的查询就涉及到这么多的词法和语法了,所以这个语法规则的编写能拿现成就拿现成的吧

同样的先对规则进行测试,查看能否正常生成语法树

在这里插入图片描述

然后在通过插件生成代码,进行测试

在这里插入图片描述

public class mysqlMain {
    public static void listenerRun(){

        //新建流
        ANTLRInputStream input = new ANTLRInputStream("SELECT column1,column2,column3 from tableC where column1 = 1");

        //新建词法解析
        MysqlQueryLexer mysqlQueryLexer = new MysqlQueryLexer(input);

        //缓存词法解析token
        CommonTokenStream token = new CommonTokenStream(mysqlQueryLexer);

        //解析语法
        MysqlQueryParser parser = new MysqlQueryParser(token);

        //通过监听器的方式
        ParseTreeWalker parserWalk = new ParseTreeWalker();
        //重写部分监听操作,输出列名和表名
        parserWalk.walk(new MysqlQueryBaseListener(),parser.selectStatement() );

    }

同样的,重写一部分的监听代码,方便我们查看

执行

在这里插入图片描述

成功识别到每个节点的数据

同样的还有大数据架构里面的hive和spark,同样使用了antlr

如果你搭建了spark的源码环境,就可以看到它的g4文件

在这里插入图片描述

或者去它的git仓库看一下

https://github.com/apache/spark/tree/master/sql/catalyst/src/main/antlr4/org/apache/spark/sql/catalyst/parser

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值