我使用Javascript window.atob()函数来解码base64编码的字符串(特别是来自GitHub API的base64编码的内容)。问题是我得到ascii编码的字符返回(如âⅱ而不是™)。我如何正确地处理传入的base64编码流,以便将其解码为utf-8?


当前回答

统一码问题

虽然JavaScript (ECMAScript)已经成熟了,但是Base64、ASCII和Unicode编码的脆弱性已经造成了很多令人头痛的问题(在这个问题的历史中有很多)。

考虑下面的例子:

const ok = "a";
console.log(ok.codePointAt(0).toString(16)); //   61: occupies < 1 byte

const notOK = "✓"
console.log(notOK.codePointAt(0).toString(16)); // 2713: occupies > 1 byte

console.log(btoa(ok));    // YQ==
console.log(btoa(notOK)); // error

为什么我们会遇到这种情况?

根据设计,Base64期望二进制数据作为输入。就JavaScript字符串而言,这意味着每个字符只占用一个字节的字符串。因此,如果您向btoa()传递一个包含占用超过一个字节的字符的字符串,您将得到一个错误,因为这不是二进制数据。

来源:MDN (2021)

最初的MDN文章还介绍了窗口的破碎特性。btoa和.atob,在现代ECMAScript中已被修复。最初的,现已死亡的MDN文章解释道:

“统一码问题” 由于domstring是16位编码的字符串,在大多数浏览器中调用window。如果字符超出8位字节(0x00~0xFF)的范围,Unicode字符串上的btoa将导致字符超出范围异常。


具有二进制互操作性的解决方案

(继续滚动查找ASCII base64解决方案)

来源:MDN (2021)

MDN推荐的解决方案是实际对二进制字符串表示进行编码:

编码UTF8⇢二进制

// convert a Unicode string to a string in which
// each 16-bit unit occupies only one byte
function toBinary(string) {
  const codeUnits = new Uint16Array(string.length);
  for (let i = 0; i < codeUnits.length; i++) {
    codeUnits[i] = string.charCodeAt(i);
  }
  return btoa(String.fromCharCode(...new Uint8Array(codeUnits.buffer)));
}

// a string that contains characters occupying > 1 byte
let encoded = toBinary("✓ à la mode") // "EycgAOAAIABsAGEAIABtAG8AZABlAA=="

解码二进制⇢UTF-8

function fromBinary(encoded) {
  const binary = atob(encoded);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < bytes.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }
  return String.fromCharCode(...new Uint16Array(bytes.buffer));
}

// our previous Base64-encoded string
let decoded = fromBinary(encoded) // "✓ à la mode"

这有点失败,您会注意到编码的字符串EycgAOAAIABsAGEAIABtAG8AZABlAA==不再匹配前一个解决方案的字符串4pyTIMOgIGxhIG1vZGU=。这是因为它是二进制编码的字符串,而不是UTF-8编码的字符串。如果这对您来说无关紧要(即,您没有转换来自另一个系统的以UTF-8表示的字符串),那么您就可以开始了。但是,如果希望保留UTF-8功能,最好使用下面描述的解决方案。


解决方案与ASCII base64互操作

这个问题的整个历史表明,多年来我们有多少种不同的方法来解决破碎的编码系统。虽然最初的MDN文章已经不存在了,但这个解决方案仍然可以说是一个更好的解决方案,并且在解决“Unicode问题”方面做得很好,同时保持了可以在base64decode.org上解码的纯文本base64字符串。

解决这个问题有两种可能的方法:

第一个是转义整个字符串(使用UTF-8,参见encodeURIComponent),然后对它进行编码; 第二步是将UTF-16 DOMString转换为UTF-8字符数组,然后对其进行编码。

关于以前的解决方案的注意事项:MDN文章最初建议使用unescape和转义来解决字符超出范围异常问题,但它们已被弃用。这里的一些其他答案建议使用decodeURIComponent和encodeURIComponent来解决这个问题,这已经被证明是不可靠和不可预测的。这个答案的最新更新使用了现代JavaScript函数来提高速度和现代化代码。

如果你想节省自己的时间,你也可以考虑使用库:

js-base64 (NPM,非常适合Node.js) base64-js

编码UTF8⇢base64

    function b64EncodeUnicode(str) {
        // first we use encodeURIComponent to get percent-encoded UTF-8,
        // then we convert the percent encodings into raw bytes which
        // can be fed into btoa.
        return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
            function toSolidBytes(match, p1) {
                return String.fromCharCode('0x' + p1);
        }));
    }
    
    b64EncodeUnicode('✓ à la mode'); // "4pyTIMOgIGxhIG1vZGU="
    b64EncodeUnicode('\n'); // "Cg=="

解码base64⇢UTF8

    function b64DecodeUnicode(str) {
        // Going backwards: from bytestream, to percent-encoding, to original string.
        return decodeURIComponent(atob(str).split('').map(function(c) {
            return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
        }).join(''));
    }
    
    b64DecodeUnicode('4pyTIMOgIGxhIG1vZGU='); // "✓ à la mode"
    b64DecodeUnicode('Cg=='); // "\n"

