HTML 编码与实体

深入理解 HTML 实体编码、字符编码历史、charset 声明、XSS 和 HTML 编码的关系,以及 Unicode emoji 在 HTML 中的处理。

HTML 编码与实体

一、HTML 实体编码

1.1 什么是 HTML 实体

HTML 实体是表示特殊字符的字符串,以 & 开头,以 ; 结尾:

<!-- 常用实体 -->
&lt;    <!-- < -->
&gt;    <!-- > -->
&amp;    <!-- & -->
&quot;    <!-- " -->
&apos;   <!-- ' -->
&nbsp;   <!-- 不换行空格 -->

1.2 为什么要用 HTML 实体

在 HTML 中,某些字符有特殊含义,不能直接使用:

<!-- ❌ 错误:< 被浏览器解析为标签开始 -->
<p>5 < 10</p>

<!-- ✅ 正确:使用实体 -->
<p>5 &lt; 10</p>

<!-- ❌ 错误:& 可能影响实体解析 -->
<p>Tom & Jerry</p>

<!-- ✅ 正确:& 实体 -->
<p>Tom &amp; Jerry</p>

1.3 数字实体

除了命名实体,还有数字实体:

<!-- 命名实体 -->
&lt;  <!-- < -->

<!-- 十进制数字实体 -->
&#60;  <!-- < -->

<!-- 十六进制数字实体 -->
&#x3C;  <!-- < -->

1.4 完整实体列表

字符 命名实体 十进制 十六进制
< &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;

二、字符编码历史

2.1 ASCII

ASCII(American Standard Code for Information Interchange)是最早的字符编码:

  • 7 位,共 128 个字符
  • 包含英文字母、数字、标点符号和控制字符
  • 无法表示非英文字符

2.2 ISO-8859-1

ISO-8859-1(Latin-1)扩展了 ASCII:

  • 8 位,共 256 个字符
  • 支持西欧语言字符
  • 仍然无法支持中文等其他语言

2.3 GB2312 和 GBK

中文编码:

  • GB2312:简体中文编码,包含约 6700 个汉字
  • GBK:扩展 GB2312,支持更多汉字和繁体字

2.4 UTF-16

Unicode 的早期实现:

  • 2 字节或 4 字节编码
  • 可以表示所有 Unicode 字符
  • 不兼容 ASCII

2.5 UTF-8

现代 Web 的标准:

  • 1-4 字节变长编码
  • 向后兼容 ASCII
  • 可以表示所有 Unicode 字符(包括 emoji)
  • 是 HTML5 的默认编码

三、charset 声明

3.1 HTML5 的 charset 声明

<head>
  <meta charset="UTF-8">
</head>

3.2 历史原因

为什么 charset 必须在 <head> 靠前位置?

早期浏览器在遇到非 ASCII 字符时可能会误判编码。为了让浏览器尽早用正确编码重新解析,HTML 规范要求 charset 必须在前 1024 字节内被解析。

<!-- ❌ 错误:charset 太靠后 -->
<html>
<head>
  <title>页面</title>
  <meta charset="UTF-8">  <!-- 可能不会被解析 -->
</head>

<!-- ✅ 正确:charset 靠前 -->
<html>
<head>
  <meta charset="UTF-8">
  <title>页面</title>
</head>

3.3 HTTP 声明 vs meta 声明

服务器也可以通过 HTTP 头声明编码:

Content-Type: text/html; charset=UTF-8

HTTP 声明优先于 meta 声明。如果服务器设置了正确的 HTTP 头,meta 声明会被忽略。

四、BOM 和 UTF-8

4.1 什么是 BOM

BOM(Byte Order Mark)是文件开头的特殊字节序列,用于表示编码方式:

UTF-8:    EF BB BF
UTF-16 LE: FF FE
UTF-16 BE: FE FF

4.2 UTF-8 BOM

UTF-8 文件开头的 EF BB BF 三个字节就是 BOM。

// 检测 UTF-8 BOM
function hasUTF8BOM(buffer) {
  return buffer[0] === 0xEF &&
         buffer[1] === 0xBB &&
         buffer[2] === 0xBF;
}

// 移除 BOM
function removeBOM(content) {
  if (content.charCodeAt(0) === 0xFEFF) {
    return content.slice(1);
  }
  return content;
}

4.3 BOM 的问题

UTF-8 BOM 有时会导致问题:

  • 在 JSON 中,BOM 开头会导致 JSON.parse 错误
  • 在某些服务器配置中,BOM 会影响输出
  • 纯 ASCII 文件不需要 BOM

建议:UTF-8 文件不要带 BOM。

五、XSS 和 HTML 编码

5.1 XSS 攻击原理

XSS(Cross-Site Scripting)发生在用户输入被当作 HTML 执行时:

<!-- 恶意输入 -->
<script>alert('XSS')</script>

<!-- 如果直接输出 -->
<p><script>alert('XSS')</script></p>

<!-- 浏览器会执行脚本!-->

5.2 HTML 编码防御

对用户输入进行 HTML 编码可以防止 XSS:

function escapeHTML(str) {
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;');
}

// 使用
const userInput = '<script>alert("XSS")</script>';
element.textContent = escapeHTML(userInput);
// 输出: &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;

5.3 不同上下文的编码

不同上下文需要不同的编码方式:

// HTML 上下文编码
function escapeHTML(str) {
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;');
}

// JavaScript 上下文编码(用于事件处理器)
function escapeJS(str) {
  return str
    .replace(/\\/g, '\\\\')
    .replace(/"/g, '\\"')
    .replace(/'/g, "\\'")
    .replace(/</g, '\\x3C')
    .replace(/>/g, '\\x3E');
}

// URL 上下文编码
function escapeURL(str) {
  return encodeURIComponent(str)
    .replace(/'/g, '%27');
}

六、Unicode emoji 在 HTML 中的处理

6.1 emoji 的编码

emoji 是 Unicode 字符,通常使用 UTF-8 编码:

😀 (Grinning Face) = F0 9F 98 80 (4 字节 UTF-8)

6.2 emoji 显示问题

有时 emoji 在某些设备或浏览器上无法正确显示:

<!-- 使用图片代替 emoji -->
<img src="emoji/smile.png" alt="😀">

<!-- 使用 CSS 控制 emoji 显示 -->
<span class="emoji">😀</span>

<style>
.emoji {
  font-family: 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', sans-serif;
}
</style>

6.3 零宽连接符

某些 emoji 组合需要零宽连接符:

<!-- 人物 + 肤色 -->
<span>&#x1F471;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;</span>
<!-- 1F471 = person -->
<!-- 1F3FB = light skin tone -->
<!-- 200D = zero width joiner -->
<!-- 2642 = male sign -->
<!-- FE0F = variation selector -->

延展阅读