夜雨聆风学习资料网

ARTICLE · 1315226

Ai实战12:从接口分析到 GUI:用 Python 写一个音色库批量下载器

Ai实战12:从接口分析到 GUI:用 Python 写一个音色库批量下载器

📢 大家好,我是「Python学在坚持」。 上一篇我们做了「声音克隆」,有同学留言问:那些现成的 AI 音色网站,几百个音色能不能一键全扒下来做素材库? 今天就手把手教你:从零分析一个真实网站的接口,再用 Python 写一个带图形界面的"音色库批量下载器",抓取、搜索、单下、批量下、下头像,全都有。 全程只用 Python 标准库,不装任何第三方包。文末有完整源码,觉得有用先点个「关注」,学 Python 这事,贵在坚持~


从接口分析到 GUI:用 Python 写一个音色库批量下载器

一、先看我们要做什么

目标网站是一个在线 AI 音色平台的「音色库」页面,上面陈列着几百个预设音色,每个都能点播放试听。我们想做的是:

  • 一键把全部音色的试听音频抓下来,存到本地当素材库;
  • 有个图形界面,能搜索、能看列表、能选中下载、能批量下载;
  • 不依赖任何第三方库,拿来即用。

最终成品是一个桌面小工具,长这样:

[抓取音色列表]  搜索:[____]  □同时下载头像  保存目录:[下载/]

┌─────────────────────────────────────────────┐
│ 音色名称 │ 性别 │ 年龄 │ 语言 │ 热度 │ 描述    │
│ 曼小波   │ 女声 │ 儿童 │普通话│ 105 │ ...     │
│ 新小闻   │ 男声 │ 青年 │普通话│ ... │ ...     │
│ ...(共 411 个)                             │
└─────────────────────────────────────────────┘

[下载选中] [下载全部] [打开目录]   进度:[====   ]  日志...

⚠️ 先说清楚:本文纯技术教学,演示如何分析公开接口与编写下载程序。请只下载你有权使用的素材,尊重版权与网站服务条款,不要把素材用于商业或侵权用途。


二、最难的一步:数据到底藏在哪?

写爬虫/下载器,80% 的功夫在「找数据源」,真正写代码反而快。我们一步步来还原这个过程。

1. 直接抓网页?行不通

先用浏览器打开音色库页面,按 F12 看源码,你会发现:HTML 里根本没有音色数据。只有一堆 <script> 和空的 <div id="__nuxt">。

这说明它是个前端渲染的单页应用(SPA)。页面框架是先加载的,真正的音色列表是页面打开后,由 JavaScript 再去后端「要」回来的。

所以直接 urlopen(页面地址) 拿到的只是空壳,没用。

2. 换思路:找后端接口

既然数据是 JS 动态请求来的,那一定有个后端 API。找它有两个办法:

  • 办法 A(最快):浏览器 F12 → Network(网络)面板 → 筛选 Fetch/XHR → 刷新页面,看页面加载时请求了哪些接口,哪个返回了音色列表的 JSON。
  • 办法 B(本文用的):把前端的 JS 文件下载下来,在里面搜接口路径。因为接口地址是写死在打包后的 JS 里的。

我们用 Python 把页面里引用的所有 JS 抓下来,正则搜一遍 API 路径:

import re, urllib.request

BASE = "https://目标站点"
HEADERS = {"User-Agent": "Mozilla/5.0 ... Chrome/120.0 Safari/537.36",
"Referer": BASE + "/音色库页面"}

# 1) 抓页面 HTML,提取所有 /_nuxt/xxx.js
html = urllib.request.urlopen(
    urllib.request.Request(BASE + "/音色库页面", headers=HEADERS)
).read().decode("utf-8", "replace")
js_files = set(re.findall(r'/_nuxt/[A-Za-z0-9_\-]+\.js', html))

# 2) 逐个下载 JS,搜索接口路径
for jf in js_files:
    code = urllib.request.urlopen(
        urllib.request.Request(BASE + jf, headers=HEADERS)
    ).read().decode("utf-8", "replace")
for m in re.findall(r'["\'`](/(?:api|voice|role)[A-Za-z0-9/_\-]*)["\'`]', code):
        print(m)

跑完就从 JS 里挖出了一批接口路径,其中有两个很可疑:

  • /role/common/list —— 看名字像「公共音色列表」
  • /voice/list —— 像「我的音色列表」

再结合 JS 里的 baseURL 前缀 /api/v1,拼出完整地址去试。

3. 验证接口

直接请求 GET https://目标站点/api/v1/role/common/list:

{
"code": 0,
"data": {
"list": [
      {
"roleId": "A1fK...",
"name": "曼小波",
"describe": "...",
"headPath": "https://.../xxx.webp",
"voicePath": "https://.../xxx.wav",
"gender": "女声",
"ageGroup": "儿童",
"language": "普通话",
"createdNum": 105
      }
    ],
"total": 411,
"page": 1,
"pageSize": 10
  }
}

