serialize VS json_encode 〜人類の存亡とか仁義とか全く関係ない戦い〜

この記事はだいぶ前に書かれたものなので情報が古いかもしれません
エイリアンVSプレデターは見たことないです

おまとめ三行

僕もよく使ってます
このエラーにはほとほと困り果てた
声:レニーハート

photoshopのスポット修復ブラシツールっての使うと、良い感じに文字を消すことができるんですねー。そんな機能、初めて知りました。

上の画像の「serialize」と「json_encode」って文字の部分、本当は「エイリアン」と「プレデター」って書いてあったんですけど、何とかこの文字だけキレイに消すことできないかなーと思っていたら、そのスポット修復ブラシツールってやつを使うと良いよっていう情報を入手しまして……試してみた結果がこれです。あまりキレイには消せませんでしたけど、まあいいでしょう。

しかし、この画像を加工してブログに貼っつけんの、良いですね。勝手に使って怒られなければの話ですけど。怒られたら謝ろう。今から準備しとくわ。

僕はphotoshopって全っ然使えないんですよ。でも使いこなせるようになりたいとは、前から思ってたんですよね。ただ、その勉強の場がなかなかなかったので……今後はここを有効に使っていこう。少しずつやり方覚えて行くっす。



あー、すみません。いきなり関係ない話になってしまいましたね。

反省はしていない。



こっからが本題

PHPでコードを書いていると、配列をしょっちゅう使うことになると思うんですが、でも配列のデータをそのままの形でDBに登録することはできない。何かしらの方法でデータを加工して登録することになります。

といっても、難しいことは何もない。PHPにはserializeっていう関数があるので、それを使えば階層の深い配列だろうと、一発で保存可能な形に変換してくれる。変換されたデータを配列に戻すには、unserializeってのを使う。

実際、変換してみるとこんな風になる。

$data = array('あいうえお', 'かきくけこ', array('さしすせそ', 'たちつてと'));
$str = serialize($data);

//$strの中身
a:3:{i:0;s:15:"あいうえお";i:1;s:15:"かきくけこ";i:2;a:2:{i:0;s:15:"さしすせそ";i:1;s:15:"たちつてと";}}

僕もよく使ってます。データベースの設計をしていて、困ったらserializeしとけば良いやってくらい使ってる。マヨネーズ感覚で使ってる。
(この記事を書いている人は極度のマヨラーのため、困ったらとりあえずマヨネーズぶっかけとけば何でも美味しく食べられると思っています)

マヨネーズはすごいよ何でも合うよ

コレステロールとコレステロールの夢のコラボレーション



と・こ・ろ・が!

今までは特に何の問題もなく使えていたんですけど、最近、たま〜にこんなエラーが出るようになりました。

Notice: unserialize() [function.unserialize]: Error at offset・・・

DBから取得したデータのunserializeに失敗して、結果がfalseになってしまうという、そんなエラーのようです。



ぶっちゃけ、このエラーにはほとほと困り果てた。

何が困っちゃうって、このエラー、どうやって解消したら良いのかよく分からねーんですよ。

エスケープしなきゃいけない文字(ダブルコーテーションとか)をエスケープしてないせいとか、その辺りに原因があるんじゃないかなーと最初は思いました。でも、どうやらそういう感じでもないらしい。

何でかってーと、エラーが出たデータと全く同じ文字(←ここ重要)を入れた配列を再度serializeしてDBに登録して、それを取得してunserializeすると、エラーが出なくなるんですよ。

なしてさ?

自分なりにいろいろ試してはみたつもりなんですけど、同じ配列データでもエラーが出るときと出ないときがあるんで、簡単にエラーの再現が行なえないこともあって、結局どうしたら良いのかよく分かりませんでした。お手上げ侍です。

誰か良い情報持ってたら教えてほしいっす。



json_encodeの出番

いつどんな状態で上記のエラーが出るか分からない以上、このままserializeを使い続けても大丈夫っていう安心感もないわけで、ひとまず、別の方法で配列を保存できる形に変換することにしました。

それがjson_encodeです。

これは、配列などのデータをJSON形式に変換してくれる関数です。

JSONっていうのは、あー……javascriptにおけるオブジェクトの表記法を基にしたデータ形式ってことらしいです。javascriptでobject型のデータを書く場合と同じ感じの文字列っていう認識で良いんですかね。

配列をjson_encodeしてみると、こうなる。

$data = array('あいうえお', 'かきくけこ', array('さしすせそ', 'たちつてと'));
$str = json_encode($data);

//$strの中身
["\u3042\u3044\u3046\u3048\u304a","\u304b\u304d\u304f\u3051\u3053",["\u3055\u3057\u3059\u305b\u305d","\u305f\u3061\u3064\u3066\u3068"]]

「u3042」とか「u3044」とかは、マルチバイト文字をJSON形式でエスケープするとこうなるっぽいです。基本、json_encodeを使うと、自動でエスケープしてくれます。PHP5.4以降であれば、エスケープを行なわないようにすることもできるみたい。

この結果($str)も文字列なので、DBに保存することができます。配列に戻すにはjson_decodeを使えば良い。流れはserializeと一緒です。



