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)は残念ながら未サポートのようです。
