Пользователь
0,0
рейтинг
30 сентября 2013 в 20:11

Разработка → Генератор utf-8 json на php с поддержкой unicode 6 из песочницы

PHP*
Разумеется, в PHP есть прекрасная функция json_encode. Но до версии 5.3 включительно те же русские символы кодируются в виде \uXXXX — в разы длиннее, чем utf-8. Чтобы уменьшить объем трафика, необходимо убрать преобразование utf-8 символов в \u-последовательности. Да, в PHP 5.4 у json_encode наконец-то появился параметр JSON_UNESCAPED_UNICODE, но многие хостеры до сих пор представляют пользователям выбор только между версиями 5.2 и 5.3.

Я бы не стал изобретать очередной велосипед, но те решения, которые мне попадались, имеют общую проблему — они корректно обрабатывают только символы базовой плоскости юникода.

Способ, в разных модификациях широко распространенный на просторах интернета, заключается в том, что результат работы ф-ции json_encode обрабатывается фильтром, заменяющим все вхождения \uXXXX на utf-8 символы. Например, так:

class Json{
  static function json_encode($data){
    return preg_replace_callback('/\\\\u([0-9a-f]{4})/i',
      function($val){
        return mb_decode_numericentity('&#'.intval($val[1], 16).';', array(0, 0xffff, 0, 0xffff), 'utf-8');
      }, json_encode($data)
    );
  }
}

И этот код работал… До тех пор, пока не понадобилось добавить поддержку юникодных emoji (эмотиконы были добавлены в стандарте Unicode 6), большинство из которых имеет коды более 0x1F000 (первая плоскость unicode).

Дело в том, что \u-последовательности имеют кодировку utf-16: слово (2 байта) на символ с кодом от 0x0000 до 0xFFFF (исключая «окно» 0xD800-0xDFFF) и 2 слова (4 байта) с кодами 0xD800-0xDFFF для символов с кодами более 0xFFFF.

Например, исходный юникод-символ с кодом 0x1f601, имеющий utf-8 представление "\xf0\x9f\x98\x81", будет преобразован функцией json_dencode в строку "\ud83d\ude01" и результатом вышеприведенной ф-ции будет строка "\xed\xa0\xbd\xed\xb8\x81". Вместо одного 4-х байтового символа получили два 3-х байтовых.

Таким образом, для нормальной обработки символов необходим анализ кодов и отдельное преобразование 2-х символьных \u-последовательностей. Например, так:

class Json{
  static public $_code;

  static public function json_encode($data){
    Json::$_code=0;
    return preg_replace_callback('/\\\\u([0-9a-f]{4})/i',
      function($val){
        $val=hexdec($val[1]);
          if(Json::$_code){
            $val=((Json::$_code&0x3FF)<<10)+($val&0x3FF)+0x10000;
            Json::$_code=0;
          }elseif($val>=0xD800&&$val<0xE000){
            Json::$_code=$val;
            return '';
          }
          return html_entity_decode(sprintf('&#x%x;', $val), ENT_NOQUOTES, 'utf-8');
      }, json_encode($data)
    );
  }
}

Данный вариант корректно преобразовывает любые utf-8 символы.

P.S. Я прекрасно понимаю, что вышеприведенный код далек от оптимального. Но он работает и с достаточной — для моих задач — производительностью. А сравнивать скорость работы всех придуманных вариантов просто лень. Вот, например, вариант, перекладывающий анализ на регулярное выражение:

class Json{
  static public function json_encode($data){
    return preg_replace_callback('/\\\\ud([89ab][0-9a-f]{2})\\\\ud([c-f][0-9a-f]{2})|\\\\u([0-9a-f]{4})/i', function($val){
      return html_entity_decode(
        empty($val[3])?
          sprintf('&#x%x;', ((hexdec($val[1])&0x3FF)<<10)+(hexdec($val[2])&0x3FF)+0x10000):
          '&#x'.$val[3].';',
        ENT_NOQUOTES, 'utf-8'
      );
    }, json_encode($data));
  }
}

P.P.S. Вызовы html_entity_decode вставлены в callback-функцию потому, что обрабатываемые данные могут содержать html-код, включающий служебные html-сущности ('<', '>', '&' и т.д.), которые не должны быть преобразованы в символы.
Андрей Ежгуров @eandr_67
карма
1,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (5)

  • +2
    Сразу скажу, что анонимные функции появились только в PHP 5.3 — а в PHP 5.2 в preg_replace_callback пришлось бы совать строку с именем функции, определённой где-то ещё.
  • 0
    Кстати, а разве функции json_encode и json_decode не взаимно обратимы?

    И если они обратимы, то почему бы не использовать (для простоты дела) такое решение, при котором итоги работы json_encode, содержащие \uXXXX-последовательности, скармливаются в json_encode для обратного преобразования?

    Я сейчас запустил вот такой тест:

    <?php
    header('Content-Type: application/json;charset=utf-8');
    
    function prepareUTF8($matches){
       return json_decode('"'.$matches[1].'"');
    }
    
    echo preg_replace_callback('/((\\\u[01-9a-fA-F]{4})+)/', 'prepareUTF8',
       json_encode( "Самшит \xf0\x9f\x98\x81" )
    );
    

    Он выдал мне ту строку, которая от него, как я это понимаю, и требовалася. Со смайлом (единственным символом) на конце.

    (Я не могу процитировать её тут: ломается Хабрахабр. Серьёзно.)
    • 0
      Опечатка.

      Вместо «скармливаются в json_encode для обратного преобразования» следует читать «скармливаются в json_decode для обратного преобразования».
      • –2
        Остроумно, но ошибочно. Например, если на вход подать '\'"\\', то на выходе получим '"\'\\"\\\\"'.
      • +1
        Прошу прощения, похоже, я не прав и именно такой результат должен быть…

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.