返回

纯JS解码HTML实体:3种实用方法转换特殊字符

javascript

用 JavaScript 把 HTML 字符实体转回普通文本

咱们在搞前端或者处理后台传来的数据时,时不时会碰到一串看着奇怪的字符串,里面包含了像 ><& 这样的东西。这些就是 HTML 字符实体(HTML Character Entities)。有时候,我们需要把它们变回它们原本的样子,比如把 > 变回 >

用 jQuery 的话,一个 .html().text() 可能就搞定了,确实方便。但如果项目没用 jQuery,或者想追求轻量化、零依赖,那纯 JavaScript 该怎么做呢?其实不难,有好几种方法可以实现。

问题是啥?

简单说,就是手里有个包含 HTML 字符实体的字符串,例如:

const encodedString = "这串文字包含了 <b>加粗</b> 标签和 & 符号。";

目标是用纯 JavaScript 把它转换成:

"这串文字包含了 <b>加粗</b> 标签和 & 符号。"

常见的 HTML 实体包括:

  • &lt; -> < (小于号)
  • &gt; -> > (大于号)
  • &amp; -> & (和号)
  • &quot; -> " (双引号)
  • &apos; -> ' (单引号,注意:在 HTML4 中无效,但 XML 和 XHTML 支持)
  • &nbsp; -> (非断行空格)
  • 还有各种数字实体,如 &#60; (小于号) 或 &#x3C; (小于号,十六进制)

我们需要一个函数或者方法,能处理这些转换。

为啥会有这玩意儿?(原因分析)

HTML 用 <> 来定义标签。如果想在页面上直接显示字符 <>,浏览器会误以为它们是标签的一部分,导致页面结构混乱或者解析错误。同样,& 符号在 HTML 实体中也有特殊用途(用来开始一个实体)。

所以,为了能在 HTML 中正确显示这些特殊字符本身,就需要用它们的“替身”——也就是 HTML 字符实体。服务器端语言在输出内容到 HTML 时,通常会自动做这个转义(Encoding)操作,防止 XSS 攻击或者保证内容正确显示。

当我们用 JavaScript 获取到这些内容(比如通过 API、或者从 DOM 的某个属性里读出来),如果需要的是原始字符(比如要在 <textarea> 里编辑、或者进行某些文本处理逻辑),就需要进行反向操作——解码(Decoding)。

咋解决呢?(解决方案)

有几种纯 JavaScript 的方法可以实现 HTML 实体的解码。

方案一:利用 DOM 解析 (老司机技巧)

这是最常用也比较可靠的方法之一。思路很简单:让浏览器替我们干这个脏活累活

浏览器本身就具备解析 HTML 和处理实体的能力。我们可以创建一个临时的 DOM 元素(它不会被渲染到页面上),把包含实体的字符串作为这个元素的 innerHTML 放进去,然后读取这个元素的 textContent 或者 innerText。在这个过程中,浏览器会自动完成解码。

原理和作用:

当你把字符串赋给一个元素的 innerHTML 时,浏览器会按照 HTML 的规则去解析这个字符串。当它遇到 &lt;&gt; 等实体时,会在构建内部 DOM 树的时候把它们转换成对应的原始字符。之后,当你读取 textContent 时,它会返回这个元素及其所有后代元素的纯文本内容,这时实体已经被解码了。

代码示例:

function decodeHtmlEntities(encodedString) {
  // 推荐使用 textarea,因为它不会尝试解析里面的 HTML 标签结构,
  // 对于纯文本解码更直接,也能处理像 &lt;script&gt; 这样的实体。
  const textarea = document.createElement('textarea');
  textarea.innerHTML = encodedString;
  return textarea.value; // 或者用 textarea.textContent 也可以
}

// 例子
const encoded = "登录 &amp; 注册 &lt;button&gt;按钮&lt;/button&gt; Count: &#x31;&#48;"; // 包含了 &, <, >, 和数字实体 10
const decoded = decodeHtmlEntities(encoded);

console.log(encoded); // "登录 &amp; 注册 &lt;button&gt;按钮&lt;/button&gt; Count: &#x31;&#48;"
console.log(decoded); // "登录 & 注册 <button>按钮</button> Count: 10"

// 如果你确定字符串里不包含需要保留的 HTML 结构,用 div 也可以
function decodeHtmlEntitiesWithDiv(encodedString) {
  const div = document.createElement('div');
  div.innerHTML = encodedString;
  return div.textContent || div.innerText; // innerText 兼容性稍差,textContent 更标准
}