一下子全明白了:

  • code: 0 表示成功,不需要登录;
  • data.list 就是音色数组,每项的 voicePath 就是播放用的音频地址,headPath 是头像;
  • data.total = 411、page/pageSize 说明有分页,默认一页才 10 条。

对比一下 /voice/list,它返回 {"code":5,"msg":"未登录或登录过期"} —— 那是「我的音色」,需要登录,和我们无关,跳过。

4. 搞定分页

一页 10 条、共 411 条,总不能翻 42 次。试试把 pageSize 调大:

GET /api/v1/role/common/list?page=1&pageSize=100   → 真的返回 100 条!

说明 pageSize 能自定义。那策略就清晰了:循环翻页,直到拿满 total 条为止。

到这里,数据源彻底搞定。你看,真正写代码之前,分析占了大半篇幅——这才是爬虫/数据采集的核心能力。


三、核心代码拆解

整个程序分两层:网络层(抓数据、下文件)和 GUI 层(界面交互)。先讲网络层,这部分和界面无关,拎出来单独就能用。

1. 抓一页

import json, urllib.request

API = "https://目标站点/api/v1/role/common/list"
HEADERS = {
"User-Agent": "Mozilla/5.0 ... Chrome/120.0 Safari/537.36",
"Referer": "https://目标站点/音色库页面",
"Accept": "application/json, text/plain, */*",
}

deffetch_page(page, page_size=100):
    url = f"{API}?page={page}&pageSize={page_size}"
    req = urllib.request.Request(url, headers=HEADERS)
with urllib.request.urlopen(req, timeout=60) as resp:
        obj = json.loads(resp.read().decode("utf-8", "replace"))
    data = obj.get("data", {}) or {}
return data.get("list", []) or [], int(data.get("total", 0) or0)

几个小细节,都是实战踩出来的:

  • **一定要带 User-Agent 和 Referer**。很多站点对"没有浏览器标识"的请求直接返回 403。前面我们试过,不带 UA 就被拒,带上就 200。
  • decode("utf-8", "replace"):遇到个别非法字节不报错,用占位符替代,程序更健壮。
  • 用 .get(..., default) 层层取值,接口偶尔少字段也不会 KeyError 崩掉。

2. 翻页抓全部

deffetch_all(progress_cb=None):
    first, total = fetch_page(1)
    items = list(first)
if progress_cb:
        progress_cb(len(items), total)
    page = 2
while len(items) < total:          # 没拿满就继续翻
        batch, total = fetch_page(page)
ifnot batch:                  # 防御:返回空就停,避免死循环
break
        items.extend(batch)
if progress_cb:
            progress_cb(len(items), total)
        page += 1
return items

这里的 progress_cb 是个回调函数:抓的是网络层,但进度要显示在界面上。网络层不直接碰界面,而是"抓到多少就喊一声",由界面决定怎么展示。这种解耦写法,让这套下载逻辑脱离 GUI 也能单独在命令行里用。

3. 下载单个文件(断点友好)

import os
from urllib.parse import urlparse, unquote

defdownload_file(url, dst):
if os.path.exists(dst) and os.path.getsize(dst) > 0:
return"skip"# 已存在,跳过,支持中断后续跑
    tmp = dst + ".part"# 先写临时文件
try:
        req = urllib.request.Request(url, headers=HEADERS)
with urllib.request.urlopen(req, timeout=60) as resp, open(tmp, "wb") as f:
whileTrue:
                chunk = resp.read(1024 * 64)   # 分块读,省内存
ifnot chunk:
break
                f.write(chunk)
        os.replace(tmp, dst)           # 下完整了再改名,保证文件完整
return"ok"
except Exception as e:
if os.path.exists(tmp):
            os.remove(tmp)             # 失败清理半截文件
return"fail"

三个值得学的健壮性技巧:

  1. 已存在就跳过:程序中断后再跑,不会重复下,天然支持"续传任务"。
  2. 先写 .part 临时文件,下完再 os.replace 改名:避免下到一半断网,留下一个"看起来下好了其实不完整"的坏文件。
  3. 分块读写:音频可能几 MB,一次性读进内存不优雅,read(64KB) 循环写最稳。

4. 文件名清洗

接口里的音色名可能带 / : * ? 这种非法字符,直接当文件名会报错:

import re

defsafe_filename(name):
    name = re.sub(r'[\\/:*?"<>|\r\n\t]+', "_", name).strip().strip(".")
return name[:60] or"voice"# 限长,兜底默认名

最终保存的文件名做成 音色名_原始文件名.wav,既好认又不冲突。


四、GUI 怎么搭:tkinter 够用了

很多同学一想到"图形界面"就去装 PyQt、wxPython。其实 Python 自带的 tkinter 做这种工具绰绰有余,零安装、跨平台。

1. 界面三段式

import tkinter as tk
from tkinter import ttk

