中文在网址里为什么变成 E4B8AD?URL 编码 22 组用例实测

0 阅读9分钟

「中」在网址里是 %E4%B8%AD,「文」是 %E6%96%87,两个字一共 6 个字节、18 个字符。

同一个空格,encodeURIComponent 给的是 %20,表单编码给的是加号。

我把 22 组输入丢进一个网页逐个拆到字节,页面上 27 项断言全绿,耗时 0.50 毫秒。

图 1:五个面板同屏——编码与解码、字节拆解、三种 API 对照、22 组用例表,右上角自检徽章显示 27/27 通过

在线案例与源码:atomgit.com/deli007/dem… 成品只有一个 index.html,双击就能打开。下面每个数字都是我在页面里读回来、又用一份 Python 脚本复算过的。

一、为什么值得搞清楚:百分号后面不是加密,是另一种读法

%E4%B8%AD 不是密文,它就是三个字节:E4、B8、AD。

浏览器拿 UTF-8 去读这三个字节,读出「中」。

网址规范只允许 ASCII 字符,中文、日文、emoji 都得先变成字节,再写成 %XX。

所以网址里的「乱码」多半不是文件坏了,是同一串字节被另一种编码读了。

这个规则平时感知不强,一到拼接参数、生成分享链接就会咬人。

我这次想弄清楚三件事:哪些字符会被编码、三种 API 差在哪、空格为什么有两种写法。

二、准备环境:进入码道 Web,把「不许写死结果」写进需求

浏览器打开码道 Web 版:devcloud.cn-north-4.huaweicloud.com/chat?source… ,登录后在对话框里写需求,就能拿到一个能跑的单文件应用,不用装软件。

码道有三种使用方式:WebUI(浏览器对话)、TUI(终端命令行)和桌面 IDE(IDE 插件)。本文用 WebUI 版演示。

这轮需求里最值钱的是三句话。

第一句是把 22 组用例的输入直接写进需求,别让模型自己回忆。

第二句是结果必须现场算:编码走 encodeURIComponent,字节走 TextEncoder。

第三句是失败路径要显示浏览器抛出的原始错误,并保留上一次有效结果。

第三句看着啰嗦,但它决定这个页面能不能当尺子用。

图 2:左边是「中文」的 6 个字节拆解,右边是同一个输入在三种 API 下的结果

三、一屏看清:五个面板加一枚自检徽章

页面从上往下是五块。

第一块是编码与解码,输入即时联动,两个方向都能看。

第二块是字节拆解,按序号列出十六进制、十进制和 %XX 三列。

第三块是三种 API 对照,把 encodeURI、encodeURIComponent、表单编码并排摆出来。

第四块是 22 组用例实测表,每行给原始输入、编码结果、字节数和说明。

第五块是内置断言,页面加载时自己跑一次。

右上角的徽章就是自检结果,27 项全过是绿色的 27/27。

四、实测:22 组输入逐个拆开

这张表的结果不是抄来的,是页面每次加载时用 encodeURIComponent 现场算的。

