Перейти к основному содержимому

Руководство по кодировке символов

· 14 мин. чтения

1. Обзор

В этом уроке мы обсудим основы кодирования символов и то, как мы справляемся с этим в Java.

2. Важность кодировки символов

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

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

Чтобы лучше понять это, давайте определим метод декодирования текста в Java:

String decodeText(String input, String encoding) throws IOException {
return
new BufferedReader(
new InputStreamReader(
new ByteArrayInputStream(input.getBytes()),
Charset.forName(encoding)))
.readLine();
}

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

Если мы запустим этот метод с входными данными как «Шаблон фасада — это шаблон проектирования программного обеспечения». и кодируя как «US-ASCII» , он выведет:

The fa��ade pattern is a software design pattern.

Ну, не совсем то, что мы ожидали.

Что могло пойти не так? Мы постараемся понять и исправить это в оставшейся части этого руководства.

3. Основы

Прежде чем копнуть глубже, давайте быстро рассмотрим три термина: кодирование , наборы символов и кодовая точка .

3.1. Кодирование

Компьютеры могут понимать только двоичные представления, такие как 1 и 0 . Обработка чего-либо еще требует некоторого преобразования реального текста в его двоичное представление. Это сопоставление называется кодировкой символов или просто кодировкой .

Например, первая буква в нашем сообщении «Т» в US-ASCII кодируется как «01010100».

3.2. Наборы символов

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

Например, ASCII имеет кодировку из 128 символов .

3.3. Кодовая точка

Кодовая точка — это абстракция, которая отделяет символ от его фактической кодировки. Кодовая точка — это целочисленная ссылка на конкретный символ.

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

Например, первая буква в нашем сообщении, T, в Unicode имеет кодовую точку «U+0054» (или 84 в десятичной системе).

4. Понимание схем кодирования

Кодировка символов может принимать различные формы в зависимости от количества символов, которые она кодирует.

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

Давайте сегодня пройдемся по некоторым популярным схемам кодирования на практике.

4.1. Однобайтовое кодирование

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

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

Давайте определим простой метод на Java для отображения двоичного представления символа в определенной схеме кодирования:

String convertToBinary(String input, String encoding) 
throws UnsupportedEncodingException {
byte[] encoded_input = Charset.forName(encoding)
.encode(input)
.array();
return IntStream.range(0, encoded_input.length)
.map(i -> encoded_input[i])
.mapToObj(e -> Integer.toBinaryString(e ^ 255))
.map(e -> String.format("%1$" + Byte.SIZE + "s", e).replace(" ", "0"))
.collect(Collectors.joining(" "));
}

Теперь символ «T» имеет кодовую точку 84 в US-ASCII (ASCII упоминается как US-ASCII в Java).

И если мы используем наш служебный метод, мы можем увидеть его бинарное представление:

assertEquals(convertToBinary("T", "US-ASCII"), "01010100");

Как мы и ожидали, это семибитное двоичное представление символа 'T'.

Исходный ASCII оставил неиспользованным старший бит каждого байта. В то же время ASCII оставил довольно много символов непредставленными, особенно для неанглийских языков.

Это привело к попытке использовать этот неиспользуемый бит и включить дополнительные 128 символов.

Было предложено и принято несколько вариантов схемы кодирования ASCII. Их вольно стали называть «расширениями ASCII».

Многие из расширений ASCII имели разный уровень успеха, но, очевидно, этого было недостаточно для более широкого распространения, поскольку многие символы все еще не были представлены.

Одним из наиболее популярных расширений ASCII было ISO-8859-1 , также называемое «ISO Latin 1».

4.2. Многобайтовое кодирование

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

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

BIG5 и SHIFT-JIS являются примерами многобайтовых схем кодирования символов, которые начали использовать как один, так и два байта для представления более широких наборов символов . Большинство из них были созданы для представления китайских и подобных им шрифтов, которые имеют значительно большее количество символов.

Давайте теперь вызовем метод convertToBinary с вводом в виде «語», китайского символа и кодировкой «Big5»:

assertEquals(convertToBinary("語", "Big5"), "10111011 01111001");

Вывод выше показывает, что кодировка Big5 использует два байта для представления символа «語».