このjson_encodeを使うことで、unserialize時の解決できなかったエラーは、とりあえず回避することができる。

僕も今回、仕事で開発しているウェブサービスで、serializeを使っている部分を一旦全部json_encodeを使うように修正して、難を逃れておきました。

unserializeが効かないせいで、シリアライズされてるデータを自分でせっせと配列に直さなきゃいけないっていうアナログ作業がだいぶめんどっちかったですけど、ひとまず、めでたしめでたし?



いざ、尋常に

エラーが出ないことに比べれば些細な問題のような気もしますが……serializeを使った場合とjson_encodeを使った場合では、変換後の文字列のバイト数とか、それを配列に戻すときの処理時間に少々差がある。なので、状況に応じて、どっちを使うのか検討する必要は、あるかもしれない。



と、いうことで、こっからが本番だ。ここまで、対決ムードではなかったからね。言ってみりゃ、選手紹介みたいなもんよ。

赤コーナー……シルリァァァァァルァイィィィィィズゥゥゥゥゥ!!

青コーナー……ジェイソンンンンンヌエンコォォォォォドゥォォォォォ!!

(声:レニーハート)



実際に簡単なプログラムを書いて、バイト数や処理にかかる時間を見てみましょう。

//serializeの場合
for($i = 0; $i < 10000; $i++) {
    $data[$i] = 'abcedfghijklmnopqrstuvwxyz';
}

$str = serialize($data);
$len = strlen($str);

$start = microtime(true);
$str = unserialize($str);
$end = microtime(true);
$time = $end - $start;

echo $len;
echo $time;


//json_encodeの場合
for($i = 0; $i < 10000; $i++) {
    $data[$i] = 'abcedfghijklmnopqrstuvwxyz';
}

$str = json_encode($data);
$len = strlen($str);

$start = microtime(true);
$str = json_decode($str);
$end = microtime(true);
$time = $end - $start;

echo $len;
echo $time;

こんな感じですかね。今回はDBに登録されているデータを取得して配列に戻すまでの時間を計測するってことで、serializeやjson_encodeの処理時間は含めないようにしてます。unserializeとjson_decodeの処理時間のみね。

僕がやってみた結果は、以下のようになりました。

//serialize
バイト数・・・408900
処理時間・・・2ms

//json_encode
バイト数・・・290001
処理時間・・・6ms

バイト数(データ量)はjson_encodeの方が少ないですね。でも処理時間はjson_encodeの方が3倍くらいかかってます。

まあ、serializeの場合は配列の要素数とかキーや値の型、バイト数も文字列の中に含まれてますから、その分データ量が多くなってしまうんでしょうね。serializeした文字列の中に、「a:3」とか「s:15」とかあったじゃん? あれがそう。「a:3」は配列の要素数が3個って意味で「s:15」はデータの型がstring型で、データ量が15バイトってことを意味しているらしい。一方のjson_encodeされた文字列には、そういう情報ないからね。

ってことで、処理時間を重視するならserialize、データ量を重視するならjson_encodeを使えば良い……とは、一概には言えないんだねぇ、これが。

今は、文字が全部アルファベット、つまり1バイト文字のみの場合の話。マルチバイト文字が入って来ると、また違って来ます。

json_encodeでマルチバイト文字をエスケープしたときって、「u3042」みたいに、一文字が5バイトになるのよ。通常、UTF-8のマルチバイト文字は3バイトだから、マルチバイト文字を多く含むデータを変換した場合は、json_encodeの方がデータ量が多くなる。

例えば……入力するデータを以下のように変えてみよう。

for($i = 0; $i < 10000; $i++) {
    //マルチバイト文字に変更してみる
    //$data[$i] = 'abcedfghijklmnopqrstuvwxyz';
    $data[$i] = 'あいうえおかきくけこさしすせそたちつてとなにぬねの';
}

これで再度serializeとjson_encodeをやって、処理時間やデータ量を見てみる。

//serialize
バイト数・・・898900
処理時間・・・2ms

//json_encode
バイト数・・・1530001
処理時間・・・20ms

json_encodeの方がデータ量が多くなったね。処理時間も、serializeの方は変わらなかったのに、json_encodeの方はさらに3倍くらいになってる。エスケープされた文字を元に戻してるからかな?

まあ、json_encodeのときに、マルチバイト文字をエスケープしないようにすれば、やっぱりjson_encodeの方がデータ量は少なくなるけれども……こういうのって、エスケープしといた方が安全そうじゃん?

ってか、僕が仕事で使ってるサーバー、PHPのバージョンが5.3だしな……さっきも言ったけど、5.4じゃないとjson_encode時のエスケープキャンセルは行なえない。






さあ、そんなわけで、対決が終了しました。

今回の結果を見た感じでは、軍配はserializeの方に上がりそうなんだけど……僕みたいに、serializeだと配列に戻せないエラーが出るからjson_encodeを使うっていう場合もあるだろうし、どっちを使うべきだってのは、勝敗とは別問題ってことですね。これでjson_encodeの完全勝利なら、「serializeよりもjson_encodeを使え!」って、僕も声を大にして叫ぶとこなんですが。

