Java爬蟲爬取漫畫示例
我看大部分的爬蟲入門教學(xué)都是爬取圖片的,但是我測(cè)試了一下,那個(gè)網(wǎng)站現(xiàn)在加了一些反爬措施(如協(xié)議頭部的 referer),并且很容易就會(huì)遇到429(太多請(qǐng)求)這個(gè)問題??赡苁嵌嗑€程速度太快,這也說明了控制爬取的合理速度的重要性。因?yàn)槲乙恢庇锌绰嫷牧?xí)慣,所以就來測(cè)試一下爬取網(wǎng)站的漫畫。(這個(gè)網(wǎng)站是提供試看功能,所以我就拿它來測(cè)試一下吧。)
網(wǎng)站地址(我喜歡的那部漫畫地址):https://www.manhuaniu.com/manhua/5830/
爬取結(jié)果
程序運(yùn)行效果

獲取的文件目錄信息

文件的總信息


注: 這里有一個(gè)小問題,獲取的文件可能有的沒有后綴名,但是可以以圖片的方式打開觀看,具體原因我也不知道,因?yàn)椴挥绊?,也就不去管它了。(或者自己使用代碼,給文件重命名。)
網(wǎng)站結(jié)構(gòu)分析
這里以一部漫畫為例,首先看上面的編號(hào),那個(gè)編號(hào)表示漫畫的目錄頁。這是很重要的,在這一頁有漫畫的目錄。然后依次點(diǎn)擊目錄中的章節(jié),可以看到每一章的漫畫信息。


這里這個(gè)分頁很奇怪,因?yàn)槊恳徽鹿?jié)的頁數(shù)不是一樣的,但是它確實(shí)直接可以選擇的,說明這個(gè)應(yīng)該是提前加載或者異步加載的(我其實(shí)不會(huì)前端的知識(shí),只是聽說了一些。)后來通過查看源(我用眼睛發(fā)現(xiàn)的)發(fā)現(xiàn)確實(shí)是提前加載所有漫畫頁的鏈接。不是異步加載的。
這里我點(diǎn)擊漫畫圖片獲取圖片的地址,然后再和自己發(fā)現(xiàn)的鏈接比對(duì)一下,就看出來了,然后拼接一下 url,就獲取到所有的鏈接了。

在相應(yīng)的章節(jié)頁中,使用瀏覽器的查看源,就可以發(fā)現(xiàn)這樣一段腳本了。經(jīng)過分析,腳本中的數(shù)組里面的信息,就是對(duì)應(yīng)的每一頁漫畫的信息。

