场景
工作中需要发送文本消息,这里的文本主要指含有中文的字符串。在很多网络服务接口中,假设发送短信,接口要求单次只接受utf-8
格式的文本,并限制了文本长度,例如1024个byte
大小的数据包。
随即就衍生出两个问题:
- 中文字符串的长度计算。
- 字符串过长需要截断发送。
其他:
本文是做一个思路分析,在任何语言上都是相通的。本文仅使用C#语言做示范。
分析
中文字符串的长度计算
本文所有的长度,均以字节byte
为计算单位。
示例:
string s1 = "123456";
string s2 = "1234你好";
Len(s1);
Len(s2);
void Len(string str)
{
Console.WriteLine($"str: {str}");
Console.WriteLine($"string len: {str.Length}");
Console.WriteLine($"utf8 bytes len: {Encoding.UTF8.GetByteCount(str)}");
Console.WriteLine("\n");
}
output:
str: 123456
string len: 6
utf8 bytes len: 6
str: 1234你好
string len: 6
utf8 bytes len: 10
由于1个字母为1个字节,但一个汉字大于1个字节,所以中文字符串的长度需要转码并计算byte
才能准确。
1个汉字占用的字节长度与其编码格式有关。gb2312
占用2个字节,utf8
是变长编码,占用3-4个字节。
因此并不用去考虑源文本是什么编码,只需要统统转成utf8
并计算字节数即可。
中文字符串的截断
涉及到截断,这里就要增加一个概念,最大长度BufferSize
(数据包大小)。超过此长度则截断。
截断后仍为字符串。
1. 转byte数组,分段取值截断
string text = "hello你好";
int buffersize = 5;
byte[] bytes = Encoding.UTF8.GetBytes(text);
// 按字节操作,步长取数据包最大值。
int stepSize = buffersize;
// 截断次数,字节数除以步长,并向上取整。
int cutTimes = (int)Math.Ceiling(bytes.Length / (double)stepSize);
Len(text);
Console.WriteLine($"buffer_size:{buffersize}\nstep_size:{stepSize}\ncut_times:{cutTimes}\n");
for(int i = 0; i< cutTimes; i++)
{
var from = i * stepSize;
var to = Math.Min((i + 1) * stepSize, bytes.Length);
var tmp = bytes[from..to];
Console.WriteLine(Encoding.UTF8.GetString(tmp));
}
output:
str: hello你好
string len: 7
utf8 bytes len: 11
buffer_size:5
step_size:5
cut_times:3
hello
你�
�
这里出现了乱码。其实很简单,这里的汉字占3个字节。截断步长
是5。取了5个字节,“你”的3个字节与“好”的前2个字节。数据缺失导致了乱码。
2. 中文占比动态截断
中文占比就是字节长度/字符串长度
。可以简单的衡量字符串中汉字的比例。如果全是字母,则为最小的1。如果是全汉字,则可能是2,3,或者4或者其他。
动态截断即截断步长
不固定,每次都根据输入的文本内容自动调整。
string text = "hello你好";
int buffersize = 5;
// 计算中文比率,最小为1,最大为4。数据包最大值除以比率并向下取整得到步长。
int stepSize = (int)(buffersize / ((double)Encoding.UTF8.GetByteCount(text) / text.Length));
// 截断次数,字符数除以步长,并向上取整。
int cutTimes = (int)Math.Ceiling(text.Length / (double)stepSize);
Len(text);
Console.WriteLine($"buffer_size:{buffersize}\nstep_size:{stepSize}\ncut_times:{cutTimes}\n");
for(int i = 0; i< cutTimes;i++)
{
var from = i * stepSize;
var to = Math.Min((i + 1) * stepSize, text.Length);
var tmp = text[from..to];
Console.WriteLine(tmp);
}
output:
str: hello你好
string len: 7
utf8 bytes len: 11
buffer_size:5
step_size:3
cut_times:3
hel
lo你
好
根据输入的文本,7字符,11字节,以及BufferSize=5
,计算得到本次的截断步长
为3。按中文占比比率换算,保证每次截取的字符对应的字节长度都不会超过最大限制,且不会出现截取不完整的情况。缺点就是会增加截断次数。