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

首先介绍下如何下载吧: 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; /**
* 哔哩哔哩: 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

B站视频下载(VideoHelper)的更多相关文章

  1. b站视频下载技术分享

    最近无聊分析了一下b站的视频流协议,简单分享下爬取的流程. 首先先要找到视频对应的aid和cid,aid就相当于av号,而av号对应网页下的每一个视频都有对应的cid,普通视频就是分p,番剧就是集数, ...

  2. 大学MOOC课程视频下载、流文件合并、批量重命名、b站视频下载及学习课程视频推荐

    计算机行业技术更新快,编程语言种类多,在当今大数据和人工智能的时代,为了能在相关领域有所成就,就必须掌握好python.R等语言,较好的数学基础和深入的行业背景知识.计算机从业人员务必践行" ...

  3. B站视频下载

    借助Chrome插件 bilibili哔哩哔哩下载助手 在谷歌应用商城下载安装后在在浏览器右上角显示如下图标 打开想要下载的视频,网页右下角会有如下图标,点击该图标 点击下面的合并下载按钮即可 htt ...

  4. 【教你zencart仿站 文章1至6教训 高清1280x900视频下载】[支持手机端]

    [教你zencart仿站 第1至6课 高清晰1280x900视频下载][支持移动端] 经过筹备, 我们的课件最终出来了- 我们 zencart联盟合伙人 项目推出的 在线yy同步演示zencart仿站 ...

  5. 爱奇艺|B站|优酷|腾讯视频高清无水印视频下载方法(软件工具教程)

    导读:经常在大型视频网站平台上看到一些很价值和视频,希望能高清无水印下载到本地学习观看,今天小程序定制开发代码哥DaiMaGe6给大家分享一招免费下载全网高清无水印视频的方法. 高清无水印视频下载工具 ...

  6. 【玩具】获取B站视频的音频片段

    事情是这样的,我有个和社畜的社会地位不太相符的小爱好--听音乐剧. 基本上是在B站上点开视频听,不是不想在网易云或者QQ音乐听,只是在这些音乐软件上面,我想听的片段要不就收费,要不版本不是我喜欢的,要 ...

  7. 视频下载四大神器—如何下载优酷/爱奇艺/腾讯/B站超清无水印视频

      视频下载四大神器—如何下载优酷/爱奇艺/腾讯/B站超清无水印视频  2018-07-11 |  标签»下载, 下载工具, 视频 又是视频下载,老生常谈的话题.阿刚同学已在乐软博客多次与大家分享推荐 ...

  8. Downie for Mac最强视频下载工具(支持B站优酷土豆腾讯等)

    我搜集到的一款简单拖放链接到Downie,它就会下载该网站上的视频.理论可以下载各种视频网站上的视频! 应用介绍 Downie 是一款Mac平台上的优秀视频下载软件,使用非常简单,只需将下载链接放置D ...

  9. Youtube最佳Red5 官方视频下载指南,字幕【亲测成功】

    前言 最近在研究Red5 流媒体服务框架,官网上的信息足以让一个新手入门 有官方參考手冊 -- 高速了解red5的相关信息 有Red5 on Stackoverflow  -- 在上面能够提问或者回答 ...

随机推荐

  1. C#常见编译报错

    mCaster.PlayAnim(ANIMID.ASTD); No overload for method 'PlayAnim' takes '1' arguments PlayAnim()内有两个参 ...

  2. 苹果微信内置浏览器cookie

    苹果微信内置浏览器cookie会被自动清掉,但safari不会清除,原因还未找到,解决方法是把前端把数据通过header传到后台

  3. vs2010远程调试断点无效问题

    ps:本人按照下面的方式设置成功,个人感觉写的也比较清楚 来源:http://www.cnblogs.com/OpenCoder/archive/2010/02/17/1668983.html   v ...

  4. [转载] C++ namespaces 使用

    原地址:http://blog.sina.com.cn/s/blog_986c99d601010hiv.html 命名空间(namespace)是一种描述逻辑分组的机制,可以将按某些标准在逻辑上属于同 ...

  5. Springcloud踩坑记---使用feignclient远程调用服务404

    公司项目进行微服务改造,由之前的dubbo改用SpringCloud,微服务之间通过FeignClient进行调用,今天在测试的时候,eureka注册中心有相应的服务,但feignclient就是无法 ...

  6. MVC进阶篇(三)——model层数据验证

    前言 常常在想,姓名性别那些个验证,真的有必要每次遇到,每次写验证吗?好麻烦,于是学到MVC这里,发现MVC自带数据验证,这个东西着实是个好东西.我写了一个小demo,分享给大家. 内容 一个表单的提 ...

  7. 【bzoj2437】[Noi2011]兔兔与蛋蛋 二分图最大匹配+博弈论

    Description Input 输入的第一行包含两个正整数 n.m. 接下来 n行描述初始棋盘.其中第i 行包含 m个字符,每个字符都是大写英文字母"X".大写英文字母&quo ...

  8. Error creating bean with name 'dateSource' defined in file 错误信息

    问题的原因: 在web项目中搭建SSM框架,启动Tomcat时出现错误信息 有配置文件:applicationContext-mybatis.xml (Spring配置) spring-servlet ...

  9. nRF51822外设应用[2]:GPIOTE的应用-按键检测

    版权声明:本文为博主原创文章,转载请注明作者和出处.    作者:强光手电[艾克姆科技-无线事业部] 1. nRF51822寄存器类型 nRF51822的寄存器和一般的单片机有所差别,nRF51822 ...

  10. js new关键字 和 this详解

    构造函数 ,是一种特殊的函数.主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中. 构造函数用于创建一类对象,首字母要大写. 构造函数要和new一起 ...