CSVをTSVへ変換するプログラム

前回はCSVデータをTSVデータに変換するプログラムを作成した。このプログラムは主に次の3つのファイルで構成されている。

ファイル 内容
util_file.c ファイル関連の処理を担当。今回は、ファイルをメモリ上に文字列として保持する処理を実装
util_csv.c CSV関連の処理を担当。今回は、メモリ上のCSVデータ(文字列)をTSVデータ(文字列)へ変換する処理を実装
main.c util_file.cとutil_csv.cの処理を使って、CSVファイルをTSVデータへ変換して標準出力へ出力する

util_file.cにファイル関連の処理を書いていき、util_csv.cにCSV関連の処理を書いてく、といった感じだ。main.cからこの2つの処理を呼び出して、CSVファイルをTSVデータに変換して標準出力で出力するといった内容にしてある。

主な関数は次のとおりだ。

ファイル 関数と内容
util_file.c file2str() - ファイルの中身を文字列としてメモリ上へ読み込む。
util_csv.c csv2tsv() - CSVデータ(文字列)をTCVデータ(文字列)へ変換する。

それぞれの実装を見ていこう。まず、util_file.cは次のようになっている。

util_file.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>

char *file2str(const char *filepath) {
  struct stat st;
  int filesize, c;

  char *buf, *p;

  FILE *fp;

  stat(filepath, &st);
  filesize = st.st_size;

  buf = calloc(filesize + 1, sizeof(char));
  p = buf;

  fp = fopen(filepath, "r");
  for (int i = 0; i < filesize; i++) {
    c = fgetc(fp);
    if (EOF == c) {
      break;
    }
    *p = (char)c;
    ++p;
  }

  return buf;
}

この実装に関する説明は不要だと思う。file2str()では引数にパスを取っており、これをfopen(2)システムコールで開いてfgetc()で1文字づつ読み込んでいるだけだ。指定されたファイルの中身をchar型のデータとしてすべてメモリへコピーしている。

CSVデータをTSVデータへ変換する実装であるutil_csv.cは次のようになっている。

util_csv.c

#include <stdbool.h>

static bool record_outputed;

static char gettsvchar(const char);

int csv2tsv(const char *ibuf, int ibufsize, char *obuf, int obufsize) {
  // When the target is empty, no processing is done.
  if (0 == ibufsize)
    return 0;

  const char *p_i, *end_i;
  char *p_o;
  int tsv_len = 0;

  p_i = ibuf;
  end_i = &ibuf[ibufsize - 1];

  p_o = obuf;

  // Indicates the state during parsing.
  typedef enum FIELD_STATUS {
    FIELD_END,
    IN_FIELD,
    IN_QUOTED_FIELD
  } record_status;
  record_status rs = FIELD_END;

  record_outputed = false;
  while (1) {
    if ('\n' == *p_i) {
      if (!record_outputed) {
        // nothing
      }
      rs = FIELD_END;
      *p_o = gettsvchar('\n');
      ++p_o;
      ++tsv_len;
    } else {
      switch (rs) {
      case FIELD_END:
        if (',' == *p_i) {
          // nothing
        } else if ('"' == *p_i) {
          rs = IN_QUOTED_FIELD;
        } else {
          rs = IN_FIELD;
          *p_o = gettsvchar(*p_i);
          ++p_o;
          ++tsv_len;
        }
        break;
      case IN_FIELD:
        if (',' == *p_i) {
          rs = FIELD_END;
        } else {
          *p_o = gettsvchar(*p_i);
          ++p_o;
          ++tsv_len;
        }
        break;
      case IN_QUOTED_FIELD:
        if ('"' == *p_i) {
          if (p_i == end_i) {
            rs = FIELD_END;
          } else if (',' == *(p_i + 1)) {
            rs = FIELD_END;
            ++p_i;
          } else if ('"' == *(p_i + 1)) {
            *p_o = gettsvchar(*p_i);
            ++p_o;
            ++tsv_len;
            ++p_i;
          }
        } else {
          *p_o = gettsvchar(*p_i);
          ++p_o;
          ++tsv_len;
        }
        break;
      }

      switch (rs) {
      case FIELD_END:
        *p_o = '\t';
        ++p_o;
        ++tsv_len;
        record_outputed = false;
        break;
      case IN_FIELD:
      case IN_QUOTED_FIELD:
        break;
      }
    }

    if (p_i == end_i || tsv_len == obufsize)
      break;
    else
      ++p_i;
  }

  return tsv_len;
}

static char gettsvchar(const char c) {
  record_outputed = true;
  if ('\t' == c) {
    return ' ';
  } else {
    return c;
  }
}

前回に説明したCSVとTSVの仕様に従って、CSVデータをTSVデータへ変換する処理をシンプルに書いていくとこんな感じになる。別段、高速化などを意識した実装はしていない。enum FIELD_STATUSという列挙型が、現在パースしているデータがフィールド内にあるか、クォートされたフィールド内にあるか、フィールドの終了か、を保持しているので、このあたりを意識しながら読んでいけば処理のロジックは見えてくると思う。

これら2つのファイルで実装されている2つの関数は、次のようにヘッダファイルに定義しておく。

main.h

int csv2tsv(const char *, int, char *, int);
char *file2str(const char *)

main.cは次のようにfile2str()とcsv2tsv()を呼び出して処理を行うだけだ。

main.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "main.h"

int main(int argc, char *argv[]) {
  char *csvdata, *tsvdata;
  int csvdata_bytes, tsvdata_bytes;

  csvdata = file2str(argv[1]);
  csvdata_bytes = strlen(csvdata);

  tsvdata_bytes = csvdata_bytes;
  tsvdata = calloc(tsvdata_bytes + 1, sizeof(char));

  csv2tsv(csvdata, csvdata_bytes, tsvdata, tsvdata_bytes);

  printf("%s", tsvdata);

  return 0;
}

この実装では、file2str()の中で確保したメモリが開放されていない。実装の見通しの良さを考えると、file2str()はちょっとばかり改良の余地があるのだが、とりあえず動作させることを優先して、こうした実装にしてある。