我正在寻找最近在 Java 中遇到的问题的解决方案:将文件名限制为 UTF-8 中的 255 个字节。
鉴于单个 UTF-8 字符可以由多个字节表示,因此这并不像以下那么简单:
String sampleString = "컴퓨터";
byte[] bytes = sampleString.getBytes("utf8");
String limitedString = new String(bytes, 0, 5, "utf8");
因为我们可以“剪切”字符,以便它最终像上面的情况一样:
컴�
我一直在寻找一个好的解决方案,但找不到。ChatGPT 建议使用StringBuilder
,然后逐个添加字符并检查是否达到限制,如下所示(这不是 ChatGPT 的代码,这是我自己的解释):
String sampleString = "컴퓨터";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < sampleString.length(); i++) {
String temp = sb.toString() + sampleString.codePointAt(i); // build temporary string
if (temp.getBytes("utf8").length > 5) { // convert it back to bytes and check size
break; // if it does not fit, break
}
sb.append(sampleString.codePointAt(i)); // add that tested character otherwise
}
然后结果就和预期的一样了:
컴
但我认为这是一个非常耗费内存的解决方案。也许存在一个性能更好的解决方案?
9
最佳答案
2
利用 UTF-8 中的事实,多字节代码点的每个字节(第一个字节除外)都以二进制数字 10xxxxxx 开头。如果您正在查看这样的字节,则您知道它是多字节代码点的一部分,并且您需要向后扫描,直到找到以 11xxxxxx 开头的字节以找到该代码点的开头。因此,您可以在所需的限制处切断字节数组,然后删除末尾的任何不完整的代码点(如果有):
String sampleString = "컴퓨터";
byte[] bytes = sampleString.getBytes(StandardCharsets.UTF_8);
int limit = 5;
String limitedString;
if (limit >= bytes.length) {
limitedString = sampleString;
} else {
while (limit > 0 && (bytes[limit] & 0xC0) == 0x80)
limit--;
limitedString = new String(bytes, 0, limit, StandardCharsets.UTF_8);
}
3
-
1@RemyLebeau 不,请注意,我从 limit开始,而不是从字符串末尾开始。因此只能有一个不完整的代码点需要删除。我修改了答案中的解释,使其更加明显。
–
-
@RemyLebeau 该
limit
变量不是索引,而是长度。因此,我查看的是字符串最后一个字节后的bytes[limit]
字节。当该字节是 UTF-8 序列 (10xxxxxx) 的一部分时,这意味着前一个字节也属于该字符。如果是 0xxxxxxx 或 11xxxxxx,则意味着字符串后的字节是新代码点的开始,因此字符串是完整的。bytes[limit]
bytes[0 to limit-1]
bytes[0 to limit-1]
– -
@RemyLebeau,证明它按预期工作。(这是一个带有
main
手动断言的单元测试,因为在线编译器不支持 JUnit 或assert
语句。)
–
|
UTF-8 的设计使得编码代码点的第一个字节指定其实际占用的字节数。因此,您可以简单地将代码点字节数相加,直到达到所需的最大字节数,例如:
int getUtf8SeqLen(byte b) {
if ((b & 0x80) == 0x00) return 1; // 0xxxxxxx
if ((b & 0xE0) == 0xC0) return 2; // 110xxxxx
if ((b & 0xF0) == 0xE0) return 3; // 1110xxxx
if ((b & 0xF8) == 0xF0) return 4; // 11110xxx
throw new Exception("Invalid");
}
String sampleString = ...; // "컴퓨터"
byte[] bytes = sampleString.getBytes(StandardCharsets.UTF_8);
int maxBytes = ...; // 255
String limitedString;
if (bytes.length <= maxBytes) {
limitedString = sampleString;
} else {
int numBytes = 0, newLength = 0;
do {
int seqLen = getUtf8SeqLen(bytes[newLength]);
numBytes += seqLen;
if (numBytes > maxBytes) break;
newLength = numBytes;
}
while (newLength < bytes.length);
limitedString = new String(bytes, 0, newLength, StandardCharsets.UTF_8);
}
或者,由于您要将字节转换回String
,因此您可以完全避开该数组并按原样byte[]
迭代原始数组,并总结编码的代码点字节数,例如:String
int getUtf8SeqLen(int cp) {
if (cp <= 0x007F) return 1; // 0xxxxxxx
if (cp <= 0x07FF) return 2; // 110xxxxx 10xxxxxx
if (cp <= 0xFFFF) return 3; // 1110xxxx 10xxxxxx 10xxxxxx
if (cp <= 0x10FFFF) return 4; // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
throw new Exception("Invalid");
}
String sampleString = ...; // "컴퓨터"
int maxBytes = ...; // 255
int numBytes = 0, newLength = 0;
while (newLength < sampleString.length()) {
int cp = sampleString.codePointAt(newLength);
numBytes += getUtf8SeqLen(cp);
if (numBytes > maxBytes) break;
newLength += Character.charCount(cp);
}
String limitedString = sampleString.substring(0, newLength);
1
-
我喜欢你的第二种方法,因为它更直接,不需要创建字节数组。
–
|
charAt
(可能会分割unicode字符),而应该使用codePointAt
(不会)。–
charAt
返回char
的并不适合所有 UTF-8 字符。–
char
不适合(大多数) UNICODE 字符–
–
–
|