65.9K
CodeProject 正在变化。 阅读更多。
Home

JavaCC导论

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2009年4月22日

CPOL

10分钟阅读

viewsIcon

79170

为解析器开发初学者提供的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文件)由四个部分组成。

  1. 选项
  2. 类声明
  3. 词法分析规范
  4. BNF表示法

Options部分用于指定可选参数,例如DEBUGSTATIC等。在本例中,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_BEGINPARSER_END关键字括起来。在此部分中,我们定义包、所有导入和解析器类。这里,“SqlParser”类有一个initParser方法,该方法充当入口点。解析器抛出“ParseException”和“TokenMgrError”异常。当扫描遇到未定义的标记时,将抛出TokenMgrError。如果遇到未定义的状语或产生式,解析器将抛出“ParseException”。默认情况下,创建的解析器代码应包含一个接受“reader”类型的构造函数。

在许多情况下,我们需要忽略文件中的某些字符,例如换行符、空格、制表符等格式字符。这些序列可能与含义无关。如果将这些字符指定为SKIP终结符,则可以在扫描时跳过它们。在上面的文法文件中,换行符、回车符(请注意,不同操作系统下的换行符表示方式不同)和空格被指定为SKIPJA终结符。扫描器读取这些字符并忽略它们。它们不会传递给解析器。

TOKEN关键字用于指定标记。每个标记都是一个与名称关联的字符序列。“|”字符用于分隔标记。JavaCC提供了一个标记限定符IGNORE_CASE。它用于使扫描器对标记不区分大小写。在本例中,SQL不区分大小写,因此“Create Table”命令可以以任何大小写形式编写。如果您有区分大小写和不区分大小写的标记,则可以在不同的TOKEN语句中指定它们。标记用尖括号括起来。

这里我们创建了CTCMDTNAMEOBRACBRA标记。标记的字符序列由正则表达式语法定义。(["a"-"z"])+表示由“a"-"z”组成的任意数量字符的序列。

BNF产生式规则在标记声明之后指定。它们看起来与方法语法非常相似。我们可以在第一组花括号中添加任何Java代码。通常,它们包含在产生式规则中使用的变量的声明。返回类型是BNF的预期返回类型。在本例中,我们只检查文件结构。因此,返回类型为void。示例中的Start函数初始化解析。您应该从类声明调用Start方法。在本例中,它定义了文件的结构为

{<CTCMD><TNAME><OBRA><CBRA><EOF>}

文件格式的任何更改都将引发错误。

生成解析器

  1. 创建文法文件后,将其保存在一个目录中。一般的命名约定是使用“.jj”扩展名,例如,demogrammar.jj
  2. 转到命令提示符,并将目录更改为demogrammar.jj文件所在的目录。
  3. 键入命令“javacc demogrammar.jj”。

如果您使用的是已安装JavaCC插件的Eclipse,请按照以下步骤操作

  1. 创建一个新项目,并将包设置为文法文件中指定的包。
  2. 要创建文法文件,请右键单击该包,然后转到新建-->其他-->JavaCC-->JavaCC模板文件。创建文法文件并保存。
  3. 构建项目。

调用Javacc处理文法文件(demogrammar.jj)后,您将在其自己的文件中获得以下7个类。

  1. Token:表示标记的类。每个标记都与一个“标记种类”关联,该种类表示标记的类型。字符串“image”表示与标记关联的字符序列。
  2. TokenMgrErrorError的子类。在词法分析错误时抛出异常。
  3. ParseExceptionException的子类。在解析器检测到的错误时抛出异常。
  4. SimpleCharStream:词法分析器的字符流接口的实现。
  5. SqlParserConstants:用于定义词法分析器和解析器使用的标记类的接口。
  6. SqlParserTokenManager:词法分析器类。
  7. 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
        (
        )
© . All rights reserved.