如何在Java中检查字节数组是否包含Unicode字符串?

16

在 Java 中,给定一个字节数组,它可能是一个 UTF-8 编码的字符串或者任意二进制数据,有哪些方法可以确定它是哪种类型?

该数组可能是类似以下代码生成的:

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
一个类似的问题有一些来自Edward Wilde的有用链接 - https://dev59.com/83RC5IYBdhLWcg3wMeHg - JonoW
7个回答

11

在所有情况下,完全准确地做出这个决定是不可能的,因为一个UTF-8编码的字符串实际上是一种任意二进制数据,但你可以查找UTF-8中无效字节序列。如果找到了任何这样的序列,就知道它不是UTF-8。

如果您的数组足够大,这应该能很好地解决问题,因为这些序列很可能出现在“随机”的二进制数据中,例如压缩数据或图像文件。

然而,有可能得到有效的UTF-8数据,但解码后却得到一串完全没有意义的字符(可能来自各种不同的脚本)。这在短序列中更有可能发生。如果您担心这个问题,您可能需要进行更详细的分析,以确定所有文字字符是否属于同一个代码表。然而,在混合使用不同脚本的有效文本输入时,这可能会产生错误的负面结果。


5
这里有一种使用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 BOM,则可以将其去除;否则,UTF-8 CharsetDecoder将将其作为文本传递。
UTF-16会更加困难,因为很少有字节序列是始终无效的。我能想到的唯一的例外是缺少低代理伴侣或高代理字符的情况。除此之外,您需要一些上下文来确定给定序列是否有效。您可能会遇到一个西里尔字母后跟一个汉字表意符号,再跟一个笑脸图案,但这在UTF-16中是完全有效的。

3
这个问题假设字符串和二进制数据之间存在根本区别。尽管这是直观的,但准确定义这种差异几乎是不可能的。
Java字符串是一系列16位数量,对应于(几乎)2 ** 16个Unicode基本代码点之一。 但如果你看看这些16位的“字符”,每个字符都可以等同地表示整数、字节对、像素等等。这些比特模式没有任何固有的东西表明它们代表什么。
现在假设您重新以另一种方式提出问题,询问如何区分UTF-8编码文本和任意二进制数据。这有帮助吗?从理论上讲不是,因为编码任何书面文本的比特模式也可以是数字序列。(很难说“任意”在这里实际上意味着什么。你能告诉我如何测试一个数字是否“任意”吗?)
在此我们所能做的最好的事情是:
1. 测试字节是否是有效的UTF-8编码。 2. 测试解码后的16位数量是否全部是合法的“已分配”的UTF-8代码点。 (某些16位数量是非法的(例如0xffff),而其他一些目前未分配相应于任何字符。)但是,如果文本文档确实使用未分配的代码点呢? 3. 测试Unicode代码点是否属于您根据文档的假定语言期望的“平面”。 但是,如果您不知道要预期哪种语言,或者文档使用多种语言怎么办? 4. 测试代码点序列是否看起来像单词、句子等。 但是,如果我们有一些“二进制数据”,其中包括嵌入文本序列怎么办?
总之,您可以确定一个字节序列肯定不是UTF-8,如果解码失败。除此之外,如果您对语言进行假设,您可以说一个字节序列可能是UTF-8编码的文本文档,也可能不是。
我个人认为,最好的方法是避免让您的程序陷入需要做出这种决定的境地。如果无法避免,请认识到您的程序可能会出错。通过思考和艰苦工作,您可以使这种概率变得不太可能,但概率永远不会为零。

1
在原问题“如何在Java中检查字节数组是否包含Unicode字符串?”中,我发现Java Unicode实际上是指Utf16 Code Units。我自己解决了这个问题,并创建了一些代码,可以帮助任何有这种问题的人找到答案。
我创建了两个主要方法,一个将显示Utf-8 Code Units,另一个将创建Utf-16 Code Units。Java和JavaScript中会遇到Utf-16 Code Units……通常以"\ud83d"的形式出现。
如需更多关于Code Units和转换的帮助,请尝试访问该网站;

