之前接触爬虫的时候,就想着试试爬一下B站的视频,经过网上搜索和查阅资料,写了多线程爬B站视频的代码。
1. 分析链接
在分析链接之前,应该都有感觉,B站的是视频播放是片段型的(可以看到有一个灰色的进度条在我们观看的时候也会一直向前走)
示例:https://www.bilibili.com/video/BV1gs411N7op?from=search&seid=8067122269966733021
打开这个视频播放界面,打开开发者工具可以看到很多以m4s结尾的请求
再仔细看看,其实只有两个请求一直被重复发送,一个是30080,一个是30216;
网上有介绍,一个是视频链接一个是音频链接;
这也验证了之前说的视频播放是片段型的,即每一小段视频都是以m4s请求表现得;
那要爬虫,两个问题,每段视频(音频)的大小怎么体现?哪个是视频链接,哪个是音频链接?
- 问题1:查看请求详情,可以看到请求头里面有一个参数Range,是一个范围,就是该片段的大小(字节)
- 问题2:再看响应头里面的一个参数Content-Range,前半部分是该片段的大小,在请求头里面的Range中有体现,后半部分就是这个视频(音频)的总大小
再看30216请求的该参数
可以看出,总大小是有差别的,即大的是视频,小的就是音频
2. 整理思路,准备爬虫
第一部已经确定了视频和音频的链接,以及知道了每次如何通过参数控制请求片段的大小;为了避免一个一个片段的下载,在能知道视频(音频)总大小的情况下,那么就可以通过改Range参数,达到直接下载完整视频的方法
具体方法就是:先发一个试探请求,得到响应头里面的Content-Range参数,拿到视频(音频)总大小,在发送一个请求,将请求头中的Range参数改为 bytes=0-总大小即可
那如何在只知视频url的情况下,找到该视频的两个m4s请求呢?
在该url的源代码里面能看到很多m4s链接,而且他们的开头还写了video和audio,是以数组的形式呈现的
一个对应视频链接一个对应音频链接
至于有多个,应该是为了做主备模式的吧
这样,在我们成功爬取到网页源代码之后,在里面正则表达式匹配即可得到所有的视频以及音频链接
3. 写代码,多线程形式下载,以及实时监控下载速度
利用多线程直接爬取B站搜索页面的所有视频,循环再提取每个视频链接
核心代码:
package crawler.bilibili;
import crawler.utils.GetWebData;
import crawler.utils.StringUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @Author: xiaoshijiu
* @Date: 2020/8/10
* @Description: $value$
*/
public class CrawlerBilibiliSerachViedo {
private static Pattern pattern = Pattern.compile("\"baseUrl\":\".+?\"");
private static CountDownLatch countDownLatch = new CountDownLatch(12);
/**
* 已经下载的bytes大小(使用线程安全的类)
*/
private static AtomicLong downloadBytes = new AtomicLong();
/**
* 上一秒已经下载的bytes大小
*/
private static long lastDownloadBytes;
public static void main(String[] args) {
long begin = System.currentTimeMillis();
Map<String, String> map = new HashMap<>();
map.put("user-agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36");
String data = GetWebData.getWebDataWithHeaders(
"https://search.bilibili.com/all?keyword=%E9%87%91%E6%B3%AB%E9%9B%85", "get",
"utf-8", map);
Document document = Jsoup.parse(data);
Elements elements = document.select(".video-list>li");
// 存放视频打开链接
List<String> viedoLinkList = new ArrayList<>();
for (Element element : elements) {
viedoLinkList.add("https:" + element.select("a").first().attr("href"));
}
int len = viedoLinkList.size();
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(20, 50, 500, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(len));
// 为避免ip被封,只爬六个视频
for (int i = 0; i < 6; i++) {
poolExecutor.execute(downloadViedo(viedoLinkList.get(i), map, i, poolExecutor));
}
// 开启下载速度监控
ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1);
executorService.scheduleAtFixedRate(() -> {
long speed = downloadBytes.get() - lastDownloadBytes;
System.out.println("下载速度:" + StringUtils.convertToDownloadSpeed(speed));
lastDownloadBytes = downloadBytes.get();
}, 1, 1, TimeUnit.SECONDS);
// 利用countdownlatch,实现main线程等待任务线程,达到监控效果
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 必须关闭线程池,不然进程不会结束
poolExecutor.shutdown();
// 关闭定时
executorService.shutdown();
// 计时
double time = (System.currentTimeMillis() - begin) / 60000.00;
System.out.printf("完成,总用时:%.2f分钟\n", time);
}
private static Runnable downloadViedo(String url, Map<String, String> map, int i,
ThreadPoolExecutor poolExecutor) {
return () -> {
Map<String, String> map1 = new HashMap<>();
map1.putAll(map);
String data = GetWebData.getWebDataWithHeaders(url, "get", "utf-8", map);
// 正则匹配到m4s链接,加入到集合中(取第一个为视频链接,最后一个为音频链接)
List<String> list = new ArrayList<>();
Matcher matcher = pattern.matcher(data);
while (matcher.find()) {
list.add(matcher.group(0).split("\"baseUrl\":\"")[1].replace("\"", ""));
}
// 试探找出视频和音频的最大长度
map1.put("Range", "bytes=0-100");
map1.put("Referer", url);
map1.put("Origin", "https://www.bilibili.com");
map1.put("Sec-Fetch-Mode", "cors");
Map<String, String> map2 = new HashMap<>();
map2.putAll(map1);
Map<String, String> vHeaders = GetWebData.getWebDataResponseHeaders(list.get(0), "get",
map1);
String vLength = vHeaders.get("Content-Range");
int vBytes = Integer.parseInt(vLength.split("/")[1]);
Map<String, String> aHeaders = GetWebData
.getWebDataResponseHeaders(list.get(list.size() - 1), "get", map1);
String aLength = aHeaders.get("Content-Range");
int aBytes = Integer.parseInt(aLength.split("/")[1]);
// 修改请求大小,下载视频和音频
map1.put("Range", "bytes=0-" + vBytes);
map2.put("Range", "bytes=0-" + aBytes);
poolExecutor.execute(() -> {
downFilesWithHeaders(list.get(0), "K:/crawler/bilibili/search", i + ".mp4", map1);
countDownLatch.countDown();
});
poolExecutor.execute(() -> {
downFilesWithHeaders(list.get(list.size() - 2), "K:/crawler/bilibili/search",
i + ".mp3", map2);
countDownLatch.countDown();
});
};
}
效果:
4. 得到了视频和音频文件,再利用格式工厂等工具将他们合并即可,清晰度也很高哦
5. tips:对m3u8和ts格式视频爬虫感兴趣的也可以滴滴我,有写过Demo