Полный список кодировок символов вместе с их псевдонимами поддерживается Международным управлением нумерации.

5. Юникод

Нетрудно понять, что, хотя кодирование важно, декодирование не менее важно для понимания представлений. На практике это возможно только в том случае, если широко используется непротиворечивая или совместимая схема кодирования.

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

Эта проблема привела к появлению единого стандарта кодирования под названием Unicode, который поддерживает все возможные символы в мире . Это включает в себя символы, которые используются, и даже те, которые больше не существуют!

Ну, что должно требовать несколько байтов для хранения каждого символа? Честно говоря, да, но у Unicode есть гениальное решение.

Юникод как стандарт определяет кодовые точки для каждого возможного символа в мире. Кодовая точка для символа «T» в Unicode — 84 в десятичном формате. Обычно мы называем это «U + 0054» в Unicode, что представляет собой не что иное, как U +, за которым следует шестнадцатеричное число.

Мы используем шестнадцатеричный код в качестве основы для кодовых точек в Unicode, поскольку существует 1 114 112 точек, что является довольно большим числом для удобного общения в десятичной системе!

То, как эти кодовые точки кодируются в биты, зависит от конкретных схем кодирования в Unicode. Мы рассмотрим некоторые из этих схем кодирования в подразделах ниже.

5.1. UTF-32

UTF-32 — это схема кодирования для Unicode, которая использует четыре байта для представления каждой кодовой точки , определенной Unicode. Очевидно, что использование четырех байтов для каждого символа неэффективно.

Давайте посмотрим, как простой символ, такой как «T», представлен в UTF-32. Мы будем использовать метод convertToBinary, представленный ранее:

assertEquals(convertToBinary("T", "UTF-32"), "00000000 00000000 00000000 01010100");

Приведенный выше вывод показывает использование четырех байтов для представления символа «T», где первые три байта — это просто потраченное впустую пространство.

5.2. UTF-8

UTF-8 — это еще одна схема кодирования для Unicode, в которой для кодирования используются байты переменной длины . Хотя для кодирования символов обычно используется один байт, при необходимости можно использовать большее количество байтов, что позволяет сэкономить место.

Давайте снова вызовем метод convertToBinary с вводом как «T» и кодировкой «UTF-8»:

assertEquals(convertToBinary("T", "UTF-8"), "01010100");

Вывод точно такой же, как в ASCII, с использованием всего одного байта. На самом деле UTF-8 полностью обратно совместим с ASCII.

Давайте снова вызовем метод convertToBinary с вводом как «語» и кодировкой как «UTF-8»:

assertEquals(convertToBinary("語", "UTF-8"), "11101000 10101010 10011110");

Как мы видим здесь, UTF-8 использует три байта для представления символа «語». Это известно как кодирование с переменной шириной .

UTF-8, благодаря своей эффективности использования пространства, является наиболее распространенной кодировкой, используемой в Интернете.

6. Поддержка кодирования в Java

Java поддерживает широкий спектр кодировок и их преобразования друг в друга. Класс Charset определяет набор стандартных кодировок , которые каждая реализация платформы Java обязана поддерживать.

Сюда входят US-ASCII, ISO-8859-1, UTF-8 и UTF-16 и многие другие. Конкретная реализация Java может дополнительно поддерживать дополнительные кодировки .

Есть некоторые тонкости в том, как Java подбирает кодировку для работы. Давайте пройдемся по ним более подробно.

6.1. Набор символов по умолчанию

Платформа Java сильно зависит от свойства, называемого набором символов по умолчанию . Виртуальная машина Java (JVM) определяет кодировку по умолчанию во время запуска .

Это зависит от языкового стандарта и набора символов базовой операционной системы, в которой работает JVM. Например, в MacOS кодировка по умолчанию — UTF-8.

Давайте посмотрим, как мы можем определить кодировку по умолчанию:

Charset.defaultCharset().displayName();

Если мы запустим этот фрагмент кода на компьютере с Windows, мы получим:

windows-1252

Теперь «windows-1252» — это кодировка по умолчанию для платформы Windows на английском языке, которая в данном случае определяет кодировку по умолчанию для JVM, работающей в Windows.

6.2. Кто использует кодировку по умолчанию?

