技術とか戦略とか

IT技術者が技術や戦略について書くブログです。

java:ファイルを1バイトずつ読み込んで解釈する例

 ファイルを1バイト/1文字ずつ読み書きすることでファイルの中身を解釈しながら処理することができる、というのは以前の記事で書いた通りです。
今回の記事では、複雑なフォーマットのファイルを例に出して説明したいと思います。
 
----
 
今回の例では、以下のCSVファイルがインプットであると仮定します。
要件は、2項目目のみを抜き出して表示する、とします。
 
【ファイルフォーマット】
1項目目:連番
2項目目:コメント(半角カンマは全角カンマにエスケープ済み、改行文字を含む)
3項目目:"END"+改行文字(CRLF)固定
 
項目中に改行文字が含まれるため、単純に1行読んでカンマで分割するという方法では処理できないのですが、先頭から1バイト(1文字)ずつ読み込むことで処理が可能になります。
 
----
 
【サンプルコード】
・FileReadWriteMain2.java
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class FileReadWriteMain2 {

  public static void main(String[] args) {

    // 1バイトずつ読込(バイナリファイルとして処理)
    FileInputStream fis = null;
    FileOutputStream fos = null;

    try {
      // 入出力ファイルパス
      fis = new FileInputStream("C:\\tmp\\test.csv");
      fos = new FileOutputStream("C:\\tmp\\test_out.csv");

      // 状態保持
      int state = 0; // 0…1項目目読み込み中(読み飛ばし)
              // 1…2項目目読み込み中(出力)
              // 2…3項目目読み込み中(読み飛ばし)

      // 3項目目文字列
      StringBuilder sb = new StringBuilder();

      // 定数群
      // UTF-8SHIFT_JISEUC-JPではマルチバイト文字で0x2cは使わない
      // そのため、判定時にマルチバイト文字の考慮は不要
      final String commaStrHex = "2c"; // カンマ
      final String endStrHex = "454e440d0a"; // END + CRLF

      // データ読み込みループ
      int data;
      while ( (data = fis.read() ) != -1){

        // 条件判定のため、読み込んだ1バイトのデータを変換
        // 10進数 → 16進数
        String hex = Integer.toHexString(data);
        if (hex.length() == 1) {
          hex = "0" + hex;
        }

        // 状態毎に処理内容を変化
        if (state == 0) { // 1項目目
          // カンマが来たことを判定
          if (hex.equals(commaStrHex)) {
            state = 1; // 以降は2項目目
          }
        } else if (state == 1) { // 2項目目
          // カンマが来たことを判定
          if (hex.equals(commaStrHex)) {
            // ファイルにCRLFを書き込み(レコード終わり)
            fos.write(13);
            fos.write(10);
            state = 2; // 以降は3項目目
          } else {
            // ファイルに書き込み
            fos.write(data);
          }
        } else if (state == 2) { // 3項目目
          // 判定のため直近5バイト(16進数で10桁)を保持
          // 既に5バイト持っている場合は最初の1バイトは消す
          if (sb.length() == 10) {
            sb.delete(0, sb.length() - 8);
          }
          sb.append(hex);
          // END + CRLFが来たことを判定
          if (sb.toString().equals(endStrHex)) {
            state = 0; // 1項目目に戻る
          }
        }

      }

    // 例外処理
    } catch (IOException e) {
      e.printStackTrace();
    } finally {
      try {
        fis.close();
        fos.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }

  }

}
 
【処理結果】
・インプットファイル
0000001,コメント
です。,END
0000002,comment,コメント,END
 
・アウトプットファイル
コメント
です。
comment,コメント