« Java:CSVパーサを作る(その2) - RFC4180対応 前編 | トップページ | Mac OS X Server:DNS設定の問題 »

Java:CSVパーサを作る(その3) - RFC4180対応 後編

前回の記事で未実装だった「レコードの確定」と「レコードのフィールドへの分割」を行うメソッドについて実装を行い,CSVパーサを完成させます。

【実装の考察】
●レコードの確定
レコード確定では,入力テキストデータに対して,ダブルクォーテーション(二重引用符)のペアをヒントに各レコードの末尾を確定して,レコードの切り分けを行います。処理手順は以下のようになります。

  1. BufferedReaderのreadLineメソッドを使ってテキストを1行分(現在の位置から改行が現れるまで,またはファイルの終了まで)取り出して,行の先頭からダブルクォーテーションを探す。見つからなければその1行を1レコードとして確定する。(readLineメソッドは「CR」「LF」「CRLF」を改行と認識するので,「CRLF以外の改行も考慮する」仕様の要求を満たしています。)
  2. ダブルクォーテーションが見つかった場合,ペアになる後ろのダブルクォーテーションを探す。後ろのダブルクォーテーションが見つかったらその位置から後続のダブルクォーテーションのペアを探す。この手順を行の終わりまで繰り返す。ダブルクォーテーションペアの外側で行が終了していれば,その行を1レコードとして確定する。
  3. ペアの後ろのダブルクォーテーションが見つからずにダブルクォーテーションペアの内側で改行に達したら,その改行を文字列フィールドに含まれる改行と見なしてBufferedReaderのreadLineメソッドより次の行を取り出して前の行と連結し,ペアの後ろのダブルクォーテーションを探すところから処理から継続する。これをダブルクォーテーションペアの外側で改行が見つかるまで繰り返す。後ろのダブルクォーテーションが見つからずにファイルの末尾に達したときは,ファイルの末尾にダブルクォーテーションを付加して行の末尾とする。この連結した行を1レコードとして確定する。
以上の処理を行うコードは次のようになります。(2009年6月22日改訂)
//------------------------------------------------------------------
/**
 * BufferedReaderから1レコード分のテキストを取り出す。
 * @param reader 行データを取り出すBufferedReader。
 * @return 1レコード分のテキスト。
 * @throws IOException 入出力エラー
 */
private String buildRecord (
  BufferedReader reader)
  throws IOException
{
  String  result = reader.readLine();
  int    pos;
  if (result != null && 0 < result.length() &&
    0 <= (pos = result.indexOf("\"")))
  {
    boolean  inString = true;
    String   rawline = result;
    String   newline = null;
    StringBuffer buff = new StringBuffer(1024);
    while (true) {
      while (0 <= (pos = rawline.indexOf("\"", ++pos))) {
        inString = !inString;
      }
      if (inString && (newline = reader.readLine()) != null) {
        buff.append(rawline);
        buff.append("\n");
        pos = -1;
        rawline = newline;
        continue;
      }
      else {
        if (inString || 0 < buff.length()) {
          buff.append(rawline);
          if (inString) {
            buff.append("\"");
          }
          result = buff.toString();
        }
        break;
      }
    }
  }
  return result;
}
注意)上記コードでは,フィールド内の改行をLF("\n")に決め打ちしていますが,実際にはCSVパーサの出力結果を受け取るプログラム(データベースなど)が要求する改行コードを挿入する必要があります。
使用すべき改行コードの選択は,プログラムが稼動するプラットフォームの改行コードを取得する
「String returnStr = System.getProperty("line.separator");」
が知られていますが,実行環境によってはCSVパーサの出力結果を受け取るプログラムがCSVパーサとは異なるOSで稼動する場合もありうるので,上記コードで取得できる改行コードを常に適用できるとは限りません。
そのため,実コードでは使用する改行コードをプロパティファイルで設定できるようにするなど動作環境に合わせて変更できる仕組みを作る必要があります。

