查看: 2152|回复: 3

Python URL解码 unquote双重编码parse_qs踩坑

[复制链接]
发表于 2026-9-30 11:00:00 | 显示全部楼层 |阅读模式
在爬虫、接口对接和自动化脚本里,URL 编码与解码几乎是绕不开的基础操作。相比编码,解码更容易踩坑,尤其是双重 URL 编码:字符串里大量出现 %25,如果只做一次 unquote,拿到的仍是第一层编码后的内容,后续 json.loads 容易直接抛 JSONDecodeError。本文基于 Python 内置 urllib.parse,不依赖第三方库,梳理单层解码、双重解码、parse_qs 自动解码以及常见错误处理。

一、URL 解码基础与 unquote/unquote_plus

URL 编码是把特殊字符转为 % 加十六进制,解码则是把 %XX 还原为原始字符。常见映射包括:%22 对应双引号,%7B 对应左花括号,%7D 对应右花括号,%3A 对应冒号,%2C 对应逗号,%25 对应百分号。其中 %25 是识别双重编码的关键字符。

Python 内置 urllib.parse.unquote 负责 URL 解码,配套的 unquote_plus 会把 + 还原为空格,后者适用于 application/x-www-form-urlencoded 表单查询串。普通 URL 路径和参数解析优先使用 unquote。

单层解码示例:
  1. from urllib.parse import unquote
  2. encoded_str = '%7B%22articleId%22%3A%2289c46c77f90e61ff7df01e3cc18eae9349f619f697ca4366e94fad724d913ee6%22%2C%22columnId%22%3A%22268f86fdf61ac8614f09db38a2d0295253043b03e092c7ff48ab94290296125c%22%7D'
  3. raw = unquote(encoded_str)
  4. print(raw)
  5. # 输出:articleId 和 columnId 组成的 JSON 对象
复制代码

二、双重 URL 解码:先识别 %25,再连续 unquote

双重 URL 编码的形成逻辑是:原始 JSON 先被 quote 一次,得到 %7B、%22 等;再被 quote 一次,% 被编码成 %25,于是出现 %257B、%2522。解码顺序必须反过来:第一次 unquote 把 %25 还原为 %,得到单层编码串;第二次 unquote 才把 %7B、%22 还原为原始字符。

接口参数示例:
  1. params=%257B%2522articleId%2522%3A%252289c46c77f90e61ff7df01e3cc18eae9349f619f697ca4366e94fad724d913ee6%2522%2C%2522columnId%2522%3A%2522268f86fdf61ac8614f09db38a2d0295253043b03e092c7ff48ab94290296125c%2522%257D
复制代码

完整解码代码:
  1. from urllib.parse import unquote
  2. import json
  3. full_param = 'params=%257B%2522articleId%2522%3A%252289c46c77f90e61ff7df01e3cc18eae9349f619f697ca4366e94fad724d913ee6%2522%2C%2522columnId%2522%3A%2522268f86fdf61ac8614f09db38a2d0295253043b03e092c7ff48ab94290296125c%2522%257D'
  4. # 截取 params= 后面的值
  5. encode_value = full_param.split('params=')[1]
  6. # 双重解码
  7. decode_step1 = unquote(encode_value)
  8. decode_step2 = unquote(decode_step1)
  9. # 转为字典
  10. data = json.loads(decode_step2)
  11. print(data)
复制代码

