Python 自动化爬取网易云音乐歌手歌词实战教程

网易云音乐歌词数据分散于多页面,手动复制效率低下、易出现内容遗漏,且无法满足批量采集需求。自动化爬取面临两大核心技术难点:其一,歌词数据通过 AJAX 异步动态加载,原生<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">requests</font>仅能获取静态空壳 HTML,无法直接解析有效数据;其二,平台反爬机制严苛,高频请求易触发 403 访问拦截、滑块验证等限制。

本文基于 Python 构建端到端企业级歌词爬取系统,覆盖 API 逆向分析、请求参数加密、请求头伪装、异常容错、本地持久化存储全流程,并集成亿牛云爬虫代理高效解决 IP 封禁问题,实现稳定、批量的歌手歌词采集。

一、环境依赖配置

各库核心作用:

  • <font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">requests</font>:高性能 HTTP 请求客户端,负责发送网络请求、获取接口响应数据
  • <font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">demjson3</font>:兼容非标准 JSON 格式解析,适配网易云音乐 API 非常规响应数据
  • <font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">cryptography</font>:提供 AES 对称加密能力,用于生成平台接口必需的加密参数

二、API 逆向:加密参数生成

网易云音乐后端接口采用参数加密校验机制,是数据爬取的核心技术壁垒,请求参数需经过加密处理后才能正常调用。

核心加密参数说明

表格

参数名 功能说明 生成规则
params 封装业务请求参数(歌曲 ID、时间戳等) AES-CBC 模式加密 + Base64 编码
encSecKey 加密密钥校验参数 随机生成 16 位十六进制字符串
nonce 防重放随机数 随机生成 16 位十六进制字符串

加密实现代码

python

运行

plain 复制代码
import base64
import random
import json
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad

def generate_encrypted_params(params):
    """
    网易云音乐API加密参数生成函数
    :param params: 原始业务参数字典
    :return: 加密后可直接用于请求的参数
    """
    # 生成随机密钥与随机数
    enc_sec_key = random.randbytes(16).hex()[:16]
    nonce = random.randbytes(16).hex()[:16]
    
    # 业务参数序列化
    params_json = json.dumps(params)
    
    # 网易云音乐固定加密密钥与偏移量
    key = b'0CoJUmKQw8gw8ig'
    iv = b'0102030405060708'
    
    # AES-CBC加密 + Base64编码
    cipher = AES.new(key, AES.MODE_CBC, iv)
    encrypted_data = cipher.encrypt(pad(params_json.encode('utf-8'), AES.block_size))
    encrypted_params_b64 = base64.b64encode(encrypted_data).decode('utf-8')
    
    return {
        'params': encrypted_params_b64,
        'encSecKey': enc_sec_key,
        'nonce': nonce
    }

三、歌词接口请求封装

网易云音乐标准歌词 API 接口:<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">https://music.163.com/weapi/song/lyric?csrf_token=</font>

基于面向对象思想封装爬虫核心类,实现请求伪装、代理集成、异常处理一体化:

python

运行

plain 复制代码
import requests
import random

class NetEaseMusicCrawler:
    def __init__(self, use_proxy=False, proxy_config=None):
        self.base_url = "https://music.163.com"
        self.use_proxy = use_proxy
        self.proxy_config = proxy_config
        
        # 模拟浏览器请求头,绕过基础反爬
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
            'Referer': 'https://music.163.com/',
            'Accept': '*/*',
            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
            'Connection': 'close'
        }
    
    def get_lyric(self, song_id):
        """
        单首歌曲歌词获取
        :param song_id: 歌曲唯一标识ID
        :return: 原始歌词文本 / None
        """
        # 构造业务参数
        params = {'id': song_id, 'lv': -1, 'tv': -1, 'csrf_token': ''}
        encrypted_params = generate_encrypted_params(params)
        url = f"{self.base_url}/weapi/song/lyric?csrf_token="
        
        # 代理配置
        proxies = self._get_proxies()
        
        try:
            # 发送POST请求
            resp = requests.post(
                url, data=encrypted_params, headers=self.headers,
                proxies=proxies, timeout=10
            )
            
            # 状态码容错处理
            if resp.status_code == 200:
                return self._parse_lyric(resp.text)
            elif resp.status_code == 429:
                print(f"请求频繁(429),建议延长请求间隔")
            elif resp.status_code == 403:
                print(f"访问被拦截(403),建议切换IP或更新请求头")
            return None
            
        except Exception as e:
            print(f"请求异常: {str(e)}")
            return None
    
    def _parse_lyric(self, response_text):
        """非标准JSON歌词数据解析"""
        try:
            data = demjson3.decode(response_text)
            return data.get('lrc', {}).get('lyric', '') if data.get('code') == 200 else None
        except Exception:
            return None

    def _get_proxies(self):
        """代理获取工具方法"""
        if not self.use_proxy or not self.proxy_config:
            return None
        proxy_meta = "http://%(user)s:%(pass)s@%(host)s:%(port)s" % self.proxy_config
        proxies = {"http": proxy_meta, "https": proxy_meta}
        self.headers["Proxy-Tunnel"] = str(random.randint(1, 10000))
        return proxies

