简单语法解析器实现参考

  有时候,我们为了屏蔽一些底层的差异,我们会要求上游系统按照某种约定进行传参。而在我们自己的系统层则会按照具体的底层协议进行适配,这是通用的做法。但当我们要求上游系统传入的参数非常复杂时,也许我们会有一套自己的语法定义,用以减轻所有参数的不停变化。比如sql协议,就是一个一级棒的语法,同样是调用底层功能,但它可以很方便地让用户传入任意的参数。

  如果我们自己能够实现一套类似的东西,想来应该蛮有意思的。

  不过,我们完全没有必要要实现一整套完整的东西,我们只是要体验下这个语法解析的过程,那就好办了。本文就来给个简单的解析示例,供看官们参考。

 

1. 实现目标描述

  目标:

    基于我们自定义的一套语法,我们要实现一套类似于sql解析的工具,它可以帮助我们检查语法错误、应对底层不同的查询引擎,比如可能是 ES, HIVE, SPARK, PRESTO...  即我们可能将用户传入的语法转换为任意种目标语言的语法,这是我们的核心任务。

  前提:

    为简单化起见,我们并不想实现一整套的东西,我们仅处理where条件后面的东西。
  定义:
    $1234: 定义为字段信息, 我们可以通过该字段查找出一些更多的信息;
    and/or/like...: 大部分时候我们都遵循sql语法, 含义一致;
    #{xxx}: 系统关键字定义格式, xxx 为具体的关键字;
    arr['f1']: 为数组格式的字段;

  示例:

    $15573 = 123 and (my_udf($123568, $82949) = 1) or $39741 = #{day+1} and $35289 like '%ccc'
    将会被翻译成ES:(更多信息的字段替换请忽略)
    $15573 = 123 and ( $123568 = 1 ) or $39741 = '2020-10-07' and $35289 like '%ccc'

  实际上整个看下来,和一道普通的算法题差不太多呢。但实际想要完整实现这么个小东西,也是要费不少精力的。

 

2. 整体实现思路

  我们要做一个解析器,或者说翻译器,首先第一步,自然是要从根本上理解原语义,然后再根据目标语言的表达方式,转换过去就可以了。

  如果大家有看过一些编译原理方面的书,就应该知道,整个编译流程大概分为: 词法分析;语法分析;语义分析;中间代码生成;代码优化;目标代码; 这么几个过程,而每个过程往往又是非常复杂的,而最复杂的往往又是其上下文关系。不过,我们不想搞得那么复杂(也搞不了)。

  虽然我们不像做一个编译器一样复杂,但我们仍然可以参考其流程,可以为我们提供比较好的思路。

  我们就主要做3步就可以了:1. 分词;2. 语义分析; 3. 目标代码生成;而且为了进一步简化工作,我们省去了复杂的上下文依赖分析,我们假设所有的语义都可以从第一个关键词中获得,比如遇到一个函数,我就知道接下来会有几个参数出现。而且我们不处理嵌套关系。

  所以,我们的工作就变得简单起来。

 

3. 具体代码实现

  我们做这个解析器的目的,是为了让调用者方便,它仅仅作为一个工具类存在,所以,我们需要将入口做得非常简单。

  这里主要为分为两个入口:1. 传入原始语法,返回解析出的语法树; 2. 调用语法树的translateTo 方法,将原始语法转换为目标语法;

具体如下:

