如何将UTF8字符串转换为字节数组?

66

.charCodeAt函数返回字符的Unicode编码。但是我想得到字节数组而不是字符编码。我知道,如果字符编码超过127,则该字符存储在两个或多个字节中。

var arr=[];
for(var i=0; i<str.length; i++) {
    arr.push(str.charCodeAt(i))
}
10个回答

81

Unicode在UTF-8中的编码逻辑基本如下:

  • 每个字符最多可以使用4个字节,尽量使用较少的字节数。
  • 字符U+007F及以下可用一个字节编码。
  • 对于多字节序列,第一个字节中前导的1的数量表示该字符需要的字节数。第一个字节的剩余位可用于编码字符的位。
  • 连续字节以10开头,其余6位用于编码字符位。

这里是我之前编写的一个函数,用于将JavaScript UTF-16字符串编码为UTF-8:

function toUTF8Array(str) {
    var utf8 = [];
    for (var i=0; i < str.length; i++) {
        var charcode = str.charCodeAt(i);
        if (charcode < 0x80) utf8.push(charcode);
        else if (charcode < 0x800) {
            utf8.push(0xc0 | (charcode >> 6), 
                      0x80 | (charcode & 0x3f));
        }
        else if (charcode < 0xd800 || charcode >= 0xe000) {
            utf8.push(0xe0 | (charcode >> 12), 
                      0x80 | ((charcode>>6) & 0x3f), 
                      0x80 | (charcode & 0x3f));
        }
        // surrogate pair
        else {
            i++;
            // UTF-16 encodes 0x10000-0x10FFFF by
            // subtracting 0x10000 and splitting the
            // 20 bits of 0x0-0xFFFFF into two halves
            charcode = 0x10000 + (((charcode & 0x3ff)<<10)
                      | (str.charCodeAt(i) & 0x3ff));
            utf8.push(0xf0 | (charcode >>18), 
                      0x80 | ((charcode>>12) & 0x3f), 
                      0x80 | ((charcode>>6) & 0x3f), 
                      0x80 | (charcode & 0x3f));
        }
    }
    return utf8;
}

结果与 unescape(encodeURIComponent()) 不同。https://dev59.com/7GMl5IYBdhLWcg3wDjSg#18729536 - don kaka
2
@donkaka 在 for 循环之后,它应该与 arr 进行比较匹配。http://jsfiddle.net/3Uz8n/ - Jonathan Lonowski
类似于http://www.onicos.com/staff/iz/amuse/javascript/expert/utf.txt,它对我包含CJK统一扩展B中的模糊、4字节字符的字符串有效。 - Ahmed Fasih
3
很棒,这比领先答案快了大约89%。干得好! - Qix - MONICA WAS MISTREATED
4
谷歌闭包库中有一个类似的函数:stringToUtf8ByteArray()。JavaScript中字符串在内存中使用UTF16编码方式,这个事实让我受益匪浅。 - Alexander Kachkaev

46

谢谢,它有效。但我想了解一下,如何编写这个Unicode到UTF8字节码转换的代码。你能否给我提供一篇相关文章的链接?我还没有找到任何相关资料。 - don kaka
@donkaka 我在我的帖子中链接了一个。http://ecmanaut.blogspot.com/2006/07/encoding-decoding-utf8-in-javascript.html。你想要手动逐个代码进行转换吗? - Jonathan Lonowski
encodeURIComponent工作得很好,但我想了解如何生成UTF8字节码。 - don kaka
2
维基百科实际上对UTF-8转换有很好的总结。https://en.wikipedia.org/wiki/UTF-8#Description 这些示例演示了原始代码点的位如何分布以及为后续解码应用的前缀。对于UTF-16 代理对,编码变得复杂,但基于按位移位和使用AND或OR进行掩码 - Jonathan Lonowski
以下是一些示例,如果您想在UTF-8文本和十六进制、二进制或base64之间进行转换,请访问:http://jsfiddle.net/47zwb41o - Beejor

46

使用编码 API,你可以轻松进行UTF-8的编码和解码(使用类型化数组):

var encoded = new TextEncoder().encode("Γεια σου κόσμε");
var decoded = new TextDecoder("utf-8").decode(encoded);
    
console.log(encoded, decoded);

浏览器支持还不错,并且有一个polyfill可以在IE11和旧版本的Edge中使用。

虽然TextEncoder只能编码为UTF-8,但TextDecoder支持其他编码方式。我用它来解码日语文本(Shift-JIS)的方法如下:

// Shift-JIS encoded text; must be a byte array due to values 129 and 130.
var arr = [130, 108, 130, 102, 130, 80, 129,  64, 130, 102, 130,  96, 130, 108, 130, 100,
           129,  64, 130,  99, 130, 96, 130, 115, 130,  96, 129, 124, 130,  79, 130, 80];
// Convert to byte array
var data = new Uint8Array(arr);
// Decode with TextDecoder
var decoded = new TextDecoder("shift-jis").decode(data.buffer);
console.log(decoded);