四、批量爬取歌手全量歌曲

通过歌手 ID 获取热门歌曲列表,实现批量歌词自动化下载与本地存储

python

运行

plain 复制代码
import os
import time

def get_artist_songs(self, artist_id):
    """获取歌手热门歌曲列表(单次最多50首)"""
    url = f"{self.base_url}/weapi/artist/top/song"
    params = {'id': artist_id, 'offset': 0, 'limit': 50, 'total': True}
    encrypted_params = generate_encrypted_params(params)
    proxies = self._get_proxies()
    
    try:
        resp = requests.post(url, data=encrypted_params, headers=self.headers, proxies=proxies, timeout=10)
        if resp.status_code == 200:
            data = demjson3.decode(resp.text)
            return data.get('songs', []) if data.get('code') == 200 else []
    except Exception:
        return []
    return []

def batch_download_lyrics(self, artist_id, save_dir='netease_lyrics'):
    """
    批量下载歌手歌词
    :param artist_id: 歌手ID
    :param save_dir: 歌词保存目录
    """
    os.makedirs(save_dir, exist_ok=True)
    songs = self.get_artist_songs(artist_id)
    print(f"成功获取{len(songs)}首歌曲")
    
    success_count = 0
    for song in songs:
        song_id = song.get('id')
        song_name = song.get('name', '未知歌曲')
        artist_name = song.get('ar', [{}])[0].get('name', '未知歌手')
        
        print(f"正在下载: {artist_name} - {song_name}")
        lyric = self.get_lyric(song_id)
        
        if lyric:
            # 过滤文件名非法字符,避免保存失败
            valid_filename = "".join([c for c in f"{artist_name}-{song_name}" if c.isalnum() or c in (' ', '-', '_')])
            filepath = os.path.join(save_dir, f"{valid_filename}.lrc")
            
            with open(filepath, 'w', encoding='utf-8') as f:
                f.write(lyric)
            print(f"  ✓ 保存成功")
            success_count += 1
        else:
            print(f"  ✗ 下载失败")
        
        # 控制请求频率,规避反爬
        time.sleep(random.uniform(1, 3))
    
    print(f"\n任务完成:成功下载{success_count}/{len(songs)}首歌词")
    return success_count

# 绑定方法到类
NetEaseMusicCrawler.get_artist_songs = get_artist_songs
NetEaseMusicCrawler.batch_download_lyrics = batch_download_lyrics

五、代理 IP 集成与反爬规避

网易云音乐对单 IP 请求频率、请求总量实施严格限制,高频访问会直接触发滑块验证、IP 永久封禁。亿牛云爬虫代理通过动态 IP 池技术,可有效分散请求来源,突破反爬限制。

代理配置与启动示例

python

运行

plain 复制代码
def main():
    # 亿牛云隧道代理配置
    proxy_config = {
        "host": "t.16yun.cn",
        "port": "31111",
        "username": "your_username",
        "password": "your_password"
    }
    
    # 初始化爬虫(开启代理模式)
    crawler = NetEaseMusicCrawler(use_proxy=True, proxy_config=proxy_config)
    
    # 批量爬取歌词(示例:周杰伦 歌手ID=6452)
    crawler.batch_download_lyrics(artist_id="6452", save_dir='netease_lyrics')

if __name__ == '__main__':
    main()

代理核心优势

  • 隧道代理技术:固定代理入口,每次请求自动分配独立出口 IP
  • 海量 IP 资源:标准版 IP 池 30 万 +,加强版 80 万 +
  • 高性能:网络延迟低至 100ms,支持毫秒级 IP 切换
  • 高并发:QPS 上限 5-300 次 / 秒,适配批量采集场景

六、边界场景处理与性能优化

  1. 文件名合法性校验 :歌曲名常包含<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">/ \ : * ?</font>等系统非法字符,需过滤后再保存文件
  2. HTTPS IP 粘性问题 :HTTPS 请求默认存在连接复用,添加<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">Connection: Close</font>请求头可强制切换 IP
  3. 异常容错:新增网络超时、解析失败、空数据等场景的降级处理,提升系统稳定性

七、完整可运行代码

整合所有模块,提供开箱即用的完整实现:

python

运行

plain 复制代码
import requests
import random
import os
import time
import json
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import demjson3

