プログラマメモ2 - programmer no memo2

いまだにCSVその2 2010/09/18

Javaです。CSVを処理します。
とりあえず、JUnit4(使ったことがなかったので)でテスト用意して、自分のニーズに対応しているだろうことを確認。
これでCSVから離れられる....
テスト用データ、ソースは全部、コミット。
CSVUtils.java - quicklunch - Project Hosting on Google Code

package quicklunch.e2.goodies.utils;

import static org.junit.Assert.*;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

import org.junit.Test;

public class CSVUtilsTest {

static InputStream getStream(String filename) {
return CSVUtilsTest.class.getResourceAsStream("doc-files/csv/"
+ filename);
}

@Test
public void testParse001() throws IOException {
// fail("Not yet implemented");
CSVUtils.parse(getStream("001.csv"), new CSVUtils.AbstractExecutor() {
@Override
public void exec(long row, List<String> line) {
assertEquals(0, row);
assertEquals(3, line.size());
}
});

CSVUtils.parse(getStream("002.csv"), new CSVUtils.AbstractExecutor() {
@Override
public void exec(long row, List<String> line) {
assertEquals(0, row);
assertEquals(5, line.size());
assertEquals("", line.get(0));// empty field
assertEquals("bbb", line.get(1));
assertEquals("", line.get(2));
assertEquals("ccc", line.get(3));
assertEquals("", line.get(4));
}
});

CSVUtils.parse(getStream("003.csv"), new CSVUtils.AbstractExecutor() {
@Override
public void exec(long row, List<String> line) {

if (0 == row) {
assertEquals(3, line.size());
assertEquals("aaa", line.get(0));
assertEquals("bbb", line.get(1));
assertEquals("ccc", line.get(2));
}

if (1 == row) {
assertEquals(4, line.size());
assertEquals("000", line.get(0));
assertEquals(" 111 ", line.get(1));
// space
assertEquals(" 222", line.get(2));
// space
assertEquals("333 ", line.get(3));
}
}
});
}

@Test
public void testParse002() throws IOException {
// fail("Not yet implemented");
CSVUtils.parse(getStream("004.csv"), new CSVUtils.AbstractExecutor() {
@Override
public void exec(long row, List<String> line) {
if (row == 0) {
// empty line
assertEquals(0, line.size());
}
if (row == 1) {
assertEquals(1, line.size());
// space
assertEquals(" ", line.get(0));
}

if (row == 2) {
assertEquals(2, line.size());
assertEquals("", line.get(0));// empty field
assertEquals("", line.get(1));// empty field
}

if (row == 3) {
assertEquals(2, line.size());
assertEquals(" ", line.get(0));// space
assertEquals("", line.get(1));// empty field
}

if (row == 4) {
// empty line
assertEquals(0, line.size());
}

if (row == 5) {
assertEquals(1, line.size());
assertEquals(" ", line.get(0));// space
}
}
});
}

@Test
public void testParse003() throws IOException {
// fail("Not yet implemented");
{
long n = CSVUtils.parse("日本語", new CSVUtils.AbstractExecutor() {
@Override
public void exec(long row, List<String> line) {
assertEquals(0, row);
assertEquals(1, line.size());
assertEquals("日本語", line.get(0));
}
});
assertEquals(1, n);
}
{
long n = CSVUtils.parse("", new CSVUtils.AbstractExecutor() {
@Override
public void exec(long row, List<String> line) {
assertEquals(0, row);
assertEquals(0, line.size());
}
});
assertEquals(0, n);
}
{
long n = CSVUtils.parse("日本語\n", new CSVUtils.AbstractExecutor() {
@Override
public void exec(long row, List<String> line) {
if (row == 0) {
assertEquals(1, line.size());
assertEquals("日本語", line.get(0));
}
}
});
assertEquals(1, n);
}
}

@Test
public void testParse004() throws IOException {
{
// encode
long n = CSVUtils.parse(getStream("005_utf8_crlf.csv"),
new CSVUtils.AbstractExecutor() {
@Override
public void exec(long row, List<String> line) {

if (0 == row) {
assertEquals(5, line.size());
assertEquals("日本語1", line.get(0));
assertEquals("日本語2", line.get(1));
assertEquals("日本語3", line.get(2));
assertEquals(" 日本語4", line.get(3));// space
assertEquals("日本語5 ", line.get(4));// space
}

if (1 == row) {
assertEquals(5, line.size());
assertEquals("dd\r\neeee", line.get(0));
assertEquals("fff", line.get(1));
assertEquals(" gg ", line.get(2));
assertEquals("", line.get(3));// epmty field
assertEquals("hhh", line.get(4));
}

if (2 == row) {
assertEquals(5, line.size());
assertEquals("aaa", line.get(0));
assertEquals("bbb\r\nッッ", line.get(1));
assertEquals("ccc", line.get(2));
assertEquals("dddd", line.get(3));
assertEquals("", line.get(4));// epmty field
}
}
}, "utf8");

assertEquals(3, n);
}
}

}



