杂记 › 技术笔记 › 引号和尖括号让页面报错?HTML实体编解码的坑

引号和尖括号让页面报错?HTML实体编解码的坑

去年我帮一个做技术博客的朋友迁移旧站评论。旧库里的正文已经存成了转义过的形态,我按"纯文本"处理,导出脚本又整段编了一次码。结果新站上一堆评论显示的不是文字,而是一串字面符号:<p>、&,读者以为排版坏了。修这件事花的时间远超预期,因为要一条一条判断到底被编过几次、该解一次还是两次。从那以后我对转义只信一个原则:先确定它发生在哪一层,再决定要不要动手。

尖括号是结构,不是内容

HTML 源码里,小于号标志着"这里开始一个标签",大于号标志着结束,和号标志着"这里开始一个实体引用"。所以当你希望这三个字符以文字的身份出现在页面上,就必须换一种写法告诉解析器别把它当结构。这就是编码的全部理由——不是"更安全"这种笼统说法,而是"这个位置上的字符会被当成语法"。

反过来,在某个位置写了实体,它会被先还原成字符再参与解析,属性值里就有这种微妙情况。我的判断固定成两句:这段内容是要显示给人看,还是要被浏览器执行?前者尖括号引号要编码,后者不能。

常见实体对照

下面这张表是我贴在显示器边上的那份。中间一列是给浏览器看的写法,右边一列是页面最终显示的字符。最后一列的码点是十进制的字符编号,数字实体就照它写,记不住名字时用码点也能拼出来。

原字符浏览器里写的实体(字面文本)页面显示成码点(十进制)
<&lt; 或 &#60;<60
>&gt; 或 &#62;>62
&&amp; 或 &#38;&38
"&quot; 或 &#34;"34
'&#39;'39

这张表里最容易搞错的是和号本身。它是"开启一个实体"的那个字符,所以想让页面显示一个和号,源码里那位必须写成 &amp;;写成裸和号再加字母,浏览器会把后面那段当成实体名吞进去,页面出现莫名的字符丢失。

双重转义是怎么攒出来的

迁移那次的现场就是这个。编码是一次函数调用,多做一次就多穿一层衣服,而显示只会脱一层。我按层数列一遍,就能倒推任何一条评论被编了几次。

被编码的次数源码里存的字面文本页面显示出来的
0 次(本来该是这样)&和号被当成实体开头,可能解析异常
1 次&amp;&
2 次&amp;amp;&amp;
3 次&amp;amp;amp;&amp;amp;

规律就是每多编一次,字面文本前面就多出一段 amp;。所以看到页面上有 &amp;,说明源码里的和号数量比该有的多一层;看到 &lt;p&gt; 这种字面标签文本,说明标签被编了两次——原本 <p> 编一次得到 &lt;p&gt;,这一串再编一次,那个和号又变成 &amp;,于是屏幕上出现 &lt;p&gt; 的字面形态。

修的时候我不做"整库统一解一次码",而是按条判断被编了几次,只解掉多出来的层数,最后在输出时统一编一次。存储层放原始字符、输出层负责转义,这是不复发的唯一办法;库里存转义后的文本,等于把转义的责任交给写入时机,早晚重演。

什么时候编码,什么时候解码

我的判断表很短。要作为文字显示的代码片段、用户输入进 HTML 之前、拼接到模板里的第三方内容——编码。从接口或 RSS 里拿到一串带 &amp;、&#39; 的文本,要落库或者做字数统计——解码。要存进数据库给别的应用读——存解码后的原始字符。要在页面上显示——显示的那一刻编一次。

属性上下文是另一个容易翻车的地方。比如给一个 input 写 title 属性,值里出现了裸的双引号,形如 title="他说"你好"",第一个内部引号就把属性提前闭合了,后面的内容被解析成一堆乱七八糟的属性。这种位置把 " 写成 &quot; 才是对的,也是为什么我用 HTML 实体编解码 时会看它对引号的处理选项,尖括号和引号一起处理才敢直接塞进属性。

还有一类是需要在 JavaScript 里放一段 HTML。字符串里单引号双引号换行混在一起,手写转义很易出错。工具提供把 HTML 转成 JS 字符串的用法(拼接形式或模板字符串形式),我用它生成内嵌提示框的片段,比自己数反斜杠可靠。

别把中文也编掉

编码选项里通常有两种口径:只处理特殊字符,或者把所有非 ASCII 字符一并转成实体。后者会把中文变成一串 &#xxxxx;,显示一样,但源码体积明显涨、可读性归零,diff 完全没法看。除非输出链路真的只吃 ASCII,我只编特殊字符。要看码点、要处理全角半角,那是另一件事,文本编码转换 做的是字符编号和宽度形态的转换,跟"要不要被浏览器当语法解析"无关。

同样容易混的还有 URL 编解码。地址里 & 是参数分隔符,要在参数值里放一个真正的和号,用的是百分号写法 %26,不是 &amp;;把 HTML 实体塞进查询串,服务器收到的就是字面的 &amp; 那串字符。反过来 %3C 出现在 HTML 正文里,浏览器只会原样显示 %3C,不会当成标签。这两套规则各管各的层,混用是我见过最常见的第二个坑。

问得比较多的几件事

Q:页面上出现了 &amp;,我该解几次码?

按显示的东西倒推:显示 &amp; 说明源码里是两次编码的结果,解一次得到 & 形态的文本、再解一次才是原始的字符。我不靠猜,用一次解码看结果——解一次后显示 & 就是还剩一层,解一次后变成和号本身就到位了。

Q:为什么我写了 &nbsp 少个分号也照样有效?

某些历史遗留的实体名在特定上下文里能被解析,但这不是可以依赖的行为。分号是实体结束的标志,我一律写全,尤其在和号后面紧跟字母的文本里,省掉分号会让解析器把后面的字母也吞进去。

Q:把用户输入编码一次就够了吗?

取决于它最终落在哪个位置。落到 HTML 文本节点,编一次;落到属性值,除了尖括号还要处理引号;如果这段内容还要拼进 URL 或 JS,那么得按那一层各自的规则再做一次。只在一处编码、其他层指望它通用,就会出问题。

Q:编辑器里看到一堆实体,是文档坏了吗?

不一定。很多富文本编辑器和 CMS 存的就是转义形态,这是正常的存储方式。真正的问题是"读它的应用以为它是纯文本",那就会二次编码。我一般会先确认存储约定,再决定要不要解码。

Q:解码会不会把不该还原的东西还原了?

会。如果一段文本本意就是要把实体写法本身显示出来,比如页面上要出现 &lt; 这串字面文本,解码后它变成 <,再被塞进 HTML 就可能被当标签。所以解码我只对自己确认"存的是转义形态"的内容做,来源不明的一律先当文本处理。

回到那批评论。最后我按"库里存原始字符、渲染时编一次"重写了导出,把双重编码的条目按层数解回来,页面上再没出现字面实体。判断标准也简单了:看到多出来的符号先数层数,别急着再解一次;每次编码之前先问那段文本最后落在哪一层——是给人读的文本,还是会被解析成语法的位置。这个问题答对了,&amp;amp; 就没有出现的理由。

📎 相关文章

排版稿没内容可填?占位文本怎么写才不像模板 2026-10-15 两列数据要列出所有组合?分列合并与笛卡尔积 2026-10-15 测试上传限制要多大文件?按需造一个指定大小的 2026-10-15 几十MB的文本发不出去也打不开?按行数切小 2026-10-15 只要文件名不要路径?几百行路径批量提取 2026-10-15