MP3 Python抓取经验分享:某音频网页的实战记录

网页实际 URL 已作隐私化处理。

背景

做某教材的英语口译练习需要打开该网页播放英语听力音频,但该网页及书中均缺少听力文本。由此需要将网页中的音频爬取下来,导入自己开发的音频分段及语音到文本转写工具中,进行文本转写以及分段听力练习。

爬取思路

  1. 该音频播放网页的 URL 为:https://www.xxx.cn/xxx/player.html?bqcg_id=123456789。首先 F12 查看该网页源码,发现其并未将 MP3 文件直接放入网页,而是包含一个 audio.js 文件。

    js文件代码段

  2. 打开该文件。由于该文件主要功能是构造 mp3 文件 URL 用于播放,则在文件中重点查找包含 .mp3 的代码段。

  3. 找到如下代码段:

    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

  4. 接下来分析如何获取 blNo 的值。继续浏览代码,发现如下语句:

    let url = `/ashx/qrList.ashx?blid=${this.query.blid}&bqcg_id=${this.query.bqcgid}&bqc_id=${this.query.bqcid}`;
    

    该语句的功能是构造一个包含 blidbqcgidbqcid 三个参数的 URL,获取章节列表。由本网页 URL 可得 this.query.bqcgid=123456789。尝试将其当前音频播放网页的域名:https://www.xxx.cn/ 相组合,构造出如下完整 URL https://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作隐私化
    
  5. 查看第三步得出的 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。

  6. 接下来进行代码实现。

Python 代码实现思路及完整代码

  1. 该代码在命令行中运行,可以在程序运行开头构造命令行接口。

  2. 考虑到访问 MP3 所在 URL 以及下载 URL 都需要使用相同的 Cookie,可以构造一个 requests.Session() 保持会话状态并共享 Cookie。

  3. 依据上述第四步,构造 fetch_track_list 函数,在函数中声明一系列变量,用于提取出 JSON 代码中的 data 字典的 blNo 值。在 main 部分调用该函数。

  4. 依据上述第三步,构造 compute_directory_name 函数,传入 blNo 参数,保留其最后 5 位,同时也用来作为存储 MP3 的文件夹名。在 main 部分调用该函数。由此,第三步的 formatSrc() 函数的前三个语句执行完毕。

  5. formatSrc() 函数的第四行语句可以看出,需要提取出 bqc_no 参数才能构造完整的 URL。从第四步的 JSON 代码可以得出,该参数的值位于 data -> list -> blistBookQrCode -> bqc_no 中。以此构造 extract_track 函数。

  6. 构造 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

导入转写工具成功分段并转写:

作者:朝野星夜

物联沃分享整理
物联沃-IOTWORD物联网 » MP3 Python抓取经验分享:某音频网页的实战记录

发表回复