简介:本资源为基于Python的网易云音乐API设计与实现源码,面向具备一定Python基础、希望学习接口设计与后端开发的开发者及音乐技术爱好者。项目以Python为核心,结合HTML、JavaScript与CSS,完整呈现从数据请求、处理到响应输出的API实现路径,可用于二次开发或课程实践参考。压缩包共89个文件,约134KB,其中65个py脚本构成核心逻辑,13个md文档提供说明与接口梳理,另含html页面、css样式、js脚本及txt配置等辅助文件,目录按功能模块划分,结构清晰便于检索。目前已有376人学习下载。读者可从中获取一套可运行的API源码,理解网易云音乐相关接口的模块组织方式、请求加密与响应处理思路,并借助文档快速定位歌曲、歌单、评论、用户等业务模块的实现细节,适合作为接口开发与项目结构学习的实践素材。
1. 从一份 Python 网易云音乐 API 源码说起:它到底能解决什么
很多人第一次想拿网易云音乐的数据,都是打开浏览器 F12 抓包,看到一堆加密参数直接懵掉。weapi、eapi、linuxapi 三套接口,AES 加密、RSA 加密、MD5 摘要混在一起,请求体还是十六进制拼接,手动拼一次能对,换个接口就翻车。这份基于 Python 的网易云音乐 API 设计与实现源码,干的就是把这一整套加密链路封装成可调用的函数,让你用requests发一个普通 POST 就能拿到歌单、歌曲 URL、歌词、评论、搜索这些数据。
它适合两类人:一类是正在做课程设计、毕业设计,需要一个完整可跑通的 Python 后端项目当参考;另一类是写爬虫或做音乐管理工具,不想每次都重新逆向加密逻辑,直接拿现成的 API 层来用。源码本身是 Python 实现,不依赖浏览器自动化,纯 HTTP 请求,跑起来轻量,Linux 和 Windows 都能部署。核心价值不在于它有多少接口,而在于它把加密参数生成、请求构造、响应解析这条链路拆得清楚,你能看懂每一步在干什么,而不是面对一个黑匣子。
2. 加密参数怎么来的:weapi 与 eapi 的请求构造逻辑
2.1 两套接口体系的差异与选型
网易云音乐的接口分几个版本,最常用的是 weapi 和 eapi。weapi 走的是https://music.163.com/weapi/路径,参数经过两次 AES 加密再拼接;eapi 走https://interface.music.163.com/eapi/,用的是 AES-ECB 加密加 MD5 摘要,请求体格式也不一样。源码里一般会同时实现这两套,因为不同接口只开放其中一种。比如获取歌曲播放地址,weapi 的/weapi/song/enhance/player/url在部分场景下返回的 URL 可能为空,需要换 eapi 的/eapi/song/enhance/player/url再试。
选型上有个经验:搜索、歌单详情、歌词这类接口用 weapi 就够了,稳定且参数简单;播放地址、评论分页这类对风控敏感的接口,eapi 的成功率更高。源码里通常会把两套加密函数分开写,调用时按接口类型切换。
2.2 weapi 加密的 Python 实现
weapi 的核心是两次 AES-CBC 加密。第一次用固定密钥0CoJUm6Qyw8W8jud加密明文,得到中间密文;第二次用随机生成的 16 位密钥加密中间密文。同时还要用 RSA 公钥加密这个随机密钥,最终请求体里带params和encSecKey两个字段。
import os import base64 import json from Crypto.Cipher import AES from Crypto.Util.Padding import pad # 固定密钥,网易云 weapi 第一层加密用 MODULUS = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7" PUB_KEY = "010001" NONCE = "0CoJUm6Qyw8W8jud" def aes_encrypt(text, key): """AES-CBC 加密,key 为 16 位字符串""" iv = b"0102030405060708" cipher = AES.new(key.encode(), AES.MODE_CBC, iv) padded = pad(text.encode(), AES.block_size) encrypted = cipher.encrypt(padded) return base64.b64encode(encrypted).decode() def rsa_encrypt(text, pubkey, modulus): """RSA 加密,返回十六进制字符串""" text = text[::-1] rs = pow(int(text.encode().hex(), 16), int(pubkey, 16), int(modulus, 16)) return format(rs, "x").zfill(256) def weapi_encrypt(data): """weapi 完整加密流程""" text = json.dumps(data) # 第一次加密,用固定密钥 first = aes_encrypt(text, NONCE) # 生成随机 16 位密钥 secret = "".join([chr(ord('a') + i % 26) for i in range(16)]) # 第二次加密,用随机密钥 params = aes_encrypt(first, secret) # RSA 加密随机密钥 enc_sec_key = rsa_encrypt(secret, PUB_KEY, MODULUS) return {"params": params, "encSecKey": enc_sec_key}这段代码里几个参数要特别注意。NONCE是固定的,不能改,改了服务端解不出来。iv也是固定的0102030405060708,这是网易云前端写死的。secret随机密钥理论上可以任意 16 位字符串,但源码里一般用字母表循环生成,保证每次请求不同即可。rsa_encrypt里先反转字符串再转十六进制,这是网易云特有的处理,顺序错了加密结果就不对。
2.3 eapi 加密的差异点
eapi 的加密逻辑和 weapi 不同。它用的是 AES-ECB 模式,密钥是e82ckenh8dichen8,加密后直接转十六进制大写,不需要 RSA 那一步。请求体格式是params=十六进制字符串,放在 POST body 里。
from Crypto.Cipher import AES from Crypto.Util.Padding import pad EAPI_KEY = "e82ckenh8dichen8" def eapi_encrypt(url, data): """eapi 加密,url 参与摘要计算""" text = json.dumps(data) # 构造摘要消息 message = f"nobody{url}use{text}md5forencrypt" digest = hashlib.md5(message.encode()).hexdigest() # 拼接加密明文 plain = f"{url}-36cd479b6b5-{text}-36cd479b6b5-{digest}" cipher = AES.new(EAPI_KEY.encode(), AES.MODE_ECB) padded = pad(plain.encode(), AES.block_size) encrypted = cipher.encrypt(padded) return encrypted.hex().upper()这里36cd479b6b5是固定分隔符,nobody和use、md5forencrypt也是固定拼接词。摘要计算时 URL 和请求体都要参与,少一个服务端就返回错误。eapi 的返回结果有时是加密的,需要再用同样的密钥解密,源码里一般会封装一个eapi_decrypt函数处理响应。
提示:AES 加密的 padding 方式必须是 PKCS7,Python 的
Crypto.Util.Padding.pad默认就是,不用手动改。如果用的是pycryptodome库,导入路径是Crypto.Cipher.AES,不是Cryptodome。
3. 从请求到数据:接口封装与响应解析的工程化处理
3.1 请求会话与公共参数注入
源码里不会每次请求都重新建连接,通常用一个requests.Session保持会话,同时把公共参数(如csrf_token、cookie)统一注入。网易云很多接口需要登录态,匿名请求只能拿到部分数据。源码一般会预留cookie配置项,你从浏览器登录后复制 cookie 填进去即可。
import requests class NeteaseAPI: def __init__(self, cookie=None): self.session = requests.Session() self.session.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": "https://music.163.com/", "Content-Type": "application/x-www-form-urlencoded", }) if cookie: self.session.headers["Cookie"] = cookie def request_weapi(self, url, data): """发送 weapi 请求""" encrypted = weapi_encrypt(data) resp = self.session.post( f"https://music.163.com/weapi{url}", data=encrypted, timeout=10 ) return resp.json() def request_eapi(self, url, data): """发送 eapi 请求""" params = eapi_encrypt(url, data) resp = self.session.post( "https://interface.music.163.com/eapi" + url, data={"params": params}, timeout=10 ) return resp.json()timeout建议设 10 秒,网易云接口偶尔会慢,但不至于超过 10 秒。Referer必须带,否则部分接口返回 403。Content-Type用表单格式,不要用 JSON,因为加密后的params是作为表单字段传的。
3.2 常用接口的调用示例
源码里一般会封装几个高频接口:搜索、歌曲详情、播放地址、歌词、评论。以搜索和播放地址为例,调用方式如下。
api = NeteaseAPI(cookie="你的cookie") # 搜索歌曲 result = api.request_weapi("/search/get/", { "s": "海阔天空", "type": 1, "limit": 10, "offset": 0 }) for song in result["result"]["songs"]: print(song["id"], song["name"], song["ar"][0]["name"]) # 获取播放地址(eapi) song_id = "347230" url_data = api.request_eapi("/song/enhance/player/url", { "ids": f"[{song_id}]", "br": 320000 }) print(url_data["data"][0]["url"])搜索接口的type参数:1 是单曲,10 是专辑,100 是歌手,1000 是歌单。limit和offset控制分页,offset是偏移量不是页码。播放地址接口的br是比特率,常见值 128000、192000、320000,但实际返回的 URL 码率取决于账号权限,普通账号拿不到 320k 的链接。
3.3 响应数据的清洗与字段映射
网易云返回的 JSON 字段名比较随意,比如歌手字段是ar不是artists,专辑是al不是album。源码里一般会做一层字段映射,把原始响应转成统一格式,方便上层业务调用。
def parse_song(raw): """把原始歌曲数据转成统一格式""" return { "id": raw["id"], "name": raw["name"], "artists": [a["name"] for a in raw.get("ar", [])], "album": raw.get("al", {}).get("name", ""), "duration": raw.get("dt", 0) // 1000, "fee": raw.get("fee", 0), # 0 免费,1 会员,4 付费专辑 }fee字段很关键,0 表示免费可播,1 表示需要会员,4 表示数字专辑需要购买。如果你做的是下载工具,这个字段决定了能不能拿到真实播放地址。dt是毫秒,转成秒更直观。
注意:评论接口分页有坑,
offset超过一定值后返回空列表,不是接口坏了,是服务端限制。常见做法是翻到空就停,不要死循环。
4. 避坑与排查:跑这份源码时最容易翻车的五个地方
4.1 加密库版本不兼容导致Crypto导入失败
现象:运行时报ModuleNotFoundError: No module named 'Crypto',或者ImportError: cannot import name 'AES'。
原因:Python 的加密库有两个常见包,pycrypto和pycryptodome。pycrypto已经停止维护,在 Python 3.10 以上装不上。很多人直接pip install crypto,装的是一个无关的空包。
解决:卸载冲突包,装pycryptodome。命令是pip uninstall crypto pycrypto -y然后pip install pycryptodome。装完后导入路径仍然是from Crypto.Cipher import AES,不要改成Cryptodome。
4.2 请求返回{"code": 400}或参数错误
现象:接口返回code: 400,提示params error或encrypt error。
原因:weapi 加密时secret随机密钥长度不对,或者 RSA 加密时字符串反转步骤漏了。eapi 加密时 URL 和请求体拼接顺序错了,或者 MD5 摘要没算对。
解决:先检查secret是不是严格 16 位,再检查rsa_encrypt里有没有text[::-1]。eapi 的话,把url、text、digest三部分打印出来,对照url-36cd479b6b5-text-36cd479b6b5-digest这个格式逐字比对。分隔符是36cd479b6b5,不是36cd479b6b。
4.3 播放地址返回null
现象:调用播放地址接口,data[0].url是null,但歌曲信息正常。
原因:三种可能。一是歌曲需要会员,fee字段是 1;二是版权限制,该歌曲在接口层面不对外放 URL;三是br参数设太高,账号权限不够。
解决:先看fee字段,如果是 1 就换免费歌曲测试。如果fee是 0 还是 null,把br降到 128000 再试。如果都不行,换 eapi 接口重试,weapi 和 eapi 的放行策略不完全一样。
4.4 Cookie 过期导致登录态失效
现象:之前能拿到的数据,过一段时间后返回code: 301或要求登录。
原因:网易云 cookie 有有效期,一般几天到几周不等。源码里如果硬编码了 cookie,过期后所有需要登录的接口都会失败。
解决:不要把 cookie 写死在代码里,放到配置文件或环境变量。过期后重新从浏览器复制。常见做法是写一个check_login函数,启动时先调一次用户信息接口,返回code: 200才继续。
4.5 高频请求触发风控
现象:连续请求几十次后,接口开始返回code: 406或直接超时。
原因:网易云对同一 IP 的请求频率有阈值,短时间大量请求会被临时限制。
解决:在请求之间加time.sleep(0.5)到1秒的间隔。如果是批量任务,用队列控制并发数,不要开几十个线程同时打。源码里一般会预留一个delay参数,默认 0.5 秒,批量场景调到 1 秒以上更稳。
5. 进阶用法:把 API 封装成可复用的命令行工具
源码跑通之后,下一步是把它变成一个随手能用的工具。我一般会在 API 层之上再包一层 CLI,用argparse接收参数,支持搜索、下载、导出歌单三种模式。这样不用每次开 Python 交互环境,终端一行命令就能出结果。
import argparse import json import os def cmd_search(api, keyword, limit): result = api.request_weapi("/search/get/", { "s": keyword, "type": 1, "limit": limit, "offset": 0 }) for song in result["result"]["songs"]: parsed = parse_song(song) print(f"{parsed['id']} {parsed['name']} {'/'.join(parsed['artists'])}") def cmd_download(api, song_id, output_dir): data = api.request_eapi("/song/enhance/player/url", { "ids": f"[{song_id}]", "br": 320000 }) url = data["data"][0]["url"] if not url: print(f"歌曲 {song_id} 无可用播放地址") return resp = api.session.get(url, timeout=30) path = os.path.join(output_dir, f"{song_id}.mp3") with open(path, "wb") as f: f.write(resp.content) print(f"已保存到 {path}") if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("action", choices=["search", "download"]) parser.add_argument("keyword") parser.add_argument("--limit", type=int, default=10) parser.add_argument("--output", default="./downloads") args = parser.parse_args() api = NeteaseAPI(cookie=os.environ.get("NETEASE_COOKIE")) if args.action == "search": cmd_search(api, args.keyword, args.limit) elif args.action == "download": os.makedirs(args.output, exist_ok=True) cmd_download(api, args.keyword, args.output)这个 CLI 有几个设计点值得说。cookie 从环境变量读,不写进代码,避免泄露。下载时先判断url是否为空,空就直接返回,不要写空文件。timeout设 30 秒,因为音频文件可能几 MB,网络慢的时候 10 秒不够。输出目录用os.makedirs(exist_ok=True),重复运行不会报错。
验证工具是否正常,可以按这个顺序走一遍:先python cli.py search 海阔天空,看能不能列出歌曲;再python cli.py download 347230 --output ./test,看文件是否下载成功且能播放。如果搜索正常但下载失败,大概率是 cookie 权限或歌曲fee字段的问题,回到第 4 章排查。
还有一个技巧:把常用歌单的 ID 存到一个 JSON 文件里,写一个export_playlist函数批量导出歌单内所有歌曲信息到 CSV。这样课程设计里做数据展示时,直接读 CSV 就行,不用每次调接口。歌单 ID 从分享链接里拿,id=后面的数字就是。
从那以后我每次拿到新的 API 源码,都强制先跑一遍「搜索 → 详情 → 播放地址」这条最小链路,确认加密和 cookie 都没问题,再往上叠业务逻辑。希望帮到你。
本文还有配套的精品资源,点击获取