编码与转义三件套:URL 编码、Base64、HTML 实体的实现与常见坑

简介: 本文详解URL编码、Base64与HTML实体三大字符处理机制的区别与适用场景:URL编码用于查询参数(如`C++→C%2B%2B`),Base64用于字节序列化(需先UTF-8再编码),HTML实体用于安全显示标签(如`<div>→<div>`)。强调“数据交给谁解析”是选择方案的关键,避免误用与重复编码。

接口里的 C++ 到了服务端变成 C 加两个空格,中文调用 btoa() 直接报错,页面上的 <div> 转义后又显示成了 &lt;div&gt;。

这些问题看起来都和“特殊字符”有关,处理方式却不一样。URL 编码负责 URL 中的数据表达,Base64 把字节转换成文本,HTML 实体则用于 HTML 中的字符表示。用错地方,多调用一次编码函数也解决不了问题。

下面用 JavaScript 拆开看,并把三个在线工具放在对应示例中,方便对照输入和输出。

一、先判断数据要交给谁

写代码前,先看下一步由什么解析器处理这段内容。

使用场景 应采用的方式 示例
把搜索词放进查询参数 URL 参数序列化 C++ → C%2B%2B
在文本字段中传输字节数据 Base64 UTF-8 文本 你好 → 5L2g5aW9
在 HTML 源码中显示标签文本 HTML 实体转义 <div> → &lt;div&gt;

这三种处理都不提供保密性。尤其是 Base64,字符串变得不直观,并不代表内容被加密了。

二、URL 编码:参数值里的 & 不能再当分隔符

假设搜索框输入:

C++ & 中文

直接拼接查询字符串:

const keyword = "C++ & 中文";
const query = "q=" + keyword;

问题出在字符含义上:& 会参与参数分隔,+ 在表单风格的查询参数解析中会被当成空格。

构造查询参数时,可以直接使用 URLSearchParams:

const keyword = "C++ & 中文";
const params = new URLSearchParams({
    q: keyword });

console.log(params.toString());
// q=C%2B%2B+%26+%E4%B8%AD%E6%96%87

console.log(params.get("q"));
// C++ & 中文

如果只需要编码一个组件,也可以使用 encodeURIComponent():

console.log(encodeURIComponent("C++ & 中文"));
// C%2B%2B%20%26%20%E4%B8%AD%E6%96%87

两段输出里的空格分别变成了 + 和 %20。这是编码规则的差异:URLSearchParams 使用表单风格的序列化规则。解析查询字符串时,它也会把 + 还原为空格。具体行为可查阅 MDN 的 URLSearchParams 文档。

加号为什么会丢失

下面两种写法接收的数据含义不同:

// 传入已经拼好的查询字符串,+ 会按空格解析
const parsed = new URLSearchParams("q=C++");

console.log(JSON.stringify(parsed.get("q")));
// "C  "

// 传入原始参数值,由 API 完成编码
const built = new URLSearchParams({
    q: "C++" });

console.log(built.toString());
// q=C%2B%2B

因此,已有原始值时,直接调用 set()、append(),或者使用对象构造参数即可。

重复编码会把百分号也编码掉

const once = encodeURIComponent("a b");
const twice = encodeURIComponent(once);

console.log(once);  // a%20b
console.log(twice); // a%2520b

%25 是百分号的编码结果。如果服务端只解码一次,得到的就是 a%20b,而不是 a b。

使用 URLSearchParams 时也一样,不要先手动编码,再把结果当原始值传进去。接收端如果已经由框架解析了查询参数,也不要无条件再调用一次 decodeURIComponent()。

可以把本文的输入放进 ToolExo URL 编码与解码工具,对照空格、加号、百分号和中文的变化。比较结果时记得确认编码模式;不同 API 对部分标点的保留规则也可能不同。

三、Base64:先确定字节,再讨论编码

Base64 处理的是字节。对于中文文本,需要先约定字符编码,通常使用 UTF-8。

它每次把 3 个字节的 24 位数据,分成 4 组,每组 6 位,再映射成字符。标准 Base64 使用字母、数字、+ 和 /,末尾可能出现填充符 =。

包含填充、没有换行时,输出长度为:

4 × ceil(输入字节数 / 3)

所以 Base64 通常会增加约三分之一的体积,短数据受填充影响更明显。标准定义见 RFC 4648。

为什么 btoa("你好") 会报错