(我们为什么要这样做?('00' + c. charcodeat (0). tostring (16)).slice(-2)将0前置到单个字符串,例如当c == \n时,c. charcodeat (0). tostring(16)返回a,迫使a表示为0a)。


打印稿的支持

下面是相同的解决方案,但增加了一些TypeScript兼容性(通过@MA-Maddin):

// Encoding UTF8 ⇢ base64

function b64EncodeUnicode(str) {
    return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) {
        return String.fromCharCode(parseInt(p1, 16))
    }))
}

// Decoding base64 ⇢ UTF8

function b64DecodeUnicode(str) {
    return decodeURIComponent(Array.prototype.map.call(atob(str), function(c) {
        return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
    }).join(''))
}

第一个解决方案(已弃用)

使用escape和unescape(现在已弃用,但在所有现代浏览器中仍然有效):

function utf8_to_b64( str ) {
    return window.btoa(unescape(encodeURIComponent( str )));
}

function b64_to_utf8( str ) {
    return decodeURIComponent(escape(window.atob( str )));
}

// Usage:
utf8_to_b64('✓ à la mode'); // "4pyTIMOgIGxhIG1vZGU="
b64_to_utf8('4pyTIMOgIGxhIG1vZGU='); // "✓ à la mode"

最后一件事:我第一次遇到这个问题是在调用GitHub API时。为了让它在(移动)Safari上正常工作,我实际上不得不在解码源代码之前从base64源代码中剥离所有空白。我不知道这在2021年是否仍有意义:

function b64_to_utf8( str ) {
    str = str.replace(/\s/g, '');    
    return decodeURIComponent(escape(window.atob( str )));
}

其他回答

包括上述解决方案,如果仍然面临问题,尝试如下,考虑转义不支持TS的情况。

blob = new Blob(["\ufeff", csv_content]); // this will make symbols to appears in excel 

对于csv_content,您可以像下面这样尝试。

function b64DecodeUnicode(str: any) {        
        return decodeURIComponent(atob(str).split('').map((c: any) => {
            return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
        }).join(''));
    }

统一码问题

虽然JavaScript (ECMAScript)已经成熟了,但是Base64、ASCII和Unicode编码的脆弱性已经造成了很多令人头痛的问题(在这个问题的历史中有很多)。

考虑下面的例子:

const ok = "a";
console.log(ok.codePointAt(0).toString(16)); //   61: occupies < 1 byte

const notOK = "✓"
console.log(notOK.codePointAt(0).toString(16)); // 2713: occupies > 1 byte

console.log(btoa(ok));    // YQ==
console.log(btoa(notOK)); // error

为什么我们会遇到这种情况?

根据设计,Base64期望二进制数据作为输入。就JavaScript字符串而言,这意味着每个字符只占用一个字节的字符串。因此,如果您向btoa()传递一个包含占用超过一个字节的字符的字符串,您将得到一个错误,因为这不是二进制数据。

来源:MDN (2021)

最初的MDN文章还介绍了窗口的破碎特性。btoa和.atob,在现代ECMAScript中已被修复。最初的,现已死亡的MDN文章解释道:

“统一码问题” 由于domstring是16位编码的字符串,在大多数浏览器中调用window。如果字符超出8位字节(0x00~0xFF)的范围,Unicode字符串上的btoa将导致字符超出范围异常。


具有二进制互操作性的解决方案

(继续滚动查找ASCII base64解决方案)

来源:MDN (2021)

MDN推荐的解决方案是实际对二进制字符串表示进行编码:

编码UTF8⇢二进制

// convert a Unicode string to a string in which
// each 16-bit unit occupies only one byte
function toBinary(string) {
  const codeUnits = new Uint16Array(string.length);
  for (let i = 0; i < codeUnits.length; i++) {
    codeUnits[i] = string.charCodeAt(i);
  }
  return btoa(String.fromCharCode(...new Uint8Array(codeUnits.buffer)));
}

// a string that contains characters occupying > 1 byte
let encoded = toBinary("✓ à la mode") // "EycgAOAAIABsAGEAIABtAG8AZABlAA=="

解码二进制⇢UTF-8

function fromBinary(encoded) {
  const binary = atob(encoded);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < bytes.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }
  return String.fromCharCode(...new Uint16Array(bytes.buffer));
}

// our previous Base64-encoded string
let decoded = fromBinary(encoded) // "✓ à la mode"

这有点失败,您会注意到编码的字符串EycgAOAAIABsAGEAIABtAG8AZABlAA==不再匹配前一个解决方案的字符串4pyTIMOgIGxhIG1vZGU=。这是因为它是二进制编码的字符串,而不是UTF-8编码的字符串。如果这对您来说无关紧要(即,您没有转换来自另一个系统的以UTF-8表示的字符串),那么您就可以开始了。但是,如果希望保留UTF-8功能,最好使用下面描述的解决方案。


解决方案与ASCII base64互操作

这个问题的整个历史表明,多年来我们有多少种不同的方法来解决破碎的编码系统。虽然最初的MDN文章已经不存在了,但这个解决方案仍然可以说是一个更好的解决方案,并且在解决“Unicode问题”方面做得很好,同时保持了可以在base64decode.org上解码的纯文本base64字符串。

