一、 写在前面

  我最早是在2005年,首次在实际开发中实现语法解析器,当时调研了Yacc&Lex,觉得风格不是太好,关键当时yacc对多线程也支持的不太好,接着就又学习了Bison&Flex,那时Bison的版本还是v1.x.y,对C++的支持比较差,最终选择了Biso++ & Flex++,两者支持C++版本并且跨平台支持Linux和windows。业务需求是实现全文检索Contains表达式的解析,包括调研、学习、实现和测试,大致用了2月,很多时间花费在解决语法冲突、内存管理等方面。

  后来换了工作单位,在2012年再次需要实现Contains表达式语法,这时的Bison和Flex都稳定支持C++版本了,所以就直接采用Bison&Flex,大约用了不到2周,实现了Contains表达式解析和单元测试。

  最近一次是在2018年,需要用Java版本的Contains表达式解析,这次采用的是Antlr4,开发和测试仅用了一个周六日在家的业余时间。感觉Antlr4明显比Bison&Flex简单,对语法规则的支持很直观易懂,用在语法冲突比较少的业务环境中非常合适,更关键的是:相对于Bison,Antlr4产生的Parser可以顺手生成对应规则的嵌套类,如果象我一样喜欢Visitor风格的话,完全可以做到语法文件与代码文件分离,从而大大缩短语法解析器的开发周期,并大大降低维护难度。

  但是Antlr4最大的问题是对低版本C++的支持不太好,它需要高版本的GCC,在Centos7中的GCC为4.8.5无法编译通过,好在Centos8刚刚发布,它的GCC为8.2.1,正好来试验Antlr4的C++版本来实现Contains表达式语法。

  网上的Antlr4生成C++版本语法解析器的资料较少,本文侧重整理与之相关的内容,并以Contains语法表达式为例,而具体的Antlr4的学习材料请见文尾的参考材料。

二、 搭建开发环境

1)、首先是安装Centos8的虚拟机环境,如上文所述,其gcc版本为8.2.1。

2)、Antlr4需要使用jdk,在Centos8中包含了jdk1.8和jdk11,我们选择安装jdk1.8

su
yum install java-1.8.-openjdk

安装完成后查看版本

java -version
openjdk version "1.8.0_201"
OpenJDK Runtime Environment (build 1.8.0_201-b09)
OpenJDK -Bit Server VM (build 25.201-b09, mixed mode)

3)、下载Antlr4的Java包,用于根据语法文件生成C++版的解析器。

  在antlr的下载页https://www.antlr.org/download.html中找到Complete ANTLR 4.7.2 Java binaries jar. Complete ANTLR 4.7.2 tool, Java runtime and ST 4.0.8, which lets you run the tool and the generated code.,注:随着时间的变化antlr4版本和下载链接都会变化呀。

mkdir libs
curl https://www.antlr.org/download/antlr-4.7.2-complete.jar -o ./libs/antlr-4.7.2-complete.jar

  验证

java -jar ./libs/antlr-4.7.-complete.jar
ANTLR Parser Generator Version 4.7.
-o ___ specify output directory where all output is generated
-lib ___ specify location of grammars, tokens files
-atn generate rule augmented transition network diagrams
-encoding ___ specify grammar file encoding; e.g., euc-jp
-message-format ___ specify output style for messages in antlr, gnu, vs2005
-long-messages show exception details when available for errors and warnings
-listener generate parse tree listener (default)
-no-listener don't generate parse tree listener
-visitor generate parse tree visitor
-no-visitor don't generate parse tree visitor (default)
-package ___ specify a package/namespace for the generated code
-depend generate file dependencies
-D<option>=value set/override a grammar-level option
-Werror treat warnings as errors
-XdbgST launch StringTemplate visualizer on generated code
-XdbgSTWait wait for STViz to close before continuing
-Xforce-atn use the ATN simulator for all predictions
-Xlog dump lots of logging info to antlr-timestamp.log
-Xexact-output-dir all output goes into -o dir regardless of paths/package

4)、下载Antltr4的C++运行库,采用编译安装的方式。

  在antlr的下载页https://www.antlr.org/download.html中找到Linux and others use source distribution: antlr4-cpp-runtime-4.7.2-source.zip (.h, .cpp),注:随着时间的变化antlr4版本和下载链接都会变化呀。

  