.decode() 在字符串上不起作用,因此如果您尝试解码一个恰好处于 utf8 格式的字节字符串(在某些环境中可能会发生),则无法使用。 - Dylan Nicholson
如果您有一个十六进制字节字符串,例如“DEADBEEF”,则不能直接使用它。您需要将其转换为TypedArray才能解码。可以用4行代码完成:https://paste2.org/5KHPxdVO - bryc
在我的情况下,我实际上有一个JavaScript(UTF-16)字符串,其中包含UTF-8字符代码。实际上比这更糟糕,因为0x80又被表示为其他东西(欧元符号的Unicode),等等。仍在努力寻找更好的解决方案,我应该能够将数据读入数组中。但不幸的是,TextDecoder在IE / Edge中存在问题。 - Dylan Nicholson
1
@DylanNicholson 2022:IE 是什么? - Kamil Kiełczewski
1
“Internet Explorer?那个评论现在已经快4年了,所以我当时遇到的任何问题很可能已经不再相关了。” - Dylan Nicholson

11

谷歌闭包库有将UTF-8转换为字节数组和反向转换的函数。如果您不想使用整个库,可以从这里复制函数。为了完整起见,将字符串转换为UTF-8字节数组的代码如下:

goog.crypt.stringToUtf8ByteArray = function(str) {
  // TODO(user): Use native implementations if/when available
  var out = [], p = 0;
  for (var i = 0; i < str.length; i++) {
    var c = str.charCodeAt(i);
    if (c < 128) {
      out[p++] = c;
    } else if (c < 2048) {
      out[p++] = (c >> 6) | 192;
      out[p++] = (c & 63) | 128;
    } else if (
        ((c & 0xFC00) == 0xD800) && (i + 1) < str.length &&
        ((str.charCodeAt(i + 1) & 0xFC00) == 0xDC00)) {
      // Surrogate Pair
      c = 0x10000 + ((c & 0x03FF) << 10) + (str.charCodeAt(++i) & 0x03FF);
      out[p++] = (c >> 18) | 240;
      out[p++] = ((c >> 12) & 63) | 128;
      out[p++] = ((c >> 6) & 63) | 128;
      out[p++] = (c & 63) | 128;
    } else {
      out[p++] = (c >> 12) | 224;
      out[p++] = ((c >> 6) & 63) | 128;
      out[p++] = (c & 63) | 128;
    }
  }
  return out;
};

Google将Closure迁移到了GitHub。已更新链接(同时也更新了代码片段,因为函数实现也发生了变化)。 - optevo
这是更新后的链接:stringToUtf8ByteArray() - Alexander Kachkaev

