html entity encode decode

HTML 实体编码与解码:安全的 Web 内容管理

瞬间对 HTML 实体进行编码和解码,防止 XSS 攻击并确保您的特殊字符在所有浏览器中完美渲染。

什么是HTML实体?

HTML实体(HTML Entity)是一种特殊的文本序列,用于在HTML文档中表示那些具有特殊含义或难以直接输入的字符。每个实体以 &(与号)开头,以 ;(分号)结尾,中间是实体名称(命名实体,如 &)或字符的数字码位(数字实体,如 &&)。

HTML实体不只是排版工具——它是Web安全的基石。任何需要处理用户输入、CMS数据或动态内容的Web开发者,都必须深刻理解HTML实体的工作原理。


字符编码的历史演变

ASCII时代(1960年代–1980年代)

ASCII(美国信息交换标准代码)定义了128个字符:26个英文字母(大小写)、数字、标点符号和控制字符。这对美式英语已经足够,但对于世界上其他语言则完全不够用。

Latin-1 / ISO-8859-1(1980年代–1990年代)

ISO-8859-1(也称Latin-1)利用第8位将字符集扩展到256个,增加了西欧语言中常用的重音字母(如é、ü、ñ等)。HTML 2.0和HTML 3.2正式采用Latin-1作为参考字符集,并于此时定义了许多命名实体,如 é(é)、ü(ü)和 ñ(ñ)。

然而,256个字符仍无法涵盖日语、阿拉伯语、中文、韩语等文字系统。各地区发明了互不兼容的编码方案(Shift-JIS、Big5、GBK等),造成了著名的"乱码"问题——编码混用时文字显示为乱码。

Unicode与UTF-8(1991年至今)

Unicode联盟于1991年发布首个Unicode标准,目标是为世界上每一种书写系统的每个字符分配唯一的码位。如今,Unicode已涵盖超过14万个字符,涵盖150多种文字系统。

UTF-8由Ken Thompson和Rob Pike于1992年提出,将Unicode码位编码为1–4字节,且与ASCII向后兼容。2000年代成为Web的主导编码方式。截至2024年,超过98%的网页使用UTF-8。

UTF-8时代实体仍有必要吗?

即使在UTF-8编码的文档中,HTML实体仍不可或缺,原因有三:

  1. 保留字符<>& 在HTML标记中具有特殊含义,必须转义才能字面显示。
  2. 属性分隔符"' 用于界定属性值,在属性值中出现时必须转义。
  3. 空白控制&nbsp;(不换行空格)提供普通空格无法实现的版式控制。

核心概念:HTML实体的工作原理

命名实体

命名实体是最易读的形式,使用基于字符描述的助记名称。HTML5定义了2000多个命名实体。

<!-- 命名实体的使用示例 -->
<p>面包 &amp; 黄油</p>          <!-- 显示:面包 & 黄油 -->
<p>3 &lt; 5,10 &gt; 7</p>     <!-- 显示:3 < 5,10 > 7 -->
<p>版权所有 &copy; 2026</p>    <!-- 显示:版权所有 © 2026 -->
<p>价格:49&euro;</p>           <!-- 显示:价格:49€ -->

数字实体:十进制与十六进制

任何Unicode字符都可以通过其码位来引用,支持十进制和十六进制两种形式:

  • 十进制&# 后跟十进制码位,例如 &#60; 表示 <(U+003C)
  • 十六进制&#x 后跟十六进制码位,例如 &#x3C; 表示 <

两种形式完全等价。技术文档中常用十六进制,因为Unicode码位通常以十六进制(U+003C)表示。

<!-- 以下三种写法等价,均显示 < -->
&lt;
&#60;
&#x3C;

5个关键安全实体

这5个字符构成HTML注入防御的基础,在将用户输入反映到HTML中时必须全部编码:

字符 命名实体 十进制 十六进制 含义
< &lt; &#60; &#x3C; 开始HTML标签
> &gt; &#62; &#x3E; 结束HTML标签
& &amp; &#38; &#x26; 实体前缀
" &quot; &#34; &#x22; 双引号属性
' &apos; &#39; &#x27; 单引号属性

HTML实体参考表

字符 命名实体 十进制 十六进制 用途
< &lt; &#60; &#x3C; 标签分隔符
> &gt; &#62; &#x3E; 标签分隔符
& &amp; &#38; &#x26; 实体前缀
" &quot; &#34; &#x22; 属性值
' &apos; &#39; &#x27; 属性值
&nbsp; &#160; &#xA0; 不换行空格
© &copy; &#169; &#xA9; 版权符号
® &reg; &#174; &#xAE; 注册商标
&trade; &#8482; &#x2122; 商标符号
&euro; &#8364; &#x20AC; 欧元符号
&mdash; &#8212; &#x2014; 破折号
&ndash; &#8211; &#x2013; 短划线

XSS防护:实体编码如何保障网站安全

跨站脚本攻击(XSS)是最常见的Web安全漏洞之一。它发生在攻击者将恶意脚本注入到其他用户可见的内容中时。HTML实体编码是防御XSS的主要手段。

经典XSS攻击场景

考虑一个回显用户搜索词的功能:

