m4s格式,多线程爬B站视频

之前接触爬虫的时候,就想着试试爬一下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

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值