HTML 实体编码器 / 解码器

HTML 特殊字符编码和解码

Common HTML Entities

& = &
< = &lt;
> = &gt;
" = &quot;
' = &#39;
/ = &#47;
` = &#96;
= = &#61;

上下文决定一切:哪些字符该在哪里转义

转义不是字符串本身的属性,而是字符串落点位置的属性。HTML 有五个结构字符 — & < > " ' — 但每个渲染上下文关心的子集不同。在元素内容(标签之间)里,只有 & 和 < 能改变解析:< 打开标签,& 开始 character reference。在双引号属性值里,解析器只在 " 处结束取值,所以必须转义 & 和 ";单引号属性则需要 & 和 '。真正危险的是不加引号的(unquoted)属性值:它会被空格、Tab、换行、form feed 终止,还与 / 和 = 产生糟糕的交互,因此像 'x onmouseover=alert(1)' 这样看似无害的 payload 一个结构字符都不用就能逃逸。这就是所有安全指南都强调"属性永远加引号"的原因 — 需要转义的字符缩减为两个。 实体编码也有硬性边界。经过实体编码的字符串放进 <script> 块里仍然完全危险:HTML 解析器不会解码 script 数据里的 character reference,但字符串可能提前终结代码块或被当作 JS 解释。放在 href 里的 javascript: URL 能完整躲过实体编码 — 浏览器在解析 URL 之前会先解码实体。内联事件处理器在 JS 解析之上又叠加了一层 HTML 解码,两层转义缺一不可。OWASP XSS Prevention Cheat Sheet 精确列举了这些上下文;规则是按输出目的地选择编码器,而不是按数据本身。
<!-- element content: escape & and < -->
<p>Tickets &lt; $20 &amp; free entry</p>

<!-- double-quoted attribute: escape & and " -->
<input value="say &quot;hi&quot; &amp; wave">

<!-- UNQUOTED attribute: breakout with none of the five chars -->
<input value=x onmouseover=alert(1)>

<!-- entity encoding does NOT help here -->
<a href="javascript:alert(1)">  <!-- decoded before URL resolution -->

命名实体与数字实体 — 以及 XML 陷阱

WHATWG 的 HTML Living Standard 精确定义了 2,231 个 named character reference — 从 &nbsp; &amp; &mdash; 这样的主力到 &CounterClockwiseContourIntegral; 这样的冷僻符号。任何 Unicode code point 也都可以用数字形式书写:十进制 &#8594; 和十六进制 &#x2192; 都生成右箭头,而十六进制形式与 Unicode 码表的 U+2192 记法直接对应,更容易核对。 陷阱在于这 2,231 个名字是 HTML 独有的词汇表。XML 只预定义五个:&amp; &lt; &gt; &quot; &apos;。把 &nbsp; 交给 XML 解析器 — 以 application/xhtml+xml 提供的 XHTML 页面、RSS/Atom feed、当作独立 XML 处理的 SVG 文件 — 得到的不是警告而是致命解析错误。可移植的写法是 &#160;。反过来,&apos; 在 XML 和 HTML5 中有定义,但从来不属于 HTML 4,所以老旧解析器和一些邮件渲染器会原样显示它;到处都能用的撇号是 &#39;,这也是 PHP 的 htmlspecialchars 之类序列化器输出 &#39; 而非 &apos; 的原因。 手动解码时记住 named reference 区分大小写:&Auml; 是 Ä,&auml; 是 ä;&AMP; 存在但 &AMp; 不存在。先把输入转小写再查表的解码器会悄悄损坏文本。
Rightwards arrow (U+2192):
  named:   &rarr;      (HTML only)
  decimal: &#8594;
  hex:     &#x2192;    (matches U+2192)

Safe everywhere (HTML + XML/RSS/SVG):
  &amp; &lt; &gt; &quot; &#39; &#160;

Fatal parse error in XML: &nbsp; &mdash; &copy; ...

缺失的分号、URL 里的 &amp; 与双重编码

HTML 解析器至今仍认得一份不带分号的旧式 named reference 列表 — &amp、&lt、&copy 等 — 因为 HTML5 之前的内容依赖它。这种宽容造就了一个经典 URL bug:在 ?page=1&copy=2 中,&copy 序列在历史规则下会被解码成 ©,悄悄破坏查询字符串。现代规范用属性值例外(旧式 reference 后面跟 = 或字母数字时保持原样)修补了它,但教训仍在:href 和 src 里的每个裸 & 都应写成 &amp;。校验器至今仍标记 URL 中的裸 ampersand,原因正在于此;浏览器发请求前会把 &amp; 还原成 &,所以这样写没有任何代价。 双重编码是反方向的失败。把 &lt; 再编码一次得到 &amp;lt;,页面上渲染出来的不是 < 而是字面文本 &lt;。当你在页面里看到 &amp;quot; 或 &amp;amp;,说明有两层各自"好心地"转义了一次:通常是模板引擎对已经手工编码过的输出又做了自动转义,或者 HTML 在写入数据库前被实体编码、渲染时又编码了一遍。防住整类问题的黄金法则:存储原始文本,在输出时刻、用匹配目标上下文的编码器、恰好编码一次。像本工具这样的解码器是诊断字符串被套了几层的最快办法 — 反复解码直到输出不再变化,数一数次数即可。
?a=1&copy=2       legacy parsing: "a=1©=2"  (broken)
?a=1&amp;copy=2   safe: browser requests "a=1&copy=2"

"<b>"  encoded once   →  "&lt;b&gt;"          (correct)
       encoded twice  →  "&amp;lt;b&amp;gt;"  (renders "&lt;b&gt;")

有了 UTF-8,你需要的实体远比想象中少

如果页面声明了 <meta charset="utf-8"> — 2026 年理应如此,超过 98% 的网页以 UTF-8 提供 — 那么 em dash、弯引号、箭头、CJK、emoji 都不需要实体。直接输入 — 而不是 &mdash;,源码更小(1–3 字节对 7 字节),更重要的是可读:满屏 &#x2192; 的 diff 没法审。仍然需要实体的字符分两类。第一类是那五个结构字符,永远需要。第二类是在编辑器里不可见或无法分辨、留成字面字符很危险的:no-break space(&nbsp; — 字面的 U+00A0 和普通空格看起来一模一样,下一个人的编辑器会把它"修正"掉)、软连字符 &shy;、用于 emoji 序列和波斯语/印度系文字塑形的 zero-width joiner &zwj; 与 zero-width non-joiner &zwnj;,以及 bidi 控制符。把这些写成实体本身就是自我注释。 一个提醒:如果你输入 — 的地方渲染出 — 这样的乱码,说明文件用错误的编码保存或传输了。实体只会掩盖这个 bug,修正 charset 才是真正的解法 — 用 &mdash; 糊住的 mojibake,会在有人粘贴 non-ASCII 内容的那一刻卷土重来。

textContent、innerHTML,以及何时该用净化器

在 DOM 里,最安全的编码器是根本不用调用的那个:element.textContent = userInput 把字符串当纯文本插入 — 不解析、不需要编码,任何嵌套深度都安全。element.innerHTML = userInput 则是教科书式的 XSS sink:字符串会完整走一遍 HTML 解析器,里面的任何标记都会被执行。(setAttribute 对值同样安全,但属性本身很关键 — setAttribute('href', 'javascript:...') 仍是一个活的 URL。) 模板框架把这条教训内建了:Svelte 的 {expression}、React 的 JSX、Vue 的双花括号插值全部默认转义,所以把数据先手工编码再交给组件不是加固,而是双重编码 bug。每个框架也都留了显式逃生口 — Svelte 的 {@html ...}、React 的 dangerouslySetInnerHTML、Vue 的 v-html — 这些刻意别扭的命名标记着责任转移给了你。 由此引出最后一个区分:编码和净化(sanitize)解决的是不同问题。编码把标记变成可见文本 — 适用于用户输入了 HTML 且应该原样看到的场合。净化(DOMPurify,或正在落地的原生 Sanitizer API)则解析标记,保留安全标签的 allowlist 并剔除脚本向量 — 适用于用户正当撰写富文本且必须按标记渲染的场合。给评论字段编码、给 WYSIWYG 正文净化,两者都对;调换过来要么显示错乱,要么开出漏洞。
// safe: never parsed as markup
el.textContent = userInput;

// XSS sink: parsed and executed
el.innerHTML = userInput;

// frameworks escape by default; escape hatches opt out
// Svelte:  {comment.text}   vs  {@html post.trustedHtml}
// React:   {comment.text}   vs  dangerouslySetInnerHTML
// Vue:     interpolation    vs  v-html

// rich text: sanitize, don't encode
import DOMPurify from 'dompurify';
el.innerHTML = DOMPurify.sanitize(userHtml);
最后更新:

关于此工具

HTML 实体编码器把 <、>、&、" 等在 HTML 中有特殊含义的字符转换为 &lt;、&gt;、&amp;、&quot; 等安全实体。所有显示在页面上的用户输入都必须经过实体编码,这是防止 XSS 的基础。

使用方法

  1. Pick Encode to convert raw characters into HTML entities, or Decode to do the reverse.
  2. Paste the text or HTML snippet into the input box.
  3. Read the encoded or decoded output on the right.
  4. Click Copy to grab the result for use in your template, email, or doc.
  5. Use the entity reference card at the bottom as a quick lookup for common characters.

常见用例

  • Sanitising user-generated content before injecting it into a server-rendered page.
  • Embedding code snippets that contain < and > inside a Markdown or HTML document.
  • Preparing email HTML so apostrophes and ampersands render correctly across clients.
  • Decoding scraped HTML where entities like &amp;quot; were double-encoded.
  • Verifying that an output escapes correctly before flagging an XSS bug.
  • Showing literal HTML markup in a tutorial or technical doc.

常见问题

Q. Do I still need to encode if I use a templating engine?

A. Most modern engines (Svelte, React, Jinja, ERB) auto-escape by default. But once you reach for a "raw" or "unsafe" helper you take responsibility for encoding yourself.

Q. What is the difference between &amp;#39; and &amp;apos;?

A. Both encode an apostrophe. &apos; is technically XHTML/XML; some legacy HTML parsers do not recognise it, so &#39; is the safer choice for HTML output.

Q. Does encoding fix all XSS issues?

A. No. Context matters — JavaScript strings, URL attributes, and CSS each need their own escaping. Use a battle-tested sanitizer for HTML you want to render with markup intact.

Q. Why does decoding a non-encoded string return it unchanged?

A. There is nothing to decode. Decoding only swaps recognised entities back to characters; raw text passes through untouched.