NiceLeeのBlog 用爱发电 bilibili~

Java 爬虫练习-bilibili视频下载 (三)

2019-01-24
nIceLee

阅读:


之前PC端Flv格式的视频下载算是有了能下载的办法,但是分辨率的选择仍旧是个问题。
本文记录定位API的相关思考。

思路

方案一

混淆加密是真的烦,还有各种临时load的js,定位十分麻烦,。要想想怎么个搞法才能定位找到有效的接口内容。
。。。
过了一段时间,我在播放页面上来回瞎捣鼓,突然一个想法,除了开始页面的时候,我在什么时候才在不存在页面跳转的情况下向服务器请求查询视频链接???
sources/archive/2019/01/24-720p.png

显然,是人工更换视频清晰度的时候。比如,我换了个720p的清晰度,那么除非已经知道,否则浏览器应该就会向服务器发起720p的查询请求。果然,第一个请求就是:
sources/archive/2019/01/24-playurl.png

仔细分析一下返回的json,发现当前返回的视频质量、可提供的视频质量、该av下的所有视频以及对应链接都有了。
sources/archive/2019/01/24-json-video.png

确认可行的情况下,我们再来看一下怎样构造请求:
主要关注点还是http方法(Get/Post/PUT/…),请求url,request参数和header构造。

  • 默认get方式没毛病;
  • url 已经get到了,如下,https://api.bilibili.com/x/player/playurl
    sources/archive/2019/01/24-request-url.png

  • request参数,如图,这个最好要多试几个av观察一下:
    sources/archive/2019/01/24-request-param.png

最后发现关键参数还是qn/avid/cid,其它可以不变。

  • qn - 视频质量,容易获得
  • avid - 这个就不说了
  • cid - avid下面毕竟可能不止一个视频,所以还需要一个id
  • header构造就不讲了,毕竟前面也提过,cookie后面再说。

现在问题就在request参数的cid这个参数了,那么我们复制这个请求里面的cid,从页面源代码里面搜一搜,发现ok(多p视频同样pass)。
sources/archive/2019/01/24-page.png
到这里其实已经差不多解决了:

  1. 模拟访问av播放页面,获取作品信息以及想要的cid;
  2. 构造参数请求https://api.bilibili.com/x/player/playurl,返回下载链接
  3. 构造参数,下载视频

这样可以解决,但对于强迫症来说不够圆满,想把查询视频信息的接口也整出来(如果不费事的话),于是就有了方案二。

方案二

好了,现在明确方向了,就是想找返回json类型的请求(xhr先不管,瞅一下找不到就不折腾了,毕竟已经有方案了),看看bilibili有木有这方面的接口。

也好。。。

刷新一下界面,再排一下类型。一共就三个json类型,瞅瞅找到了,包括前面半天才搞到的下载链接接口。话说我在搞母鸡啊o((>ω< ))o
sources/archive/2019/01/24-order-json.png

分析一波,发现虽然查询的是参数是需要avid和cid两个,但是返回的是整个多p的信息,感觉这个cid没啥用啊,要我是开发肯定不会多此一举再拿这个参数搞事。

当然搞事也没辙,需要cid的话还不如采用前面的方案一。

然后测试一波,发现cid填错或者不填对结果没啥影响,哈哈。O(∩_∩)O

类似前面:

  1. 模拟访问https://api.bilibili.com/x/web-interface/view?aid=52454,获取作品信息(包括cid)
  2. 构造参数请求https://api.bilibili.com/x/player/playurl?otype=json&fnver=0&fnval=2&player=1&qn=16&avid=52454&cid=90108,返回下载链接
  3. 构造参数,下载视频

关键代码

/**
 * 获取AVid 视频的所有信息(全部)
 * 
 * @param avId
 * @param isGetLink
 * @return
 */
