最近在家裏無聊每天刷頭條,看到一個很可愛的小姐姐,突然蹦出一個主意,就是想把它這些視頻全部搞下來存到本地。網上搜了一下,發現這些視頻其實是來自西瓜視頻,根據用戶名搜索就找到了。剛好會一點爬蟲,這下就好辦了。
跟Python的requests和bs4一樣,Java也有HttpClient和Jsoup分別用於發送請求和解析網頁。因爲Jsoup同時也具備發送請求的功能,並且本例也不涉及複雜的請求,所以這裏僅使用Jsoup即可。
研究了一下:首先點到"TA的小視頻"選項卡(小視頻首頁),下面會有視頻列表,每個標籤裏面包含了視頻詳情頁的地址,然後進到視頻詳情頁,這個頁面內容加載一會之後會把視頻的實際地址暴露出來,最後根據這個地址去把視頻下載就OK了。
很快,寫了一段代碼:首先請求小視頻首頁的地址得到頁面內容, 然後把所有視頻標籤解析得到各自的詳情頁地址,接着遍歷這些地址進行請求得到各自的詳情頁內容,最後解析拿到視頻實際地址就可以下載了。
跑了一下,很可惜,遇到了兩個難題
- 不管是視頻列表頁還是視頻詳情頁,直接請求拿到的頁面內容都是一堆js代碼:它們的頁面內容都是js加載生成的。
- 視頻列表頁剛進去只加載一部分,滾動條下拉纔會繼續加載更多內容。
遇到這種情況,讓你不得不使用Selenium了。其實,Jsoup+Selenium是一對黃金搭檔,唯一不好的就是Selenium也具備一點解析網頁的功能使得作用有點重疊(強迫症覺得)。
到此,完整思路如下
- 首先使用Selenium打開小視頻首頁,然後一直下拉滾動條直到視頻列表全部加載出來,接着把網頁內容交給Jsoup解析,得到所有小視頻的詳情頁地址。
- 使用Selenium分別打開每個小視頻詳情頁(多線程並行),等到加載完成視頻實際地址暴露出來之後,就可以使用Jsoup去下載視頻了。
成果展示
項目展示
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();
}
}
}