package quicklunch.e2.goodies.utils;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PushbackReader;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;


/**
*
*
* @author nakawakashigeto
*
*/
public abstract class CSVUtils {

public interface IExecutor {
public void pre();

/**
*
* @param row
* start 0
* @param line
*/
public void exec(long row, List<String> line);

public void post();
}

abstract static public class AbstractExecutor implements IExecutor {
public void pre() {
}

public void exec(long row, List<String> line) {
}

public void post() {
}
}// end

// ===================

public enum TT {
EOF("EOF"), FIELD("FIELD"), COMMA("COMMA"), CRLF("CRLF"), CR("CR"), LF(
"LF");

String s;

TT(String s) {
this.s = s;
}

public String toString() {
return s;
}
}

/**
*
* @author nakawakashigeto
*
*/
public static class Token {
TT type;
public StringBuilder val = new StringBuilder();

public Token build(TT type) {
this.type = type;
return this;
}

public void append(int ch) {
this.val.append((char) ch);
}

public void append(String s) {
this.val.append(s);
}

public String toString() {
return "T:[" + type + "] V:[" + val + "]";
}
}

/**
*
* @author nakawakashigeto
*
*/
public static class CSVTokenizer {

PushbackReader reader;

static final int DQUOTE = '"';
static final int QUOTE = '\'';
static final int COMMA = ',';
static final int EOF = -1;
static final int CR = '\r';
static final int LF = '\n';

/* STATE */
static final int ST_nonescaped = 1;
static final int ST_escaped = 2;
static final int ST_escaped_single_quote = 3;

public CSVTokenizer(String s) {
this.reader = new PushbackReader(new BufferedReader(
new StringReader(s)));
}

public CSVTokenizer(InputStream inputStream) {
this.reader = new PushbackReader(new BufferedReader(
new InputStreamReader(inputStream)));
}

public CSVTokenizer(InputStream inputStream, String charasetname)
throws UnsupportedEncodingException {
this.reader = new PushbackReader(new BufferedReader(
new InputStreamReader(inputStream, charasetname)));
}

public Token token() throws IOException {

int state = 0;

Token token = new Token();
loop: while (true) {
int ch = read();

switch (state) {
case 0:
/*
* -- START --
*/
if (ch == EOF) {
return token.build(TT.EOF);
}

// dpuble quote
if (ch == DQUOTE) {
state = ST_escaped;
token.type = TT.FIELD;
break;
}

// single quote
if (ch == QUOTE) {
state = ST_escaped_single_quote;
token.type = TT.FIELD;
break;
}

if (ch == COMMA) {
token.append(ch);
return token.build(TT.COMMA);
}

if (ch == CR) {
ch = read();
if (ch == LF) {
// default CRLF
return token.build(TT.CRLF);
}

// suport CR
unread(ch);
return token.build(TT.CR);
}

// suport LF
if (ch == LF) {
return token.build(TT.LF);
}

state = ST_nonescaped;
token.type = TT.FIELD;
case ST_nonescaped:
/*
* -- non-escaped --
*/
if (ch == EOF || ch == CR || ch == LF || ch == DQUOTE) {
unread(ch);
return token;
}

if (!isTextdata(ch)) {
unread(ch);
return token;
}

token.append(ch);
break;
case ST_escaped:
/*
* -- escaped(double quote) --
*/

if (ch == EOF) {
return token.build(TT.FIELD);
}

// 2DQUOTE
if (ch == DQUOTE) {
ch = read();
if (ch == DQUOTE) {
token.append("\"");
state = ST_escaped;
break;
}
unread(ch);
return token;
}

token.append(ch);
break;

case ST_escaped_single_quote:
/*
* -- escaped(single quote) --
*/
if (ch == EOF) {
return token.build(TT.FIELD);
}

// 2DQUOTE
if (ch == QUOTE) {
ch = read();
if (ch == QUOTE) {
token.append("\'");
state = ST_escaped_single_quote;
break;
}
unread(ch);
return token;
}

token.append(ch);
break;

default:
break loop;
}
}

return token;
}

boolean isTextdata(int ch) {
if (notEq(ch, '\r') && notEq(ch, '\n') && notEq(ch, '"')
&& notEq(ch, ',')) {
return true;
}
return false;
}

int read() throws IOException {
if (reader != null)
return reader.read();
return -1;
}

boolean notEq(int l, int r) {
return (l != r);
}

void unread(int ch) throws IOException {
if (reader != null && ch != -1) {
reader.unread(ch);
}
}

public void close() {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
}
}
}
} // end

