网易云音乐第三方库分页被锁死,绕过 weapi 加密用 legacy 接口救场
我在做一个音乐数据抓取工具,需要对某个关键词分页搜索几百首曲目。用的是社区里一个现成的 Python 封装库,API 长这样:
search_music(keywords, cookies, limit)翻遍文档和源码,没有 offset 参数。结果无论把 limit 设多大,搜索结果在 300 条附近死死卡住——跑不到更深的页面。
现象
- 调用
search_music(query, cookies, 500)返回的列表始终在 ~300 条截断 - 试图手动循环调用同一关键词的
search_music,得到的是同一批重复结果 - 想直接 fork 库改源码,发现内部走的是
/weapi/search/get端点,需要 RSA 加密随机 AES key + AES-CBC 加密请求体 +__csrfcookie 签名,改起来工程量不小
根因
网易云音乐现代 API(/weapi/ 系列)对请求体做了两层加密:
- 随机生成 16 字节 AES key
- 用 AES-CBC 加密真实 params,再用平台 RSA 公钥加密这把 AES key
- 最终 POST
params=<base64>+encSecKey=<hex>+__csrf=<cookie>
这套机制来自客户端 JS 里的 asrsea() 函数,目的是防止简单爬取。封装库封了这一层,但封装时没暴露 offset——于是分页能力被彻底封死在库的接口层。
只调大 limit 并不能突破结果上限——网易云服务端对单次请求有自己的 cap(约 300 条)。要拿更多结果必须靠 offset 翻页,而封装库如果不暴露该参数就无路可走。改库源码又要重新实现 weapi 加密链,工程成本极高。
正解
网易云音乐保留了一套 legacy /api/ 端点,供老版移动客户端和第三方播放器使用,接受明文 POST form data,不需要任何加密,也支持 offset:
import requests
SEARCH_URL = "https://music.163.com/api/search/get/web"
HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36"
),
"Referer": "https://music.163.com/",
"Origin": "https://music.163.com",
}
session = requests.Session()
resp = session.post(
SEARCH_URL,
data={"s": "关键词", "type": 1, "limit": 30, "offset": 60}, # 第 3 页
headers=HEADERS,
timeout=15,
)
songs = resp.json()["result"]["songs"]type=1 是歌曲,其他值:10 专辑、100 歌手、1000 歌单。匿名请求(无 cookie)即可搜索。
分页循环
def paginated_search(query: str, target: int = 500, page_size: int = 30):
session = requests.Session()
session.headers.update({
"User-Agent": "Mozilla/5.0 ...",
"Referer": "https://music.163.com/",
})
seen: set[str] = set()
results: list[dict] = []
for page in range(target // page_size + 1):
resp = session.post(
"https://music.163.com/api/search/get/web",
data={"s": query, "type": 1, "limit": page_size, "offset": page * page_size},
timeout=15,
)
songs = resp.json().get("result", {}).get("songs") or []
if not songs:
break
for s in songs:
key = s["name"].lower().strip()
if key not in seen:
seen.add(key)
results.append(s)
if len(results) >= target:
return results
return resultsfee 字段预过滤
搜索结果里每首歌都带 fee 字段,在拿下载 URL 之前先过滤能省很多无效请求:
| fee | 含义 | 匿名可拿 URL | |-----|------|-------------| | 0 | 免费 | 可以 | | 1 | VIP 专享 | 不行(只有 30 秒试听) | | 8 | 数字专辑单独购买 | 不行 |
下载 URL(/weapi/song/enhance/player/url/v1)确实需要加密和 cookie,这部分继续交给原封装库的 url_v1(song_id, level, cookies) 处理即可。混合模式:自己写搜索/分页走 legacy,下载 URL 走封装库——各司其职。
POST https://music.163.com/api/search/get/web 接受明文 {s, type, limit, offset},无加密,匿名可用,rate limit 约 2 req/s(超出 IP 会被限流 30 分钟)。同端点族还有 /api/song/detail、/api/playlist/detail 可无 cookie 读取元数据。
一句话外卖
第三方封装库缺能力时,先看原始平台有没有更老、更简单的端点——legacy API 为了兼容老客户端往往不加密、参数更全,是绕过封装层的最低成本出口。