Как проверить, содержит ли массив байтов строку Unicode в Java?

Учитывая массив байтов, который является либо кодированной кодировкой UTF-8, либо произвольными двоичными данными, какие подходы можно использовать в Java, чтобы определить, что это такое?

Массив может быть сгенерирован кодом, похожим на:

byte[] utf8 = "Hello World".getBytes("UTF-8");

В качестве альтернативы он может быть сгенерирован кодом, похожим на:

byte[] messageContent = new byte[256];
for (int i = 0; i < messageContent.length; i++) {
    messageContent[i] = (byte) i;
}

Ключевым моментом является то, что мы не знаем, что содержит массив, но нужно выяснить, чтобы заполнить следующую функцию:

public final String getString(final byte[] dataToProcess) {
    // Determine whether dataToProcess contains arbitrary data or a UTF-8 encoded string
    // If dataToProcess contains arbitrary data then we will BASE64 encode it and return.
    // If dataToProcess contains an encoded string then we will decode it and return.
}

Как это будет распространено на покрытие UTF-16 или других механизмов кодирования?

Ответы

Ответ 1

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

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

Тем не менее, можно получить действительные данные UTF-8, которые декодируют абсолютно бессмысленную строку символов (возможно, из всех видов разных скриптов). Это более вероятно с короткими последовательностями. Если вы беспокоитесь об этом, вам, возможно, придется провести более тщательный анализ, чтобы увидеть, принадлежат ли символы, которые являются буквами, к той же диаграмме кода , Опять же, это может привести к ложным негативам, когда у вас есть действительный ввод текста, который смешивает скрипты.

Ответ 2

Здесь можно использовать "двоичное" регулярное выражение UTF-8 из сайта W3C

static boolean looksLikeUTF8(byte[] utf8) throws UnsupportedEncodingException 
{
  Pattern p = Pattern.compile("\\A(\n" +
    "  [\\x09\\x0A\\x0D\\x20-\\x7E]             # ASCII\\n" +
    "| [\\xC2-\\xDF][\\x80-\\xBF]               # non-overlong 2-byte\n" +
    "|  \\xE0[\\xA0-\\xBF][\\x80-\\xBF]         # excluding overlongs\n" +
    "| [\\xE1-\\xEC\\xEE\\xEF][\\x80-\\xBF]{2}  # straight 3-byte\n" +
    "|  \\xED[\\x80-\\x9F][\\x80-\\xBF]         # excluding surrogates\n" +
    "|  \\xF0[\\x90-\\xBF][\\x80-\\xBF]{2}      # planes 1-3\n" +
    "| [\\xF1-\\xF3][\\x80-\\xBF]{3}            # planes 4-15\n" +
    "|  \\xF4[\\x80-\\x8F][\\x80-\\xBF]{2}      # plane 16\n" +
    ")*\\z", Pattern.COMMENTS);

  String phonyString = new String(utf8, "ISO-8859-1");
  return p.matcher(phonyString).matches();
}

Как первоначально написано, регулярное выражение предназначено для использования в байтовом массиве, но вы не можете делать это с помощью регулярных выражений Java; цель должна быть чем-то, что реализует интерфейс CharSequence (так что char[] тоже). Декодируя byte[] как ISO-8859-1, вы создаете строку, в которой каждый char имеет такое же неподписанное числовое значение, что и соответствующий байт в исходном массиве.

Как отмечали другие, тесты, подобные этому, могут только сказать вам, что byte[] может содержать текст UTF-8, а не то, что он делает. Но регулярное выражение настолько исчерпывающее, что крайне маловероятно, что из него могут проскользнуть исходные двоичные данные. Даже массив из всех нулей не будет соответствовать, так как регулярное выражение никогда не совпадает с NUL. Если единственные возможности - UTF-8 и двоичные, я бы хотел доверять этому тесту.

И пока вы на нем, вы можете лишить спецификацию UTF-8, если она есть; в противном случае UTF-8 CharsetDecoder передаст его, как если бы это был текст.

UTF-16 будет намного сложнее, потому что очень мало байтовых последовательностей, которые всегда недействительны. Единственные, о которых я могу думать, - это суррогатные персонажи с низким уровнем суррогатов, или наоборот. Помимо этого, вам понадобится какой-то контекст, чтобы решить, действительна ли данная последовательность. У вас может быть кириллическое письмо, сопровождаемое китайской идеограммой, сопровождаемой дингбатом с смайликом, но это будет совершенно верно UTF-16.

Ответ 3

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

Строка Java - это последовательность из 16 битовых величин, которые соответствуют одному из (почти) 2 ** 16 базовых кодовых точек Unicode. Но если вы посмотрите на эти 16-битные "символы", каждый из них может в равной мере представлять целое число, пару байтов, пиксель и т.д. Битовые шаблоны не имеют ничего внутреннего, что говорит о том, что они представляют.

Теперь предположим, что вы перефразировали свой вопрос и попросили способ отличить TEXT с кодировкой UTF-8 от произвольных двоичных данных. Помогает ли это? Теоретически нет, потому что битовые шаблоны, которые кодируют любой письменный текст, также могут быть последовательностью чисел. (Трудно сказать, что здесь означает "произвольный". Можете ли вы рассказать мне, как проверить, является ли число "произвольным"?)

Самое лучшее, что мы можем сделать здесь, это следующее:

  • Проверьте, являются ли байты допустимой кодировкой UTF-8.
  • Проверьте, все ли декодированные 16-битные величины являются законными, "назначены" кодовыми точками UTF-8. (Некоторые 16-разрядные количества являются незаконными (например, 0xffff), а другие в настоящее время не назначены для соответствия любому символу.) Но что, если текстовый документ действительно использует неназначенный код?
  • Проверьте, соответствуют ли кодовые страницы Unicode "плоскостям", которые вы ожидаете, на основе предполагаемого языка документа. Но что, если вы не знаете, какого языка ожидать, или если документ, который использует несколько языков?
  • Тест - это последовательности кодовых точек, которые выглядят как слова, предложения или что-то еще. Но что, если бы у нас были некоторые "двоичные данные", которые включали встроенные текстовые последовательности?

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

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

Ответ 4

Если массив байтов начинается с Byte Order Mark (BOM), тогда будет легко отличить, какая кодировка была использована. Стандартные классы Java для обработки текстовых потоков, вероятно, будут иметь дело с этим для вас автоматически.

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

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

Ответ 5

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

Ответ 6

Я думаю, что Майкл объяснил это хорошо в своем ответе, это может быть единственный способ узнать, содержит ли массив байтов все допустимые последовательности utf-8. Я использую следующий код в php

function is_utf8($string) {

    return preg_match('%^(?:
          [\x09\x0A\x0D\x20-\x7E]            # ASCII
        | [\xC2-\xDF][\x80-\xBF]             # non-overlong 2-byte
        |  \xE0[\xA0-\xBF][\x80-\xBF]        # excluding overlongs
        | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}  # straight 3-byte
        |  \xED[\x80-\x9F][\x80-\xBF]        # excluding surrogates
        |  \xF0[\x90-\xBF][\x80-\xBF]{2}     # planes 1-3
        | [\xF1-\xF3][\x80-\xBF]{3}          # planes 4-15
        |  \xF4[\x80-\x8F][\x80-\xBF]{2}     # plane 16
    )*$%xs', $string);

} 

Взято из W3.org