public static long parse(InputStream inputStream, IExecutor executor,
String charasetname) throws IOException {
long row = 0;

executor.pre();

try {
CSVTokenizer tokenizer = new CSVTokenizer(inputStream, charasetname);
CSVUtils.Token token = null;
// one previous token
CSVUtils.Token preToken = null;

do {
List<String> line = new ArrayList<String>();

while ((token = tokenizer.token()) != null
&& !(token.type == TT.EOF || token.type == TT.CRLF
|| token.type == TT.CR || token.type == TT.LF)) {
// check empty field.
if ((preToken == null || preToken.type == TT.COMMA)
&& token.type == TT.COMMA) {
line.add("");// empty field...
preToken = token;
continue;
}

if (token.type == TT.COMMA) {
preToken = token;
continue;// skip comma
}
line.add(token.val.toString());
preToken = token;
}

// ignore empty line
if (preToken == null && token.type == TT.EOF) {
break;
}

// check empty field.
if ((preToken != null && preToken.type == TT.COMMA)
&& (token.type == TT.EOF || token.type == TT.CRLF
|| token.type == TT.CR || token.type == TT.LF)) {
line.add("");// empty field...
}

executor.exec(row++, line);
preToken = null;
} while (token != null && token.type != TT.EOF);
} finally {
executor.post();
}

return row;
}

public static long parse(InputStream inputStream, IExecutor executor)
throws IOException {
return parse(inputStream, executor, System.getProperty("file.encoding"));
}

public static long parse(String s, IExecutor executor) throws IOException {

InputStream inputStream = new ByteArrayInputStream(s.getBytes("utf-8"));

return parse(inputStream, executor, "utf-8");
}

}

いまだにCSVです。 2010/09/17

Javaです。いまだにCSVです。
以前つくりかけていたものが、なんかいろいろだめだったので少しずつですが、テストして試してます。
あと、RFCだけに対応させるとCSVのパーサーとしては使いづらいものになってしまうかなと考えてたりしてます。
あと、osx上のエクセルでcsvで保存するとCRLFでは保存されていないことに気がついて....

