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()
相关推荐
深蓝电商API2 小时前
京东API批量操作优化:单次1000条限制的突破方案
爬虫·接口·api·京东api
风之所往_3 小时前
Python 3.0 新特性全面总结
python
2401_882273723 小时前
如何在 CSS 中正确加载本地 JPG 背景图片
jvm·数据库·python
Lucas_coding3 小时前
【Claude Code Router】 Claude Code 兼容 OpenAI 格式 API, Claude code 接入本地部署模型
人工智能·python
测试员周周3 小时前
【AI测试系统】第5篇:从 Archon 看 AI 工程化落地:为什么"确定性编排+AI 弹性智能"是终局?
人工智能·python·测试
大飞记Python4 小时前
【2026更新】Python基础学习指南(AI版)——04数据类型
开发语言·人工智能·python
Hello eveybody5 小时前
介绍一下背包DP(Python)
开发语言·python·动态规划·dp·背包dp
2301_795099746 小时前
让 CSS Grid 自适应容器尺寸的动态布局方案
jvm·数据库·python
呆萌的代Ma6 小时前
python读取并加载.env的配置文件
python