Многие API-интерфейсы Java используют кодировку по умолчанию, определенную JVM. Назвать несколько:

  • InputStreamReader и FileReader
  • OutputStreamWriter и FileWriter
  • Форматтер и сканер
  • URLEncoder и URLDecoder

Итак, это означает, что если бы мы запустили наш пример без указания кодировки:

new BufferedReader(new InputStreamReader(new ByteArrayInputStream(input.getBytes()))).readLine();

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

И есть несколько API, которые делают такой же выбор по умолчанию.

Таким образом, кодировка по умолчанию приобретает важность, которую мы не можем игнорировать.

6.3. Проблемы с кодировкой по умолчанию

Как мы видели, кодировка по умолчанию в Java определяется динамически при запуске JVM. Это делает платформу менее надежной или подверженной ошибкам при использовании в разных операционных системах.

Например, если мы запустим

new BufferedReader(new InputStreamReader(new ByteArrayInputStream(input.getBytes()))).readLine();

в macOS будет использоваться UTF-8.

Если мы попробуем тот же фрагмент в Windows, он будет использовать Windows-1252 для декодирования того же текста.

Или представьте, что вы записываете файл в macOS, а затем читаете тот же файл в Windows.

Нетрудно понять, что из-за разных схем кодирования это может привести к потере или повреждению данных.

6.4. Можем ли мы переопределить кодировку по умолчанию?

Определение кодировки по умолчанию в Java приводит к двум системным свойствам:

  • file.encoding : значением этого системного свойства является имя набора символов по умолчанию.
  • sun.jnu.encoding : значение этого системного свойства — это имя набора символов, используемого при кодировании/декодировании путей к файлам.

Теперь интуитивно понятно переопределить эти системные свойства с помощью аргументов командной строки:

-Dfile.encoding="UTF-8"
-Dsun.jnu.encoding="UTF-8"

Однако важно отметить, что эти свойства в Java доступны только для чтения. Их использование, как указано выше, в документации отсутствует . Переопределение этих системных свойств может привести к нежелательному или предсказуемому поведению.

Следовательно, нам следует избегать переопределения кодировки по умолчанию в Java .

6.5. Почему Java не решает эту проблему?

Существует Предложение по усовершенствованию Java (JEP), которое предписывает использовать «UTF-8» в качестве кодировки по умолчанию в Java вместо того, чтобы основывать его на кодировке локали и операционной системы.

На данный момент этот JEP находится в черновом состоянии, и когда он (надеюсь!) будет принят, он решит большинство проблем, которые мы обсуждали ранее.

Обратите внимание, что более новые API, такие как в java.nio.file.Files , не используют кодировку по умолчанию. Методы в этих API читают или записывают потоки символов с кодировкой UTF-8, а не с кодировкой по умолчанию.

6.6. Решение этой проблемы в наших программах

Обычно при работе с текстом следует указывать кодировку, а не полагаться на настройки по умолчанию . Мы можем явно объявить кодировку, которую хотим использовать, в классах, которые имеют дело с преобразованием символов в байты.

К счастью, в нашем примере кодировка уже указана. Нам просто нужно выбрать правильный, а Java сделает все остальное.

К настоящему времени мы должны понять, что символы с диакритическими знаками, такие как 'ç', отсутствуют в схеме кодирования ASCII, и, следовательно, нам нужна кодировка, которая их включает. Возможно, UTF-8?

Давайте попробуем, теперь мы запустим метод decodeText с тем же вводом, но в кодировке «UTF-8»:

The façade pattern is a software-design pattern.

Бинго! Мы можем увидеть результат, который мы надеялись увидеть сейчас.

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

Точно так же OutputStreamWriter и многие другие API поддерживают настройку схемы кодирования через свой конструктор.

6.7. MalformedInputException

Когда мы декодируем последовательность байтов, бывают случаи, когда это недопустимо для данного набора символов или это недопустимый шестнадцатибитный Unicode. Другими словами, данная последовательность байтов не имеет отображения в указанном Charset .

Существует три предопределенных стратегии (или CodingErrorAction ), когда входная последовательность имеет искаженный ввод:

  • IGNORE игнорирует неправильно сформированные символы и возобновляет операцию кодирования.
  • REPLACE заменит искаженные символы в выходном буфере и возобновит операцию кодирования.
  • ОТЧЕТ вызовет исключение MalformedInputException .