とりあえず、コミット。



以下、ちょっと工夫してます。
以前から実装したいアイデアがあって、まあ単純にCSVの行ごとに処理をしやすいようにしたいということなのですが、
まあ、なんとなく実装できてるかなと。
手書きのパーサで、依存するものを極力少なくしておいて、ソースをひとつにしておくと、個人で使う場合に便利なことが多いので、ひとつのソースにまとめてます。
package quicklunch.e2.goodies.utils;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PushbackReader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;

public abstract class CSVUtils {

public interface IExecutor {
public void pre();

public void exec(List<String> line);

public void post();
}

abstract static public class AbstractExecutor implements IExecutor {
public void pre() {
}

public void exec(List<String> line) {
}

public void post() {
}
}

// ===================

public enum TT {
EOF("EOF"), FIELD("FIELD"), COMMA("COMMA"), CRLF("CRLF"), CR("CR"), LF(
"LF");

String s;

TT(String s) {
this.s = s;
}

public String toString() {
return s;
}
}

public static class Token {
TT type;
public StringBuilder val = new StringBuilder();

public Token build(TT type) {
this.type = type;
return this;
}

public void append(int ch) {
this.val.append((char) ch);
}

public void append(String s) {
this.val.append(s);
}

public String toString() {
return "T:[" + type + "] V:[" + val + "]";
}
}

public static class CSVTokenizer {

PushbackReader reader;

static final int DQUOTE = '"';
static final int QUOTE = '\'';
static final int COMMA = ',';
static final int EOF = -1;
static final int CR = '\r';
static final int LF = '\n';

/* STATE */
static final int ST_nonescaped = 1;
static final int ST_escaped = 2;
static final int ST_escaped_single_quote = 3;

public CSVTokenizer(String s) {
this.reader = new PushbackReader(new BufferedReader(
new StringReader(s)));
}

public CSVTokenizer(InputStream inputStream) {
this.reader = new PushbackReader(new BufferedReader(
new InputStreamReader(inputStream)));
}

public Token token() throws IOException {

int state = 0;

Token token = new Token();
loop: while (true) {
int ch = read();

switch (state) {
case 0:
/*
* -- START --
*/
if (ch == EOF) {
return token.build(TT.EOF);
}

// dpuble quote
if (ch == DQUOTE) {
state = ST_escaped;
token.type = TT.FIELD;
break;
}

// single quote
if (ch == QUOTE) {
state = ST_escaped_single_quote;
token.type = TT.FIELD;
break;
}

if (ch == COMMA) {
// empty field
token.append(ch);
return token.build(TT.COMMA);
}

if (ch == CR) {
ch = read();
if (ch == LF) {
// default CRLF
return token.build(TT.CRLF);
}

// suport CR
unread(ch);
return token.build(TT.CR);
}

// suport LF
if (ch == LF) {
return token.build(TT.LF);
}

state = ST_nonescaped;
token.type = TT.FIELD;
case ST_nonescaped:
/*
* -- non-escaped --
*/
if (ch == EOF || ch == CR || ch == LF || ch == DQUOTE) {
unread(ch);
return token;
}

if (!isTextdata(ch)) {
unread(ch);
return token;
}

token.append(ch);
break;
case ST_escaped:
/*
* -- escaped(double quote) --
*/

if (ch == EOF) {
return token.build(TT.FIELD);
}

// 2DQUOTE
if (ch == DQUOTE) {
ch = read();
if (ch == DQUOTE) {
token.append("\"");
state = ST_escaped;
break;
}
unread(ch);
return token;
}

token.append(ch);
break;

case ST_escaped_single_quote:
/*
* -- escaped(single quote) --
*/
if (ch == EOF) {
return token.build(TT.FIELD);
}

// 2DQUOTE
if (ch == QUOTE) {
ch = read();
if (ch == QUOTE) {
token.append("\'");
state = ST_escaped_single_quote;
break;
}
unread(ch);
return token;
}

token.append(ch);
break;

default:
break loop;
}
}

return token;
}

boolean isTextdata(int ch) {
if (notEq(ch, '\r') && notEq(ch, '\n') && notEq(ch, '"')
&& notEq(ch, ',')) {
return true;
}
return false;
}

int read() throws IOException {
if (reader != null)
return reader.read();
return -1;
}

boolean notEq(int l, int r) {
return (l != r);
}

void unread(int ch) throws IOException {
if (reader != null && ch != -1) {
reader.unread(ch);
}
}

public void close() {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
}
}
}
}

