読者です 読者をやめる 読者になる 読者になる

Boost.statechart で CSV を読む。

C++

 CodeZine に『StateパターンでCSVを読む』という記事があります。この記事は C++ で State パターンの利用例として CSV ファイルを読んで HTML に変換するプログラムの解説をしています。ちょうど良い例題だったので同じモノを Boost.statechart を使って実装してみました。

 まずは CSV 読み込みのライブラリ側を作ります。

#include <string>
#include <vector>
#include <boost/statechart/simple_state.hpp>
#include <boost/statechart/state_machine.hpp>
#include <boost/statechart/transition.hpp>
#include <boost/statechart/event.hpp>
#include <boost/statechart/custom_reaction.hpp>
#include <boost/mpl/list.hpp>

using namespace std;
namespace sc = boost::statechart;

typedef string           csv_field;
typedef vector< string > csv_record;

namespace states
{
	struct reading_record;
		struct before_field;
		struct after_field;
		struct reading_field;
		struct reading_field_escaped;
	struct out_of_csv;
}

class csv_handler : public sc::state_machine< csv_handler, states::out_of_csv >
{
public:
	csv_handler() { initiate(); }
	
	// csv の読み込みが始まったよ!
	virtual void on_initiate_csv() = 0;
	// レコードを読み込んだよ!
	virtual void on_recognize_record( csv_record const & record ) = 0;
	// csv の読み込みが完了したよ!
	virtual void on_terminate_csv() = 0;
};

namespace events
{
	// 通常文字
	struct charactor        : sc::event< charactor >
	{
		char const c_; charactor( char c ) : c_( c ) {};
	};
	
	template < char c > struct char_base { char const c_; char_base() : c_( c ) {} };
	
	// ダブルクオート。
	struct double_quote     : sc::event< double_quote     > {};
	// 2個連続するダブルクオート
	struct double_quote_con : sc::event< double_quote_con >, char_base< '\"' > {};
	// カンマ。
	struct comma            : sc::event< comma            >, char_base< ',' >  {};
	// 改行。
	struct line_break       : sc::event< line_break       >, char_base< '\n' > {};
	
	// csv の開始
	struct start_of_csv     : sc::event< start_of_csv     > {};
	// cvs の終了
	struct end_of_csv	    : sc::event< end_of_csv       > {};
}

namespace states
{
	// 状態「レコード読み込み中」
	struct reading_record : sc::simple_state< reading_record, csv_handler, before_field >
	{
		typedef boost::mpl::list<
			sc::transition     < events::line_break, reading_record >,
			sc::custom_reaction< events::end_of_csv >
		> reactions;
		
		sc::result react( events::end_of_csv const & )
		{
			context< csv_handler >().on_terminate_csv();
			return transit< out_of_csv >();
		}
		~reading_record()
		{
			context< csv_handler >().on_recognize_record( fields_ );
		}
		
		csv_record fields_;
	};
	
	// 状態「フィールド読み込み前」
	struct before_field : sc::simple_state< before_field, reading_record >
	{
		typedef boost::mpl::list<
			sc::custom_reaction< events::charactor >,
			sc::transition     < events::double_quote, reading_field_escaped >,
			sc::custom_reaction< events::double_quote_con >,
			sc::custom_reaction< events::comma >,
			sc::custom_reaction< events::line_break >
		> reactions;
		
		sc::result react( events::charactor const & e )
		{
			post_event( e );
			return transit< reading_field >();
		}
		sc::result react( events::line_break const & )
		{
			context< reading_record >().fields_.push_back( "" );
			return forward_event();
		}
		
		template < typename T_Event >
		sc::result react( T_Event const & )
		{
			context< reading_record >().fields_.push_back( "" );
			return discard_event();
		}
	};
	
	// 状態「フィールド読み込み直後」
	struct after_field : sc::simple_state< after_field, reading_record >
	{
		typedef sc::transition< events::comma, before_field > reactions;
	};
	
	// 状態「フィールド読み込み中」
	struct reading_field : sc::simple_state< reading_field, reading_record >
	{
		typedef boost::mpl::list<
			sc::custom_reaction< events::charactor >,
			sc::custom_reaction< events::double_quote_con >,
			sc::transition     < events::comma, before_field >
		> reactions;
		
		template < typename T_Event >
		sc::result react( T_Event const & e )
		{
			field_.push_back( e.c_ );
			return discard_event();
		}
		
		~reading_field()
		{
			context< reading_record >().fields_.push_back( field_ );
		}
		
		csv_field field_;
	};
	
	// 状態「(エスケープされてる)フィールド読み込み中」
	struct reading_field_escaped : sc::simple_state< reading_field_escaped, reading_record >
	{
		typedef boost::mpl::list<
			sc::custom_reaction< events::charactor >,
			sc::transition     < events::double_quote, after_field >,
			sc::custom_reaction< events::double_quote_con >,
			sc::custom_reaction< events::comma >,
			sc::custom_reaction< events::line_break >
		> reactions;
		
