Audible 是 Amazon 的有声书服务,其公开目录涵盖横跨各类题材的数十万个标题,每个都列着它的作者、朗读者、时长、星级评分和价格。那些搜索页和分类页是开放网络上对有声书市场最干净的视图之一,正因如此,分析师、图书馆员和爱好者都从中抓取数据,用来追踪定价、构建书单以及研究什么在热卖。
本指南将向你展示如何用 Python 抓取 Audible 有声书数据,并把结果组装成一个小小的 "迷你书库" 文件。你会构建一个可运行的抓取器,它通过 Crawling API 获取一个 Audible 搜索页或分类页,为每本有声书解析出一条干净的记录,处理分页,并把这套数据导出为 JSON 和 CSV。整个演练严格限定在公开目录数据上:任何人无需登录就能在结果页上看到的标题、作者、朗读者、时长、评分和价格。它从不触碰音频本身。
你将构建什么
一个 Python 脚本,它接收一个 Audible 搜索页或分类页 URL,通过 Crawling API 获取渲染后的列表,并为每本有声书提取一条结构化记录。我们用一个分类结果页作为贯穿全文的示例,并从每张产品卡片中取出这些字段:
- 标题 列表卡片上显示的有声书名称。
- 作者 该标题署名的作者,卡片上的 "By: ..."。
- 朗读者 配音表演者,卡片上的 "Narrated by: ..."。
- 时长 总播放时长,例如 "10 hrs and 32 mins"。
- 评分 当该标题有评分时的平均星级评分。
- 价格 当卡片显示价格时的标价。
- 链接 通往该有声书自己详情页的 URL。
为什么对 Audible 发起普通请求会失败
如果你把一个裸 HTTP 客户端指向某个 Audible 结果 URL,你很少能得到你想要的那份列表。有两件事在跟你作对。第一,Audible 在客户端渲染产品网格的大部分内容:它先发来一个轻量外壳,并随着页面 JavaScript 运行才把卡片填进去,所以初始 HTML 往往缺了你想要的时长、价格和评分。第二,作为 Amazon 旗下的产业,Audible 会很快标记出自动化流量。数据中心 IP 段以及看起来不像真实浏览器的请求模式,在你抵达列表之前就会撞上一个 CAPTCHA、一个 "robot check" 插页或干脆被封锁。
因此,一个能用的 Audible 抓取器需要在同一个请求里满足两件事:一个渲染页面的浏览器,以及一个被 Audible 读作真实访客的 IP。你可以自己用无头浏览器加一池轮换住宅代理来拼凑这套方案,但让那套技术栈保持健康才是大部分工作。Crawling API 把两者折叠进一次调用:你把结果 URL 发给它,它在一个受信任的住宅 IP 之后渲染页面,处理轮换和 CAPTCHA 求解,并返回已完成的 HTML 供你解析。
前置条件
在写任何代码之前,你需要准备好几样东西。它们都花不了多久。
基础 Python。你应当能自如地编写和运行一个 Python 脚本,并用 pip 安装软件包。如果你刚接触这门语言,官方 Python 文档或任何入门课程都涵盖了本教程默认你已具备的水平。
Python 3.8 或更高版本。用 python --version(或 python3 --version)确认你的版本。如果还没有,请从 python.org 安装,并确保 Python 在你的系统 PATH 上。
一个 Crawlbase 账户和 token。注册一个免费账户,打开你的仪表盘,复制你的 token。免费额度最多包含 20,000 次请求且无需信用卡,足够用来构建和测试这个抓取器。把 token 当作密码对待,并把它放在版本控制之外。
搭建项目
创建一个虚拟环境,让项目依赖保持隔离,然后安装抓取器需要的两个库。crawlbase 是 Crawling API 的官方客户端,而 beautifulsoup4 解析返回的 HTML,于是你能按 CSS 选择器从列表卡片里取出每个字段。
python --version python -m venv audible_env source audible_env/bin/activate pip install crawlbase beautifulsoup4
在 Windows 上,请用 audible_env\Scripts\activate 替代那行 source 命令来激活环境。两个库都装好后,创建本指南其余部分将逐步构建的脚本文件:
touch audible_library.py
理解 Audible 结果页
Audible 像图书馆那样组织它的目录,用着相互重叠的分类,一个标题可以同时归属于其中好几个。每个分类和每次搜索都位于一个稳定的 URL,例如分类节点用 https://www.audible.com/search?node=18573211011,关键词搜索用 https://www.audible.com/search?keywords=science+fiction。两者都渲染出同一种结果网格:一个有序的产品卡片列表,每本有声书一张,每张都带着一个标题、一行作者、一行朗读者、一个时长、一个评分和一个价格。
在写选择器之前,在你的浏览器里打开一个结果页,右键点击一张产品卡片,然后选择检查。Audible 把每条结果包在一个标记为 li.productListItem 的列表项里,把标题放进一个 h3.bc-heading 链接,并用 li.authorLabel 和 li.narratorLabel 标注作者行和朗读者行。时长位于 li.runtimeLabel,评分位于 li.ratingsLabel。这些就是你要瞄准的元素。Audible 的工具类名会随时间变动,所以请依靠更耐用的标签类,而不是一长串生成名的脆弱链条。
第 1 步:抓取渲染后的结果页
先从获取已完成的页面开始。导入 CrawlingAPI 类,用你的 token 初始化它,设置结果 URL,并请求它。在解析之前检查状态码,能让失败大声暴露而不是悄无声息。
from crawlbase import CrawlingAPI api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"}) def crawl(page_url): options = {"ajax_wait": "true", "page_wait": 4000} response = api.get(page_url, options) if response["status_code"] == 200: return response["body"].decode("latin1") print(f"Request failed: {response['status_code']}") return None if __name__ == "__main__": results_url = "https://www.audible.com/search?keywords=science+fiction" html = crawl(results_url) print(html[:500] if html else "No HTML returned")
这两个等待选项对一个随页面加载才填满的网格很重要。ajax_wait 告诉 API 等待异步内容完成,而 page_wait 在加载后再保持固定毫秒数,好让渲染较晚的卡片在页面被捕获之前出现。响应体被解码为 latin1,因为 Audible 页面混入了一些严格 UTF-8 解码会噎住的字符。运行脚本,你应当看到真实的列表标记,而不是一个 robot-check 外壳。这在你写下第一个选择器之前就确认了渲染正常工作。
那个 Audible 结果页需要在一个受信任的 IP 之后、在一次调用里得到一个渲染后的网格。Crawling API 接收你的 token,在一个真实浏览器里运行搜索页,在服务端轮换住宅 IP,并处理 CAPTCHA 求解,然后把已完成的 HTML 交给你。你免去了自己运行无头浏览器舰队和代理池的工作。先在最多 20,000 次请求的免费额度上把它指向一个搜索页或分类页 URL。
第 2 步:用 BeautifulSoup 解析有声书卡片
拿到渲染后的 HTML 后,把它载入 BeautifulSoup,找到每张产品卡片,并按选择器取出每个字段。Audible 把每条结果包在 li.productListItem 里,把标题放进标题链接,并用各自的类标注作者、朗读者、时长和评分行。从标题的锚点读取详情页链接。把每张卡片包在一个 try/except 里,这样一条格式错乱的列表不会让整次运行崩溃。
from bs4 import BeautifulSoup BASE = "https://www.audible.com" def text_of(card, selector): el = card.select_one(selector) return el.get_text(strip=True) if el else None def strip_label(value, label): if value and value.startswith(label): return value[len(label):].strip() return value def parse_link(card): a = card.select_one("h3.bc-heading a[href]") if not a: return None href = a["href"].split("?")[0] return href if href.startswith("http") else BASE + href def scrape_audiobooks(html): soup = BeautifulSoup(html, "html.parser") cards = soup.select("li.productListItem") results = [] for card in cards: try: author = text_of(card, "li.authorLabel") narrator = text_of(card, "li.narratorLabel") results.append({ "title": text_of(card, "h3.bc-heading a"), "author": strip_label(author, "By:"), "narrator": strip_label(narrator, "Narrated by:"), "length": strip_label(text_of(card, "li.runtimeLabel"), "Length:"), "rating": text_of(card, "li.ratingsLabel span.bc-text"), "price": text_of(card, "p.buybox-regular-price span.bc-text"), "link": parse_link(card), }) except Exception as e: print(f"Skipped a card: {e}") return results
text_of 辅助函数查询一张卡片内部的单个元素,并在它缺失时返回 None,而不是对一个空值调用 .get_text() 而抛错。这在某个字段缺失时让提取保持健壮,而这很常见,因为并非每个标题都显示价格或评分。strip_label 辅助函数修剪掉 Audible 打印在那些标签行内的 By:、Narrated by: 和 Length: 前缀,于是你存的是干净的值。链接从标题锚点读取,其查询字符串被丢弃,并被规范化为一个绝对 URL,因为 Audible 提供的是一个相对的 href。
Audible 生成的工具类名会不经通知地改变,而语义化的标签类(li.authorLabel、li.narratorLabel、li.runtimeLabel、li.ratingsLabel)更耐用。把上面的选择器当作一个起点模板,而不是一份契约。当某个字段对每张卡片都返回 None 时,在你浏览器的开发者工具里重新检查实时结果页并更新选择器。对任何生产环境的抓取器来说,定期维护选择器都很正常。
第 3 步:处理分页、组装并导出
一页结果只是演示;一个真正的迷你书库横跨整个分类。Audible 用一个 ?page= 参数对它的搜索页和分类页分页,所以你按顺序逐页走过,解析每一页,并在某页返回不到卡片时停下。然后把抓取和解析串成一个可运行的脚本,并把记录同时写入 JSON 和 CSV,于是你能把它们载入笔记本或电子表格。
import csv import json import time from crawlbase import CrawlingAPI from bs4 import BeautifulSoup api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"}) BASE = "https://www.audible.com" FIELDS = ["title", "author", "narrator", "length", "rating", "price", "link"] def crawl(page_url): options = {"ajax_wait": "true", "page_wait": 4000} response = api.get(page_url, options) if response["status_code"] == 200: return response["body"].decode("latin1") print(f"Request failed: {response['status_code']}") return None def text_of(card, selector): el = card.select_one(selector) return el.get_text(strip=True) if el else None def strip_label(value, label): if value and value.startswith(label): return value[len(label):].strip() return value def parse_link(card): a = card.select_one("h3.bc-heading a[href]") if not a: return None href = a["href"].split("?")[0] return href if href.startswith("http") else BASE + href def scrape_audiobooks(html): soup = BeautifulSoup(html, "html.parser") cards = soup.select("li.productListItem") results = [] for card in cards: try: author = text_of(card, "li.authorLabel") narrator = text_of(card, "li.narratorLabel") results.append({ "title": text_of(card, "h3.bc-heading a"), "author": strip_label(author, "By:"), "narrator": strip_label(narrator, "Narrated by:"), "length": strip_label(text_of(card, "li.runtimeLabel"), "Length:"), "rating": text_of(card, "li.ratingsLabel span.bc-text"), "price": text_of(card, "p.buybox-regular-price span.bc-text"), "link": parse_link(card), }) except Exception as e: print(f"Skipped a card: {e}") return results def build_library(search_url, max_pages=5): library = [] for page in range(1, max_pages + 1): sep = "&" if "?" in search_url else "?" page_url = f"{search_url}{sep}page={page}" html = crawl(page_url) if not html: break found = scrape_audiobooks(html) if not found: break library.extend(found) print(f"Page {page}: {len(found)} audiobooks") time.sleep(2) return library def export(rows, name="audible_library"): with open(f"{name}.json", "w", encoding="utf-8") as f: json.dump(rows, f, indent=2, ensure_ascii=False) with open(f"{name}.csv", "w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=FIELDS) writer.writeheader() writer.writerows(rows) print(f"Saved {len(rows)} audiobooks to {name}.json and {name}.csv") def main(): url = "https://www.audible.com/search?keywords=science+fiction" library = build_library(url, max_pages=5) if library: export(library) if __name__ == "__main__": main()
用 python audible_library.py 运行完整脚本。它走过最多五页结果,为每本有声书解析一行,并写出 audible_library.json 和 audible_library.csv 两者。空结果中断在分类用尽页面时让你提早停下,请求之间的 time.sleep(2) 为运行控速,于是你不会因为连珠炮般的流量而被标记,而共享的 FIELDS 列表让 CSV 的列顺序与字典键保持一致,于是这两份导出永不偏离彼此。
输出长什么样
你会得到一个干净的有声书记录列表,按列表顺序排列,可以直接写入 JSON、CSV 或数据库。这就是你的迷你书库。
[ { "title": "Project Hail Mary", "author": "Andy Weir", "narrator": "Ray Porter", "length": "16 hrs and 10 mins", "rating": "4.9 out of 5 stars", "price": "$24.49", "link": "https://www.audible.com/pd/Project-Hail-Mary-Audiobook/B08G9PRS1K" }, { "title": "Dune", "author": "Frank Herbert", "narrator": "Scott Brick, Orlagh Cassidy, Euan Morton", "length": "21 hrs and 2 mins", "rating": "4.6 out of 5 stars", "price": "$29.65", "link": "https://www.audible.com/pd/Dune-Audiobook/B002V1OF70" } ]
从这里起,这个迷你书库就归你使用了。把 CSV 载入电子表格按时长或评分排序,按某个价格上限筛选 JSON,或把这套数据喂进一个推荐列表。因为每条记录都带着详情页链接,当你需要的不止列表字段时,你可以日后跟进,去抓取单个标题的页面,获取完整的描述或出版商。
跨分类扩展规模
一次搜索是一个起点;一个更完整的书库横跨好几个分类。Audible 为每个分类节点和每次关键词搜索都暴露一个结果页,每个都在它自己的 URL 上,所以你可以保留一份名称到 URL 的映射,并对每一个跑同一个 build_library 例程,按分类给输出加键。用循环里已有的延迟为请求控速,并给 max_pages 设个上限,这样一次宽泛的运行不会膨胀。要随时间追踪价格和评分趋势,把这项工作按计划运行,给每次导出打上日期戳,并对比相继的快照看看什么变了。同样的方法支撑着任何随时间盯着一个目录的电商抓取项目。
保持不被封锁
即便渲染已经被处理好,Audible 仍会监视带抓取器特征的流量。有几个习惯能让一次运行保持健康,它们适用于任何难啃的商业目标。
- 为你的请求控速。在页面和分类之间加一段延迟把请求摊开,而不是全速爬取一切。把较重的工作安排在非高峰时段,以减轻 Audible 服务器的负载。
- 依靠轮换。一池住宅 IP 把请求分散到许多真实用户的地址上,于是没有任何单个地址会触发速率限制。Crawling API 替你处理这一点;如果你自建技术栈,这就是需要做对的部分。
- 只保留你需要的。存下你项目要用的目录字段,丢弃其余的。定期复查你的选择器,让抓取器跟上标记的变化。
关于避免被封锁的更广套路,参见如何抓取网站而不被封锁。如果你想深化解析这一侧,关于在 Python 中使用 BeautifulSoup 的指南对这个库讲得很详细,而对于一个相关的目录目标,关于抓取 Goodreads 评分和评论的演练与一套有声书数据天然配套。
抓取 Audible 合法吗?
抓取 Audible 是否被允许,取决于 Audible 和 Amazon 的使用条款、你所在的司法管辖区,以及你拿这些数据做什么。那些条款对自动化访问设有限制,所以无论你的工具多么谨慎,抓取都可能与它们相抵触。这里的任何代码都不会改变这一点;它只是让技术部分能跑通。请阅读 Audible 的使用条款及其 robots.txt,并把两者都当作你采集范围的边界。对于商业或竞争用途,法律图景会更复杂,就你的具体情况咨询一位法律专家是明智之举。
有几条值得坚守的准则。只采集公开的目录数据:任何人无需账户就能在结果页上看到的标题、作者、朗读者、时长、评分、价格和列表链接。本指南从不触碰音频本身,那是你无权下载或重新发布的受版权保护的内容,而且它远离任何登录墙之后的东西、账户或购买数据,以及关于可识别听众或评论者的个人信息。把你的请求量保持得足够低,以免给 Audible 的服务器造成压力,而如果你打算把数据用于商业用途,请取得许可或一份官方协议,而不是假定沉默就是同意。
本指南刻意被限定在公开的搜索页和分类页上,因为那正是让这项工作站得住脚的边界。要获得授权或大批量访问,Amazon 提供 Product Advertising API 和其他官方计划,当你需要大批量、有保证的结构或商业权利时,那才是正确的工具。如果你的项目需要的不止于公开目录元数据,那么一个官方 API 或一份数据协议才是正确的路径,而不是一个更聪明的抓取器,并且当存在一个时,它永远是更好的选择。
核心要点
- Audible 的目录是丰富的公开元数据。每个结果页都列着标题、作者、朗读者、时长、评分和价格,这正是它对构建书单或研究有声书市场如此有用的原因。
- 你需要渲染和受信任的 IP 一起具备。Audible 在客户端填充产品网格并封锁机器人流量,所以 Crawling API 在一次调用里于一个住宅 IP 之后渲染页面。
-
BeautifulSoup 负责提取。循环
li.productListItem卡片,并把标题、作者、朗读者、时长、评分、价格和链接映射到当前的选择器,并预期那些选择器会漂移。 -
分页构建出书库。走过
?page=各页直到某页返回不到卡片,然后用一份共享的字段列表导出为 JSON 和 CSV,于是两份文件保持同步。 - 只停留在公开元数据上。尊重 Audible 的使用条款和 robots.txt,绝不触碰受版权保护的音频或任何登录墙之后的东西,对授权或大批量数据优先用 Amazon 的官方 API。
常见问题
为什么一次普通请求返回不到 Audible 的任何有声书?
有两个原因。Audible 随页面加载在客户端填充产品网格的大部分内容,所以一次原始请求往往得到一个缺了时长、评分和价格的外壳。在此之上,作为 Amazon 旗下的产业,Audible 会挑战或封锁看起来不像真实浏览器的流量。通过 Crawling API 在一个受信任的 IP 之后渲染页面同时解决了这两点,这正是这里的抓取器把它的请求经由它路由的原因。
我能从一个 Audible 列表抓取哪些字段?
从一张搜索页或分类结果卡片,你可以取出标题、作者、朗读者、总时长、平均评分、价格,以及通往该有声书详情页的链接。这些是公开的列表字段。如果你需要完整的描述、出版商或发行日期,跟着每张卡片的链接去抓取单个详情页,那里暴露了更多元数据。
我如何抓取一个特定的 Audible 分类?
每个分类和搜索都有它自己的 URL,例如关键词搜索用 /search?keywords=science+fiction,分类节点用 /search?node=...。把抓取器指向你想要的那个 URL。要覆盖许多分类,保留一份名称到 URL 的映射并循环遍历它,用一段短暂的延迟为请求控速。
Audible 上的分页是怎么工作的?
Audible 追加一个 ?page= 参数(或在 URL 已经带查询字符串时用 &page=)来走过结果页。脚本递增页码,解析每一页,并在某页返回不到产品卡片时停下,这就是你已抵达分类末尾的信号。
我能下载有声书本身吗?
不能,而且你不应该尝试。音频文件是与 Audible 账户和购买绑定的受版权保护的内容,本指南刻意远离它们。它只采集公开的目录元数据,即出现在列表页上的标题、作者、朗读者、时长、评分和价格,绝不采集音频。
在抓取 Audible 时,我如何避免被封锁?
把你每个 IP 的请求速率保持得低,在页面和分类之间加一段延迟,并通过轮换住宅 IP 路由,这样就没有任何单个地址会触发速率限制。Crawling API 替你管理轮换、一个受信任的 IP 池以及 CAPTCHA 处理;如果你自建技术栈,那就是需要投入的部分。盯着状态码,在你开始看到挑战时退避。
大规模爬取任何站点,无需与基础设施对抗。
Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。
