B站视频下载(VideoHelper)

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

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

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

抓包是注意几个参数:

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

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

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

这里详细的流程我就不介绍了(其实我是来宣传VideoHelper 的,目前还支持知乎等网站视频, 欢迎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;

/**

// 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,509评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,806评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,875评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,441评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,488评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,365评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,190评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,062评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,500评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,706评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,834评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,559评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,167评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,779评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,912评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,958评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,779评论 2 354