⭐ 推荐:社区规则条款 V1.0

HTML 字符实体与特殊符号:容易踩坑的转义字符汇总

ipzh
ipzh 正式会员正式会员认证极客认证极客 钻石卡会员钻石卡会员
发布于 2026-10-12 05:23 ·1 浏览 ·3 回复
内容摘要

HTML 中需重点处理 &、小于号、大于号、双引号和单引号的转义,优先用命名实体、冷门符号用数字实体且分号不可省,转义时应先替换 &,否则易导致实体嵌套、解析错误和编码乱码。

学完这篇你能分清 HTML 里哪些字符必须转义、哪些写法看着对其实会出错,以及  、&、编码乱码这几个高频坑怎么绕开。

先记住一张「必转义字符表」

在 HTML 正文里,只有 5 个字符有硬性转义要求,其余都是可读性选择:

字符实体说明
&&必须,否则会吞掉后面的实体写法
<&lt;必须,否则被当成标签开头
>&gt;建议,单独出现时浏览器多数能容错
"&quot;属性值用双引号包裹时必须
'&#39;属性值用单引号包裹时必须

注意:只有这 5 个。很多人顺手把中文、字母、括号也转成实体,结果是文件变大、源码没法读,而且搜索和替换会变得极其痛苦。没必要。

第一步:搞清三种写法,别混着用

同一个字符有三种合法写法:

&amp;      <!-- 命名实体 -->
&#38;      <!-- 十进制数字实体 -->
&#x26;     <!-- 十六进制数字实体 -->

命名实体好记但数量有限(HTML5 约 2000 个),数字实体万能但需要查码点。日常优先用命名实体,遇到冷门符号再用数字实体。

注意:分号不能省。&amp 这种缺分号的写法在 HTML 里靠浏览器容错,进了 XML、SVG、RSS 或邮件模板就直接报解析错误。写全 &amp;。

第二步:转义顺序——& 永远第一个换

这是最经典的坑。假设你要展示一行代码 &lt;,如果先转 < 再转 &:

原文: <
转 < 后: &lt;
再转 & 后: &amp;lt;   ← 错了,页面上会显示 &lt; 而不是 <

正确顺序永远是:先 & → 再 < > → 最后 " '。

如果你用后端函数(PHP 的 htmlspecialchars、Python 的 html.escape、JS 里自己写的工具函数),确认它内部就是这个顺序,别自己再套一层。

第三步:属性值里的引号,是个概率游戏

<!-- 危险:值里有双引号会直接截断 -->
<input title="他说"你好"">

<!-- 正确 -->
<input title="他说&quot;你好&quot;">

注意:只要属性值里可能出现引号,就统一把引号转义成实体,或者干脆给这个值外面套另一种引号。别赌数据里没有引号——用户输入的昵称、签名、标题里什么都有。

第四步:&nbsp; 不是空格,别拿它排版

&nbsp; 是不换行空格(U+00A0),它的特点是:不折叠、不换行、宽度固定。所以:

  • 拿它做缩进 → 手机上文字被撑出屏幕、出现横向滚动条
  • 拿它做间距 → 复制粘贴出去全是奇怪的空白字符
  • 屏幕阅读器读它 → 会读出「空格空格空格」

正确做法:缩进用 text-indent: 2em,间距用 margin/padding,段间距用 margin-bottom。

真要用 &nbsp; 的场景只有两个:防止某个短词组被拆行(比如「30 kg」「第 3 页」)、以及空单元格占位。

第五步:URL 里的 & 和 HTML 里的 & 要分开看

这一条极容易搞混:

URL 本身:      a.html?x=1&y=2
写进 href 时:  <a href="a.html?x=1&amp;y=2">

URL 语法里 & 就是分隔参数用的,不需要转义;但这段 URL 出现在 HTML 属性里时,就要按 HTML 规则转成 &amp;。浏览器读出属性值后会还原成 & 再请求,所以服务器端收到的还是 &。

注意:如果你用 JS 的 URLSearchParams 或后端函数生成 URL,别忘了放进模板前再过一遍 HTML 转义。反过来的错误也常见——在 URL 编码函数里把 & 转成 &amp;,服务器就收到一个不存在的参数名 amp;y。

第六步:JS 里拼 HTML,别用 innerHTML

// 危险:用户输入直接进 DOM
box.innerHTML = '<div>' + userInput + '</div>';

// 安全:要文本用 textContent
box.textContent = userInput;

确实需要拼 HTML 时,先转义再拼。另外还有个隐蔽坑:内联 <script> 里出现的字符串 </script> 会提前关闭脚本块,必须写成 <\/script>。

第七步:编码不一致,转义写得再对也是乱码

页面上出现「锟斤拷」「é」「&#65533;」这类东西,通常不是实体写错,而是:

  1. 页面没有声明 <meta charset="utf-8">
  2. 文件本身存成了 GBK,却按 UTF-8 输出
  3. 数据库连接不是 utf8mb4,Emoji(4 字节)直接存坏

注意:MySQL 的 utf8 只支持 3 字节,只写 utf8 会丢 Emoji,必须用 utf8mb4。这一条踩过的人最多。

第八步:常用符号速查

排版类:&mdash;(—)、&ndash;(–)、&hellip;(…)、&laquo; &raquo;(« »)、&middot;(·)、&bull;(•)

版权类:&copy;(©)、&reg;(®)、&trade;(™)

数学类:&times;(×)、&divide;(÷)、&plusmn;(±)、&deg;(°)、&ne;(≠)、&le; &ge;(≤ ≥)、&infin;(∞)

箭头类:&rarr;(→)、&larr;(←)、&harr;(↔)

Emoji 这类超出基本平面的字符,用数字实体写:&#x1F600;(😀)。

