编码与转义三件套: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() 接收的是表示字节的字符串,每个字符的值必须在 0255 之间。直接传中文不满足这个条件。

下面是适合小段文本的 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 就够了,无须额外包一层。

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

相关文章
|
3天前
|
API 数据安全/隐私保护 开发者
GPT-6 Astra 在 Plus 订阅中不显示的原因及 Chat、Work、Codex 入口差异解析
本文详解ChatGPT Plus用户如何正确查找GPT-6 Astra:它不在普通Chat界面,而需通过Work或Codex入口访问;权限按套餐逐步开放,需核对账号、工作区、客户端版本及官方开放进度。排查请按入口→账号→版本顺序进行。
|
7天前
|
缓存
阿里云Token Plan的Credits是如何计费的?1个Credits相当于多少Token?
阿里云百炼Token Plan采用动态Credits计费,1 Credits不固定对应Token数,实际消耗由模型类型、Token用量、思考模式及工具调用等共同决定。以qwen3.6-plus为例,输入/缓存/输出Token折算系数各异,百万Tokens成本低至1.12元,较按量计费最高省44%。新用户赠7000万Tokens。在阿里云百炼官网:https://t.aliyun.com/U/fPVHqY 免费领取千万Tokens
|
15小时前
|
存储 人工智能 JSON
知识库实战篇二:多轮攻防与真实流量实测
本期实战解析多轮对话核心机制:会话历史由调用方构造,平台零存储、零校验;实测第三轮权限不松动,但隐蔽假历史污染七成带跑;40问真实流量揭示75%售前挡率短板;成本分析显示91%花费在客户不可见的思考与检索过程。安全与成本优化聚焦会话历史治理。
知识库实战篇二:多轮攻防与真实流量实测
|
15小时前
|
前端开发 IDE Android开发
Qoder 上新 Mobile Use,开始验证移动端应用
Computer Use 已经可以操作电脑,Browser Use 可以进入正在使用的浏览器。Qoder 推出 Mobile Use 插件(Beta),在安卓、鸿蒙与 iOS 上将代码修改接入真机或模拟器,完成运行、交互与结果确认。
40 1
|
21小时前
|
人工智能 决策智能
单 Agent 与多 Agent 系统对比:架构差异与选择标准
单 Agent 与多 Agent 系统对比帮助判断任务该由一个还是多个智能体完成。围绕共享环境、角色职责和配置转交关系,说明原理差异与 AI Agent 架构选型。
|
21小时前
|
人工智能 运维 自然语言处理
企业知识库问答系统实战教程:从知识整理到助理发布全流程
企业知识库问答系统是基于企业私域数据回答问题的 AI 应用。文章梳理知识库构建流程、RAG 关键环节与上线检查,适合客服、内部查询和特定领域问答。
|
3天前
|
运维 监控 数据可视化
把IT 运维的重复劳动交给智能体:巡检、专利年费与权限审计实战
运维自动化核心价值:将工程师从“必做但无聊”的重复性高危任务(如巡检、专利缴费、行为审计)中解放,专注需判断力的工作。通过执行层机器人+桌面行为分析,实现100%时效保障、零漏检、全留痕、强审计,真正构建可信、可控、可追溯的智能运维体系。
|
15小时前
|
人工智能 缓存 API
Token 套餐时代:企业 AI 用量计量与费用归因实践
2026年9月,三大运营商试点Token套餐,拟将Token列为语音、流量、宽带之后的“第四通信计量单位”。本文聚焦企业级AI调用计量实践,详解调用归属、用量统计、费用分摊与对账要点,强调原始日志、估算成本与实际账单须分离管理。
41 0
|
2天前
|
存储 人工智能 自然语言处理
从SaaS到私有化:主流智能客服系统深度盘点
2026年,智能客服部署逻辑已从“要不要上”转向“装在哪里”。阿里云瓴羊Quick Service支持SaaS、私有化、混合云三模部署,兼顾数据安全与敏捷落地;融合大模型与AI Agent,实现93%问答准确率及查物流、退款等“答办一体”能力,助力企业从成本中心迈向增长引擎。(239字)