最近在家里无聊每天刷头条,看到一个很可爱的小姐姐,突然蹦出一个主意,就是想把它这些视频全部搞下来存到本地。网上搜了一下,发现这些视频其实是来自西瓜视频,根据用户名搜索就找到了。刚好会一点爬虫,这下就好办了。

java爬取动态网页 java爬取网站视频_java爬取动态网页

java爬取动态网页 java爬取网站视频_java爬取动态网页_02

跟Python的requests和bs4一样,Java也有HttpClient和Jsoup分别用于发送请求和解析网页。因为Jsoup同时也具备发送请求的功能,并且本例也不涉及复杂的请求,所以这里仅使用Jsoup即可。

研究了一下:首先点到"TA的小视频"选项卡(小视频首页),下面会有视频列表,每个标签里面包含了视频详情页的地址,然后进到视频详情页,这个页面内容加载一会之后会把视频的实际地址暴露出来,最后根据这个地址去把视频下载就OK了。

很快,写了一段代码:首先请求小视频首页的地址得到页面内容, 然后把所有视频标签解析得到各自的详情页地址,接着遍历这些地址进行请求得到各自的详情页内容,最后解析拿到视频实际地址就可以下载了。

跑了一下,很可惜,遇到了两个难题

  1. 不管是视频列表页还是视频详情页,直接请求拿到的页面内容都是一堆js代码:它们的页面内容都是js加载生成的。
  2. 视频列表页刚进去只加载一部分,滚动条下拉才会继续加载更多内容。

遇到这种情况,让你不得不使用Selenium了。其实,Jsoup+Selenium是一对黄金搭档,唯一不好的就是Selenium也具备一点解析网页的功能使得作用有点重叠(强迫症觉得)。

到此,完整思路如下

  • 首先使用Selenium打开小视频首页,然后一直下拉滚动条直到视频列表全部加载出来,接着把网页内容交给Jsoup解析,得到所有小视频的详情页地址。
  • 使用Selenium分别打开每个小视频详情页(多线程并行),等到加载完成视频实际地址暴露出来之后,就可以使用Jsoup去下载视频了。

成果展示

java爬取动态网页 java爬取网站视频_Selenium_03

项目展示

1、pom文件,引入Jsoup和Selenium的依赖。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>cn.zhh</groupId>
    <artifactId>crawl-xigua-video</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.jsoup/jsoup -->
        <dependency>
            <groupId>org.jsoup</groupId>
            <artifactId>jsoup</artifactId>
            <version>1.12.1</version>
        </dependency>

        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>selenium-chrome-driver</artifactId>
            <version>3.141.59</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <encoding>UTF-8</encoding>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

2、Java代码,配置小视频首页地址和下载目录,把浏览器驱动放在指定位置。

package cn.zhh;

import org.jsoup.Connection;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.openqa.selenium.Keys;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.interactions.Actions;

import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * 主类
 * 修改“小视频首页”和“存放目录”之后运行main函数即可
 *
 * @author Zhou Huanghua
 */
@SuppressWarnings("all")
public class Application {

    /**
     * 小视频首页,按需修改
     */
    private static final String MAIN_PAGE_URL = "https://www.ixigua.com/home/3276166340814919/hotsoon/";

    /**
     * 存放目录,按需修改
     */
    private static final String FILE_SAVE_DIR = "C:/Users/SI-GZ-1766/Desktop/MP4/";

    /**
     * 线程池,按需修改并行数量。实际开发请自定义避免OOM
     */
    private static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    /**
     * 谷歌浏览器参数
     */
    private static final ChromeOptions CHROME_OPTIONS = new ChromeOptions();

    static {
        // 驱动位置
        System.setProperty("webdriver.chrome.driver", "src/main/resources/static/chromedriver.exe");
        // 避免被浏览器检测识别
        CHROME_OPTIONS.setExperimentalOption("excludeSwitches", Collections.singletonList("enable-automation"));
    }

