
简介这份 Python 编写的网易云音乐直链解析 API 项目面向需要绕过官方客户端限制、直接获取音乐播放地址的开发者与个人用户适合有一定 Python 基础的读者动手实践。它通过模拟浏览器请求调用网页版接口实现对歌曲、歌单等资源的链接解析可作为学习网络爬虫、API 设计与反爬策略的实战案例。压缩包共 13 个文件大小仅 11KB包含 2 个 Python 脚本index.py、redis_session.py、2 个 JS 与 2 个 CSS 前端静态文件、2 个 J2 页面模板以及 YAML 配置示例、README 文档和许可证等结构精简便于快速部署与二次开发目前已有 1652 人学习下载。解压后可获得完整项目源码、依赖清单及配置样例参考 index.py 的接口实现和 redis_session.py 的会话处理逻辑能够清晰了解直链解析的完整流程同时为处理请求加密、维护登录状态等常见问题提供直接参考。1. 某音乐平台直链解析 API为什么说这是本地音乐库的必需品做本地音乐备份的人基本都遇到过同一个问题在线播放流畅程序却拿不到稳定的音频直链。某音乐平台的直链解析 API 就是为此设计的输入歌曲 id输出一条带签名时效的播放地址用 Python 落地整个流程只需要三个步骤构造加密参数、发起请求、解析响应。适合三种人批量备份歌单的收藏党、做离线缓存服务的开发者、以及搭自动化音频管线的工程师。如果你只会用 requests 但没碰过 AES/RSA 这类反爬参数这篇文章会把最关键的封装逻辑、cookie 维护和限流避坑全部拆开讲清楚。动手之前先记住一句话直链不是从浏览器里“抠”出来的是按接口规则“算”出来的。2. 直链解析的工作原理播放地址藏在接口响应的哪一层拆代码之前先把“直链藏在哪”这个问题说透。打开网页端播放一首歌浏览器 Network 面板里能看到一条媒体请求以 .m4a 或 .mp3 结尾带一长串签名参数很多人以为直接把这个地址复制下来就行。但实际上这条地址是会话级的临时签名链接有效期短则几分钟、长则几小时而且同一个地址换一个环境去请求大概率直接 403。真正稳定的做法是复现浏览器在点击播放那一刻发出的那一次带加密参数的 POST 请求。2.1 网页播放接口的工作流程从歌曲页面到签名音频浏览器打开播放页页面框架先拿到歌曲的元信息包括歌名、封面、艺人、时长但里面没有音频地址。用户点击播放按钮后前端才会发起一个 POST 请求把歌曲 id、期望音质、时间戳等参数包在加密体里提交给接口。服务端校验 cookie 会话状态、加密参数正确性和版权信息之后返回一个 JSON里面带一条 CDN 签名地址。整个链路里解析 API 要做的事就是替你把“点击播放”这一步自动化。它需要具备三样东西可用的会话 cookie、与页面逻辑一致的加密参数生成器、能正确解读返回结构的解析层。这里有一个重要认知接口返回的 JSON 字段里url 只是其中一项旁边还有 br、size、type、encodeType 等字段。盲写脚本的人通常只盯着 url 取但遇到 VIP 歌曲或登录态失效时url 不是报错而是直接为空新手在这里会卡很久。音质等级这一层也容易忽略。接口并不总是接受“越大越好”的数值参数名不同版本有差异常见做法是用 level 枚举值可以用一张表来说明对应关系level 值含义典型码率standard标准128kbpshigher较高192kbpsexhigh高320kbpslossless无损1411kbpshires高解析高于无损普通匿名会话返回的 br 通常被压缩到 standard 或 higher 档位VIP 会话才能解锁 exhigh 以上。所以解析脚本里能不能拿到真正的高音质直链取决于你维护的 cookie 是什么状态而不是参数里写得多高。2.2 三个关键输入cookie、加密参数与接口路径所有播放类接口在接受请求之前先验证三件事你是谁、参数对不对、这首歌你配不配听。第一是 cookie。匿名请求也能跑通接口但返回的音质上限低而且部分 VIP 歌曲会直接返回空 url。登录后的 cookie 里不止有会话令牌平台方还会用同一份 cookie 绑定 User-Agent这意味着你从浏览器里复制 cookie 之后程序里发送请求的 UA 必须和浏览器保持一致否则会话会判定异常。第二是加密参数。网页端的请求体不是明文 JSON而是经过两层封装。第一层用 AES-128-CBC 对参数字符串做对称加密第二层把 AES 密钥再做一次 RSA 非对称加密最后提交的是 params 和 encSecKey 两个字段。这个机制是为了让服务端能够验明请求确实来自官方网页逻辑而不是随便拿 curl 拼出来的裸请求。第三是接口路径。网页端和客户端走的路径不同签名算法也不同。网页端常用的是 weapi 封装客户端逻辑里另有一套 eapi。解析 API 这个项目主要落在 weapi 这条线上因为它和浏览器抓包能直接对应调试成本最低。在浏览器控制台里复制出来的请求体不要直接埋进代码。页面里每一秒都会更新 timestamp、requestId 这类动态字段整体复刻意味着你的脚本在几秒钟之后就失效了。正确做法是只保留服务端强校验的固定字段把动态字段每次请求时重新生成。3. Python 实现解析 API加密参数、请求发起与响应处理现在进入代码部分。我给一个能跑通的最小实现覆盖加密参数生成、请求发起、响应解析三件事。写完直接调用拿到的直链可以用 requests 或者任意下载器拉取。需要安装 pycryptodome 和 requests 两个依赖。3.1 请求头构造与 weapi 加密参数生成weapi 的参数生成逻辑是整个解析流程的核心它分为两步AES 加密参数体、RSA 加密 AES 密钥。import time import base64 import json import requests from Crypto.Cipher import AES, PKCS1_v1_5 from Crypto.PublicKey import RSA # 固定常量来自网页端 JS 配置不要随意修改 AES_KEY bgX5fQmYqYbG9KqDv # 16 字节 AES 密钥 AES_IV bzZq2fQpK8vRjWbHx # 16 字节 AES 偏移量 RSA_PUBLIC_KEY -----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8asrfSfoKUR4TqZ1Z... -----END PUBLIC KEY----- def aes_encrypt(text: str) - str: # 标准 PKCS7 补位然后 AES-128-CBC 加密输出 base64 字符串 pad 16 - len(text) % 16 text text chr(pad) * pad cipher AES.new(AES_KEY, AES.MODE_CBC, AES_IV) encrypted cipher.encrypt(text.encode(utf-8)) return base64.b64encode(encrypted).decode(utf-8) def rsa_encrypt(plain: str) - str: # 对 AES 密钥明文做 RSA 加密输出 base64 字符串 pub RSA.importKey(RSA_PUBLIC_KEY) cipher PKCS1_v1_5.new(pub) encrypted cipher.encrypt(plain.encode(utf-8)) return base64.b64encode(encrypted).decode(utf-8) def build_weapi_payload(api_params: dict) - dict: # 序列化时去掉多余空格保证加密结果和官方一致 text json.dumps(api_params, separators(,, :)) params aes_encrypt(text) enc_sec_key rsa_encrypt(AES_KEY.decode(utf-8)) return {params: params, encSecKey: enc_sec_key}这段代码有三个地方容易翻车。AES 密钥和 IV 是固定写死的常量不是每次随机生成。你在浏览器里看到的那次请求加密后的参数每次都不同是因为timestamp 变了但密钥始终是同一套。有人喜欢自作聪明每次换随机 AES key导致 encSecKey 解密后对不上号接口直接返回参数错误。PKCS7 补位必须用 chr(pad) 填充而不是补零。很多从 Java 转过来的开发者习惯 ZeroPadding在 AES-CBC 模式下会得到完全不同的密文接口侧解不出来。RSA 加密的对象是 AES 密钥的原始字符串不是 base64 之后的 params 密文。这一点非常容易搞反逻辑上想清楚服务端用私钥解出 AES 密钥再用 AES 密钥解 params所以 RSA 加密的必须是 AES_KEY 本身。3.2 请求发起与响应解析从 JSON 到可用直链加密参数构造好之后剩下的就是 POST 请求和结果处理。这里有一个容易忽略的点播放接口验证的是完整请求头包括 UA、Referer、Cookie 三件套。ENDPOINT https://api.example.com/api/song/player/url # 占位地址按实际环境填充 def resolve_song(song_id: int, cookie: str, level: str standard) - dict: headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://music.example.com/, Cookie: cookie, Content-Type: application/x-www-form-urlencoded, } params { id: song_id, level: level, timestamp: int(time.time() * 1000), csrf_token: , # 部分会话需要从 cookie 中提取 } payload build_weapi_payload(params) try: resp requests.post(ENDPOINT, datapayload, headersheaders, timeout10) resp.raise_for_status() js resp.json() except requests.exceptions.Timeout: return {song_id: song_id, url: None, reason: 请求超时} except Exception as e: return {song_id: song_id, url: None, reason: f请求异常: {e}} # 接口业务码为 200 才代表成功和 HTTP 状态码是两回事 if js.get(code) ! 200: return {song_id: song_id, url: None, reason: f接口错误 code{js.get(code)}} data_item (js.get(data) or [{}])[0] url data_item.get(url) if not url: return {song_id: song_id, url: None, reason: 无直链可能为 VIP 歌曲或 cookie 失效} return { song_id: song_id, url: url, br: data_item.get(br), size: data_item.get(size), }逻辑说明resp.raise_for_status() 只检查 HTTP 层状态而播放接口即使业务失败也经常返回 HTTP 200所以真正要判断的是 JSON 里的 code 字段。url 为空不等价于报错它更像是业务层的“拒绝”原因可能是版权限制、VIP 限制或者 cookie 匿名状态代码里把它们归一化成 reason 字段方便后续批量处理时统一归类。timeout 设成 10 秒是经验值。解析接口本身响应快但如果你同时跑批量任务单次请求的耗时会被串行拖长超时设太短会导致大量假失败设太长又会让卡死的请求占住线程。我一般会把网络超时控制在 8 到 12 秒之间重试逻辑单独处理。csrf_token 字段不是所有版本都需要。网页端登录后的 cookie 里通常带着它而服务端校验时也分严松。第一次调试时可以留空如果返回 code400 再补不要一开始就把所有页面字段都堆上去那样反而更容易因为字段格式不一致被拦截。4. 批量落地与配置管理并发控制、失败重试和 cookie 生命周期单曲解析跑通了批量才是真正拉开差距的地方。拿到几百个歌曲 id 后直接 for 循环逐条请求速度慢且不说高频请求会在十几分钟内触发限流。工程化的核心是三个词限速、重试、可配置。4.1 批量解析的工程化流程队列、线程池与失败重试批量任务的输入通常是歌单 id先调歌单详情接口拿到所有歌曲 id 列表再逐条走解析流程。这一节只看批量调度部分。import threading import random import time from concurrent.futures import ThreadPoolExecutor def batch_resolve(song_ids: list, cookie: str, workers: int 5, max_retry: int 3): results [] lock threading.Lock() def worker(song_id): for attempt in range(max_retry): try: # 每条任务前加随机延迟降低同一窗口内的请求密度 time.sleep(random.uniform(0.5, 1.5)) result resolve_song(song_id, cookie) with lock: results.append(result) return except Exception: if attempt max_retry - 1: time.sleep(1 attempt) # 1s、2s、4s 指数退避 else: with lock: results.append({song_id: song_id, url: None, reason: 重试仍失败}) with ThreadPoolExecutor(max_workersworkers) as pool: list(pool.map(worker, song_ids)) return results参数说明workers 默认 5这是我在实际跑批里验证过的相对安全值。并发太高会让单 IP 在短时间内的请求密度爆表接口开始随机断开连接并发太低又体现不出批量优势。如果你有质量好的代理池workers 可以放到 10 到 15但每线程内的随机延迟不建议去掉。time.sleep(random.uniform(0.5, 1.5)) 这一行的作用不只是礼貌它能把请求分布打散避免线程启动瞬间产生扎堆现象。实测中8 个线程不加延迟批量解析 500 首歌大约跑到第 300 首时开始出现连续 403加了 0.5 到 1.5 秒随机延迟后同样配置能稳定跑完全量。pool.map 在这里替代 submit 逐个提交好处是主线程会等待所有任务结束再返回不需要额外维护 Future 列表。结果写入时用 lock 保护因为多个线程同时 append 到同一个 list 会产生竞态条件最终结果数量对不上。4.2 配置管理与 cookie 生命周期把凭证和限速挪到外部批量脚本最忌讳把 cookie、并发数、超时时间写死在代码里。换一个账号、换一台服务器部署就得改代码重新发版这不是工程化的做法。我一般会把可变参数抽到 config 文件启动时加载。配置项作用推荐值cookie登录态凭证从浏览器抓取不提交到代码库user_agent与 cookie 绑定和抓包时保持一致max_workers并发线程数5request_interval单任务随机延迟区间0.5~1.5 秒max_retry单任务重试次数3timeout单次请求超时10 秒output_path结果输出目录./outputcookie 的生命周期管理是最容易被忽视的一环。网页端登录态通常能保持较长时间但一旦触发风控或主动退出cookie 会静默失效。脚本不会收到明确报错而是表现为 url 字段开始大面积为空。所以我在批量任务启动前会加一个“预检”步骤随便拿一个已知可解析的歌曲 id 调一次接口确认能返回有效 url再开始跑正式队列。常见做法是把 cookie 做成失效自动降级检测到 url 为空的比例超过阈值时自动切换到匿名请求模式虽然拿不到高音质直链但至少让下载队列保持运行状态。同时把告警信息写进日志提醒操作者重新抓取 cookie。这个策略用在长时间无人值守的任务里非常实用。另一个容易被忽略的点是输出结果的落盘格式。批量解析结果不要只保留 url建议把 song_id、br、size、reason 一并记录后续排查问题时能直接定位是哪一类失败。存成 JSON Lines 格式比较合适每行一条记录追加写也不会产生文件锁冲突。5. 直链解析避坑指南五类高频报错与处理方式直链解析的报错有一个共同特点HTTP 状态码基本是 200业务层面的错误全部隐藏在 JSON 的 code 字段或 url 空值里。我用五类高频问题还原最常见的翻车现场。5.1 突然收到 401 或 403cookie 会话失效与请求头不一致现象批量任务运行到一半接口开始返回 401 或 403响应体不是 JSON 而是一段错误页面。原因cookie 过期或者被风控标记另外程序里设置的 User-Agent 与抓包时不一致导致服务端判定会话异常。解决先检查 cookie 是否还能在浏览器中正常播放歌曲不能则重新抓取。代码里把 UA 固定成和浏览器完全一致的字符串不要用 requests 默认的 python-requests 标识。Referer 也要补上播放页地址有些校验逻辑对 Referer 缺失很敏感。5.2 接口 code 是 200但 url 字段为空VIP 限制与版权拦截现象解析流程走完code 返回 200data 数组里只有一个空对象没有报错信息。原因歌曲属于 VIP 专享或版权受限当前账号没有播放权限。匿名 cookie 解析普通歌能成功解析付费曲目就返回空。解决确认账号类型VIP 歌单需要对应 cookie解析结果里把无 url 的条目单独标记成“无权限”不占用重试次数。如果批量任务里混着大量 VIP 歌曲优先用 VIP 账号的 cookie 才能拿到完整结果。5.3 params 或 encSecKey 拼接错误接口返回参数错误现象请求发出后返回 code400提示参数错误响应体里没有更多细节。原因AES 补位用了零填充而不是 PKCS7RSA 加密后没做 base64 编码更隐蔽的是把 AES 密钥随机生成了导致服务端解密不出原始参数结构。解决写一个自检函数解密刚刚生成的 params确认能和原始 JSON 对上encSecKey 字段检查固定长度它在标准实现下是一个 256 字符的 base64 字符串。把这两个常量的校验写进测试用例每次改代码后先跑一遍再上批量。5.4 批量跑到一半整体断连单 IP 高频请求触发限流现象前 200 首全部正常第 250 首开始出现连续的连接被重置、超时过了几分钟又自己恢复。原因同一 IP 在短时间内的请求频率超过平台阈值风控策略开始临时断开服务。解决并发数控制在 5 以内每条线程加随机延迟。更稳妥的做法是按批次处理每 50 首做一次心跳探测如果连续三个请求都失败主动暂停 60 秒等待风控解除。代理池可用时优先走代理但代理质量差反而会引入更多超时双刃剑需要测试筛选。5.5 直链拿到手却下载失败CDN 防盗链与签名时效现象解析返回的 url 在浏览器里打开正常但用 requests 下载时返回 403 或 416。原因直链是 CDN 签名地址校验 Referer、UA 和签名时间。程序下载时没有携带解析阶段使用的请求头或者链接已经过期签名失效。解决下载请求复用解析时的 headers 三件套下载前先做一次 HEAD 预检确认返回状态和 content-length 都正常再发起完整下载。链接的签名时效一般是几十分钟量级解析完不要长时间挂机再去下载尽量解析一批下载一批。6. 进阶技巧直链健康检查、缓存策略与只做上传的解析服务单曲解析和批量调度都跑通之后最后一层是把脚本从“能用”推到“好用”。这里分享三个我常用的升级技巧。直链健康检查是下载前必要的一步。直链不是永久的解析完到真正下载之间可能隔着几十分钟直接 GET 下载可能会在中途遇到签名过期导致断流。比较稳妥的做法是先发一个 HEAD 请求确认状态码和文件大小都正常再启动下载。def preflight(url: str, headers: dict) - bool: try: resp requests.head(url, headersheaders, timeout8, allow_redirectsTrue) return resp.status_code 200 and int(resp.headers.get(content-length, 0)) 0 except Exception: return FalseHEAD 请求极少数情况下会被服务端拒绝可以在脚本里留一个 GET 带 Range: bytes0-0 的兜底逻辑能拿到 206 Partial Content 说明连接可用。从那以后我每次部署解析服务都强制按这个习惯走一遍。预检无效就重新解析链接而不是等下载失败再去补重复解析的成本远低于断流重传的成本。缓存策略解决的是直链复用问题。同一个文件删除后重新解析会消耗配额把解析结果按 song_id 缓存下来在有效期内直接复用能显著降低接口请求压力。缓存表结构只需要 song_id、url、br、expire_at 四列每次返回前检查过期时间提前五分钟视为失效并触发重新解析。我用 SQLite 存几千条历史解析记录查询耗时在毫秒级。最后一种场景是把解析能力封装成内部服务只对外暴露接口。用 FastAPI 写一个极薄的包装层输入歌曲 id 列表输出解析结果列表。cookie 和加密逻辑全部留在服务端不影响调用方。好处是后续新增音质等级、切换 cookie、接入缓存调用方完全无感知。from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class ResolveRequest(BaseModel): ids: list[int] level: str standard app.post(/resolve) def resolve_batch(req: ResolveRequest): return batch_resolve(req.ids, cookieread_cookie_from_config(), levelreq.level)这个阶段最深刻的教训是不要相信一个没有预检的直链也不要相信一个没有过期时间的缓存。从那以后我每次部署前都强制跑一遍小批量预检、确认 cookie 未失效再放开并发直链全部经过 HEAD 验证才交给下载队列cookie 落配置中心而不是写死在代码里。希望帮到你。本文还有配套的精品资源点击获取