TwitterなんかのJSONを返すAPIからデータを取って、その結果をどこかのDBに格納するというのは大変ありがちな処理です。
まさに最近、そういうことをする必要があって、保存用のDBとしてMongoDBを使う前提でぽちぽちとコードを書いていました。MongoDBはJSONを元にしたデータ形式(BSON1)を使っているので、そのまま突っ込むのに適しているだろうというもくろみです。
ところが。Perlでさくっと書くには書けたのですが、思わぬところで引っかかってしまいました。原因は、Perlが真偽値(boolean)を扱う標準的なリテラルを持っていないこと。もちろん回避できたのですが、ちょっと面倒だったので、ぼやきついでにここに書いておきます。
はまり例
ライブラリは奇をてらわずCPANのJSON(もしくはJSON::XS2)とMongoDB使います。こんな感じでしょうか。(このサンプルではAPIから値を取得する代わりにJSONドキュメントをスクリプト中に書いています)
#!/usr/bin/perl use strict; use warnings; use JSON; use MongoDB; my $coll = MongoDB::Connection->new()->get_database('db1')->get_collection('items'); my $original = <<EOD; [ { "id": 0, "status": true }, { "id": 1, "status": false }, { "id": 2, "status": true }, { "id": 3, "status": false } ] EOD my $array = from_json($original); foreach my $a (@$array){ $coll->insert($a); }
単純化していますが、APIの応答に含まれる複数の要素を個別にMongoDBに登録するようなイメージです。実行結果をmongodbのコンソールから実行結果を確認します。
> db.items.find(); { "_id" : ObjectId("51420ba61d22572366000000"), "status" : BinData(2,"AQAAADE="), "id" : NumberLong(0) } { "_id" : ObjectId("51420ba61d22572366000001"), "status" : BinData(2,"AQAAADA="), "id" : NumberLong(1) } { "_id" : ObjectId("51420ba61d22572366000002"), "status" : BinData(2,"AQAAADE="), "id" : NumberLong(2) } { "_id" : ObjectId("51420ba61d22572366000003"), "status" : BinData(2,"AQAAADA="), "id" : NumberLong(3) }
元々のJSONではtrue/falseだったはずのstatusが、BinData(2,"AQAAADE=")
のような謎のバイナリに化けています。
原因
化けたように見えているバイナリは、実はPerlのオブジェクトに対するリファレンスの内部表記がそのままJSONのバイナリ型として変換されてしまったものです。Perl上で$arrayの内容をData::Dumperで表示すると、各レコードのstatusにはJSON::Boolean(JSON::XS::Boolean)オブジェクトが代入されているのがわかります。
$VAR1 = [ { 'status' => bless( do{\(my $o = 1)}, 'JSON::XS::Boolean' ), 'id' => 0 }, { 'status' => bless( do{\(my $o = 0)}, 'JSON::XS::Boolean' ), 'id' => 1 }, { 'status' => $VAR1->[0]{'status'}, 'id' => 2 }, { 'status' => $VAR1->[1]{'status'}, 'id' => 3 } ];
JSON(JavaScript)では、変数の値としてstring, number, object3, array, true, false, nullが使えます。ところが、Perlは組み込み型でtrue/falseを示す事が出来ないため、JSONをPerlの変数に取り込んだ際に値を表すことが出来ません。
そこで、JSONモジュールではtrue/falseをJSON::Booleanオブジェクトで表します。JSON::Boolean::true、JSON::Boolean::falseはif文などで真偽値と扱われるようにoverloadされているため、普段は特に意識して処理する必要はありません。
JavaScript(JSON) | string | number | object | array | true | false | null |
---|---|---|---|---|---|---|---|
Perl組み込み型 | scalar | hash | array | (なし) | (なし) | undef4 | |
Perl(JSONモジュール) | scalar | hash | array | JSON::Boolean::true | JSON::Boolean::false | undef |
ところが、JSON::Booleanオブジェクトがbooleanを意味していると知っているのはJSONモジュールだけで、MongoDBモジュールはそれを意識してくれません。ということで、何も考えずに愚直にバイト列としてバイナリにエンコードしてくれます。このバイト列は、Perl実行環境内でリファレンスを管理するための値ですので、値自体に意味はありません。
回避策
最初に書いた通り、MongoDBのデータ型はJSONが元になっているので、当然booleanは扱えます。ということで、PerlのMongoDBモジュールでもboolean型を扱えるようになっているのですが、JSON::Booleanではなくbooleanモジュールを使うことになっています。
このbooleanモジュール。use boolean;
と書くのでプラグマのように見えますが、普通のCPANモジュールです。5.14.2でもバンドルされていない様子。名前空間にtrue
, false
がexportされていて、一見そういうリテラルがあるように見えますが、そういう名前の関数です。
ということで、JSONモジュールでPerlの変数に取り込んだデータをMongoDBに渡すためには、このあたりの変換をしてやる必要があります。
たとえば、こんな感じでto_boolean()という処理を作ってみます。なお、冒頭の$MongoDB::BSON::use_boolean = 1;
は、MongoDBからPerl変数へ変換する場合にbooleanを使うという指示なので、このサンプルコードの範囲内では特に意味がありません。
#!/usr/bin/perl use strict; use warnings; use boolean; use JSON; use MongoDB; use Data::Dumper; my $coll = MongoDB::Connection->new()->get_database('db1')->get_collection('items'); $MongoDB::BSON::use_boolean = 1; my $original = <<EOD; [ { "id": 0, "status": true }, { "id": 1, "status": false }, { "id": 2, "status": true }, { "id": 3, "status": false } ] EOD my $array = from_json($original); to_boolean($array); foreach my $a (@$array){ $coll->insert($a); } sub to_boolean{ my $ref = shift; my $type = ref($ref); if ($type eq 'JSON::XS::Boolean' || $type eq 'JSON::Boolean'){ return $ref ? true : false; } elsif ($type eq 'ARRAY'){ foreach my $r (@$ref){ $r = to_boolean($r); } } elsif ($type eq 'HASH'){ foreach my $r (%$ref){ $r = to_boolean($r); } } return $ref; }
次のように、無事、MongoDB上でもtrue/falseとして保存されるようになりました。
> db.items.find() { "_id" : ObjectId("5142ebc6b60a8caf6d000000"), "status" : true, "id" : NumberLong(0) } { "_id" : ObjectId("5142ebc6b60a8caf6d000001"), "status" : false, "id" : NumberLong(1) } { "_id" : ObjectId("5142ebc6b60a8caf6d000002"), "status" : true, "id" : NumberLong(2) } { "_id" : ObjectId("5142ebc6b60a8caf6d000003"), "status" : false, "id" : NumberLong(3) }
ちなみに、to_boolean()実行後の$arrayはこんな状態です。
$VAR1 = [ { 'status' => bless( do{\(my $o = 1)}, 'boolean' ), 'id' => 0 }, { 'status' => bless( do{\(my $o = 0)}, 'boolean' ), 'id' => 1 }, { 'status' => $VAR1->[0]{'status'}, 'id' => 2 }, { 'status' => $VAR1->[1]{'status'}, 'id' => 3 } ];
JSON::Booleanもbooleanも同じ事やってるだけなんですけどねぇ……。JSON側でdecode時にJSON::Booleanじゃなくてbooleanを使うようなスイッチとかあればいいんですが。
これに限らず、ライブラリ間でのデータ型の問題は色々引っかかります。この前書いたUTF-8フラグのありなしもそういう事情でしたが。
最後になりましたが、Perlにおける真偽値の扱いはこちらも参考にして下さい。
てな記事をポストしようと思って準備していたところ、なんか面白げなKindle本の案内がTLに流れてきました。目次を見ると「Perlにおけるデータ表現」とかあるので面白そうです。あとで読んでみようと思います。