SeimiCrawler是一个敏捷的,独立部署的,支持分布式的Java爬虫框架,希望能在最大程度上降低新手开发一个可用性高且性能不差的爬虫系统的门槛,以及提升开发爬虫系统的开发效率。在SeimiCrawler的世界里,绝大多数人只需关心去写抓取的业务逻辑就效率高够了,其余的Seimi帮你搞定。

接触过Jsoup的同学肯定了解过Jsouphtml5Xpath,JsoupXpath极大简化了Jsoup数据库软件爬虫的使用,他的作者就是Sei效率意识方面存在的问题miCrawler的作者,汪浩淼。数据库软件

在此向大佬致敬!

如今的Sei数据库设计miCrawler已经可以整合入Sprinhtml个人网页完整代码gBoothttps认证效率英文翻译,本文将使用SpringBoot进行快速构windows7旗舰版建。官方文档中也有详细说明,本文不再赘述。

一、使用SpringBoot构效率是什么意思建项目

这里我使用的是idea,项目构建时习惯性勾上了Spring Web。 我们只windows11好用吗需添加SeimiCrawler官方依赖即可。 这里我多加了一个lombok依赖,如果你没有使用过可以效率意识方面存在的问Windows忽略,在实体类中增加构造方html法即可。

        <dependency>
            <groupId>cn.wanghaomiao</groupId>
            <artifactId>SeimiCrawler</artifactId>
            <version>2.1.2</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.12</version>
        </dependency>

二、编写代码

1.项目结构如下:

SeimiCrawler爬取B站视频(Java爬虫永不为奴)

2.实体类:

package com.junki.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
 * 视频实体类,暂时数据不保存到数据库,只是为了解析json,所以只写了两个字段。
 * @author junki
 * @date 2020/3/20 20:57
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Video {
    private String title;
    private String arcurl;
}

3.爬虫类:

package com.junki.crawlers;
import cn.wanghaomiao.seimi.annotation.Crawler;
import cn.wanghaomiao.seimi.def.BaseSeimiCrawler;
import cn.wanghaomiao.seimi.struct.Request;
import cn.wanghaomiao.seimi.struct.Response;
import com.alibaba.fastjson.JSON;
import com.junki.entity.Video;
import jodd.io.FileUtil;
import org.jsoup.Connection;
import org.jsoup.Jsoup;
import org.seimicrawler.xpath.JXDocument;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 * B站视频爬虫
 * 特别声明:本代码仅供学习使用,所爬取视频不得用于商业用途,请务必在24小时内删除!
 * @author junki
 */
