使用ANTLR.js解析网络应用中的原始文本输入

1,367 阅读8分钟

简介

网络应用程序经常接受用户的输入。在大多数情况下,网络应用程序要求每个用户单独输入。例如,一个典型的网络应用程序会要求你在注册过程中输入你的名字、姓氏和电子邮件地址。

这种填表机制来自于最初的Web 2.0阶段。现在,为了获得更好的用户体验,几乎所有的应用程序都在试图减少强制性的用户输入的数量。例如,一些应用程序现在只要求在注册时提供你的登录电子邮件。

一些网络应用做了复杂的用户输入处理,比如分析一个日志文件,接受一个带有自定义语法的文本(例如,标签、内部文件标识符和用户提及的内容),以及特定领域的搜索查询。如果模式匹配的要求很简单,我们可以用正则表达式来实现解决方案。然而,如果我们需要有一个可扩展的解决方案,我们必须实现我们自己的解析器。

本教程将解释如何用ANTLR.js工具包创建一个解析器来处理原始文本输入。为了演示,我们将创建一个简单的日志解析器应用,将原始文本转换为HTML风格的输出。

编译器设计概念

在开始使用ANTLR之前,我们必须熟悉以下编译器设计的原则。

符号化

这是解析过程的初始通用步骤。这个步骤接受一个原始文本流并产生一个标记流。标记代表了语法的最小部分。例如,在许多编程语言中,return 字是一个标记。

解析树

解析树是一个树状数据结构实例,它有关于解析结果的信息。它包含令牌和复杂的解析器节点。

编译器前台

一个典型的编译器有三个关键模块:前端、中端和后端。编译器前端通过使用语言语法定义建立源代码的内部表示。

编译器后端

编译器后端从源代码的内部表示中生成目标语言代码。

什么是ANTLR.js?

ANTLR(ANother Tool for Language Recognition)是一个用Java编写的解析器生成器工具包。ANLTR广泛用于软件开发行业,用于开发编程语言、查询语言和模式匹配。它从自己的语法中生成解析器代码。

如果我们要从头开始实现一个解析器,我们必须写代码进行标记化和生成解析器树。当给出语言规范时,ANTLR会生成可扩展的解析器代码。换句话说,如果我们定义规则,解释我们需要如何使用ANTLR的语法进行解析,它将自动生成解析器的源代码。

ANTLR可以生成10种不同编程语言的解析器代码。ANTLR.js被称为JavaScript解析器代码和运行时间。

ANTLR.js教程

在本教程中,我将解释如何使用ANTLR.js制作一个简单的日志解析器。

让我们将我们的日志文件语法命名为SimpleLog。我们的日志解析器程序接受一个原始的日志输入。之后,它将从日志文件内容中产生一个HTML表格。意味着SimpleLog翻译器有一个编译器后端,从解析树中生成一个HTML表。

你可以按照类似的步骤,用JavaScript制作任何复杂的输入解析器。

用Webpack设置ANTLR.js

如果你需要在应用程序的后端使用ANTLR.js,你可以使用Node的npm包。

否则,如果你需要在你的应用程序的前端使用ANTLR.js,有几种方法。最舒服和最简单的方法是用Webpack将ANTLR.js运行时间与你的项目源捆绑在一起。在本教程中,我们将用Webpack设置ANTLR.js。

首先,我们需要为ANTLR.js创建开发环境。确保先安装JRE(Java Runtime Environment)。创建一个目录并下载ANTLR分析器生成器CLI。

$ wget https://www.antlr.org/download/antlr-4.9.2-complete.jar

上面的命令是针对Linux的。对于其他操作系统,使用相等的命令来下载.jar文件。另外,你也可以用网络浏览器手动下载特定文件。

npm init 命令创建一个新的npm项目。之后,在package.json 文件中添加以下内容。

