MP3 Python抓取经验分享:某音频网页的实战记录
网页实际 URL 已作隐私化处理。
背景
做某教材的英语口译练习需要打开该网页播放英语听力音频,但该网页及书中均缺少听力文本。由此需要将网页中的音频爬取下来,导入自己开发的音频分段及语音到文本转写工具中,进行文本转写以及分段听力练习。
爬取思路
-
该音频播放网页的 URL 为:
https://www.xxx.cn/xxx/player.html?bqcg_id=123456789
。首先F12
查看该网页源码,发现其并未将 MP3 文件直接放入网页,而是包含一个audio.js
文件。 -
打开该文件。由于该文件主要功能是构造
mp3
文件 URL 用于播放,则在文件中重点查找包含.mp3
的代码段。 -
找到如下代码段:
formatSrc(f) { let src = "https://yyy.com/yyy/" let no = this.model.blNo.replace(/-/g, "") no = no.substr(no.length - 5) f.bqc_content = `${src}${no}/${f.bqc_no}.mp3` },
可以得出,该函数的逻辑为:① 取到当前书目 / 章节的
blNo
,去掉所有-
,再只保留它的最后 5 位,作为目录名。② 每个音频项f
都有一个字段bqc_no
,即文件名(不带.mp3
后缀)。③ 得出最终 URL 格式:https://yyy.com/yyy/{no}/{bqc_no}.mp3
。 -
接下来分析如何获取
blNo
的值。继续浏览代码,发现如下语句:let url = `/ashx/qrList.ashx?blid=${this.query.blid}&bqcg_id=${this.query.bqcgid}&bqc_id=${this.query.bqcid}`;
该语句的功能是构造一个包含
blid
、bqcgid
和bqcid
三个参数的 URL,获取章节列表。由本网页 URL 可得this.query.bqcgid=123456789
。尝试将其当前音频播放网页的域名:https://www.xxx.cn/
相组合,构造出如下完整 URLhttps://www.xxx.cn/ashx/qrList.ashx?bqcg_id=123456789
。访问该 URL,得到大量 JSON 代码,关键信息如下:{"msg":"","code":0,"data":{"list":[{"bookQrCodeGroup":{"blId":11847,"bqcg_id":3541,"bqc_name":"","bqcg_order":1},"listBookQrCode":[{"bqc_id":50955,"blId":11847,"bqcg_id":123456789,"bqc_no":"01","bqc_name":"","bqc_type":"mp3","bqc_note":"","bqc_downUrl":"|","bqc_order":0,"bqc_downTimes":0,"bqc_clickTimes":8749,"bqc_content":null},{"bqc_id":50956,"blId":11847,"bqcg_id":123456789,"bqc_no":"02","bqc_name":"","bqc_type":"mp3","bqc_note":"","bqc_downUrl":"|","bqc_order":0,"bqc_downTimes":0,"bqc_clickTimes":5105,"bqc_content":null}, // 中间部分省略 "model":{"blId":11847,"bsId":0,"bcId":0,"bcId2":0,"bscId":0,"bb_id":0,"blName":"","blName2":"","blNo":"123-4-5678-9012-3" //此处的blNo作隐私化
-
查看第三步得出的
formatSrc()
函数,首先将blNo
所有-
去掉得到1234567890123
,再保留最后 5 位90123
,最后拼接出完整的 URL:https://yyy.com/yyy/90123/01.mp3
。但尝试直接访问该 URL 会出错,提示<Message>You are denied by bucket referer policy.</Message>
,得出存在防盗链,需要在代码中构造Referer
下载 MP3。 -
接下来进行代码实现。
Python 代码实现思路及完整代码
-
该代码在命令行中运行,可以在程序运行开头构造命令行接口。
-
考虑到访问 MP3 所在 URL 以及下载 URL 都需要使用相同的 Cookie,可以构造一个
requests.Session()
保持会话状态并共享 Cookie。 -
依据上述第四步,构造
fetch_track_list
函数,在函数中声明一系列变量,用于提取出 JSON 代码中的data
字典的blNo
值。在main
部分调用该函数。 -
依据上述第三步,构造
compute_directory_name
函数,传入blNo
参数,保留其最后 5 位,同时也用来作为存储 MP3 的文件夹名。在main
部分调用该函数。由此,第三步的formatSrc()
函数的前三个语句执行完毕。 -
从
formatSrc()
函数的第四行语句可以看出,需要提取出bqc_no
参数才能构造完整的 URL。从第四步的 JSON 代码可以得出,该参数的值位于data -> list -> blistBookQrCode -> bqc_no
中。以此构造extract_track
函数。 -
构造
download_tracks
函数,将上述所有必需参数组合为完整的 URL,再从中下载音频文件。header
中的Referer
参数设置为本网页的 URL。
import os
import argparse
import requests
# 第三步
def fetch_track_list(base_url, blid, bqcg_id, bqc_id, session):
"""
抓取JSON代码,返回data['data']
"""
params = {
'blid': blid or '',
'bqcg_id': bqcg_id or '',
'bqc_id': bqc_id or ''
}
url = f"{base_url}/ashx/qrList.ashx"
resp = session.get(url, params=params)
resp.raise_for_status() # 判断网络连接是否正常
data = resp.json()
if data.get('code') != 0:
raise RuntimeError(f"API error: {data.get('msg')}")
return data['data']
# 第四步
def compute_directory_name(blNo: str) -> str:
"""
保留blNo最后5位
"""
cleaned = blNo.replace('-', '')
return cleaned[-5:]
# 第五步
def extract_tracks(data: dict) -> list:
"""
提取出所有bqc_no的值并返回一个列表
"""
tracks = []
for group in data.get('list', []):
for item in group.get('listBookQrCode', []):
tracks.append(item['bqc_no'])
return tracks
# 第六步
def download_tracks(tracks, directory, base_url, referer, session):
"""
下载MP3文件
"""
os.makedirs(directory, exist_ok=True)
headers = {
'User-Agent': 'Mozilla/5.0 (compatible)',
'Referer': referer
}
for bqc_no in tracks:
filename = f"{bqc_no}.mp3"
url = f"{base_url}/{directory}/{filename}"
print(f"Downloading {url} ...")
resp = session.get(url, headers=headers, stream=True)
try:
resp.raise_for_status()
except requests.HTTPError as e:
print(f"Failed to download {filename}: {e}")
continue
path = os.path.join(directory, filename)
with open(path, 'wb') as f:
for chunk in resp.iter_content(chunk_size=1024*1024):
if chunk:
f.write(chunk)
print(f"Saved {path}")
if __name__ == '__main__':
# 第一步
parser = argparse.ArgumentParser(description='MP3 downloading tool')
parser.add_argument('--base', default='https://xxx.cn', help='Base domain URL')
parser.add_argument('--blid', default='', help='blid parameter')
parser.add_argument('--qcg', required=True, help='bqcg_id parameter')
parser.add_argument('--qcid', default='', help='bqc_id parameter')
args = parser.parse_args()
# 第二步
session = requests.Session()
# 第三步
data = fetch_track_list(args.base, args.blid, args.qcg, args.qcid, session)
blNo = data['model']['blNo']
# 第四步
dir_name = compute_directory_name(blNo)
# 第五步
tracks = extract_tracks(data)
# 第六步
base_url = 'https://yyy.com/yyy'
referer = f"{args.base}/xxx/player.html?bqcg_id={args.qcg}"
download_tracks(tracks, dir_name, base_url, referer, session)
下载 MP3 以及导入转写工具结果
在py文件所在文件夹中打开命令行,输入命令python temp.py --qcg 123456789
,显示结果如下:
Downloading https://yyy.com/yyy/90123/01.mp3 ...
Saved 90123\01.mp3
Downloading https://yyy.com/yyy/90123/02.mp3 ...
Saved 90123\02.mp3
导入转写工具成功分段并转写:
作者:朝野星夜