@Crawler(name = "bili")
public class BiliCrawler extends BaseSeimiCrawler {
    /**
     * 创建基础目录,用于保存爬取的资源
     */
    private String baseDir = "E:/bilibili/";
    /**
     * 创建基础url,把分页处先替换成{pageNum}
     */
    private String baseUrl = "https://s.search.bilibili.com/cate/search?callback=jqueryCallback_bili_7223822642229205&main_ver=v3&search_type=video&view_type=hot_rank&order=click&copy_right=-1&cate_id=28&page={pageNum}&pagesize=20&jsonp=jsonp&time_from=20200313&time_to=20200320&_=1584708487005";
    /**
     * 用于保存cookie
     */
    private Map<String, String> cookies = null;
    /**
     * 获取需要爬取的网页入口地址
     * @return url数组
     */
    @Override
    public String[] startUrls() {
        // 创建数组存放前五页的url地址
        String[] urls = new String[5];
        for (int i = 0; i < 5; i++) {
            urls[i] = baseUrl.replace("{pageNum}", i + 1 + "");
        }
        // 获取cookie
        Connection.Response response = null;
        try {
            response = Jsoup.connect("https://www.bilibili.com/").ignoreContentType(true).execute();
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (response != null) {
            cookies = response.cookies();
            logger.info("cookies={}", cookies);
        }
        return urls;
    }
    /**
     * 开始爬虫
     * @param response 自动请求url数组中url得到的响应体
     */
    @Override
    public void start(Response response) {
        // 获取返回的内容
        String content = response.getContent();
        // 获取其中的json部分
        String result = content.substring(content.indexOf(""result":") + 9, content.indexOf(","show_column""));
        logger.info("{}", result);
        // 将json转为实体集合
        List<Video> videos = JSON.parseArray(result, Video.class);
        // 遍历集合获取每个视频的链接并进入视频页面
        videos.forEach(video -> {
            Request request = Request.build(video.getArcurl(), "getVideoAndAudioUrl");
            Map<String,String> header = new HashMap<>();
            header.put("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36");
            // 添加cookie,防止请求失败
            header.put("Cookie", cookies.toString().replace("{", "").replace("}", "").replace(",", ";"));
            // 将稿件标题添加到请求头,方便后续请求中获取,这里将特殊字符替换为空格,避免创建目录失败
            header.put("title", video.getTitle().replaceAll("[\^/:*?"<>|]", " ").trim());
            request.setHeader(header);
            push(request);
        });
    }
    /**
     * 爬取视频和音频的url地址
     * @param response 请求每个视频地址得到的响应体
     */
    public void getVideoAndAudioUrl(Response response) {
        logger.info("稿件地址:{}", response.getUrl());
        JXDocument doc = response.document();
        // 获取页面标题
        Object pageTitle = doc.selOne("//title/text()");
        // 标题为空或者包含出错啦,说明没有请求成功,直接结束
        if (pageTitle == null || pageTitle.toString().contains("出错啦")) {
            return;
        }
        // 获取页面源码内容
        String content = response.getContent();
        // B站较新的投稿,视频音频源都是分离的,所以分别获取视频和音频的地址
        int videoUrlBeginIndex = content.indexOf(""baseUrl":", content.indexOf(""video":"));
        int videoUrlEndIndex = content.indexOf("",", videoUrlBeginIndex);
        String videoUrl = content.substring(videoUrlBeginIndex + 11, videoUrlEndIndex);
        int audioUrlBeginIndex = content.indexOf(""baseUrl":", content.indexOf(""audio":"));
        int audioUrlEndIndex = content.indexOf("",", audioUrlBeginIndex);
        String audioUrl = content.substring(audioUrlBeginIndex + 11, audioUrlEndIndex);
        logger.info("视频地址:{}", videoUrl);
        logger.info("音频地址:{}", audioUrl);
        // 构造请求头参数
        Map<String,String> header = new HashMap<>();
        // 设置Referer,防止请求数据源失败
        header.put("Referer", response.getUrl());
        header.put("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36");
        header.put("title", response.getRequest().getHeader().get("title"));
        // 开始下载音频
        Request audioRequest = Request.build(audioUrl, "getAudio");
        audioRequest.setHeader(header);
        push(audioRequest);
        // 开始下载视频
        Request videoRequest = Request.build(videoUrl, "getVideo");
        videoRequest.setHeader(header);
        push(videoRequest);
    }
    /**
     * 音频下载
     * @param response 请求音频源的响应体
     */
    public void getAudio(Response response) {
        // 获取稿件标题
        String title = response.getRequest().getHeader().get("title");
        // 创建目录
        File file = new File(baseDir + title);
        file.mkdirs();
        // 开始爬取音频
        try {
            FileUtil.writeBytes(new File(baseDir + title + "/audio.mp3"), response.getData());
            logger.info("音频下载完成={}", title);
        } catch (IOException e) {
            logger.info("音频下载失败={};异常={}", title, e);
        }
    }
    /**
     * 视频下载
     * @param response 请求视频源的响应体
     */
    public void getVideo(Response response) {
        // 获取稿件标题
        String title = response.getRequest().getHeader().get("title");
        // 创建目录
        File file = new File(baseDir + title);
        file.mkdirs();
        // 开始爬取视频
        try {
            FileUtil.writeBytes(new File(baseDir + title + "/video.mp4"), response.getData());
            logger.info("视频下载完成={}", title);
        } catch (IOException e) {
            logger.info("视频下载失败={};异常={}", title, e);
        }
        // 判断音频是否爬取完毕,如果没有1秒后继续判断,音频比视频爬取快很多,一般不会判断很多次
        while (! new File(baseDir + title + "/audio.mp3").exists()) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 如果音频文件也爬取完毕,就开始合并视频音频
        mergeVideoAndAudio(this.getClass().getClassLoader().getResource("ffmpeg.exe").getPath(), baseDir + title + "/video.mp4", baseDir + title + "/audio.mp3", baseDir + title + "/merge.mp4");
        logger.info("视频音频合并成功={}", title);
    }
    /**
     * 合并视频音频
     * @param ffmpegPath ffmpeg.exe路径
     * @param videoPath 视频文件路径
     * @param audioPath 音频文件路径
     * @param outPath 合并后输出路径
     */
    private static void mergeVideoAndAudio(String ffmpegPath, String videoPath, String audioPath, String outPath) {
        List<String> commend = new ArrayList<>();
        commend.add(ffmpegPath);
        commend.add("-i");
        commend.add(audioPath);
        commend.add("-i");
        commend.add(videoPath);
        commend.add("-acodec");
        commend.add("copy");
        commend.add("-vcodec");
        commend.add("copy");
        commend.add(outPath);
        try {
            ProcessBuilder builder = new ProcessBuilder(commend);
            builder.command(commend);
            Process p = builder.start();
            if (p.isAlive()) {
                p.waitFor();
            }
            p.destroy();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

4.applicationwindows许可证即将过期怎么办.properties配置文件:

#启动SeimiCrawler
seimi.crawler.enabled=true
seimi.crawler.names=bili

5.fwindows11有必要升级吗fmpeg.exe文件:

你可效率高发票查验以去ffmpeg官网下载,下方可以选择Windows并下载exe文件。

三、核心代码解析

1.@Crawler注解

SeimiCrawler爬取B站视频(Java爬虫永不为奴)
该注解,我只写了一个name属性,用于和配置文件中的name对应,表数据库系统概论第五版课后答案示启动这个爬虫类。

2.属性参数

SeimiCrawler爬取B站视频(Java爬虫永不为奴)

这里的三个属性,分别用于设置爬取资源保存路径、起始页面基路径、全局C效率符号ookie效率,你也可以考虑在配置文件中配置然后注入值HTTPS

这里着重讲解起始页面基路径:获取起始路径是学习爬虫的关键之一,这里我略讲路径获取过程效率: 首先我看上了B站音乐区:

SeimiCrawler爬取B站视频(Java爬虫永不为奴)
然后准备爬取热效率公式榜:
SeimiCrawler爬取B站视频(Java爬虫永不为奴)
此处视频时延迟加载:
SeimiCrawler爬取B站视频(Java爬虫永不为奴)
分析network找到请求路径,点击翻页瞬间就找到了:
SeimiCrawler爬取B站视频(Java爬虫永不为奴)
效率的英文请求拿到浏览器直接访问,我们要的数据就在result里面了:
SeimiCrawler爬取B站视频(Java爬虫永不为奴)
至此,地址我们已经拿到了,找到分页参数,我将它替换成了{pageNum}方便后续进行字效率意识方面存在的问题符串替效率高换。

3.核心方法

这里我们继承BaseSwindows10eimiCrawler之后重写了两个方法:

SeimiCrawler爬取B站视频(Java爬虫永不为奴)

其中windows是什么意思效率高发票查验startUrls用户返回所要爬取的地址,我这里通HTTPS过遍历生Windows成了前5页的地址,你可可以自行修改,或者通过配置文件配置。 需要注意的是,这里我用了原生Jsoup获取了Cook数据库系统工程师ie:

SeimiCrawler爬取B站视频(Java爬虫永不为奴)

其中start方法用于开始爬虫,这里通过字符串切割获取需要的j效率符号sonhtml文件怎么打开部分然后解析成实体html简单网页代码集合:

SeimiCrawler爬取B站视频(Java爬虫永不为奴)
需要注意的是,这里创建请求对象时数据库系统工程师,指定的callBack就是我下方编写的处理方法名,Seimi会自动帮我发送请求,并用指定方法获取响应,此过程有消息队列,多线程异步:
SeimiCrawler爬取B站视频(Java爬虫永不为奴)

4.其它方法

getVideoAndAudioUrl方法获取视频和音频地址是关键,B站较新的视频都是视频音频分离的,网页源码中可以找到地址,比较windows是什么意思老的视频只有一个视频,获取方式不一样。另外不同分区略有差别,特别是电影区,这部分的优化我会在后续的博客中更新。

SeimiCrawler爬取B站视频(Java爬虫永不为奴)

既然音频视频分离,那就一定要合并音频视频,所以有了m效率高windows11有必要升级吗erge数据库系统工程师率高发票查验VideoAndAudio方法,这里调用了FFmpeg实现了合并,效率极高。FFmpeg是一套可以用来记录、转https安全问题换数字https协议音频、视频,并能将其转化为流的开源计算机程序。这里不再对FFm数据库系统工程师peg进行赘述。

四、运行结果

直接运行SpringBoot启效率英文翻译动类即可,为了让大家看到效果,我加了数据库系统工程师大量日志,你可以选择关闭日志。

SeimiCrawler爬取B站视频(Java爬虫永不为奴)

本地文件:

SeimiCrawler爬取B站视频(Java爬虫永不为奴)

每个目录下都有三个文件,分别是纯音频、纯视频和合并后的视数据库原理频:

SeimiCrawler爬取B站视频(Java爬虫永不为奴)

五、特别声明

本文https安全问题旨在使用实例讲解SeimiCrhttps安全问题awler爬虫,并非针对B站。

本代码仅供学习使用,所爬取视频不得用于商业用途,请务必在24小时内删除!