/**
*
*
* @param inputStream
* @return
*/
public static CSVTokenizer tokenizer(InputStream inputStream) {
return new CSVTokenizer(inputStream);
}

public static IExecutor tokenize(InputStream inputStream,
IExecutor executor) throws IOException {
executor.pre();
try {
CSVTokenizer tokenizer = new CSVTokenizer(inputStream);
CSVUtils.Token token = null;
do {
List<String> line = new ArrayList<String>();

while ((token = tokenizer.token()) != null
&& !(token.type == CSVUtils.TT.EOF || token.type == CSVUtils.TT.CRLF
|| token.type == CSVUtils.TT.CR || token.type == TT.LF)) {
if (token.type == TT.COMMA)
continue;// skip comma
line.add(token.val.toString());
}

executor.exec(line);

} while (token != null && token.type != TT.EOF);
} finally {
executor.post();
}

return executor;
}
}

RFC 2234風パーサーを目指してantlrで定義 その2 2008/09/13

CSVの処理系って以外と適当につくっているところが多いのかなと。
Javaならカンマで適当にStringのsplitを使うかStringTokenizerとかでやったりしている人が多いかも。
それで改行コードがフィールドに入ってたりすると、あわてたりするわけで。Orz...

ANTLR使って、CSVをListのListにして返すように定義してみました。

CSVのヘッダーってわざわざ定義する必要がないですよね。

呼び出しコード

package csv;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;

import org.antlr.runtime.ANTLRInputStream;
import org.antlr.runtime.CommonTokenStream;
import org.antlr.runtime.RecognitionException;

public class Test2 {

public static void main(String[] args) throws IOException,
RecognitionException, URISyntaxException {
URL url = Test2.class.getResource("test.csv");
FileInputStream fileInputStream = new FileInputStream(new File(url
.toURI()));
ANTLRInputStream inputStream = new ANTLRInputStream(fileInputStream);
CsvLexer lexer = new CsvLexer(inputStream);
CommonTokenStream tokens = new CommonTokenStream(lexer);
CsvParser parser = new CsvParser(tokens);
System.out.println(parser.file());
}
}


ANTLRのグラマーなほうCsv.g
ヘッダーとして使う使わないかはあとから決定することにしてグラマーには定義しませんでした。
たぶんうまくいっていると思うんだ。
grammar Csv;

@header {
package csv;
}
@lexer::header {
package csv;
}
// file = [header CRLF] record *(CRLF record) [CRLF]
file returns[List list]
scope {
List l;
}
@init {
$file::l = new ArrayList();
}

:r=record{$file::l.add($r.list);} ((LF | CRLF) r=record{$file::l.add($r.list);})*{
// System.out.println("*** *** file!!");
// System.out.println($file::l);
$list = $file::l;
};
// header = name *(COMMA name)
header
: name (COMMA name)*;
// record = field *(COMMA field)
record returns[List list]
scope {
List f;
}
@init {
$record::f = new ArrayList();
}
: l=field{ $record::f.add($l.text); } (COMMA l=field{$record::f.add($l.text);})*
{
$list = $record::f;
};

