1. 荔枝FM个人作品抓取从手动下载到脚本跑通荔枝FM这个平台很多朋友可能不太熟它算是比较小众的一款音频APP早年也能和喜马拉雅、蜻蜓FM比一比。可惜逐渐没落了。孩子小时候我和娃一起在上面录了不少故事想着趁服务器没停赶紧把音频文件下载下来做个纪念。网上查遍各种方法通过手机就是没法导出来只能下载到应用内部iOS下压根找不到文件路径也找不到客服来回答。既然正规渠道不行那干脆写个程序爬下来吧。也顺便给Trae一个机会说干就干。通过浏览器F12看了一下音频地址url规则非常简单。实际地址是https://cdn5.lizhi.fm/audio/2020/08/11/2821634_hd.mp3参数应该都可以从列表页接口里拿到。我希望文件存下来的时候文件命名成2020_08_11_漫画宋词第一章.mp3存到目录下的audio文件夹里。这是列表页的接口地址https://njnew.lizhi.fm/voice/getVoiceList?page1voiceType-1status-1keyword作品一共有18页。这篇文章我会把整个流程拆开先用Trae生成可运行的Python爬虫骨架再把TaoToken的统一Key/API通道接进项目配置里让后续调用模型做解析、排障、字段映射时不用来回切平台。你跟着做十分钟内能跑通第一页半小时内能把全部作品拉下来。2. TaoToken前置准备统一Key与API通道在写爬虫之前先把TaoToken的配置搞定。为什么要先做这一步因为后面解析接口返回的JSON、处理字段缺失、生成下载URL规则这些环节我都习惯让模型帮我做一次校验和补全。TaoToken的好处是它把多家模型的调用收敛到一个Key、一个API地址上项目里只需要维护一份配置不用每个模型单独申请。你需要先拿到API Key。打开TaoToken官网注册后在控制台里创建一个Key复制出来备用。API地址统一用https://taotoken.net/api注意这个地址后面不加任何UTM参数保持干净。拿到Key之后在项目根目录建一个settings.json骨架长这样{ taotoken: { api_key: sk-你的Key粘贴在这里, base_url: https://taotoken.net/api, default_model: claude-sonnet-4-5, timeout: 60 }, lizhi: { user_id: 1234567, base_url: https://njnew.lizhi.fm/voice/getVoiceList, start_page: 1, end_page: 18, download_dir: audio, request_interval: 0.5 } }这个文件的作用是把敏感信息和业务参数分离。爬虫代码只读lizhi段模型调用只读taotoken段。后面如果你要换模型只改default_model一个字段就行。注意api_key不要提交到公开仓库建议在.gitignore里加上settings.json或者用环境变量覆盖。如果你还没创建Key可以直接去API Keys页面生成一个。接入文档在doc页面里有完整的参数说明包括请求头格式、超时设置、错误码含义建议先扫一遍再动手。3. 可复制配置requests爬虫与解析骨架现在进入核心部分。整个爬虫分三层请求层负责拿列表数据解析层负责把JSON里的字段映射成下载任务下载层负责按年份归档存文件。先装依赖pip install requests然后建lizhi_downloader.py把下面的代码完整贴进去。这段代码我实测过能直接跑import os import json import time import requests from datetime import datetime class LizhiFMDownloader: 荔枝FM音频下载器 def __init__(self, config_pathsettings.json): with open(config_path, r, encodingutf-8) as f: cfg json.load(f) lz cfg[lizhi] self.base_url lz[base_url] self.user_id lz[user_id] self.download_dir os.path.join( os.path.dirname(os.path.abspath(__file__)), lz[download_dir] ) self.interval lz.get(request_interval, 0.5) self.headers { Host: njnew.lizhi.fm, User-Agent: ( Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 ), content-type: application/json;charsetutf-8, Accept: */*, Origin: https://nj.lizhi.fm, Referer: https://nj.lizhi.fm/, Cookie: 在这里粘贴你的Cookie } os.makedirs(self.download_dir, exist_okTrue) def get_voice_list(self, page1, voice_type-1, status-1, keyword): params { page: page, voiceType: voice_type, status: status, keyword: keyword } try: resp requests.get( self.base_url, paramsparams, headersself.headers, timeout10 ) print(f第{page}页状态码: {resp.status_code}) return resp.json() except requests.exceptions.RequestException as e: print(f请求失败: {e}) return None except ValueError as e: print(fJSON解析失败: {e}) return None def parse_date(self, ts): dt datetime.fromtimestamp(int(ts) / 1000) return dt.strftime(%Y_%m_%d), dt.year def clean_filename(self, name): for ch in [\\, /, :, *, ?, , , , |]: name name.replace(ch, _) return name def build_audio_url(self, audio_id, create_time): dt datetime.fromtimestamp(int(create_time) / 1000) date_str dt.strftime(%Y/%m/%d) return fhttps://cdn5.lizhi.fm/audio/{date_str}/{audio_id}_hd.mp3 def download_audio(self, url, filename, year): year_dir os.path.join(self.download_dir, str(year)) os.makedirs(year_dir, exist_okTrue) file_path os.path.join(year_dir, filename) if os.path.exists(file_path): print(f已存在跳过: {filename}) return True try: resp requests.get( url, headersself.headers, streamTrue, timeout30 ) resp.raise_for_status() with open(file_path, wb) as f: for chunk in resp.iter_content(chunk_size8192): if chunk: f.write(chunk) print(f下载完成: {filename}) return True except requests.exceptions.RequestException as e: print(f下载失败: {url} - {e}) return False def process_item(self, item): audio_id item.get(id) name item.get(name) create_time item.get(createTime) if not all([audio_id, name, create_time]): print(f字段缺失跳过: {item.get(name, 未知)}) return False date_str, year self.parse_date(create_time) filename f{date_str}_{self.clean_filename(name)}.mp3 url self.build_audio_url(audio_id, create_time) return self.download_audio(url, filename, year) def run(self, start_page1, end_page18): total, success 0, 0 for page in range(start_page, end_page 1): print(f\n正在处理第 {page} 页...) data self.get_voice_list(pagepage) if not data or data not in data: print(f第{page}页数据异常跳过) continue voice_list data[data].get(list, []) if not voice_list: print(f第{page}页无数据) continue print(f本页共 {len(voice_list)} 条) for i, item in enumerate(voice_list, 1): print(f[{page}-{i}/{len(voice_list)}] {item.get(name, 未知)}) if self.process_item(item): success 1 total 1 time.sleep(self.interval) time.sleep(1) print(f\n完成! 总计: {total}, 成功: {success}, 失败: {total - success}) if __name__ __main__: downloader LizhiFMDownloader() downloader.run(start_page1, end_page18)几个关键点说明一下。headers里的Cookie必须换成你自己登录荔枝FM后从浏览器里复制的值否则接口会返回“没有权限访问”。build_audio_url里的CDN域名cdn5是实测可用的不同地区可能略有差异你随便点开一个作品看一下实际地址就能找到规律。request_interval设成0.5秒避免请求过于频繁。4. 验证请求与结果校验代码写完之后先跑第一页验证python lizhi_downloader.py如果一切正常你会看到类似输出正在处理第 1 页... 第1页状态码: 200 本页共 20 条 [1-1/20] 漫画宋词第一章 下载完成: 2020_08_11_漫画宋词第一章.mp3 ...目录结构会自动按年份归档audio/ ├── 2019/ │ └── 2019_01_01_标题.mp3 ├── 2020/ │ └── 2020_08_11_漫画宋词第一章.mp3 └── 2021/ └── 2021_03_15_标题.mp3验证成功的三个标志状态码200、JSON里data.list有内容、audio目录下出现mp3文件。如果状态码是200但list为空说明Cookie失效了重新登录复制一次。如果下载下来的文件是几KB的碎片说明URL规则变了需要重新抓包确认。我试过第一遍跑完发现拿到的m4a比特率很低改一下路径就能得到128kbps的APP端貌似有更高清的不过抓包没成功算了128k也够用了。5. 本篇常见错误排查5.1 返回“没有权限访问”这是最常见的报错。原因是接口需要登录态。解决办法是在浏览器里登录荔枝FM按F12打开开发者工具在Network面板里找到getVoiceList请求把Request Headers里的Cookie整段复制出来替换代码里的占位符。5.2 JSON解析失败或字段缺失接口返回的结构可能随版本变化。如果data.list取不到先打印完整响应看看实际结构data downloader.get_voice_list(page1) print(json.dumps(data, ensure_asciiFalse, indent2)[:500])确认字段名是id、name、createTime还是别的。有些版本用audioId、title、createAt代码里加个兼容就行。5.3 下载失败但列表正常通常是CDN域名或路径规则变了。打开任意一个作品的播放页F12看实际音频请求的URL对比build_audio_url生成的地址。如果域名从cdn5变成了cdn或其他改一下就行。5.4 请求被限流如果连续请求后开始返回403或超时说明触发了频率限制。把request_interval调到1秒以上页与页之间加2秒延迟。别贪快个人作品几百条慢慢跑也就半小时。5.5 文件名乱码或含非法字符Windows下文件名不能包含\/:*?|代码里的clean_filename已经处理了。如果还有问题检查标题里是否有emoji或特殊Unicode字符可以再加一层过滤。遇到这些报错时如果你不确定是代码问题还是接口变更可以把报错信息和响应片段丢给模型对话做一次分析让它帮你判断是Cookie过期、字段改名还是URL规则调整。这比自己逐行debug快很多。6. 长期维护与Coding Plan接入建议这个爬虫脚本本身不复杂但荔枝FM的接口策略可能会变。如果你打算长期维护或者想把它扩展成定时任务、多用户批量抓取建议把模型调用也纳入工程化流程。具体做法是在settings.json里已经配好的taotoken段基础上写一个简单的校验函数每次跑之前先把接口返回的第一条数据发给模型让它判断字段结构是否和预期一致不一致就输出差异报告。这样接口一变你立刻知道不用等下载失败才发现。对于需要长期跑编码任务、Agent自动化的场景Coding Plan比按次调用更划算额度固定、不用每次算token。如果你只是偶尔跑一次爬虫用模型对话手动问几次就够了。接入文档里有完整的请求示例和错误码说明建议收藏。API Keys页面可以随时查看和轮换Key控制台里能看到调用量和余额。整个流程跑通之后你手里就有了一套可复用的“爬虫模型校验”骨架换个目标站点改改参数就能用。