import com.my.mvc.app.common.helper.parser.*; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import java.util.*; /** * 功能描述: 简单语法解析器实现示例 * */ @Slf4j public class SimpleSyntaxParser { /** * 严格模式解析语法 * * @see #parse(String, boolean) */ public static ParsedClauseAst parse(String rawClause) { return parse(rawClause, true); } /** * 解析传入词为db可识别的语法 * * @param rawClause 原始语法, 如: * $15573 = 123 and (week_diff($123568, $82949) = 1) or $39741 like '%abc%' (week_diff($35289)) = -1 * @param strictMode 是否是严格模式, true:是, false:否 * @return 解析后的结构 */ public static ParsedClauseAst parse(String rawClause, boolean strictMode) { log.info("开始解析: " + rawClause); List<TokenDescriptor> tokens = tokenize(rawClause, strictMode); Map<String, Object> idList = enhanceTokenType(tokens); return buildAst(tokens, idList); } /** * 构建抽象语法树对象 * * @param tokens 分词解析出的tokens * @param idList id信息(解析数据源参照) * @return 构建好的语法树 */ private static ParsedClauseAst buildAst(List<TokenDescriptor> tokens, Map<String, Object> idList) { List<SyntaxStatement> treesFlat = new ArrayList<>(tokens.size()); Iterator<TokenDescriptor> tokenItr = tokens.iterator(); while (tokenItr.hasNext()) { TokenDescriptor token = tokenItr.next(); String word = token.getRawWord(); TokenTypeEnum tokenType = token.getTokenType(); SyntaxStatement branch; switch (tokenType) { case FUNCTION_SYS_CUSTOM: String funcName = word.substring(0, word.indexOf('(')).trim(); SyntaxStatementHandlerFactory handlerFactory = SyntaxSymbolTable.getUdfHandlerFactory(funcName); branch = handlerFactory.newHandler(token, tokenItr, tokenType); treesFlat.add(branch); break; case KEYWORD_SYS_CUSTOM: branch = SyntaxSymbolTable.getSysKeywordHandlerFactory() .newHandler(token, tokenItr, tokenType); treesFlat.add(branch); break; case KEYWORD_SQL: branch = SyntaxSymbolTable.getSqlKeywordHandlerFactory() .newHandler(token, tokenItr, tokenType); treesFlat.add(branch); break; case WORD_NORMAL: case WORD_NUMBER: case WORD_STRING: case CLAUSE_SEPARATOR: case SIMPLE_MATH_OPERATOR: case WORD_ARRAY: case COMPARE_OPERATOR: case FUNCTION_NORMAL: case ID: case FUNCTION_SQL: default: // 未解析的情况,直接使用原始值解析器处理 branch = SyntaxSymbolTable.getCommonHandlerFactory() .newHandler(token, tokenItr, tokenType); treesFlat.add(branch); break; } } return new ParsedClauseAst(idList, treesFlat); } /** * 语义增强处理 * * 加强token类型描述,并返回 id 信息 */ private static Map<String, Object> enhanceTokenType(List<TokenDescriptor> tokens) { Map<String, Object> idList = new HashMap<>(); for (TokenDescriptor token : tokens) { String word = token.getRawWord(); TokenTypeEnum newTokenType = token.getTokenType(); switch (token.getTokenType()) { case WORD_NORMAL: if(word.startsWith("$")) { newTokenType = TokenTypeEnum.ID; idList.put(word, word.substring(1)); } else if(StringUtils.isNumeric(word)) { newTokenType = TokenTypeEnum.WORD_NUMBER; } else { newTokenType = SyntaxSymbolTable.keywordTypeOf(word); } token.changeTokenType(newTokenType); break; case WORD_STRING: // 被引号包围的关键字,如 '%#{monthpart}%' String innerSysCustomKeyword = readSplitWord( word.toCharArray(), 1, "#{", "}"); if(innerSysCustomKeyword.length() > 3) { newTokenType = TokenTypeEnum.KEYWORD_SYS_CUSTOM; } token.changeTokenType(newTokenType); break; case FUNCTION_NORMAL: newTokenType = SyntaxSymbolTable.functionTypeOf(word); token.changeTokenType(newTokenType); break; } } return idList; } /** * 查询语句分词操作 * * 拆分为单个细粒度的词如: * 单词 * 分隔符 * 运算符 * 数组 * 函数 * * @param rawClause 原始查询语句 * @param strictMode 是否是严格模式, true:是, false:否 * @return token化的单词 */ private static List<TokenDescriptor> tokenize(String rawClause, boolean strictMode) { char[] clauseItr = rawClause.toCharArray(); List<TokenDescriptor> parsedTokenList = new ArrayList<>(); Stack<ColumnNumDescriptor> specialSeparatorStack = new Stack<>(); int clauseLength = clauseItr.length; StringBuilder field; String fieldGot; char nextChar; outer: for (int i = 0; i < clauseLength; ) { char currentChar = clauseItr[i]; switch (currentChar) { case '\'': case '\"': fieldGot = readSplitWord(clauseItr, i, currentChar, currentChar); i += fieldGot.length(); parsedTokenList.add( new TokenDescriptor(fieldGot, TokenTypeEnum.WORD_STRING)); continue outer; case '[': case ']': case '(': case ')': case '{': case '}': if(specialSeparatorStack.empty()) { specialSeparatorStack.push( ColumnNumDescriptor.newData(i, currentChar)); parsedTokenList.add( new TokenDescriptor(currentChar, TokenTypeEnum.CLAUSE_SEPARATOR)); break; } parsedTokenList.add( new TokenDescriptor(currentChar, TokenTypeEnum.CLAUSE_SEPARATOR)); char topSpecial = specialSeparatorStack.peek().getKeyword().charAt(0); if(topSpecial == '(' && currentChar == ')' || topSpecial == '[' && currentChar == ']' || topSpecial == '{' && currentChar == '}') { specialSeparatorStack.pop(); break; } specialSeparatorStack.push( ColumnNumDescriptor.newData(i, currentChar)); break; case ' ': // 空格忽略 break; case '@': nextChar = clauseItr[i + 1]; // @{} 扩展id, 暂不解析, 原样返回 if(nextChar == '{') { fieldGot = readSplitWord(clauseItr, i, "@{", "}@"); i += fieldGot.length(); parsedTokenList.add( new TokenDescriptor(fieldGot, TokenTypeEnum.ID)); continue outer; } break; case '#': nextChar = clauseItr[i + 1]; // #{} 系统关键字标识 if(nextChar == '{') { fieldGot = readSplitWord(clauseItr, i, "#{", "}"); i += fieldGot.length(); parsedTokenList.add( new TokenDescriptor(fieldGot, TokenTypeEnum.KEYWORD_SYS_CUSTOM)); continue outer; } break; case '+': case '-': case '*': case 'http://www.likecs.com/': nextChar = clauseItr[i + 1]; if(currentChar == '-' && nextChar >= '0' && nextChar <= '9') { StringBuilder numberBuff = new StringBuilder(currentChar + "" + nextChar); ++i; while ((i + 1) < clauseLength){ nextChar = clauseItr[i + 1]; if(nextChar >= '0' && nextChar <= '9' || nextChar == '.') { ++i; numberBuff.append(nextChar); continue; } break; } parsedTokenList.add( new TokenDescriptor(numberBuff.toString(), TokenTypeEnum.WORD_NUMBER)); break; } parsedTokenList.add( new TokenDescriptor(currentChar, TokenTypeEnum.SIMPLE_MATH_OPERATOR)); break; case '=': case '>': case '<': case '!': // >=, <=, !=, <> nextChar = clauseItr[i + 1]; if(nextChar == '=' || currentChar == '<' && nextChar == '>') { ++i; parsedTokenList.add( new TokenDescriptor(currentChar + "" + nextChar, TokenTypeEnum.COMPARE_OPERATOR)); break; } parsedTokenList.add( new TokenDescriptor(currentChar, TokenTypeEnum.COMPARE_OPERATOR)); break; default: field = new StringBuilder(); TokenTypeEnum tokenType = TokenTypeEnum.WORD_NORMAL; do { currentChar = clauseItr[i]; field.append(currentChar); if(i + 1 < clauseLength) { // 去除函数前置名后置空格 if(SyntaxSymbolTable.isUdfPrefix(field.toString())) { do { if(clauseItr[i + 1] != ' ') { break; } ++i; } while (i + 1 < clauseLength); } nextChar = clauseItr[i + 1]; if(nextChar == '(') { fieldGot = readSplitWord(clauseItr, i + 1, nextChar, ')'); field.append(fieldGot); tokenType = TokenTypeEnum.FUNCTION_NORMAL; i += fieldGot.length(); break; } if(nextChar == '[') { fieldGot = readSplitWord(clauseItr, i + 1, nextChar, ']'); field.append(fieldGot); tokenType = TokenTypeEnum.WORD_ARRAY; i += fieldGot.length(); break; } if(isSpecialChar(nextChar)) { // 严格模式下,要求 -+ 符号前后必须带空格, 即会将所有字母后紧连的 -+ 视为字符连接号 // 非严格模式下, 即只要是分隔符即停止字符解析(非标准分隔) if(!strictMode || nextChar != '-' && nextChar != '+') { break; } } ++i; } } while (i + 1 < clauseLength); parsedTokenList.add( new TokenDescriptor(field.toString(), tokenType)); break; } // 正常单字解析迭代 i++; } if(!specialSeparatorStack.empty()) { ColumnNumDescriptor lineNumTableTop = specialSeparatorStack.peek(); throw new RuntimeException("检测到未闭合的符号, near '" + lineNumTableTop.getKeyword()+ "' at column " + lineNumTableTop.getColumnNum()); } return parsedTokenList; } /** * 从源数组中读取某类词数据 * * @param src 数据源 * @param offset 要搜索的起始位置 offset * @param openChar word 的开始字符,用于避免循环嵌套 如: '(' * @param closeChar word 的闭合字符 如: ')' * @return 解析出的字符 * @throws RuntimeException 解析不到正确的单词时抛出 */ private static String readSplitWord(char[] src, int offset, char openChar, char closeChar) throws RuntimeException { StringBuilder builder = new StringBuilder(); for (int i = offset; i < src.length; i++) { if(openChar == src[i]) { int aroundOpenCharNum = -1; do { builder.append(src[i]); // 注意 openChar 可以 等于 closeChar if(src[i] == openChar) { aroundOpenCharNum++; } if(src[i] == closeChar) { aroundOpenCharNum--; } } while (++i < src.length && (aroundOpenCharNum > 0 || src[i] != closeChar)); if(aroundOpenCharNum > 0 || (openChar == closeChar && aroundOpenCharNum != -1)) { throw new RuntimeException("syntax error, un closed clause near '" + builder.toString() + "' at column " + --i); } builder.append(closeChar); return builder.toString(); } } // 未找到匹配 return ""; } /** * 重载另一版,适用特殊场景 (不支持嵌套) * * @see #readSplitWord(char[], int, char, char) */ private static String readSplitWord(char[] src, int offset, String openChar, String closeChar) throws RuntimeException { StringBuilder builder = new StringBuilder(); for (int i = offset; i < src.length; i++) { if(openChar.charAt(0) == src[i]) { int j = 0; while (++j < openChar.length() && ++i < src.length) { if(openChar.charAt(j) != src[i]) { break; } } // 未匹配开头 if(j < openChar.length()) { continue; } builder.append(openChar); while (++i < src.length){ int k = 0; if(src[i] == closeChar.charAt(0)) { while (++k < closeChar.length() && ++i < src.length) { if(closeChar.charAt(k) != src[i]) { break; } } if(k < closeChar.length()) { throw new RuntimeException("un closed syntax, near '" + new String(src, i - k, k) + ", at column " + (i - k)); } builder.append(closeChar); break; } builder.append(src[i]); } return builder.toString(); } } // 未找到匹配 return " "; } /** * 检测字符是否特殊运算符 * * @param value 给定检测字符 * @return true:是特殊字符, false:普通 */ private static boolean isSpecialChar(char value) { return SyntaxSymbolTable.OPERATOR_ALL.indexOf(value) != -1; } }

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wppfpd.html