PHP mb_strlen со строкой, добавляющей chr(241)

Итак, я столкнулся с этой проблемой и максимально упростил ее.

$test = 'XXX' . chr(241) . 'XXX';
print($test); // XXX�XXX
print(mb_strlen($test, 'UTF-8')); // 4
print(count(str_split($test))); // 7

В общем, мой вопрос: почему chr(241) не возвращает ни одного символа, составляющего длину строки 7? Шесть символов, я добавляю один, и получается четыре символа? Почему chr(241) не равен html-сущности 241?

Другая информация указана ниже. Обратите внимание, что пока вы не добавляете X ПОСЛЕ chr(241), все довольны:

print(mb_detect_encoding($test)); // UTF-8
print(mb_strlen('XX' . chr(241) . 'XX', 'UTF-8')); // 3
print(mb_strlen('X' . chr(241) . 'X', 'UTF-8')); // 2
print(mb_strlen('' . chr(241) . 'X', 'UTF-8')); // 1
print(mb_strlen('X' . chr(241) . '', 'UTF-8')); // 2
print(mb_strlen('XXX' . chr(241) . '', 'UTF-8')); // 4
print(mb_strlen(chr(241), 'UTF-8')); // 1

Похоже на проблему с кодировкой, но как? Файл сохранен как UTF-8, внутренняя кодировка — UTF-8, и я никуда не передаю данные, чтобы их испортить.


person Andrew    schedule 25.10.2013    source источник


Ответы (1)


В UTF-8 все символы ASCII до 127 представлены одним байтом (двоичное представление 0xxxxxxx), а кодовые точки больше 127 представляются многобайтными< /strong> последовательности. Многобайтовые последовательности состоят из начального байта и одного или нескольких байтов продолжения.

Биты старшего порядка начального байта указывают нам, сколько байтов продолжения использовать, и для этой цели он имеет две или более старших единиц, за которыми следует 0, т. е. старшие биты могут быть 110 или 1110. или 11110 или 111110. Количество старших бит равно сумме начального байта плюс байты продолжения, т.е.

110   means 1 leading byte + 1 continuation byte 
1110  means 1 leading byte + 2 continuation bytes
11110 means 1 leading byte + 3 continuation bytes

Байты продолжения, которые следуют за начальным байтом, имеют формат 10xxxxxx.

Применив приведенное выше к вашей строке $test:

У нас есть три байта ord('X'), которые все являются символами ascii под 127, поэтому они считаются как 1 символ на 1 байт,

Затем у нас есть chr(241) с двоичным представлением 11110001, так что это начальный байт, так как он имеет два или более старших бита.

Поскольку он имеет 4 старших бита, это означает, что кодовая точка, которую он представляет, состоит из 1 ведущего байта плюс 3 байта продолжения, поэтому 3 ord('X') байта, оставшиеся в строке, считаются на mb_strlen() в качестве байтов продолжения*, и хотя вместе с chr(241) в общей сложности четыре байта, они считаются одной кодовой точкой UTF-8.

*Здесь мы должны заявить, что эти завершающие 'X' не являются допустимыми байтами продолжения, поскольку они не соответствуют стандарту байта продолжения. Однако mb_strlen() будет потреблять, как описано выше, еще до 3 байтов после chr(241). Вы можете проверить это, если добавите еще 'X' или вычтете 'X's из конца строки $test.

ОБНОВЛЕНИЕ: проверка результатов:

/*
 * The following strings are non valid UTF-8 encodings.
 * We test to see if mb_strlen() consumes non VALID UTF-8
 * byte strings like they are valid (driven by the leading bytes)
 *
 */

/*
 * 0xc0 as a leading byte should consume one continuation byte
 * so the length reported should be 6
 */ 
$test = 'XXX' . chr(0xc0) . 'XXX'; 
echo '6 == ', mb_strlen($test, 'UTF8');

/*
 * 0xe0 as a leading byte should consume two continuation bytes
 * so the length reported should be 5
 */ 
$test = 'XXX' . chr(0xe0) . 'XXX'; 
echo '5 == ', mb_strlen($test, 'UTF8'), PHP_EOL;

// results in 6 == 6 and 5 == 5

ОБНОВЛЕНИЕ 2:

Пример построения с помощью chr() одного и того же символа в латинице-1 и UTF-8.