public VideoInfo getVideoDetail(String avId, boolean isGetLink) {
    VideoInfo viInfo = new VideoInfo();
    viInfo.setVideoId(avId);

    String avIdNum = avId.replace("av", "");
    String url = "https://api.bilibili.com/x/web-interface/view?aid=" + avIdNum;
    HttpHeaders headers = new HttpHeaders();
    String json = util.getContent(url, headers.getBiliJsonAPIHeaders(avId),
            HttpCookies.getGlobalCookies());

    JSONObject jObj = new JSONObject(json).getJSONObject("data");
    String videoName = jObj.getString("title");
    String brief = jObj.getString("desc");
    String author = jObj.getJSONObject("owner").getString("name");
    String authorId = String.valueOf(jObj.getJSONObject("owner").getLong("mid"));
    String videoPreview = jObj.getString("pic");
    viInfo.setVideoName(videoName);
    viInfo.setBrief(brief);
    viInfo.setAuthor(author);
    viInfo.setAuthorId(authorId);
    viInfo.setVideoPreview(videoPreview);

    //
    JSONArray array = jObj.getJSONArray("pages");
    HashMap<Integer, ClipInfo> clipMap = new HashMap<Integer, ClipInfo>();
    for (int i = 0; i < array.length(); i++) {
        JSONObject clipObj = array.getJSONObject(i);
        ClipInfo clip = new ClipInfo();
        clip.setAvId(avId);
        clip.setcId(clipObj.getLong("cid"));
        clip.setPage(clipObj.getInt("page"));
        clip.setTitle(clipObj.getString("part"));

        int qnList[] = getVideoQNList(avId, String.valueOf(clip.getcId()));
        HashMap<Integer, String> links = new HashMap<Integer, String>();
        for (int qn : qnList) {
            if(isGetLink) {
                String link = getVideoFLVLink(avId, String.valueOf(clip.getcId()), qn);
                links.put(qn, link);
            }else {
                links.put(qn, "");
            }
        }
        clip.setLinks(links);
        clipMap.put(clip.getPage(), clip);
    }
    viInfo.setClips(clipMap);
    viInfo.print();
    return viInfo;
}

/**
 * 查询视频链接
 * 
 * @param avId 视频的avid
 * @param cid  av下面可能不只有一个视频, avId + cid才能确定一个真正的视频
 * @param qn   112: hdflv2;80: flv; 64: flv720; 32: flv480; 16: flv360
 * @return
 */
public String getVideoFLVLink(String avId, String cid, int qn) {
    String avIdNum = avId.replace("av", "");
    String url = "https://api.bilibili.com/x/player/playurl?fnval=2&fnver=0&player=1&otype=json&avid=%s&cid=%s&qn=%s";
    url = String.format(url, avIdNum, cid, qn);
    // System.out.println(url);
    HttpHeaders headers = new HttpHeaders();
    String json = util.getContent(url, headers.getBiliJsonAPIHeaders(avId),
            HttpCookies.getGlobalCookies());
    JSONObject jObj = new JSONObject(json).getJSONObject("data");
    int linkQN = jObj.getInt("quality");
    System.out.println("查询质量为:" + qn + "的链接, 得到质量为:" + linkQN + "的链接");
    return jObj.getJSONArray("durl").getJSONObject(0).getString("url");
}

遗留问题

前面一直避而不谈cookies的问题,是抱着万一的心态不想搞太复杂增加工作量
(毕竟万一需要的话还得整一个模拟登录来获取cookies,因为让用户自己动手从浏览器抓感觉很Low…话说除了模拟登录,整个cookie插件也不错~ o( ̄▽ ̄)o)

可惜,结果还是出了点毛病。。。也是,好歹高清资源是要登录,更高清更是要大会员的。。
其它接口没问题,还是下载链接查询出了毛病,毛病是啥呢?
打个比方,请求1080p高清资源时,没登录的话可能返回的是320p。

所以,下一步就是模拟登录咯。。。

源代码


内容
隐藏