浏览器的 btoa() 接收的是表示字节的字符串,每个字符的值必须在 0 到 255 之间。直接传中文不满足这个条件。

下面是适合小段文本的 UTF-8 编解码实现:

function encodeBase64Text(text) {
   
  const bytes = new TextEncoder().encode(text);

  let binary = "";
  for (const byte of bytes) {
   
    binary += String.fromCharCode(byte);
  }

  return btoa(binary);
}

function decodeBase64Text(encoded) {
   
  const binary = atob(encoded);
  const bytes = Uint8Array.from(
    binary,
    char => char.charCodeAt(0)
  );

  return new TextDecoder("utf-8", {
   
    fatal: true
  }).decode(bytes);
}

const encoded = encodeBase64Text("你好");

console.log(encoded);
// 5L2g5aW9

console.log(decodeBase64Text(encoded));
// 你好

这里分了两步:UTF-8 文本转换为字节,再把字节编码为 Base64。解码时按相反顺序处理。btoa() 对输入的限制及 Unicode 处理方式,见 MDN 的 btoa 文档。

fatal: true 会让无效 UTF-8 字节触发异常。这样可以发现“解码成功,但结果根本不是文本”的情况。若原始内容是图片或压缩包,应按二进制处理,不要强行转换成字符串。实际接收外部输入时,也应捕获 Base64 格式错误和文本解码错误。

标准 Base64 和 Base64URL 别混用

Base64URL 把标准 Base64 中的两个字符替换掉:

+ → -
/ → _

是否保留末尾的 =,由使用它的协议约定,不能只凭字符串长相判断。

这也解释了一个联调问题:把标准 Base64 直接拼进查询字符串,其中的 + 可能被解析成空格。可以使用 URLSearchParams 编码参数;如果接口明确要求 Base64URL,则按接口约定生成。两者的定义同样可查阅 RFC 4648 第 5 节。

需要手动核对时,可以使用 ToolExo Base64 编码与解码工具,用 你好 检查 UTF-8 往返结果,再对照标准 Base64 与 URL-safe 模式。

四、HTML 实体:让标签作为文本显示

技术文档里经常需要显示这样的代码:

<div>Hello & goodbye</div>

如果把它作为 HTML 解析,浏览器会把 <div> 当成元素。要在 HTML 源码中表达这段文字,可以转义成:

&lt;div&gt;Hello &amp; goodbye&lt;/div&gt;

一个基础实现如下:

function escapeHtml(text) {
   
  const entities = {
   
    "&": "&amp;",
    "<": "&lt;",
    ">": "&gt;",
    '"': "&quot;",
    "'": "&#39;"
  };

  return text.replace(/[&<>"']/g, char => entities[char]);
}

console.log(escapeHtml('<div class="box">A & B</div>'));
// &lt;div class=&quot;box&quot;&gt;A &amp; B&lt;/div&gt;

使用一次替换,可以避免后续替换把刚生成的 &lt; 又变成 &amp;lt;。

这段函数用于普通 HTML 文本、带引号的普通属性值等场景,不能直接套到 JavaScript、CSS 或事件属性里。URL 属性还需要检查协议和业务允许的地址范围。输出位置不同,防护方式也不同,见 OWASP 的 XSS 防护指南。

只是显示文字,就直接用 textContent

在浏览器里,如果目标只是展示一段字符串,通常不用手写转义:

const code = document.createElement("pre");

code.textContent = '<div class="box">A & B</div>';

document.body.appendChild(code);

这里传入原始文本即可。如果先调用 escapeHtml(),再赋给 textContent,页面会把 &lt; 等字符原样显示出来。

这也是“明明转义了,页面却显示不对”的常见原因:渲染方式已经按文本处理,业务代码又提前转义了一次。

解码结果不能直接当作可信 HTML

把 &lt; 还原成 < 只是字符转换。随后若把结果交给 innerHTML,它又会进入 HTML 解析流程。

纯文本展示继续使用 textContent;确实需要富文本时,应使用适合业务的 HTML 清理方案。实体编码不能替代富文本清理。

可以通过 ToolExo HTML 实体编码与解码工具 对照标签、引号和 & 的转换结果。调试时要分清:你需要的是字符的解码结果,还是浏览器解析后的页面效果。

五、把三个知识点放进一次联调

假设接口要求把一段 UTF-8 文本编码为标准 Base64,再放进查询参数。发送端可以这样写,复用前面的函数:

const original = "你好,C++ & HTML";
const payload = encodeBase64Text(original);

const params = new URLSearchParams({
    payload });
const query = params.toString();

// 模拟接收端解析
const received = new URLSearchParams(query).get("payload");

if (received === null) {
   
  throw new Error("缺少 payload 参数");
}

const restored = decodeBase64Text(received);

console.log(restored === original);
// true

如果还需要把结果显示到页面:

const output = document.createElement("pre");
output.textContent = restored;
document.body.appendChild(output);

这里的处理顺序由接口和输出位置决定:

原始文本
→ UTF-8 字节
→ Base64
→ 查询参数序列化
→ 查询参数解析
→ Base64 解码
→ UTF-8 文本
→ textContent 显示

这个例子以接口明确要求 Base64 为前提。普通搜索词直接交给 URLSearchParams 就够了,无须额外包一层。

排查乱码或字符丢失时,可以把每一步的输入和输出记录下来:发送前是什么,参数解析后是什么,解码后又是什么。定位到字符第一次变化的位置,通常就能找到多做或少做的那一次转换。

相关文章
|
1月前
|
存储 人工智能 JSON
知识库实战篇二:多轮攻防与真实流量实测
本期实战解析多轮对话核心机制:会话历史由调用方构造,平台零存储、零校验;实测第三轮权限不松动,但隐蔽假历史污染七成带跑;40问真实流量揭示75%售前挡率短板;成本分析显示91%花费在客户不可见的思考与检索过程。安全与成本优化聚焦会话历史治理。
知识库实战篇二:多轮攻防与真实流量实测
|
1月前
|
人工智能 运维 自然语言处理
企业知识库问答系统实战教程:从知识整理到助理发布全流程
企业知识库问答系统是基于企业私域数据回答问题的 AI 应用。文章梳理知识库构建流程、RAG 关键环节与上线检查,适合客服、内部查询和特定领域问答。
159 0
|
1月前
|
人工智能 决策智能
单 Agent 与多 Agent 系统对比:架构差异与选择标准
单 Agent 与多 Agent 系统对比帮助判断任务该由一个还是多个智能体完成。围绕共享环境、角色职责和配置转交关系,说明原理差异与 AI Agent 架构选型。
118 0
|
1月前
|
人工智能 自然语言处理 安全
GEO优化实战工程中的:权威性与EEAT
本文剖析EEAT在生成式搜索中的真实作用:权威不可自证,需拆解为可核验的结构化字段(作者、来源、时间等);可信靠积累,非靠宣称。强调分平台验证信号有效性,反对“缺一不可”等误读,主张用实体标注替代笼统评分。(239字)
|
1月前
|
机器学习/深度学习 人工智能 KVM
从 3 天排查缩到 20 分钟,一个内核老兵的 AI Skill 诞生记 | 龙蜥 SkillHub 精选
把上游 mainline 的内核补丁安全地移植到低版本稳定内核,并证明移植后的语义与上游一致。
|
1月前
|
前端开发 IDE Android开发
Qoder 上新 Mobile Use,开始验证移动端应用
Computer Use 已经可以操作电脑,Browser Use 可以进入正在使用的浏览器。Qoder 推出 Mobile Use 插件(Beta),在安卓、鸿蒙与 iOS 上将代码修改接入真机或模拟器,完成运行、交互与结果确认。
299 1
|
1月前
|
缓存 API 开发者
DeepSeek V4.1 Flash 内测接入实战:改个模型名即可调用(附代码)、curl/Python完整代码与Agent工具配置全教程
大模型迭代节奏持续加快,在V4‑Flash与V4‑Pro大规模落地之后,DeepSeek放出中间内测版本**DeepSeek‑V4.1‑Flash**,采用全新CED非对称MoE架构,实现原生多模态图文理解,推理吞吐速度大幅提升,内测阶段计费标准与V4‑Flash保持一致,开发者不需要修改接口地址,**仅仅更换模型名称就可以完成调用**,接入改造成本极低。
567 0
|
7月前
|
存储 安全 Java
详解 Java 中修改列表(List)元素的多种方式与最佳实践
在 Java 开发中,List 作为最常用的集合类型,广泛应用于数据存储与处理场景。修改 List 中的元素是日常开发的高频操作,不同场景下(已知索引、匹配内容、操作自定义对象)需选择不同的修改方式。本文将从基础到进阶,全面讲解 List 元素的修改方法、避坑要点及最佳实践,结合萌宠场景的示例让新手也能快速理解。
411 1