mkdir work
url https://www.antlr.org/download/antlr4-cpp-runtime-4.7.2-source.zip -o ./work/antlr4-cpp-runtime-4.7.2-source.zip
mkdir cpp
cd cpp
unzip ../antlr4-cpp-runtime-4.7.-source.zip
mkdir build && mkdir run && cd build
cmake .. -DANTLR_JAR_LOCATION=/home/ansible/libs/antlr-4.7.-complete.jar -DWITH_DEMO=True
make
su
make install
ll /usr/local/include/antlr4-runtime/antlr4-runtime.h
ll /usr/local/lib/libantlr4*

三、编写Contains表达式解析器

Contains函数完整语法如下:

Contains(column_name,query_expression[,score_flag])

其中query_expression是一个字符串表达式,它可以由SQL解析完成。

3.1、  基本功能

功能列表如下:

1)、显式的与(AND)操作符‘&’,例如 hello & world

2)、隐式的与(AND)操作符‘空格’,例如'hello  world'

3)、或(OR)操作符‘|’, 例如 hello | world

4)、非(NOT)操作符‘-’, 例如 hello – world

5)、首字词操作符‘^’, 例如 ^hello

6)、尾字词操作符‘$’, 例如 mouse$

7)、词组查询操作符 "",例如"南大"

8)、分组操作符(),例如( hello world ) & (cat | dog)

9)、阀值匹配符 ‘/’, 例如 "the great wall is a wonderful place"/3

10)、  NEAR搜索函数 near((term1, term2), num,order), 例如 near((great, place), 2, 1), num表示词距,order 为 0 代表无词序, 为 1代表有词序

11)、  扩展选项,搜索表达式通过":"分作基本表达式和扩展选项两个部分,总长度的限制为255字符,其中扩展选项可以为空,目前扩展选项仅支持rank=tf,表示相关度算法采用词频而不是缺省的bm25算法。例如"南大: rank=tf" 表示搜索南大,相关度为词频。

3.2、  语法规则

query_expression具体由Contains表达式解析器完成,其语法规则用Antlr4语法描述如下:

contains_param:
contains_expr
|contains_expr ':' fti_optstring
;
fti_optstring :
fti_opt
| fti_opt '&' fti_optstring
;
fti_opt:
ID '=' string_value
;
contains_expr:
contains_string
| CONST_STRING '/' NUMBER
| func_near_expr
| '(' contains_expr ')'
| contains_expr contains_expr
| contains_expr OPT_AND contains_expr
| contains_expr OPT_OR contains_expr
| contains_expr OPT_NOT contains_expr
;
contains_string:
string_value
| SENTENCE_HEAD string_value
| SENTENCE_HEAD string_value SENTENCE_TAIL
| string_value SENTENCE_TAIL
;
string_value:
ID
| STRING
| CONST_STRING
| NUMBER
;
func_near_expr:
NEAR '(' '(' near_term_list ')' ',' NUMBER near_order ')'
;
near_term_list:
near_term
| near_term ',' near_term_list
;
near_term:
func_near_expr
| contains_string
;
near_order:
| ',' NUMBER
;

3.3、  词法规则

