尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python爬虫实战:批量获取网易云热门歌单与封面图片

Python爬虫实战:批量获取网易云热门歌单与封面图片 前阵子我在整理本地音乐素材库时突然冒出一个需求把网易云音乐当前的热门歌单列表连同封面图全部批量拉下来按播放量排个序方便自己平时找歌和做数据存档。翻了半天网页手动一个个复制歌单链接和封面太痛苦干脆写了个 python 脚本自动化处理。如果你也想用 python 获取网易云热门歌单及封面这篇文章可以帮你省掉很多弯路。这个项目本质上是一个轻量级的爬虫数据采集任务调用网易云公开接口获取热门歌单信息提取歌单名称、封面、作者、播放量等字段并把封面图批量下载到本地。它不涉及登录、不依赖浏览器模拟只需要 requests 就能跑起来。整个过程很适合用来练习接口调试、JSON 解析、图片二进制流保存也是入门爬虫阶段非常有代表性的练手案例。无论你是刚学完 Python 基础语法、想看真实项目怎么组织代码还是单纯想做一个能用的“歌单备份工具”这个项目都有参考价值。1. 思路拆解先弄清楚数据到底从哪里来1.1 为什么我不去解析网页 HTML而是直接调公开接口很多新手写爬虫时第一反应就是拿 requests 去请求网易云首页然后通过正则或者 BeautifulSoup 去匹配页面里的歌单区块。这个思路不能说完全错但实操成本很高。原因在于网易云这类网站的前端页面大量使用 JavaScript 动态渲染歌单数据并不是一开始就写在 HTML 源码里而是页面加载后由脚本向后端接口发请求拿数据再动态生成 DOM 节点。这意味着你用 requests 抓到的 HTML 源码里可能只有一堆框架标签真正有用的歌单列表数据根本不在里面需要额外去找接口地址、处理 JS 执行逻辑非常吃力。更稳妥的方式是直接找到数据接口。网易云的 Web 前端在渲染过程中其实会向后端发送结构化的 API 请求返回格式通常是 JSON里面直接包含歌单列表、播放量、封面地址等字段。我们只需要用开发者工具F12切到 Network 面板刷新页面找到返回数据符合预期的 XHR 请求拿到它的 URL 和参数就能用 python 模拟这个请求。用接口而不是解析 HTML有几个明显好处数据结构固定字段名清晰提取信息直接用字典索引不用写繁琐的正则。返回体积小不像 HTML 那样有大段的样式和脚本噪音。不容易受页面改版影响只要接口不变代码就能继续跑。所以我建议你动手之前先花十分钟把网易云网页版打开按下 F12刷新页面观察几个请求的返回内容。这个“先确认数据源再写代码”的习惯能帮你节省大量调试时间。我见过太多人上来就写解析代码结果数据根本没拿到最后才发现是请求地址不对。1.2 请求头、Cookie 与反爬策略的微妙平衡接口找到了不代表请求就一定能成功。网易云对无登录状态的匿名请求是有一定限制的尤其是短时间内高频访问时很容易触发风控返回错误码或者直接拒绝服务。我在实际调试中第一版脚本只带了一个最简单的 GET 请求结果接口返回 403。后来检查才发现是缺少 User-Agent服务器无法识别客户端类型直接拒绝了。这就引出一个关键点模拟浏览器请求时请求头必须尽量伪装得“像真人”。一个基础的请求头至少需要包含 User-Agent 和 Refererheaders { 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/, }其中 User-Agent 可以从自己浏览器的 NetWork 面板里复制Referer 填网易云的域名表示请求是从网易云页面发起的而不是从一个来路不明的脚本。至于 Cookie热门歌单接口属于公开数据未登录状态下是可以访问的所以我这个项目里没有强制要求处理 Cookie。如果你后续要做更深入的操作比如获取私人歌单、收藏功能那就需要携带登录后的 Cookie 了。这一点后面第 5 节扩展部分会详细讲。另外特别提醒一下爬虫的本质是高频、批量地访问对方服务器所以一定要控制频率。我在代码里专门写了随机延时每两次请求之间间隔 0.5 到 1.5 秒避免给对方服务器造成压力。这不是怕事而是爬虫的基本素质也是保证脚本能长期稳定运行的策略。一上来就高并发抓取大概率几分钟内就会被封 IP。2. 环境准备与工具选型几行代码跑通前置条件2.1 Python 版本与依赖库安装这个项目对 Python 版本的要求不高3.8 以上都可以我自己用的 3.10。依赖库方面核心只需要 requests 这一个第三方库用来发 HTTP 请求。另外我习惯加装 tqdm 显示下载进度以及 tabulate 用来格式化输出歌单表格这两个不是必需但能明显提升使用体验。安装命令如下pip install requests tqdm tabulate如果你人在国内直接 pip 安装偶尔会遇到网络问题建议加上国内镜像源速度会快很多pip install requests tqdm tabulate -i https://pypi.tuna.tsinghua.edu.cn/simple这里有个小常识需要说一下requests 库底层虽然调用了 urllib3但它封装了会话管理Session、连接池ConnectionPool、重试机制等写业务代码时比手写 urllib 方便太多。比如自动管理 Cookie、保持 Keep-Alive 连接这些特性在做连续多次请求时特别关键能让脚本更快也更稳。如果你之前没配过虚拟环境我建议每个小项目都单独创建一套虚拟环境避免不同项目之间依赖版本冲突。Windows 下用命令python -m venv venv进入环境后激活然后在这个环境里装库这样依赖范围足够清晰。2.2 动手验证接口返回先探路再织网写了这么多年代码我养成的习惯是拿到一个接口先用 requests 发一个最简单的请求把原始响应打印出来确认字段结构然后再写正式的解析逻辑。这样可以避免一次性写太多代码最后却找不到问题出在哪一步。我这里的验证方法非常简单import requests url https://music.163.com/api/playlist/list/hot params { cat: 全部, limit: 10, offset: 0, } 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/, } resp requests.get(url, paramsparams, headersheaders, timeout10) print(resp.status_code) print(resp.text[:500])如果返回的 JSON 里能看到 playlists 字段说明接口路径和参数是对的。然后再把resp.text转成字典去提取其中的歌单列表部分。这一步虽然看起来简单但它是整个项目的定海神针。接口没通后面所有代码都是白写。确认返回值正常之后我才会开始写正式的抓取脚本。这里也顺便强调一个爬虫开发的通用思路面向数据写代码先把数据流跑通再优化代码结构和异常处理。3. 核心代码实现从拉取歌单列表到批量下载封面3.1 热门歌单数据获取与字段解析当接口验证通过后就可以写正式的抓取函数了。我把它拆成三部分获取歌单列表、解析关键字段、分页抓取更多数据。先看获取歌单列表这个函数import requests import json from typing import List, Dict def fetch_hot_playlists(page: int 0, page_size: int 20) - List[Dict]: url https://music.163.com/api/playlist/list/hot params { cat: 全部, limit: page_size, offset: page * page_size, } 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/, } resp requests.get(url, paramsparams, headersheaders, timeout10) if resp.status_code ! 200: raise RuntimeError(f请求失败状态码{resp.status_code}) data resp.json() if data.get(code) ! 200: raise RuntimeError(f接口返回异常{data}) return data.get(playlists, [])这里有几个细节值得说为什么用 params 参数而不是直接拼接 URL 字符串因为 requests 的 params 参数会自动处理中文和特殊字符的 URL 编码比如“全部”这样的中文参数直接拼在 URL 里可能产生编码问题而用params会自动转成合法的查询字符串。为什么把 limit 设成 20 而不是一次拉 100因为分页拉取时单页数据量太大会增加单次请求耗时遇到网络波动时也更容易超时。而且对于“热门歌单”这种接口单次返回太多数据反而容易触发风控。我实测下来20 到 30 条一次比较稳妥。拿到 playlists 列表后每条数据是一个歌单对象核心字段如下表所示字段名含义说明id歌单 ID全局唯一可用于后续接口调用name歌单名称用作文本展示和文件命名coverImgUrl封面图地址可能缺少http:前缀需要补全playCount播放次数可用于排序和热度分析trackCount歌曲数量歌单内歌曲总数creator.nickname创建者昵称展示作者信息updateFrequency更新频率部分歌单有可为空description歌单描述适合做文本分析解析函数我写成了这样def parse_playlist(item: dict) - dict: return { id: item.get(id), name: item.get(name), cover_url: item.get(coverImgUrl, ), play_count: item.get(playCount, 0), track_count: item.get(trackCount, 0), creator: (item.get(creator) or {}).get(nickname, 未知), description: (item.get(description) or ).strip(), }注意creator字段可能为 None所以要用(item.get(creator) or {})的方式做空值兜底不然容易直接抛 TypeError。这种小细节在实际爬取脏数据时非常常见处理好了能省很多调试时间。3.2 封面图批量下载与文件命名策略拿到歌单数据后下一步就是把封面图下载到本地。封面图 URL 有一个坑接口返回的coverImgUrl有时是//p1.music.126.net/xxx.jpg这样的协议相对地址缺失了最前面的http:直接用 requests 请求会报错。解决办法很简单判断地址是否以//开头如果是就手动拼接上http:。def normalize_url(url: str) - str: if url.startswith(//): return http: url return url下载封面的函数我加了超时、重试和文件完整性检查import os import time def download_cover(item: dict, save_dir: str covers, timeout: int 15) - str: os.makedirs(save_dir, exist_okTrue) cover_url normalize_url(item[cover_url]) file_name f{item[id]}_{item[name][:20]}.jpg # 清理文件名中的非法字符 file_name file_name.replace(/, _).replace(\\, _).replace(:, _) file_path os.path.join(save_dir, file_name) if os.path.exists(file_path): return file_path 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/, } for attempt in range(3): try: resp requests.get(cover_url, headersheaders, timeouttimeout, streamTrue) if resp.status_code 200: with open(file_path, wb) as f: for chunk in resp.iter_content(chunk_size1024): f.write(chunk) file_size os.path.getsize(file_path) if file_size 0: return file_path except requests.RequestException as e: print(f下载失败重试 {attempt 1}/3{e}) time.sleep(1) return 这里我用streamTrueiter_content的方式写文件对大图下载特别友好不会一次性把整个图片读进内存尤其批量下载几十上百个封面时内存占用会明显更小。文件命名方面我以歌单 ID 为主名再拼接歌单名前 20 个字符兼顾可读性和唯一性。这里要特别注意Windows 系统不允许文件名包含\/:*?|等字符所以必须提前替换掉。早期我没清理时遇到带冒号的歌单名程序直接抛 OSError后来统一加了过滤逻辑才稳。3.3 本地结果展示与数据落盘下载完成之后光在终端打印数据还不够最好把歌单信息保存成结构化文件方便后续分析和归档。我用 CSV 进行存储并额外保存了一份 JSON 备查。写 CSV 有一个非常容易踩的坑直接用默认的utf-8编码写入生成的 CSV 用 Excel 打开全是乱码因为 Excel 在 Windows 下默认按 GBK 解析。解决办法是写入时指定encodingutf-8-sig也就是带 BOM 头的 UTF-8Excel 才能正确识别。import csv def save_to_csv(playlists: List[dict], filename: str hot_playlists.csv) - None: with open(filename, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter( f, fieldnames[id, name, creator, play_count, track_count, cover_url] ) writer.writeheader() writer.writerows(playlists)保存 JSON 就简单多了def save_to_json(playlists: List[dict], filename: str hot_playlists.json) - None: with open(filename, w, encodingutf-8) as f: json.dump(playlists, f, ensure_asciiFalse, indent2)终端展示部分我用 tabulate 做一个简单表格把排名、歌单名、作者、播放量、歌曲数列出来。播放量过万后我会转成“xx.x万”的形式看着更直观from tabulate import tabulate def format_play_count(count: int) - str: if count 10000: return f{count / 10000:.1f}万 return str(count) def display_playlists(playlists: List[dict]) - None: table_data [] for idx, item in enumerate(playlists, start1): table_data.append([ idx, item[name], item[creator], format_play_count(item[play_count]), item[track_count], ]) print(tabulate(table_data, headers[排名, 歌单名, 作者, 播放量, 歌曲数], tablefmtgrid))这样脚本跑完后终端会显示一个清晰的榜单同时本地多出一个 covers 文件夹里面是下载好的封面图以及 hot_playlists.csv 和 hot_playlists.json 两个数据文件。整个项目到这里已经是一个“能用”的工具了。4. 常见问题与排查技巧实录4.1 高频错误速查表爬虫项目的调试过程很大一部分时间花在排查各种反爬和编码问题上。我把这个项目里最常见的几个问题整理成了表格方便你直接对照排查。问题现象可能原因解决办法请求返回 403缺少 UA 或 Referer被服务器拒绝在 headers 中补充浏览器 UA 和 Referer接口返回 code400参数不正确比如 cat 传了中文但未 URL 编码使用 requests 的 params 参数不要手动拼 URL返回 JSON 里没有 playlists接口地址或请求方式不对用 F12 刷新页面重新确认实际接口路径封面图下载失败coverImgUrl 是//开头的协议相对地址调用 normalize_url 补全http:前缀文件名保存报错歌单名包含 Windows 非法字符用歌单 ID 作为主文件名或提前清理非法字符CSV 用 Excel 打开乱码写入编码不是 UTF-8 with BOM写文件时指定encodingutf-8-sig请求一段时间后开始失败请求频率过高触发风控增加随机延时降低单页请求数量图片下载不完整网络波动或超时加重试机制用 stream 模式分块写入这些问题是增量出现的一开始代码简单时只遇到一两个但随着功能扩展问题也越来越多。建议你写的时候就把异常处理和重试机制考虑进去别等问题出现了再补。4.2 我自己踩过的几个坑第一个坑是忘记带 User-Agent。那会儿我刚写完脚本运行后第一时间看到 403第一反应是接口地址写错了查了半天 Network 面板才发现浏览器能访问是因为浏览器自动带了 UA而我这个脚本什么身份信息都没有相当于裸奔去敲门肯定被拒。第二个坑是协议相对地址。第一次成功拿到封面 URL 后我直接拿//p1.music.126.net/xxx.jpg去下载requests 直接报错或返回空内容。我当时愣了几秒才意识到这是一个缺协议头的地址浏览器会根据当前页面协议自动补全但脚本不会。第三个坑是请求太频繁。我第一次跑完整列表测试一页拉了 100 条循环下载封面中间没有任何延时跑了不到两分钟接口就开始陆续报错返回的 code 也不是 200。后来我把单页改成 20 条并在循环里加了time.sleep(random.uniform(0.5, 1.5))才稳定下来。第四个坑跟 Windows 文件系统有关。有个歌单叫“电影/经典原声大赏”带斜杠还有一个叫“2024: 年终盘点”带冒号。直接用这些字符串做文件名程序直接 OSError。从那以后我所有文件名都统一用“ID_前20字符”的格式干净又唯一。这些坑其实都不难解决但每个坑背后都对应一类真实开发场景。建议你实现的时候把这些错误处理一次性写进代码里可以减少很多不必要的折腾。5. 把脚本升级成一个能长期用的信息汇总工具5.1 定时监控歌单热度变化热门歌单的数据不是一成不变的播放量每小时都在涨。如果只是手动跑一次只能得到某个时间点的快照。更有意思的玩法是每天固定时间跑一次脚本把结果存进历史记录然后对比不同日期的播放量增量。这就涉及定时任务。两种常见方案在 Python 脚本内部用schedule或apscheduler库做定时循环。在系统层用 cronLinux/macOS或任务计划程序Windows定时执行。我用过schedule库做过一段时间的榜单监控写法很简洁import schedule import time def job(): print(开始抓取热门歌单…) playlists fetch_hot_playlists(page0, page_size20) save_to_csv(playlists, fhot_playlists_{time.strftime(%Y%m%d)}.csv) print(抓取完成) schedule.every().day.at(09:30).do(job) while True: schedule.run_pending() time.sleep(60)这样每天上午九点半自动抓一次数据文件按日期区分时间久了就形成了一份“歌单热度趋势表”。对比两次播放量的差值就能看出哪个歌单最近涨得特别快这种信息对判断最近什么风格的音乐突然火起来很有参考价值。5.2 进一步关联歌单详情与歌曲数据拿到热门歌单 ID 列表之后下一步可以调用歌单详情接口获取歌单内的歌曲列表。网易云的歌单详情接口通常需要歌单 ID 作为参数返回的字段包括歌曲名、歌手、专辑、时长等。这个步骤能让数据分析更深一层。比如拿到 20 个热门歌单里的歌曲后可以统计哪些歌手出现频次最高哪些歌曲被收藏在多个热门歌单里甚至可以按歌曲时长、风格做聚类分析。这正好能和“层次聚类python”这类数据分析技能关联起来形成一个完整的“采集 - 清洗 - 统计 - 可视化”链路。不过这里要注意歌单详情接口对访问频率更敏感部分敏感维度可能还需要登录 Cookie。建议仍然保持低频访问每次只拉一个歌单详情间隔两三秒再拉下一个。如果你对数据完整性没有硬性要求可以考虑只在运行时登录一次并保存 Cookie但也要控制好频率避免账号出现异常。5.3 数据可视化做一个简易的热门趋势图数据收集了一大堆最后还是可视化最直观。Python 生态里最常用的可视化库是 matplotlib画一个播放量 Top10 的横向柱状图非常容易。import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False def plot_top10(playlists: List[dict], filename: str top10.png) - None: sorted_playlists sorted(playlists, keylambda x: x[play_count], reverseTrue)[:10] names [item[name][:10] for item in sorted_playlists] counts [item[play_count] for item in sorted_playlists] plt.figure(figsize(10, 6)) plt.barh(names[::-1], counts[::-1], color#ec4141) plt.xlabel(播放次数) plt.title(网易云热门歌单 Top10) plt.tight_layout() plt.savefig(filename) plt.show()这里有一个容易忽略的点matplotlib 默认字体不含中文字符直接画中文标题会变成方块。所以要先通过plt.rcParams[font.sans-serif]指定中文字体Windows 下通常用 SimHei 或 Microsoft YaHei。如果不想依赖 matplotlib 的重型环境也可以直接生成 HTML 报告把数据渲染成表格和柱状图用 ECharts CDN本地双击就能打开。这对非编程用户更友好适合把脚本结果分享给朋友看。我个人比较喜欢 HTML 方案因为不需要安装额外 Python 库只要用字符串模板拼 HTML 文件就行发布起来也更方便。写在最后这个项目从头到尾跑下来表面上只是“拿一段数据、下载几张图片”但它把爬虫开发里最核心的几个环节都覆盖了接口定位、请求伪造、参数编码、JSON 解析、二进制文件保存、异常重试、数据落盘和可视化。更重要的是它会逼着你养成一种“先确认数据源再动手写逻辑”的习惯这个习惯在以后做任何数据采集任务时都会受用。我个人在实际操作中最深的体会是不要一上来就追求“功能大而全”先把一条最核心的数据链路跑通比如先拉 10 条歌单、下载 3 张封面再逐步往里加功能。这样每加一步你都能清楚地知道当前阶段的问题出在数据层、网络层还是解析层排查起来特别快。另外无论爬什么网站都要控制请求频率、遵守站点规则这既是对对方服务器的尊重也是保证自己脚本稳定跑下去的前提。希望这篇内容能帮你顺利跑通自己的第一个网易云数据采集项目。
返回列表