JavaCC导论
为解析器开发初学者提供的JavaCC简单介绍。
引言
大多数时候,当需要解析文件或流时,程序员倾向于依赖“Tokenizer
”或“StreamTokenizer
”,而不是创建解析器。当然,创建解析器非常耗时,因为它需要对所有可能的状态进行迭代测试。然而,它能使您的应用程序健壮且无错误,尤其是在处理具有特定格式的文件时。一旦您开始创建简单的解析器,您一定会在许多棘手的情况下发现它是一个更好的选择。本文面向解析器开发的初学者。它将通过一个合适的循序渐进的示例,让您了解如何创建解析器。在本教程结束时,我们将解析一个SQL文件并提取表规范(请注意,这仅用于说明目的;完整的SQL格式不受支持)。
必备组件
JavaCC可与Java VM 1.2或更高版本配合使用。您需要安装JavaCC。有关安装帮助,请参阅https://javacc.dev.java.net/doc/installhelp.html。如果您使用Eclipse,则有免费的JavaCC插件可用。下载并安装一个插件(搜索easy-javacc-1.5.7.exe,这是一个免费的JavaCC的Eclipse插件)。
什么是JavaCC?
Java Compiler Compiler(Java编译器编译器)是一款开源的Java程序解析器生成器。与YACC(Yet Another Compiler Compiler)不同,JavaCC是一款用于LL类型语法的自顶向下解析器。因此,严格来说,JavaCC不支持LR解析(左递归)。JavaCC为上下文无关文法(上下文无关文法包含NT -->T格式的产生式规则,其中NT是一个已知的终结符,t是终结符和/或非终结符的组合)创建LL解析器。LL解析器从左到右解析输入,并创建句子的最左推导,而LR解析器创建句子的最右推导。这类解析器使用下一个标记来做出解析决策,而无需任何回溯(前瞻)。因此,这些LL解析器并不复杂,并且即使它们相当受限制,也被广泛使用。
开始之前
解析器和词法分析器是描述任何字符流的语法和语义的软件组件。词法分析器识别并分离一组预定义的字符,这些字符称为标记。解析器指定语句中不同标记的语义和/或不同语句的语义。因此,解析器可以轻松地用于检查文件结构和从文件中提取特定组件。例如,在下面的Java语句中
String test = "Testing";
词法分析发现以下标记
{ "String"|" "|"Test"|" "|"="|" "| "\"" |"Testing"| "\""|";"}
因此,在词法分析之后,我们将得到这些标记组
{STRING_TYPE|SPACE|VAR_NAME|SPACE|EQUAL|SPACE|D_QUATES|VAR_VAL|D_QUATES|SEMCOLN}
解析器根据文法文件中的规范,检查词法分析器识别出的标记的语义。在JavaCC的情况下,它本身不是词法分析器或解析器,而是根据上下文无关文法文件中的规范生成词法分析器和解析器的Java代码。这些文法文件按照约定命名,扩展名为.jj。与手动编写的Java解析器相比,这些文法文件具有更高的模块化,并且易于阅读、修改或编写,从而节省大量时间和精力。
文件中文本的关联由BNF产生式描述。Backus-Naur Form(BNF)是一种元语法表示法,用于表示上下文无关文法。大多数解析器生成器支持BNF产生式规则,用于指定无错误输出中文本种类的顺序。在接下来的部分中,您将看到如何在.jj文件中使用BNF产生式指定文本关联。
那么,让我们用一个非常简单的例子开始创建JavaCC文件。假设我们有一个包含SQL Create
语句的文件。在本例中,我们不考虑Create Table语句中的字段。我们将在后续步骤中向文件添加更多内容。通常,在文件复杂性不断增加的情况下,分步创建和修改文法是一种好习惯。
Create Table test()
在上面的示例中,您有标记
CTCMD :- "Create Table" //the create table command
TNAME :- "test" //the table name followed
OBRA :- "(" //Opening bracket
CBRA: - ")" //Closing bracket
EOF //End of the file
文法文件结构
要生成解析器,JavaCC的唯一输入是一个上下文无关文法文件。JavaCC在输出中生成7个Java文件。JavaCC文法文件(.jj文件)由四个部分组成。
- 选项
- 类声明
- 词法分析规范
- BNF表示法
Options
部分用于指定可选参数,例如DEBUG
、STATIC
等。在本例中,STATIC
设置为false
,以便解析器类是非静态的,这样就可以同时存在多个解析器实例。STSTIC
默认为true
。文法文件中的类声明提供了生成解析器中的主入口点。文法文件中的另一部分用于指定用于词法分析的标记。文法可以包含BNF产生式规则,这些规则指定定义文件结构的标记的关联。为了解析上述文件,我们的文法文件如下所示:
/* Sample grammar file*/
options
{STATIC = false ;}
PARSER_BEGIN(SqlParser)
package sqlParserDemo;
class SqlParser {
{Start () ;}
}
PARSER_END (SqlParser)
SKIP: { "\n" | "\r" | "\r\n" |"\\"|"\t"|" "}
TOKEN [IGNORE_CASE]:
{
<CTCMD :("Create Table")>
| <TNAME :(["a"-"z"])+ >
| <OBRA :("(")>| <CBRA:(")")>
}
void Start (): {}
{<CTCMD><TNAME><OBRA><CBRA><EOF>}
在上面的文法文件中,类声明部分用PARSER_BEGIN
和PARSER_END
关键字括起来。在此部分中,我们定义包、所有导入和解析器类。这里,“SqlParser
”类有一个initParser
方法,该方法充当入口点。解析器抛出“ParseException
”和“TokenMgrError
”异常。当扫描遇到未定义的标记时,将抛出TokenMgrError
。如果遇到未定义的状语或产生式,解析器将抛出“ParseException
”。默认情况下,创建的解析器代码应包含一个接受“reader
”类型的构造函数。
在许多情况下,我们需要忽略文件中的某些字符,例如换行符、空格、制表符等格式字符。这些序列可能与含义无关。如果将这些字符指定为SKIP
终结符,则可以在扫描时跳过它们。在上面的文法文件中,换行符、回车符(请注意,不同操作系统下的换行符表示方式不同)和空格被指定为SKIPJA
终结符。扫描器读取这些字符并忽略它们。它们不会传递给解析器。
TOKEN
关键字用于指定标记。每个标记都是一个与名称关联的字符序列。“|”字符用于分隔标记。JavaCC提供了一个标记限定符IGNORE_CASE
。它用于使扫描器对标记不区分大小写。在本例中,SQL不区分大小写,因此“Create Table”命令可以以任何大小写形式编写。如果您有区分大小写和不区分大小写的标记,则可以在不同的TOKEN
语句中指定它们。标记用尖括号括起来。
这里我们创建了CTCMD
、TNAME
、OBRA
和CBRA
标记。标记的字符序列由正则表达式语法定义。(["a"-"z"])+表示由“a"-"z”组成的任意数量字符的序列。
BNF产生式规则在标记声明之后指定。它们看起来与方法语法非常相似。我们可以在第一组花括号中添加任何Java代码。通常,它们包含在产生式规则中使用的变量的声明。返回类型是BNF的预期返回类型。在本例中,我们只检查文件结构。因此,返回类型为void
。示例中的Start
函数初始化解析。您应该从类声明调用Start
方法。在本例中,它定义了文件的结构为
{<CTCMD><TNAME><OBRA><CBRA><EOF>}
文件格式的任何更改都将引发错误。
生成解析器
- 创建文法文件后,将其保存在一个目录中。一般的命名约定是使用“.jj”扩展名,例如,demogrammar.jj。
- 转到命令提示符,并将目录更改为demogrammar.jj文件所在的目录。
- 键入命令“javacc demogrammar.jj”。
如果您使用的是已安装JavaCC插件的Eclipse,请按照以下步骤操作
- 创建一个新项目,并将包设置为文法文件中指定的包。
- 要创建文法文件,请右键单击该包,然后转到新建-->其他-->JavaCC-->JavaCC模板文件。创建文法文件并保存。
- 构建项目。
调用Javacc处理文法文件(demogrammar.jj)后,您将在其自己的文件中获得以下7个类。
Token
:表示标记的类。每个标记都与一个“标记种类”关联,该种类表示标记的类型。字符串“image”表示与标记关联的字符序列。TokenMgrError
:Error
的子类。在词法分析错误时抛出异常。ParseException
:Exception
的子类。在解析器检测到的错误时抛出异常。SimpleCharStream
:词法分析器的字符流接口的实现。SqlParserConstants
:用于定义词法分析器和解析器使用的标记类的接口。SqlParserTokenManager
:词法分析器类。SqlParser
:解析器类。
编译生成的类(javac *.java)。
运行解析器
成功编译后,您就可以测试一个示例文件了。要测试该示例,请在包中添加一个具有main
函数的类。
/*for testing the parser class*/
public class ParseDemoTest {
public static void main(String[] args) {
try{SqlParserparser = new SqlParser(new FileReader(FilePath));
parser.initParser () ;}
catch (Exception ex)
{ex.printStackTrace() ;}}
创建解析器对象时,可以将reader作为构造函数参数。请注意,生成的解析器代码包含一个接受reader的构造函数。InitParser
方法初始化解析。现在您可以构建并运行“ParseDemoTest”。如果给定文件路径中指定的文件不符合文法,则会抛出异常。正如我们已经讨论了JavaCC操作的整体思路,现在我们可以向文件添加更多内容以进行解析。在本例中,我们将提取SQL文件中提供的表规范(请注意,本示例仅用于说明目的,因此文法不符合所有SQL语法)。新的SQL文件格式如下:
##JavaccParserExample#####
CREATE TABLE STUDENT
(
StudentName varchar (20),
Class varchar(10),
Rnum integer,
)
CREATE TABLE BOOKS
(
BookName varchar(10),
Edition integer,
Stock integer,
)
CREATE TABLE CODES
(
StudentKey varchar(20),
StudentCode varchar(20),
)
从文件中可以清楚地看出,它可能包含多个Create
语句,并且每个Create
语句可能包含多个列。此外,还有用字符“#”括起来的注释。为了获取表规范,我们需要一个包含表列表及其详细信息的结构。在包中创建一个类来表示表
public class TableStruct {
String TableName;
HashMap<String,String> Variables =
new HashMap<String, String> ();
}
此类表示表,其中包含表名以及映射到其数据类型的列名。下面是针对新文件的修改后的文法文件:
通过检查新的文法文件,您可以看到SPECIAL_TOKEN
。如前所述,特殊标记是那些没有实际意义但仍具有信息量(如注释)的标记。这里,注释由特殊标记定义。词法分析器识别特殊标记并将其传递给解析器。特殊标记没有BNF表示法。您可以看到,与BNF标记相关的所有变量声明和其他Java代码都包含在“{}”中。表达式可以包含其他表达式,例如TType = DType()
。识别可重用表达式并单独指定它们是一种好习惯。在变量的BNF表示法中
( TName = <TNAME>
TType = DType()
<COMMA>
{var.put(TName.image,TType.image);}
)*
“*”表示该标记序列可以出现任意次数。要运行和测试此文法文件,请按如下方式更改您的主类:
public class ParseDemoTest {
public static void main(String[] args) {
try{
SqlParser parser = new SqlParser (new FileReader("D:\\sqltest.txt"));
ArrayList<TableStruct> tableList = parser.initParser();
for(TableStruct t1 : tableList)
{ System.out.println("--------------------------");
System.out.println("Table Name :"+t1.TableName);
System.out.println("Field names :"+t1.Variables.keySet());
System.out.println("Data Types :"+t1.Variables.values());
System.out.println("--------------------------");
}
}catch (Exception ex)
{ex.printStackTrace() ;}
}
}
编译文法文件并运行应用程序。对于SQL测试文件,您将获得以下输出:
--------------------------
Table Name :STUDENT
Field names :[StudentName, Rnum, Clas
--------------------------
--------------------------
Table Name :BOOKS
Field names :[Stock, Edition, BookName]
Data Types :[integer, integer, varchar]
--------------------------
--------------------------
Table Name :CODES
Field names :[StudentCode, StudentKey]
Data Types :[varchar, varchar]
--------------------------
结论
JavaCC是一种广泛使用的词法和解析器组件生成工具,它遵循正则表达式和BNF表示法语法来进行词法和解析器规范。创建解析器需要迭代步骤。切勿期望一次性获得所需输出。在示例中创建第一个解析器后,尝试修改它并为输入文件添加其他可能性。在处理复杂文件时,应采用循序渐进的方法。此外,尽量使表达式尽可能通用和可重用。一旦您熟悉了.jj文件,就可以使用**JJTree**和**JTB**等更高级的工具与JavaCC一起自动化文法的增强。
/* demo grammar.jj*/
options
{
STATIC = false ;
}
PARSER_BEGIN (SqlParser)
package sqlParserDemo;
import java.util.ArrayList;
import java.util.HashMap;
class SqlParser {
ArrayList<TableStruct> initParser()throws ParseException, TokenMgrError
{ return(init()) ; }
}
PARSER_END (SqlParser)
SKIP: { "\n" | "\r" | "\r\n" |"\\"|"\t"|" "}
TOKEN [IGNORE_CASE]:
{
<CTCMD :("Create Table")>
|<NUMBER :(["0"-"9"])+ >
|<TNAME:(["a"-"z"])+ >
|<OBRA:("(")+>
|<CBRA:(")")+>
|<COMMA:(",")>
}
SPECIAL_TOKEN : {<COMMENT:("#")+(<TNAME>)+("#")+>}
ArrayList<TableStruct> init():
{
Token T;
ArrayList<TableStruct> tableList = new ArrayList<TableStruct>();
TableStruct tableStruct;
}
{
(
<CTCMD>
T =<TNAME>
{ tableStruct = new TableStruct ();
tableStruct.TableName = T.image ;}
<OBRA>
tableStruct.Variables = Variables()
<CBRA>
{tableList.add (tableStruct) ;}
)*
<EOF>
{return tableList;}
}
HashMap Variables():
{
Token TName;
Token TType;
HashMap<String,String> var = new HashMap<String, String>();
}
(
TName = <TNAME>
TType = DType()
<COMMA>
{var.put(TName.image,TType.image);}
)*
{return var;}
}
Token DType():
{
Token TDType;
}
{
TDType=<TNAME>
[<OBRA><NUMBER><CBRA>]
{return TDType;}
}Create Table test
(
)