CONST_STRING : DQuote ( EscSeq | ~["\r\n\\] )* DQuote	;
NEAR : N E A R ;
SENTENCE_HEAD : '^' ;
SENTENCE_TAIL : '$' ;
OPT_AND : '&' ;
OPT_OR : '|' ;
OPT_NOT : '-' ;
NUMBER :
'0'
| [1-9] DecDigit*
; ID: [a-zA-Z] ([a-zA-Z] | DecDigit | '_')* ;// Identifier
STRING : NameChar + ;
WS : ( Hws | Vws )+ -> skip; fragment DQuote : '"' ;
fragment Esc : '\\' ; 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 DecDigits : DecDigit+ ;
fragment DecDigit : [0-9] ; fragment HexDigits : HexDigit+ ;
fragment HexDigit : [0-9a-fA-F] ; fragment Hws : [ \t] ;
fragment Vws : '\r'? [\n\f] ; fragment NameChar
: NameStartChar
| '0'..'9'
| '_'
| '\u00B7'
| '\u0300'..'\u036F'
| '\u203F'..'\u2040'
;
fragment NameStartChar
: 'A'..'Z' | 'a'..'z'
| '\u00C0'..'\u00D6'
| '\u00D8'..'\u00F6'
| '\u00F8'..'\u02FF'
| '\u0370'..'\u037D'
| '\u037F'..'\u1FFF'
| '\u200C'..'\u200D'
| '\u2070'..'\u218F'
| '\u2C00'..'\u2FEF'
| '\u3001'..'\uD7FF'
| '\uF900'..'\uFDCF'
| '\uFDF0'..'\uFFFD'
; fragment UnicodeEsc
: 'u' (HexDigit (HexDigit (HexDigit HexDigit?)?)?)?
; // Any kind of escaped character that we can embed within ANTLR literal strings.
fragment EscSeq
: Esc
( [btnfr"'\\] // The standard escaped character set such as tab, newline, etc.
| UnicodeEsc // A Unicode escape sequence
| . // Invalid escape character
| EOF // Incomplete at EOF
)
;

四、代码实现

  Antlr4支持Visitor模式和Listener模式,一个是在语法分析完成后执行遍历语法树,一个是在语法分析过程中实时处理,相当于XML分析的DOM模式和SAX模式。在本次实验中因为表达式是相对简单的小对象,所以仅考虑Visitor模式。

  由语法规则文件生成C++代码:

java -jar /home/ansible/libs/antlr-4.7.-complete.jar -Dlanguage=Cpp FtiExpr.g4 -visitor -no-listener -o ./antlr4

  在antlr4下生成cpp代码文件列表如下:

词法分析器
FtiExprLexer.h
FtiExprLexer.cpp
语法分析器
FtiExprParser.h
FtiExprParser.cpp
Visitor模式访问语法树的抽象类
FtiExprVisitor.h
FtiExprVisitor.cpp
Visitor模式访问语法树的最简示例类
FtiExprBaseVisitor.h
FtiExprBaseVisitor.cpp

对于Visitor模式,我们自然要从FtiExprVisitor派生出遍历语法树的类,同时一般还会从BaseErrorListener派生出合适的错误处理类,来收集错误信息。

驱动框架的代码如下:

///\brief 分析Contains表达式
///\param strExpr 表达式字符串
///\param strOutput 如果符合语法,返回格式化后的表达式字符串,反之则返回分析过程中的错误信息
///\return 是否符合语法 true 符合;false 不符合
bool CTestFtiExprVisitorFixture::ParseString(const std::string &strExpr, std::string &strOutput)
{
bool bParse = false;
ANTLRInputStream input(strExpr);
FtiExprLexer lexer(&input);
CommonTokenStream tokens(&lexer);
FtiExprParser parser(&tokens);
parser.removeErrorListeners();
CFtiExprErrorListener listenerError;
parser.addErrorListener(&listenerError);
FtiExprParser::Contains_paramContext *pParamContext = parser.contains_param();
if(listenerError.m_strErrMsg.empty())
{
CTestFtiExprVisitor visitor;
antlrcpp::Any strExpr = visitor.visit(pParamContext);
strOutput = strExpr.as<std::string>();
bParse = true;
}
else
{
char cLine[],cCol[];
snprintf(cLine, , "%d", listenerError.m_nLine);
snprintf(cCol, , "%d", listenerError.m_nPositionInLine);
strOutput = "Line: " + std::string(cLine) + " Col: " + std::string(cCol) + " Msg:" + listenerError.m_strErrMsg;
}
return bParse;
}

  主要就是字符串=》词法分析器 =》Token串 =》语法规则

  FtiExprParser::Contains_paramContext *pParamContext = parser.contains_param();

  语法解析

  antlrcpp::Any strExpr = visitor.visit(pParamContext);

  进行语法树遍历。

   其他测试的主要代码如下:

  

void CTestFtiExprVisitorFixture::TestParseOk(std::string strExpr, std::string strExpected)
{
std::string strOutput;
ParseString(strExpr, strOutput);
CPPUNIT_ASSERT_EQUAL(strExpected, strOutput);
} void CTestFtiExprVisitorFixture::TestParseFail(std::string strExpr, std::string strExpected)
{
std::string strOutput;
ParseString(strExpr, strOutput);
CPPUNIT_ASSERT_EQUAL(strExpected, strOutput);
} void CTestFtiExprVisitorFixture::TestParsePass(void)
{
TestParseOk("tianjin", "tianjin");
TestParseOk("中国", "中国");
TestParseOk("tianjin", "tianjin");
TestParseOk("", "");
TestParseOk("\"tianjin\"", "\"tianjin\""); TestParseOk("^tianjin", "^ tianjin");
TestParseOk("^tianjin$", "^ tianjin $");
TestParseOk("tianjin$", "tianjin $"); TestParseOk("\"tianjin beijing\"/ 12", "\"tianjin beijing\" / 12"); TestParseOk("tianjin beijing", "( tianjin ) & ( beijing )");
TestParseOk("tianjin & beijing", "( tianjin ) & ( beijing )");
TestParseOk("tianjin | beijing", "( tianjin ) | ( beijing )");
TestParseOk("tianjin - beijing", "( tianjin ) - ( beijing )"); TestParseOk("tianjin beijing | shangxi hebei", "( ( tianjin ) & ( beijing ) ) | ( ( shangxi ) & ( hebei ) )");
TestParseOk("(tianjin beijing) | (shangxi hebei)", "( ( ( tianjin ) & ( beijing ) ) ) | ( ( ( shangxi ) & ( hebei ) ) )"); TestParseOk("NEAR((tianjin , beijing),10)", "NEAR((tianjin,beijing),10)");
TestParseOk("NEAR((tianjin,beijing),10,1)", "NEAR((tianjin,beijing),10,1)");
} void CTestFtiExprVisitorFixture::TestParseNoPass(void)
{
TestParseFail("", "Line: 1 Col: 0 Msg:mismatched input '<EOF>' expecting {'(', CONST_STRING, NEAR, '^', NUMBER, ID, STRING}");
} void CTestFtiExprVisitorFixture::TestFtiOpt(void)
{
TestParseOk("NEAR((tianjin,beijing),10,1) : rank = wordcount", "NEAR((tianjin,beijing),10,1) : rank = wordcount");
TestParseOk("NEAR((tianjin,beijing),10,1) : rank = wordcount&mode=fast", "NEAR((tianjin,beijing),10,1) : rank = wordcount & mode = fast");
TestParseOk("NEAR((12tianjin,beijing),10,1) : rank = wordcount", "NEAR((12tianjin,beijing),10,1) : rank = wordcount");
TestParseFail("NEAR((tianjin,beijing),10,1) : 1rank = wordcount", "Line: 1 Col: 31 Msg:mismatched input '1rank' expecting ID");
}

  完整代码示例在:https://github.com/ZhenYongFan/Blog/tree/master/TestFtiExpr

五、参考资料

官方资料,生成目标语言为C++的Antlr4

https://github.com/antlr/antlr4/blob/master/doc/cpp-target.md

ANTLR 4简明教程

https://www.cntofu.com/book/115/index.html

Antlr4 ---词法规则

https://blog.csdn.net/yangguosb/article/details/85624640

antlr v4 使用指南连载4——词法规则入门之黄金定律

https://www.cnblogs.com/laud/p/antlr4_4.html

antlr v4 使用指南连载5——如何编写词法定义

https://www.cnblogs.com/laud/p/anltrv4_5.html

Anrlr4 生成C++版本的语法解析器的更多相关文章

  1. 在.NET Core中使用Irony实现自己的查询语言语法解析器

    在之前<在ASP.NET Core中使用Apworks快速开发数据服务>一文的评论部分,.NET大神张善友为我提了个建议,可以使用Compile As a Service的Roslyn为语 ...

  2. Boost学习之语法解析器--Spirit

    Boost.Spirit能使我们轻松地编写出一个简单脚本的语法解析器,它巧妙利用了元编程并重载了大量的C++操作符使得我们能够在C++里直接使用类似EBNF的语法构造出一个完整的语法解析器(同时也把C ...

  3. 用java实现编译器-算术表达式及其语法解析器的实现

    大家在参考本节时,请先阅读以下博文,进行预热: http://blog.csdn.net/tyler_download/article/details/50708807 本节代码下载地址: http: ...

  4. 使用 java 实现一个简单的 markdown 语法解析器

    1. 什么是 markdown Markdown 是一种轻量级的「标记语言」,它的优点很多,目前也被越来越多的写作爱好者,撰稿者广泛使用.看到这里请不要被「标记」.「语言」所迷惑,Markdown 的 ...

  5. 手写token解析器、语法解析器、LLVM IR生成器(GO语言)

    最近开始尝试用go写点东西,正好在看LLVM的资料,就写了点相关的内容 - 前端解析器+中间代码生成(本地代码的汇编.执行则靠LLVM工具链完成) https://github.com/daibinh ...

  6. 语法解析器续:case..when..语法解析计算

    之前写过一篇博客,是关于如何解析类似sql之类的解析器实现参考:https://www.cnblogs.com/yougewe/p/13774289.html 之前的解析器,更多的是是做语言的翻译转换 ...

  7. 【读书笔记】-【编程语言的实现模式】-【LL(1)递归下降的语法解析器】

    形如:[a,b,c] [a,[b,cd],f] 为 嵌套列表 其ANTLR文法表示: list :'[' elements ']'; // 匹配方括号 elements : elements (',' ...

  8. JSP编译成Servlet(一)语法树的生成——语法解析

    一般来说,语句按一定规则进行推导后会形成一个语法树,这种树状结构有利于对语句结构层次的描述.同样Jasper对JSP语法解析后也会生成一棵树,这棵树各个节点包含了不同的信息,但对于JSP来说解析后的语 ...

  9. rest framework的框架实现之 (版本,解析器,序列化,分页)

    一版本 版本实现根据访问的的方式有以下几种 a : https://127.0.0.1:8000/users?version=v1  ---->基于url的get方式 #settings.pyR ...

随机推荐

  1. Day004课程内容

    本节主要内容: 1.列表List L = [1,'哈哈哈','吼吼',[1,8,0],('"我“,"叫","元","组"),”ab ...

  2. charles 映射到本地文件/文件夹

    本文参考:charles 映射到本地文件/文件夹 本地映射/Map Local Settings 功能:把需要请求网络的文件映射为请求本地文件 本地映射工具 本地映射工具使您能够使用本地文件,就好比他 ...

  3. Nginx限流

    文章原创于公众号:程序猿周先森.本平台不定时更新,喜欢我的文章,欢迎关注我的微信公众号. 在当今流量徒增的互联网时代,很多业务场景都会涉及到高并发.这个时候接口进行限流是非常有必要的,而限流是Ngin ...

  4. [STL] Implement "map", "set"

    练习热身 Ref: STL中map的数据结构 C++ STL中标准关联容器set, multiset, map, multimap内部采用的就是一种非常高效的平衡检索二叉树:红黑树,也成为RB树(Re ...

  5. 【linux】【sonarqube】安装sonarqube7.9

    前言 SonarQube 是一款用于代码质量管理的开源工具,它主要用于管理源代码的质量. 通过插件形式,可以支持众多计算机语言,比如 java, C#, go,C/C++, PL/SQL, Cobol ...

  6. MySQL中对字段内容为Null的处理

    使用如下指令,意思就是 select IFNULL(jxjy,0) AS jxjy from yourTable ifnull(a,b) 意思是指:如果字段a为null,就等于b if( sex = ...

  7. C++进程间通讯方式

    1.剪切板模式. 在MFC里新建两个文本框和两个按钮,点击发送按钮相当于复制文本框1的内容,点击接收按钮相当于粘贴到文本框2内: 发送和接收按钮处功能实现如下: void CClipboard2Dlg ...

  8. Centos安装PhantomJS

    1.下载PhantomJS [root@liuge ~]# wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-l ...

  9. FILETIME类型到LARGE_INTEGER类型的转换

    核心编程第5版 245页到247页的讲到SetWaitableTimer函数的使用 其中提到 FILETIME类型到LARGE_INTEGER类型的转换问题,如下代码 //我们声明的局部变量 HAND ...

  10. How to setup Electrum testnet mode and get BTC test coins

    For some reason we need to use BTC test coins, but how to set up the Bitcoin testnet wallet and get ...