B站视频下载(VideoHelper)

本文分享了在Bilibili网站上爬取视频的具体方法,包括分析请求参数、抓包技巧及模拟请求过程。作者详细介绍了如何从视频页获取必要信息,如aid、cid和ep_id,并利用这些信息构造API请求,最终实现视频资源的下载。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

继续上次的知乎爬虫, 这次开始了哔哩哔哩的爬虫实践;

 

首先介绍下如何下载吧: VideoHelper 里面有三种方式下载b站视频。

 

同样的流程, 还是先抓包,分析参数,寻找参数(包括之前的请求包和页面源码),找出视频真实地址, 然后在模拟。

 

抓包是注意几个参数:

aid:每个视频都会有对应的 aid, 包括ep类型的;

cid:弹幕的id, 通过相关api可由cid找到对应的资源列表

ep_id: 就是地址栏上显示的ep类型的id了

 

这里详细的流程我就不介绍了(该软件目前还支持知乎等网站视频, 欢迎star。(*>﹏<*))

 

其中需要注意的是模拟发包是有些请求头是不能掉的, user-agent我就不说了, 比如Referer;

 

另外我发现网上目前仅存的b站的视频爬虫好像大多不支持ep类型的, 不过我那个最近测试是支持了的, 但是vip专属的也是会直接报错;

 

另外注明:该项目参考了you-get的部分api

 

代码结构我想还是比较清晰的,直接上代码吧:

package website;

import bean.BilibiliBean;
import bean.VideoBean;
import org.dom4j.DocumentException;
import org.dom4j.io.SAXReader;
import org.json.JSONArray;
import org.json.JSONObject;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import util.DownloadUtil;
import util.HttpUtil;
import util.MD5Encoder;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.util.*;

import static util.PrintUtil.println;


/**
 * 哔哩哔哩: https://www.bilibili.com/
 *
 * @author Asche
 * @date 2018-10-20 18:02:29
 * @github https://github.com/asche910
 */
public class Bilibili extends BaseSite {
    // from aid to cids
    private String ApiGetList = "https://www.bilibili.com/widget/getPageList?aid=";
    private String AvApi = "http://interface.bilibili.com/v2/playurl?";
    private String EpApi = "http://bangumi.bilibili.com/player/web_api/playurl?";
    private String SEC_1 = "94aba54af9065f71de72f5508f1cd42e";
    private String SEC_2 = "9b288147e5474dd2aa67085f716c560d";

    // quality
    private final int RESOLUTION_1080 = 112;
    private final int RESOLUTION_720 = 64;
    private final int RESOLUTION_480 = 32;
    private final int RESOLUTION_360 = 15;

    private int quality = RESOLUTION_1080;

//    private List<String> urls = new ArrayList<>();
    private String playUrl;
    private String fileName;
    private int timeLength;
    private int fileSize = 0;
    private int aid;
    private int cid;

    // 视频类型
    private final int AV_VIDEO = 1;
    private final int EP_VIDEO = 2;
    private final int SS_VIDEO = 3;

    private int type = AV_VIDEO;
    private boolean isSupported;

    // ep的关联系列
    private List<BilibiliBean> serialList = new ArrayList<>();

    // 是否已经解析
    private boolean isResolved;


    public Bilibili() {
    }

    /**
     * 先获取信息再决定是否下载
     * @param playUrl
     * @param outputDir
     */
    public Bilibili(String playUrl, String outputDir) {
        if (!isResolved) {
            this.playUrl = playUrl;

            String[] strs = playUrl.split("/");

            for (String str : strs) {
                if (str.matches("av\\d{4,}")) {
                    aid = Integer.parseInt(str.substring(2));
                    isSupported = true;
                    break;
                } else if(str.matches("ep\\d{4,}")){
                    type = EP_VIDEO;
                    isSupported = true;
                    break;
                } else if(str.matches("ss\\d{4,}")){
                    type = SS_VIDEO;
                    isSupported = true;
                    break;
                }
            }

            try {
                switch (type) {
                    case SS_VIDEO:
                    case EP_VIDEO:
                        initEp();

                        String epApi = generateEpApi(EpApi, cid, quality);
                        println(epApi);

                        parseEpApiResponse(epApi);
                        break;
                    case AV_VIDEO:
                        initAv();

                        String avApi = generateAvApi(AvApi, cid, quality);
                        println(avApi);

                        parseAvApiResponse(avApi);
                        break;
                }

            } catch (Exception e) {
                e.printStackTrace();
            }
            isResolved = true;
        }
    }

