株式会社エス・スリー・フォー

CSVからXMLへ

CSV、そしてXML

CSV(Comma Separated Value)をご存知ですか? 2次元の表をテキストで表現したもので、表の1行をテキストの1行で、行の各列を’,’で区切った形式です。

大抵の表計算アプリケーションはこのCSV形式のテキストファイルを出力することができます。

以下のCSVはExcelが出力したものです。

sales.csv

品名,単価,数量
りんご,128,12
みかん,36,24
バナナ,98,4

いやはや、まったくもって単純明解。ですがこの形式は人が読むのにはいいかもしれませんけど、コンピュータ(アプリケーション)がこれを読むのはかなり困難です。

このテキストの第1行目が、引き続くデータの各列の意味を表していることをあらかじめ教えておかなければなりません。それにこの第1行目自体、Excelの利用者が入力したものであり、すべてのCSVに必須な行ではありません。ただ単に

りんご,128,12
みかん,36,24
バナナ,98,4

であってもCSVとしては正当です。このときアプリケーション(人であっても)は各列の意味を知ることはできません。

これが以下のようなXMLで表現されていたら、人にもアプリケーションにも、それぞれの値の意味は(タグから)明らかです。

<売上>
  <項目>
    <品名>りんご</品名>
    <単価>128</単価>
    <数量>12</数量>
  </項目>

  <項目>
    <品名>みかん</品名>
    <単価>36</単価>
    <数量>24</数量>
  </項目>

  <項目>
    <品名>バナナ</品名>
    <単価>98</単価>
    <数量>4</数量>
  </項目>

</売上>

このXMLにスタイルシートを追加すればWebブラウザで見映えよく表示できますし、XMLパーサを積んだアプリケーションがこのファイルを読み込んで本日の売上を集計することも簡単です。

そこで、読み込んだCSVファイルの内容に基づいてXMLを出力するアプリケーション"csv2xml"を作ってみました。

CSVTokenizer

CSVをXMLに変換するには、まずCSVの一行を区切り文字’,’で分割しなければなりません。

