谈谈前端二进制数据和字符串的相互转换
前言
笔者在最近的AI开发工作当中,经常遇到前端处理流式输出的情况。众所周知,在前端中,处理流式接口的常用方式是通过自带fetch接口获取响应流,并通过TextDecoder将流数据转换为字符串。
与TextDecoder对应的是TextEncoder,它将字符串转换为二进制数据。这也是在JavaScript中处理二进制数据和字符串相互转换的主要方式。其本身的使用方式也非常简单:
let s = '你好,世界。';
let b = new TextEncoder().encode(s);
console.log(b); // 返回Uinit8Array [228, 189, 160, 229, 165, 189, 239, 188, 140, 228, 184, 150, 231, 149, 140, 227, 128, 130]
let a = new TextDecoder().decode(b);
console.log(a);// 返回字符串 "你好,世界。"
TextEncoder().encode(s) 返回的是Uint8Array类型数组,是浏览器端处理ArrayBuffer的常用方式。
正文
作为一个合格的前端老司机,要做的就是由点到面的发散思考,从基础开始,逐层深入,直到最深。
TextEncoder
TextEncoder().encode 将字符串按照UTF-8编码转换为二进制数据。仅支持UTF-8编码,不支持其他编码。
TextEncoder在上古时期的浏览器(IE)中,是不被支持的(很久很久以前,曾经的前端程序员都活在被“浏览器兼容性开发”的恐惧支配中)。所以,我们可以看下,在JavaScript中,怎么把字符串转换为UTF-8编码的二进制数据。
通常情况下,在utf-8编码下,一个中文字符占三个字节,一个英文字符占一个字节。
let s = '我';
let b = new TextEncoder().encode(s);
console.log(b); // b.length = 3
1、利用encodeURIComponent
encodeURIComponent是将不被URL支持的字符进行utf-8编码,变成%+两个16进制字符形式;比如空格会被编码为%20。
let s = '我i你'
let b = new TextEncoder().encode(s);
console.log(b);//[230, 136, 145, 105, 228, 189, 160]
let c = encodeURIComponent(s);
let temp = []
for (let i = 0; i < c.length; i++) {
if (c[i] === '%') {
temp.push(parseInt(c.slice(i + 1, i + 3), 16));
i += 2;
} else
temp.push(c.charCodeAt(i))
}
let d = Uint8Array.from(temp);
console.log(d);//[230, 136, 145, 105, 228, 189, 160]
2、纯手撸
talk is cheap, show me the code
function utf8Encode(str) {
let temp = []
for (let n = 0; n < str.length; n++) {
let c = str.charCodeAt(n);
//console.log(c);
if (c < 128) {
temp.push(c);
}
else if (c > 127 && c < 2048){
temp.push((c >> 6) | 192)
temp.push((c & 63) | 128)
}else{
temp.push((c >> 12) | 224)
temp.push(((c >> 6) & 63) | 128)
temp.push((c & 63) | 128)
}
}
return Uint8Array.from(temp);
}
console.log(utf8Encode('我i你')) // [230, 136, 145, 105, 228, 189, 160]
不用记住,找AI,找度娘都行!!!
TextDecoder
TextDecoder默认采用UTF-8编码,但同时也支持其他编码格式。与上面两种方式对应,我们也可以自定义实现二进制数据转换为字符串。
1、利用decodeURIComponent
利用decodeURIComponent时,前面的编码阶段要把%也要加入进去,不然会报错。
let str = "我i你";
let b = s2u(str);
console.log(b);
let str2 = u2s(b);
console.log(str2);// 我i你
function s2u(s){
let temp = [];
s = encodeURIComponent(s);
for(let i =0;i<s.length;i++){
temp.push(s.charCodeAt(i));
}
return Uint8Array.from(temp);
}
function u2s(u) {
let s = '';
for (let i = 0; i < u.length; i++) {
s += String.fromCharCode(u[i]);
}
return decodeURIComponent(s);
}
2、手撸
function decodeUTF8(bytes) {
let str = '', i = 0;
const len = bytes.length;
while (i < len) {
const byte1 = bytes[i++];
if (byte1 < 0x80) { // 0xxxxxxx
str += String.fromCharCode(byte1);
} else if (byte1 > 0xBF && byte1 < 0xE0) { // 110xxxxx 10xxxxxx
const byte2 = bytes[i++];
str += String.fromCharCode(((byte1 & 0x1F) << 6) | (byte2 & 0x3F));
} else if (byte1 > 0xDF && byte1 < 0xF0) { // 1110xxxx 10xxxxxx 10xxxxxx
const byte2 = bytes[i++];
const byte3 = bytes[i++];
str += String.fromCharCode(((byte1 & 0x0F) << 12) | ((byte2 & 0x3F) << 6) | (byte3 & 0x3F));
} else if (byte1 > 0xEF && byte1 < 0xF8) { // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
const byte2 = bytes[i++];
const byte3 = bytes[i++];
const byte4 = bytes[i++];
str += String.fromCharCode(((byte1 & 0x07) << 18) | ((byte2 & 0x3F) << 12) | ((byte3 & 0x3F) << 6) | (byte4 & 0x3F));
} else {
throw new Error('Invalid UTF-8 detected');
}
}
return str;
}
// 示例使用
const utf8Bytes = new TextEncoder().encode('我i你'); // "我i你" 的UTF-8编码
console.log(decodeUTF8(utf8Bytes)); // 输出: 我i你
3、利用Blob和FileReader
如果你在浏览器环境中工作,可以使用Blob和FileReader来解码数据,这种方法更适合处理文件等大型二进制数据:
function decodeArrayBuffer(arrayBuffer) {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onloadend = () => resolve(fileReader.result);
fileReader.onerror = reject;
fileReader.readAsText(new Blob([arrayBuffer])); // 将ArrayBuffer转换为Blob并读取为文本
});
}
// 使用示例:假设你有一个ArrayBuffer变量arrayBuffer
decodeArrayBuffer(new TextEncoder().encode('我i你')).then(text => {
console.log(text); // 将输出解码后的文本字符串:我i你
}).catch(error => {
console.error(error); // 处理错误情况
});
不考虑必须采用UTF-8编码
如果不是必须采用UTF-8编码,我们也可以把包含中文字符的字符串处理成unicode转义序列,进行传输。
let s = '我i你';
let u = s2u(s);
console.log(u);// \u6211\u0069\u4f60
// TODO 将unicode转换成二进制
let s1 = u2s(u);
console.log(s1);// 我i你
function s2u(s){
let u = '';
for (let i = 0; i < s.length; i++){
let n = s.charCodeAt(i).toString(16);
// 处理成标准的\uXXXX格式
while (n.length < 4){
n = '0' + n;
}
u += '\\u' + n;
}
return u;
}
// 简单的按照\uXXXX格式解析,非标准格式大家可以考虑下怎么处理
function u2s(u){
let s = '';
for(let i = 0; i < u.length; i += 6){
let n = parseInt(u.substring(i + 2, i + 6), 16);
s += String.fromCharCode(n);
}
return s;
}