●レコードのフィールドへの分割
フィールド分割では,レコードに切り分けたテキストに対して,最初にレコード全体をカンマで分割し,分割した個々の文字列にダブルクォーテーションをヒントに必要な連結やエスケープ処理を行って,個々のフィールドを確定します。処理手順は以下のようになります。
  1. レコード全体をStringクラスのsplitメソッドを使ってカンマで分割し,分割した個々の文字列データを順に先頭からダブルクォーテーションを探す。見つからなければその文字列は1フィールドとして確定する。
  2. ダブルクォーテーションが見つかったら,次のダブルクォーテーションを探す。次のダブルクォーテーションの直後にダブルクォーテーションがあれば,エスケープされたダブルクォーテーションとして処理し,そうでなければフィールドの終わりと見なす。
  3. フィールドで後ろのダブルクォーテーションが見つからない場合,フィールドに含まれるカンマでsplitメソッドが分割したものと見なして,フィールドの後ろに(splitメソッドが削除した)カンマと次のフィールドを連結する。
  4. フィールドの開始と終了のダブルクォーテーションは削除する。
以上の処理を行うコードは次のようになります。(2009年6月22日改訂)
//------------------------------------------------------------------
/**
 * 1レコード分のテキストを分割してフィールドの配列にする。
 * @param src 1レコード分のテキストデータ。
 * @param dest フィールドの配列の出力先。
 */
private void splitRecord (
  String    src,
  LinkedList dest)
{
  String[]  columns = src.split(",");
  int     maxlen = columns.length;
  int     startPos, endPos, columnlen;
  StringBuffer buff = new StringBuffer(1024);
  String   column;
  boolean  isInString, isEscaped;

  for (int index = 0; index < maxlen; index++) {
    column = columns[index];
    if ((endPos = column.indexOf("\"")) < 0) {
      dest.addLast(column);
    }
    else {
      isInString = (endPos == 0);
      isEscaped = false;
      columnlen = column.length();
      buff.setLength(0);
      startPos = (isInString)? 1: 0;
      while (startPos < columnlen) {
        if (0 <= (endPos = column.indexOf("\"", startPos))) {
          buff.append((startPos < endPos)?
                column.substring(startPos, endPos): isEscaped? "\"": "");
          isEscaped = !isEscaped;
          isInString = !isInString;
          startPos = ++endPos;
        }
        else {
          buff.append(column.substring(startPos));
          if (isInString && index < maxlen - 1) {
            column = columns[++index];
            columnlen = column.length();
            buff.append(",");
            startPos = 0;
          }
          else {
            break;
          }
        }
      }
      dest.addLast(buff.toString());
    }
  }
}


以上で「RFC4180対応のCSVパーサ」はひととおりできあがったかと思います。

※上記コードでは,整形のため全角スペースを使用している部分があります。
【著作権表記】上記コードを含む本ブログのプログラムコードは,私的利用可,商用利用可,改変しての利用可です。利用の際に作者に許諾を得る必要はありません。

■関連書籍をAmazonで検索:[Java]
ソースコードリーディングから学ぶ Javaの設計と実装



にほんブログ村 IT技術ブログへ にほんブログ村 IT技術ブログ プログラム・プログラマへ 人気ブログランキングへ ←この記事が役に立ったという方はクリックお願いします。
▼CSVパーサを作る[][][その3]
Wave SoundTouch music system IV

|

« Java:CSVパーサを作る(その2) - RFC4180対応 前編 | トップページ | Mac OS X Server:DNS設定の問題 »

プログラミング」カテゴリの記事

Java」カテゴリの記事

トラックバック


この記事へのトラックバック一覧です: Java:CSVパーサを作る(その3) - RFC4180対応 後編:

« Java:CSVパーサを作る(その2) - RFC4180対応 前編 | トップページ | Mac OS X Server:DNS設定の問題 »