7
假设问题是关于DOMString作为输入并且目标是获取一个数组,当作字符串解释(例如写入磁盘文件时)应该是UTF-8编码的:
现在几乎所有现代浏览器都支持Typed Arrays,如果这种方法没有列出来,那就太可惜了:
  • 根据W3C,支持File API的软件应该在其Blob构造函数中接受DOMString(也请参阅:构造Blob时的字符串编码
  • Blobs可以使用File Reader.readAsArrayBuffer()函数转换为ArrayBuffer
  • 使用DataView或使用由File Reader读取的缓冲区构造Typed Array,可以访问ArrayBuffer的每个单独字节

示例:

// Create a Blob with an Euro-char (U+20AC)
var b = new Blob(['€']);
var fr = new FileReader();

fr.onload = function() {
    ua = new Uint8Array(fr.result);
    // This will log "3|226|130|172"
    //                  E2  82  AC
    // In UTF-16, it would be only 2 bytes long
    console.log(
        fr.result.byteLength + '|' + 
        ua[0]  + '|' + 
        ua[1] + '|' + 
        ua[2] + ''
    );
};
fr.readAsArrayBuffer(b);

JSFiddle上尝试一下。我还没有对此进行基准测试,但我可以想象这对于大型DOM字符串作为输入是很有效的。


太好了。在JS中不需要疯狂的位操作,只需直接将其传递给Blob构造函数。谢谢! - Roy Tinker

2
您可以使用 FileReader 将字符串原样保存。

将字符串保存到 blob 中,然后调用 readAsArrayBuffer()。然后 onload 事件会返回一个数组缓冲区,该缓冲区可以转换为 Uint8Array。不幸的是,此调用是异步的。

以下这个小函数将会帮助您:

function stringToBytes(str)
{
    let reader = new FileReader();
    let done = () => {};

    reader.onload = event =>
    {
        done(new Uint8Array(event.target.result), str);
    };
    reader.readAsArrayBuffer(new Blob([str], { type: "application/octet-stream" }));

    return { done: callback => { done = callback; } };
}

这样调用:

stringToBytes("\u{1f4a9}").done(bytes =>
{
    console.log(bytes);
});

输出: [240, 159, 146, 169]

解释:

JavaScript使用UTF-16和代理对来存储内存中的Unicode字符。为了将Unicode字符保存在原始二进制字节流中,需要进行编码。

通常情况下,大多数情况下使用UTF-8进行编码。如果不使用编码,则只能保存ASCII字符,最高到0x7f。

FileReader.readAsArrayBuffer()使用UTF-8编码。


1
由于JavaScript中没有纯的byte类型,因此我们可以将字节数组表示为数字数组,其中每个数字表示一个字节,因此其整数值介于0和255之间(包括0和255)。
以下是一个简单的函数,将JavaScript字符串转换为包含该字符串UTF-8编码的数字数组:
function toUtf8(str) {
    var value = [];
    var destIndex = 0;
    for (var index = 0; index < str.length; index++) {
        var code = str.charCodeAt(index);
        if (code <= 0x7F) {
            value[destIndex++] = code;
        } else if (code <= 0x7FF) {
            value[destIndex++] = ((code >> 6 ) & 0x1F) | 0xC0;
            value[destIndex++] = ((code >> 0 ) & 0x3F) | 0x80;
        } else if (code <= 0xFFFF) {
            value[destIndex++] = ((code >> 12) & 0x0F) | 0xE0;
            value[destIndex++] = ((code >> 6 ) & 0x3F) | 0x80;
            value[destIndex++] = ((code >> 0 ) & 0x3F) | 0x80;
        } else if (code <= 0x1FFFFF) {
            value[destIndex++] = ((code >> 18) & 0x07) | 0xF0;
            value[destIndex++] = ((code >> 12) & 0x3F) | 0x80;
            value[destIndex++] = ((code >> 6 ) & 0x3F) | 0x80;
            value[destIndex++] = ((code >> 0 ) & 0x3F) | 0x80;
        } else if (code <= 0x03FFFFFF) {
            value[destIndex++] = ((code >> 24) & 0x03) | 0xF0;
            value[destIndex++] = ((code >> 18) & 0x3F) | 0x80;
            value[destIndex++] = ((code >> 12) & 0x3F) | 0x80;
            value[destIndex++] = ((code >> 6 ) & 0x3F) | 0x80;
            value[destIndex++] = ((code >> 0 ) & 0x3F) | 0x80;
        } else if (code <= 0x7FFFFFFF) {
            value[destIndex++] = ((code >> 30) & 0x01) | 0xFC;
            value[destIndex++] = ((code >> 24) & 0x3F) | 0x80;
            value[destIndex++] = ((code >> 18) & 0x3F) | 0x80;
            value[destIndex++] = ((code >> 12) & 0x3F) | 0x80;
            value[destIndex++] = ((code >> 6 ) & 0x3F) | 0x80;
            value[destIndex++] = ((code >> 0 ) & 0x3F) | 0x80;
        } else {
            throw new Error("Unsupported Unicode character \"" 
                + str.charAt(index) + "\" with code " + code + " (binary: " 
                + toBinary(code) + ") at index " + index
                + ". Cannot represent it as UTF-8 byte sequence.");
        }
    }
    return value;
}

function toBinary(byteValue) {
    if (byteValue < 0) {
        byteValue = byteValue & 0x00FF;
    }
    var str = byteValue.toString(2);
    var len = str.length;
    var prefix = "";
    for (var i = len; i < 8; i++) {
        prefix += "0";
    }
    return prefix + str;
}

0

我之前使用了 Joni的解决方案,它运行良好,但这个更短。

这个灵感来自于 Mozilla的Base64 Unicode讨论中第三个解决方案的atobUTF16()函数。

function convertStringToUTF8ByteArray(str) {
    let binaryArray = new Uint8Array(str.length)
    Array.prototype.forEach.call(binaryArray, function (el, idx, arr) { arr[idx] = str.charCodeAt(idx) })
    return binaryArray
}

这对于非ASCII字符无效。这是因为JavaScript字符串是UTF-16编码的。charCodeAt将返回0到65535之间的数字,而给定的Uint8Array索引只能存储0到255。 - Tate Thurston

0
在我的测试中(并且据我所知),这与使用escape / unescape方法的unescape(encodeURIComponent(instr))方法给出相同的结果,但不使用escape / unescape。
    function utf8_toBinary(instr) {
        //this is the same as unescape(encodeURIComponent(instr))
        const binAry = (new TextEncoder().encode(instr));
        let safeStr = String.fromCharCode(...binAry);
        return btoa(safeStr);
    }

    function binary_toUtf8(binstr) {
        let safeStr = atob(binstr);
        let arr = new Uint8Array(safeStr.length);
        for (let i = 0; i < safeStr.length; i++) {
            arr[i] = safeStr.charCodeAt(i);
        }
        return new TextDecoder().decode(arr);
    }

-1
function convertByte()
{
    var c=document.getElementById("str").value;
    var arr = [];
    var i=0;
    for(var ind=0;ind<c.length;ind++)
    {
        arr[ind]=c.charCodeAt(i);
        i++;
    }    
    document.getElementById("result").innerHTML="The converted value is "+arr.join("");    
}

2
欢迎来到 Stack Overflow。仅有代码的答案通常可以通过解释它们如何以及为什么工作来改进,如果在对已有答案和被接受的答案的老问题添加答案时,指出这个答案解决了问题的哪个新方面也是很重要的。 - Jason Aller

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