小结

  • 正文只需转义 & < > " ' 五个字符,其余别多转
  • 转义顺序固定:& 排第一,否则会产生 &amp;lt; 这种双重转义
  • 实体末尾的分号不能省,XML 和邮件模板不给你容错
  • &nbsp; 是排版毒药,布局问题交给 CSS
  • URL 里的 & 在写进 HTML 属性时才转义,两个层面别搞混
  • 乱码优先怀疑编码声明和数据库字符集,而不是转义写法
  • 拼接 HTML 用 innerHTML 前先转义,能不用就不用
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-799.html
转载请注明出处,版权归原作者所有。

全部回复 3

shandian
shandian 见习用户见习用户 1楼 2026-10-12 05:29

**这篇骨架是对的,5 个必转义字符点得也准;但「第五步:URL 的 & 和 HTML 的 &」恰恰是翻车率最高的一条,我顺着补几句。**

① 两个上下文别叠加,顺序错了就是双重转义。 URL 参数分隔符 & 出现在 HTML 属性里时必须写 &amp;(href="list.php?tid=1&amp;page=2");但这段 URL 如果只是写在 JS 字符串里再 location.href = url,就绝不能转,否则服务端收到的参数名会变成 amp;page。正确工序是:先按 URL 规则编码参数值(urlencode / encodeURIComponent),最后落到 HTML 里再整体做一次 HTML 转义。反了就是 &amp;amp;。

② 库里存原文,只在输出口转一次。 线上那种「越修越多 &amp;amp;amp;」的根因基本都是入库转一次、出库又转一次。原则很简单:数据库永远存原文,只在最终输出到 HTML 的那一层转义;JS 侧能 textContent 就别用 innerHTML。

③ 单引号建议统一用 &#39;。 &apos; 是 HTML5 才补进命名实体表的,HTML4/XHTML 1.0 不认。要经过 XML、SVG、RSS、邮件模板的场景,&#39; 更省心。

**④ &nbsp; 还有个隐蔽问题。

zero
zero 见习用户见习用户 #693 2楼 2026-10-12 05:35
shandian:**这篇骨架是对的,5 个必转义字符点得也准;但「第五步:URL 的 & 和 HTML 的 &」恰恰是翻车率最高的一条,我顺着补几句。** **① 两个上下文…

补得准,①的顺序和②的「只在出口转一次」基本就是治双重转义的药方,我顺着把①的边界和④被截断的后半截接上。

① 再钉一个容易反的细节:urlencode 的粒度是参数值,不是整条 query string。tid=1&page=2 里那个 & 是结构符,编码成 %26 之后服务端收到的是「参数 tid,值 1&page=2」。正确工序是:每个 value 先 urlencode → 拼成完整 URL → 落到 HTML 属性时整体 HTML 转义一次。反过来整条 encode,点开就是参数错乱或 404。

③ 同意,过 XML/SVG/RSS/邮件模板确实 &#39; 更省心。顺手补个 PHP 侧的坑:8.1 之前 htmlspecialchars 默认是 ENT_COMPAT,单引号不转,很多人「引号明明转了还是被截断」就栽在这。建议显式写 htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'),别吃默认值。

④ 我猜你截断的地方是「它不算常规空白」:PHP 的 trim() 和 PCRE 的 \s 默认都不认 U+00A0。用户输入里夹一个 NBSP,前台看和空格一模一样,查重、用户名唯一索引、搜索匹配却全对不上,还常被拿来伪造近似用户名。稳妥做法是显式 str_replace("\xC2\xA0", ' ', $s) 再走常规 trim;JS 侧 trim/正则相对宽容,但别指望后端也一样。

zjlxcf
zjlxcf 正式会员正式会员认证极客认证极客 #694 3楼 2026-10-12 05:42
zero:补得准,①的顺序和②的「只在出口转一次」基本就是治双重转义的药方,我顺着把①的边界和④被截断的后半截接上。 ① 再钉一个容易反的细节:urlencode 的粒…

**三点都收,尤其 ④ 那条——「不可见字符的等价类」比双重转义更难查,因为页面上完全看不出来。**

把 NBSP 的排查口径钉死一点:PHP 的 trim() 白名单是写死的 " \t\n\r\0\x0B",PCRE 的 \s 不带 (*UCP) 或 /u 时也走 ASCII,两层都不认 U+00A0;MySQL 这边 utf8mb4 各排序规则里 NBSP 与空格也不等价,所以「前台看一模一样、唯一索引却不拦」是能稳定复现的,不是玄学。建议收敛到一个 normalize 入口:先把 \xC2\xA0、全角空格 U+3000、零宽空格 U+200B、BOM \xEF\xBB\xBF 统一换成半角空格,再走常规 trim——这四个是同一类坑,一起收掉省事。

htmlspecialchars 再补一刀:第四个参数 $double_encode = false 千万别拿来「治」双重转义。它只是把已编码的实体原样放过,症状消失但根因还在,库里那些老 &amp;amp; 照样越攒越多,等于把 bug 藏起来。就老实出口转一次,参数写 ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5,最后这个顺带把 &apos; 也开出来,跟你前面说的 &#39; 口径统一。

① 的粒度问题,JS 侧能再省一步:别手拼 query,用 new URLSearchParams({tid:1, page:2}).toString() 或往 URL 对象上 searchParams.set(),它天然只对 value 编码、不动结构符,正好对上你说的「先编码值 → 拼完整 URL → 整体 HTML 转义」这个工序。

最后给个自检姿势:从线上随机抽一批富文本字段,一条查是否含 &amp;amp;,一条用 HEX() 或正则查是否含 NBSP 字节。前者命中说明出口转重了,后者命中说明入库前没 normalize。两个查询基本能把这类诡异 bug 定位到具体哪一层。