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

固定長データの解析

C++

"123Akira25"

のような固定長データを解析する処理を考えます。


まず、簡単なところではbasic_string::substr()を使う方法があります。

#include <string>

int main()
{
    const std::string s = "123Akira25";

    const std::string id   = s.substr(0, 3);
    const std::string name = s.substr(3, 5);
    const std::string age  = s.substr(8); // s.substr(8, 2)でもいいけど「残り全部」という意図
}

これはうっかりミスが多いので、オフセットを最初に定義しましょう。

#include <string>

int main()
{
    const std::string s = "123Akira25";

    const std::size_t offsets[] = { 3, 5, 2 };
    std::size_t ofs = 0;

    const std::string id   = s.substr(ofs, offsets[0]); ofs += offsets[0];
    const std::string name = s.substr(ofs, offsets[1]); ofs += offsets[1];
    const std::string age  = s.substr(ofs); // これもs.substr(ofs, offsets[2])でもいい
}

いくらかマシになりましたが、管理がめんどう。
もうちょいいじれるけど、やっぱりめんどうなので省略。


次に、Boost.Tokenizerを使う方法。

#include <string>
#include <boost/tokenizer.hpp>
#include <boost/range.hpp>

int main()
{
    const std::string s = "123Akira25";

    const int offsets[] = { 3, 5, 2 };
    const boost::offset_separator sep(boost::begin(offsets), boost::end(offsets));

    typedef boost::tokenizer<boost::offset_separator> tokenizer;
    const tokenizer tok(s, sep);
    tokenizer::const_iterator it = tok.begin();

    const std::string id   = *it++;
    const std::string name = *it++;
    const std::string age  = *it;
}

オフセットの管理をまかせられるので、良くなったようにも見えますが、
Tokenizerだと「残り全部」という処理ができないため、

数量:N
データ0
・
・
・
データN-1

のような、個数によって長さが変わるようなデータがあった場合に対応できません。

#include <iostream>
#include <string>
#include <boost/tokenizer.hpp>
#include <boost/range.hpp>

int main()
{
    const std::string s = "abcde3xxxyyyzzz"; // いらないデータ, 個数, N個のデータ

    const int offsets[] = { 5, 1 };
    const boost::offset_separator sep(boost::begin(offsets), boost::end(offsets));

    typedef boost::tokenizer<boost::offset_separator> tokenizer;
    const tokenizer tok(s, sep);
    tokenizer::const_iterator it = tok.begin();

    const std::string hoge  = *it++;
    const std::string count = *it++;
    const std::string data  = *it; // "xxxyyyzzz"を取得したい

    std::cout << hoge << "," << count << "," << data << std::endl;
}
abcde,3,xxxyy

「残り全部」という意図に反し、オフセットがループしてしまってますね。


最後に全てを解決するBoost.Spirit.Qi。

#include <string>
#include <boost/spirit/include/qi.hpp>

namespace qi = boost::spirit::qi;
namespace fusion = boost::fusion;

int main()
{
    const std::string s = "123Akira25";

    typedef fusion::vector<std::string, std::string, std::string> result_type;
    typedef qi::rule<std::string::const_iterator, result_type()> rule;

    const rule r = qi::repeat(3)[ qi::char_ ] >>
                   qi::repeat(5)[ qi::char_ ] >>
                   *qi::char_; // 残り全部

    result_type result;
    std::string::const_iterator it = s.begin();
    qi::parse(it, s.end(), r, result);

    const std::string& id   = fusion::at_c<0>(result);
    const std::string& name = fusion::at_c<1>(result);
    const std::string& age  = fusion::at_c<2>(result);
}

「残り全部」の処理も、オフセットの管理も簡単です。



さらに、BOOST_FUSION_ADAPT_STRUCTを使えば構造体に一発変換もできます。

#include <iostream>
#include <string>
#include <boost/spirit/include/qi.hpp>
#include <boost/fusion/include/struct.hpp>

namespace qi = boost::spirit::qi;

struct person {
    std::string id;
    std::string name;
    std::string age;
}; 

BOOST_FUSION_ADAPT_STRUCT(
    person,
    (std::string, id)
    (std::string, name)
    (std::string, age)
)

int main()
{
    const std::string s = "123Akira25";

    typedef person result_type;
    typedef qi::rule<std::string::const_iterator, result_type()> rule;

    const rule r = qi::repeat(3)[ qi::char_ ] >>
                   qi::repeat(5)[ qi::char_ ] >>
                   *qi::char_;

    result_type result;
    std::string::const_iterator it = s.begin();
    qi::parse(it, s.end(), r, result);

    std::cout << result.id << "," << result.name << "," << result.age << std::endl;
}
123,Akira,25

Spirit.Qiを使う場合の注意点としては、デフォルトで扱えるのが10フィールドまで、ということです。
これは、Boost.Fusionのシーケンスの最大要素数がデフォルト10で定義されているためです。
なので、それよりも多くのフィールドを扱う場合は、

#define FUSION_MAX_VECTOR_SIZE 15

のようにサイズを変更する必要があります。


それと、さすがに戻り値の型にこんなのを書きたくはないので

typedef 
    fusion::vector< 
        std::string, 
        std::string, 
        std::string, 
        std::string, 
        std::string, 
        std::string, 
        std::string, 
        std::string, 
        std::string, 
        std::string, 
        std::string, 
        std::string, 
        std::string, 
        std::string, 
        std::string 
    > 
result_type;

同じ型の場合には、N個のTから成るFusionのシーケンスを作りましょう。
それらしいメタ関数が用意されていなかったので、boost::arrayをFusionのシーケンスにアダプトします。

#include <boost/fusion/include/boost_array.hpp>

...

typedef fusion::result_of::as_vector<boost::array<std::string, 15> >::type result_type;

これでコンパイラが許す限り、長さを制御できます。