シーマークすずきです。
前回の続きで、antlr4が生成したパーサーソースをjavaから呼び出して、利用してみます。
■ eclipseのGradleプロジェクト作成
build.gradleは以下としました。antlr4が生成するソースをjp.co.seamark.csv.generatedパッケージに吐かれる様にします。また、構文木を捜索する方式は、リスナー方式とビジター方式がありますが、今回は、リスナー方式としました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | /* * This build file was auto generated by running the Gradle 'init' task * by 'suzuki' at '16/10/13 14:08' with Gradle 2.13 * * This generated file contains a sample Java project to get you started. * For more details take a look at the Java Quickstart chapter in the Gradle * user guide available at https://docs.gradle.org/2.13/userguide/tutorial_java_projects.html */ // Apply the java plugin to add support for Java apply plugin: 'java' apply plugin: 'antlr' // In this section you declare where to find the dependencies of your project repositories { // Use 'jcenter' for resolving your dependencies. // You can declare any Maven/Ivy/file repository here. jcenter() } ext.antlr = [ grammarpackage: "jp.co.seamark.csv.generated", destinationDir: "src/main/java/jp/co/seamark/csv/generated" ] dependencies { antlr 'org.antlr:antlr4:4.5' compile 'org.antlr:antlr4-runtime:4.5' compile 'org.slf4j:slf4j-api:1.7.12' testCompile 'junit:junit:4.12' } generateGrammarSource { outputDirectory = file(new File("${antlr.destinationDir}")) arguments = ["-package", "jp.co.seamark.csv.generated", "-listener", "-no-visitor"].flatten() } compileJava { dependsOn generateGrammarSource source antlr.destinationDir } jar { version="1.0.0" } clean { delete antlr.destinationDir } |
■ CSVの単純なパース
前回生成したソースのCSVBaseListenerクラスを拡張します。簡単にcsvファイルの全行をString型で受け取る方式とします。まずは、構文木の文字列形式に変換して返すだけにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | public class CSVSimpleParser extends CSVBaseListener { public String parser(final String csvfile){ try{ final CSVLexer lexer = new CSVLexer(new org.antlr.v4.runtime.ANTLRInputStream(csvfile)); CommonTokenStream tokens = new CommonTokenStream(lexer); CSVParser parser = new CSVParser(tokens); ParseTree tree = parser.file(); return tree.toStringTree(parser); }catch (IllegalArgumentException iae){ throw iae; } } public static void main(String[] args) { CSVSimpleParser parser = new CSVSimpleParser(); try{ String csv = fileToString(new File("test.csv")); System.out.println("AST=>"+parser.parser(csv)); }catch(Exception e){ System.out.print(e); } } } |
実行すると、以下の様に前回grunでコンソール出力した結果と同じ結果が表示されました。(注意)fileToString()は割愛します。
テスト用のtest.csvは前回と同じ以下。
1 2 3 4 | user.name, string#!user.age, *omit*user.gender suzuki,50,male ange,12,female seamark,20,male |
CSVSimpleParserの実行結果。
>AST=>(file (hdr (row (field user.name) , (field string#!user.age) , (field *omit*user.gender) \n)) (row (field suzuki) , (field 50) , (field male) \n) (row (field ange) , (field 12) , (field female) \n) (row (field seamark) , (field 20) , (field male) \n))
■ 構文木をウォークスルーしてみましょう
ParseTreeクラスをParseTreeWalkerクラスでwalk()することで、構文木の各構文ノードへのenter,とexitのリスナーイベントが呼ばれますのでメソッドをOverrideすることで自分用に処理を追加できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | import jp.co.seamark.csv.generated.CSVBaseListener; import jp.co.seamark.csv.generated.CSVLexer; import jp.co.seamark.csv.generated.CSVParser; import jp.co.seamark.csv.generated.CSVParser.RowContext; public class CSV2JSONParser extends CSVBaseListener { private List<String> row = new ArrayList<>(); private List<String> header = new ArrayList<>(); private List<Map<String,String>> rows = new ArrayList<Map<String,String>>(); static final String indent=" "; public String parser(final String csvfile){ try{ final CSVLexer lexer = new CSVLexer(new org.antlr.v4.runtime.ANTLRInputStream(csvfile)); CommonTokenStream tokens = new CommonTokenStream(lexer); CSVParser parser = new CSVParser(tokens); ParseTree tree = parser.file(); ParseTreeWalker walker = new ParseTreeWalker(); walker.walk(this, tree); return tree.toStringTree(parser); }catch (IllegalArgumentException iae){ throw iae; } } @Override public void exitField(CSVParser.FieldContext ctx) { if ( ctx.getText()!=null && !header.isEmpty()){ row.add(ctx.getText()); } } @Override public void exitHdr(CSVParser.HdrContext ctx) { if ( ctx.row()!=null){ RowContext rowCtx = ctx.row(); header = rowCtx.field().stream().map( h->h.getText().trim()) .collect(Collectors.toList()); } } @Override public void exitRow(CSVParser.RowContext ctx) { if ( !header.isEmpty()){ Map<String,String> rowmap = new ConcurrentHashMap<>(); for(String value : header){ rowmap.put(value,row.remove(0)); } rows.add(rowmap); } } } |
プログラムで処理する場合、構文ルール毎にexitほげほげのタイミングが分かりやすいでしょう。以前のCSV.gの文法を記述した際、構成要素はfieldというルールで各行rowが構成される、としました。fieldの構文ノードを抜けるタイミング、つまり、exitField()の関数でrowリストにfield文字列を追加していきます。先頭行のヘッダー行として、headerに、取り込みたい場合、exitHdr()のタイミングです。各CSV行の行毎のタイミングであれば、exitRow()となります。headerが空でない場合にCSVデータをheaderのラベルと一緒にMapに取り込みましょう。
■ JSON形式に変換してみる。
List<Map<String,String>> rowsに全ての行が取り込まれましたので、exitFile()をOverrideしてJSON形式に出力してみましょう。以下のメソッドを追記します。
1 2 3 4 5 6 7 8 9 10 11 12 | @Override public void exitFile(CSVParser.FileContext ctx) { System.out.println("[");int lines = rows.size(); for(Map<String,String> row : rows){ System.out.print(indent+"{"); int items=row.size(); for(Map.Entry<String, String> e : row.entrySet()) { System.out.print("\""+e.getKey() + "\":\"" + e.getValue()+"\""+((--items>0) ? ",":"")); } System.out.println("}"+((--lines>0) ? ",":"")); } System.out.println("]"); } |
出力結果は、以下。
1 2 3 4 5 | [ {"user.name":"suzuki","string#!user.age":"50","*omit*user.gender":"male"}, {"user.name":"ange","string#!user.age":"12","*omit*user.gender":"female"}, {"user.name":"seamark","string#!user.age":"20","*omit*user.gender":"male"} ] |
もう少し考慮すれば、以前のNode.jsのcsvtojson.jsと同じ様な変換ができそうです。”string#!”や”number#!”がヘッダーに付いていれば、JSON型を文字列、数値型にするとか、”*omit*”が付いていれば、JSON出力しない、とか。残りは、後日。