上面的截圖是一個(gè)大概的結(jié)構(gòu)信息,所以獲取流程是: 目錄頁–>章節(jié)頁–>漫畫頁
對(duì)于這里,獲取到這段腳本作為字符串,然后以 “[” 和 “]” 獲取字串,然后使用 fastjson 將其轉(zhuǎn)化為一個(gè) List 集合。
// 獲取的script 無法直接解析,必須先將 page url 取出來,
// 這里以 [ ] 為界限,分割字符串。
String pageUrls = script.data();
int start = pageUrls.indexOf("[");
int end = pageUrls.indexOf("]") + 1;
String urls = pageUrls.substring(start, end);
//json 轉(zhuǎn) 集合,這個(gè)可以總結(jié)一下,不熟悉。
List<String> urlList = JSONArray.parseArray(urls, String.class);
這里強(qiáng)調(diào)一點(diǎn):Element對(duì)象的 text 方法是獲取可見信息,而 data 方法是獲取不可見信息。腳本信息是不可直接看見的,所以我使用 data 方法獲取它。所謂可見和不可見大概就是網(wǎng)頁上可以顯示和通過查看源可以獲取的信息的意思。比如轉(zhuǎn)義字符,通過t ext 獲取就變成轉(zhuǎn)義的字符了。
代碼部分
HttpClientUtil 類
使用HttpClient連接池來管理連接,但是我沒有使用多線程,因?yàn)槲抑挥幸粋€(gè)ip地址,萬一被封了,很麻煩。當(dāng)線程的時(shí)間還是可以 接受的,畢竟一部漫畫,大概也就是十來分鐘吧。(以600話為例)
package com.comic;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
public class HttpClientUtil {
private static final int TIME_OUT = 10 * 1000;
private static PoolingHttpClientConnectionManager pcm; //HttpClient 連接池管理類
private static RequestConfig requestConfig;
static {
requestConfig = RequestConfig.custom()
.setConnectionRequestTimeout(TIME_OUT)
.setConnectTimeout(TIME_OUT)
.setSocketTimeout(TIME_OUT).build();
pcm = new PoolingHttpClientConnectionManager();
pcm.setMaxTotal(50);
pcm.setDefaultMaxPerRoute(10); //這里可能用不到這個(gè)東西。
}
public static CloseableHttpClient getHttpClient() {
return HttpClients.custom()
.setConnectionManager(pcm)
.setDefaultRequestConfig(requestConfig)
.build();
}
}
ComicSpider 類
最重要的一個(gè)類,用來解析HTML頁面獲取鏈接數(shù)據(jù)。 注意:這里的 DIR_PATH 是硬編碼路徑,所以你想要測(cè)試,還請(qǐng)自己創(chuàng)建相關(guān)目錄。
package com.comic;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import org.apache.http.HttpEntity;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import com.alibaba.fastjson.JSONArray;
public class ComicSpider {
private static final String DIR_PATH = "D:/DBC/comic/";
private String url;
private String root;
private CloseableHttpClient httpClient;
public ComicSpider(String url, String root) {
this.url = url;
// 這里不做非空校驗(yàn),或者使用下面這個(gè)。
// Objects.requireNonNull(root);
if (root.charAt(root.length()-1) == '/') {
root = root.substring(0, root.length()-1);
}
this.root = root;
this.httpClient = HttpClients.createDefault();
}
public void start() {
try {
String html = this.getHtml(url); //獲取漫畫主頁數(shù)據(jù)
List<Chapter> chapterList = this.mapChapters(html); //解析數(shù)據(jù),得到各話的地址
this.download(chapterList); //依次下載各話。
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 從url中獲取原始的網(wǎng)頁數(shù)據(jù)
* @throws IOException
* @throws ClientProtocolException
* */
private String getHtml(String url) throws ClientProtocolException, IOException {
HttpGet get = new HttpGet(url);
//下面這兩句,是因?yàn)榭偸菆?bào)一個(gè) Invalid cookie header,然后我在網(wǎng)上找到的解決方法。(去掉的話,不影響使用)。
RequestConfig defaultConfig = RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).build();
get.setConfig(defaultConfig);
//因?yàn)槭浅鯇W(xué),而且我這里只是請(qǐng)求一次數(shù)據(jù)即可,這里就簡(jiǎn)單設(shè)置一下 UA
get.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3100.0 Safari/537.36");
HttpEntity entity = null;
String html = null;
try (CloseableHttpResponse response = httpClient.execute(get)) {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) {
entity = response.getEntity();
if (entity != null) {
html = EntityUtils.toString(entity, "UTF-8");
}
}
}
return html;
}
//獲取章節(jié)名 鏈接地址
private List<Chapter> mapChapters(String html) {
Document doc = Jsoup.parse(html, "UTF-8");
Elements name_urls = doc.select("#chapter-list-1 > li > a");
/* 不采用直接返回map的方式,封裝一下。
return name_urls.stream()
.collect(Collectors.toMap(Element::text,
name_url->root+name_url.attr("href")));
*/
return name_urls.stream()
.map(name_url->new Chapter(name_url.text(),
root+name_url.attr("href")))
.collect(Collectors.toList());
}
/**
* 依次下載對(duì)應(yīng)的章節(jié)
* 我使用當(dāng)線程來下載,這種網(wǎng)站,多線程一般容易引起一些問題。
* 方法說明:
* 使用循環(huán)迭代的方法,以 name 創(chuàng)建文件夾,然后依次下載漫畫。
* */
public void download(List<Chapter> chapterList) {
chapterList.forEach(chapter->{
//按照章節(jié)創(chuàng)建文件夾,每一個(gè)章節(jié)一個(gè)文件夾存放。
File dir = new File(DIR_PATH, chapter.getName());
if (!dir.exists()) {
if (!dir.mkdir()) {
try {
throw new FileNotFoundException("無法創(chuàng)建指定文件夾"+dir);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
//開始按照章節(jié)下載
try {
List<ComicPage> urlList = this.getPageUrl(chapter);
urlList.forEach(page->{
SinglePictureDownloader downloader = new SinglePictureDownloader(page, dir.getAbsolutePath());
downloader.download();
});
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
//獲取每一個(gè)頁漫畫的位置
private List<ComicPage> getPageUrl(Chapter chapter) throws IOException {
String html = this.getHtml(chapter.getUrl());
Document doc = Jsoup.parse(html, "UTF-8");
Element script = doc.getElementsByTag("script").get(2); //獲取第三個(gè)腳本的數(shù)據(jù)
// 獲取的script 無法直接解析,必須先將 page url 取出來,
// 這里以 [ ] 為界限,分割字符串。
String pageUrls = script.data();
int start = pageUrls.indexOf("[");
int end = pageUrls.indexOf("]") + 1;
String urls = pageUrls.substring(start, end);
//json 轉(zhuǎn) 集合,這個(gè)可以總結(jié)一下,不熟悉。
List<String> urlList = JSONArray.parseArray(urls, String.class);
AtomicInteger index=new AtomicInteger(0); //我無法使用索引,這是別人推薦的方式
return urlList.stream() //注意這里拼接的不是 root 路徑,而是一個(gè)新的路徑
.map(url->new ComicPage(index.getAndIncrement(),"https://restp.dongqiniqin.com//"+url))
.collect(Collectors.toList());
}
}
注意: 這里我的思路是,所有的漫畫都存放到 DIR_PATH 目錄中。 然后每一章節(jié)是一個(gè)子目錄(以章節(jié)名來命名),然后每一個(gè)章節(jié)的漫畫放到一個(gè)目錄中,但是這里會(huì)遇到一個(gè)問題。因?yàn)閷?shí)際上漫畫是一頁一頁觀看的,所以漫畫就有一個(gè)順序的問題(畢竟一堆亂序漫畫,看起來也很費(fèi)勁,雖然我這里不是為了看漫畫)。所以我就給每一個(gè)漫畫頁一個(gè)編號(hào),按照上面腳本上的順序,進(jìn)行編號(hào)。但是由于我使用了Java8的 Lambda 表達(dá)式,所以我無法使用索引。(這涉及到另一個(gè)問題了)。 這里的解決辦法是我看別人推薦的: 每次調(diào)用 index的 getAndIncrement 方法就可以增加 index 的值,非常方便。
AtomicInteger index=new AtomicInteger(0); //我無法使用索引,這是別人推薦的方式 return urlList.stream() //注意這里拼接的不是 root 路徑,而是一個(gè)新的路徑 .map(url->new ComicPage(index.getAndIncrement(),"https://restp.dongqiniqin.com//"+url)) .collect(Collectors.toList());
Chapter 和 ComicPage 類
兩個(gè)實(shí)體類,因?yàn)槭敲嫦驅(qū)ο舐?,我就設(shè)計(jì)了兩個(gè)簡(jiǎn)單的實(shí)體類來封裝一下信息,這樣操作比較方便一點(diǎn)。
Chapter 類代表的是目錄中的每一個(gè)章節(jié)的信息,章節(jié)的名字和章節(jié)的鏈接。 ComicPage 類代表的是每一個(gè)章節(jié)中的每一頁漫畫信息,每一頁的編號(hào)和鏈接地址。
package com.comic;
public class Chapter {
private String name; //章節(jié)名
private String url; //對(duì)應(yīng)章節(jié)的鏈接
public Chapter(String name, String url) {
this.name = name;
this.url = url;
}
public String getName() {
return name;
}
public String getUrl() {
return url;
}
@Override
public String toString() {
return "Chapter [name=" + name + ", url=" + url + "]";
}
}
package com.comic;
public class ComicPage {
private int number; //每一頁的序號(hào)
private String url; //每一頁的鏈接
public ComicPage(int number, String url) {
this.number = number;
this.url = url;
}
public int getNumber() {
return number;
}
public String getUrl() {
return url;
}
}
SinglePictureDownloader 類
因?yàn)榍皫滋焓褂枚嗑€程下載類爬取圖片,發(fā)現(xiàn)速度太快了,ip 好像被封了,所以就又寫了一個(gè)當(dāng)線程的下載類。 它的邏輯很簡(jiǎn)單,主要是獲取對(duì)應(yīng)的漫畫頁鏈接,然后使用get請(qǐng)求,將它保存到對(duì)應(yīng)的文件夾中。(它的功能大概和獲取網(wǎng)絡(luò)中的一張圖片類似,既然你可以獲取一張,那么成千上百也沒有問題了。)
package com.comic;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Random;
import org.apache.http.HttpEntity;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import com.m3u8.HttpClientUtil;
public class SinglePictureDownloader {
private CloseableHttpClient httpClient;
private ComicPage page;
private String filePath;
private String[] headers = {
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:30.0) Gecko/20100101 Firefox/30.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/537.75.14",
"Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Win64; x64; Trident/6.0)",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.8.1.11) Gecko/20071127 Firefox/2.0.0.11",
"Opera/9.25 (Windows NT 5.1; U; en)",
"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
"Mozilla/5.0 (compatible; Konqueror/3.5; Linux) KHTML/3.5.5 (like Gecko) (Kubuntu)",
"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.12) Gecko/20070731 Ubuntu/dapper-security Firefox/1.5.0.12",
"Lynx/2.8.5rel.1 libwww-FM/2.14 SSL-MM/1.4.1 GNUTLS/1.2.9",
"Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.7 (KHTML, like Gecko) Ubuntu/11.04 Chromium/16.0.912.77 Chrome/16.0.912.77 Safari/535.7",
"Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:10.0) Gecko/20100101 Firefox/10.0 "
};
public SinglePictureDownloader(ComicPage page, String filePath) {
this.httpClient = HttpClientUtil.getHttpClient();
this.page = page;
this.filePath = filePath;
}
public void download() {
HttpGet get = new HttpGet(page.getUrl());
String url = page.getUrl();
//取文件的擴(kuò)展名
String prefix = url.substring(url.lastIndexOf("."));
Random rand = new Random();
//設(shè)置請(qǐng)求頭
get.setHeader("User-Agent", headers[rand.nextInt(headers.length)]);
HttpEntity entity = null;
try (CloseableHttpResponse response = httpClient.execute(get)) {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) {
entity = response.getEntity();
if (entity != null) {
File picFile = new File(filePath, page.getNumber()+prefix);
try (OutputStream out = new BufferedOutputStream(new FileOutputStream(picFile))) {
entity.writeTo(out);
System.out.println("下載完畢:" + picFile.getAbsolutePath());
}
}
}
} catch (ClientProtocolException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
//關(guān)閉實(shí)體,關(guān)于 httpClient 的關(guān)閉資源,有點(diǎn)不太了解。
EntityUtils.consume(entity);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Main 類
package com.comic;
public class Main {
public static void main(String[] args) {
String root = "https://www.manhuaniu.com/"; //網(wǎng)站根路徑,用于拼接字符串
String url = "https://www.manhuaniu.com/manhua/5830/"; //第一張第一頁的url
ComicSpider spider = new ComicSpider(url, root);
spider.start();
}
}
總結(jié)
注:這里的代碼是為了學(xué)習(xí)爬蟲知識(shí),而不是為了看漫畫,如果你想看的話,還是推薦去作者推薦的地方付費(fèi)觀看,畢竟作者創(chuàng)作也是很辛苦的。
這個(gè)網(wǎng)站基本上沒有反爬措施,所以處理起來也很方便(我也就是剛學(xué),只知道一個(gè) UA 和 referer,哈哈!)只要仔細(xì)分析,還是很容易爬取到相應(yīng)信息的。最近看了一些人關(guān)于爬蟲的介紹,使用什么語言并不是最重要的(不一定非要用 python,如果學(xué)Java的爬蟲,必要的Java基礎(chǔ)是必不可少的:IO流、多線程等,還是看你喜歡使用那個(gè),像我就喜歡我最順手的,而不是簡(jiǎn)單的。因?yàn)閜ython意味著學(xué)習(xí)更多的 python 知識(shí),但是我學(xué)習(xí)爬蟲就是為了爬蟲的知識(shí)而已),關(guān)鍵是對(duì)于各種反爬的處理,像我這樣小爬蟲,也就是一個(gè)小玩具而已。不過,千里之行始于足下,這是一個(gè)積累知識(shí)的過程。
到此這篇關(guān)于Java爬蟲爬取漫畫示例的文章就介紹到這了,更多相關(guān)Java爬蟲爬取漫畫內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java設(shè)計(jì)模式之橋模式(Bridge模式)介紹
這篇文章主要介紹了Java設(shè)計(jì)模式之橋模式(Bridge模式)介紹,本文講解了為什么使用橋模式、如何實(shí)現(xiàn)橋模式、Bridge模式在EJB中的應(yīng)用等內(nèi)容,需要的朋友可以參考下2015-03-03
Spring Cloud-Feign服務(wù)調(diào)用的問題及處理方法
Feign 是一個(gè)聲明式的 REST 客戶端,它用了基于接口的注解方式,很方便實(shí)現(xiàn)客戶端配置。接下來通過本文給大家介紹Spring Cloud-Feign服務(wù)調(diào)用,需要的朋友可以參考下2021-10-10
Java連接FTP服務(wù)器并使用ftp連接池進(jìn)行文件操作指南
使用FTP最主要的功能是對(duì)文件進(jìn)行管理,下面這篇文章主要給大家介紹了關(guān)于Java連接FTP服務(wù)器并使用ftp連接池進(jìn)行文件操作的相關(guān)資料,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-08-08
SpringBoot yaml語法與數(shù)據(jù)讀取操作詳解
YAML 是 “YAML Ain’t Markup Language”(YAML 不是一種標(biāo)記語言)的遞歸縮寫。在開發(fā)的這種語言時(shí),YAML 的意思其實(shí)是:“Yet Another Markup Language”(仍是一種標(biāo)記語言),本文給大家介紹的非常詳細(xì),需要的朋友可以參考下2022-07-07
關(guān)于Java中的實(shí)體類要?implements?Serializable的原因分析
這篇文章主要介紹了Java中的實(shí)體類為什么要?implements?Serializable,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-06-06
Spring Security賬戶與密碼驗(yàn)證實(shí)現(xiàn)過程
這篇文章主要介紹了Spring Security賬戶與密碼驗(yàn)證實(shí)現(xiàn)過程,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2023-03-03

