Руководство по кодированию символов

Руководство по кодированию символов

1. обзор

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

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

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

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

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

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

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

If we run this method with input as “The façade pattern is a software design pattern.” and encoding as “US-ASCII”, он выведет:

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

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

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

3. основы

Однако, прежде чем копать глубже, давайте быстро рассмотрим три термина:encoding,charsets иcode point.

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

Компьютеры могут понимать только двоичные представления, такие как1 и0. Обработка чего-либо еще требует некоторого отображения от реального текста до его двоичного представления. This mapping is what we know as character encoding or simply just as encoding.

Например, первая буква в нашем сообщении «T» в US-ASCIIencodes to «01010100».

3.2. Кодировки

Отображение символов в их двоичные представления может сильно различаться в зависимости от символов, которые они включают. Количество символов, включенных в отображение, может варьироваться от нескольких до всех символов при практическом использовании. The set of characters that are included in a mapping definition is formally called a charset.

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

Кодовая точка - это абстракция, отделяющая символ от его фактической кодировки. A code point is an integer reference to a particular character.с

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

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

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

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

Количество закодированных символов имеет прямое отношение к длине каждого представления, которая обычно измеряется как число байтов. Having more characters to encode essentially means needing lengthier binary representations.с

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

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

Одна из самых ранних схем кодирования, называемая ASCII (американский стандартный код для обмена информацией), использует однобайтовую схему кодирования. По сути, это означает, чтоeach character in ASCII is represented with seven-bit binary numbers. Это все еще оставляет один бит свободным в каждом байте!

Набор из 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 '.

The original ASCII left the most significant bit of every byte unused. В то же время, ASCII оставил довольно много символов непредставленными, особенно для неанглийских языков.

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

There were several variations of the ASCII encoding scheme proposed and adopted over the time. Их в общих чертах стали называть «расширениями ASCII».

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

One of the more popular ASCII extensions was ISO-8859-1, также обозначаемый как «ISO Latin 1».

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

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

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

BIG5 и SHIFT-JIS являются примерамиmulti-byte character encoding schemes which started to use one as well as two bytes to represent wider charsets. Большинство из них были созданы для того, чтобы представлять китайские и аналогичные сценарии, которые имеют значительно большее количество символов.

Теперь вызовем методconvertToBinary сinput как "", китайский символ, аencoding как "Big5":

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

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

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

5. Unicode

Нетрудно понять, что, хотя кодирование важно, декодирование одинаково важно для понимания представлений. This is only possible in practice if a consistent or compatible encoding scheme is used widely.с

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

Эта проблема привела кa singular encoding standard called Unicode which has the capacity for every possible character in the world. Это включает в себя символы, которые используются и даже те, которые больше не существуют!

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

Unicode as a standard defines code points for every possible character in the world. Кодовая точка для символа «T» в Юникоде - 84 в десятичной системе. Мы обычно называем это «U + 0054» в Unicode, который является ничем иным, как U +, за которым следует шестнадцатеричное число.

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

How these code points are encoded into bits is left to specific encoding schemes within Unicode. Мы рассмотрим некоторые из этих схем кодирования в подразделах ниже.

5.1. UTF-32

UTF-32 - этоan encoding scheme for Unicode that employs four bytes to represent every code point, определенный Unicode. Очевидно, неэффективно использование четырех байтов для каждого символа.

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

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

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

5.2. UTF-8,

UTF-8 - этоanother encoding scheme for Unicode which employs a variable length of bytes to encode. Хотя он обычно использует один байт для кодирования символов, при необходимости он может использовать большее количество байтов, тем самым экономя место.

Давайте снова вызовем метод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 использует три байта для представления символа ‘語 '. This is known as variable-width encoding.с

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

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

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

Это включает в себя US-ASCII, ISO-8859-1, UTF-8 и UTF-16 и многие другие. A particular implementation of Java may optionally support additional encodings.

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

6.1. Кодировка по умолчанию

Платформа Java сильно зависит от свойстваthe default charset. The Java Virtual Machine (JVM) determines the default charset during start-up.

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

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

Charset.defaultCharset().displayName();

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

windows-1252

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

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

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

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

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. Their usage as above is not present in the documentation. Переопределение этих системных свойств может не иметь желаемого или предсказуемого поведения.

Следовательно,we should avoid overriding the default charset in Java.

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

ЭтоJava Enhancement Proposal (JEP) which prescribes using “UTF-8” as the default charset in Java вместо того, чтобы основывать его на кодировке языка и операционной системы.

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

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

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

Обычно мы должныchoose to specify a charset when dealing with text instead of relying on the default settings. Мы можем явно объявить кодировку, которую мы хотим использовать в классах, которые имеют дело с символьно-байтовыми преобразованиями.

К счастью, наш пример уже указывает кодировку. We just need to select the right one and let Java do the rest.с

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

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

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

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

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

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

6.7. MalformedInputExceptionс

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

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

  • IGNORE игнорирует искаженные символы и возобновляет операцию кодирования

  • REPLACE заменит искаженные символы в выходном буфере и возобновит операцию кодирования

  • REPORT выдастMalformedInputException

По умолчаниюmalformedInputAction дляCharsetDecoder is REPORT, иmalformedInputAction по умолчанию для декодера по умолчанию вInputStreamReader -REPLACE.

Давайте определим функцию декодирования, которая получает указанный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. Другие места, где важна кодировка

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

most common cause of problems in these cases is the conversion of text from one encoding scheme to another, что может привести к потере данных.

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

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

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

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

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

7.3. сеть

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

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

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

7.5. Браузеры

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

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

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

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

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

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