- 平岡知隆
- 約 4,400 文字
- 7,100 View
はじめに
こんにちは、TIG コアテクノロジーユニットの平岡です。
コアテクノロジーユニットはフューチャーグループ全体のビジネスを支えるコアとなる技術をグループ横断で提供することをミッションにしています。またOSS活動も活発に行っています。コアテクノロジーユニットのメンバーが以前に投稿した記事をいくつか紹介しておきますので、是非そちらもご覧ください。
- RedmineからGoogle Hangouts Chat へ連携するプラグインを作成しました!
- その問い合わせ、AIが解決します!~Redmineチケットレコメンドシステムのご紹介~
- SQL開発者を幸せにする!? Sublime Text 3でも使える uroboroSQL Formatter を公開しました
ANTLRとは
皆さんはANTLRをご存知でしょうか? ANTLRとはparser(構文解析器)を生成するためのツール(パーサジェネレータ)で、以下のような特徴があります。
- grammar(解析したいテキストの構造に関するルールを定義したもの)を元に、parserを自動生成できる
- 解析対象をparserに渡した後に構成されるAST(抽象構文木)をトラバースすることで、目的に合った解析ができる
- 様々なプログラミング言語のgrammarが公式で整備されている
- もちろん、自分でgrammarを作ることもできる
- ターゲット言語(生成されるparserの実装に使用可能な言語)はJava, JavaScript, Pythonなど主要なものをサポートしている(詳細)
- Javaで開発をしない人向けに:ただし、ANTLRはJavaで実装されているため、Java以外の言語で実装されたparser生成にもJavaが必要となる(Javaコマンドでparser生成したい場合の手順)
このように非常に汎用性の高いツールであり、応用先は多岐にわたるようです。公式サイトによると、Twitterの検索機能にも使われているという記述がありました。
パーサジェネレータや構文解析に関連した他のトピックもいくつか紹介しておきましょう。
- ANTLR以外のパーサジェネレータの例としては、JavaCC(Java), PEG.js(JavaScript), Bison(C)などが有名ですが、いずれもターゲット言語=実装言語です。一方で、ANTLRはターゲット言語の種類が多いという特徴を持ちます。
- 同じコアテクノロジーユニットのメンバーである太田さん(Vue.jsのコミッターです! )がメンテしているeslint-plugin-vueでは、ANTLRは使っていませんがASTに基づいた構文解析をしています。
この記事では、まずシンプルな例を通してANTLRの概要を説明します。その後、ANTLRを使って業務で生じた課題を解決した時の話について述べたいと思います。
ANTLRの概要
lexerとparser
ANTLRはgrammarを元にparserを自動生成できることを上で述べましたが、より正確には、解析対象のテキストをparserに渡すために必要なlexer(字句解析器)も生成します。ここでは、123 + 456のような2個の整数を足す式を構文解析する例を通してlexer, parserの役割について説明します。
まず、解析対象がlexerを経由してparserに渡るイメージを見てみましょう。
lexerの役割は、解析対象を読んでトークン(1つの意味を持つ最小単位)の列に分解することです。この例では、2個の整数を足す式123 + 456を3つのトークン123,+,456に分解しています。なお、123 + 456には空白が含まれていますが、grammarを上手く設定すると読み飛ばすことができます(後述)。
parserの役割は、トークンの列を読んでAST(抽象構文木)を構成することです。listenerやvisitor(後述)を実装してASTをトラバースすることで、目的に合った解析処理を行うことができます。
lexer, parserを生成するためにgrammarには何を記述すれば良いのでしょうか?
基本的には、ルール名:ルール定義;の形式でルールを列挙します。実際に例を見てみましょう。
/** |
/** |
VSCodeをお使いの場合は、ANTLR4 grammar syntax supportという拡張を導入するとシンタックスハイライトが使えて見やすくなります。また、この拡張はgrammarの構造を可視化する機能も備えており(詳しくはこちら)、非常に便利です。
grammarの説明に戻りましょう。
lexer用とparser用のgrammarを分割して記載していますが、同一ファイルで記述できます。保守性を考慮する場合は、分割するのが良いでしょう。
lexerとparserのルールを区別するために、lexerの場合はルール名は大文字始まり、parserの場合はルール名は小文字始まりで記述する必要があります。
lexerのルール定義について見てみましょう。例えば、整数を表すトークンNUMの定義は、[0-9]+ですが、これは数字1個以上からなる文字列を最長一致でマッチさせてNUMというトークンに割り当てることを意味します。
parserのルール定義について見てみましょう。sumというルールは、NUM,PLUS,NUMという3つのトークンから構成される列(つまり、足し算の式)であるという定義です。sumのようなルール名はASTの節点に対応し、NUM, PLUS, NUMのようなルール定義を構成する要素はルール名に対応する節点の子に対応しています。
lexer用grammarのSPACEでは、skipコマンドを使うことで空白を読み飛ばしています。skipコマンドを使わないで空白をケアする場合は、parser用grammarのsumの定義が下記のように複雑になってしまいます。このように、lexerの定義次第でparserの定義が複雑になり得るので注意が必要です。
sum |
grammarの詳細については、公式ドキュメントを参照して下さい。
では、上に載せたgrammarを元に、Mavenでparserを生成してみましょう。
ANTLR v4 Maven pluginのデフォルトの動作の特徴は次の通りです。
src/main/antlr4以下に配置されたgrammarファイル(.g4ファイル)を参照してparser等を生成する- 生成されるparser等のパッケージ構造は、
src/main/antlr4から見た相対パスを元に自動で設定される
jp.co.future.antlr.parserパッケージにparser等を格納したい場合は下記のようにgrammarを配置すれば良いです(参照するgrammarや設定したいパッケージ名をpom.xml内で明示的に指定できます)。
src |
また、pom.xmlに以下のように追記します。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
mvn antlr4:antlr4を実行すると、下記のようにparser等が生成されました。
src |
listenerとvisitor
grammarを元にparserを生成できました。
ここからは、parserから得られるASTをトラバース(ASTのノードを深さ優先探索で順番に訪問)し、目的に合った解析をする方法について見ていきましょう。
今回は、+の右側の整数を取得したいという目的があるとしましょう(例えば、123 + 456が入力の場合は456を出力)。
ASTをトラバースするためには、listenerもしくはvisitorを実装する必要があります。
まずは、listenerの実装例を見てみましょう。
package jp.co.future.antlr.parser; |
package jp.co.future.antlr; |
456 |
listenerの特徴は以下の通りです。
- ASTの全ノードを訪問する
- ノードに入るタイミング(
enterXXXメソッド)・ノードを抜けるタイミング(exitXXXメソッド)に行う処理を目的に合わせてOverrideする - Overrideするメソッドは返り値を持たないため、欲しい値はフィールドで保持する必要がある
上の実装例のExtractRhsNumListener#enterSumは、ASTでsumノードを最初に訪問した時に2番目のNUM(つまり、+の右側の整数)をセットする処理を行っています。enterSumではなくexitSumでOverrideしても同様の実行結果が得られます。
続いて、visitorの実装例を見てみましょう。
package jp.co.future.antlr.parser; |
package jp.co.future.antlr; |
456 |
visitorの特徴は以下の通りです。
- ASTの全ノードを訪問するとは限らない
- ノードに入った時(
visitXXXメソッド)に行う処理を目的に合わせてOverrideする - Overrideするメソッドは返り値を持つため、欲しい値をフィールドで保持しなくてよい
上の実装例のExtractRhsNumVisitor#visitSumは、ASTでsumノードを最初に訪問した時に2番目のNUM(つまり、+の右側の整数)を返す処理を行っています。
listenerでは自動的にASTの全ノードを訪問するのに対し、visitorの場合は今いるノードの部分木を訪問したい場合は明示的に実装する必要があるという大きな違いがあります。
業務で生じた課題
以降では、業務で生じた課題をANTLRで解決した話について述べたいと思います。課題の概要は以下の通りです。
以下のような<template>, <script>, <style>ブロックから構成されるvueファイルが与えられます。
<template> |
このとき、以下のように<script>ブロックの中身を抽出する処理をJavaで実装して下さい。
export default { |
解決策
- 正規表現では難しい
<script>(.*)</script>で良さそうに見えるが、以下のようなコメントがある場合に対応できない
<!-- <script></script> --> |
- DOMやNekoHTMLなどのJavaで動作するXML用parserでは正しくparseできない場合があった
- 当時はANTLRについて調査するタスクも担当していたため、ANTLRで対応を試みた
- Vue.js用のgrammarは公式で用意されていなかったが、
<script>ブロックの中身を取り出せれば十分なので、grammarを自作することにした
- Vue.js用のgrammarは公式で用意されていなかったが、
コード
lexer用grammarを表示
/** |
parser用grammarを表示
/** |
抽出用listenerを表示
package jp.co.future.antlr.parser; |
この記事に掲載したコードをGitHub上でもご確認頂けます。
https://github.com/f-t-hiraoka/antlr-sample
まとめ
正規表現や既存のparserでは対応が難しい課題をANTLRを利用して解決しました。文字列の処理で困ったときは、選択肢の1つとして検討してみてはいかがでしょうか。
コアテクノロジーユニットでは、現在チームメンバーを募集しています。
私たちと一緒にテクノロジーで設計、開発、テストの高品質・高生産性を実現する仕組みづくりをしませんか?