    /**
     * main函数
     *
     * @param args 运行参数
     * @throws InterruptedException 睡眠中断异常
     */
    public static void main(String[] args) throws InterruptedException {
        // 获取小视频列表的div元素,批量处理
        Document mainDoc = Jsoup.parse(getMainPageSource());
        Elements divItems = mainDoc.select("div[class=\"BU-CardB UserDetail__main__list-item\"]");
        // 这里使用CountDownLatch关闭线程池,只是避免执行完一直没退出
        CountDownLatch countDownLatch = new CountDownLatch(divItems.size());
        divItems.forEach(item ->
                EXECUTOR.execute(() -> {
                    try {
                        Application.handleItem(item);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    countDownLatch.countDown();
                })
        );
        countDownLatch.await();
        EXECUTOR.shutdown();
        System.exit(0);
    }

    /**
     * 获取首页内容
     *
     * @return 首页内容
     * @throws InterruptedException 睡眠中断异常
     */
    private static String getMainPageSource() throws InterruptedException {
        ChromeDriver driver = new ChromeDriver(CHROME_OPTIONS);
        try {
            driver.get(MAIN_PAGE_URL);
            long waitTime = Double.valueOf(Math.max(3, Math.random() * 5) * 1000).longValue();
            TimeUnit.MILLISECONDS.sleep(waitTime);
            long timeout = 30_000;
            // 循环下拉,直到全部加载完成或者超时
            do {
                new Actions(driver).sendKeys(Keys.END).perform();
                TimeUnit.MILLISECONDS.sleep(waitTime);
                timeout -= waitTime;
            } while (!driver.getPageSource().contains("已经到底部,没有新的内容啦")
                    && timeout > 0);
            return driver.getPageSource();
        } finally {
            driver.close();
        }
    }

    /**
     * 处理每个小视频
     *
     * @param div 小视频div标签元素
     * @throws Exception 各种异常
     */
    private static void handleItem(Element div) throws Exception {
        String href = div.getElementsByTag("a").first().attr("href");
        String src = getVideoUrl("https://www.ixigua.com" + href);
        // 有些blob开头的(可能还有其它)暂不处理
        if (src.startsWith("//")) {
            Connection.Response response = Jsoup.connect("https:" + src)
                    // 解决org.jsoup.UnsupportedMimeTypeException: Unhandled content type. Must be text/*, application/xml, or application/xhtml+xml. Mimetype=video/mp4, URL=
                    .ignoreContentType(true)
                    // The default maximum is 1MB.
                    .maxBodySize(100 * 1024 * 1024)
                    .execute();
            Files.write(Paths.get(FILE_SAVE_DIR, href.substring(1) + ".mp4"), response.bodyAsBytes());
        } else {
            System.out.println("无法解析的src:[" + src + "]");
        }
    }

    /**
     * 获取小视频实际链接
     *
     * @param itemUrl 小视频详情页
     * @return 小视频实际链接
     * @throws InterruptedException 睡眠中断异常
     */
    private static String getVideoUrl(String itemUrl) throws InterruptedException {
        ChromeDriver driver = new ChromeDriver(CHROME_OPTIONS);
        try {
            driver.get(itemUrl);
            long waitTime = Double.valueOf(Math.max(5, Math.random() * 10) * 1000).longValue();
            long timeout = 50_000;
            Element v;
            /**
             * 循环等待,直到链接出来
             * ※这里可以考虑浏览器驱动自带的显式等待()和隐士等待
             */
            do {
                TimeUnit.MILLISECONDS.sleep(waitTime);
                timeout -= waitTime;
            } while ((Objects.isNull(v = Jsoup.parse(driver.getPageSource()).getElementById("vs"))
                        || Objects.isNull(v = v.getElementsByTag("video").first()))
                    && timeout > 0);

            return v.attr("src");
        } finally {
            driver.close();
        }
    }
}