serializeだとエラーが出るようになったのも、専用サーバーに鞍替えしてからだから、僕がやったサーバーの設定にどっかミスがあるせいだと思うのよね。だからserialize自体に脆弱性があるとかではないはず。でも確かめてないから自信はない。めんご。



ちなみに、エイリアンとプレデターは、映画を見たことないんだけど、どっちが勝つん?

PHP24日目太郎 2013年11月10日 13:34:00
こんにちは。

分かり易い解説をありがとうございます。

本コメントは、両関数の処理速度やエラーの観点とは少し違うのですが、気になったので書かせていただきました。


PHPのHPでserializeの項目について見てみてると、ユーザーからの入力データを渡すのは危険なので、json_encodeを使ってくださいと書いてあります。

どんなに処理速度が速い関数でも、危険性が生じるとなると、やはり遅くても安全な関数(json_encode)に切り替えるべきなのでしょうか??


また、フォームのCHECKBOXなどからPOSTで入力データを受け取って、何らかの関数でエスケープしてからserializeして、最後にMYSQLなどのDBに格納する場合、json_encodeと比べるとどちらの処理速度が早いのでしょうか?
まっち~(管理人) 2013年11月10日 16:05:58
>PHP24日目太郎さん
そうですねぇ…ケースバイケースだとは思うんですが、今回の僕みたいに、エラーが出てしまう危険がある場合とかは、やっぱり遅くても安全な関数を使った方が良いのかなーとは思います。

HPの方で言っている、ユーザーからの入力データを渡す危険性っていうのは、たぶん文字のエスケープとかに関することだと思うので、そこを対応しているのなら、serializeを使っても問題ないと思います。

ただ、エスケープ処理を入れると、やはりそれだけ処理時間は長くなっちゃいますね。

僕もちょっと検証してみたんですが、記事と同じようにfor文で1万回データを変数に入れる際、「htmlentities」を使ってエスケープしてserializeする場合と、エスケープせずに変数に入れてjson_encodeした場合では、serializeの方が3倍くらい時間はかかりましたね。json_encodeの方が速かったです。

「htmlspecialchars」の場合はhtmlentitiesより速かったですけど、それでもやっぱり、json_encodeの倍くらいの処理時間にはなるみたいですね。

これはあくまでも僕がやった場合の結果なので、入力データの内容とかによっても違うと思いますし、もしあれなら、POSTデータを処理してDBに格納している処理の中で、microtime()とかを使って実際に検証してみた方が確実かもです。
PHP27日目太郎 2013年11月13日 00:13:55
お返事ありがとうございます。


あの後いろいろ調べてみたんですが、やはり速さが大事だったので、serializeを使いたいと思います。

HTMLフォームからPOSTで受け取ってserializeした後、bindvalueでパラメータを挿入する形でエスケープしてSQLインジェクション対策して、HTMLに出力する直前に、出力部分だけhtmlspecialcharsでXSS対策しておくことにしました。


ただ、せっかくなので、管理人さんのおっしゃる通り、json_encodeとserializeの比較を、自サーバーで検証してみたいと思います。
まっち~(管理人) 2013年11月13日 12:06:06
>PHP27日目太郎さん
serializeでも全然オッケーだと思います。この記事のエラーは、ちょっと例外的なケースっぽいですしね。

それにしても、太郎さんはPHPを触ってまだ27日なんですかね…すごいです。
僕なんか、PHPを触って27日目の頃なんて正直、serializeとかSQLインジェクションとか、存在すら知らないレベルでしたよ(笑)
PHP27日目太郎 2013年11月13日 17:12:00
またまたお返事ありがとうございます。


お褒めにあずかり光栄です。

分かり易い解説本があったり、パソコン初心者にも優しい言語設計になっていたり、御サイトのような分かり易いサイトがあるお陰で、非常にスムーズに修得できるのだと思います。

特にこのPHPやMYSQLは、プログラミングの具体的なコードと出力結果が掲載されているサイトが沢山あるので、非常に学習しやすい環境が整っていると感じております。
まっち~(管理人) 2013年11月14日 13:13:01
>PHP27日目太郎さん
こちらこそ、そう言っていただけると嬉しいです。

なるべく分かりやすいよう心掛けてはいるつもりなのですが、何せ隙あらば駄文を挟もうとする人間ですから、本当に読みやすく書けているのかは常に疑問に思っているところがありまして…。

僕はPHP以外の言語を触ったことがないので分からないんですが、いろんな言語を触ってる知り合いのエンジニアに話を聞くと、確かにPHPは取っかかりやすい方だって言いますねー。
sounisi5011 2016年02月05日 22:38:57
serializeの、ユーザーからの入力データを渡す危険性というのは、おそらく以下の事だと思われます。

http://blog.tokumaru.org/2015/07/phpunserialize.html

要約すると、プログラム内で利用されている任意のクラスを攻撃者が生成することができるようになります。
まっち~(管理人) 2016年02月08日 12:57:18
なるほど。単純なエスケープの話ではなかったんですね。勉強不足でした。ありがとうございます。
もしかしたら何か関連しているかも?