// name = field
name: field;
// field = (escaped / non-escaped)
field
: escaped {
}
| nonescaped {
};
// escaped = DQUOTE *(TEXTDATA / COMMA / CR / LF / 2DQUOTE) DQUOTE
escaped returns [String var]
scope {
StringBuilder b;
}
@init {
$escaped::b = new StringBuilder();
}
: DQUOTE (TEXTDATA{ $escaped::b.append($TEXTDATA); }
| COMMA{ $escaped::b.append($COMMA); }
| CR{ $escaped::b.append($CR); }
| LF{ $escaped::b.append($LF); }
| d=DQUOTE{ $escaped::b.append($d); } DQUOTE)* DQUOTE {

$var = $escaped::b.toString();
};
// non-escaped = *TEXTDATA
nonescaped: TEXTDATA* {
};

// COMMA = %x2C
COMMA
: ',';
// CR = %x0D ;as per section 6.1 of RFC 2234 [2]
CR : '\r';
// DQUOTE = %x22 ;as per section 6.1 of RFC 2234 [2]
DQUOTE : '"';
// LF = %x0A ;as per section 6.1 of RFC 2234 [2]
LF : '\n';
// CRLF = CR LF ;as per section 6.1 of RFC 2234 [2]
CRLF: CR LF;
// TEXTDATA = %x20-21 / %x23-2B / %x2D-7E
TEXTDATA
: (~(COMMA| CR| LF| DQUOTE))+
;

RFC 2234風パーサーを目指してantlrで定義 2008/09/13
2008/09/13

ふとCSVのパーサーをつくろうと再び考え、今回はANTLRを使ってみることに。
オレオレDSLをつくってみたいのでその練習もかねて。

改行をCRLF以外にLFでもうけつけるようにしてみたことと、
トークンTEXTDATAを定義するのCOMMA 、CR、LF、DQUOTEでないものはというふうに乱暴な定義にしてみた。
これうごくのだろうか。。。
まだ動作させていない....


grammar Csv;
@header {
package csv;
}
@lexer::header {
package csv;
}
// file = [header CRLF] record *(CRLF record) [CRLF]
file: record ((LF | CRLF) record)*;
// header = name *(COMMA name)
header
: name (COMMA name)*;
// record = field *(COMMA field)
record
: field (COMMA field)*;
// name = field
name: field;
// field = (escaped / non-escaped)
field
: escaped
| nonescaped;
// escaped = DQUOTE *(TEXTDATA / COMMA / CR / LF / 2DQUOTE) DQUOTE
escaped
: DQUOTE (TEXTDATA|COMMA|CR|LF|DQUOTE DQUOTE)* DQUOTE;
// non-escaped = *TEXTDATA
nonescaped: TEXTDATA*;

// COMMA = %x2C
COMMA
: ',';
// CR = %x0D ;as per section 6.1 of RFC 2234 [2]
CR : '\r';
// DQUOTE = %x22 ;as per section 6.1 of RFC 2234 [2]
DQUOTE : '"';
// LF = %x0A ;as per section 6.1 of RFC 2234 [2]
LF : '\n';
// CRLF = CR LF ;as per section 6.1 of RFC 2234 [2]
CRLF: CR LF;
// TEXTDATA = %x20-21 / %x23-2B / %x2D-7E
TEXTDATA
: (~(COMMA| CR| LF| DQUOTE))+
;

RFC 4180対応版 CSVレコードの分解 - 精進しないと。 2007/08/14
2007/08/17

RFC 4180対応版 CSVレコードの分解 どう書く?org

あとで書く。

うーん、しばらく暇つぶしにことかかないなぁ

いろいろな人の回答みたのですが、みなさんコンパクトですね。
自分のJava版だとサイズが大きいなぁ

参考
Comma-Separated Values - Wikipedia
RFC 4180 Common Format and MIME Type for Comma-Separated Values (CSV) Files

2007-08-16 その2



アプローチはきわめてオーソドックスだと思います。
コンパイラの本とかのはじめのページあたりででてくる、字句解析の感じを目指しています。