解决这个问题有两种可能的方法:

第一个是转义整个字符串(使用UTF-8,参见encodeURIComponent),然后对它进行编码; 第二步是将UTF-16 DOMString转换为UTF-8字符数组,然后对其进行编码。

关于以前的解决方案的注意事项:MDN文章最初建议使用unescape和转义来解决字符超出范围异常问题,但它们已被弃用。这里的一些其他答案建议使用decodeURIComponent和encodeURIComponent来解决这个问题,这已经被证明是不可靠和不可预测的。这个答案的最新更新使用了现代JavaScript函数来提高速度和现代化代码。

如果你想节省自己的时间,你也可以考虑使用库:

js-base64 (NPM,非常适合Node.js) base64-js

编码UTF8⇢base64

    function b64EncodeUnicode(str) {
        // first we use encodeURIComponent to get percent-encoded UTF-8,
        // then we convert the percent encodings into raw bytes which
        // can be fed into btoa.
        return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
            function toSolidBytes(match, p1) {
                return String.fromCharCode('0x' + p1);
        }));
    }
    
    b64EncodeUnicode('✓ à la mode'); // "4pyTIMOgIGxhIG1vZGU="
    b64EncodeUnicode('\n'); // "Cg=="

解码base64⇢UTF8

    function b64DecodeUnicode(str) {
        // Going backwards: from bytestream, to percent-encoding, to original string.
        return decodeURIComponent(atob(str).split('').map(function(c) {
            return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
        }).join(''));
    }
    
    b64DecodeUnicode('4pyTIMOgIGxhIG1vZGU='); // "✓ à la mode"
    b64DecodeUnicode('Cg=='); // "\n"

(我们为什么要这样做?('00' + c. charcodeat (0). tostring (16)).slice(-2)将0前置到单个字符串,例如当c == \n时,c. charcodeat (0). tostring(16)返回a,迫使a表示为0a)。


打印稿的支持

下面是相同的解决方案,但增加了一些TypeScript兼容性(通过@MA-Maddin):

// Encoding UTF8 ⇢ base64

function b64EncodeUnicode(str) {
    return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) {
        return String.fromCharCode(parseInt(p1, 16))
    }))
}

// Decoding base64 ⇢ UTF8

function b64DecodeUnicode(str) {
    return decodeURIComponent(Array.prototype.map.call(atob(str), function(c) {
        return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
    }).join(''))
}

第一个解决方案(已弃用)

使用escape和unescape(现在已弃用,但在所有现代浏览器中仍然有效):

function utf8_to_b64( str ) {
    return window.btoa(unescape(encodeURIComponent( str )));
}

function b64_to_utf8( str ) {
    return decodeURIComponent(escape(window.atob( str )));
}

// Usage:
utf8_to_b64('✓ à la mode'); // "4pyTIMOgIGxhIG1vZGU="
b64_to_utf8('4pyTIMOgIGxhIG1vZGU='); // "✓ à la mode"

最后一件事:我第一次遇到这个问题是在调用GitHub API时。为了让它在(移动)Safari上正常工作,我实际上不得不在解码源代码之前从base64源代码中剥离所有空白。我不知道这在2021年是否仍有意义:

function b64_to_utf8( str ) {
    str = str.replace(/\s/g, '');    
    return decodeURIComponent(escape(window.atob( str )));
}

这是我的一行程序解决方案,结合了Jackie Hans的答案和另一个问题的一些代码:

const utf8_encoded_text = new TextDecoder().decode(Uint8Array.from(window.atob(base_64_decoded_text).split("").map(x => x.charCodeAt(0))));

解码base64到UTF8字符串

以下是@brandonscript目前投票最多的答案

function b64DecodeUnicode(str) {
    // Going backwards: from bytestream, to percent-encoding, to original string.
    return decodeURIComponent(atob(str).split('').map(function(c) {
        return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
    }).join(''));
}

上面的代码可以工作,但是非常慢。如果您的输入是一个非常大的base64字符串,例如,对于一个base64 html文档,30,000个字符。这需要大量的计算。

这是我的答案,使用内置的TextDecoder,比上面的大输入代码快近10倍。

function decodeBase64(base64) {
    const text = atob(base64);
    const length = text.length;
    const bytes = new Uint8Array(length);
    for (let i = 0; i < length; i++) {
        bytes[i] = text.charCodeAt(i);
    }
    const decoder = new TextDecoder(); // default is utf-8
    return decoder.decode(bytes);
}

对我有用的完整文章:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding

我们从Unicode/UTF-8编码的部分是

function utf8_to_b64( str ) {
   return window.btoa(unescape(encodeURIComponent( str )));
}

function b64_to_utf8( str ) {
   return decodeURIComponent(escape(window.atob( str )));
}

// Usage:
utf8_to_b64('✓ à la mode'); // "4pyTIMOgIGxhIG1vZGU="
b64_to_utf8('4pyTIMOgIGxhIG1vZGU='); // "✓ à la mode"

这是当今最常用的方法之一。