const decodedWithDiv = decodeHtmlEntitiesWithDiv(encoded);
console.log(decodedWithDiv); // "登录 & 注册 <button>按钮</button> Count: 10"

为啥用 textarea 可能更好?

textarea 元素的内容模型是 RCDATA,它对待 innerHTML 里的内容相对宽容,不会像 div 那样严格地构建 DOM 树。对于解码含有像 &lt;script&gt; 这样实体的字符串时,textarea 能更忠实地把 &lt;script&gt; 解码成 <script> 文本,而 div 可能会因安全原因或解析行为差异导致结果不同。textarea.value 也能直接获取解码后的文本。

安全建议:

这种方法本身用于解码是安全的。需要注意的是,解码后的字符串不应该不经处理就直接用 innerHTML 插入到页面的其他地方 。因为如果原始编码字符串来源于不可信的输入,解码后可能包含恶意的 HTML 或 JavaScript 代码(比如原始是 &lt;script&gt;alert('XSS')&lt;/script&gt;,解码后变成 <script>alert('XSS')</script>)。如果你需要把解码后的内容显示在页面上,请确保使用 textContent 来设置,或者进行严格的 HTML 清理。

进阶使用技巧:

  • 性能考虑: 创建 DOM 元素是有开销的,虽然现代浏览器优化得很好,但如果在非常频繁的循环中调用,可能需要考虑性能。不过对于绝大多数场景,这点开销可以忽略不计。
  • 复用元素: 如果需要大量解码,可以考虑创建一个 textarea 元素后复用它,而不是每次都 createElement
// 复用解码器
const decoderElement = document.createElement('textarea');
function decodeHtmlEntitiesReusable(encodedString) {
  decoderElement.innerHTML = encodedString;
  return decoderElement.value;
}

console.log(decodeHtmlEntitiesReusable("第一次解码 &amp;")); // 第一次解码 &
console.log(decodeHtmlEntitiesReusable("第二次解码 &lt;")); // 第二次解码 <

方案二:使用 DOMParser API (现代方法)

DOMParser 是一个更现代、专门用来将 XML 或 HTML 源代码解析为 DOM Document 的接口。它也能达到类似的效果,并且在某些情况下被认为更“标准”。

原理和作用:

DOMParserparseFromString() 方法接收一个字符串和 MIME 类型(如 "text/html"),然后返回一个全新的 Document 对象。这个解析过程同样会处理 HTML 实体。之后我们可以从这个 Documentbody (对于 HTML) 或者 documentElement (对于 XML) 中提取 textContent

代码示例:

function decodeHtmlEntitiesUsingDOMParser(encodedString) {
  // 检测是否在浏览器环境,因为 DOMParser 是浏览器 API
  if (typeof window === 'undefined' || typeof window.DOMParser !== 'function') {
    console.warn('DOMParser is not available in this environment.');
    // 可以考虑回退到其他方法,或者直接抛出错误
    // 这里简单返回原字符串或抛错
    // throw new Error('DOMParser not supported');
     return encodedString; // 或者提供一个非 DOM 的备选方案
  }

  const parser = new DOMParser();
  // 使用 'text/html' 类型进行解析
  const doc = parser.parseFromString(encodedString, 'text/html');

  // 对于 'text/html',解码后的文本通常在 body 的 textContent 中
  // 注意:直接 parseFromString 的结果会包含 <html><body>...</body></html> 结构
  return doc.documentElement.textContent;
}

// 例子
const encoded = "带有 &quot;引号&quot; 和 &#169; (版权符号) 的文本。";
const decoded = decodeHtmlEntitiesUsingDOMParser(encoded);

console.log(encoded); // "带有 &quot;引号&quot; 和 &#169; (版权符号) 的文本。"
console.log(decoded); // "带有 "引号" 和 © (版权符号) 的文本。"

安全建议:

同方案一,DOMParser 解析本身是安全的(它在内存中创建文档,不执行脚本)。但解码得到的字符串如果包含潜在的恶意内容,并且之后你打算把它插入到页面 DOM 中,依然需要采取预防措施,比如使用 textContent 而不是 innerHTML

