什么是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实体仍不可或缺,原因有三:
- 保留字符:
<、>、&在HTML标记中具有特殊含义,必须转义才能字面显示。 - 属性分隔符:
"和'用于界定属性值,在属性值中出现时必须转义。 - 空白控制:
(不换行空格)提供普通空格无法实现的版式控制。
核心概念:HTML实体的工作原理
命名实体
命名实体是最易读的形式,使用基于字符描述的助记名称。HTML5定义了2000多个命名实体。
<!-- 命名实体的使用示例 -->
<p>面包 & 黄油</p> <!-- 显示:面包 & 黄油 -->
<p>3 < 5,10 > 7</p> <!-- 显示:3 < 5,10 > 7 -->
<p>版权所有 © 2026</p> <!-- 显示:版权所有 © 2026 -->
<p>价格:49€</p> <!-- 显示:价格:49€ -->
数字实体:十进制与十六进制
任何Unicode字符都可以通过其码位来引用,支持十进制和十六进制两种形式:
- 十进制:
&#后跟十进制码位,例如<表示<(U+003C) - 十六进制:
&#x后跟十六进制码位,例如<表示<
两种形式完全等价。技术文档中常用十六进制,因为Unicode码位通常以十六进制(U+003C)表示。
<!-- 以下三种写法等价,均显示 < -->
<
<
<
5个关键安全实体
这5个字符构成HTML注入防御的基础,在将用户输入反映到HTML中时必须全部编码:
| 字符 | 命名实体 | 十进制 | 十六进制 | 含义 |
|---|---|---|---|---|
< |
< |
< |
< |
开始HTML标签 |
> |
> |
> |
> |
结束HTML标签 |
& |
& |
& |
& |
实体前缀 |
" |
" |
" |
" |
双引号属性 |
' |
' |
' |
' |
单引号属性 |
HTML实体参考表
| 字符 | 命名实体 | 十进制 | 十六进制 | 用途 |
|---|---|---|---|---|
< |
< |
< |
< |
标签分隔符 |
> |
> |
> |
> |
标签分隔符 |
& |
& |
& |
& |
实体前缀 |
" |
" |
" |
" |
属性值 |
' |
' |
' |
' |
属性值 |
|
|
  |
  |
不换行空格 |
© |
© |
© |
© |
版权符号 |
® |
® |
® |
® |
注册商标 |
™ |
™ |
™ |
™ |
商标符号 |
€ |
€ |
€ |
€ |
欧元符号 |
— |
— |
— |
— |
破折号 |
– |
– |
– |
– |
短划线 |
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>您搜索的是:<script>document.cookie</script></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, '&') // 必须最先处理 &
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
const safe = `<p>搜索结果:${escapeHtml(userInput)}</p>`;
注意:必须最先替换 &——若先替换 <,则 < 中的 & 会被再次转义为 &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() 还会转义带重音的字母和符号(如 é → é)。对于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))
# 输出:<script>alert("XSS")</script>
模板引擎(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>
使用 <div> 和 </div> 包裹内容区域。
& 字符用于开始一个HTML实体。
</code></pre>
2. CMS与用户生成内容
任何存储和展示用户生成文本的CMS,在输出到页面之前都必须进行HTML实体编码。这包括博客评论、论坛帖子、产品评价和社交媒体内容。疏忽此步骤是大量真实XSS攻击事件的根源。
3. HTML电子邮件模板
HTML邮件客户端的渲染标准极不统一。对排版字符使用命名实体(—、‘、’、…)有助于在Gmail、Outlook、Apple Mail等客户端中实现一致的渲染效果。
4. 排版与特殊符号
实体提供了访问键盘难以输入的排版字符的便捷方式:
<p>破折号—常用于插入说明—比连字符更具表达力。</p>
<p>她说“你好”并微笑了。</p>
<p>价格:29 €</p>
<!-- 防止数字和货币符号换行到不同行 -->
5. 遗留系统的国际化支持
在无法可靠处理UTF-8的遗留系统中,数字实体可以表示任何Unicode字符:
<!-- 中文"龙"(U+9F99)的十进制实体 -->
龙
<!-- 日文平假名あ(U+3042) -->
あ
命名实体 vs 数字实体
| 方面 | 命名实体(<) |
十进制(<) |
十六进制(<) |
|---|---|---|---|
| 可读性 | 高 | 中 | 低 |
| 覆盖范围 | 约2000个字符 | 全部Unicode | 全部Unicode |
| HTML5支持 | 完整 | 完整 | 完整 |
| XML支持 | 仅5个预定义实体 | 完整 | 完整 |
| 适用场景 | 常用字符 | 任意Unicode字符 | 技术文档/Unicode引用 |
HTML与XML实体的关键差异
XML仅预定义5个实体(<、>、&、"、')。©、 等命名实体在XML中未定义,除非在DTD中声明。
<!-- XML中无效(未定义实体):-->
<p>版权所有 © 2026</p>
<!-- XML中有效(数字实体通用):-->
<p>版权所有 © 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而非<)
针对某一上下文的编码在其他上下文中不一定安全。
3. 在输出时编码,而非输入时
在数据库中存储原始数据,在输出到HTML时再进行编码。若在输入时编码,输出时可能产生双重编码,且数据在JSON API、纯文本邮件等非HTML场景中无法正常使用。
4. 永远不要在处理前解码不可信输入
在应用安全过滤器之前解码用户提供的实体会绕过防护。<script> 解码后变为 <script>,可轻松绕过简单的"禁止尖括号"过滤器。
5. 避免双重编码
双重编码(&lt; 显示为 < 而非 <)是多个应用层各自独立编码时的常见错误。将编码逻辑集中在展示层的一处。
6. HTML4中的 ' 问题
'(单引号)在XML和XHTML中有定义,但在HTML4中未定义。在HTML4环境中请使用 '。HTML5已正式将 ' 纳入命名实体列表。
常见问题解答
问:是否需要对每个特殊字符都进行编码?
出于安全目的,至少必须对5个关键字符(< > & " ')进行编码。对于版权符号、破折号、货币符号等排版字符,在UTF-8文档中直接使用UTF-8字符完全可行,而且通常更简洁。实体在遗留系统或无法保证字符编码的环境中更为重要。
问:& 和 & 有什么区别?
& 是字面与号字符,& 是其HTML实体表示。在HTML源代码中,每当需要显示字面 & 时必须写成 &。若直接在单词前写 &,浏览器可能将其解析为实体开始,导致渲染错误。
问: 为何与普通空格表现不同?
普通空格(U+0020)是"可断行空格",浏览器可以在此处换行,且连续多个普通空格会被折叠为一个。 (不换行空格,U+00A0)阻止相邻字符间的换行,且不会被折叠。适用于保持"100 km"、"张 先生"等值在同一行。
问:可以用数字实体表示表情符号吗?
可以。表情符号有Unicode码位,可以用数字实体表示。例如,😀(U+1F600)的十六进制形式是 😀,十进制形式是 😀。在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开发中不可或缺的机制,涉及以下四个核心价值:
- 安全性——通过转义
<、>、&、"、'来防御XSS攻击 - 正确性——确保具有HTML特殊含义的字符以字面形式显示
- 兼容性——在遗留或受限环境中表示任意Unicode字符
- 排版——插入破折号、不换行空格、货币符号等特殊字符
在现代UTF-8技术栈中,主要需要在将动态内容输出到HTML时对5个安全关键字符进行编码。 和 — 等命名实体在排版中依然有价值。理解命名实体与数字实体的区别、HTML与XML规则的差异,将使你成为更高效、更具安全意识的Web开发者。