$euroSignAscii = chr(0x80); // Latin-1 extended ASCII
$euroSignUtf8 = chr(0xe2) . chr(0x82) . chr(0xac); // UTF-8

Обратите внимание, если вы повторяете приведенные выше строки, кодировка вашей консоли или веб-страницы (если это latin-1, то $euroSignAscii будет выводиться правильно, если это UTF-8, то $euroSignUtf8 будет выводиться правильно).


Ссылки:

Хорошей ссылкой является соответствующая статья UTF-8 в Википедии.

Классический пост Джоэла Спольски Абсолютный минимум, который каждый разработчик программного обеспечения обязательно должен знать о Unicode и наборах символов ( Никаких оправданий!)

И чтобы почувствовать таблицу кодировки UTF-8 и символы Unicode

person Ioannis Lalopoulos    schedule 25.10.2013
comment
Я не уверен, что объяснение такое сложное, или это просто недопустимая UTF-8. В любом случае, хорошее объяснение. - person deceze♦; 25.10.2013
comment
Спасибо. Конечно, это недопустимый UTF-8 - правильный ответ и, возможно, правильный короткий. Однако это не тот, который объясняет наблюдаемые факты и, следовательно, тот, который имеет наибольшую познавательную ценность. Из ответа вы можете сделать вывод: а) что если добавленный символ был не chr(241), а chr(195), то сообщаемая длина будет 6 вместо 4 и б) что mb_strlen() будет анализировать неверный UTF-8 как правильный рассматривая только начальные байты для руководства (мне нужно будет проверить реализацию, чтобы быть на 100% уверенным в этом). - person Ioannis Lalopoulos; 25.10.2013
comment
Спасибо за этот ответ. Я предполагаю, что если бы мой файл был закодирован в ASCII, он дал бы мне ожидаемый ответ (поскольку мое исправление состояло в том, чтобы сделать mb_strlen($test, 'ASCII')). Но какая часть кода неверна? Это функция chr()? В документации сказано: Возвращает односимвольную строку, содержащую символ, указанный в ascii. Похоже, слово символ неточно. - person Andrew; 25.10.2013
comment
@Andrew Эндрю Действительно, слово «персонаж» - это упрощение, которое не работает при определенных обстоятельствах. chr возвращает байт. Точнее, он просто возвращает двоичное значение десятичного числа, которое вы ввели. Вот почему максимально допустимый ввод для chr равен 255. То, что вы создаете, представляет собой двоичную строку, вы можете сделать то же самое с "XXX\xF1XXX" для того же эффекта. Сколько символов это зависит от того, в какой кодировке вы интерпретируете этот двоичный файл; поскольку это недопустимый UTF-8, вы получите плохие результаты. Дополнительные сведения см. на странице kunststube.net/encoding. - person deceze♦; 26.10.2013
comment
@ilalopoulos Я просто имел в виду, что я не уверен, просто ли mb_strlen терпит неудачу, потому что он икает из-за неправильной кодировки и просто останавливается после 4 символов, или он действительно считается так, как вы описываете. Я не проводил никаких экспериментов по этому поводу, я просто бросил эту мысль из головы. - person deceze♦; 26.10.2013
comment
@deceje, поскольку я тоже не был уверен на 100%, я провел несколько простых тестов для результатов, которые показывают, что mb_strlen будет обрабатывать даже недействительные utf-8 как действительные. Я обновил свой ответ этими тестами. - person Ioannis Lalopoulos; 26.10.2013
comment
@andrew, так как вы добавляете один байт › 127 внутри этой строки байтов, результаты будут отличаться, когда вы просите PHP обрабатывать его как UTF-8 (допустимый или недействительный) от того, что вы получаете, когда вы обрабатываете его как ASCII. Я добавил несколько ссылок в ответ. Возможно, Джоэл о кодировках поможет прояснить ситуацию. - person Ioannis Lalopoulos; 26.10.2013
comment
Учитывая это, имеет ли реальное применение chr(), вызванная для значений › 127? - person Andrew; 28.10.2013
comment
Почему chr() не имеет реального применения? chr() предназначен для манипуляций с символами/байтами (здесь символ означает один байт). Я добавил простой пример использования chr() для создания одного и того же символа в Latin-1, расширенном ASCII и UTF-8. - person Ioannis Lalopoulos; 29.10.2013