Skip to content
返回博客
教程

HTML 实体详解:命名实体、数字实体与转义时机

HTML 实体完全指南:命名、十进制与十六进制引用的区别,五个必须转义的字符,以及避免 XSS 的上下文转义规则。

8 分钟

HTML 实体(entity)是一种书写字符的方式,让浏览器把它当作普通文本显示,而不是当作标记来解析。在内容里直接写一个 <,浏览器会以为你在开始一个标签;改写成 &lt;,页面上就会原样渲染出一个 <。这种替换,正是 HTML 实体编码的核心思路。

有五个字符在 HTML 中带有特殊含义,也是最常需要转义(escape)的字符:<>&"'。转义它们出于两个原因:一是显示,你想把代码或标记当作文本展示出来;二是安全(这一点更重要),转义不可信的输入是阻止跨站脚本攻击(XSS)的根本手段。

任何实体都有三种可互换的写法——命名(&lt;)、十进制(&#60;)和十六进制(&#x3C;)——它们最终都解析为同一个字符。更棘手的问题是「什么时候」转义、「用哪种方式」转义,因为正确答案取决于值最终落在哪里:HTML 文本、属性、脚本,还是 URL。下面先讲清三种写法和保留字符集合,再用一张矩阵说明按上下文该怎么选,最后收一些容易踩坑的地方。

什么是 HTML 实体?(结构剖析)

HTML 实体,又称字符引用(character reference),是一段代表单个字符的简短代码。每个实体都以一个 & 符号(ampersand)& 开头,以一个分号 ; 结尾。两者之间的内容决定了你得到哪个字符。

它有三种形式:

  • &name; —— 命名引用,比如 &lt;&copy;
  • &#decimal; —— 十进制数字引用,比如 &#60;
  • &#xhex; —— 十六进制数字引用,比如 &#x3C;

浏览器读取这个引用,查出它指向的字符,然后渲染出那个单一字符。可见结果毫无变化——&lt; 和直接写的 < 显示效果完全一致。唯一的区别是,实体始终被当作文本处理,绝不会被当成标签的开头。

三种写法:命名、十进制、十六进制

三种写法引用的是同一个 Unicode 码点(code point),只是拼写不同。命名实体是可读性最好的形式,但只对有定义名称的字符存在。十进制实体用十进制(base 10)写码点。十六进制实体用十六进制(base 16)写同一个码点,它与 Unicode 标准中你看到的 U+XXXX 写法一一对应。

字符命名十进制十六进制
<&lt;&#60;&#x3C;
&&amp;&#38;&#x26;
©&copy;&#169;&#xA9;
é&eacute;&#233;&#xE9;

正因为十六进制直接对应 U+XXXX——éU+00E9,所以写作 &#xE9;——很多开发者在记录或推敲某个具体码点时会优先用它。而对日常的标记书写来说,命名实体可读性最佳。

五个必须转义的保留字符

这些就是会改变浏览器解析文档方式的 HTML 特殊字符。如果它们出现在本应「显示」而非「执行」的内容里,就要转义。

字符命名十进制十六进制不转义会出什么问题
<&lt;&#60;&#x3C;开启一个标签——浏览器把后续文本当作标记读取
>&gt;&#62;&#x3E;提前闭合标签
&&amp;&#38;&#x26;开启一个实体——后面的内容可能被误读为引用
"&quot;&#34;&#x22;提前结束双引号属性值
'&#x27;&#39;&#x27;提前结束单引号属性值

HTML 中的 & 实体是整个体系的根。& 字符是「每一个」实体的开头,所以它必须最先被转义——要是先转义尖括号再转义 & 符号,你就会把刚生成的那些实体里的 & 又转义一遍。这个陷阱下文细说。

到底什么时候才需要转义?(按上下文判断)

大多数 bug 和漏洞都藏在这里。核心原则很短:在输出时转义,并匹配值所落入的上下文。 一个值在某处安全,换个地方就可能危险,所以你施加的编码必须与目标位置相匹配。

HTML 元素内容

当你把一个值放进标签之间——<p><div><td> 里面——要转义 <>&。在这里转义引号无害,但没有必要。如果你想把文本 <strong> 当作字面字符显示,而不是让后面的词变粗体,就把它编码成 &lt;strong&gt;,浏览器便会打印出这个标签,而不是应用它。

HTML 属性值

在属性内部,引号字符变得至关重要。如果一个值位于 title="…" 中且包含未转义的 ",它会提前结束属性,让攻击者得以追加新的属性——这是一种经典的 XSS 攻击途径。在属性上下文中要转义 "(最好也转义 ')。像 He said "hi" 这样的值必须变成 He said &quot;hi&quot; 才能被安全地包住。

<script> 或行内 JavaScript 中

HTML 实体在这里帮不上忙。构建进 <script> 块或行内事件处理器里的字符串,需要的是 JavaScript 或 JSON 字符串转义,而不是字符引用。在 JS 字符串字面量里写 &quot;,得到的是字面上的六个字符,而不是一个引号。对于这种上下文,请使用 JSON 转义 工具,并阅读 JSON 字符串转义完全指南,了解在脚本内真正适用的 \uXXXX 规则。

在 URL 中

URL 有自己的转义方案:百分号编码(percent-encoding)。HTML 实体无法让一个值变得 URL 安全。字符串 a&b c 在查询串里应写作 a%26b%20c,而不是 a&amp;b c——后者里空格仍会破坏 URL,& 也仍然会分隔参数。这种情况请使用 URL 编码解码工具,并参阅 URL 编码与解码指南,了解保留字符与非保留字符的完整规则。

决策矩阵

上下文用什么转义示例会失败的错误选择
HTML 元素内容HTML 实体(< > &<strong>&lt;strong&gt;< 保持原样会注入一个标签
HTML 属性值HTML 实体(" ' 至关重要)"hi"&quot;hi&quot;未转义的 " 会突破属性
<script> / 行内 JSJS / JSON 字符串转义"\"HTML 实体在 JS 中是惰性的
URL / 查询串百分号编码空格 → %20&amp; 与实体仍会破坏 URL

命名还是数字:该用哪种?

命名实体可读性好,对于常见的保留字符和广为人知的符号——&lt;&amp;&copy;&mdash;——是最合适的默认选择。但它们只对有定义名称的字符存在。数字实体,无论十进制还是十六进制,可以编码「任何」码点,包括那些没有名称的,这使它们成为通用的后备方案。当你无法保证消费方系统支持某个特定的命名实体时,数字写法才是稳妥之选。

为什么单引号是 &#x27; 而不是 &apos;

命名实体 &apos; 直到 HTML5 和 XML 才被引入。它在 HTML4 中未定义,因此一小部分较老的解析器和邮件客户端会把它渲染成字面文本 &apos;,而不是一个撇号。数字引用 &#x27;——以及它的十进制孪生兄弟 &#39;——指向的是完全相同的字符 U+0027,并且能被有史以来每一个符合规范的解析器理解。正因如此,像 he 这样经过充分测试的转义库会用 &#x27; 来表示单引号,而一个好的编码器也会遵循这一约定,使输出能安全地放入任何 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;&gt;)开头的 & 也会被转义,于是 < 最终变成 &amp;lt;,渲染出来是字面文本 &lt;。一定要先转义 &,再处理其余字符,这样就能绕开最常见的那个编码 bug。

对已转义的文本重复编码

把已经转义过的文本再丢进编码器,会被二次编码。&amp; 变成 &amp;amp;,访客在页面上看到的是 &amp; 而非 &。只在输出时转义一次。如果一个值要经过好几层处理,务必确保只有其中一层做转义。

解码时出现乱码

反向操作也有它自己的坑。用错误的字符集解码,或者解码两次,就会得到乱七八糟的输出——也就是经典的乱码(mojibake)。如果某个页面把你期望显示成 < 的地方显示成了字面的 &amp;lt;,把它粘进 HTML 实体解码器,就能看清这些实体究竟解析成什么;它能处理命名、十进制、十六进制,甚至像 &copy 这种没有结尾分号的遗留未终止引用。

把转义当成 XSS 的万能解药

转义是第一道防线,而非唯一一道。由于 HTML 有好几种规则各异的上下文,为错误的上下文做转义会留下漏洞——属性里的引号、脚本里的 JS 转义、URL 里的百分号编码。把实体编码当作地基,在它之上再叠加内容安全策略(CSP)和框架自带的自动转义。

实践中如何编码和解码实体

当你手写 HTML 时,转义就得自己来。下面是一个正确的 escapeHtml(),它处理好了「& 优先」的顺序,并附上了真实应用代码中的更佳做法。

// The five reserved characters and their safe entities:
//   <  →  &lt;     >  →  &gt;     &  →  &amp;     "  →  &quot;     '  →  &#x27;

function escapeHtml(str) {
  return str
    .replace(/&/g, '&amp;')   // & FIRST, so later entities are not double-escaped
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;'); // numeric form — safe in HTML4, HTML5 and XML
}

const userInput = `<a href="x">Tom & Jerry's</a>`;
const safe = escapeHtml(userInput);
// → &lt;a href=&quot;x&quot;&gt;Tom &amp; Jerry&#x27;s&lt;/a&gt;

// 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 实体是一段简短代码,以 & 开头、以 ; 结尾,代表单个字符。浏览器会渲染出实体所指向的字符,而不是把它当作标记处理。例如,&lt; 显示为字面的 <&amp; 显示为字面的 &

在 HTML 中我需要转义哪些字符?

五个保留的 HTML 特殊字符:<>&"'。在元素内容里你主要需要转义 <>&;在属性值里,引号 "' 也变得至关重要。要先转义 & 符号,这样其他实体才不会被重复转义。

我该用命名实体还是数字(十进制/十六进制)实体?

对于常见字符,用命名实体(&lt;&copy;)以保证可读性,因为它们容易辨认。当你需要编码一个没有定义名称的字符,或者无法保证消费方支持某个命名实体时,用数字实体(十进制 &#60; 或十六进制 &#x3C;)。两种形式引用的是同一个码点。

HTML 实体能防御 XSS 吗?

正确使用时,它们是根基。在把不可信输入放入 HTML 元素或属性内容之前,转义那五个保留字符,能阻止标签注入和脚本注入。但转义是依上下文而定的:脚本块需要 JavaScript 转义,URL 需要百分号编码。把正确的、按上下文区分的转义,与 CSP 和框架的自动转义结合起来。

为什么我的页面显示的是 &amp;lt; 而不是 <

那是重复转义。文本被编码了两次,或者 & 在尖括号之后才被转义,导致 &lt; 里的 & 被变成了 &amp;。于是访客看到的是字面文本 &lt;。只转义一次,并且永远先转义 &。解码工具可以确认这些实体究竟解析成什么。

我需要转义 é、— 或 emoji 这类字符吗?

通常不需要。在一个声明了 <meta charset="utf-8"> 的页面上,带重音的字母、破折号和 emoji 都是合法的原始字符,无需编码——保持原样即可。只有当文本必须经过一个老旧的单字节字符集,或者一个会损坏原始 UTF-8 的系统时,才去编码非 ASCII 字符。

标签: HTML Encoding Security Web