Skip to content

Latest commit

 

History

History
executable file
·
211 lines (120 loc) · 41.9 KB

strings.md

File metadata and controls

executable file
·
211 lines (120 loc) · 41.9 KB

Представление строк в памяти компьютера и кодировки

Все данные, с которыми работает компьютер, представлены внутри него в виде чисел. Это неудивительно - ведь компьютеры создавались в первую очередь для вычислений (что и отражено в названии). Внутри компьютера данные хранятся и передаются в двоичном виде. Один бит может принимать значения 0 или 1, а 8 бит образуют байт.

Память компьютера состоит из ячеек, каждая из которых хранит 1 байт (или 8 бит) информации. Данные на жестком диске записаны в виде секторов, хранящих байты. По сети передаются пакеты, состоящие из байтов.

Байт состоит из 8 бит, каждый из которых может принимать 2 значения (0 или 1). В совокупности получается 28 = 256 значений. Их можно обозначить числами от 0 до 255, и получается, что содержимое каждой ячейки памяти можно представить таким числом.

Ascii

Первые компьютеры разрабатывались в западных странах, где используется латиница. Текст разбивался на символы, каждому символу соответствовало какое-то число, и эти числа записывалось в память. Таблица, которая отражает соответствие символов и цифр, называется кодировка (charset).

Одна из самых древних кодировок - это ascii (американский стандартный код для обмена информацией), придуманная еще в 1963 году. Ascii содержит 96 видимых символов и 32 невидимых, управляющих символов. Управляющие символы - это символы вроде "перевод строки", которые не вызывают печать символов, а управляют выводом текста на экран или печать.

Каждый символ кодируется ровно 1 байтом (вообще-то, в Ascii 128 символов, и для их кодирования достаточно 7, а не 8 бит, но удобнее, когда каждый символ хранится в своей ячейке. Оставшийся ненужным бит иногда использовался для других целей, например, обозначения конца текста).

Вот таблица кодов Ascii:

