返回

ANTLR4 解析键值对难题:深入解析与解决方案

java

ANTLR4解析键值对问题解析

在使用 ANTLR4 处理键值对格式的数据时,经常遇到无法正确解析的问题,尤其在面对包含空格、注释以及多行数据时。 常见的错误表现为匹配失败,导致提取的键和值为空,或者出现语法错误。 本文深入分析问题原因,提供有效的解决方案,并附带实际的代码示例和操作步骤,帮助大家理清思路,快速解决解析问题。

问题根源分析

问题中,虽然语法看似简单,却隐藏着几个潜在的陷阱。首要问题是 value规则的定义不当 ,它定义valuevalueChar+ EOL,这意味着 value 必须由一个或多个 valueCharEOL组成。这里的 valueChar 为任何字符,这会导致它提前吞噬后续的空白符和其他token,同时要求value 必须以EOL结尾, 但是实际的数据经常是行末才有换行符。这也就造成了解析器在遇到值中间的空格或其他非Char定义的字符时直接报错,而无法将它们视为值的一部分。

此外,注释的定义也会影响到行末字符的处理 ,注释规则虽然正确跳过了整行,但当注释存在于值的末尾时,也会因为换行符消耗错误而产生不匹配问题,且导致 value 中缺少值部分。

解决方案一: 重新定义 Value规则

第一个改进方向是重新定义 value 规则,使它可以包含空格以及注释,并且以行的结尾,也就是下一个键值对开始,或文件结尾为边界。 这里引入 NotLineBreak 概念。

原理: value 规则不再强制必须包含EOL, 使用NotLineBreak* ( EOL | EOF ) 来匹配 任何不是换行的字符,0个或者多个,直到遇到行尾(或者文件末尾)。同时对非空白字符做进一步收窄,保证不包含换行符等token, 使用 [\u0020-\u007E\u00A0-\u00FF] 更加精准的控制了字符范围。

grammar KEYVALUE;

COMMENT: ';' .*? ('\r'? '\n' | EOF) -> skip;

file: line* EOF;

line: Space* keyValuePair?; // make the keyValuePair option

keyValuePair:
    key EQ value
    {
        String k = $key.text;
        String v = $value.text;

        System.out.println("\nkey   : `" + k + "`");
        System.out.println("value : `" + v + "`");
     };

key: AlphaNum+;


value: NotLineBreak* (EOL|EOF) ;

NotLineBreak : [\u0020-\u007E\u00A0-\u00FF]; // all ascii & ansi but linebreaks



EOL: LineBreak | EOF;
EQ: '=';
LineBreak: '\r'? '\n' | '\r';
Space: ' ' | '\t' | '\f';
AlphaNum: [a-zA-Z0-9];

操作步骤:

  1. 将上述代码保存为KEYVALUE.g4
  2. 使用antlr工具生成解析器:
    antlr4 KEYVALUE.g4
    javac KEYVALUE*.java
    
  3. 创建一个名为test.txt的文件,内容同题目中。
  4. 使用grun进行测试,例如:
    more test.txt | java org.antlr.v4.gui.TestRig KEYVALUE file -gui
    

结果分析:
使用新 grammar 生成的 parser,可以正确解析测试数据中的键值对,并且将值部分的空格和末尾的注释包括进来。

解决方案二: 利用词法分析器更精准处理注释

第二种方案尝试让词法分析器更加智能地处理注释,避免换行符造成的干扰。

原理: 将注释规则从语法规则提升至词法规则,使得注释的识别在更早阶段进行。 词法分析器直接抛弃注释部分的tokens。 使用 mode区分行末注释和 行间注释(实际本案例不存在行间注释,此处只是作为举例),配合 channel关键字将注释发送至隐藏的频道 HIDDEN 中, 而语法分析器仅考虑默认频道。

grammar KEYVALUE;

file: line* EOF;
line: Space* keyValuePair?; 

keyValuePair:
    key EQ value
    {
        String k = $key.text;
        String v = $value.text;

        System.out.println("\nkey   : `" + k + "`");
        System.out.println("value : `" + v + "`");
     };

key: AlphaNum+;
value: NotLineBreak* ( EOL| EOF );
NotLineBreak : [\u0020-\u007E\u00A0-\u00FF];


EOL: LineBreak | EOF;
EQ: '=';
LineBreak: '\r'? '\n' | '\r';
Space: ' ' | '\t' | '\f';
AlphaNum: [a-zA-Z0-9];



//词法器模式 - 行尾注释的处理。
COMMENT: ';' .*? ('\r'? '\n' | EOF) -> skip;
//mode COMMENT_MODE;
    // COMMENT:   ';' ~('\n' | '\r')*   (' \r'?  '\n'|EOF)  -> channel(HIDDEN);


操作步骤:

  1. 保存上述代码为KEYVALUE.g4
  2. 执行代码生成和编译过程同上。
  3. 执行grun命令测试效果,操作方法同上。

结果分析:
调整注释解析方案后, 可以看到解析结果更加稳定。 注释完全被跳过,值也成功提取。 这个方法让语法结构更为简洁,也为后续处理更复杂的文本奠定基础。

安全建议

在实际应用中,需要对用户输入进行验证,例如,可以对key或者value的长度进行限制,并防止诸如命令注入的安全问题。 可以使用其他工具例如 regex 或 String api 结合来校验输入的格式是否符合预期。另外, 在提取数据前应该检查输入,并且提前进行校验和清洗。