HTML 实体转义与还原(& < 中 与浏览器的宽容规则)
把文本转成 HTML 安全的写法(& < > 引号,以及可选的中文和 emoji 数字引用),或者反过来把实体引用还原成文字:命名表、缺分号的遗留名字、Windows-1252 数字段和代理区处理都按浏览器的口径来。全部在浏览器本地完成。
A · 转义(文本 → HTML 里能安全写出来的样子)
B · 还原(实体引用 → 文字)
转义与还原的规则
往网页里塞文本的两种翻车方式
往网页里塞文本时有两种翻车方式:一种是把 <b> 或用户输入的 <script> 当成标签渲染出来,一种是把已经写成 & 的东西再处理一遍,结果页面上显示的是字面上的 &amp;。这两种都需要一个「看得懂浏览器怎么解释」的转换,而不是随便 replace 一下。
所以这里做两个方向:A 转义——把要放进 HTML 的普通文本变成安全的写法;B 还原——把实体引用(&、中、😀、 )变回文字。B 段刻意照着浏览器解析器的口径写:命名表、能省分号的遗留名字、0x80-0x9F 的 Windows-1252 老映射、代理区和越界码点怎么处理,都和 HTML 规范一致(所有解码结果都跟浏览器自己的解析结果逐条比对过)。
A 段:哪几个字符必须动
按用途分两个口径:
- 放在元素的内容里(
<p>这里</p>):只需要动三个——&→&、<→<、>→>。其中>严格说不是每处都必须,但-->、]]>这类序列里会出问题,所以一律转。 - 放在属性值里(
title="这里"):再加两个——"→"、'→'。HTML 里属性用双引号包时只有"必须转,但工具不知道你会用哪种引号,所以「属性口径」默认把两种引号一起转,粘进任何一种属性都安全。勾掉它就退化成内容口径,"和'原样保留。
转义是一次性的,不是幂等的:& 进去会变成 &amp;。已经转过一遍的文本再转一次,页面显示的就是字面上的 &。这段文本此前是不是 HTML,工具不知道,也不猜。
勾上「非 ASCII 转数字引用」后,所有码点大于 0x7F 的字符(中文、重音字母、emoji)都写成 &#十进制;,一个码点一个引用:emoji 😀 在 JavaScript 里是 2 个 UTF-16 单元,但输出是 😀 一个引用,不会被拆成两半。这个选项适合要发进只认 ASCII 的老系统的内容;日常网页不需要,UTF-8 的页面直接写中文更好读。
B 段:还原的规则
&名字;:先找「后面紧跟分号的最长名字」。表里有 187 个常用名字(& < > " '两种大小写写法、拉丁文重音字母大小写两套、标点排版、货币单位、希腊字母、数学箭头)。名字认不出来又以分号结尾 → 原样保留,并计入「未识别的引用数」。- 没写分号的老名字:HTML 4 时代那批 ISO 8859-1 实体历史上允许省分号,浏览器到今天还认。勾上「按遗留规则还原」后,
©→©、 → 不换行空格,而且用的是最长匹配:¬itice会还原成「¬itice」而不是报错,因为能当遗留名字用的最长前缀是not。hellip、mdash、euro、apos、trade这些 HTML 4/5 才有的名字不在省分号的名单里,必须写全。关掉这个开关,未写分号的名字就原样保留。 - 数字引用:
&#后面跟十进制或#x十六进制,值按码点解释。分号同样可以省(A还原成A),只是会被记进提示里。 - 孤立的
&:AT&T里的&后面不接合法名字时不算引用,原样输出,也不计入未识别。反过来AT&T是合法的,会还原。
数字引用有几类值不能按「码点就是字符」直接输出,工具照浏览器解析器的实际做法处理(下面每一行都和浏览器自己的解析结果比对过):
| 输入 | 结果 | 原因 |
|---|---|---|
� |
替换字符(U+FFFD) | 规范里唯一被明确替换的控制码点就是 NUL |
 … 、 |
原样输出那个不可见的控制字符 | 这些位置只是「解析错误」,规范并不替换它们;	  就是制表、换行、换页、回车 |
€ … Ÿ |
按 Windows-1252 老表映射 | € → €、— → —、” → “;表里本来就空着的 5 个位置(    )浏览器仍吐出对应的不可见控制字符 |
� … � |
替换字符(U+FFFD) | 代理区码点不能单独存在,规范禁止用引用写出来 |
�(超过 U+10FFFF) |
替换字符(U+FFFD) | 超出 Unicode 范围 |
&#x; |
替换字符,分号被消耗 | 一个十六进制位都没有:浏览器给替换字符 |
&#;、&#xGG; |
原样保留 | 没有数字就根本不是引用,浏览器把开头的 & 当普通字符放回 |
因为控制字符真的会被还原出来,结果框里可能出现看不见的内容:复制出去就是那几个原始字节,而 <textarea> 自己会把 \r 显示成换行。这不是解码错了,是输入框的行为。
什么时候结果不能直接用
- A 段不是 XSS 防护的替代品:它只保证「文本作为文本出现」。如果内容要放进
<script>、<style>或 URL 里,光转义引号是不够的——那些位置的解析规则完全不同(比如<script>里只有</script能结束它,<在那里不是小于号)。请让模板引擎或框架的转义函数负责,它们知道自己的上下文。 - 换行不会因为转义而保留:HTML 把源码里的换行当空白折叠。想让断行显示出来要用
<br>(那就得允许标签,和上面的转义思路冲突)或者 CSS 的white-space: pre-line。 - B 段还原出来的文本不能直接再塞进 HTML:
<b>还原成<b>之后,它就是真的标签了。还原和插入之间要做什么校验是调用方的事。 - 命名表是精选的,不是全量:HTML 规范有 2231 个命名引用(
𝕒、∳这类数学大字全在里面,Α这种大写希腊字母也没收)。这里只收录日常会遇到的 187 个,其余的会进「未识别」并原样保留。要还原生僻数学实体就用数字引用。 - 不要把 & 的宽容规则当依据:
 能用是 1990 年代的遗留,新写的代码应该一律带分号;&在 XHTML 里直接是错误。工具支持只因为你粘到的东西往往就是这么写的。
常见问题
Q:µ 和 μ 有什么区别?
不是同一个字符。µ 是 U+00B5 MICRO SIGN(拉丁文单位的 µ,来自 ISO 8859-1),μ 是 U+03BC GREEK SMALL LETTER MU(希腊字母 μ)。两者外形几乎一样,码点不同,复制出去在搜索、比较里就是两个字符。工具把两者都按各自码点还原,不合并。
Q:为什么不直接用浏览器解析(innerHTML 一行搞定)?
因为那是把整段内容交给 HTML 解析器,任何 <img onerror> 都会立刻被执行。做转换的脚本只扫字符串,没有 eval、没有 innerHTML,也不会把文本交给任何服务器,你粘进去的文本不会离开这台设备;而且正因为不依赖解析器,同一份规则在任何浏览器里结果一模一样。
Q:’ 为什么不是 ’?
它就是 ’(U+2019)。’ 落在 Windows-1252 老表里,工具按那张表映射——这也正是浏览器过去二十年的行为。注意这层的存在意味着「把 ’ 当作’第 146 号 Unicode 码点’」是错的:U+0092 是个不可见的控制字符,规范特意把这一段接回了老编码。
Q:一次能处理多少? 每段上限 4000 个字符,超出只处理前 4000 个并在提示里写明。按键 60 毫秒后合并计算一次,两次输入互不影响:A 段的输出可以直接按 Ctrl+C 拿走去用,也可以整块粘进 B 段验证还原结果对不对。