使用方法
- 把十六进制字节串粘进输入框。页面打开时已预填 E4B8ADE69687616263313334(对应文本「中文abc134」)。可以带空格、逗号、冒号、连字符和换行分隔,页面会先清洗再解码——从
xxd或 Wireshark 抄出来的 hex 通常都是带空格或换行的。 - 按 Ctrl/Cmd + Enter,或点「按 UTF-8 解码」。这里用组合键而不是裸 Enter,是因为输入框是多行文本,Enter 必须留给换行。
- 看结果区的四行:解码文本、字节数、字符数、码点列表。默认示例是 12 字节 → 8 字符,因为「中文」两个汉字各占 3 字节而
abc134六个字符各占 1 字节。 - 粘一串带 BOM 的试试:EFBBBF E4B8AD。你会看到输出只有一个「中」字——开头那 3 个字节被
TextDecoder默认配置吃掉了,本页会明确告诉你「已跳过 BOM 3 字节」,而不是让你以为 hex 写错了。 - 粘一个非法序列试试:41FF42。输出是
A�B,中间那个替换字符 U+FFFD 就是「FF 不是合法 UTF-8 字节」的证据。点「导出 CSV」可把 12 行字节对照表带走(带 BOM,Excel 打开中文不乱码)。
十六进制转文本转换器能解决什么问题
十六进制是「字节的十六进制写法」,不是「字符的十六进制写法」——这是本页存在的前提。一个 UTF-8 汉字占 3 字节,写成 hex 是 6 个十六进制字符;一个 emoji 占 4 字节,写成 hex 是 8 个字符。所以它解决的第一类问题是把「看不懂的字节串」还原成人能读的内容。
一是调试编码问题。接口返回乱码、文件打开是乱码、数据库里存了一串看不懂的字符——绝大多数情况下的第一动作就是把乱码转成 hex 看看字节层面到底是什么。中文 的 UTF-8 字节是 E4B8AD E69687,如果你看到的字节是 D6D0 CEC4,那是 GBK 编码,编码口径从一开始就错了,后面怎么转都是错的。
二是核对 hash / 签名 / 校验和。下载校验、文件指纹、API 签名比对,给的都是一串 hex。你需要确认「这个 hash 对应的原文是不是我要的那段」——办法就是把候选原文按 UTF-8 编成字节,看 hex 是否一致。下面对照表里每一行都给出双向可验证的字节与码点。
三是从源码或日志里抠出原始字符串。JS 源码里的 \u4e2d、Java 的 \u4e2d、URL 里的百分号编码、日志里的 hex dump,都是同一件事的不同外衣。十六进制转文本转换器处理的是其中最直接的一种:直接给你一串 hex。
四是判断一个字节序列是不是合法 UTF-8。这一步很多人跳过,结果是把乱码直接写进数据库。默认 TextDecoder 不报错,只把非法字节替换成 U+FFFD——这意味着「解码成功」不等于「输入合法」。本页把 U+FFFD 的个数单独列成一个指标,出现就说明有字节被丢弃了。
本页不做这些事:不做十六进制 → 十进制或二进制(那是数值进制转换,不涉及字符编码)、不做 ASCII 口径的解码(ASCII 只有 7 位,规则完全不同)、也不做 GBK/Big5 这类浏览器原生 TextDecoder 不支持的编码。数值口径的转换请用「十六进制转十进制转换器」和「十六进制转二进制转换器」;反向的「文本 → hex」请用「文本转十六进制转换器」,两页是同一链路的两个方向。
原理和变量:hex → 字节 → 字符的三段转换
第一段,hex 字符串 → 字节数组。规则极简:每 2 个十六进制字符 = 1 个字节,因为一个字节是 8 位,十六进制一位正好表示 4 位。页面先做清洗——去掉 0x 前缀、去掉所有空白与 , ; : _ - 分隔符——然后按 parseInt(hex.substr(i, 2), 16) 逐对转换。这一步必须自己判奇偶:Node 的 Buffer.from('4E2D658', 'hex') 会静默截断成 4E 2D 65 三个字节,不报错。浏览器没有 Buffer,但同样不能假设库会替你发现奇数长度。
第二段,字节数组 → 码点(code point)。这是 UTF-8 解码的核心规则,本质是按首字节判断后续有几个字节:
- 首字节
00~7F:1 字节,码点 U+0000~U+007F(ASCII 区); - 首字节
C2~DF:2 字节,码点 U+0080~U+07FF; - 首字节
E0~EF:3 字节,码点 U+0800~U+FFFF; - 首字节
F0~F4:4 字节,码点 U+10000~U+10FFFF。
关键阈值实测:U+007F 编成 7F(1 字节),U+0080 编成 C2 80(2 字节)——128 这个数字就是 ASCII 与 UTF-8 的分界。往上,U+07FF 是 DF BF(2 字节边界),U+0800 是 E0 A0 80(3 字节起点);U+FFFF 是 EF BF BF,U+10000 是 F0 90 80 80(4 字节起点)。U+10FFFF 是 Unicode 的硬上限,F4 8F BF BF 之后再多的位在规范里不存在。
第三段,码点序列 → JavaScript 字符串。这一步有个容易踩的坑:JS 字符串是 UTF-16 码元序列,不是码点序列。一个 emoji 的码点是 U+1F600(属于 U+10000 以上),在 UTF-16 里必须用代理对表示成 0xD83D 0xDE00 两个码元。所以 '😀'.length 等于 2,但 [...'😀'].length 等于 1。实测 emoji 的 hex 是 F09F9880(4 字节),charCodeAt(0) = 0xD83D(高代理)、charCodeAt(1) = 0xDE00(低代理),反算公式 (0xD83D−0xD800)×0x400 + (0xDE00−0xDC00) + 0x10000 = 0x1F600 验证通过。所以本页的「字符数」一律按码点算([...s].length),不用 s.length——否则一个 emoji 会被算成 2 个字符,字节数与字符数的比值就永远解释不清。
第四步,也是本页最反直觉的一条:TextDecoder 默认会吃掉 BOM。实测 new TextDecoder('utf-8') 的 ignoreBOM 默认值是 false,含义是「忽略 BOM」=「识别到 BOM 就删掉它」。喂给它 EF BB BF 三个字节,输出是空字符串(长度 0);喂 EF BB BF E4 B8 AD(BOM + 「中」),输出是 中文 里的「中」,长度 1,码点只有 U+4E2D——BOM 消失得干干净净。
这不是 bug,是规范行为:U+FEFF 在 Unicode 里的正式名称是 ZERO WIDTH NO-BREAK SPACE,它的首要语义就是「ZWNBSP」而不是「BOM」,只有在文件起始位置才被额外解释为字节序标记。如果传 ignoreBOM: true,BOM 会被保留成 U+FEFF 字符(实测码点序列变成 U+FEFF U+4E2D,长度 2)。所以「解码后开头多了一个看不见的字符」和「解码后开头少了一个字符」是两个不同的问题,取决于你要不要它——本页的选择是:显式检测 EFBBBF 前缀,在结果区告知「已跳过 BOM 3 字节」,让这个隐式行为变成可见的。
第五步,非法序列的替换行为。默认 fatal: false 时,TextDecoder 不抛异常,而是把非法字节替换成 U+FFFD(REPLACEMENT CHARACTER),U+FFFD 自己的 UTF-8 编码是 EF BF BD(3 字节)。实测规则是「一个非法字节换成一个 U+FFFD」:
FF(孤立的后续字节)→ 1 个 U+FFFD;C3(2 字节序列被截断)→ 1 个 U+FFFD;EDA080(CESU-8 编码的代理项 U+D800)→ 3 个 U+FFFD,因为 3 个字节都不合法;F4908080(码位 U+110000 超出 Unicode 上限)→ 4 个 U+FFFD;41FF42→A�B,长度 3——中间的 FF 只换 1 个 U+FFFD,不会「吞掉后续所有字节」。
如果传 fatal: true,同样的输入会抛 TypeError: The encoded data was not valid for encoding utf-8。「默认不报错」是好是坏取决于用途:做数据清洗时你希望它不抛(能看到尽可能多的内容),做校验时你希望它抛(U+FFFD 本身就是不可靠信号)。本页两种都做了对照,fatal: true 的结果放在对照表里。
第六步,字节数 ≠ 字符数,这是本工具最该记住的一条。同一个 4E2D6587(4 字节)在 UTF-8 下解出 2 个汉字,在 ISO-8859-1(Latin-1)下解出 4 个字符(每个字节各成一个码位,N-e‡)。所以「hex 转文本」这个说法必须补上编码口径,否则「字符数」根本无从谈起。本页固定用 UTF-8,并在结果区同时给出字节数与码点数,让这个 2:1 的关系可核对。
完整示例(数值仅作演示)
示例条件与页面打开时的默认值完全一致:hex = E4B8ADE69687616263313334,解码结果是 中文abc134。结果区在打开时就显示着这行文本,点一次「按 UTF-8 解码」得到完全一样的结果。
- hex 字符串长度 24 个字符,字节数 12(24 ÷ 2,每 2 个 hex 字符 1 字节);
- 字节逐个拆开:
0xE4 0xB8 0xAD(中)0xE6 0x96 0x87(文)0x61(a)0x62(b)0x63(c)0x31 0x33 0x34(134); - 码点数 8,码点序列
U+4E2D U+6587 U+0061 U+0062 U+0063 U+0031 U+0033 U+0034; - 字节数 12 与字符数 8 不相等,差额来自两个 3 字节汉字:
2×(3−1) = 4; - 反向验证:把「中文abc134」按 UTF-8 编码回 hex,得
E4B8ADE69687616263313334,与输入逐位一致; - 换一组:
F09F9880→ 4 字节 → 1 个码点(😀,代理对),字节与字符比是 4:1,是 3 字节汉字的 1.33 倍。
输入范围与对照表
输入侧的清洗与边界规则,每一条都对应一种真实的抄写错误:
| 输入形态 | 示例 | 清洗后 | 页面行为 | 为什么要这么定 |
|---|---|---|---|---|
| 连续大写 hex | E4B8AD | E4B8AD | 正常解码 | 最常见形态,xxd 与 Wireshark 输出的就是这种 |
| 全小写 hex | e4b8ad | E4B8AD | 正常解码 | 大小写不敏感,页面统一转大写后解析 |
| 半角空格分隔 | E4 B8 AD | E4B8AD | 正常解码 | 抄 hexdump 最常见的形态 |
| 换行 / 逗号 / 冒号分隔 | E4B8 AD, AD:AD | E4B8ADAD | 正常解码 | 日志与抓包工具的常见分隔符 |
0x 前缀 | 0xE4B8AD | E4B8AD | 正常解码 | 从 C 代码里复制的常量带前缀 |
| 奇数长度 | 4E2D658 | 4E2D658 | 拒绝 | 7 个 hex 字符无法配对成完整字节,说明复制时少了一位 |
| 含非 hex 字符 | 4E2D65ZZ | 4E2D65ZZ | 拒绝 | Z 不是十六进制数字,parseInt('ZZ',16) 返回 NaN 会静默变 0 |
| 空输入 | (留空) | — | 拒绝 | 空串不是合法的空文本,decode(new Uint8Array(0)) 返回空串会让「没填」和「填了空」无法区分 |
下表是 12 组字节串的逐项实测,「含 U+FFFD」列是判断字节是否合法的唯一可靠指标——没有替换字符,说明这串字节是合法 UTF-8:
| 行 | hex | 字节 | 字符 | 码点 | 解码结果 | 含 U+FFFD | fatal:true |
|---|---|---|---|---|---|---|---|
| 1(默认) | E4B8ADE69687616263313334 | 12 | 8 | 8 | 中文abc134 | 无 | 成功 |
| 2 | 48656C6C6F | 5 | 5 | 5 | Hello | 无 | 成功 |
| 3 | 4E2D | 2 | 2 | 2 | N- | 无 | 成功 |
| 4 | E4BDA0E69687 | 6 | 2 | 2 | 你文 | 无 | 成功 |
| 5 | F09F9880 | 4 | 1 | 1 | 😀 | 无 | 成功 |
| 6 | E282AC | 3 | 1 | 1 | € | 无 | 成功 |
| 7 | C3A9 | 2 | 1 | 1 | é | 无 | 成功 |
| 8 | FF | 1 | 1 | 1 | � | 1 个 | 抛 TypeError |
| 9 | 41FF42 | 3 | 3 | 3 | A�B | 1 个 | 抛 TypeError |
| 10 | C3 | 1 | 1 | 1 | � | 1 个 | 抛 TypeError |
| 11 | 0A | 1 | 1 | 1 | 换行 LF | 无 | 成功 |
| 12 | EFBBBF | 3 | 0 | 0 | (空字符串) | 无 | 成功 |
第 4 行值得看一眼:E4BDA0E69687 是 6 字节却只解出 2 个字符,因为 E4BDA0 正好是「你」(U+4F60),E69687 正好是「文」(U+6587)——看起来像乱码的两个字节段其实各自构成了一个合法汉字。这是「乱码」最常见的真实成因:编码猜错了,但字节恰好合法。此时页面不会报任何错,因为字节层面确实没有非法序列。
第 12 行是 BOM 的极端形态:只有 EFBBBF 三个字节,TextDecoder 吃掉了 BOM 之后输出空字符串(字符数 0,码点数 0),但 fatal: true 却「成功」——这说明「解码成功 + 有输出」不是同一件事,页面把字符数 0 单独标出来就是为了让你看到这一点。
边界与精度
- BOM 被吃掉时不会报错,这是本页最隐蔽的行为。
EFBBBF单独解码得到空字符串,EFBBBFE4B8AD得到「中」(不是「中」)。如果你正在做字节级比对,输入多粘了 BOM 会让首字节比对失败但两边都「看起来正常」——因为对方工具的ignoreBOM配置可能和你不同。页面因此显式检测前缀并提示「已跳过 BOM 3 字节」。 - 奇数长度必须自己拒。Node 的
Buffer.from('4E2D658', 'hex')实测会静默截断成4e2d65(只吃 3 字节),不抛异常。如果你的代码直接Buffer.from(input, 'hex'),一个少抄一位的输入会变成一条被截断的「合法」数据。页面的做法是先判cleaned.length % 2 === 0,不满足就报错并告诉你清洗后的实际长度。 - 非 hex 字符会静默变 0,不是变 NaN。
parseInt('ZZ', 16)返回NaN,但写进Uint8Array时 NaN 会被截成 0——于是4E2D65ZZ里的 ZZ 变成了两个\0字节,页面上看起来像解出了控制字符。这比报错更危险,因为它不报错。页面用/^[0-9a-fA-F]*$/先做字符集校验。 - U+FFFD 的个数能告诉你丢了几个字节,但丢了哪些信息就没了。规则是「1 个非法字节 → 1 个 U+FFFD」,所以
EDA080(3 字节)产出 3 个 U+FFFD,F4908080(4 字节)产出 4 个。但替换是不可逆的:A�B里那个�可能是 FF、也可能是 FE、也可能是 80——想知道原字节是什么,只能回看输入。 - 代理对:字节数与字符数的比值最大是 4:1。U+10000 以上的码点用 4 字节编码(
F0~F4开头的序列),而码点数算 1。所以同一串字节里 emoji 越多,字节与字符的差距越大:全 ASCII 是 1:1,混排中文约 3:1,emoji 是 4:1。极端例子👨👩👧是 18 字节但只有 5 个码点(3 个人 + 2 个 U+200D 零宽连接符),比值 3.6:1——比单个 emoji 还高,因为 ZWJ 也要占 3 字节。 - 容量阈值:超过 20 万 hex 字符先提示,超过 200 万直接拒绝。20 万 hex 字符 = 10 万字节,实测
TextDecoder解码耗时约 0.6 毫秒;200 万 hex 字符 = 100 万字节,实测约 5.2 毫秒。真正的问题不是耗时而是逐字节parseInt的 100 万次调用开销,以及超长文本塞进 DOM 后的渲染卡顿。页面在这两个阈值上分别给出提示与拒绝,文案里写明对应的字节数。 - 输入长度本身没有 16 位或 32 位的概念。很多教程会说「32 位 hex 就是一个 16 字节的 UUID」——那只是 UUID 的约定,不是 hex 字符串的规则。本页对长度不设上限,只在超过 200 万字符时拒绝。UUID 去掉连字符后是 32 个 hex 字符 = 16 字节,这个换算在对照表第 2、3 行就能验证。
常见问题
解码结果里出现「�」,是我的 hex 抄错了吗?
有可能,但更可能是你抄的这段字节本身不是合法 UTF-8——这是两件不同的事。十六进制转文本转换器会把「非法字节」替换成 U+FFFD(显示为 �)并照常输出,不报错,所以你必须主动看「含 U+FFFD」这一列。判断顺序是这样:先看 U+FFFD 的个数——1 个通常对应 1 个坏字节(对照表第 8、9、10 行),2~4 个往往是一整段都坏了(比如第 8 行的 EDA080、以及超出 Unicode 上限的 4 字节序列)。再看位置:U+FFFD 出现在开头,多半是多粘了 EFBBBF 或截断的 BOM;出现在中间,多半是某段编码口径不同(比如混入 GBK 字节)。最后回到源文件核对:U+FFFD 里的原始字节信息已经丢失,页面上任何方法都无法还原「原来是 FF 还是 80」。唯一可靠的做法是用 fatal: true 跑一遍——它会直接抛 TypeError: The encoded data was not valid for encoding utf-8,至少告诉你「这段字节不合法」而不是让你对着 � 猜。
我贴进去明明是 6 个汉字,为什么只解出 3 个字符?
因为 hex 是字节的十六进制写法,不是字符的写法。一个 UTF-8 汉字占 3 字节,写成 hex 是 6 个十六进制字符。所以 6 个汉字 = 18 字节 = 36 个 hex 字符,不是 12 个。看到「36 个 hex 字符只解出 6 个字」时,字节与字符是 3:1 的正常关系,不是少解了一半。页面在结果区把字节数和字符数(按码点算)分开列,就是为了让你一眼看出这个比例。唯一会出现「字符数远少于预期」的情况是代理对:emoji 占 4 字节但只算 1 个字符,所以 emoji 混排时字节数会比字符数大得多(👨👩👧 是 18 字节 5 码点)。如果你需要的是「字符」而不是「字节」,唯一正确的做法是先把 hex 解成文本再数,而不是在 hex 层面数。
开头少了 3 个字节,或者开头多了个看不见的字符,是 Bug 吗?
都不是,是 BOM 的两种处理方式,取决于 TextDecoder 的 ignoreBOM 参数。默认 false,含义是「识别到 BOM 就删掉」——所以贴 EFBBBFE4B8AD 只得到「中」,BOM 那 3 字节被静默吃掉,字符数是 1 不是 2。传 ignoreBOM: true 则 BOM 被保留成 U+FEFF 这个不可见字符,字符数变成 2,码点序列是 U+FEFF U+4E2D。
为什么会这样?因为 U+FEFF 在 Unicode 里的正式名称是 ZERO WIDTH NO-BREAK SPACE(零宽不换行空格),它的首要语义是 ZWNBSP,只有出现在文件起始位置时才被额外解释为字节序标记。TextDecoder 默认选择「识别并移除」这个更符合直觉的行为。实践建议:做字节级比对时,把两边的 ignoreBOM 配置统一,否则会出现「一边多 3 字节一边不多」的诡异不一致。本页固定用默认配置,并显式检测 EFBBBF 前缀、在结果区提示「已跳过 BOM 3 字节」,把这个隐式行为变成可见的。
同一串 hex 换 Latin-1 解出来的字符数完全不同,是我算错了吗?
不是算错,是编码口径不同,而 hex 本身不携带编码信息。同一串 4E2D6587(4 字节):UTF-8 下解出 2 个汉字(中文),ISO-8859-1 / Latin-1 下解出 4 个字符(N-e‡),因为 Latin-1 的规则是「每个字节直接对应码位 U+0000~U+00FF」,不做任何多字节组合。windows-1252 与 Latin-1 在这个区间也几乎一致。所以「hex 转文本」这个说法本身是不完整的,必须补上「按什么编码解」。
还有一层:utf-16le 会把同样这 4 字节解成完全不同的两个汉字(因为它按 2 字节一组读),而 utf-16be 会因为字节序不同解出另一组——同一串 hex 在不同 TextDecoder 编码参数下能解出四五个完全不同的结果,这正是「不写编码口径的 hex 转换工具没有意义」的原因。十六进制转文本转换器固定 UTF-8 并在正文给出 Latin-1 的对照,是因为 UTF-8 是 Web 与现代软件的默认口径;但如果你手上的字节来自老系统(GBK、Big5),浏览器的 TextDecoder 根本不支持这两种编码(实测 Node 完整 ICU 下也不在 TextDecoder 支持列表里),需要专门的解码库。
emoji 的 hex 是 8 个字符,为什么只算 1 个字符?
因为 emoji 的码点 U+1F600 大于 U+FFFF,超出了「基本多文种平面」(BMP)的范围,UTF-8 用 4 字节编码它(hex 共 8 个字符:F0 9F 98 80),而 JS 字符串用 UTF-16 表示,需要用代理对拆成 0xD83D(高代理)和 0xDE00(低代理)两个码元。实测 '😀'.length === 2(UTF-16 码元数),但 [...'😀'].length === 1(码点数)。
反算公式是 (高代理 − 0xD800) × 0x400 + (低代理 − 0xDC00) + 0x10000,代入本页实测值:(0xD83D − 0xD800) × 1024 + (0xDE00 − 0xDC00) + 65536 = 0x1F600,验证通过。这就是本页一律用码点计字符数的原因——如果用 s.length,一个 emoji 会被算成 2 个字符,字节与字符的比值就从 4:1 变成 2:1,你就没法用它反查字节数了。
顺带一个容易意外的实测:带变体选择符的 emoji 更复杂。☕️ 实际是 U+2615 + U+FE0F 两个码点(6 字节),🛠️ 是三个码元(7 字节)。而组合 emoji 👨👩👧 用了 3 个 U+200D 零宽连接符,18 字节但只有 5 个码点。结论:emoji 的「字符成本」差异很大,跨平台做长度校验时必须先明确按码点还是按 UTF-16 码元算。
为什么我的代码用 Buffer.from(hex, 'hex') 少了一字节却没报错?
因为它静默截断。实测 Buffer.from('4E2D658', 'hex') 返回的是 <Buffer 4e 2d 65>——只吃了 3 个字节,末尾那个孤立的 8 被直接丢弃,不抛异常也不警告。这在实践中非常危险:一个少抄一位的 16 进制串会变成一条「看起来合法」但少了一个字节的数据,如果这条数据是长度前缀、校验和或长度固定的协议头,错误会一路传播到业务逻辑里。
正确做法是在转换前自己判奇偶:const t = hex.replace(/[\s,;:_-]/g, '').replace(/^0x/i, ''); if (t.length % 2 !== 0) throw new Error('hex 长度为奇数:' + t.length);。本页的输入校验就是这么做的,并且报错文案会直接告诉你清洗后的实际长度。另一个坑是非 hex 字符:parseInt('ZZ', 16) 返回 NaN,但把 NaN 写进 Uint8Array 时会被截成 0——所以 4E2D65ZZ 里的两个 Z 变成了两个 \0 字节,页面上显示成控制字符而不是报错。必须先用 /^[0-9a-fA-F]*$/ 校验字符集,再做长度奇偶校验,两道检查的顺序不能反。
fatal: true 抛的 TypeError 该怎么用?
把它当成校验手段而不是错误处理。对照表最后一行就是两种配置的实测结果:FF、41FF42、C3 这三组非法序列在 fatal: false(默认)下解出带 U+FFFD 的文本,而 fatal: true 会直接抛 TypeError: The encoded data was not valid for encoding utf-8;而 EFBBBF(BOM)两种配置都不报错,只是后者会多给一个 U+FEFF。
什么时候该用 fatal: true:校验外部输入、验证 hash 比对、做数据管道清洗时的「脏数据检出」。它给你一个布尔答案,不需要你去数 U+FFFD 的个数。
什么时候该用默认配置:做尽力而为的内容提取,比如从一段损坏数据里捞还能读的部分。它不会中断处理,而且 U+FFFD 的个数本身就是信息——1 个说明只坏了一个字节,3 个说明整个 3 字节序列都不合法。
注意 fatal: true 不会帮你发现「编码口径错了但字节恰好合法」的情况:对照表第 4 行的 E4BDA0E69687 会被两个配置都判为「成功」,因为它确实能解出「你文」两个合法汉字——字节层面没错,错的是你以为它是别的编码。这种情况任何解码器都检测不出来。
这段字节是 UTF-8 还是 GBK?怎么判断?
先看有没有 U+FFFD:本页的结果区把「含 U+FFFD」单列成一个指标,有替换字符说明不是 UTF-8(GBK 字节里 0x80~0x9F 这段在 UTF-8 里是孤立的后续字节,必然非法)。但反过来不成立——GBK 字节也可能恰好全是合法 UTF-8 序列,比如「中文」的 GBK 编码是 D6D0 CEC4,解出来是四个不可见字符而不是「中文」,而某些生僻字的 GBK 编码碰巧就是合法 UTF-8。
判断顺序应该是:第一,看有没有 U+FFFD(有 → 一定不是 UTF-8);第二,看解出来的文本是否可读(是乱码 → 编码猜错了,即使字节合法);第三,对照已知明文——如果你知道原文里有中文,那 D6D0 CEC4 这种「解出来是两个不可见字符」的结果就说明是 GBK,因为 UTF-8 的汉字至少占 3 字节。
至于转换:浏览器与 Node 的标准 TextDecoder 都不支持 GBK 和 Big5(完整 ICU 下也不在支持列表里),要转必须引入专门的解码库。UTF-8 与 Latin-1 的互转则本页能覆盖思路:new TextDecoder('latin1') 可以用。
相关工具
- 文本转十六进制转换器——同一链路的反方向,把文本按 UTF-8 编成字节再转 hex;用两页互验能确认「这段 hex 对应的原文到底是什么」。
- 十进制转十六进制转换器——纯数值口径(每个字节的十进制值),不做字符编码,适合调试协议里的字节字段。
- 十六进制转十进制转换器——本工具的数值侧对照:同样一串 hex,逐字节看十进制值更容易发现「某个字节明显超出 ASCII 范围」。
- 二进制转文本——同一个 TextDecoder 逻辑,但输入是 0/1 串,适合看字节的二进制位模式(判断首字节落在 0x00~0x7F 还是 0xC0~0xFF 哪一档)。
- ASCII转文本转换器——纯 7 位口径,每个 hex 字节直接对应一个字符,可以和本页的 UTF-8 结果逐行对照,看出多字节编码到底占了哪几个字节。
- Unicode转义反转义工具——处理
\u4e2d这种转义形式,与 hex 是同一批字符的两种文本外衣,常在 JS/Java 源码里混着出现。 - 数字进制转换器——一次处理二/八/十/十六进制的批量转换,适合先把 hex 串拆成字节值再逐个核对。