Python爬虫实战:逆向网易云音乐加密接口实现歌单下载
1. 项目概述与核心价值
最近在技术社区和论坛里,经常看到有朋友在问,怎么才能把自己网易云音乐歌单里的歌“搬”到本地来。无论是出于备份收藏、离线收听,还是为了做一些音乐数据分析,这都算是一个挺普遍的需求。但稍微尝试过的朋友都知道,这事儿没那么简单。直接去网页上找下载按钮?不存在的。官方客户端下载的歌曲是加密格式,只能在自家App里播放。于是,很多人的目光就投向了爬虫技术。
这个项目,就是针对这个具体需求的一次深度实战。它不是一个泛泛而谈的“爬虫入门”,而是聚焦于“破解网易云音乐加密接口”这个核心难点,最终实现批量、自动化地下载标准MP3音频文件。整个过程,你会接触到现代Web应用常见的反爬策略,特别是参数加密和动态令牌,这比简单的静态页面抓取要硬核得多。对于已经掌握了Python和Requests库基础,想要挑战更复杂、更贴近真实生产环境爬虫的朋友来说,这是一个绝佳的练手项目。
通过这个项目,你不仅能学会如何分析一个复杂Ajax接口的调用链,逆向其核心加密逻辑,还能掌握一套应对类似加密接口的通用排查和破解思路。更重要的是,我会附上完整、可运行的代码,并详细解释每一行代码背后的意图和可能遇到的坑,让你不仅能“跑起来”,更能“懂得为什么这么写”。无论是为了技术学习,还是解决实际需求,这篇文章都将提供一条清晰的路径。
2. 核心思路与技术选型解析
2.1 为什么不能直接抓取音频链接?
很多新手的第一反应是:打开浏览器开发者工具(F12),在“Network”(网络)标签页里找.mp3或.m4a的请求。如果你真的去网易云音乐的播放页试过,会发现根本找不到这样的直接链接。这是因为网易云音乐对音频资源采取了严格的保护措施:
- 动态加密链接:音频文件的真实URL不是静态的,它由服务器动态生成,并且包含了时间戳、用户令牌(如果登录)、歌曲ID以及最重要的——一个经过加密算法计算的参数(常被命名为
params,encSecKey等)。这个链接通常有效期很短,过期即失效。 - 核心参数加密:用于请求音频链接或歌曲详情的关键参数(如歌曲ID列表)在发送前会被一套复杂的加密算法处理。你无法直接构造一个明文请求。
- 反爬虫机制:除了加密,还可能存在请求头校验、频率限制、行为验证等常见反爬手段。
因此,我们的核心任务从“找链接”变成了“模拟加密过程,成功向官方接口请求到解密后的音频真实地址”。
2.2 整体技术路线图
我们的技术路线非常明确,分为四个核心阶段:
- 接口定位与分析:找到负责提供歌曲详情和音频链接的那个“关键”API接口。
- 加密逻辑逆向:破解该接口请求参数(特别是
params和encSecKey)的生成算法。这是本项目最核心、最具挑战性的部分。 - 请求模拟与数据解析:用Python代码模拟浏览器,携带正确的加密参数去请求接口,并解析返回的JSON数据,提取出音频的真实URL。
- 音频下载与存储:根据获取的真实URL,下载音频二进制流,并保存为本地MP3文件,实现批量处理。
2.3 工具与库选型
工欲善其事,必先利其器。以下是本项目所需的核心Python库及其选型理由:
requests:HTTP库的不二之选。比内置的urllib更简洁易用,会话保持、请求头设置、代理支持等功能完善。我们将用它来发送所有网络请求。json:Python标准库,用于解析接口返回的JSON数据。re(正则表达式):标准库,在分析网页源码或JavaScript文件时,用于快速提取关键的函数定义或密钥。hashlib:标准库,用于计算MD5、SHA等哈希值。某些加密步骤可能会用到。Crypto(或pycryptodome):这是一个关键库。因为网易云音乐的加密算法很可能涉及AES、RSA等标准加密算法。pycryptodome是PyCrypto的一个流行分支,维护更活跃,功能齐全。我们将用它来实现AES加密、CBC模式、PKCS7填充等操作。注意:安装时使用
pip install pycryptodome。如果遇到与旧版Crypto模块的命名冲突,可能需要先卸载pycrypto。os/pathlib:标准库,用于创建本地目录、保存文件,处理文件路径。
为什么不选用Scrapy?Scrapy是一个强大的异步爬虫框架,适合大规模、结构固定的网站爬取。但本项目核心在于对单个复杂加密接口的逆向分析与破解,属于“攻坚”型任务,使用轻量级的requests库配合手动分析更加灵活、直观,也更利于学习和理解底层原理。在成功破解加密逻辑后,你可以很容易地将这套逻辑移植到Scrapy的下载器中间件中,以实现更高性能的批量爬取。
3. 逆向工程:定位与破解加密接口
这是整个项目最硬核的部分,需要一些耐心和技巧。我们主要依靠浏览器的开发者工具。
3.1 定位关键API接口
- 打开目标页面:在Chrome或Edge浏览器中,打开网易云音乐官网,并进入一个具体的歌曲播放页面,例如某首歌曲的详情页。
- 开启开发者工具:按F12,切换到Network(网络)标签页。务必勾选上 “Preserve log”(保留日志),以防止页面跳转时请求记录被清空。
- 触发数据请求:在页面中,点击播放按钮,或者查看歌曲详情。同时,仔细观察Network面板中新增的请求。
- 筛选与查找:在请求列表上方的筛选框内,输入关键词如
song、detail、player、url、music等。同时,将请求类型XHR/Fetch选中,因为现代Web应用的数据交互大多通过这类Ajax请求完成。 - 识别目标接口:在众多请求中,寻找一个看起来像是获取歌曲信息或播放地址的请求。它的Name可能包含
weapi/song/detail、weapi/song/enhance/player/url、api/song/url等路径。点击这个请求,查看它的Headers(标头)和Payload(负载)。
通常,weapi/song/enhance/player/url这个接口就是用来获取歌曲播放链接的。你会看到它的Request Method是POST,并且Query String Parameters或Form Data里有一堆看起来像乱码的参数,最主要的就是params和encSecKey。这就是我们需要攻破的堡垒。
3.2 分析加密参数生成逻辑
找到目标接口后,点击它,查看Payload标签(如果是POST请求)。你会看到类似这样的数据:
params: x1B2c3D4e5F6... (很长一串密文) encSecKey: y7G8h9I0j1K2... (很长一串密文)我们的目标是弄明白这两个字符串是怎么生成的。它们肯定是在浏览器端的JavaScript代码里计算出来的。
- 全局搜索:在开发者工具的Sources(源代码)标签页,或者直接在所有已加载的JS文件中(可以通过Network标签页筛选
js文件后查看),使用搜索功能(Ctrl+Shift+F),搜索params:或encSecKey:或encSecKey =。 - 查找加密函数:搜索后,你很可能会定位到一段JS代码,其中有一个函数,它接收一些明文参数(比如歌曲ID
ids),然后经过一系列处理,最终生成了params和encSecKey。这个函数的名字可能叫window.asrsea、encrypt或其它。 - 关键线索:固定公钥:在分析网易云音乐加密的众多资料中,一个被广泛验证的结论是,其加密核心采用了一套RSA公钥加密结合AES加密的模式。并且,RSA的公钥是固定不变的。这大大简化了我们的逆向工作。你通常可以在JS代码中找到类似这样的公钥字符串:
以及一个固定的AES密钥(如-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBGLbCiK45wIDAQAB -----END PUBLIC KEY-----0CoJUm6Qyw8W8jud)和初始化向量IV(如0102030405060708)。 - 理解加密流程:典型的加密流程(简化版)可能是: a. 将请求的明文数据(如
{"ids":"[123456]","level":"standard"})先进行一次AES加密,密钥是0CoJUm6Qyw8W8jud。 b. 将上一步得到的密文,再进行一次AES加密,但这次使用一个随机生成的16字节密钥。 c. 这个随机生成的AES密钥,会用上面那个固定的RSA公钥进行加密,得到的结果就是encSecKey。 d. 第二次AES加密得到的密文,就是params。 e. 此外,可能还会对明文进行JSON.stringify和zfill(补位)等预处理。
实操心得:逆向时,不要试图完全理解整个庞大JS文件的所有逻辑。我们的目标是“找到并复现”。重点盯住生成
params和encSecKey的那个函数,把它里面调用的子函数逻辑也一并抠出来。可以尝试在开发者工具的Console(控制台)中,手动调用这个加密函数,传入测试数据,看输出是否与Network里捕获的请求参数一致,这是验证我们理解是否正确的最快方法。
4. Python代码实现:完整复现加密与下载
在彻底分析清楚加密流程后,我们就可以用Python来复现这一过程了。下面我将分模块给出代码并详细解释。
4.1 环境准备与依赖安装
首先,确保你的Python环境(建议3.6以上)并安装必要库:
pip install requests pycryptodome4.2 核心加密模块实现
我们创建一个名为encryptor.py的文件,将逆向出来的加密逻辑用Python实现。
import base64 import json import random import string from Crypto.Cipher import AES from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_v1_5 from Crypto.Util.Padding import pad import binascii class NeteaseMusicEncryptor: """网易云音乐API参数加密器""" # 固定的AES密钥和IV AES_KEY = '0CoJUm6Qyw8W8jud'.encode('utf-8') AES_IV = '0102030405060708'.encode('utf-8') # 固定的RSA公钥 RSA_PUBLIC_KEY = """-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBGLbCiK45wIDAQAB -----END PUBLIC KEY-----""" @staticmethod def _random_string(size): """生成指定长度的随机字符串""" return ''.join(random.choices(string.ascii_letters + string.digits, k=size)) @staticmethod def _aes_encrypt(text, key, iv): """AES加密,CBC模式,PKCS7填充""" cipher = AES.new(key, AES.MODE_CBC, iv) # 确保文本是bytes,并进行PKCS7填充 padded_text = pad(text.encode('utf-8'), AES.block_size) encrypted_bytes = cipher.encrypt(padded_text) # 结果进行base64编码 return base64.b64encode(encrypted_bytes).decode('utf-8') @staticmethod def _rsa_encrypt(text, public_key): """RSA加密,使用PKCS1_v1_5填充""" rsa_key = RSA.import_key(public_key) cipher = PKCS1_v1_5.new(rsa_key) # RSA加密的数据也需要是bytes encrypted_bytes = cipher.encrypt(text.encode('utf-8')) # 结果转换为16进制字符串 return binascii.b2a_hex(encrypted_bytes).decode('utf-8') @classmethod def encrypted_request(cls, data): """ 核心加密函数,模拟网易云音乐前端加密流程 :param data: 字典,请求的明文数据,如 {'ids': '[123456]', 'level': 'standard'} :return: 包含加密后params和encSecKey的字典 """ # 第一步:将数据转换为JSON字符串 text = json.dumps(data) # 第二步:生成一个16字节的随机密钥 secret_key = cls._random_string(16) # 第三步:第一次AES加密,使用固定密钥 params = cls._aes_encrypt(text, cls.AES_KEY, cls.AES_IV) # 第四步:第二次AES加密,使用随机密钥 params = cls._aes_encrypt(params, secret_key.encode('utf-8'), cls.AES_IV) # 第五步:用RSA公钥加密随机密钥,生成encSecKey enc_sec_key = cls._rsa_encrypt(secret_key, cls.RSA_PUBLIC_KEY) return {'params': params, 'encSecKey': enc_sec_key}代码关键点解析:
_aes_encrypt函数:我们使用Crypto.Cipher.AES的MODE_CBC模式。pad函数用于进行PKCS7填充,这是AES CBC模式的标准要求。加密后输出Base64字符串,这与前端观察到的结果一致。_rsa_encrypt函数:使用PKCS1_v1_5填充方案(这是网易云音乐使用的)。加密后,我们将字节结果转换为十六进制字符串,这也是前端encSecKey的格式。encrypted_request类方法:这是对逆向流程的忠实复现。注意两次AES加密的对象:第一次加密明文JSON,第二次加密第一次加密后的结果(Base64字符串)。
4.3 主爬虫程序实现
接下来,我们创建主文件main.py,它负责组织请求、调用加密器、解析数据并下载。
import requests import json import os from encryptor import NeteaseMusicEncryptor class NeteaseMusicDownloader: def __init__(self): self.session = requests.Session() # 设置通用请求头,模拟浏览器 self.headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Referer': 'https://music.163.com/', 'Content-Type': 'application/x-www-form-urlencoded', } self.session.headers.update(self.headers) def get_song_urls(self, song_ids, quality='standard'): """ 获取一批歌曲的播放链接 :param song_ids: 歌曲ID列表,如 [123456, 789012] :param quality: 音质,可选 'standard' (标准), 'higher' (较高), 'exhigh' (极高), 'lossless' (无损), 'hires' (Hi-Res) :return: 字典,{song_id: url, ...} """ api_url = 'https://music.163.com/weapi/song/enhance/player/url' # 构造请求明文数据 data = { 'ids': json.dumps(song_ids), # 需要是JSON字符串格式的数组 'level': quality, 'encodeType': 'aac', # 或 'mp3',根据实际情况调整 'csrf_token': '' # 如果未登录,通常为空 } # 加密参数 encrypted_data = NeteaseMusicEncryptor.encrypted_request(data) # 发送POST请求 resp = self.session.post(api_url, data=encrypted_data) resp.raise_for_status() # 检查请求是否成功 result = resp.json() url_map = {} if result['code'] == 200: for song in result['data']: if song['code'] == 200 and song['url']: # 确保请求成功且有链接 url_map[song['id']] = song['url'] else: print(f"歌曲 {song['id']} 获取链接失败,原因: {song.get('msg', '未知')}") url_map[song['id']] = None else: print(f"接口请求失败: {result}") return url_map def get_song_detail(self, song_ids): """ 获取歌曲详细信息(如名称、歌手) :param song_ids: 歌曲ID列表 :return: 字典,{song_id: {'name':..., 'artists':...}, ...} """ api_url = 'https://music.163.com/weapi/v3/song/detail' data = { 'c': json.dumps([{'id': sid} for sid in song_ids]), 'ids': json.dumps(song_ids), } encrypted_data = NeteaseMusicEncryptor.encrypted_request(data) resp = self.session.post(api_url, data=encrypted_data) resp.raise_for_status() result = resp.json() detail_map = {} if result['code'] == 200: for song in result['songs']: sid = song['id'] name = song['name'] artists = ' & '.join([ar['name'] for ar in song['ar']]) # 拼接所有歌手名 detail_map[sid] = {'name': name, 'artists': artists} return detail_map def download_song(self, url, song_id, song_name, artist, save_dir='./downloads'): """ 下载单首歌曲 """ if not url: print(f"[跳过] {song_name} - {artist} 无有效链接") return False # 创建保存目录 os.makedirs(save_dir, exist_ok=True) # 清理文件名中的非法字符 def sanitize_filename(filename): illegal_chars = '<>:"/\\|?*' for char in illegal_chars: filename = filename.replace(char, '_') return filename safe_name = sanitize_filename(f"{song_name} - {artist}") filepath = os.path.join(save_dir, f"{safe_name}.mp3") # 发送请求下载 print(f"[下载中] {song_name} - {artist}") try: resp = self.session.get(url, stream=True, timeout=30) resp.raise_for_status() with open(filepath, 'wb') as f: for chunk in resp.iter_content(chunk_size=1024*1024): # 1MB chunks if chunk: f.write(chunk) print(f"[成功] 已保存至: {filepath}") return True except Exception as e: print(f"[失败] 下载 {song_name} 时出错: {e}") # 如果下载失败,删除可能不完整的文件 if os.path.exists(filepath): os.remove(filepath) return False def batch_download(self, song_ids, quality='standard', save_dir='./downloads'): """ 批量下载主函数 """ print(f"开始处理 {len(song_ids)} 首歌曲...") # 1. 获取歌曲详情 print("正在获取歌曲信息...") detail_map = self.get_song_detail(song_ids) if not detail_map: print("获取歌曲信息失败,请检查网络或歌曲ID。") return # 2. 获取播放链接 print("正在获取播放链接...") url_map = self.get_song_urls(song_ids, quality) # 3. 逐个下载 success_count = 0 for sid in song_ids: if sid in detail_map and sid in url_map and url_map[sid]: info = detail_map[sid] success = self.download_song(url_map[sid], sid, info['name'], info['artists'], save_dir) if success: success_count += 1 else: print(f"[错误] 歌曲ID {sid} 信息或链接缺失,跳过。") print(f"\n批量下载完成!成功 {success_count}/{len(song_ids)} 首。") if __name__ == '__main__': downloader = NeteaseMusicDownloader() # 示例:下载歌曲ID为 1330348068 (起风了) 和 526464293 (平凡之路) 的歌曲 # 你可以从网易云音乐网页版分享链接中获取歌曲ID,通常是链接中的 `id=` 后面的数字 song_id_list = [1330348068, 526464293] # 开始批量下载,音质选择 'standard'(标准), 'higher'(较高), 'exhigh'(极高), 'lossless'(无损) downloader.batch_download(song_id_list, quality='higher', save_dir='./my_music')主程序逻辑解析:
- 会话保持:使用
requests.Session()可以自动管理cookies,保持一些必要的会话状态。 - 请求头模拟:设置
User-Agent和Referer是绕过基础反爬的常见手段。 - 分步操作:
batch_download方法清晰地分为三步:获取详情、获取链接、逐个下载。结构清晰,易于调试。 - 错误处理:在下载和请求过程中加入了基本的异常捕获和错误提示,提高了程序的健壮性。
- 文件命名:使用歌曲名和歌手组合作为文件名,并清理了Windows文件名中不允许的字符。
5. 常见问题、排查技巧与伦理考量
5.1 常见问题与解决方案
在实际运行中,你可能会遇到以下问题:
接口返回
-460错误码:这通常意味着“校验失败”。最可能的原因是加密算法复现不准确。- 排查:仔细核对
encryptor.py中的每一个步骤。特别是AES的模式(CBC)、填充(PKCS7)、密钥和IV的编码(必须转为bytes),以及RSA加密后的输出格式(十六进制)。建议使用浏览器的Console,用相同的明文数据分别运行你的Python加密函数和网页上的JS加密函数,对比输出的params和encSecKey是否完全一致。
- 排查:仔细核对
获取到的音频URL为空或无法播放:
- 原因:该歌曲可能因版权限制,无法获取到免费播放链接。或者你请求的音质(
level)该歌曲不支持。 - 解决:尝试降低音质,如从
lossless改为exhigh或standard。在代码中,我们已经判断了song['url']是否存在。
- 原因:该歌曲可能因版权限制,无法获取到免费播放链接。或者你请求的音质(
下载速度慢或连接超时:
- 原因:网易云音乐的音频服务器可能对单个IP的请求频率有限制。
- 解决:在
download_song函数的session.get中增加timeout参数。对于大规模批量下载,必须在下载每个文件之间添加随机延时(例如time.sleep(random.uniform(1, 3))),并考虑使用代理IP池来分散请求。
ModuleNotFoundError: No module named 'Crypto':- 原因:
pycryptodome库安装后,导入时使用的模块名仍然是Crypto。 - 解决:确保使用
pip install pycryptodome安装。如果同时存在旧的pycrypto,可能需要先卸载它:pip uninstall pycrypto。
- 原因:
歌曲信息获取失败:
- 排查:检查
get_song_detail函数中构造的data字典格式,是否与通过浏览器捕获的请求负载格式完全一致。有时接口的字段名或结构会发生微调。
- 排查:检查
5.2 高级技巧与优化建议
- 并发下载优化:对于成百上千首歌的下载,单线程太慢。可以使用
concurrent.futures库的ThreadPoolExecutor来实现多线程下载,但务必注意控制并发数(建议5-10个线程),并在每个线程的下载函数中保持独立的会话或妥善处理会话对象,避免线程安全问题。 - 断点续传:可以检查本地已存在的文件大小,然后在请求音频URL时通过设置请求头
Range: bytes=已下载大小-来实现断点续传。但这需要服务器支持Range请求。 - 歌单解析:本示例核心是给定歌曲ID进行下载。一个更实用的扩展是:输入一个公开歌单的分享链接,程序自动解析出歌单内所有歌曲的ID。这需要你分析歌单页面的接口(如
weapi/v3/playlist/detail),其参数加密方式与歌曲接口类似。 - 音质选择逻辑:可以先尝试请求最高音质(如
lossless),如果返回的URL为空,则自动降级到下一档音质重新请求,直到成功。
5.3 重要的伦理与法律提醒
在享受技术带来的便利时,我们必须清醒地认识到边界和责任。
重要提示:本项目和所有相关技术分享,仅限于个人学习、研究和合理使用范畴。你必须严格遵守以下原则:
- 尊重版权:音乐是创作者的心血。通过此技术下载的音频文件,严禁用于任何商业用途、网络公开传播或盗版销售。请仅作为个人离线备份和欣赏。
- 遵守
robots.txt:虽然技术上有能力抓取,但应尊重网站运营者的意愿。对目标网站保持友好,避免对其服务器造成过大压力。- 控制请求频率:在代码中务必添加显著的延时(例如每请求几次后休眠数秒),避免高频请求对网易云音乐服务器造成干扰,这既是道德要求,也能防止你的IP被封锁。
- 个人使用边界:不要开发、传播或提供以此技术为核心的自动化下载服务或工具。技术的滥用会损害平台和内容创作者的合法权益,也可能让你面临法律风险。
技术是一把双刃剑。掌握爬虫和逆向工程技术,能极大地提升我们解决问题的能力,但我们必须用它来做正确的事。希望这个项目能成为你技术成长路上的一块坚实的垫脚石,而不是一个麻烦的开端。在实际操作中,如果遇到任何问题,回顾加密逻辑的比对和请求头的模拟这两个关键点,大部分难题都能迎刃而解。