执行过程拆解:第一次 unquote 后,%257B 变成 %7B,%2522 变成 %22,得到单层编码串;第二次 unquote 后,%7B 变成 {,%22 变成双引号字符,得到原始 JSON 字符串;最后 json.loads 加载成 Python 字典。如果只解码一次,字符串中仍会保留大量 %,json.loads 无法正常解析。

三、解析完整 URL 查询参数:parse_qs 已自动解码

日常开发中经常拿到完整 URL,需要直接解析 query 参数。此时可以用 urlparse 加 parse_qs,parse_qs 内部自带解码,不需要手动 unquote。
  1. from urllib.parse import urlparse, parse_qs
  2. url = 'https://www.chinaisa.org.cn/gxportal/xfgl/portal/content.html?articleId=89c46c77f90e61ff7df01e3cc18eae9349f619f697ca4366e94fad724d913ee6&columnId=268f86fdf61ac8614f09db38a2d0295253043b03e092c7ff48ab94290296125c'
  3. parsed = urlparse(url)
  4. query = parse_qs(parsed.query)
  5. # parse_qs 返回 value 是列表
  6. res = {k: v[0] for k, v in query.items()}
  7. print(res)
复制代码

注意:parse_qs 已经完成一次 URL 解码,不要再对结果手动 unquote,否则会重复解码,遇到 %25 这类字符时容易产生脏数据。

四、常见解码踩坑清单

坑 1:双重编码只解码一次。现象是解码后的字符串还包含大量 %,json.loads 直接抛出 JSONDecodeError。判断依据是字符串包含 %25,几乎可以认定是双重编码,需要连续两次 unquote。

坑 2:对 parse_qs 的结果再次 unquote。parse_qs 内部已经自动解码,再次 unquote 会二次解码,遇到 %25 这类字符会产生脏数据。

坑 3:中文解码编码不匹配。unquote 默认 encoding='utf-8',如果碰到 GBK 编码的老网站,会抛出 UnicodeDecodeError。解决方式是显式指定编码:
  1. from urllib.parse import unquote
  2. s = '%D6%D0%CE%C4'
  3. print(unquote(s, encoding='gbk'))
复制代码

坑 4:不合法的 % 编码串。原始字符串被截断,出现 %3 这类不完整编码时,默认会抛异常。可以设置 errors='replace' 忽略错误:
  1. unquote(s, errors='replace')
复制代码

五、封装通用函数:自动处理单层与双重解码

如果项目里经常遇到单层或双重编码,可以写一个简易工具函数,判断字符串中是否包含 %25,自动选择解码次数。代码可以直接复用:
  1. from urllib.parse import unquote
  2. import json
  3. def auto_double_decode(s: str):
  4.     '''自动判断单层/双重URL解码,返回原始字符串'''
  5.     if '%25' in s:
  6.         s = unquote(unquote(s))
  7.     else:
  8.         s = unquote(s)
  9.     return s
  10. # 测试
  11. encoded = '%257B%2522articleId%2522%3A%252289c46c77f90e61ff7df01e3cc18eae9349f619f697ca4366e94fad724d913ee6%2522%2C%2522columnId%2522%3A%2522268f86fdf61ac8614f09db38a2d0295253043b03e092c7ff48ab94290296125c%2522%257D'
  12. raw_json = auto_double_decode(encoded)
  13. data = json.loads(raw_json)
  14. print(data)
复制代码

提示:这个简易函数只适用于爬虫常见场景,极端多层编码(3 层及以上)不适用。

六、什么时候会遇到双重编码

双重编码常见于老 Java 政务、国企网站;网关或 Nginx 多层转发时,每层可能自动解码,前端 JS 编码后浏览器发起请求再次自动编码;接口返回的参数是经过两层编码的 JSON 字符串,并放在 URL query 里传递。这些场景下,如果只依赖一次 unquote,参数解析失败的概率会很高。

七、小结

URL 解码核心是 urllib.parse.unquote,单层解码是基础;%25 是识别双重 URL 编码最直观的标记,遇到它就要连续两次解码。开发中还要区分 unquote 和 unquote_plus,不要在 parse_qs 解析完成后重复解码。遇到中文乱码时记得切换 gbk 编码。编码和解码成对学习,在爬虫对接老旧门户、复杂网关转发接口时,能解决相当一部分参数解析失败问题。

以上内容围绕 Python 单层与双重 URL 解码的实战代码和踩坑点展开,核心是保留 unquote 的调用顺序、parse_qs 的自动解码行为以及编码错误处理方式。
回复

使用道具 举报

发表于 2026-9-30 19:00:00 | 显示全部楼层

Re: Python URL解码 unquote双重编码parse_qs踩坑

感谢楼主整理,URL 解码这些坑确实很常见。双重编码先看有没有 %25,再连续 unquote 两次,这个判断和操作顺序很实用;只解一次后 json.loads 报 JSONDecodeError 的情况确实容易让人卡住。parse_qs 会自动解码、不要对结果再手动 unquote 这点也很重要,二次解码遇到 %25 很容易产生脏数据。中文 GBK 老站要显式指定 encoding='gbk' 也记下了。坑 4 不合法 % 编码串看起来还没写完,期待后续补充,比如截断或 %3 这种不完整转义怎么安全处理。
回复 支持 反对

使用道具 举报

发表于 2026-9-30 19:10:00 | 显示全部楼层

Re: Python URL解码 unquote双重编码parse_qs踩坑

感谢分享,整理得很系统。之前我也踩过双重编码的坑,接口参数里一堆 %25,只 unquote 一次之后 json.loads 直接报 JSONDecodeError,后来才反应过来要连续解两次。你用 %25 作为双重编码的判断依据,这个很实用。parse_qs 自动解码那部分也提醒得很好,拿到完整 URL 时确实容易手痒再 unquote 一遍,结果反而把数据弄脏。中文编码不匹配和非法 % 编码这两个坑也很常见,显式指定 encoding 和加异常处理能省不少排查时间。收藏了,后面写爬虫和接口脚本可以对照检查。
回复 支持 反对

使用道具 举报

发表于 2026-9-30 19:20:00 | 显示全部楼层

Re: Python URL解码 unquote双重编码parse_qs踩坑

写得挺清楚的,尤其是把 %25 作为判断双重编码的关键点,以及 parse_qs 内部已经自动解码、不要重复 unquote 这两处,实际排查时很容易忽略。单层和双重解码的顺序也讲明白了:先 unquote 一次得到单层串,再 unquote 一次才能 json.loads。中文 GBK 那个指定 encoding 的例子也很实用。坑4 好像没贴完,像原始字符串被截断出现 %3 这种情况,如果后面能补上异常捕获或校验思路就更完整了。感谢分享。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-10-14 11:46 , Processed in 0.043430 second(s), 17 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部