トークンクラスです。
package csv;

public class Token {
TT type;
private StringBuffer val = new StringBuffer();

public Token build(TT type) {
this.type = type;
return this;
}

public void append(int ch) {
this.val.append((char) ch);
}

public void append(String s) {
this.val.append(s);
}

public String toString() {
return "type:[" + type + "] val:[" + val + "]";
}
}


トークンタイプのクラスです。enum使ってます。
package csv;

public enum TT {
EOF("EOF"), FIELD("FIELD"), COMMA("COMMA"), CRLF("CRLF");

String s;

TT(String s) {
this.s = s;
}

public String toString() {
return s;
}
}


CSVトークナイザーという名前にしました。
whileの部分のstateは状態遷移図に起こしやすことを目指してます。が、厳密ではないので、あとで直します。

package csv;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PushbackReader;
import java.io.StringReader;

public class CSVTokenizer {

PushbackReader reader;

static final int DQUOTE = '"';
static final int COMMA = ',';
static final int EOF = -1;
static final int CR = '\r';
static final int LF = '\n';

public CSVTokenizer(String s) {
this.reader = new PushbackReader(new BufferedReader(new StringReader(s)));
}

public CSVTokenizer(InputStream inputStream) {
this.reader = new PushbackReader(new BufferedReader(new InputStreamReader(inputStream)));
}

public Token token() throws IOException {

int state = 0;

Token token = new Token();
loop: while (true) {
int ch = read();

switch (state) {
case 0:
/*
* -- START --
*/
if (ch == EOF) {
return token.build(TT.EOF);
}

if (ch == DQUOTE) {
state = 2;
token.type = TT.FIELD;
break;
}

if (ch == COMMA) {
// empty field
token.append(ch);
return token.build(TT.COMMA);
}

if (ch == CR) {
state = 4;
break;
}

state = 1;
token.type = TT.FIELD;
// break しない
case 1:
/*
* -- non-escaped --
*/
if (ch == EOF || ch == CR || ch == LF) {
unread(ch);
return token;
}

if (!isTextdata(ch)) {
unread(ch);
return token;
}

token.append(ch);
break;
case 2:
/*
* -- escaped(double quote) --
*/

if (ch == EOF) {
return token.build(TT.FIELD);
}

if (ch == DQUOTE) {
ch = read();
if (ch == DQUOTE) {
token.append("\"");
state = 2;
break;
}
unread(ch);
return token;
}

token.append(ch);
break;

case 3:
/*
* -- escaped(single quote) --
*/

break loop;

case 4:
if (ch == LF) {
return token.build(TT.CRLF);
}
if (ch == EOF) {
return token.build(TT.EOF);
}
default:
break loop;
}
}

return token;
}

boolean isTextdata(int ch) {
if (notEq(ch, '\r') && notEq(ch, '\n') && notEq(ch, '"')
&& notEq(ch, ',')) {
return true;
}
return false;
}

int read() throws IOException {
if (reader != null)
return reader.read();
return -1;
}

boolean notEq(int l, int r) {
return (l != r);
}

void unread(int ch) throws IOException {
if (reader != null && ch != -1) {
reader.unread(ch);
}
}

public void close() {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
}

}
}
}


テスト書かないときちんと動いているかわからないですね。

2007-08-16 その1


最初の試み。その2をあとで書こう。全然厳密ではない。
まず、トークンに分けて、トークンを処理するという流れで。
package csv;

import java.io.IOException;
import java.io.PushbackReader;
import java.io.StringReader;

