HTML 实体编码解码与 Unicode 检查
把文本转成 HTML 实体或还原,看清一段文本由哪些码位、字节和字符组成,并得到 JavaScript、JSON、Python、CSS 字符串用的转义序列。不会上传任何内容,全部在本页里运行。
把文本转成 HTML 字符引用(&、é、😀),或把它们还原成文本。
已编码 9 个字符:8 个用名字,1 个用数字。
粘贴任意文本,查看每个码位及其 UTF-8、UTF-16 形式,读者眼里算几个字符,以及四种规范化形式下它会怎样变化。
- UTF-16 单元
- 23
- 码位
- 18
- 读者看到的字符数
- 11
- UTF-8 字节
- 42
码位列表
| # | 字符 | 码位 | UTF-8 字节 | UTF-16 单元 | 类别 |
|---|---|---|---|---|---|
| 1 | e | U+0065 | 65 | 0065 | Ll 小写字母 |
| 2 | ◌́ | U+0301 | CC 81 | 0301 | Mn 非间距组合符 |
| 3 | ␣ | U+0020 | 20 | 0020 | Zs 空格 |
| 4 | 👨 | U+1F468 | F0 9F 91 A8 | D83D DC68 | So 其他符号 |
| 5 | · | U+200D | E2 80 8D | 200D | Cf 格式字符(不可见) |
| 6 | 👩 | U+1F469 | F0 9F 91 A9 | D83D DC69 | So 其他符号 |
| 7 | · | U+200D | E2 80 8D | 200D | Cf 格式字符(不可见) |
| 8 | 👧 | U+1F467 | F0 9F 91 A7 | D83D DC67 | So 其他符号 |
| 9 | ␣ | U+0020 | 20 | 0020 | Zs 空格 |
| 10 | 🇨 | U+1F1E8 | F0 9F 87 A8 | D83C DDE8 | So 其他符号 |
| 11 | 🇦 | U+1F1E6 | F0 9F 87 A6 | D83C DDE6 | So 其他符号 |
| 12 | ␣ | U+0020 | 20 | 0020 | Zs 空格 |
| 13 | A | U+0041 | 41 | 0041 | Lu 大写字母 |
| 14 | ◌̊ | U+030A | CC 8A | 030A | Mn 非间距组合符 |
| 15 | ␣ | U+0020 | 20 | 0020 | Zs 空格 |
| 16 | Å | U+00C5 | C3 85 | 00C5 | Lu 大写字母 |
| 17 | ␣ | U+0020 | 20 | 0020 | Zs 空格 |
| 18 | fi | U+FB01 | EF AC 81 | FB01 | Ll 小写字母 |
这里不显示字符名称,因为名称表会给本页增加很大的数据文件。通用类别(字母、组合符号、符号等)来自你浏览器自带的 Unicode 数据。
这段文本里的类别
- Lu 大写字母: 2
- Ll 小写字母: 2
- Mn 非间距组合符: 2
- So 其他符号: 5
- Zs 空格: 5
- Cf 格式字符(不可见): 2
读者眼里的一个字符
每个方框是浏览器切出的一个字形簇。一面旗帜、一个家庭表情、一个带音调的字母,可以由好几个码位组成,但只占一个方框。
- é2 个码位
- ␣U+0020
- 👨👩👧5 个码位
- ␣U+0020
- 🇨🇦2 个码位
- ␣U+0020
- Å2 个码位
- ␣U+0020
- ÅU+00C5
- ␣U+0020
- fiU+FB01
规范化形式
同一段看起来一样的文本可以有不同的存储方式,比如 e 加一个组合音调,或者一个预先组合好的字母。规范化能让比较和搜索得到一致的结果。
| 形式 | 结果 | 码位数 | UTF-8 字节 | 与输入相比 | 复制 |
|---|---|---|---|---|---|
| NFC | é 👨👩👧 🇨🇦 Å Å fi | 16 | 40 | 有变化 | |
| NFD | é 👨👩👧 🇨🇦 Å Å fi | 19 | 43 | 有变化 | |
| NFKC | é 👨👩👧 🇨🇦 Å Å fi | 17 | 39 | 有变化 | |
| NFKD | é 👨👩👧 🇨🇦 Å Å fi | 20 | 42 | 有变化 |
NFC 和 NFD 只重排或合并等价的字符。NFKC 和 NFKD 还会替换外观相近的字符,比如 fi 连字或带圈数字,可能改变含义,需要原样保留的文本请不要用。
把文本写成 JavaScript、JSON、Python 或 CSS 字符串里的转义序列。反斜杠和控制字符总会被转义。
已转义 8 个字符。
它是怎么工作的
- •HTML 实体:第一种模式只把 & < > " 和 ' 写成引用,这正是把文本放进 HTML 页面或带引号的属性时需要的。另外两种模式会把所有非 ASCII 字符或所有字符也写出来,给只接受纯 ASCII 的系统用。可以写成名字(é)、十进制(é)或十六进制(é)。表情符号写成一个引用,而不是两个半截。
- •解码只做一遍,所以 &lt; 变成 <,而不是 <。它认十进制和十六进制引用,以及下一条说的 253 个命名引用。数字引用遵循 HTML 标准:�、代理项和大于 U+10FFFF 的值变成 U+FFFD,€ 到 Ÿ 按 Windows-1252 字符读取(– 是短破折号)。这里的命名引用必须带结尾分号。不认识的内容原样保留,并列出来。
- •名字表共 253 项:HTML 4.01 的 252 个名字(带音调的字母、希腊字母、箭头、常用符号、 、©、€ 等)加上 '。Python 自带的 HTML5 表有 2,125 个名字,所以还有很多少见的名字本页不会解码。这张表是用脚本从 Python 的表生成的,不是手敲的。
- •Unicode 检查:码位是 Unicode 表里的一个编号。JavaScript 按 UTF-16 单元计数,一个表情占两个;文件和网页多用 UTF-8,一个码位占一到四个字节。读者眼里的一个字符(字形簇)可能由多个码位组成,这个数字来自你浏览器的文本切分。类别(字母、组合符号、符号)来自浏览器自带的 Unicode 数据,所以很新的字符在较旧的浏览器里可能显示为未分配。这里不显示字符名称,因为名称表会让本页大很多。
- •规范化:NFC、NFD、NFKC、NFKD 四种形式由你的浏览器生成。NFC 和 NFD 只在等价写法之间转换,比如 e 加组合音调与单个字母 é。NFKC 和 NFKD 还会替换外观相近的字符,比如 fi 连字,可能改变含义。
- •转义格式:JavaScript 的 \uXXXX(表情变成代理项对)、JavaScript 的 \u{X}、JSON、Python(\xXX、\uXXXX、\UXXXXXXXX)和 CSS(\X,只有当下一个字符会被误读成数字的一部分时才在后面加空格)。反斜杠和控制字符总会被转义,字符串用哪种引号由你选。孤立代理项和 U+0000 无法写进 CSS,也无法写成 HTML 引用,会变成 U+FFFD 并提示你。
- •怎么验证的:逻辑与 Python(html、unicodedata、json、ast)、Perl(字形簇)和 Node 自带的解析器在 156 个字符串上做了对比,其中有表情、组合符号、连接符序列、孤立代理项和形似实体的乱码;全部 1,114,112 个码位的字符类别也和 Python 的做了对比,只有一个码位不同(U+0295,在最新的 Unicode 数据里类别与 Python 的不一样)。有少数字符串没有参加部分对比,因为 Python 或 Perl 的 Unicode 数据或字形簇规则比你的浏览器旧(比如天城文的连字)。超过 200,000 个字符的文本会被拒绝并给出提示,而不是硬处理。
编码不等于消毒
哪些字符危险取决于文本放在哪里。在 HTML 文本和带引号的属性里,& < > 和引号是关键,第一种模式正好覆盖。在 <script> 或 <style> 块里,实体根本不会被解码,需要用 JavaScript 或 CSS 的转义。在 href 这样的链接里,编码拦不住 javascript: 地址,必须检查协议。在不带引号的属性里,空格和许多其他字符都会出问题。
本页只改变文本,不会删除标签、检查网址,也不知道你要把结果贴到哪里。它不是消毒工具,不能让不可信的输入变安全。请使用模板引擎针对对应场景的内置转义;也不要先用“解码”处理不可信文本,再把结果当作 HTML 放进页面。
常见问题
- 会上传我的文本吗?
- 不会。所有计算都由本页里运行的代码完成,不会发送到任何地方。复制也在你的设备上完成。
- 为什么看起来一个字符,却算成好几个?
- 文本是按码位存储的。字母 é 可以是一个码位(U+00E9,UTF-8 占 2 字节),也可以是 e 加一个组合音调(2 个码位,3 字节)。由三个人用不可见连接符连起来的家庭表情是 5 个码位、8 个 UTF-16 单元、18 个 UTF-8 字节,但读者只看到一个字符。检查页里的第一个示例就能看到。规范化成 NFC 会把 e 和音调合成一个字母。
- 为什么 ★ 没有被解码?
- 因为它不在本页的 253 个名字表里。与其乱猜,不如保持原文并列出来。HTML5 有两千多个名字,本页只认常见的。数字引用比如 ★ 永远可用。
- 用第一种模式,我的页面就能防住跨站脚本攻击吗?
- 只有当文本放进 HTML 文本或带引号的属性时才算。对 script 块、style 块、网址和不带引号的属性都没有作用,也不能代替你所用框架的转义。见上面的说明。