很多爬虫、接口对接时都会踩URL编码的坑,尤其是双重URL编码 ,乍一看很诡异,
%25到底是什么?本文结合真实业务场景讲清楚原理与Python实现。
一、什么是URL编码(urlquote)
URL中部分字符属于保留字符 ,不能直接放在查询参数里,例如 { } " : , 等。
URL编码作用:将特殊字符转为 %十六进制 格式。
常见对照表:
"→%22{→%7B}→%7D:→%3A,→%2C%→%25👉 重点!百分号本身编码后变成 %25,这就是双重编码的核心根源
Python内置模块 urllib.parse.quote 用来做URL编码,无需额外安装第三方包。
quote 关键参数:safe
quote(string, safe='/', encoding=None, errors=None)
safe:设置不需要编码 的字符,默认是/,也就是斜杠不会被编码。safe='':强制所有字符全部编码,不保留任何原始字符,爬虫对接很多后端接口时必须用这个。
示例单层编码:
python
from urllib.parse import quote
import json
data = {
"articleId": "89c46c77f90e61ff7df01e3cc18eae9349f619f697ca4366e94fad724d913ee6",
"columnId": "268f86fdf61ac8614f09db38a2d0295253043b03e092c7ff48ab94290296125c"
}
json_str = json.dumps(data, separators=(',', ':'))
# 单层URL编码
encode_1 = quote(json_str, safe='')
print(encode_1)
# 输出:%7B%22articleId%22%3A%2289c46c77f90e61ff7df01e3cc18eae9349f619f697ca4366e94fad724d913ee6%22%2C%22columnId%22%3A%22268f86fdf61ac8614f09db38a2d0295253043b03e092c7ff48ab94290296125c%22%7D
二、双重URL编码是什么(本次业务场景)
很多老旧Java/PHP后端、门户站点接口,会在服务端自动做一次URL解码。
如果前端传递JSON字符串作为URL参数:
- 前端把JSON编码一次 →
%7Bxxx%7D - 浏览器/网关还会再自动编码一遍 ,把里面的
%编码成%25
最终就变成我们看到的:%257B%2522articleId%2522...%257D
本质:对已经编码后的字符串,再执行一次quote
python
# 双层URL编码
encode_1 = quote(json_str, safe='')
encode_2 = quote(encode_1, safe='')
final_param = f"params={encode_2}"
print(final_param)
# params=%257B%2522articleId%2522%3A%252289c46c77f90e61ff7df01e3cc18eae9349f619f697ca4366e94fad724d913ee6%2522%2C%2522columnId%2522%3A%2522268f86fdf61ac8614f09db38a2d0295253043b03e092c7ff48ab94290296125c%2522%257D
完整链路:从原始URL提取参数 + 双重编码
业务场景:从一个url链接中提取query参数,组装成json,再双重url编码拼成params=xxx
python
from urllib.parse import urlparse, parse_qs, quote, unquote
import json
url = '[https://www.chinaisa.org.cn/gxportal/xfgl/portal/content.html?articleId=89c46c77f90e61ff7df01e3cc18eae9349f619f697ca4366e94fad724d913ee6&columnId=268f86fdf61ac8614f09db38a2d0295253043b03e092c7ff48ab94290296125c](https://www.chinaisa.org.cn/gxportal/xfgl/portal/content.html?articleId=89c46c77f90e61ff7df01e3cc18eae9349f619f697ca4366e94fad724d913ee6&columnId=268f86fdf61ac8614f09db38a2d0295253043b03e092c7ff48ab94290296125c)'
# 1. 解析url,提取查询参数
parsed = urlparse(url)
query_dict_raw = parse_qs(parsed.query)
# parse_qs返回值是列表,取第一个值转为普通字典
data = {k: v[0] for k, v in query_dict_raw.items()}
# 2. 字典压缩成json字符串,去掉多余空格
json_str = json.dumps(data, separators=(',', ':'))
# 3. 双重URL编码
encode1 = quote(json_str, safe='')
encode2 = quote(encode1, safe='')
result = f"params={encode2}"
print(result)
三、解码:双重编码反向还原
调试的时候,我们拿到params=%257B...字符串,需要还原回原始字典,解码顺序反过来:先unquote一次,再unquote第二次
python
from urllib.parse import unquote
import json
encoded_str = "params=%257B%2522articleId%2522%3A%252289c46c77f90e61ff7df01e3cc18eae9349f619f697ca4366e94fad724d913ee6%2522%2C%2522columnId%2522%3A%2522268f86fdf61ac8614f09db38a2d0295253043b03e092c7ff48ab94290296125c%2522%257D"
# 截取params=后面部分
val = encoded_str.split("params=")[1]
# 两次解码
d1 = unquote(val)
d2 = unquote(d1)
origin_dict = json.loads(d2)
print(origin_dict)
四、常见踩坑总结
- 忘记
safe='':默认safe="/",/不会编码,很多接口校验严格,会直接报错。 - 分不清单层和双层编码:接口返回参数带有大量
%25,基本就是双重URL编码。 parse_qs返回list:同一个参数允许多次出现,{k:v[0]}只适用于参数唯一场景。- json序列化带空格:
json.dumps(data)默认带换行空格,编码后后端识别异常,务必用separators=(',',':')压缩json。 quote_plus和quote的区别:quote_plus会把空格编码成+,常用于application/x-www-form-urlencoded表单;quote空格编码成%20,url路径参数优先使用quote。
五、什么时候会遇到双重URL编码
- 老项目Java后端,服务端自动做一次decode
- 多层网关、Nginx转发,每层都会自动解码一次
- 前端JS先编码,浏览器请求时又自动编码一遍
- 爬虫对接政务、国企门户网站(本次案例就是典型场景)
调试小技巧:看到编码串大量
%25,第一反应:这不是普通url编码,是双重编码。
六、小结
URL编码本质就是特殊字符转义,单层编码是基础;而双重URL编码 是爬虫对接老旧网站高频遇到的特殊场景。核心逻辑:JSON字符串 → quote一次 → quote第二次。
Python依靠urllib.parse内置库就能全部实现,无需requests或者其他第三方编码库。调试时可以逆向解码验证结果是否正确,避免接口报参数解析失败。