TwitterなんかのJSONを返すAPIからデータを取って、その結果をどこかのDBに格納するというのは大変ありがちな処理です。
まさに最近、そういうことをする必要があって、保存用のDBとしてMongoDBを使う前提でぽちぽちとコードを書いていました。MongoDBはJSONを元にしたデータ形式(BSON
ところが。Perlでさくっと書くには書けたのですが、思わぬところで引っかかってしまいました。原因は、Perlが真偽値(boolean)を扱う標準的なリテラルを持っていないこと。もちろん回避できたのですが、ちょっと面倒だったので、ぼやきついでにここに書いておきます。
はまり例
ライブラリは奇をてらわずCPANのJSON(もしくはJSON::XS
#!/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, object
そこで、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 | (なし) | (なし) | undef |
|
| 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におけるデータ表現」とかあるので面白そうです。あとで読んでみようと思います。

