HTML 实体(entity)是一种书写字符的方式,让浏览器把它当作普通文本显示,而不是当作标记来解析。在内容里直接写一个 <,浏览器会以为你在开始一个标签;改写成 <,页面上就会原样渲染出一个 <。这种替换,正是 HTML 实体编码的核心思路。
有五个字符在 HTML 中带有特殊含义,也是最常需要转义(escape)的字符:<、>、&、" 和 '。转义它们出于两个原因:一是显示,你想把代码或标记当作文本展示出来;二是安全(这一点更重要),转义不可信的输入是阻止跨站脚本攻击(XSS)的根本手段。
任何实体都有三种可互换的写法——命名(<)、十进制(<)和十六进制(<)——它们最终都解析为同一个字符。更棘手的问题是「什么时候」转义、「用哪种方式」转义,因为正确答案取决于值最终落在哪里:HTML 文本、属性、脚本,还是 URL。下面先讲清三种写法和保留字符集合,再用一张矩阵说明按上下文该怎么选,最后收一些容易踩坑的地方。
什么是 HTML 实体?(结构剖析)
HTML 实体,又称字符引用(character reference),是一段代表单个字符的简短代码。每个实体都以一个 & 符号(ampersand)& 开头,以一个分号 ; 结尾。两者之间的内容决定了你得到哪个字符。
它有三种形式:
&name;—— 命名引用,比如<或©。&#decimal;—— 十进制数字引用,比如<。&#xhex;—— 十六进制数字引用,比如<。
浏览器读取这个引用,查出它指向的字符,然后渲染出那个单一字符。可见结果毫无变化——< 和直接写的 < 显示效果完全一致。唯一的区别是,实体始终被当作文本处理,绝不会被当成标签的开头。
三种写法:命名、十进制、十六进制
三种写法引用的是同一个 Unicode 码点(code point),只是拼写不同。命名实体是可读性最好的形式,但只对有定义名称的字符存在。十进制实体用十进制(base 10)写码点。十六进制实体用十六进制(base 16)写同一个码点,它与 Unicode 标准中你看到的 U+XXXX 写法一一对应。
| 字符 | 命名 | 十进制 | 十六进制 |
|---|---|---|---|
< | < | < | < |
& | & | & | & |
© | © | © | © |
é | é | é | é |
正因为十六进制直接对应 U+XXXX——é 是 U+00E9,所以写作 é——很多开发者在记录或推敲某个具体码点时会优先用它。而对日常的标记书写来说,命名实体可读性最佳。
五个必须转义的保留字符
这些就是会改变浏览器解析文档方式的 HTML 特殊字符。如果它们出现在本应「显示」而非「执行」的内容里,就要转义。
| 字符 | 命名 | 十进制 | 十六进制 | 不转义会出什么问题 |
|---|---|---|---|---|
< | < | < | < | 开启一个标签——浏览器把后续文本当作标记读取 |
> | > | > | > | 提前闭合标签 |
& | & | & | & | 开启一个实体——后面的内容可能被误读为引用 |
" | " | " | " | 提前结束双引号属性值 |
' | ' | ' | ' | 提前结束单引号属性值 |
HTML 中的 & 实体是整个体系的根。& 字符是「每一个」实体的开头,所以它必须最先被转义——要是先转义尖括号再转义 & 符号,你就会把刚生成的那些实体里的 & 又转义一遍。这个陷阱下文细说。
到底什么时候才需要转义?(按上下文判断)
大多数 bug 和漏洞都藏在这里。核心原则很短:在输出时转义,并匹配值所落入的上下文。 一个值在某处安全,换个地方就可能危险,所以你施加的编码必须与目标位置相匹配。
HTML 元素内容
当你把一个值放进标签之间——<p>、<div>、<td> 里面——要转义 <、> 和 &。在这里转义引号无害,但没有必要。如果你想把文本 <strong> 当作字面字符显示,而不是让后面的词变粗体,就把它编码成 <strong>,浏览器便会打印出这个标签,而不是应用它。
HTML 属性值
在属性内部,引号字符变得至关重要。如果一个值位于 title="…" 中且包含未转义的 ",它会提前结束属性,让攻击者得以追加新的属性——这是一种经典的 XSS 攻击途径。在属性上下文中要转义 "(最好也转义 ')。像 He said "hi" 这样的值必须变成 He said "hi" 才能被安全地包住。
在 <script> 或行内 JavaScript 中
HTML 实体在这里帮不上忙。构建进 <script> 块或行内事件处理器里的字符串,需要的是 JavaScript 或 JSON 字符串转义,而不是字符引用。在 JS 字符串字面量里写 ",得到的是字面上的六个字符,而不是一个引号。对于这种上下文,请使用 JSON 转义 工具,并阅读 JSON 字符串转义完全指南,了解在脚本内真正适用的 \uXXXX 规则。
在 URL 中
URL 有自己的转义方案:百分号编码(percent-encoding)。HTML 实体无法让一个值变得 URL 安全。字符串 a&b c 在查询串里应写作 a%26b%20c,而不是 a&b c——后者里空格仍会破坏 URL,& 也仍然会分隔参数。这种情况请使用 URL 编码解码工具,并参阅 URL 编码与解码指南,了解保留字符与非保留字符的完整规则。
决策矩阵
| 上下文 | 用什么转义 | 示例 | 会失败的错误选择 |
|---|---|---|---|
| HTML 元素内容 | HTML 实体(< > &) | <strong> → <strong> | 让 < 保持原样会注入一个标签 |
| HTML 属性值 | HTML 实体(" ' 至关重要) | "hi" → "hi" | 未转义的 " 会突破属性 |
<script> / 行内 JS | JS / JSON 字符串转义 | " → \" | HTML 实体在 JS 中是惰性的 |
| URL / 查询串 | 百分号编码 | 空格 → %20 | & 与实体仍会破坏 URL |
命名还是数字:该用哪种?
命名实体可读性好,对于常见的保留字符和广为人知的符号——<、&、©、———是最合适的默认选择。但它们只对有定义名称的字符存在。数字实体,无论十进制还是十六进制,可以编码「任何」码点,包括那些没有名称的,这使它们成为通用的后备方案。当你无法保证消费方系统支持某个特定的命名实体时,数字写法才是稳妥之选。
为什么单引号是 ' 而不是 '
命名实体 ' 直到 HTML5 和 XML 才被引入。它在 HTML4 中未定义,因此一小部分较老的解析器和邮件客户端会把它渲染成字面文本 ',而不是一个撇号。数字引用 '——以及它的十进制孪生兄弟 '——指向的是完全相同的字符 U+0027,并且能被有史以来每一个符合规范的解析器理解。正因如此,像 he 这样经过充分测试的转义库会用 ' 来表示单引号,而一个好的编码器也会遵循这一约定,使输出能安全地放入任何 HTML、XML 或属性上下文。
字符集还是实体:何时编码非 ASCII
字符集(charset),比如 UTF-8,决定字符如何以字节形式存储。而实体是一种只用纯 ASCII 字符(&、#、;、字母、数字)来拼写某个字符的方式。这是两个不同的层次,把它们混为一谈会导致不必要的编码。
在一个 UTF-8 页面上——也就是几乎每一个声明了 <meta charset="utf-8"> 的现代页面——带重音的字母、破折号和 emoji 都是合法的原始字符。让 é、— 和 😀 保持原样即可。只有当文本必须穿过一个老旧的单字节字符集,或者一个会破坏原始 UTF-8 的系统时,把一切都编码成实体才有意义;正是为这些场景,存在一个「编码所有非 ASCII」的模式。如果你不太清楚字节、码点和字符之间的关系,UTF-8 vs UTF-16 vs Unicode 编码指南 梳理了这套模型。
常见的 HTML 实体陷阱
最后才转义 & 会导致重复转义
顺序很重要。如果你在转义 & 之前先替换了 < 和 >,那么刚生成的实体(<、>)开头的 & 也会被转义,于是 < 最终变成 &lt;,渲染出来是字面文本 <。一定要先转义 &,再处理其余字符,这样就能绕开最常见的那个编码 bug。
对已转义的文本重复编码
把已经转义过的文本再丢进编码器,会被二次编码。& 变成 &amp;,访客在页面上看到的是 & 而非 &。只在输出时转义一次。如果一个值要经过好几层处理,务必确保只有其中一层做转义。
解码时出现乱码
反向操作也有它自己的坑。用错误的字符集解码,或者解码两次,就会得到乱七八糟的输出——也就是经典的乱码(mojibake)。如果某个页面把你期望显示成 < 的地方显示成了字面的 &lt;,把它粘进 HTML 实体解码器,就能看清这些实体究竟解析成什么;它能处理命名、十进制、十六进制,甚至像 © 这种没有结尾分号的遗留未终止引用。
把转义当成 XSS 的万能解药
转义是第一道防线,而非唯一一道。由于 HTML 有好几种规则各异的上下文,为错误的上下文做转义会留下漏洞——属性里的引号、脚本里的 JS 转义、URL 里的百分号编码。把实体编码当作地基,在它之上再叠加内容安全策略(CSP)和框架自带的自动转义。
实践中如何编码和解码实体
当你手写 HTML 时,转义就得自己来。下面是一个正确的 escapeHtml(),它处理好了「& 优先」的顺序,并附上了真实应用代码中的更佳做法。
// The five reserved characters and their safe entities:
// < → < > → > & → & " → " ' → '
function escapeHtml(str) {
return str
.replace(/&/g, '&') // & FIRST, so later entities are not double-escaped
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, '''); // numeric form — safe in HTML4, HTML5 and XML
}
const userInput = `<a href="x">Tom & Jerry's</a>`;
const safe = escapeHtml(userInput);
// → <a href="x">Tom & Jerry's</a>
// Better in app code: let the platform escape for you.
// el.textContent = userInput; // the browser escapes; no manual replace
// React / Vue / Angular escape interpolated text by default
// Server templates (Jinja, ERB, Blade) auto-escape unless you opt out
手写函数有助于理解背后发生了什么,也适合一次性的转换,但在生产环境中应优先走内置路径。设置 element.textContent 会让浏览器替你转义,现代框架则会自动转义插值的值。把手动转义留给平台覆盖不到的场景。
临时性的工作,可以用 HTML 实体编码器 来转义保留字符集(命名、十进制或十六进制),用 HTML 实体解码器 来还原。对于保留字符,这两者互为精确的逆运算,所以你可以让文本在两者之间往返而不丢失任何信息。
常见问题
什么是 HTML 实体?
HTML 实体是一段简短代码,以 & 开头、以 ; 结尾,代表单个字符。浏览器会渲染出实体所指向的字符,而不是把它当作标记处理。例如,< 显示为字面的 <,& 显示为字面的 &。
在 HTML 中我需要转义哪些字符?
五个保留的 HTML 特殊字符:<、>、&、" 和 '。在元素内容里你主要需要转义 <、> 和 &;在属性值里,引号 " 和 ' 也变得至关重要。要先转义 & 符号,这样其他实体才不会被重复转义。
我该用命名实体还是数字(十进制/十六进制)实体?
对于常见字符,用命名实体(<、©)以保证可读性,因为它们容易辨认。当你需要编码一个没有定义名称的字符,或者无法保证消费方支持某个命名实体时,用数字实体(十进制 < 或十六进制 <)。两种形式引用的是同一个码点。
HTML 实体能防御 XSS 吗?
正确使用时,它们是根基。在把不可信输入放入 HTML 元素或属性内容之前,转义那五个保留字符,能阻止标签注入和脚本注入。但转义是依上下文而定的:脚本块需要 JavaScript 转义,URL 需要百分号编码。把正确的、按上下文区分的转义,与 CSP 和框架的自动转义结合起来。
为什么我的页面显示的是 &lt; 而不是 <?
那是重复转义。文本被编码了两次,或者 & 在尖括号之后才被转义,导致 < 里的 & 被变成了 &。于是访客看到的是字面文本 <。只转义一次,并且永远先转义 &。解码工具可以确认这些实体究竟解析成什么。
我需要转义 é、— 或 emoji 这类字符吗?
通常不需要。在一个声明了 <meta charset="utf-8"> 的页面上,带重音的字母、破折号和 emoji 都是合法的原始字符,无需编码——保持原样即可。只有当文本必须经过一个老旧的单字节字符集,或者一个会损坏原始 UTF-8 的系统时,才去编码非 ASCII 字符。