標準のAPIは?
CSV(Comma-Separated Values)は、データの交換によく使われる形式ですがJavaにはCSVを扱う標準のAPIがありません。いろいろな方が独自のAPIを作って公開されていますが、勉強を兼ねて自分で作ってみました。CSVのフォーマット
Wikipediaによれば、2005年にRFC 4180でCSVの仕様が成文化されたようです。ただ、これを全部読むのは大変なので、次のような簡単なルールで行単位に処理を行うことを目標にしました。区切り記号にコンマ(,)を使用する。
行末にコンマが現れる場合は末尾に空のカラムを追加する。
行頭にコンマが現れる場合は先頭に空のカラムを追加する。
コンマが連続して現れる場合は、コンマの間に空のカラムを追加する。
データ中にコンマが現れる場合は、カラム全体をダブルクオート(")で囲む。
データ中にダブルクオートが現れる場合は、カラム全体をダブルクオートで囲み、更にダブルクオートをダブルクオートでエスケープする(ダブルクオートを2つ重ねる)。
ダブルクオートで囲まれていないカラムにダブルクオートが現れた場合はエラーとする。
ダブルクオートで始まったカラムがダブルクオートで終っていなければエラーとする。
ダブルクオートで囲まれたカラムであっても、単独のダブルクオートが現れた場合はエラーとする。
このようなルールで、1行を渡すと、文字列の配列を返すstaticメソッドをCSVTokenizerクラスに実装します。
状態の種類
文字列を解釈して、そこからデータを取り出すことを構文解析とかパースとかパーズなどと呼びます。構文解析はプログラミング言語のコンパイラを作る際にも使われる技術でさまざまな方式がありますが、基本的には有限個の「状態」を定義して、次に現れる文字によって別の(または同じ)状態に「遷移」させる、ということをします。1行のCSVを解析するために次の4種類の状態を定義します。状態 | 説明 |
INIT | カラム開始時の初期状態 |
NOQUOT | カラムがダブルクオートで囲まれていない状態 |
QUOT | カラムがダブルクオートで囲まれている状態 |
QUOT_ESCAPE | ダブルクオートで囲まれたカラムで ダブルクオートがエスケープされている状態 |
状態の遷移
それぞれの状態において、次に現れた文字に応じて行う処理と、その後に遷移する状態を定義します。INIT
現れた文字 | 処理 | 遷移先 |
カンマ | 空のカラムを追加 | そのまま |
ダブルクオート | QUOT | |
上記以外 | 文字をカラムに追加 | NOQUOT |
NOQUOT
現れた文字 | 処理 | 遷移先 |
カンマ | カラム内容の確定 | INIT |
ダブルクオート | エラー | 終わり |
上記以外 | 文字をカラムに追加 | そのまま |
QUOT
現れた文字 | 処理 | 遷移先 |
ダブルクオート(次がダブルクオート) | QUOT_ESCAPE | |
ダブルクオート(次がカンマ) | NOQUOT | |
ダブルクオート(上記以外) | エラー | 終わり |
上記以外 | 文字をカラムに追加 | そのまま |
QUOT_ESCAPE
現れた文字 | 処理 | 遷移先 |
ダブルクオート | ダブルクオートをカラムに追加 | QUOT |
上記以外 | エラー(ここには来ないはず) | 終わり |
このように有限個の状態を条件によって遷移するような仕組みを「有限オートマトン」と言います(オートマトンを「自動羊肉」と発音してはいけません。危険なオヤジギャグです)。
プログラム
このルールをCSVTokenizerというクラスのparseメソッドに実装します。インスタンスを作らずに呼べるようにstaticにしておきます。package jp.veltec.util;
import java.util.ArrayList;
public class CSVTokenizer{
static final int INIT = 0;
static final int NOQUOT = 1;
static final int QUOT = 2;
static final int QUOT_ESCAPE = 3;
public static String[] parse(String aLine){
ArrayList<String> record = new ArrayList<String>();
StringBuilder sb = new StringBuilder();
int state = INIT;
for(int i=0;i<aLine.length();i++){
char c=aLine.charAt(i);
switch(state){
case INIT:
if(c==','){
record.add("");
}else if(c=='"'){
state=QUOT;
}else{
state=NOQUOT;
sb.append(c);
}
break;
case NOQUOT:
if(c==','){
record.add(sb.toString());
sb=new StringBuilder();
state = INIT;
}else if(c=='"'){
throw new IllegalStateException("bad quot at:" + (i+1));
}else{
sb.append(c);
}
break;
case QUOT:
if(c=='"'){
if(i==aLine.length()-1){
state=NOQUOT;
}else{
if(aLine.charAt(i+1)=='"'){
state=QUOT_ESCAPE;
}else if(aLine.charAt(i+1)==','){
state=NOQUOT;
}else{
throw new IllegalStateException("bad quot at:" + (i+1));
}
}
}else{
sb.append(c);
}
break;
case QUOT_ESCAPE:
if(c=='"'){
sb.append(c);
state=QUOT;
}else{
throw new IllegalStateException("bug! at:" + (i+1));
}
break;
}//end switch
}//end for
if(state==QUOT){
throw new IllegalStateException("open quot");
}
record.add(sb.toString());
String[] tokens = new String[record.size()];
for(int i=0;i<record.size();i++){
tokens[i]=record.get(i);
}
return tokens;
}
}
import java.util.ArrayList;
public class CSVTokenizer{
static final int INIT = 0;
static final int NOQUOT = 1;
static final int QUOT = 2;
static final int QUOT_ESCAPE = 3;
public static String[] parse(String aLine){
ArrayList<String> record = new ArrayList<String>();
StringBuilder sb = new StringBuilder();
int state = INIT;
for(int i=0;i<aLine.length();i++){
char c=aLine.charAt(i);
switch(state){
case INIT:
if(c==','){
record.add("");
}else if(c=='"'){
state=QUOT;
}else{
state=NOQUOT;
sb.append(c);
}
break;
case NOQUOT:
if(c==','){
record.add(sb.toString());
sb=new StringBuilder();
state = INIT;
}else if(c=='"'){
throw new IllegalStateException("bad quot at:" + (i+1));
}else{
sb.append(c);
}
break;
case QUOT:
if(c=='"'){
if(i==aLine.length()-1){
state=NOQUOT;
}else{
if(aLine.charAt(i+1)=='"'){
state=QUOT_ESCAPE;
}else if(aLine.charAt(i+1)==','){
state=NOQUOT;
}else{
throw new IllegalStateException("bad quot at:" + (i+1));
}
}
}else{
sb.append(c);
}
break;
case QUOT_ESCAPE:
if(c=='"'){
sb.append(c);
state=QUOT;
}else{
throw new IllegalStateException("bug! at:" + (i+1));
}
break;
}//end switch
}//end for
if(state==QUOT){
throw new IllegalStateException("open quot");
}
record.add(sb.toString());
String[] tokens = new String[record.size()];
for(int i=0;i<record.size();i++){
tokens[i]=record.get(i);
}
return tokens;
}
}
呼び出し側のプログラム
このCSVTokenizerの使い方の例を次に示します。import jp.veltec.util.CSVTokenizer;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.IOException;
public class CSVTokenizerTest{
public static void main(String[] args){
try{
FileInputStream fis = new FileInputStream(args[0]);
InputStreamReader isr = new InputStreamReader(fis,"utf-8");
BufferedReader br = new BufferedReader(isr);
while(br.ready()){
String mLine = br.readLine();
String tokens[] = CSVTokenizer.parse(mLine);
for(String token:tokens){
System.out.println(token);
}
System.out.println("-------");
}
}catch(IOException e){
e.getMessage();
}
}
}
このテストプログラムでは、csvファイルを1行ずつ読み込んでCSVTokenizerクラスのparseメソッドに渡し、Stringの配列を受け取っています。CSVファイルの内容が次のような場合import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.IOException;
public class CSVTokenizerTest{
public static void main(String[] args){
try{
FileInputStream fis = new FileInputStream(args[0]);
InputStreamReader isr = new InputStreamReader(fis,"utf-8");
BufferedReader br = new BufferedReader(isr);
while(br.ready()){
String mLine = br.readLine();
String tokens[] = CSVTokenizer.parse(mLine);
for(String token:tokens){
System.out.println(token);
}
System.out.println("-------");
}
}catch(IOException e){
e.getMessage();
}
}
}
aaa,bbb,ccc
"aaa","""bbb""","c""c""c"
"aa,a",bbb,ccc
,bbb,
出力は次のようになります。"aaa","""bbb""","c""c""c"
"aa,a",bbb,ccc
,bbb,
aaa
bbb
ccc
-------
aaa
"bbb"
c"c"c
-------
aa,a
bbb
ccc
-------
bbb
-------
状態や処理内容を増やしていくと、独自のプログラミング言語なども作れるわけですが、言語の設計はそんなに簡単な話ではなく、単純にやろうとするとif文の嵐になってしまいます。ただ、この方法を応用して日本語を処理するプログラムを書けば面白ボットが作れるかも知れません。bbb
ccc
-------
aaa
"bbb"
c"c"c
-------
aa,a
bbb
ccc
-------
bbb
-------
CSVTokenizerのソースを貼っておきます。
投稿:竹形 誠司[takegata]/2011年 05月 07日 19時 06分
/更新:2011年 05月 19日 23時 01分