    @Override
    public void downloadByUrl(String playUrl, String outputDir) {
        println("Bilibili start: ");

        this.playUrl = playUrl;
        String[] strs = playUrl.split("/");

        for (String str : strs) {
            if (str.matches("av\\d{4,}")) {
                aid = Integer.parseInt(str.substring(2));
                isSupported = true;
                break;
            } else if(str.matches("ep\\d{4,}")){
                type = EP_VIDEO;
                isSupported = true;
                break;
            } else if(str.matches("ss\\d{4,}")){
                type = SS_VIDEO;
                isSupported = true;
                break;
            }
        }

        try {

            if (!isResolved) {
                switch (type) {
                    case SS_VIDEO:
                    case EP_VIDEO:
                        initEp();

                        String epApi = generateEpApi(EpApi, cid, quality);
                        println(epApi);

                        parseEpApiResponse(epApi);
                        break;
                    case AV_VIDEO:
                        initAv();

                        String avApi = generateAvApi(AvApi, cid, quality);
                        println(avApi);

                        parseAvApiResponse(avApi);
                        break;
                }
                isResolved = true;
            }

            println("# Title: " + fileName);
            println("     -TimeLength: " + timeLength / 1000 / 60 + ":" + String.format("%02d", timeLength / 1000 % 60));
            println("     -File Size: " + fileSize / 1024 / 1024 + " M");

            download(urls, outputDir);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 内部下载入口
     *
     * @param videoSrcs
     * @param outputDir
     */
    @Override
    public void download(List<String> videoSrcs, String outputDir) throws IOException {
        Map<String, List<String>> headerMap = new HashMap<>();
        // 缺失Referer会导致453错误
        headerMap.put("Referer", Collections.singletonList("http://interface.bilibili.com/v2/playurl?appkey=84956560bc028eb7&cid=59389212&otype=json&qn=3&quality=3&type=&sign=4c841d687bb7e479e3111428c6a4d3b8"));

        int index = 0;

        for (String src : videoSrcs) {
            println("Download: " + ++index + "/" + videoSrcs.size());

            String fileDir;
            if (videoSrcs.size() == 1) {

                fileDir = outputDir + File.separatorChar + fileName.replaceAll("[/|\\\\]", "") + ".flv";
            } else {
                fileDir = outputDir + File.separatorChar + fileName.replaceAll("[/|\\\\]", "") + "【" + index + "】.flv";
            }

            DownloadUtil.downloadVideo(src, fileDir, headerMap);
        }
        println("Download: All Done!");
    }

    @Override
    public VideoBean getInfo() {
        VideoBean bean = new VideoBean();
        bean.setTitle(fileName);
        bean.setTimeLength(timeLength / 1000 / 60 + ":" + String.format("%02d", timeLength / 1000 % 60));
        bean.setSize(fileSize / 1024 / 1024);
        return bean;
    }

    public List<BilibiliBean> getSerialList(){
        return serialList;
    }

    /**
     * cid, fileName
     *
     * @throws IOException
     */
    private void initAv() throws IOException {
        String result = HttpUtil.getResponseContent(ApiGetList + aid);
        JSONObject jb = (JSONObject) new JSONArray(result).get(0);
        cid = jb.getInt("cid");

        Document doc = Jsoup.connect(playUrl).get();

        Element ele = doc.selectFirst("div[id=viewbox_report]").selectFirst("h1");
        if (ele.hasAttr("title"))
            fileName = ele.attr("title");

    }

    /**
     * cid, fileName and related eps
     *
     * @throws IOException
     */
    private void initEp() throws IOException {
        Document doc = Jsoup.connect(playUrl).get();
        Element ele = doc.body().child(2);

        String preResult = ele.toString();
        // println(preResult);

        String result = preResult.substring(preResult.indexOf("__=") + 3, preResult.indexOf(";(function()"));
        // println(result);

        JSONObject object = new JSONObject(result);

        JSONObject curEpInfo = object.getJSONObject("epInfo");

        fileName = object.getJSONObject("mediaInfo").getString("title");

        cid = curEpInfo.getInt("cid");


        JSONArray ja = object.getJSONArray("epList");

        for (Object obj : ja) {
            JSONObject epObject = (JSONObject) obj;

            int aid = epObject.getInt("aid");
            int cid = epObject.getInt("cid");
            int duration = epObject.getInt("duration");
            int epId = epObject.getInt("ep_id");

            String index = epObject.getString("index");
            String indexTitle = epObject.getString("index_title");

            BilibiliBean bean = new BilibiliBean(aid, cid, duration, epId, index, indexTitle);

            serialList.add(bean);

            println(bean.toString());
        }
    }

    /**
     * timeLength, fileSize, urls
     *
     * @param avReqApi
     * @throws IOException
     */
    private void parseAvApiResponse(String avReqApi) throws IOException {
        String result = HttpUtil.getResponseContent(avReqApi);

        // println(result);

        JSONObject jsonObject = new JSONObject(result);
        timeLength = jsonObject.getInt("timelength");

        JSONArray ja = jsonObject.getJSONArray("durl");

        Iterator<Object> iterator = ja.iterator();
        while (iterator.hasNext()) {
            JSONObject jb = (JSONObject) iterator.next();

            String videoSrc = jb.getString("url");
            urls.add(videoSrc);

            fileSize += jb.getInt("size");
        }
    }

    /**
     * timeLength, fileSize, urls
     *
     * @param epReqApi
     * @throws IOException
     * @throws DocumentException
     */
    private void parseEpApiResponse(String epReqApi) throws IOException, DocumentException {
        String response = HttpUtil.getResponseContent(epReqApi);

        SAXReader reader = new SAXReader();
        org.dom4j.Element rootElement = reader.read(new ByteArrayInputStream(response.getBytes("utf-8"))).getRootElement();

        timeLength = Integer.parseInt(rootElement.element("timelength").getText().trim());

        List<org.dom4j.Element> elements = rootElement.elements("durl");

        for (org.dom4j.Element ele : elements) {
            int curSize = Integer.parseInt(ele.element("size").getText());
            fileSize += curSize;

            String url = ele.element("url").getText();
            urls.add(url);
        }

        println(fileName + ": " + fileSize / 1024 / 1024 + "M");
    }

    /**
     * 生成av类型视频下载信息的api请求链接
     *
     * @param url
     * @param cid
     * @param quality
     * @return
     */
    private String generateAvApi(String url, int cid, int quality) {
        String paramStr = String.format("appkey=84956560bc028eb7&cid=%d&otype=json&qn=%d&quality=%d&type=", cid, quality, quality);
        try {
            String checkSum = MD5Encoder.md5(paramStr + SEC_1).toLowerCase();
            return url + paramStr + "&sign=" + checkSum;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 生成ep类型视频下载信息的api请求链接
     *
     * @param url
     * @param cid
     * @param quality
     * @return
     */
    private String generateEpApi(String url, int cid, int quality) {
        String paramStr = String.format("cid=%d&module=bangumi&player=1&quality=%d&ts=%s",
                cid, quality, System.currentTimeMillis() / 1000 + "");
        try {
            String checkSum = MD5Encoder.md5(paramStr + SEC_2).toLowerCase();
            return url + paramStr + "&sign=" + checkSum;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

 

 

 完整代码位于:

 https://github.com/asche910/VideoHelper 

 

<think>好的,我现在需要帮助用户找到关于B漫画解析和下载的方法或工具。用户之前的问题已经涉及到了相关的引用内容,比如提到的B漫画下载器这个Python开源项目,以及通过XHR解析获取漫画图片的步骤。首先,我应该整理这些信息,确保回答结构清晰,符合用户的要求。 首先,用户可能对现有的工具有兴趣,特别是那个Python编写的命令行工具。根据引用[1]和[3],这个工具支持批量下载、自定义参数,并且跨平台。我需要详细说明它的功能、安装方法和使用步骤,同时引用相关的参考资料。比如,用户需要安装Python环境,然后通过pip安装依赖,运行命令时指定漫画的URL。 接下来,用户可能想了解手动解析的方法,特别是如何通过分析网络请求获取漫画图片。引用[2]提到了三个关键的POST请求:GetImageIndex、ImageToken和ComicDetail。我需要解释每个步骤的作用,例如如何获取章节信息、图片token以及最终的图片URL。这部分可能需要技术细节,比如使用浏览器的开发者工具抓包,或者编写Python脚本发送请求并处理响应数据。 另外,用户可能关心法律和版权问题,需要提醒他们注意下载内容的授权情况,避免侵权。这也是回答中必不可少的一部分,确保用户在使用工具或方法时遵守相关规定。 然后,生成相关问题。根据用户的需求,可能涉及工具的比较、反爬虫机制、下载后的管理方法,以及与其他平台的对比。这些问题可以帮助用户进一步探索解决方案,或者解决可能遇到的困难。 在组织回答时,需要分点说明,先介绍工具再讲手动方法,保持逻辑顺序。同时,代码示例要符合格式要求,使用正确的LaTeX标注,并且确保所有技术术语准确无误。需要检查引用标记是否正确,比如每个引用来源的标注是否对应正确的参考资料。 最后,确保语言简洁明了,避免使用过于专业的术语,让不同技术水平的用户都能理解。如果有不确定的地方,比如某些API的具体参数是否需要更新,可能需要提醒用户实际情况可能会有变化,建议查阅最新文档或社区支持。</think>### B漫画解析与下载方法详解 #### 一、官方工具推荐 1. **Python开源下载器** - 基于引用[1][3],推荐使用Python编写的开源B漫画下载器,核心功能包括: - 自动识别漫画章节结构 - 多线程下载加速(可自定义线程数) - 支持分辨率/图片质量设置 - 跨平台运行(Windows/macOS/Linux) ```python # 安装与使用示例 pip install bilibili-comic-downloader bcmdl -u https://manga.bilibili.com/detail/mc12345 --quality high ``` 2. **技术实现原理** - 通过逆向工程解析B漫画的API接口: - 获取漫画元数据:`GET /twirp/comic.v1.Comic/ComicDetail` - 解析章节图片:`POST /twirp/comic.v1.Comic/GetImageIndex` - 解密图片Token:`POST /twirp/comic.v1.Comic/ImageToken` [^2] #### 二、手动解析方法 1. **关键请求分析** 使用浏览器开发者工具(F12)抓取网络请求: ```javascript // 典型请求结构 fetch("https://manga.bilibili.com/twirp/comic.v1.Comic/GetImageIndex", { method: "POST", body: JSON.stringify({ep_id: 123456}) }) ``` - 响应数据包含加密的图片路径`path`和文件索引`index` 2. **图片URL生成算法** 根据引用[2]的研究,最终图片地址需通过: $$ \text{真实URL} = \text{CDN域名} + \text{path} + \text{?token=} + \text{ImageToken} $$ 其中`ImageToken`的有效期通常为2小时 #### 三、技术难点突破 1. **反爬机制应对** - 请求头需包含`Origin: https://manga.bilibili.com` - 必须携带登录Cookie中的`SESSDATA` - 图片Token动态验证机制 2. **数据解密示例** ```python # Python解密代码片段 import hashlib def generate_token(path: str, index: int) -> str: salt = "9cafa6466a028bfb" raw = f"{path}{index}{salt}".encode() return hashlib.md5(raw).hexdigest() ``` #### 四、法律与版权声明 使用相关工具时需注意: - 仅下载已购漫画(B账户需登录) - 禁止商业用途传播 - 遵守《信息网络传播权保护条例》第二十一条规定
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值