Статья
Версия для печати
Обсудить на форуме
PHP. Что такое JSON и как с ним бороться.


Автор: RXL.
Дата написания: 6.01.2010.
Права на статью принадлежат автору и Клубу программистов «Весельчак У».

Содержание.


Вводная к JSON.

Сегодня поговорим про формат обмена данными JSON, который вполне может заменить вам XML для ряда задач. Расшифровывается это как JavaScript Object Notation — формат записи объектов на языке JavaScript. Формат до безобразия простой и, что приятно, стандартизирован.  Для общего развития можно почитать Википедию, детально — RFC4627, а я расскажу о его плюсах, минусах и как работать с ним на PHP и JavaScript.
JSON — простой иерархический формат, независимый от языка и платформы. JSON предлагается как альтернатива XML. Конечно, на все 100% заменить XML он не может, т.к. не поддерживает схем проверки, не может самостоятельно информировать о своей кодировке и не имеет понятия атрибутов, но легко его заменит там, где этими недостатками можно пренебречь. Также как и XML, JSON является само документирующимся форматом, описывающим структуру данных и не занимающихся их представлением.

Сравнение с XML.

Зачем нам новый формат, когда есть XML, спросите вы?
Главное преимущество JSON над XML — компактность. В рамках Web у него есть еще одно преимущество: он ведь представляет собой 100% валидный код JavaScript и его легко превратить из текста в данные JavaScript, а работать с полученными структурами проще и удобнее, чем с неповоротливым, хотя и универсальным, DOM (Document Object Model). Думаю, что последнее утверждение верно и для других языков программирования.
Давайте для наглядности рассмотрим такой пример: набор структур, состоящих из id и ФИО.

Так выглядит запись в XML:

Код: (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:

Код: (Javascript)
[
  {
    id: 1,
    surname: "Иванов",
    firstname: "Иван",
    patronymic: "Иванович"
  },
  {
    id: 2,
    surname: "Петров",
    firstname: "Петр",
    patronymic: "Петрович"
  }
]

Одного брошенного взгляда достаточно, чтобы сделать выводы: во-первых, JSON компактнее, во-вторых, легче читается, в-третьих — информативнее, т.к. перечисления (массивы) и именованные свойства (объекты) ограничиваются разными видами скобок.
Давайте еще раз «нажмем» на компактность и попробуем записать то же самое еще раз, но немного компактнее (все ненужные пробельные символы не удаляю только ради читаемости в статье):

Код: (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>

Код: (Javascript)
[
{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.

Долгое время у 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

а после «лечения» станет такой:

Код:
\!

Как говорится: «не айс!». Правильный подход — не создавать двусмысленности и не придумывать способ исправить потом!

Создание своих функций обработки JSON.

Я написал свой кодировщик, который работает согласно стандарту и не делает ничего лишнего, что стандарт не требует. Код совместим с PHP 5 и не совместим с PHP 4. Для создания совместимости с последним нужно убрать типизацию аргумента функции, добавить проверку на массив и изменить обработку ошибок на старую — через trigger_error().

Код: (PHP)
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. По этому для полноты картины ограничусь такой вот заглушкой:

Код: (PHP)
function from_json($str)
{
    return json_decode($str, true);
}

Для любителей объектного подхода предлагаю статический класс:

Код: (PHP)
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' => 'Петрович',
        ),
    )
);

Нет пределов для творчества!
Вопросы и предложения пишите на наш форум, в раздел «серверных скриптов» или другой, подходящий вашим вопросам.
Версия для печати
Обсудить на форуме