		template < typename T_Event >
		sc::result react( T_Event const & e )
		{
			field_.push_back( e.c_ );
			return discard_event();
		}
		
		~reading_field_escaped()
		{
			context< reading_record >().fields_.push_back( field_ );
		}
		
		csv_field field_;
	};
	
	// 状態「いま CSV 読んでない」
	struct out_of_csv : sc::simple_state< out_of_csv, csv_handler >
	{
		typedef sc::custom_reaction< events::start_of_csv > reactions;
		
		sc::result react( events::start_of_csv const & )
		{
			context< csv_handler >().on_initiate_csv();
			return transit< reading_record >();
		}
	};
}

// csv_handler にイベントを渡すクラス。
class csv_event_sender
{
public:
	csv_event_sender( csv_handler & handler )
		: handler_( handler )
		, prev_is_double_quote_( false )
	{}
	
	void flush()
	{
		if( prev_is_double_quote_ ) {
			handler_.process_event( events::double_quote() );
			prev_is_double_quote_ = false;
		}
	}
	
	void operator()( char const c )
	{
		if( c == '\"' ) {
			if( prev_is_double_quote_ ) {
				handler_.process_event( events::double_quote_con() );
				prev_is_double_quote_ = false;
			} else {
				prev_is_double_quote_ = true;
			}
		} else {
			flush();
			switch( c ) {
			case '\n': handler_.process_event( events::line_break() );   break;
			case ',' : handler_.process_event( events::comma() );        break;
			default  : handler_.process_event( events::charactor( c ) ); break;
			}
		}
	}
	
	csv_handler & handler_;
	bool prev_is_double_quote_;
};

// csv をパースして handler に順次通知する。
void parse_csv( csv_handler & handler, istream & stream )
{
	handler.process_event( events::start_of_csv() );
	
	csv_event_sender sender( handler );
	
	for( istreambuf_iterator< char > i( stream ), end; i != end; ++ i ) { sender( * i ); }
	sender.flush();
	
	handler.process_event( events::end_of_csv() );
}

 で、これを利用するユーザープログラム。HTML への書き出しクラスの定義もユーザーの都合なのでこっちに置いておきます。

#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <boost/foreach.hpp>

// 読み込んだ csv データを HTML に書き出すクラス。
class html_handler : public csv_handler {
public:
	html_handler( ostream & stream )
		: stream_( stream )
		, header_( true )
	{}
	
private:
	bool header_;
	ostream & stream_;
	
	virtual void on_initiate_csv()
	{
		stream_ << "<table border='1'>" << endl;
	}
	
	virtual void on_recognize_record( csv_record const & record )
	{
		stream_ << "<tr>";
		BOOST_FOREACH( csv_field const & f, record )
		{
			stream_ << ( header_? "<th>": "<td>" );
			BOOST_FOREACH( char const c, f )
			{
				switch( c ) {
				case '\n' : stream_ << "<br />"; break;
				case '&'  : stream_ << "&amp;";  break;
				case '<'  : stream_ << "&lt;";   break;
				case '>'  : stream_ << "&gt;";   break;
				case '"'  : stream_ << "&quot;"; break;
				default   : stream_ << c;        break;
				}
			}
			stream_ << ( header_? "</th>": "</td>" );
		}
		stream_ << "</tr>" << endl;

		header_ = false;
	}
	
	virtual void on_terminate_csv()
	{
		stream_ << "</table>" << endl;

		header_ = true;
	}
};

int main()
{
	ifstream ifs( "input.csv", ios::binary );
	ofstream ofs( "output.html" );
	
	html_handler machine( ofs );
	parse_csv( machine, ifs );
}

 長いですかねー。まぁいいんだけど。
 これに、CodeZine の記事と同じ csv ファイルを食わせてやるとこんなんができます。

読めてますね。

 さてさて。ライブラリが違うと設計の選択肢も変わってきます。ちょっとだけ説明を添えておきます。

●内部状態の定義について。
 CodeZine の方は「nonescaped」「escaped」「afterCR」「afterDQ」の 4 状態です。本記事では冒頭の図の通り「before_field」「after_field」「reading_field」「reading_field_escaped」の 4 状態と、その親となる「reading_record」、あと csv を読んでいない状態を示す「out_of_csv」の全 6 状態となります。

●イベントの定義について。
 CodeZine の方は CR, LF, DQ, CM, OT の 5 種。1 つの文字が各イベントのいずれかに対応します。本記事では double_quote, double_quote_con, comma, line_break, start_of_csv, end_of_csv の 6 種。double_quote は「"」, double_quote_con は「""」, start_of_csv は文字無し, end_of_csv はストリーム終端が対応します。

※ forward_event() は親コンテキストに状態遷移の判断を任せる、という Boost.statechart の機能です。
※ post_event() はイベントキューにイベントを追加 post する、という Boost.statechart の機能です。