序号原始输入encodeURIComponent 结果UTF-8 字节说明
1中文%E4%B8%AD%E6%96%876中文
2a中ba%E4%B8%ADb5中英混排
3a ba%20b3空格
4a+ba%2Bb3加号
5a/ba%2Fb3斜杠
6?%3F1问号
7#%231井号
8=%3D1等号
9&%261和号
10%%251百分号
11~~1波浪线
12--1连字符
13__1下划线
14..1点号
15!!1感叹号
16**1星号
17''1单引号
18((1左括号
19emoji%F0%9F%98%804emoji
20あ%E3%81%823日文平假名
21中文 测试%E4%B8%AD%E6%96%87%20%E6%B5%8B%E8%AF%9513中文加空格
22%25%25253双重编码

图 3:22 行用例的完整结果,右上角是重算耗时

第 11 到 18 行的结果和输入长得一模一样,这不是没算,是这些字符本来就不用编码。

第 22 行最能说明问题:输入本身已经是 %25,再编码一次变成 %2525。

一个参数被编码两次,服务端解一次就只能拿到 %25,拿不到真正的 %。

五、三种 API 差在哪:同一个斜杠,两种结果

encodeURI 把整个网址当成一个整体,所以 ; , / ? : @ & = + 和 # 都不动它。

encodeURIComponent 假设你在拼参数的值,这些字符全部要转义。

表单编码另起一套:空格写成加号,其余和 encodeURIComponent 一样。

输入encodeURIencodeURIComponent表单编码
a/b?c=d&e=fa/b?c=d&e=fa%2Fb%3Fc%3Dd%26e%3Dfa%2Fb%3Fc%3Dd%26e%3Df
中 文%E4%B8%AD%20%E6%96%87%E4%B8%AD%20%E6%96%87%E4%B8%AD+%E6%96%87
100%100%25100%25100%25
两个 emoji%F0%9F%98%80%F0%9F%98%80%F0%9F%98%80%F0%9F%98%80%F0%9F%98%80%F0%9F%98%80

前两行的差别就是全部:路径里的斜杠可以留,参数值里的斜杠必须转。

有个反直觉的点:表单编码把空格写成加号,但网址路径里的加号就是加号本身。

所以用 decodeURIComponent 解 %2B 得到的是加号,不是空格。

六、那 6 个字节是怎么来的

「中文」两个字,页面拆出来正好 6 行。

序号HEXDEC%XX
1E4228%E4
2B8184%B8
3AD173%AD
4E6230%E6
596150%96
687135%87

UTF-8 里一个中文占 3 字节,所以两个汉字正好 6 字节、18 个字符。

emoji 占 4 字节,日文平假名占 3 字节,ASCII 占 1 字节。

这些数字全部由 TextEncoder 现算,页面里没有预先写死的表。

七、验证与踩坑:27 项自检加一处真实不一致

页面的自检是 22 条用例加 5 条边界,一共 27 项。

我点了一次运行自检,结果是 27/27 通过,页面自己记的耗时 0.50 毫秒。

图 4:27 项断言逐条列出,22 条用例加 5 条边界全部打勾

不信页面自己报的数,我又用 Python 标准库的 urllib.parse.quote 重算了一遍。

脚本跑出来 30/30 项一致,含 22 组编码结果、5 组字节数、%25 解码、空格两种写法和保留字符集合。

失败路径我拿 %E4%B8 试的:一个百分号后面本该跟两个十六进制数字,这里只有两个,字节数不完整。

页面把浏览器抛的原文贴了出来:URIError: URI malformed。

同时它保留了上一次的有效结果,没有把解码框清空。

图 5:解码输入 %E4%B8,红色报错给出浏览器原文,下方仍保留上一次解出来的「中文」

再往下是我这轮真揪出来的两处问题。

第一处是真实的不一致:输入一个孤立的代理字符,encodeURIComponent 直接抛 URIError,而同一页面的 TextEncoder 把它替换成 U+FFFD,也就是 EF BF BD 三个字节。

同一个输入,两个面板给出两种答案。这不算页面算错,是两个 API 的规范本来就不同。

但页面当时把错误文本当成了编码结果,统计行写着字符 23、UTF-8 字节 23,看上去像一串正经输出。

我把统计行改成「编码失败:URIError: URI malformed · 没有有效编码串」,错误就不再冒充结果。

第二处是排版:22 行表格和 6 行字节表被塞进固定高度的滚动容器,一屏是放得下,看全却要来回滚。

我把两处高度限制都去掉了,表格改成一次显示完整,截图也就是现在这样。

八、边界与取舍

下面这几条是我实测出来的真边界,不是免责声明式的套话。

第一,页面用 decodeURIComponent 解码,它不把加号当空格,所以表单编码串里的加号会原样留下。

第二,输入 10000 个「中」时,字节表要渲染 30000 行,页面耗时 178.9 毫秒,长输入下逐字节表会明显拖慢。

第三,自检只在页面加载和手动点击时跑一次,它不是持续监控。

第四,这一页做的是 URL 编码层,不做字符集转换,UTF-8 与 GBK 的对照不在这里。

第五,表单编码的空格规则只属于 application/x-www-form-urlencoded,网址路径里没有这一套。

九、直接下载试玩

这个 Demo 已经完整开源,源代码、本文档、五张截图和一个复算脚本都在公开仓库里。

把 index.html 下载到本地,用浏览器打开就能改输入、切 API,看着字节和编码串一起变。

十、本地复现与下一步

本地跑两步就够。

git clone https://atomgit.com/deli007/demo_park.git
cd demo_park\codearts-url-encoding-lab
python -m http.server 8080

浏览器打开 http://localhost:8080/index.html ,把编码输入从「中文」换成「中 文」,能看到字节数从 6 涨到 13,空格那一位变成 %20。

复算脚本单独跑一次,命令在案例目录里。

python verify_url_encoding.py

下一步我打算补两件事:一是把历史编码相关的坑做成对照表,比如 %2525 这类双重编码在不同框架里怎么解;二是给逐字节表加个上限,超过一定长度只显示前后各若干字节,免得长输入把页面拖慢。

十一、使用码道体会

  • 把判定用的输入直接写进需求,比写「覆盖常见场景」有用得多,22 组用例是我在需求里逐条列出来的。
  • 结果必须让页面现场算,凡是让模型回忆表格和数值的地方,都要在验收时多留个心眼。
  • 失败路径要连错误原文一起写进需求,这一条让页面在非法输入时给了我能核对的东西。
  • 自检项要由页面自己报数,27/27 这种数字比「测试通过」四个字可信。
  • 页面生成的布局未必合你的阅览习惯,表格滚不滚动这种小事,自己动手改比重新生成快。
  • 同一个输入两个面板给出不同答案时,先去查规范差异,别急着改代码。

一句实在的话:码道把可运行的第一版做得很快,但这一页能不能当尺子用,取决于你把验收条件写得多具体。