public class A {

public static void main(String[] args) throws IOException {
a();
}

public static void a() throws IOException {
String s;
CSVTokenizer tokenizer = new CSVTokenizer("aaa");
print(tokenizer);
System.out.println("-------------");
tokenizer = new CSVTokenizer("aaa,bbb,ccc");
System.out.println("-------------");
print(tokenizer);
tokenizer = new CSVTokenizer("aaa,bbb,ccc\r\n");
System.out.println("-------------");
print(tokenizer);
tokenizer = new CSVTokenizer(" aaa , bbb , ccc \r\n");
System.out.println("-------------");
print(tokenizer);
tokenizer = new CSVTokenizer("日本語, bbb , ccc \r\n日本語2, bbb2 , ccc2");
System.out.println("-------------");
print(tokenizer);
System.out.println("------------- check double quote.");
tokenizer = new CSVTokenizer("\"日本語\",\" bbb \",\" ccc \r\n日本語2\", bbb2 , ccc2");
print(tokenizer);
System.out.println("------------- ");
s = "\"日本語\",\" bbb \",\" ccc \r\n日本語2\", bbb2 , ccc2\r\n二行目aaa,二行目 bbb, \"二行目ccc \"\"OK OK \"";
System.out.println(s);
tokenizer = new CSVTokenizer(s);
print(tokenizer);
}

static void print(CSVTokenizer tokenizer) throws IOException {
Token token = null;
do {
token = tokenizer.token();
System.out.println(token);
} while (token != null && token.type != TT_EOF);

}

static final int TT_EOF = -1;
static final int TT_FIELD = 0;
static final int TT_COMMA = 1;
static final int TT_CRLF = 2;

// --
static final int DQUOTE = '"';
static final int COMMA = ',';
static final int EOF = -1;
static final int CR = '\r';
static final int LF = '\n';

static class Token {
int type;
private StringBuffer val = new StringBuffer();

public Token build(int type) {
this.type = type;
return this;
}

public void append(int ch) {
this.val.append((char) ch);
}

public void append(String s) {
this.val.append(s);
}

public String toString() {
return "type:[" + type + "] val:[" + val + "]";
}
}

static class CSVTokenizer {
PushbackReader reader;

public CSVTokenizer(String s) {
this.reader = new PushbackReader(new StringReader(s));
}

public Token token() throws IOException {

int state = 0;

Token token = new Token();
loop: while (true) {
int ch = read();
// if(ch == -1) System.exit(0);

switch (state) {
case 0:
/*
* -- START --
*/

if (ch == EOF) {
return token.build(TT_EOF);
}

if (ch == DQUOTE) {
state = 2;
token.type = TT_FIELD;
break;
}

if (ch == COMMA) {
// empty field
token.append(ch);
return token.build(TT_COMMA);
}

if (ch == CR) {
state = 4;
break;
}

state = 1;
token.type = TT_FIELD;
// break しない
case 1:
/*
* -- non-escaped --
*/
if (ch == EOF || ch == CR || ch == LF) {
unread(ch);
return token;
}

if (!isTextdata(ch)) {
unread(ch);
return token;
}

token.append(ch);
break;
case 2:
/*
* -- escaped(double quote) --
*/
if (ch == DQUOTE) {
ch = (char) reader.read();
if (ch == DQUOTE) {
token.append("\"");
state = 2;
break;
}

unread(ch);
return token;
}

if (ch == EOF) {
unread(ch);
return token.build(TT_FIELD);
}

token.append(ch);
break;

case 3:
/*
* -- escaped(single quote) --
*/

break loop;

case 4:
if (ch == LF) {
return token.build(TT_CRLF);
}
if (ch == EOF) {
return token.build(TT_EOF);
}
default:
break loop;
}
}

return token;
}

boolean isTextdata(int ch) {
if (not(ch, '\r') && not(ch, '\n') && not(ch, '"') && not(ch, ',')) {
return true;
}
return false;
}

int read() throws IOException{
if(reader != null) return reader.read();
return -1;
}
boolean not(int l, int r) {
return (l != r);
}

void unread(int ch) throws IOException{
if(reader != null && ch != -1){
reader.unread(ch);
}
}
}
}