https://r12a.github.io/apps/conversion/

这里是代码...

    byte[] array_bytes = text.toString().getBytes();
    char[] array_chars = text.toString().toCharArray();
    System.out.println();
    byteArrayToUtf8CodeUnits(array_bytes);
    System.out.println();
    charArrayToUtf16CodeUnits(array_chars);


public static void byteArrayToUtf8CodeUnits(byte[] byte_array)
{
    /*for (int k = 0; k < array.length; k++)
    {
        System.out.println(name + "[" + k + "] = " + "0x" + byteToHex(array[k]));
    }*/
    System.out.println("array.length: = " + byte_array.length);
    //------------------------------------------------------------------------------------------
    for (int k = 0; k < byte_array.length; k++)
    {
        System.out.println("array byte: " + "[" + k + "]" + " converted to hex" + " = " + byteToHex(byte_array[k]));
    }
    //------------------------------------------------------------------------------------------
}
public static void charArrayToUtf16CodeUnits(char[] char_array)
{
    /*Utf16 code units are also known as Java Unicode*/
    System.out.println("array.length: = " + char_array.length);
    //------------------------------------------------------------------------------------------
    for (int i = 0; i < char_array.length; i++)
    {
        System.out.println("array char: " + "[" + i + "]" + " converted to hex" + " = " + charToHex(char_array[i]));
    }
    //------------------------------------------------------------------------------------------
}
static public String byteToHex(byte b)
{
    //Returns hex String representation of byte b
    char hexDigit[] =
            {
                    '0', '1', '2', '3', '4', '5', '6', '7',
                    '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
            };
    char[] array = { hexDigit[(b >> 4) & 0x0f], hexDigit[b & 0x0f] };
    return new String(array);
}
static public String charToHex(char c)
{
    //Returns hex String representation of char c
    byte hi = (byte) (c >>> 8);
    byte lo = (byte) (c & 0xff);

    return byteToHex(hi) + byteToHex(lo);
}

0

如果字节数组以字节顺序标记(BOM)开头,则很容易区分使用了哪种编码。处理文本流的标准Java类可能会自动为您处理此问题。

如果您的字节数据中没有BOM,则这将更加困难-.NET类可以执行统计分析,以尝试确定编码,但我认为这是在假定您知道正在处理文本数据的情况下进行的(只是不知道使用了哪种编码)。

如果您对输入数据的格式有任何控制,则最好选择确保其中包含字节顺序标记。


1
Java不会自动插入BOM,并且在解码时也不会删除它。 - McDowell
1
额,我应该说Java不处理UTF-8的BOM。它是否处理UTF-16/UTF-32取决于所选择的编码机制:http://java.sun.com/javase/6/docs/technotes/guides/intl/encoding.doc.html - McDowell

-1

我认为Michael在他的回答中已经解释得很好(链接),这可能是查找字节数组是否包含所有有效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


"Java 中可以使用哪些方法?" - james.garriss

-1

尝试解码它。如果没有出现任何错误,则它是一个有效的UTF-8字符串。


2
-1:事实错误。非文本二进制流有可能被解码为有效的UTF-8字符串。如果UTF-8解码失败,则意味着您的二进制数据不是UTF-8;但是,如果UTF-8解码没有失败,那并不能保证二进制数据是UTF-8。 - Daniel Fortunov
1
+1 绝对正确。如果解码没有错误,它就是有效的 UTF-8 文本数据。它可能是绝对毫无意义的文本数据,比如拉丁、中文、泰文和希腊字符的混合,但这是一个语义上的区别,而不是技术上的区别。 - Michael Borgwardt
1
迈克尔说得有道理。我想在这种情况下,我应该说:-1 不回答问题。声称它是一个有效的UTF-8字符串并不是回答问题,问题是要找出它是字符串还是二进制数据。仅仅因为它是有效的UTF-8表示,并不能告诉你太多关于原始数据是二进制的(可能只是碰巧是有效的UTF-8)还是原始数据是真正的文本数据。 - Daniel Fortunov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接