PHP從5.2版本開始提供json_encode()和json_decode()函數,分別用於JSON的序例化和反序列化。不幸的是,現在仍然有許多主機運行著PHP5.2之前的版本,所以就不得不自己動手寫JSON解析了。
JSON的編碼並沒有什麼難度,要點就兩個:
下面是JSON編碼方法的實現:
/**
* JSON Encode
* @warn Any input string must be UTF-8 encoding
* @param {Any} $data Any type object to serialize.
* @return {String} serialized json string.
*/
function json_stringify($data) {
if (is_null($data)) return 'null';
if (is_scalar($data)) return json_stringify_scalar($data);
if (empty($data)) return '[]';
if (is_object($data)) {
$data=get_object_vars($data);
if (empty($data)) return '{}';
}
$keys=array_keys($data);
if ($keys===array_keys($keys)) {
$data=array_map(__FUNCTION__,$data);
return '['.join(',',$data).']';
} else {//不是有序數字下標的數組即視為關聯數組
$a=array();
foreach ($data as $k=>$v) {
$a[]=json_stringify_scalar(strval($k)).':'.json_stringify($v);
}
return '{'.join(',',$a).'}';
}
}
function json_stringify_scalar($v) {
if (is_bool($v)) {
$v = $v?'true':'false';
} else if (is_string($v)) {
$v=addcslashes($v,"\t\n\r\"\/\\");//轉義特殊字符
//將所有非ASCII字符轉換成Unicode Escape格式
$v='"'.preg_replace_callback('|[^\x00-\x7F]+|','_unicode_escape',$v).'"';
}
return (string)$v;
}
function _unicode_escape($s) {
//Warning:字符串必須為UTF-8編碼
$s=str_split(iconv('UTF-8','UCS-2BE',$s[0]),2);
foreach ($s as $i=>$c) {
$s[$i]=sprintf('\u%02x%02x',ord($c{0}),ord($c{1}));
}
return join('',$s);
}
而將JSON轉換成PHP中的對象就沒有那麼簡單了,自己寫Parser進行詞法分析、語法分析是很累人的。反觀在JavaScript中實現JSON Parse就簡單多了,因為JSON本身即是JavaScript語言的子集,直接使用eval方法,就能將JSON字符串轉換成JS中的對象。
其實,看到JavaScript中的eval方法,實在不能不聯想到PHP也有一個eval,它們的功能是類似的,都是將字符串當作代碼運行。不同的是,JS中的eval執行JS代碼,PHP的eval執行PHP代碼。Trick就在這裡:要讓PHP能直接用eval方法解析JSON,只要將JSON代碼轉換成PHP格式的代碼就行了!如對於下面的JSON:
{
"name":"CJ",
"age":18,
"tags":["PHP","JavaScript","Python","Haskell"],
"life":{
"summary":"Too complex!"
}
}
只要將其轉換成這樣的PHP代碼就能直接eval了:
(object)array(
"name"=>"CJ",
"age"=>18,
"tags"=>array("PHP","JavaScript","Python","Haskell"),
"life"=>(object)array(
"summary"=>"Too complex!"
)
)
而將JSON轉換成PHP代碼則只需很少的步驟與注意點:
最終的實現代碼出人意料的簡單:
/**
* JSON Decode
* @param {String} $s The string json data.
* @param {Boolean} [$assoc=false] Return assoc array if $assoc is true.
* @return decode result corresponding object.
*/
function json_parse($s,$assoc=false) {
static $strings,$count=0;
if (is_string($s)) {
$s=trim($s);
$strings=array();
//匹配字符串結束引號應該確保前面只能有偶數個'\'
//如 "ab\"c"中 \" 不能被視為字符串結束引號
$s=preg_replace_callback('/"([\s\S]*?(?<!\\\\)(?:\\\\\\\\)*)"/',__FUNCTION__,$s);
//去除特殊字符後做簡單的安全檢測
$clean=str_replace(array('true','false','null','{','}','[',']',',',':','#','.'),'',$s);
if ($clean && !is_numeric($clean)) {//可能是格式不正確的JSON、惡意代碼
return NULL;
}
$s=str_replace(
array('{','[',']','}',':','null'),
//通過'(object)'類型轉換將關聯數組轉換成stdClass instance
array(($assoc?'':'(object)').'array(','array(',')',')','=>','NULL')
,$s);
$s=preg_replace_callback('/#\d+#/',__FUNCTION__,$s);
//抑制錯誤,如{3##}能通過上面的安全檢測但卻無法轉換成正確的PHP代碼
@$data=eval("return $s;");
$strings=$count=0;//GC
return $data;
} elseif (count($s)>1) {//存儲字符串
$strings[]=_unicode_unescape(str_replace(array('$','\\/'),array('\\$','/'),$s[0]));
return '#'.($count++).'#';
} else {//讀取存儲的值
$index=substr($s[0],1,strlen($s[0])-2);
return $strings[$index];
}
}
function _unicode_unescape($data) {
if (is_string($data)) {
//匹配 Unicode escape時,需要注意匹配'\u'前面只能有偶數個'\',如'\\u5409'不應被匹配
return preg_replace_callback(
'/(?<!\\\\)((?:\\\\\\\\)*)\\\\u([a-f0-9]{2})([a-f0-9]{2})/i',
__FUNCTION__,$data);
}
return $data[1].iconv("UCS-2BE","UTF-8",chr(hexdec($data[2])).chr(hexdec($data[3])));
}
和PHP 5.2之後的C實現的JSON方法相比,這裡的實現自然要慢近百倍,根本不是一個數量級上的。 PEAR上有一個純PHP實現的JSON庫:Services_JSON。作為比較,我做了一個簡單的性能對比測試,結果是,Services_JSON的encode方法比json_stringify方法慢三四倍,而Services_JSON的decode方法更是比json_parse方法慢十幾倍。 對於json_stringify方法,應該是主要得益於使用iconv先將UTF-8字符串轉換成UCS-2BE,再轉換成Unicode轉義序列的形式,自然要比Services_JSON自己實現UTF-8到Unicode轉義序列的轉換性能更高。而json_parse方法,雖然一般的遞歸下降Parser只需要掃描一次JSON字符串,而這裡的實現會掃描多次字符串,但由於都是使用的PHP Native字符串方法,再加上eval這個非常規手段,最終性能上反而要高出好幾倍。
原文地址:http://jex.im/programming/json-eval-trick-in-php.html