classVoiceApp(tk.Tk):
def__init__(self):
        super().__init__()
        self.title("NiceVoice 音色库下载工具")
        self.geometry("1040x620")
        self._build_top_bar()    # 顶部:抓取按钮、搜索框、目录选择
        self._build_table()      # 中间:Treeview 表格
        self._build_bottom()     # 底部:下载按钮、进度条、日志

把界面拆成"顶/中/底"三个 _build_xxx 方法,结构清爽,后面要加按钮知道往哪塞。

2. 用 Treeview 做表格

ttk.Treeview 是 tkinter 里现成的表格控件,设成 show="headings" 就是纯表格:

cols = ["name", "gender", "ageGroup", "language", "createdNum", "describe"]
self.tree = ttk.Treeview(frame, columns=cols, show="headings", selectmode="extended")
for key, title, width in COLUMNS:
    self.tree.heading(key, text=title)   # 列标题
    self.tree.column(key, width=width)   # 列宽

selectmode="extended" 让用户能按住 Ctrl/Shift 多选——这正是"批量下载选中"的基础。

3. 最关键的坑:别让界面"卡死"

这是新手做 GUI 最常翻车的地方。如果你在按钮的点击函数里直接写"抓 411 条 + 下载",整个窗口会假死,转圈点不动,因为下载把界面主线程占满了。

正确做法:耗时操作丢到子线程,子线程通过"队列"把进度/日志传回界面主线程更新。

import threading, queue

# 点"抓取":开一个后台线程,不阻塞界面
defon_fetch(self):
    threading.Thread(target=self._fetch_worker, daemon=True).start()

def_fetch_worker(self):
    items = fetch_all(progress_cb=lambda got, total:
                      self.post("status", text=f"抓取中 {got}/{total}"))
    self.post("items", items=items)     # 把结果放进队列

# 后台只往队列放消息,不直接碰界面控件
defpost(self, kind, **kw):
    self.msg_queue.put((kind, kw))

# 界面每 100ms 来队列取一次消息,统一在主线程更新
def_drain_queue(self):
try:
whileTrue:
            kind, kw = self.msg_queue.get_nowait()
            getattr(self, f"_on_{kind}")(**kw)   # 分发给对应处理函数
except queue.Empty:
pass
    self.after(100, self._drain_queue)           # 定时轮询自己

记住这条铁律:

tkinter 的控件只能在主线程里改。 子线程想更新界面,必须通过 queue 把消息传回来,由主线程用 after 定时取出来处理。

这套"子线程干活 + 队列通信 + after 轮询"的模式,是 Python GUI 做耗时任务的标准解法,学会了能复用到任何类似工具。


五、跑起来

把完整代码存成 voice_gui.py,命令行运行:

python voice_gui.py

操作流程:

  1. 点「抓取音色列表」,稍等几秒,411 个音色加载进表格;
  2. 想找特定音色,在搜索框输关键字(名称/描述/语言/性别都能搜),实时过滤;
  3. 选中一个或多个(Ctrl/Shift 多选),点「下载选中」;双击某行也能直接下;
  4. 想全要,点「下载全部」;勾上「同时下载头像」会连 .webp 头像一起下;
  5. 默认存到 下载/,可改目录,「打开目录」直接弹出文件夹。

底部进度条 + 日志实时滚动,已下过的文件自动跳过,中断了重跑也不怕。


六、这个项目你能学到什么

别只当它是个下载器。回头看,这一路覆盖了几个很通用的 Python 技能:

技能点
用在哪
接口分析
从 SPA 页面逆向出后端 API
urllib
 网络请求
伪装 UA、带 Referer、处理 JSON
分页抓取
循环翻页直到拿满 total
文件下载健壮性
跳过已存在、临时文件、分块写
tkinter GUI
Treeview 表格、搜索过滤、多选
多线程 + 队列
耗时任务不卡界面

这些拼在一起,就是一个完整的"数据采集 + 桌面工具"实战项目。把其中任何一块换个数据源,你就能做出下一个工具。


七、几句提醒

  • 接口和字段是会变的。哪天网站改版、换了接口,按第二节的思路重新分析一遍即可——分析能力才是真正带得走的东西,具体地址只是一次性的。
  • 再强调一次版权:下载来的音色只用于个人学习、合法授权的场景,别拿去商用或侵权。
  • 想进阶?可以给它加:下载失败自动重试、多线程并发下载、把音色清单导出成 Excel、按语言/性别分文件夹保存……这些都是很好的练手方向。

🎯 今天从「看不到数据」一路做到「批量下载 + 图形界面」,核心不是代码多难,而是遇到问题一步步拆解的思路。 完整源码在 voice_gui.py,配套还有一个命令行版 download.py(读 URL 清单批量下),都可以直接跑。 如果这篇对你有帮助,点个「赞」+「在看」,关注「Python学在坚持」。 学编程没有捷径,但每天进步一点点,坚持下来你会谢谢现在的自己。我们下期见~

回首页,搜索你想要的文档

在首页搜索框输入关键词,快速定位资料

文库首页搜索指引 立即搜索

相关学习资料