程式運行效果
所取得的檔案目錄資訊
#檔案的總資訊
#註: 這裡有個小問題,取得的檔案可能有的沒有後綴名,但是可以以圖片的方式打開觀看,具體原因我也不知道,因為不影響,也就不去管它了。 (或自己使用程式碼,為檔案重新命名。)
這裡以一部漫畫為例,先看上面的編號,那個編號表示漫畫的目錄頁。這是很重要的,在這一頁有漫畫的目錄。然後依序點選目錄中的章節,可以看到每一章的漫畫資訊。
這裡這個分頁很奇怪,因為每一章的頁數不是一樣的,但是它確實直接可以選擇的,說明這個應該是提前加載或非同步加載的(我其實不會前端的知識,只是聽說了一些。)後來通過查看源(我用眼睛發現的)發現確實是提前加載所有漫畫頁的連結。不是異步載入的。
這裡我點擊漫畫圖片取得圖片的地址,然後再和自己發現的連結比對一下,就看出來了,然後拼接一下 url,就獲取到所有的連結了。
在對應的章節頁中,使用瀏覽器的檢視來源,就可以發現這樣一段腳本了。經過分析,腳本中的陣列裡面的訊息,就是對應的每一頁漫畫的資訊。
上面的截圖是一個大概的結構訊息,所以獲取流程是:目錄頁–>章節頁–>漫畫頁
對於這裡,取得到這段腳本作為字串,然後以「[」 和「]」 取得字符串,然後使用fastjson 將其轉換為List 集合。
// 获取的script 无法直接解析,必须先将 page url 取出来, // 这里以 [ ] 为界限,分割字符串。 String pageUrls = script.data(); int start = pageUrls.indexOf("["); int end = pageUrls.indexOf("]") + 1; String urls = pageUrls.substring(start, end); //json 转 集合,这个可以总结一下,不熟悉。 List<String> urlList = JSONArray.parseArray(urls, String.class);
這裡強調一點:Element物件的 text 方法是獲取可見信息,而 data 方法是獲取不可見信息。腳本資訊是不可直接看見的,所以我使用 data 方法來取得它。所謂可見和不可見大概就是網頁上可以顯示和透過查看來源可以獲得的資訊的意思。例如轉義字符,透過t ext 取得就變成轉義的字符了。
使用HttpClient連接池來管理連接,但是我沒有使用多線程,因為我只有一個ip位址,萬一被封了,很麻煩。當線程的時間還是可以 接受的,畢竟一部漫畫,大概也就是十來分鐘吧。 (以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); //这里可能用不到这个东西。 } public static CloseableHttpClient getHttpClient() { return HttpClients.custom() .setConnectionManager(pcm) .setDefaultRequestConfig(requestConfig) .build(); } }
最重要的一個類,用來解析HTML頁面取得連結資料。注意:這裡的 DIR_PATH 是硬編碼路徑,所以你想要測試,也請自己建立相關目錄。
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; // 这里不做非空校验,或者使用下面这个。 // 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); //获取漫画主页数据 List<Chapter> chapterList = this.mapChapters(html); //解析数据,得到各话的地址 this.download(chapterList); //依次下载各话。 } catch (IOException e) { e.printStackTrace(); } } /** * 从url中获取原始的网页数据 * @throws IOException * @throws ClientProtocolException * */ private String getHtml(String url) throws ClientProtocolException, IOException { HttpGet get = new HttpGet(url); //下面这两句,是因为总是报一个 Invalid cookie header,然后我在网上找到的解决方法。(去掉的话,不影响使用)。 RequestConfig defaultConfig = RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).build(); get.setConfig(defaultConfig); //因为是初学,而且我这里只是请求一次数据即可,这里就简单设置一下 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; } //获取章节名 链接地址 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()); } /** * 依次下载对应的章节 * 我使用当线程来下载,这种网站,多线程一般容易引起一些问题。 * 方法说明: * 使用循环迭代的方法,以 name 创建文件夹,然后依次下载漫画。 * */ public void download(List<Chapter> chapterList) { chapterList.forEach(chapter->{ //按照章节创建文件夹,每一个章节一个文件夹存放。 File dir = new File(DIR_PATH, chapter.getName()); if (!dir.exists()) { if (!dir.mkdir()) { try { throw new FileNotFoundException("无法创建指定文件夹"+dir); } catch (FileNotFoundException e) { e.printStackTrace(); } } //开始按照章节下载 try { List<ComicPage> urlList = this.getPageUrl(chapter); urlList.forEach(page->{ SinglePictureDownloader downloader = new SinglePictureDownloader(page, dir.getAbsolutePath()); downloader.download(); }); } catch (IOException e) { e.printStackTrace(); } } }); } //获取每一个页漫画的位置 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); //获取第三个脚本的数据 // 获取的script 无法直接解析,必须先将 page url 取出来, // 这里以 [ ] 为界限,分割字符串。 String pageUrls = script.data(); int start = pageUrls.indexOf("["); int end = pageUrls.indexOf("]") + 1; String urls = pageUrls.substring(start, end); //json 转 集合,这个可以总结一下,不熟悉。 List<String> urlList = JSONArray.parseArray(urls, String.class); AtomicInteger index=new AtomicInteger(0); //我无法使用索引,这是别人推荐的方式 return urlList.stream() //注意这里拼接的不是 root 路径,而是一个新的路径 .map(url->new ComicPage(index.getAndIncrement(),"https://restp.dongqiniqin.com//"+url)) .collect(Collectors.toList()); } }
注意: 這裡我的想法是,所有的漫畫都存放到 DIR_PATH 目錄中。然後每一章節都是一個子目錄(以章節名來命名),然後每一個章節的漫畫放到一個目錄中,但是這裡會遇到一個問題。因為實際上漫畫是一頁一頁觀看的,所以漫畫就有一個順序的問題(畢竟一堆亂序漫畫,看起來也很費勁,雖然我這裡不是為了看漫畫)。所以我就給每一個漫畫頁一個編號,按照上面腳本上的順序,進行編號。但是由於我使用了Java8的 Lambda 表達式,所以我無法使用索引。 (這涉及到另一個問題了)。這裡的解決方法是我看別人推薦的: 每次呼叫 index的 getAndIncrement
方法就可以增加 index 的值,非常方便。
AtomicInteger index=new AtomicInteger(0); //我无法使用索引,这是别人推荐的方式 return urlList.stream() //注意这里拼接的不是 root 路径,而是一个新的路径 .map(url->new ComicPage(index.getAndIncrement(),"https://restp.dongqiniqin.com//"+url)) .collect(Collectors.toList());
兩個實體類,因為是物件導向嘛,我就設計了兩個簡單的實體類別來封裝一下訊息,這樣操作比較方便一點。
Chapter 類別代表的是目錄中的每一個章節的信息,章節的名字和章節的連結。 ComicPage 類別代表的是每一個章節中的每一頁漫畫訊息,每一頁的編號和連結位址。
package com.comic; public class Chapter { private String name; //章节名 private String url; //对应章节的链接 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; //每一页的序号 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; } }
因为前几天使用多线程下载类爬取图片,发现速度太快了,ip 好像被封了,所以就又写了一个当线程的下载类。 它的逻辑很简单,主要是获取对应的漫画页链接,然后使用get请求,将它保存到对应的文件夹中。(它的功能大概和获取网络中的一张图片类似,既然你可以获取一张,那么成千上百也没有问题了。)
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(); //取文件的扩展名 String prefix = url.substring(url.lastIndexOf(".")); Random rand = new Random(); //设置请求头 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 { //关闭实体,关于 httpClient 的关闭资源,有点不太了解。 EntityUtils.consume(entity); } catch (IOException e) { e.printStackTrace(); } } } }
package com.comic; public class Main { public static void main(String[] args) { String root = "https://www.manhuaniu.com/"; //网站根路径,用于拼接字符串 String url = "https://www.manhuaniu.com/manhua/5830/"; //第一张第一页的url ComicSpider spider = new ComicSpider(url, root); spider.start(); } }
以上是怎麼使用Java爬取漫畫的詳細內容。更多資訊請關注PHP中文網其他相關文章!