我正在使用 Javascriptwindow.atob()
函数来解码 base64 编码的字符串(特别是来自 GitHub API 的 base64 编码内容)。问题是我得到了 ASCII 编码的字符(比如â¢
而不是™
)。如何正确处理传入的 base64 编码流,以便将其解码为 utf-8?
使用 Javascript 的 atob 解码 base64 无法正确解码 utf-8 字符串
Unicode 问题
尽管 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 文章还介绍了window.btoa
and 的.atob
破坏性,此后在现代 ECMAScript 中得到了修复。原始的,现已死亡的 MDN 文章解释说:
“Unicode 问题” 由于
DOMString
s 是 16 位编码的字符串,因此在大多数浏览器中window.btoa
,Character Out Of Range exception
如果字符超出 8 位字节(0x00~0xFF)的范围,则调用Unicode 字符串将导致 a 。
具有二进制互操作性的解决方案
(继续滚动以获取 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) {
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==
不再与先前解决方案的 string 匹配4pyTIMOgIGxhIG1vZGU=
。这是因为它是二进制编码的字符串,而不是 UTF-8 编码的字符串。如果这对您来说无关紧要(即,您没有从另一个系统转换以 UTF-8 表示的字符串),那么您就可以开始了。但是,如果您想保留 UTF-8 功能,最好使用下面描述的解决方案。
具有ASCII base64互操作性的解决方案
这个问题的整个历史显示了多年来我们不得不用多少种不同的方法来解决损坏的编码系统。尽管最初的 MDN 文章不再存在,但这个解决方案仍然可以说是更好的解决方案,并且在解决“Unicode 问题”方面做得很好,同时维护了可以在base64decode.org上解码的纯文本 base64 字符串。
有两种可能的方法可以解决这个问题:
- 第一个是转义整个字符串(使用 UTF-8,请参阅
encodeURIComponent
),然后对其进行编码;- 第二种是将 UTF-16
DOMString
转换为 UTF-8 字符数组,然后对其进行编码。
关于以前解决方案的说明:MDN 文章最初建议使用unescape
和escape
来解决Character Out Of Range
异常问题,但它们已被弃用。这里的一些其他答案建议使用decodeURIComponent
and解决这个问题encodeURIComponent
,这已被证明是不可靠和不可预测的。此答案的最新更新使用现代 JavaScript 函数来提高速度和现代化代码。
如果你想节省一些时间,你也可以考虑使用一个库:
编码 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,例如 when c == \n
,c.charCodeAt(0).toString(16)
return a
, forcea
表示为0a
)。
typescript支持
这是具有一些额外 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 )));
}
事情会改变的。该逃逸/ UNESCAPE方法已被弃用。
您可以在对字符串进行 Base64 编码之前对其进行 URI 编码。请注意,这不会产生 Base64 编码的 UTF8,而是 Base64 编码的 URL 编码数据。双方必须就相同的编码达成一致。
请参阅此处的工作示例:http : //codepen.io/anon/pen/PZgbPW
// encode string
var base64 = window.btoa(encodeURIComponent('€ 你好 æøåÆØÅ'));
// decode string
var str = decodeURIComponent(window.atob(tmp));
// str is now === '€ 你好 æøåÆØÅ'
对于 OP 的问题,第三方库(例如js-base64)应该可以解决问题。
对我有用的完整文章: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"
这是当今最常用的方法之一。
如果将字符串视为字节更适合您,则可以使用以下函数
function u_atob(ascii) {
return Uint8Array.from(atob(ascii), c => c.charCodeAt(0));
}
function u_btoa(buffer) {
var binary = [];
var bytes = new Uint8Array(buffer);
for (var i = 0, il = bytes.byteLength; i < il; i++) {
binary.push(String.fromCharCode(bytes[i]));
}
return btoa(binary.join(''));
}
// example, it works also with astral plane characters such as '𝒞'
var encodedString = new TextEncoder().encode('✓');
var base64String = u_btoa(encodedString);
console.log('✓' === new TextDecoder().decode(u_atob(base64String)))
将 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);
}