+0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15
0+ NUL SOH STX ETX EOT ENQ ACK BEL BS HT LF VT FF CR SO SI
16+ DLE DC1 DC2 DC3 DC4 NAK SYN ETB CAN EM SUB ESC FS GS RS US
32+ ! " # $ % & ' ( ) * + , - . /
48+ 0 1 2 3 4 5 6 7 8 9 : ; < = > ?
64+ @ A B C D E F G H I J K L M N O
80+ P Q R S T U V W X Y Z [ \ ] ^ _
96+ ` a b c d e f g h i j k l m n o
112+ p q r s t u v w x y z { | } ~ DEL

Чтобы определить код символа, надо сложить числа слева и сверху от этого символа. Ну например, символ E имеет код 64 + 5 = 69. А строка cat кодируется 3 байтами с значениями 99, 97, 116.

Как видно, кодировка Ascii содержит такие виды символов:

  • заглавные и прописные латинские буквы
  • цифры
  • математические знаки +-*/=<>% (звездочка * обозначает умножение, а косая черта / - деление), скобки трех видов (()[]{}), знаки препинания, кавычки ", апостроф ' и дополнительные символы вроде #&|^~
  • пробел (код 32)
  • управляющие, невидимые символы с кодами 0-31 и 127 (обозначенные сокращениями вроде HT или LF)

На кнопках стандартной компьютерной клавиатуры представлены все видимые символы ASCII.

Большинство управляющих символов сейчас не используется, а те, что стоит знать, я выделил жирным. Вот они:

  • HT (код 9), табуляция. Она использовалась для форматирования таблиц на печати. Табуляция передвигает курсор (на экране) или печатающую головку (в принтере) на ближайшую позицию, номер которой кратен 8 (или другому числу, которое задает шаг табуляции). Например, если попытаться напечатать hello HT world с шагом табуляции 8, то после hello будет вставлено 3 пробела. Подробнее написано в статье Википедии про табуляцию

    В языках программирования вроде PHP и Javascript вставить символ табуляции в строку можно специальной последовательностью \t, например: echo "Hello\tworld";

  • LF (код 10), line feed, перевод строки. Переставляет курсор на начало новой строки. В языках программирования PHP, Javascript и многих других обозначается последовательностью \n.

    В текстовых файлах обозначает конец строки (однако, под Windows по историческим причинам принято обозначать конец строки двумя идущими подряд символами, CR LF (коды 13 и 10). Когда создавали MS-DOS, в Макинтошах для обозначения конца строки использовался CR, а в Юниксе LF. Майкрософт решила использовать сразу оба символа, чтобы тексты отображались корректно в других системах. В наше время Маки перешли на символ LF, как и Юникс/Линукс).

  • CR (код 13), carriage return, переставляет курсор в начало строки. Используется в текстовых файлах под Windows совместно с LF, чтобы обозначить конец строки. В коде часто пишется как \r.

  • ESC (код 27), используется для создания последовательностей, позволяющих управлять такими параметрами, как цвет выводимого в консоли текста. Подробнее можно прочитать в Википедии в статье про последовательности ANSI

Подробнее эта кодировка описана в статье про ASCII на Википедии.

В старых языках программирования (вроде Си) также использовался символ NUL (код 0) для обозначения конца строки в памяти. В современных языках вроде PHP или Javascript обычно длина строки хранится в памяти отдельно и обозначать ее конец не требуется, и строка может содержать внутри символы NUL, не имеющие специального значения. В PHP и JS вставить в строку символ NUL можно с помощью последовательности символов \0

8-битные кодировки

По мере распространения компьютеров возможностей кодировки Ascii стало не хватать. В европейских языках используются символы вроде Ä, в России используется кириллица, а их в ASCII не было. Потому таблицу Ascii стали расширять, используя ранее не занятые коды 128-255. Коды 0-127 обозначали точно те же символы, что и ранее, и это обеспечивало совместимость со старыми программами и текстовыми файлами. Текстовый ASCII файл правильно отображался в более новых программах.

Также в некоторые кодировки были добавлены символы псевдографики, позволявшие рисовать рамки и окошки в текстовом режиме, вроде таких:

╓────────╖
║ ░ OK   ║
╙────────╜

К сожалению, какого-то единого стандарта не было, и каждый производитель компьютеров и программ придумывал собственную кодировку. Например, только для поддержки кириллицы были разработаны такие кодировки:

  • КОИ-8 (Код Обмена Информацией), описанная в ГОСТ в СССР еще в 1974 году. На ее основе позже была сделана KOI8-R, использовавшаяся в основном в linux. Также, было еще несколько разновидностей КОИ-8, отличавшиеся добавлением украинских, белорусских и среднеазиатских символов: https://ru.wikipedia.org/wiki/%D0%9A%D0%9E%D0%98-8
  • кодировка CP866, разработанная IBM для русской версии MSDOS
  • кодировка Windows-1251, разработанная Microsoft для русскоязычной версии Windows. К сожалению, в ней не было казахских символов, потому в этой стране сделали свой вариант этой кодировки.
  • MacCyrillic, когда-то использовавшаяся на компьютерах Макинтош
  • ISO_8859-5, которую пытались сделать как единый стандарт и замену остальным кодировкам, но было поздно. В итоге эта кодировка почти нигде и не использовалась.

В этих 5 кодировках (а также их альтернативных версиях для стран вроде Украины или Беларуси) одни и те же русские буквы кодировались разными символами (а латинские - везде одинаково). И чтобы просмотреть текстовый файл, надо было угадать кодировку, в которой он записан. Более того, программное обеспечение в те времена часто ничего не знало о кодировках и использовались какие-то единые для всей операционной системы настройки.

С распространением интернета, электронной почты, обмена файлами это начало вызывать проблемы. Браузер или текстовый редактор мог не отобразить корректно текст, созданный на другом компьютере, а средств выбора кодировки в них не было.

Потому в 90-е годы каждый уважающий себя русскоязычный сайт имел версии своих страниц в нескольких кодировках. Вот, например, как выглядела ранняя версия Яндекса: https://www.artlebedev.ru/yandex/site/saved/ - вверху видны ссылки для выбора кодировки.

В азиатских странах проблем было еще больше. Например, кандзи содержат тысячи символов, и закодировать их одним байтом невозможно. Пришлось придумывать кодировку, где символы кандзи кодируются 2 подряд идущими байтами, и это тоже вызывало проблемы, например, многие программы ожидали, что символ занимает 1 байт и не могли верно посчитать их количество. И наоборот, операционные системы, в которые костылями была вбита поддержка кандзи, не могли отображать тексты в европейских кодировках.

Unicode

Решить проблему в рамках 8-битных (где символ занимает 1 байт) кодировок было нельзя. Число разных символов было больше, чем 256. Потому при разработке нового стандарта Unicode решили отказаться от идеи «1 символ = 1 байт» и просто присваивать символам номера по порядку (эти номера называются codepoint), не ограничиваясь каким-либо числом. Таким образом, получается единая таблица кодов символов, которую не придется менять, и можно добавлять новые коды в конец таблицы.

Первые 128 символов с кодами 0-127 повторяют таблицу ASCII, а далее идут символы и буквы различных алфавитов. Цель Юникода - присвоить код всем существующим и существовавшим когда-либо символам. Юникод включает даже буквы умерших алфавитов вроде древнеегипетского, а также современные значки вроде эмодзи. Сейчас Юникод содержит около 110 000 символов.

Кроме обычных символов, Юникод содержит символы-модификаторы. Они печатаются поверх предыдущего символа. Это позволяет, например, добавить кружочек или черточку сверху к любому символу. С другой стороны, это немного усложняет работу с текстом, так как один символ на печати кодируется несколькими кодами (символ + модификаторы). Например, текстовый редактор должен уметь воспринимать такую комбинацию как один символ.

Я советую посмотреть, какие виды символов есть в Юникоде:

Казалось бы, с приходом Юникода проблема разных кодировок исчезнет, но не тут-то было.

Поскольку один байт не позволяет вместить код символа из Юникода, для их хранения приходится выделять несколько байт. 2 байта содержат 16 бит, которыми вместе можно закодировать число от 0 до 65535. В первых версиях Юникода было меньше 65 000 символов, и поначалу их решили кодировать 2 байтами (эта таблица кодов называлась UCS-2), позже, когда символов стало больше, символы решили кодировать 4 байтами (UCS-4, позволяет закодировать чуть более 4 млрд. символов). Но даже с 2 байтами на символ разработчики умудрились сделать 2 (две!) разновидности их кодирования: UTF-16 LE и UTF-16 BE. Они отличаются друг от друга порядком байт. Если в одной версии кодировке символ кодируется байтами A B, то в другой - идущими в обратном порядке байтами B A. Аналогично и для UCS-4 сделали 2 разновидности UTF-32 LE, UTF-32 BE, где байты идут в прямом или обратном порядке. Код символа один и тот же, но он записывается в памяти разными способами. LE расшифровывается как Little Endian, BE как Big Endian, подробнее в статье про порядок байтов.

В UTF-16 символы с кодами менее 55296 (D800 в 16-чной системе счисления) кодируются 2 байтами, и символы с кодами выше (которые появились в UCS-4) кодируются как суррогатная пара, 4 байтами, как будто это 2 обычных символа с кодами больше 55296.

Переход на Юникод требовал полной переделки программ. Библиотеки для операций со строками считали, что один символ занимает 1 байт, и не могли даже посчитать число символов в юникодной строке, не говоря о более сложных операциях. Майкрософт, проделав огромный объем работы, сделала поддержку UTF-16 в Windows, и ей пришлось сделать по 2 варианта каждой системной функции - для старых программ с 1-байтными кодировками, и для новых, юникодных. Появившийся в то время язык Java тоже решил использовать UTF-16 для хранения строк.

Но многие разработчики не хотели переделывать код. Также, англоязычным разработчикам не нравилась необходимость тратить в 2 раза больше места на хранение строк. Потому был придуман еще один способ кодирования Юникода - UTF-8. В этой кодировке символы с кодами 0-127 (из ASCII) кодируются одним байтом, и текст из ASCII-символов кодируется одинаково и в ASCII и в UTF-8. Таким образом, старая программа может обрабатывать UTF-8 текст, если он содержит только латиницу.

Другие символы Юникода в UTF-8 кодируются большим числом байт - от 1 до 6, чем больше код символа, тем больше байт требуется. Кириллица, например, требует 2 байта на символ. Старые программы воспримут символ кириллицы как 2 отдельных символа и могут повредить строку, например, отрезав один байт из двух.

Подробнее про UTF-8 (wiki), про UTF-16 (wiki), UTF-32 (wiki).

Таким образом, есть как минимум 5 вариантов записи кодов Юникода в памяти: UTF-32 LE/BE, UTF-16 LE/BE, UTF-8. Вот их преимущества и недостатки:

  • в UTF-32 каждый символ занимает ровно 4 байта, что позволяет делать операции со строками быстрее. Ну например, чтобы перейти к 500-му символу, нам достаточно умножить 500 на 4 и пропустить 2000 байт от начала строки. В кодировках вроде UTF-8 с разным размером символов нам придется просматривать байты от начала строки, отсчитывая эти 500 символов, что делает любые операции со строками намного медленее. С другой стороны, строки обычно не очень большие по размеру, а процессоры - быстрые, так что в большинстве случаев это не влияет на производительность программы.
  • текст на латинице в UTF-32 занимает в 4 раза больше памяти и места на диске, чем в UTF-8
  • в UTF-16 символ занимает ровно 2 байта при отказе от суррогатных пар (они кодируют редко используемые символы и не всегда нужны), что экономнее, при этом работа со строками оптимизируется. С другой стороны, символы эмодзи (смайлики) кодируются именно суррогатными парами, и для их поддержки приходится отказываться от оптимизаций.
  • UTF-8 позволяет использовать старые библиотеки при условии обработки текстов на латинице и строка в этой кодировке, как правило, занимает меньше места, чем UTF-16 и UTF-32. Но операции со строками становятся медленнее.

На практике в большинстве случаев используют UTF-8. Windows, Java и Javascript внутри используют UTF-16.

Стоит помнить, что текст в памяти или в файле - это лишь набор байт. Чтобы понять, каким символам они соответствуют, надо знать кодировку текста.

Символ BOM

Для автоматического определения разновидности кодировки Юникода было предложено использовать специальный символ BOM (Byte Order Mark) в начале текста. Он кодируется по-разному в каждой из кодировок и не должен выводиться на печать. Увы, если программа не знает о BOM, то этот символ может вызвать какую-то ошибку или вывестись в виде знака вопроса. Например, в PHP наличие этого символа в начале файла может вызвать ошибку при попытке установить куки или выдать HTTP-заголовки. Важно сохранять PHP код без BOM.

Подробнее про BOM (Byte Order Mark)

Поддержка кодировок в PHP

В PHP решили принципиально не поддерживать какую-то определенную кодировку (из-за большого объема работы, нужного для переключения на Юникод). Строки в PHP - это просто последовательности произвольных байт. Строки в исходном коде закодированы в кодировке, использованной при создании PHP файла. Рекомендуется использовать кодировку UTF-8.

При выводе в консоль (например, с помощью echo) байты будут интерпретироваться в зависимости от настроек ОС. В русской версии Windows консоль по умолчанию будет отображать выводимые байты в 8-битной кодировке CP866, в линуксах и маках обычно по умолчанию используется UTF-8. Если исходный код сохранен в UTF-8, под Windows он не сможет без перекодирования выводить русский текст в консоль.

При отображении результата работы скрипта в браузере тот будет воспринимать текст в той кодировке, которая указана в теге <meta charset="utf-8"> либо в опции charset в полученном от сервера заголовке Content-Type: text/html; charset=utf-8. Заголовок имеет приоритет над метатегом, а если кодировка не указана, браузер использует какую-нибудь кодировку по умолчанию, например, Windows-1251 или UTF-8. Стоит всегда указывать кодировку для отдаваемых HTML-страниц.

Многие функции для работы со строками в PHP унаследованы со старых времен и предполагают, что 1 символ занимает 1 байт. Они не могут работать с Юникодом, разве что с UTF-8 текстом на латинице. Например, функция strlen(), которая должна считать длину строки, вернет число байт в строке, а не число символов. Обращение к строке по индексу $s = "Hello"; $char = $s{0}; вернет N-й байт, а не N-й символ строки. Для многобайтовых кодировок функции вроде substr() будут возвращать неправильные результаты.

Не работают с Unicode: strrev, strlen, substr, strpos, ucfirst, wordwrap, str_pad и большинство других строковых функций, для работы которых нужно правильно считать число символов. Не работает задание ширины в функциях вроде sprintf и printf.

Небольшая часть старых функций работает корректно с UTF-8 (но не UTF-16 или UTF-32), так как им не требуется уметь выделять в строке отдельные символы. Вот они: strtr (если передавать массив), str_replace, str_repeat, explode, addslashes, trim.

Подробности можно прочитать в моем уроке про работу с UTF-8 строками.

Функции работы с регулярными выражениями вроде preg_match() требуют наличие флага u в регулярном выражении, чтобы воспринимать строки как UTF-8. Без него они будут считать, что строки закодированы в 1-байтовой кодировке и работать некорректно. Другие варианты Юникода, вроде UTF-16, не поддерживаются.

Для работы с многобайтовыми кодировками в PHP есть расширение mbstring. Оно содержит функции вроде mb_strlen(), поддерживающие текст в разных кодировках. Кодировка указывается либо в аргументах функции, либо в начале программы вызовом mb_internal_encoding().

Если необходимо работать со строкой в UTF-8 посимвольно, можно разбить ее на массив символов таким кодом:

$chars = preg_split("//u", $string, null, PREG_SPLIT_NO_EMPTY);

Выражение //u соответствует границам между UTF-8 символами, а PREG_SPLIT_NO_EMPTY убирает из массива символов 2 пустых строки в начале и конце.

Текст можно преобразовать из одной кодировки в другую функцией iconv.

Функция mb_detect_encoding, и аналогичные, обещающие автоматически определить кодировку текста, на самом деле не работают.

При сравнении (if ($a > $b)) и сортировке строк сравниваются значения байт, которыми они закодированы. Ну например, если попытаться функцией sort отсортировать массив ['apple', 'cat', 'BYTE'], то получится ['BYTE', 'apple', 'cat'], так как буква B кодируется как 66, a как 97, а c как 99. На практике такая сорировка малополезна.

Для правильного сравнения и сортировки строк стоит использовать класс Collator из расширения intl. Стоит учесть, что в разных странах приняты разные правила сортировки (особенно для букв с точечками сверху), и надо указать нужные правила при использовании класса. Подробнее я это описал в моем уроке про сравнение строк.

При соединении с базой данных через расширение PDO или mysqli необходимо указывать, в какой кодировке передаются данные (обычно это UTF-8).

Получить i-й (начиная с нуля) байт строки в виде числа можно функцией $byte = ord($string{$i}). Собрать строку из байтов можно кодом вида $string = chr(65) . chr(66) . chr(67); или $string = pack("c*", 65, 66, 67);.

Если строка записана в двойных кавычках, то в нее можно вставить произвольные байты, зная их код в 16-чной системе счисления. Например, символ @ кодируется в utf-8 кодом 64, или 4016 в 16-чной системе счисления. Соответственно, последовательность \x40 вставляет в строку байт с кодом 64, и echo "\x40"; выведет символ @. С помощью таких кодов в строку можно вставить любые байты, от \x00 (код 0) до \xff (код 255), надо только перевести коды в 16-чную систему.

Поддержка кодировок в Яваскрипте

В Яваскрипт строки хранят символы, закодированные в UTF-16. Яваскрипт считает, что 1 символ занимает 2 байта, что верно для большинства, но не для всех символов Юникода. Новые символы вроде эмодзи с большими кодами кодируются 4 байтами (суррогатной парой) и будут восприниматься Яваскриптом как 2 отдельных символа.

Если яваскрипт встроен в HTML-страницу, то исходный код воспринимается в кодировке страницы. Если он хранится в отдельном .js файле, он будет интерпретироваться как код в кодировке UTF-8 (речь идет имеено об интерпретации кода, при выполнении кода строки все равно будут преобразованы в UTF-16).

Так как Яваскрипт воспринимает строки как набор символов, то встроенные функции работы со строками и регулярные выражения работают корректно (за исключением суррогатных пар). Например, метод str.charAt(i) вернет i-й символ строки.

Сравнение и сортировка строк по умолчанию происходит с использованием кодов символов. Для правильного сравнения по алфавиту с учетом особенностей языка необходимо использовать либо метод localeCompare, либо класс Intl.Collator. Эти возможности есть только в новых браузерах, и для поддержки старых браузеров надо искать библиотеку-полифилл.

Получить Юникодный код i-го символа строки можно методом charCodeAt: var code = s.charCodeAt(i), собрать строку из кодов можно так: var s = String.fromCharCode(66, 67, 68);. Но символы, кодируемые суррогатными парами, будут восприниматся как 2 отдельных символа. В новой версии Яваскрипт предложены методы String.prototype.codePointAt(i) и String.fromCodePoint(), не имеющие этого недостатка.

Подробнее о строках в Javascript.

Определение кодировки текста

На первый взгляд, определить кодировку, имея текстовый файл, невозможно, так как она в нем не задана. Но если человек попробует просматривать этот файл в различных кодировках, он скорее всего сможет сказать, какая из них правильная. Ведь например текст на русском языке состоит в основном из букв кириллицы, а не из случайных символов. В разных кодировках эти буквы кодируются разными байтами, и посчитав частоту, с которой каждый байт встречается в тексте, можно сделать выводы о вероятности, что текст закодирован в той или иной кодировке.

К примеру, в кодировке Windows-1251 байты с кодами 192-255 кодируют русские буквы, а байты с кодами 128-191 - различные редковстречающиеся символы и буквы вроде ¤ или Љ. Если в тексте часто встречаются коды 192-255, и редко - 128-191, то, возможно этот текст использует кодировку Windows-1251. И наоборот, если текст содержит в основном байты 128-191, кодировка скорее всего другая.

Также, можно анализировать сочетания (пары или тройки) букв. В русском языке часто встречаются сочетания вроде "кот", но не встречаются тройки вроде "бзщ".

Еще можно анализировать частоту появления букв. Буква "а" встречается гораздо чаще, чем буква "щ".

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

Также, можно анализировать особенности кодировки. Например, в UTF-8 не могут встречаться определенные последовательности байт. Если их много, то текст скорее всего в другой кодировке.

Задачи

Задачи для того, чтобы закрепить материал и лучше понять способы кодирования текста.

  1. Дана строка в UTF-8. Выведите коды Юникодных символов (codepoint), из которых состоит текст

    Подсказка: можно конечно написать алгоритм раскодировки UTF-8 в коды символов, но это долго и легко сделать ошибку. Проще будет сконвертирвать строку в UTF-16 или UTF-32, где каждый символ кодируется одинаковым числом байт. Например, для UTF-16 (без суррогатных пар) код символа получается как $byte1 * 256 + $byte2, где $byte1 и $byte2 - это значение 2 байтов, которыми закодирован символ.

  2. Дан файл с текстом из русских или английских букв в неизвестной кодировке. Определите кодировку текста.

    Подсказка: надо написать функцию, определяющую вероятность, что текст закодирован в кодировке N. Вызывая эту функцию для всех кодировок, надо найти ту, которая даст наибольшую вероятность (при этом другие кодировки должны показывать низкую вероятность).