{
  "name": "log-parser",
  "version": "1.0.0",
  "scripts": {
    "build": "webpack --mode=development",
    "generate": "java -jar antlr-4.9.2-complete.jar SimpleLog.g4 -Dlanguage=JavaScript -o src/parser"
  },
  "dependencies": {
    "antlr4": "^4.9.2",
  },
  "devDependencies": {
    "@babel/core": "^7.13.16",
    "@babel/plugin-proposal-class-properties": "^7.13.0",
    "@babel/preset-env": "^7.13.15",
    "babel-loader": "^8.2.2",
    "webpack": "^4.46.0",
    "webpack-cli": "^4.6.0"
  }
}

创建webpack.config.js ,内容如下。

const path = require('path');

module.exports = {
  entry: path.resolve(__dirname, './src/index.js'),
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      },
    ],
  },
  resolve: {
    extensions: ['.js'],
    fallback: { fs: false }
  },
  output: {
    filename: 'logparser.js',
    path: path.resolve(__dirname, 'static'),
    library: 'LogParser',
    libraryTarget: 'var'
  }
};

我们也需要有一个.babelrc ,因为ANTLR.js使用一些最新的ECMAScript特性。

因此,在.babelrc 中添加以下片段。

{
  "presets": [
    "@babel/preset-env"
  ],
  "plugins": [
    [
      "@babel/plugin-proposal-class-properties",
      {
        "loose": true
      }
    ]
  ]
}

确保在你的终端上输入npm install ,以拉取所需的依赖项,包括ANTLR.js运行库。现在我们的ANTLR.js环境有足够的命令来生成解析器代码和构建最终的源代码。

然而,我们仍然缺少一个重要的部分。这就是我们的日志文件格式的语法。让我们继续前进,实现ANTLR的语法。

编写ANTLR语法

让我们假设我们的日志文件遵循以下格式,我们需要对其进行解析以确定所需的信息。

[ERROR] => Missing user input end time :: src/createInvoice.js:10
[WARNING] => No user found with matching query :: src/search.js:50
[INFO] => Invoice is ready for downloading :: src/invoice.js:100
[INFO] => Email sent successfully :: src/resetPassword.js:20

上面的日志文件行有三个日志级别:ERROR,WARNING, 和INFO 。在这之后,有一条信息。最后,我们有触发日志进程的代码模块和行号。

在为上述日志文件语法编写ANTLR语法之前,我们需要识别标记。SimpleLog语法有三个关键标记,如下图所示。

  • 日志类型 (ERROR,WARNING, 和INFO)
  • 文本 (Datetime,Message, 和Module)
  • 数字(被触发的行)

现在,我们对词法规则有了一个概念。让我们通过使用上述标记和一些分析器树生成规则来编写ANTLR语法。当你写语法时,你可以遵循自下而上的方法。换句话说,你可以从标记开始,以分析器规则结束。将以下语法逻辑添加到SimpleLog.g4

grammar SimpleLog;

logEntry    : logLine+;

logLine     : '[' logType ']' ' => ' logMessage ' :: ' logSender;
logType     : (INFO | WARNING | ERROR);
logMessage  : TEXT+?;
logSender   : logFile ':' DIGITS;
logFile     : TEXT+?;

INFO     : 'INFO';
WARNING  : 'WARNING';
ERROR    : 'ERROR';
TEXT     : [a-zA-Z ./]+?;
DIGITS   : [0-9]+;
WS       : [ \n\t]+ -> skip;

在上述SimpleLang语法文件中,Camelcase单词代表解析器规则。这些解析器规则通过使用标记来帮助建立一个解析树。在最顶端,我们的解析树有一个指向行的条目。之后,每一行的节点都有logType,logMessage, 和logSender 的节点。

大写的定义是词法规则。这些词法规则有助于标记化过程。来自用户的原始输入将使用这些标记,如文本片段、数字和日志类型,进行标记化。

在你的项目目录下的终端上运行以下命令来触发解析器代码的生成。

$ npm run generate

如果你正确地制作了语法文件,你将能够在src/parser 目录内看到自动生成的解析器代码。让我们来实现SimpleLog翻译程序的后端。

实现一个树状访问者

ANTLR解析过程将生成一个内存中的解析树。它还提供了一个监听器类来在解析树上遍历。我们需要创建一个树形访问者,以遍历解析树并产生输出的HTML表结构。在编译器理论中,这被称为代码生成过程。

将以下代码添加到src/TableGenerator.js