<!-- 存在漏洞:直接将用户输入插入HTML -->
<p>您搜索的是:<?php echo $_GET['q']; ?></p>

攻击者构造如下URL:

https://example.com/search?q=<script>document.cookie</script>

浏览器会渲染 <script> 标签并执行攻击者的代码。利用 document.cookie 可以窃取会话令牌;利用 fetch() 可以将数据发送到攻击者控制的服务器。

修复方案:输出时进行编码

<!-- 安全:对所有输出进行编码 -->
<p>您搜索的是:<?php echo htmlspecialchars($_GET['q'], ENT_QUOTES, 'UTF-8'); ?></p>

浏览器现在看到的是:

<p>您搜索的是:&lt;script&gt;document.cookie&lt;/script&gt;</p>

脚本以普通文本形式展示,不会被执行。


各语言实践示例

JavaScript:安全的DOM操作

在JavaScript中插入用户生成内容时,最安全的方式是使用 textContent,它从不解析HTML:

// 安全:textContent从不解析HTML
const el = document.getElementById('output');
el.textContent = userInput; // 自动转义所有内容

// 危险:innerHTML会解析并执行HTML
el.innerHTML = userInput; // 切勿对不可信输入使用

如果必须在JavaScript中拼接HTML字符串,务必先转义:

function escapeHtml(str) {
  return str
    .replace(/&/g, '&amp;')   // 必须最先处理 &
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

const safe = `<p>搜索结果:${escapeHtml(userInput)}</p>`;

注意:必须最先替换 &——若先替换 <,则 &lt; 中的 & 会被再次转义为 &amp;lt;,造成双重编码。

PHP:htmlspecialchars() 与 htmlentities()

PHP提供了两个主要的HTML编码函数:

// htmlspecialchars:仅转义5个关键字符
$safe = htmlspecialchars($input, ENT_QUOTES | ENT_HTML5, 'UTF-8');

// htmlentities:转义所有有命名实体对应的字符
$safe = htmlentities($input, ENT_QUOTES | ENT_HTML5, 'UTF-8');

关键区别htmlspecialchars() 仅转义5个危险字符;htmlentities() 还会转义带重音的字母和符号(如 é&eacute;)。对于UTF-8文档,通常推荐 htmlspecialchars()——UTF-8可以直接表示所有字符,只需转义危险字符即可。务必传入 ENT_QUOTES 以同时转义单双引号,并始终指定 'UTF-8' 字符集。

Python:html.escape()

import html

# 基本转义
safe = html.escape(user_input)

# 同时转义单引号(Python 3.2+中quote默认为True)
safe = html.escape(user_input, quote=True)

# 示例
user_input = '<script>alert("XSS")</script>'
print(html.escape(user_input))
# 输出:&lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;

模板引擎(Jinja2、Django、Handlebars)

大多数现代模板引擎默认开启自动转义:

<!-- Jinja2 / Django:默认自动转义 -->
<p>{{ user_comment }}</p>

<!-- 渲染原始HTML(若内容不可信则危险!)-->
<p>{{ user_comment | safe }}</p>

<!-- Handlebars:双大括号转义,三大括号不转义 -->
<p>{{userComment}}</p>    <!-- 已转义,安全 -->
<p>{{{userComment}}}</p>  <!-- 原始HTML,危险! -->

实际应用场景

1. 技术文档与代码博客

在撰写关于HTML的技术文章时,经常需要展示包含 <>& 的代码示例。实体让这些字符以字面形式显示,而不破坏页面结构:

<pre><code>
使用 &lt;div&gt; 和 &lt;/div&gt; 包裹内容区域。
&amp; 字符用于开始一个HTML实体。
</code></pre>

2. CMS与用户生成内容

任何存储和展示用户生成文本的CMS,在输出到页面之前都必须进行HTML实体编码。这包括博客评论、论坛帖子、产品评价和社交媒体内容。疏忽此步骤是大量真实XSS攻击事件的根源。

3. HTML电子邮件模板

HTML邮件客户端的渲染标准极不统一。对排版字符使用命名实体(&mdash;&lsquo;&rsquo;&hellip;)有助于在Gmail、Outlook、Apple Mail等客户端中实现一致的渲染效果。

4. 排版与特殊符号

实体提供了访问键盘难以输入的排版字符的便捷方式:

<p>破折号&mdash;常用于插入说明&mdash;比连字符更具表达力。</p>
<p>她说&ldquo;你好&rdquo;并微笑了。</p>
<p>价格:29&nbsp;&euro;</p>
<!-- &nbsp; 防止数字和货币符号换行到不同行 -->

5. 遗留系统的国际化支持

在无法可靠处理UTF-8的遗留系统中,数字实体可以表示任何Unicode字符:

<!-- 中文"龙"(U+9F99)的十进制实体 -->
&#40857;

<!-- 日文平假名あ(U+3042) -->
&#12354;

命名实体 vs 数字实体

方面 命名实体(&lt; 十进制(&#60; 十六进制(&#x3C;
可读性
覆盖范围 约2000个字符 全部Unicode 全部Unicode
HTML5支持 完整 完整 完整
XML支持 仅5个预定义实体 完整 完整
适用场景 常用字符 任意Unicode字符 技术文档/Unicode引用

HTML与XML实体的关键差异

XML仅预定义5个实体(&lt;&gt;&amp;&quot;&apos;)。&copy;&nbsp; 等命名实体在XML中未定义,除非在DTD中声明。

<!-- XML中无效(未定义实体):-->
<p>版权所有 &copy; 2026</p>

<!-- XML中有效(数字实体通用):-->
<p>版权所有 &#169; 2026</p>

<!-- HTML5中两种均有效 -->

如果编写XHTML或SVG,对5个基本实体之外的字符请使用数字实体,或直接使用UTF-8字符。


最佳实践

1. 全链路使用UTF-8

在数据库排序规则、HTTP Content-Type 响应头和HTML <meta charset> 标签中统一声明UTF-8,消除对非ASCII字符使用实体的必要性:

<meta charset="UTF-8">
header('Content-Type: text/html; charset=UTF-8');

2. 根据上下文进行适当编码

不同的注入上下文需要不同的转义策略:

  • HTML正文:转义 <>&
  • HTML属性:转义 <>&"'
  • JavaScript字符串:使用 \uXXXX 转义或JSON编码
  • CSS值:适用不同的转义规则
  • URL:使用百分号编码(%3C 而非 &lt;

针对某一上下文的编码在其他上下文中不一定安全。

3. 在输出时编码,而非输入时

在数据库中存储原始数据,在输出到HTML时再进行编码。若在输入时编码,输出时可能产生双重编码,且数据在JSON API、纯文本邮件等非HTML场景中无法正常使用。

4. 永远不要在处理前解码不可信输入

在应用安全过滤器之前解码用户提供的实体会绕过防护。&#60;script&#62; 解码后变为 <script>,可轻松绕过简单的"禁止尖括号"过滤器。

5. 避免双重编码

双重编码(&amp;lt; 显示为 &lt; 而非 <)是多个应用层各自独立编码时的常见错误。将编码逻辑集中在展示层的一处。

6. HTML4中的 &apos; 问题

&apos;(单引号)在XML和XHTML中有定义,但在HTML4中未定义。在HTML4环境中请使用 &#39;。HTML5已正式将 &apos; 纳入命名实体列表。


常见问题解答

问:是否需要对每个特殊字符都进行编码?

出于安全目的,至少必须对5个关键字符(< > & " ')进行编码。对于版权符号、破折号、货币符号等排版字符,在UTF-8文档中直接使用UTF-8字符完全可行,而且通常更简洁。实体在遗留系统或无法保证字符编码的环境中更为重要。

问:&amp;& 有什么区别?

& 是字面与号字符,&amp; 是其HTML实体表示。在HTML源代码中,每当需要显示字面 & 时必须写成 &amp;。若直接在单词前写 &,浏览器可能将其解析为实体开始,导致渲染错误。

问:&nbsp; 为何与普通空格表现不同?

普通空格(U+0020)是"可断行空格",浏览器可以在此处换行,且连续多个普通空格会被折叠为一个。&nbsp;(不换行空格,U+00A0)阻止相邻字符间的换行,且不会被折叠。适用于保持"100 km"、"张 先生"等值在同一行。

问:可以用数字实体表示表情符号吗?

可以。表情符号有Unicode码位,可以用数字实体表示。例如,😀(U+1F600)的十六进制形式是 &#x1F600;,十进制形式是 &#128512;。在UTF-8文档中可以直接粘贴表情符号,数字实体可作为备选方案。

问:href 属性有特殊的XSS风险吗?

是的。href 属性存在特殊风险:URL可以使用 javascript: 协议,仅做HTML编码不足以防御:

<!-- 即使 < 和 > 已编码,仍然危险:-->
<a href="javascript:alert(1)">点击我</a>

<!-- 安全做法:验证协议 -->
<?php
$url = $_GET['url'];
if (!preg_match('/^https?:\/\//i', $url)) { $url = '#'; }
echo '<a href="' . htmlspecialchars($url) . '">链接</a>';
?>

问:现代JavaScript框架会自动处理HTML编码吗?

是的——React、Vue、Angular、Svelte等现代框架默认自动转义输出。React的JSX会自动转义 {} 中的插值。但每个框架都提供了显式的"跳过转义"接口(React的 dangerouslySetInnerHTML、Vue的 v-html),使用时必须极为谨慎,仅用于可信内容。


总结

HTML实体是Web开发中不可或缺的机制,涉及以下四个核心价值:

  1. 安全性——通过转义 <>&"' 来防御XSS攻击
  2. 正确性——确保具有HTML特殊含义的字符以字面形式显示
  3. 兼容性——在遗留或受限环境中表示任意Unicode字符
  4. 排版——插入破折号、不换行空格、货币符号等特殊字符

在现代UTF-8技术栈中,主要需要在将动态内容输出到HTML时对5个安全关键字符进行编码。&nbsp;&mdash; 等命名实体在排版中依然有价值。理解命名实体与数字实体的区别、HTML与XML规则的差异,将使你成为更高效、更具安全意识的Web开发者。