malformedInputAction по умолчанию для CharsetDecoder — REPORT, а malformedInputAction по умолчанию для декодера по умолчанию в InputStreamReaderREPLACE.

Давайте определим функцию декодирования, которая получает указанный Charset , тип CodingErrorAction и строку для декодирования:

String decodeText(String input, Charset charset, 
CodingErrorAction codingErrorAction) throws IOException {
CharsetDecoder charsetDecoder = charset.newDecoder();
charsetDecoder.onMalformedInput(codingErrorAction);
return new BufferedReader(
new InputStreamReader(
new ByteArrayInputStream(input.getBytes()), charsetDecoder)).readLine();
}

Итак, если мы расшифруем «шаблон фасада — это шаблон проектирования программного обеспечения». с US_ASCII вывод для каждой стратегии будет другим. Во- первых, мы используем CodingErrorAction.IGNORE , который пропускает недопустимые символы:

Assertions.assertEquals(
"The faade pattern is a software design pattern.",
CharacterEncodingExamples.decodeText(
"The façade pattern is a software design pattern.",
StandardCharsets.US_ASCII,
CodingErrorAction.IGNORE));

Для второго теста мы используем CodingErrorAction.REPLACE , который помещает � вместо недопустимых символов:

Assertions.assertEquals(
"The fa��ade pattern is a software design pattern.",
CharacterEncodingExamples.decodeText(
"The façade pattern is a software design pattern.",
StandardCharsets.US_ASCII,
CodingErrorAction.REPLACE));

Для третьего теста мы используем CodingErrorAction.REPORT , что приводит к выдаче MalformedInputException:

Assertions.assertThrows(
MalformedInputException.class,
() -> CharacterEncodingExamples.decodeText(
"The façade pattern is a software design pattern.",
StandardCharsets.US_ASCII,
CodingErrorAction.REPORT));

7. Другие места, где важно кодирование

Нам не нужно просто учитывать кодировку символов при программировании. Тексты могут окончательно испортиться во многих других местах.

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

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

7.1. Текстовые редакторы

В большинстве случаев текстовый редактор является источником текстов. Существует множество популярных текстовых редакторов, включая vi, Notepad и MS Word. Большинство этих текстовых редакторов позволяют нам выбирать схему кодирования. Следовательно, мы всегда должны убедиться, что они подходят для текста, с которым мы работаем.

7.2. Файловая система

После того, как мы создадим тексты в редакторе, нам нужно сохранить их в какой-то файловой системе. Файловая система зависит от операционной системы, в которой она работает. Большинство операционных систем имеют встроенную поддержку нескольких схем кодирования. Однако все еще могут быть случаи, когда преобразование кодировки приводит к потере данных.

7.3. Сеть

Тексты при передаче по сети с использованием протокола, такого как протокол передачи файлов (FTP), также включают преобразование между кодировками символов. Все, что закодировано в Unicode, безопаснее всего передавать в виде двоичного кода, чтобы свести к минимуму риск потери при преобразовании. Однако передача текста по сети является одной из менее частых причин повреждения данных.

7.4. Базы данных

Большинство популярных баз данных, таких как Oracle и MySQL, поддерживают выбор схемы кодировки символов при установке или создании баз данных. Мы должны выбрать это в соответствии с текстами, которые мы ожидаем хранить в базе данных. Это одно из наиболее частых мест, где происходит повреждение текстовых данных из-за преобразований кодирования.

7.5. Браузеры

Наконец, в большинстве веб-приложений мы создаем тексты и пропускаем их через разные уровни с намерением просматривать их в пользовательском интерфейсе, таком как браузер. Здесь также крайне важно выбрать правильную кодировку символов, которая может правильно отображать символы. Большинство популярных браузеров, таких как Chrome, Edge, позволяют выбирать кодировку символов через свои настройки.

8. Заключение

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

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

Мы также взяли пример неправильного использования кодировки символов в Java и увидели, как это сделать правильно. Наконец, мы обсудили некоторые другие распространенные сценарии ошибок, связанные с кодировкой символов.

Как всегда, код примеров доступен на GitHub .