import SimpleLogListener from "./parser/SimpleLogListener"

export default class TableGenerator extends SimpleLogListener {

    tableSource = "";

    exitLogLine(ctx) {
        const logType = ctx.logType().getText();
        const logMessage = ctx.logMessage().getText();
        const logFile = ctx.logSender().logFile().getText();
        const logLine = ctx.logSender().DIGITS(0).getText();
        this.tableSource += 
        `
        <tr>
            <td>${logType}</td>
            <td>${logMessage}</td>
            <td>${logFile}</td>
            <td>${logLine}</td>
        <tr>
        `
    }

    getTable() {
        const table = `
            <table>
                <thead>
                    <th>Type</th>
                    <th>Message</th>
                    <th>File</th>
                    <th>Line</th>
                <thead>
                ${this.tableSource}
            </table>
        `;
        return table;
    }
}

上面的类扩展了自动生成的基监听器类。基层监听器类拥有所有与树行走相关的方法。在我们的方案中,为了简单起见,我们只覆盖了exitLogLine 方法。我们可以从exitLogLine 方法中获得日志类型、消息、文件和行号。编写代码的过程被称为 "发射"。在这里,我们从树形行走器类中发射HTML表的语法。

最后完成SimpleLog解析器库

我们正在用Webpack准备一个客户端库,因为我们需要在浏览器中直接使用解析器逻辑。现在我们需要为我们的库提供一个公共入口点。让我们把LogParser.parse() 方法暴露给浏览器。

src/index.js ,这是我们解析器库的入口点,添加以下代码。

import antlr4 from 'antlr4';
import SimpleLogLexer from './parser/SimpleLogLexer';
import SimpleLogParser from './parser/SimpleLogParser';
import TableGenerator from './TableGenerator';

export let parse = (input) => {
   const chars = new antlr4.InputStream(input);
   const lexer = new SimpleLogLexer(chars);
   const tokens  = new antlr4.CommonTokenStream(lexer);
   const parser = new SimpleLogParser(tokens);
   parser.buildParseTrees = true;
   const tree = parser.logEntry();
   const tableGenerator = new TableGenerator();
   antlr4.tree.ParseTreeWalker.DEFAULT.walk(tableGenerator, tree);
   return tableGenerator.getTable();
}

解析方法接受一个原始输入,并相应地返回HTML表格结构。现在,我们的解析器库已经完成。

在你的终端上执行以下命令,从源代码中制作一个单一的JavaScript源文件。

$ npm run build

结果的JavaScript文件将被保存到static/logparser.js

最后,我们可以实现我们的SimpleLog分析器程序的图形用户界面(GUI)。

开发SimpleLog分析器的网络应用程序

我们的网络应用程序有三个主要部分:文本区、解析按钮和结果区。我为这个例子程序使用普通的HTML和vanilla JavaScript建立了一个简单的界面。

将以下HTML和JavaScript代码添加到static/index.html 文件中。

<script src="logparser.js"></script>
<textarea id="rawText"></textarea>
<button onclick="generateTable();">Parse</button>
<div id="tableWrapper"></div>
<script>
    function generateTable() {
        const textarea = document.getElementById("rawText");
        const tableWrapper = document.getElementById("tableWrapper");
        tableWrapper.innerHTML = LogParser.parse(textarea.value);
    }
</script>

恭喜!我们的SimpleLog分析器网络应用程序现在已经准备好了。该网络应用程序可以通过静态文件服务器或直接双击HTML文件来启动。试着复制粘贴一个输入样本。之后,点击解析按钮,得到原始文本的HTML版本。

Screenshot of parser web application

完整的项目源代码可以在GitHub上找到。

结论

我们也可以使用ANTLR.js来解析用户的原始文本输入。ANTLR.js有各种各样的用例。本教程解释了一个简单的例子。同样的方法可以用来制作网络转译器、高级网络刮削、复杂的模式匹配和基于网络的查询语言,以建立下一级的网络应用。

你是否试图为你的网络应用建立一个自定义的查询语言?试试ANTLR.js吧。

The postParsing raw text inputs in web applications using ANTLR.jsappeared first onLogRocket Blog.