返回博客

网易云音乐第三方库分页被锁死,绕过 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 加密请求体 + __csrf cookie 签名,改起来工程量不小

根因

网易云音乐现代 API(/weapi/ 系列)对请求体做了两层加密:

  1. 随机生成 16 字节 AES key
  2. 用 AES-CBC 加密真实 params,再用平台 RSA 公钥加密这把 AES key
  3. 最终 POST params=<base64> + encSecKey=<hex> + __csrf=<cookie>

这套机制来自客户端 JS 里的 asrsea() 函数,目的是防止简单爬取。封装库封了这一层,但封装时没暴露 offset——于是分页能力被彻底封死在库的接口层。

坑:第三方库 limit 和 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 results

fee 字段预过滤

搜索结果里每首歌都带 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 走封装库——各司其职。

正解:legacy /api/ 明文分页 + 封装库处理下载 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 为了兼容老客户端往往不加密、参数更全,是绕过封装层的最低成本出口。