CSVは

  • 項目に , が含まれていたら項目全体を""で囲む
  • 項目中の """に置き換える

という規則に基づいて作られています。この規則に従い、CSVの1行から項目を切り出すクラスを用意しました。

入力となる文字列に応じて、

  • scsv_tokenizer (SBCS:ASCII用)
  • mcsv_tokenizer (MBCS:Shift_Jis用)
  • wcsv_tokenizer (DBCS:Unicode用)

を使い分けてください。

csvtoken.h

#ifndef __CSVTOKEN_H__
#define __CSVTOKEN_H__

/*
 * CSV tokenizer
 */

#include <cwchar> // wchar_t

/*
 * SBCS(ASCII)
 */
class scsv_tokenizer {
public:
  typedef char             char_type;
  typedef char_type*       pointer;
  typedef const char_type* const_pointer;

  explicit scsv_tokenizer(const_pointer src);
  pointer next();
  bool empty() const;
private:
  const_pointer src_;
  const_pointer cur_;
  const_pointer max_;
  const_pointer next_comma(const_pointer ind);
};

/*
 * MBCS(Shift_JIS)
 */
class mcsv_tokenizer {
public:
  typedef unsigned char    char_type;
  typedef char_type*       pointer;
  typedef const char_type* const_pointer;

  explicit mcsv_tokenizer(const_pointer src);
  pointer next();
  bool empty() const;
private:
  const_pointer src_;
  const_pointer cur_;
  const_pointer max_;
  const_pointer next_comma(const_pointer ind);
};

/*
 * DBCS(Unicode)
 */
class wcsv_tokenizer {
public:
  typedef wchar_t          char_type;
  typedef char_type*       pointer;
  typedef const char_type* const_pointer;

  explicit wcsv_tokenizer(const_pointer src);
  pointer next();
  bool empty() const;
private:
  const_pointer src_;
  const_pointer cur_;
  const_pointer max_;
  const_pointer next_comma(const wchar_t* ind);
};

#endif

使い方は簡単です。CSVで表現された1行を引数としてそれぞれのインスタンスを生成し、empty()がfalseの間next()を呼び出せば、next()の戻り値が切り出された文字列となります。

ただしnext()で得られた文字列はヒープ領域からoperator newによって取得した領域ですから、その後必ずdelete[]してください。

// example
const char* input = "this,is,a,csv,string";
scsv_tokenizer csv(input);
while ( !csv.empty() ) {
  char* output = csv.next();
  cout << output << endl;
  delete[] output; // 忘れないでね!!
}

csvtoken.cpp

#include "csvtoken.h"

#include <mbstring.h>
#include <mbctype.h>
#include <tchar.h>

/*
 * SBCS(ASCII)
 */

// ind 以降にある次の区切り位置を見つける
scsv_tokenizer::const_pointer
scsv_tokenizer::next_comma(const_pointer ind) {
  bool inquote = false; // "の中ならtrue
  while ( ind < max_ ) {
    char_type ch = *ind;
    if ( !inquote && ch == ',' ) { // "の外にある , は項目の区切り
      break;
    } else if ( ch == '"' ) { // "の内/外を切り換え
      inquote = !inquote;
    }
    ++ind;
  }
  return ind;
}

// コンストラクタ
scsv_tokenizer::scsv_tokenizer(const_pointer src) : src_(src) {
  cur_ = src;
  max_ = src + strlen(src);
}

// 次の項目を切り出す
scsv_tokenizer::pointer
scsv_tokenizer::next() {
  if ( cur_ > max_ ) { // これでおしまい
    return 0;
  }
  const_pointer st = cur_;
  cur_ = next_comma(cur_);
  pointer buf = new char_type[cur_ - st + 1]; // 必要な領域を確保
  pointer p = buf;
  while ( st < cur_ ) {
    char_type ch = *st++;
    if ( ch == '"' ) { "" を " に変換
      if ( (st < cur_) &&  *st == '"' ) {
        *p++ = ch;
        ++st;
      }
    } else {
      *p++ = ch;
    }
  }
  *p = '¥0'; // 文字列の終わり
  ++cur_;
  return buf;
}

// これでおしまい?
bool
scsv_tokenizer::empty() const {
  return cur_ > max_;
}

/*
 * MBCS(Shift_JIS)
 */
mcsv_tokenizer::const_pointer
mcsv_tokenizer::next_comma(const_pointer ind) {
  bool inquote = false;
  while ( ind < max_ ) {
    char_type ch = *ind;
    int type = _mbbtype(ch, 0);
    if ( !inquote && type == _MBC_SINGLE && ch == ',' ) {
      break;
    } else if ( type == _MBC_SINGLE && ch == '"' ) {
      inquote = !inquote;
    }
    ind = _mbsinc(ind);
  }
  return ind;
}

mcsv_tokenizer::mcsv_tokenizer(const_pointer src) : src_(src) {
  cur_ = src;
  max_ = src + strlen((const char*)src);
}

mcsv_tokenizer::pointer
mcsv_tokenizer::next() {
  if ( cur_ > max_ ) {
    return 0;
  }
  const_pointer st = cur_;
  cur_ = next_comma(cur_);
  pointer buf = new char_type[cur_ - st + 1];
  pointer p = buf;
  while ( st < cur_ ) {
    char_type ch = *st;
    st = _mbsinc(st);
    int type = _mbbtype(ch, 0);
    if ( type == _MBC_SINGLE && ch == '"' ) {
      if ( (st < cur_) &&  _mbbtype(*st,0) == _MBC_SINGLE && *st == '"' ) {
        *p++ = ch;
        ++st;
      }
    } else {
      if ( type == _MBC_LEAD ) {
        *p++ = ch;
      }
      *p++ = *(st-1);
    }
  }
  *p = '¥0';
  cur_ = _mbsinc(cur_);
  return buf;
}

bool
mcsv_tokenizer::empty() const {
  return cur_ > max_;
}

/*
 * DBCS(Unicode)
 */
wcsv_tokenizer::const_pointer
wcsv_tokenizer::next_comma(const_pointer ind) {
  bool inquote = false;
  while ( ind < max_ ) {
    char_type ch = *ind;
    if ( !inquote && ch == L',' ) {
      break;
    } else if ( ch == L'"' ) {
      inquote = !inquote;
    }
    ++ind;
  }
  return ind;
}

wcsv_tokenizer::wcsv_tokenizer(const_pointer src) : src_(src) {
  cur_ = src;
  max_ = src + wcslen(src);
}

wcsv_tokenizer::pointer
wcsv_tokenizer::next() {
  if ( cur_ > max_ ) {
    return 0;
  }
  const_pointer st = cur_;
  cur_ = next_comma(cur_);
  pointer buf = new char_type[cur_ - st + 1];
  pointer p = buf;
  while ( st < cur_ ) {
    char_type ch = *st++;
    if ( ch == L'"' ) {
      if ( (st < cur_) &&  *st == L'"' ) {
        *p++ = ch;
        ++st;
      }
    } else {
      *p++ = ch;
    }
  }
  *p = L'¥0';
  ++cur_;
  return buf;
}

bool
wcsv_tokenizer::empty() const {
  return cur_ > max_;
}

CSVをXMLに変換する

さて、それではcsv_tokenizerの助けを借りてCSVをXMLに変換するアプリケーション csv2xml を作ります。

csv2xml.cpp

/*
 * csv2xml : convert CSV to XML using mcsv_tokenizer
 *    compile : cl -GX csv2xml.cpp csvtoken.cpp
 */

// C++ libs
#include <string>    // string
#include <iostream>  // cin, cout, cerr
#include <fstream>   // ofstream
#include <vector>    // vector

// C libs
#include <cstdio>     // vsprintf
#include <cstdarg>    // va_start, va_end

// MSVC libs
#include <mbstring.h> // _mbbtype
#include <mbctype.h>  // _MBC_SINGLE, _MBC_LEAD

// csv tokenizer
#include "csvtoken.h" // scsv_tokenizer

using namespace std;

/*
 * global valiables
 */
string         docname   = "doc"; // ドキュメントの名前
string         rowname   = "row"; // 行の名前
vector<string> colnames; // 各列の名前
bool           gencol    = false; // 列名を生成するか?

bool           genxsl    = false; // スタイルシート(XSL)を生成するか?
bool           gendtd    = false; // ドキュメント型定義(DTD)を生成するか?

string         output; // 出力ファイル名

/*
 * printf-like formatter
 */
const char* form(const char* format, ...) {
  static char buffer[512]; // 512バイトもあれば十分だろう...
  va_list marker;
  va_start(marker, format);
  vsprintf(buffer, format, marker);
  va_end(marker);
  return buffer;
}

/*
 * & " ' < > はそのままXMLに書いてはならない
 */
string escape(const unsigned char* str) {
  string result;
  while ( *str ) {
    int type = _mbbtype(*str, 0);
    if ( type == _MBC_SINGLE ) {
      switch ( *str ) {
      case '&'  : result += "&amp;"; break;
      case '"'  : result += "&quot;"; break;
      case '¥'' : result += "&apos;"; break;
      case '<'  : result += "&lt;"; break;
      case '>'  : result += "&gt;"; break;
      default   : result += *str;
      }
    } else {
      if ( type == _MBC_LEAD ) {
        result += *str++;
      }
      result += *str;
    }
    ++str;
  }
  return result;
}

/*
 * CSVの1行をXMLに変換
 */
ostream& line2xml(ostream& strm, const string& line) {
  mcsv_tokenizer csv((mcsv_tokenizer::const_pointer)line.c_str());
  const char* row = rowname.c_str();
  strm << form("  <%s>¥n", row);
  for ( int index = 0; !csv.empty(); ++index ) {
    mcsv_tokenizer::pointer token = csv.next();
    const char* col = colnames[index].c_str();
    strm << form("    <%s>%s</%s>¥n", col, escape(token).c_str(), col);
    delete[] token;
  }
  strm << form("  </%s>¥n", row);
  return strm;
}

/*
 * 使い方
 */
void usage() {
  cerr << "csv2xml [option...] <output>¥n"
          "  -doc <doc_tag> : document name (default:<doc>)¥n"
          "  -row <row_tag> : row name (default:<row>¥n"
          "  -col           : 1'st row as column names (default:<column##>)¥n"
          "  -xsl           : create XSL(stylesheet) <output>.xsl¥n"
          "  -dtd           : create DTD <output>.dtd¥n"
          "<output>         : create XML <output>.xml¥n"
       << endl;
}

/*
 * encoding は "Shift_JIS" とした。
 */
#define ENCODING "Shift_JIS"

/*
 * XMLの生成
 */
void makexml() {
  ofstream strm;
  strm.open((output+".xml").c_str());
          "<!-- created by csv2xml -->¥n"
       << endl;
  if ( genxsl ) {
    strm << form("<?xml-stylesheet type=¥"text/xsl¥" href=¥"%s.xsl¥"?>¥n",output.c_str()) << endl;
  }
  if ( gendtd ) {
    strm << form("<!DOCTYPE %s SYSTEM ¥"%s.dtd¥">¥n", docname.c_str(), output.c_str()) << endl;
  }

  strm << form("<%s>¥n", docname.c_str());
  bool at_first = true;
  string line;
  for ( getline(cin, line); !cin.eof(); getline(cin, line) ) {
    if ( at_first ) {
      at_first = false;
      mcsv_tokenizer csv((mcsv_tokenizer::const_pointer)line.c_str());
      while ( !csv.empty() ) {
        mcsv_tokenizer::pointer token = csv.next();
        if ( gencol ) {
          colnames.push_back((const char*)token);
        } else {
          colnames.push_back(form("column%d",colnames.size()));
        }
        delete[] token;
      }
      if ( !gencol ) {
        line2xml(strm, line) << endl;
      }
    } else {
      line2xml(strm, line) << endl;
    }
  }
  strm << form("</%s>¥n", docname.c_str());
  strm.close();
}

/*
 * XSLの生成
 */
void makexsl() {
  ofstream strm;
  strm.open((output+".xsl").c_str());
          "<!-- created by csv2xml -->¥n"
          "<xsl:stylesheet xmlns:xsl=¥"http://www.w3.org/TR/WD-xsl¥" >¥n¥n";

  strm << "<xsl:template match=¥"/¥">¥n"
          "  <html>¥n"
          "    <header>¥n"
          "      <title>" << docname << "</title>¥n"
          "    </header>¥n"
          "    <body>¥n"
          "      <xsl:apply-templates select=¥"" << docname << "¥"/>¥n"
          "    </body>¥n"
          "  </html>¥n"
          "</xsl:template>¥n¥n";

  strm << "<xsl:template match=¥"" << docname << "¥">¥n"
          "  <table border=¥"3¥">¥n"
          "  <tr>¥n";
  for ( int i = 0; i < colnames.size(); ++i ) {
    strm << form("    <th>%s</th>¥n", colnames[i].c_str());
  }
  strm << "  </tr>¥n"
       << form("  <xsl:for-each select=¥"%s¥">¥n", rowname.c_str())
       << "    <tr>¥n";
  for ( i = 0; i < colnames.size(); ++i ) {
    strm << form("      <td><xsl:value-of select=¥"%s¥"/></td>¥n", colnames[i].c_str());
  }
  strm << "    </tr>¥n"
          "  </xsl:for-each>¥n"
          "  </table>¥n"
          "</xsl:template>¥n¥n"
          "</xsl:stylesheet>¥n"
       << endl;
  strm.close();
}

/*
 * DTDの生成
 */
void makedtd() {
  ofstream strm;
  strm.open((output+".dtd").c_str());
       << "<!-- created by csv2xml -->¥n¥n";
  strm << form("<!ELEMENT %s (%s*)>¥n¥n", docname.c_str(), rowname.c_str());
  strm << form("<!ELEMENT %s (", rowname.c_str());
  for ( int i = 0; i < colnames.size(); ++i ) {
    strm << colnames[i] << ( i == colnames.size()-1 ? ')' : ',');
  }
  strm << ">¥n¥n";
  for ( i = 0; i < colnames.size(); ++i ) {
    strm << form("<!ELEMENT %s (#PCDATA)>¥n", colnames[i].c_str());
  }
  strm.close();
}

/*
 * main
 */
int main(int argc, char* argv[]) {
  /*
   * コマンドラインの解析
   */
  if ( argc <= 1 ) {
    usage();
    return 1;
  }
  for ( int i = 1; i < argc; ++i ) {
    string arg = argv[i];
    if ( arg == "-doc" ) { docname = argv[++i]; } else
    if ( arg == "-row" ) { rowname = argv[++i]; } else
    if ( arg == "-col" ) { gencol = true;       } else
    if ( arg == "-xsl" ) { genxsl = true;       } else
    if ( arg == "-dtd" ) { gendtd = true;       } else
    {
      if ( arg[0] == '-' ) {
        usage();
        return 1;
      }
      output = arg;
    }
  }

  if ( output.empty() ) {
    usage();
    return 1;
  }

  /*
   * XML, XSL & DTD
   */
  cerr << output << ".xml " << flush;
  makexml();
  if ( genxsl ) {
    cerr << output << ".xsl " << flush;
    makexsl();
  }
  if ( gendtd ) {
    cerr << output << ".dtd " << flush;
    makedtd();
  }
  cerr << endl;

  return 0;
}

csv2xmlはコマンドライン・アプリケーションです。標準入力からCSVを読み込み、XML(,XSL,DTD)を出力します。

csv2xml [オプション] <output>
  • -doc <docname> : ドキュメント名を指定(デフォルト:”doc”)
  • -row <rowname> : 列名を指定(デフォルト:”row”)
  • -col : CSVの第1行を項目名とする(デフォルト:”column1″, “column1”, …)
  • -xsl : <output&gt.xslを生成する
  • -dtd : <output&gt.dtdを生成する

csv2xml -doc 売上 -row 項目 -col -xsl -dtd sales < saels.csv

によって出力された sales.xml, sales.dtd, そして sales.xsl を以下に示します。

sales.xml

<!-- created by csv2xml -->

<?xml-stylesheet type="text/xsl" href="sales.xsl"?>

<!DOCTYPE 売上 SYSTEM "sales.dtd">

<売上>
  <項目>
    <品名>りんご</品名>
    <単価>128</単価>
    <数量>12</数量>
  </項目>

  <項目>
    <品名>みかん</品名>
    <単価>36</単価>
    <数量>24</数量>
  </項目>

  <項目>
    <品名>バナナ</品名>
    <単価>98</単価>
    <数量>4</数量>
  </項目>

</売上>

DTDはXMLパーサにXMLを検証させるときに必要となります。

sales.dtd

<!-- created by csv2xml -->

<!ELEMENT 売上 (項目*)>

<!ELEMENT 項目 (品名,単価,数量)>

<!ELEMENT 品名 (#PCDATA)>
<!ELEMENT 単価 (#PCDATA)>
<!ELEMENT 数量 (#PCDATA)>

XMLをIE5等のWebブラウザで表示させるとき、このスタイルシートがXMLをHTMLに変換します。

IE5では次のような表示が得られます。

IE5による表示

sales.xsl

<!-- created by csv2xml -->
<xsl:stylesheet xmlns:xsl="http://www.w3.org/TR/WD-xsl" >

<xsl:template match="/">
  <html>
    <header>
      <title>売上</title>
    </header>
    <body>
      <xsl:apply-templates select="売上"/>
    </body>
  </html>
</xsl:template>

<xsl:template match="売上">
  <table border="3">
  <tr>
    <th>品名</th>
    <th>単価</th>
    <th>数量</th>
  </tr>
  <xsl:for-each select="項目">
    <tr>
      <td><xsl:value-of select="品名"/></td>
      <td><xsl:value-of select="単価"/></td>
      <td><xsl:value-of select="数量"/></td>
    </tr>
  </xsl:for-each>
  </table>
</xsl:template>

</xsl:stylesheet>