Автор:
RXL.
Дата написания: 6.01.2010.
Права на статью принадлежат автору и
Клубу программистов «Весельчак У».
Сегодня поговорим про формат обмена данными JSON, который вполне может заменить вам XML для ряда задач. Расшифровывается это как JavaScript Object Notation — формат записи объектов на языке JavaScript. Формат до безобразия простой и, что приятно, стандартизирован. Для общего развития можно почитать Википедию, детально — RFC4627, а я расскажу о его плюсах, минусах и как работать с ним на PHP и JavaScript.
JSON — простой иерархический формат, независимый от языка и платформы. JSON предлагается как альтернатива XML. Конечно, на все 100% заменить XML он не может, т.к. не поддерживает схем проверки, не может самостоятельно информировать о своей кодировке и не имеет понятия атрибутов, но легко его заменит там, где этими недостатками можно пренебречь. Также как и XML, JSON является само документирующимся форматом, описывающим структуру данных и не занимающихся их представлением.
Зачем нам новый формат, когда есть XML, спросите вы?
Главное преимущество JSON над XML — компактность. В рамках Web у него есть еще одно преимущество: он ведь представляет собой 100% валидный код JavaScript и его легко превратить из текста в данные JavaScript, а работать с полученными структурами проще и удобнее, чем с неповоротливым, хотя и универсальным, DOM (Document Object Model). Думаю, что последнее утверждение верно и для других языков программирования.
Давайте для наглядности рассмотрим такой пример: набор структур, состоящих из id и ФИО.
Так выглядит запись в XML:
<peoples>
<record>
<id>1</id>
<surname>Иванов</surname>
<firstname>Иван</firstname>
<patronymic>Иванович</patronymic>
</record>
<record>
<id>2</id>
<surname>Петров</surname>
<firstname>Петр</firstname>
<patronymic>Петрович</patronymic>
</record>
</peoples>
А так выглядит то же самое в JSON:
[
{
id: 1,
surname: "Иванов",
firstname: "Иван",
patronymic: "Иванович"
},
{
id: 2,
surname: "Петров",
firstname: "Петр",
patronymic: "Петрович"
}
]
Одного брошенного взгляда достаточно, чтобы сделать выводы: во-первых, JSON компактнее, во-вторых, легче читается, в-третьих — информативнее, т.к. перечисления (массивы) и именованные свойства (объекты) ограничиваются разными видами скобок.
Давайте еще раз «нажмем» на компактность и попробуем записать то же самое еще раз, но немного компактнее (все ненужные пробельные символы не удаляю только ради читаемости в статье):
<peoples>
<record><id>1</id><surname>Иванов</surname>
<firstname>Иван</firstname>
<patronymic>Иванович</patronymic></record>
<record><id>2</id><surname>Петров</surname>
<firstname>Петр</firstname>
<patronymic>Петрович</patronymic></record>
</peoples>
[
{id:1,surname:"Иванов",firstname:"Иван",patronymic:"Иванович"},
{id:2,surname:"Петров",firstname:"Петр",patronymic:"Петрович"}
]
У меня получилось (в UTF-8 и с переводами строк в стиле Windows): XML — 259 байт, JSON — 135 байт. При этом читаемость JSON еще хорошая, а XML сливается в сплошной текст. Если убрать все ненужные символы, то получится 243 и 127 соответственно, что дает почти двух кратное сокращение!
Давайте теперь рассмотрим недостатки JSON и разберемся, являются ли они критичными и как их обойти. Вот они:
- не поддерживает схем проверки;
- не может самостоятельно информировать о своей кодировке;
- не имеет понятия атрибутов;
- в рамках JavaScript формату JSON приписывается уязвимость.
Необходимость схем проверки довольно сомнительна, т.к. даже с XML, зачастую, их не используют. Будем считать это ограничением. Клиенту с сервером придется заранее договориться о структуре данных (замечу, что JSON, как и XML, легко переживает появление новых полей).
Невозможность объявить в документе кодировку компенсируется тремя способами:
- Стандарт RFC4627 предписывает кодировать JSON только в Unicode. Кодировкой по умолчанию является UTF-8 и допускаются еще UTF-16 и UTF-32 в разновидности BE и LE (порядок следования байт). Тип кодировки должен определяться парсером автоматически на основе первых 4 байт. Детальнее это описано в стандарте в главе 3.
- Можно применять только ASCII, а все прочие кодировки представлять в виде escape-последовательностей: \xXX для 8-битных кодировок и \uXXXX для Юникода, где X — одна шестнадцатеричная цифра. Этот вариант неудобен тем, что в 3-4 раза увеличивает размер не-ASCII текста.
- В принципе, отойдя от стандарта, кодировку можно использовать любую: либо по предварительному соглашению между клиентом и сервером, либо положиться на клиентскую библиотеку и возможности протокола через указание кодировки в заголовке. Например, в HTTP:
Content-Type: application/json; charset=windows-1251
Что касается атрибутов, то это быстрее костыли, чем возможность, т.к. можно добавить новые поля и перенести в них информацию из атрибутов. По моему, основное их назначение в XML — как раз борьба за компактность. В сети я встречал такое соглашение, как кодировать имена XML-атрибутов в JSON: предварять имя префиксом «@».
Теперь о безопасности. В JavaScript естественным путем превращения текста JSON в объект JavaScript является выполнение этого текста командой eval(). При этом, т.к. нет схем проверки, то ничего не мешает злоумышленникам подложить в JSON свой исполнимый вредоносный код. Но это на первый взгляд. Выход из ситуации описан в том же RFC4627 в главе 6 и заключается в предварительной проверке текста JSON регулярным выражением, которое там и приведено. Пользоваться же полученными возможностями можно с блаженной улыбкой на лице. :)
Долгое время у PHP не было штатных возможностей для работы с JSON (такая возможность была в репозитории библиотек PECL). Начиная с PHP версии 5.2.0, библиотеку PECL json внесли в основной код PHP. Рассмотрим, что может сейчас PHP:
- Кодировать объекты и массивы PHP в JSON — json_encode();
- Создавать объекты и массивы PHP из JSON — json_decode().
API прост, как валенок! Но радоваться рано, так как у него есть существенный недостаток, который перечеркивает главное достоинство JSON — компактность. Это то, что данные функции работают только с кодировкой UTF-8 и кодируют все непечатные и юникодные символы в формат \uXXXX, что увеличивает занимаемый текстом объем примерно в 3 раза. Никакой возможности отказаться от такого кодирования не предусмотрено. Замечу, что такое поведение не предписывается стандартом, хотя и не запрещается.
Попробуем решить эту проблему. Первое, что приходит на ум — вернуть назад \uXXXX к бинарному представлению. Код приводить не буду — его легко найти на официальном сайте PHP через Google. Но у этого подхода есть недостаток: если исходная строка объекта, который будет закодирован json_encode(), будет содержать текст формата \uXXXX, то он исказится. Например, следующая строка:
\u0021
после json_encode() превратится в:
\\u0021
а после «лечения» станет такой:
\!
Как говорится: «не айс!». Правильный подход — не создавать двусмысленности и не придумывать способ исправить потом!
Я написал свой кодировщик, который работает согласно стандарту и не делает ничего лишнего, что стандарт не требует. Код совместим с PHP 5 и не совместим с PHP 4. Для создания совместимости с последним нужно убрать типизацию аргумента функции, добавить проверку на массив и изменить обработку ошибок на старую — через trigger_error().
function to_json(array $data)
{
$isArray = true;
$keys = array_keys($data);
$prevKey = -1;
// Необходимо понять — перед нами список или ассоциативный массив.
foreach ($keys as $key)
if (!is_numeric($key) || $prevKey + 1 != $key)
{
$isArray = false;
break;
}
else
$prevKey++;
unset($keys);
$items = array();
foreach ($data as $key => $value)
{
$item = (!$isArray ? "\"$key\":" : '');
if (is_array($value))
$item .= to_json($value);
elseif (is_null($value))
$item .= 'null';
elseif (is_bool($value))
$item .= $value ? 'true' : 'false';
elseif (is_string($value))
$item .= '"' . preg_replace(
'%([\\x00-\\x1f\\x22\\x5c])%e',
'sprintf("\\\\u%04X", ord("$1"))',
$value
) . '"';
elseif (is_numeric($value))
$item .= $value;
else
throw new Exception('Wrong argument.');
$items[] = $item;
}
return
($isArray ? '[' : '{') .
implode(',', $items) .
($isArray ? ']' : '}');
}
Также я отказался от поддержки объектов PHP, что упрощает код и дисциплинирует: по моему мнению, данные для передачи должны быть не объектами, а структурами на базе массивов. Впрочем, вы сами можете дописать поддержку объектов. Также можете добавить форматирование, для большей читаемости.
Полученный через to_json() текст без проблем принимается встроенной функцией json_decode().
Была еще идея написать свой декодер, но это несколько сложнее, да json_decode() работает быстрее, т.к. написана на C, а не на PHP. По этому для полноты картины ограничусь такой вот заглушкой:
function from_json($str)
{
return json_decode($str, true);
}
Для любителей объектного подхода предлагаю статический класс:
class JSON
{
public static function encode(array $data) { ... }
public static function decode($str) { ... }
}
$json = JSON::encode(
array(
array(
'id' => 1,
'surname' => 'Иванов',
'firstname' => 'Иван',
'patronymic' => 'Иванович',
),
array(
'id' => 2,
'surname' => 'Петров',
'firstname' => 'Петр',
'patronymic' => 'Петрович',
),
)
);
Нет пределов для творчества!