PicoJSON
さて、このJSON textを解析し、データと構造を読み取るからくり:パーサ(parser)が欲しくてあちこち探してみたところ、PicoJSONを見つけました。PicoJSONの最大のウリは「ヘッダだけで実装された」軽量/コンパクトさ、#include <picojson.h> を一行追加するだけで使えます。
おためしに先ほどのexample.jsonをPicoJSONでパースし、そのナカミのいくつかを読み出してみましょう。
#include <picojson.h> #include <fstream> #include <iostream> #include <cassert> #include <memory> #include <string> using namespace std; using namespace picojson; int main() { value root; { ifstream stream("example.json"); if ( !stream.is_open() ) return 1; stream >> root; assert( get_last_error().empty() ); } object image = root.get<object>()["Image"].get<object>(); cout << "Width=" << image["Width"].get<double>() << endl; cout << "Height=" << image["Height"].get<double>() << endl; cout << "Title=" << image["Title"].get<string>() << endl; array ids = image["IDs"].get<array>(); // arrayはstd::vector<picojson::value>なのでrange-based forが使える! for ( value item : ids ) { cout << item.get<double>() << endl; } } /* 実行結果: Width=800 Height=600 Title=View from 15th Floor 116 943 234 38793 */
JSON textのパースは実にお気楽、streamをオープンしてpicojson::valueに >> するだけ。そしてそのvalueをboolean,number,string,array,objectのいずれかに変換するのがメンバ関数 X value::get<X>()、ここでXはそれぞれbool,doube,std::string,picojson::array,picojson::objectです。また、bool value::is<X>() はそのvalueがXであるか否かを返します。
picojson::arrayはstd::vector<picojson::value>、picojson::objectはstd::map<std::string,picojson::value>のtypedefですから、STLに慣れた方なら苦もなく使えることでしょう。
RFC4627には「JSON text SHALL be encoded in Unicode. The default encoding is UTF-8.」とありますから、JSON text が内包するstringは漢字やかなを含んでいても無問題なはず。やってみましょう。
コレをUTF-8エンコードしたテキストファイル:utf8.jsonをパースし、幅や高さ、Titleなどを取り出します。
//#undef NDEBUG #include <picojson.h> #include <fstream> #include <iostream> #include <cassert> #include <memory> #include <string> using namespace std; using namespace picojson; int main() { value root; { ifstream stream("utf8.json"); if ( !stream.is_open() ) return 1; stream >> root; assert( get_last_error().empty() ); } object image = root.get<object>()["Image"].get<object>(); cout << "幅=" << image["幅"].get<double>() << endl; cout << "高さ=" << image["高さ"].get<double>() << endl; cout << "Title=" << image["Title"].get<string>() << endl; array ids = image["IDs"].get<array>(); for ( value item : ids ) { cout << item.get<double>() << endl; } } /* 実行結果: 幅=0 高さ=0 Title=15髫弱°繧峨・譎ッ隕ウ 116 943 234 38793 */
……あらら、おかしなことになってますね。Windowsだと多バイト文字はShift-JISなのでご覧のありさまです。幅と高さが0になってるのはobject検索時のキーがバケているため検索に引っかからず、対応する値が拾えなかったのでしょう(Debugモードだとpicojson.h内でassertします)。objectのキーや取り出したstringは適宜コード変換せにゃならんのです。
WindowsAPI:MultiByteToWideChar/WideCharToMultiByteの助けを借りて文字コード変換を施した版がコチラ。
//#undef NDEBUG #include <picojson.h> #include <fstream> #include <iostream> #include <cassert> #include <memory> #include <string> #include <locale> #include <Windows.h> // Unicode(wide) -> multibyte std::string fromUnicode(const std::wstring& str, UINT codepage =CP_UTF8) { std::string result; if ( str.size() != 0U ) { int size = ::WideCharToMultiByte(codepage, 0, str.data(), str.size(), NULL,0, NULL, NULL ); assert ( size > 0 ); auto buffer = std::make_unique<char[]>(size); size = ::WideCharToMultiByte(codepage, 0, str.data(), str.size(), buffer.get(), size, NULL, NULL); assert ( size > 0 ); result.assign(buffer.get(), size); } return result; } // multibyte -> Unicode(wide) std::wstring toUnicode(const std::string& str, UINT codepage =CP_UTF8) { std::wstring result; if ( str.size() != 0U ) { int size = ::MultiByteToWideChar(codepage, 0, str.data(), str.size(), NULL, 0); assert( size > 0 ); auto buffer = std::make_unique<wchar_t[]>(size); size = ::MultiByteToWideChar(codepage, 0, str.data(), str.size(), buffer.get(), size); assert( size > 0 ); result.assign(buffer.get(), size); } return result; } // Shift-JIS -> UTF-8 inline std::string stou(const std::string& str) { return fromUnicode(toUnicode(str, CP_ACP), CP_UTF8); } // UTF-8 -> Shift-JIS inline std::string utos(const std::string& str) { return fromUnicode(toUnicode(str, CP_UTF8), CP_ACP); } using namespace std; using namespace picojson; int main() { value root; { ifstream stream("utf8.json"); if ( !stream.is_open() ) return 1; stream >> root; assert( get_last_error().empty() ); } #if 1 object image = root.get<object>()["Image"].get<object>(); cout << "幅=" << image[stou("幅")].get<double>() << endl; cout << "高さ=" << image[fromUnicode(L"高さ",CP_UTF8)].get<double>() << endl; cout << "Title=" << utos(image["Title"].get<string>()) << endl; array ids = image["IDs"].get<array>(); for ( value item : ids ) { cout << item.get<double>() << endl; } #else /* あるいはこうやってもいい */ wcout.imbue(locale("japanese")); object image = root.get<object>()["Image"].get<object>(); wcout << L"幅=" << image[fromUnicode(L"幅")].get<double>() << endl; wcout << L"高さ=" << image[fromUnicode(L"高さ")].get<double>() << endl; wcout << L"Title=" << toUnicode(image["Title"].get<string>()) << endl; array ids = image["IDs"].get<array>(); for ( value item : ids ) { wcout << item.get<double>() << endl; } #endif } /* 実行結果: 幅=800 高さ=600 Title=15階からの景観 116 943 234 38793 */
ちょいとめんどくさいです。C++11にきっちり準拠した処理系なら u8"高さ" のように文字列リテラルの頭に u8 をつけることでUTF-8リテラル扱いとなり、例えば image[u8"幅"].get<double>() などとかなり楽に書けるのですが、VC12(Visual C++ 2013)は残念ながら未サポートのようです。