进阶使用技巧:

  • MIME 类型选择: 使用 text/html 通常是最符合直觉的。如果处理的是 XML 片段,或许可以用 application/xmltext/xml,但注意 XML 对实体和解析规则可能更严格。
  • 错误处理: parseFromString 在遇到严重错误时可能会返回一个包含错误信息的文档。对于简单的实体解码场景,通常不用太担心。
  • 环境兼容性: DOMParser 在所有现代浏览器中都支持良好。如果在非常旧的环境(比如 IE9 之前)或者非浏览器环境(如纯 Node.js,除非用了像 jsdom 这样的库)运行,这个 API 可能不可用。

方案三:正则表达式替换 (自己动手,风险自担)

理论上,你也可以用正则表达式来查找并替换 HTML 实体。

原理和作用:

维护一个常见 HTML 实体到其对应字符的映射表(Map 或 Object),然后用正则表达式找出所有的实体(比如 &...; 格式),在替换函数里查找这个实体对应的字符,然后换掉它。

代码示例 (仅用于演示思路,不推荐用于生产):

function decodeHtmlEntitiesRegex(encodedString) {
  // 一个非常不完整的映射表,仅用于演示
  const entityMap = {
    '&lt;': '<',
    '&gt;': '>',
    '&amp;': '&',
    '&quot;': '"',
    '&apos;': "'",
    '&nbsp;': ' ',
    // ... 你需要添加所有你想支持的命名实体
  };

  // 替换命名实体
  let decoded = encodedString.replace(/&[a-zA-Z]+;/g, match => {
    return entityMap[match] || match; // 找不到映射就保持原样
  });

  // 替换十进制数字实体,例如 &#60;
  decoded = decoded.replace(/&#([0-9]+);/g, (match, dec) => {
    return String.fromCharCode(dec);
  });

  // 替换十六进制数字实体,例如 &#x3C;
  decoded = decoded.replace(/&#x([0-9A-Fa-f]+);/g, (match, hex) => {
    return String.fromCharCode(parseInt(hex, 16));
  });

  return decoded;
}

// 例子
const encoded = "处理 &lt;tag&gt; &amp; &#64; 和 &#x40;"; // @ 符号的两种数字实体
const decoded = decodeHtmlEntitiesRegex(encoded);

console.log(encoded); // "处理 &lt;tag&gt; &amp; &#64; 和 &#x40;"
console.log(decoded); // "处理 <tag> & @ 和 @"

这种方法的缺点:

  1. 不完整: HTML 定义了大量的命名实体。手动维护一个完整的映射表非常困难且容易出错。上面示例中的 entityMap 只是冰山一角。
  2. 复杂性: 正则表达式需要精确地匹配所有实体格式(命名、十进制、十六进制),并且处理好边界情况。
  3. 性能: 对于非常长的字符串和复杂的正则表达式,性能可能不如浏览器内置的 DOM 解析。
  4. 维护成本: 标准可能会更新,新的实体可能会被添加。

因此,除非有非常特殊的需求(比如在不支持 DOM 的环境里做简单替换),一般不推荐用正则表达式来做完整的 HTML 实体解码。

选哪个好? (对比和建议)

  • 方案一 (利用 DOM 解析 - textareadiv):
    • 优点: 非常简单,代码量少,利用了浏览器内置的高效、健壮的解析器,能处理所有标准实体。兼容性极好(几乎所有浏览器都支持)。
    • 缺点: 依赖浏览器 DOM 环境。轻微的 DOM 操作开销(通常可忽略)。
  • 方案二 (DOMParser API):
    • 优点: API 设计更清晰,明确用于解析。同样利用浏览器内置解析器,功能强大。
    • 缺点: 依赖浏览器 DOM 环境。相比方案一可能代码稍微多一点点。旧版浏览器可能不支持。
  • 方案三 (正则表达式替换):
    • 优点: 不依赖 DOM,理论上可以在非浏览器环境使用(如果 String.fromCharCode 可用)。
    • 缺点: 实现复杂,难以做到完全准确和覆盖所有实体,性能可能较差,维护困难。强烈不推荐用于通用解码。

总的建议是:优先选择方案一或方案二。 两者都很可靠且易于使用。

  • 如果你追求代码最简洁,或者需要兼容非常古老的浏览器,方案一(特别是用 textarea 的技巧)很流行且有效。
  • 如果你喜欢更现代、语义更明确的 API,方案二 (DOMParser) 是个不错的选择。

在绝大多数 Web 开发场景中,这两种基于 DOM 的方法都能很好地解决问题。