def generate_encrypted_params(params):
    enc_sec_key = random.randbytes(16).hex()[:16]
    nonce = random.randbytes(16).hex()[:16]
    params_json = json.dumps(params)
    key = b'0CoJUmKQw8gw8ig'
    iv = b'0102030405060708'
    cipher = AES.new(key, AES.MODE_CBC, iv)
    encrypted_data = cipher.encrypt(pad(params_json.encode('utf-8'), AES.block_size))
    encrypted_params_b64 = base64.b64encode(encrypted_data).decode('utf-8')
    return {'params': encrypted_params_b64, 'encSecKey': enc_sec_key, 'nonce': nonce}

class NetEaseMusicCrawler:
    def __init__(self, use_proxy=False, proxy_config=None):
        self.base_url = "https://music.163.com"
        self.use_proxy = use_proxy
        self.proxy_config = proxy_config
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
            'Referer': 'https://music.163.com/',
            'Accept': '*/*',
            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
            'Connection': 'close'
        }

    def _get_proxies(self):
        if not self.use_proxy or not self.proxy_config:
            return None
        proxy_meta = "http://%(user)s:%(pass)s@%(host)s:%(port)s" % self.proxy_config
        proxies = {"http": proxy_meta, "https": proxy_meta}
        self.headers["Proxy-Tunnel"] = str(random.randint(1, 10000))
        return proxies

    def get_lyric(self, song_id):
        params = {'id': song_id, 'lv': -1, 'tv': -1, 'csrf_token': ''}
        encrypted_params = generate_encrypted_params(params)
        url = f"{self.base_url}/weapi/song/lyric?csrf_token="
        proxies = self._get_proxies()
        try:
            resp = requests.post(url, data=encrypted_params, headers=self.headers, proxies=proxies, timeout=10)
            if resp.status_code == 200:
                return self._parse_lyric(resp.text)
            return None
        except:
            return None

    def _parse_lyric(self, response_text):
        try:
            data = demjson3.decode(response_text)
            return data.get('lrc', {}).get('lyric', '') if data.get('code') == 200 else None
        except:
            return None

    def get_artist_songs(self, artist_id):
        url = f"{self.base_url}/weapi/artist/top/song"
        params = {'id': artist_id, 'offset': 0, 'limit': 50, 'total': True}
        encrypted_params = generate_encrypted_params(params)
        proxies = self._get_proxies()
        try:
            resp = requests.post(url, data=encrypted_params, headers=self.headers, proxies=proxies, timeout=10)
            if resp.status_code == 200:
                data = demjson3.decode(resp.text)
                return data.get('songs', []) if data.get('code') == 200 else []
        except:
            return []
        return []

    def batch_download_lyrics(self, artist_id, save_dir='netease_lyrics'):
        os.makedirs(save_dir, exist_ok=True)
        songs = self.get_artist_songs(artist_id)
        success_count = 0
        for song in songs:
            song_id = song.get('id')
            song_name = song.get('name', '未知')
            artist_name = song.get('ar', [{}])[0].get('name', '未知')
            lyric = self.get_lyric(song_id)
            if lyric:
                valid_fn = "".join([c for c in f"{artist_name}-{song_name}" if c.isalnum() or c in (' ', '-', '_')])
                with open(os.path.join(save_dir, f"{valid_fn}.lrc"), 'w', encoding='utf-8') as f:
                    f.write(lyric)
                success_count += 1
            time.sleep(random.uniform(1, 3))
        print(f"完成:{success_count}/{len(songs)}")

def main():
    proxy_config = {"host": "t.16yun.cn", "port": "31111", "username": "your_user", "password": "your_pwd"}
    crawler = NetEaseMusicCrawler(use_proxy=True, proxy_config=proxy_config)
    crawler.batch_download_lyrics("6452")

if __name__ == '__main__':
    main()
相关推荐
Scott9999HH6 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
AI云海7 小时前
python 列表、元组、集合和字典
开发语言·python
二十雨辰8 小时前
[爬虫]-Urllib
爬虫·python
玉鸯9 小时前
Agent Hook:在概率推理之上,为 Agent 叠加确定性控制
python·langchain·agent
2601_960567969 小时前
电商套图自动化效率的工程量化分析——从逐张生成到批量套图的架构演进
运维·架构·自动化
weixin_4462608510 小时前
HACO:面向动态部署环境的对冲式智能计算可靠多智能体调度框架
后端·python·flask
我的xiaodoujiao10 小时前
API 接口自动化测试详细图文教程学习系列32--Allure测试报告2
python·学习·测试工具·pytest
qetfw10 小时前
MXU:Tauri 2 + React 的 MaaFramework 跨平台 GUI 源码
前端·python·react.js·前端框架·开源项目·效率工具
HiDev_11 小时前
【非标自动化】2、认识元器件(确定目标)
运维·自动化