Java爬蟲之批量下載LibreStock圖片(可輸入關鍵詞查詢下載)

佛系programer發表於2019-02-19

前言(廢話)

公司產品新版本剛剛上線,所以也終於得空休息一下了,有了一點時間。由於之前看到過爬蟲,可以把網頁上的資料通過程式碼自動提取出來,覺得挺有意思的,所以也想接觸一下,但是網上很多爬蟲很多都是基於Python寫的,本人之前也學了一點Python基礎,但是還沒有那麼熟練和自信能寫出東西來。所以就想試著用Java寫一個爬蟲,說起馬上開幹!爬點什麼好呢,一開始還糾結了一下,到底是文字還是音樂還是什麼呢,突然想起最近自己開始練習寫文章,文章需要配圖,因為文字太枯燥,看著密密麻麻的文字,誰還看得下去啊,俗話說好圖配文章,閱讀很清爽 ~ 哈哈哈ヾ(◍°∇°◍)ノ゙”,對,我的名字就叫俗話,皮了一下嘻嘻~。所以要配一個高質量的圖片才能賞心悅目,所以就想要不爬個圖片吧,這樣以後媽媽再也不用擔心我的文章配圖了。加上我之前看到過一個國外的圖片網站,質量絕對高標準,我還經常在上面找桌布呢,而且支援各種尺寸高清下載,還可以自定義尺寸啊,最重要的是免費哦 ~ 簡直不要太方便,在這裡也順便推薦給大家,有需要的可以Look一下,名字叫LibreStock。其實這篇文章的配圖就是從這上面爬下來的哦~好了,說了這麼多其實都是廢話,下面開始進入正題。

概述

爬蟲,顧名思義,是根據網頁上的資料特徵進行分析,然後編寫邏輯程式碼對這些特徵資料進行提取加工為自己可用的資訊。ImageCrawler是一款基於Java編寫的爬蟲程式,可以爬取LibreStock上的圖片資料並下載到本地,支援輸入關鍵詞爬取,執行效果如下。

Java爬蟲之批量下載LibreStock圖片(可輸入關鍵詞查詢下載)
Java爬蟲之批量下載LibreStock圖片(可輸入關鍵詞查詢下載)
Java爬蟲之批量下載LibreStock圖片(可輸入關鍵詞查詢下載)

分析

首先開啟LibreStock網站,點選F12檢視原始碼,如下圖

Java爬蟲之批量下載LibreStock圖片(可輸入關鍵詞查詢下載)

從圖中可以看出每個圖片對應的一個

  • 元素,
  • 下有一個圖片的超連結,這個就是對應圖片的詳情地址,所以這裡還不能拿到圖片的源地址,我們再點進去再看
    Java爬蟲之批量下載LibreStock圖片(可輸入關鍵詞查詢下載)

    可以看到,在多層的div有一個href超連結,這個就是圖片的源地址,但是好像下面還有href誒,而且也是圖片的地址,這裡不用管,我們取一個就可以。這個href是在image-section__photo-wrap-width的這個div裡面的,所以大概特徵我們就找到了。此處你認為就完成了就太天真了,經過我多次測試,踩了一些坑之後才發現並沒有那麼簡單。

    其實最開始我的做法是通過比較列表頁的

  • 下的src屬性和圖片詳情頁的源地址href屬性,然後將src中的值進行提取拼接成一個固定格式的連結,這個連結就是這張圖片的源地址(後面發現圖片的源地址連結可以有很多,其中可以配置不同的圖片引數,連結就是對應引數的圖片),然後進行下載。後來發現此方法並不穩定,因為測試發現並不是所有圖片源地址都支援這種格式,而且這種方法也只能獲取到頁面第一次載入的圖片數,因為下拉會載入更多,這個時候是處理不了這種情況的,此時就很尷尬了 ~ 既然要爬取,肯定是要針對大量資料的,所以首先是要解決不能爬取下拉載入更多的這種情況,既然有資料載入,肯定會設計到網路訪問,於是通過Fiddler進行抓包發現下拉到底部的時候會觸發一個非同步載入。
    Java爬蟲之批量下載LibreStock圖片(可輸入關鍵詞查詢下載)

    會請求一次介面,然後返回下一頁的列表資料,既然知道了資料的獲取方式,我們就可以偽造一個一模一樣的資料請求,然後拿到下一頁的資料。但是什麼時候載入完呢,通過觀察發現每次介面的返回資料裡有一個js的部分,如下圖:

    Java爬蟲之批量下載LibreStock圖片(可輸入關鍵詞查詢下載)

    這個last_page就是標識,當載入到最後一頁時,last_page就會為true,但是我們只能獲取到返回的資料的字串,怎麼對這個js的函式進行判斷呢,測試發現載入到最後一頁時,False==True這個會變成True==True,所以可以通過判斷這個字串來作為爬取的頁數標識。好了,至此就解決了載入更多的問題。

    我們可以拿到每一頁的圖片列表資料,但是圖片列表裡面沒有圖片的源地址,接下來就是解決這個問題,我之前一直都是想直接通過爬取列表頁的資料就拿到源地址,但是發現通過拼接的源地址並不適用於所有的圖片,於是我試著改變思路,通過

  • 中的資料拿到圖片的詳細地址,再進行一次原始碼爬取,這樣就可以拿到對應圖片的詳細頁面的原始碼了,通過詳情頁的原始碼就可以獲取到圖片的源地址了。OK,大體流程就是這樣了。

    實施

    通過分析已經清楚大致的流程了,接下來就是編碼實現了。由於本人從事的是Android開發,所以專案就建在了一個Android專案裡,但是可以單獨執行的Java程式。
    首先需要偽造一個一模一樣的非同步網路請求,觀察上面圖中的資料可以看出,請求包含一些頭部的設定和token引數等配置資訊,照著寫下來就可以了,另外,請求是一個Post,還帶有三個引數(querypagelast_id),query則是我們查詢的圖片的關鍵詞,page是當前頁數,last_id不清楚,不用管,設定為固定的和模板請求一樣的即可。

     public static String requestPost(String url, String query, String page, String last_id) {
    
            String content = "";
            HttpsURLConnection connection = null;
            try {
    
                URL u = new URL(url);
                connection = (HttpsURLConnection) u.openConnection();
                connection.setRequestMethod("POST");
                connection.setConnectTimeout(50000);
                connection.setReadTimeout(50000);
                connection.setRequestProperty("Host", "librestock.com");
                connection.setRequestProperty("Referer", "https://librestock.com/photos/scenery/");
                connection.setRequestProperty("X-Requested-With", "XMLHttpRequest");
                connection.setRequestProperty("Origin", "https://librestock.com");
                connection.setRequestProperty("User-Agent", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.3; Trident/7.0;rv:11.0)like Gecko");
                connection.setRequestProperty("Accept-Language", "zh-CN");
                connection.setRequestProperty("Connection", "Keep-Alive");
                connection.setRequestProperty("Charset", "UTF-8");
                connection.setRequestProperty("X-CSRFToken", "0Xf4EfSJg03dOSx5NezCugrWmJV3lQjO");
                connection.setRequestProperty("Cookie", "__cfduid=d8e5b56c62b148b7450166e1c0b04dc641530080552;cookieconsent_status=dismiss;csrftoken=0Xf4EfSJg03dOSx5NezCugrWmJV3lQjO;_ga=GA1.2.1610434762.1516843038;_gid=GA1.2.1320775428.1530080429");
    
                connection.setDoInput(true);
                connection.setDoOutput(true);
                connection.setUseCaches(false);
    
                if (!TextUtil.isNullOrEmpty(query) && !TextUtil.isNullOrEmpty(page) && !TextUtil.isNullOrEmpty(last_id)) {
                    DataOutputStream out = new DataOutputStream(connection
                            .getOutputStream());
                    // 正文,正文內容其實跟get的URL中 `? `後的引數字串一致
                    String query_string = "query=" + URLEncoder.encode(query, "UTF-8");
                    String page_string = "page=" + URLEncoder.encode(page, "UTF-8");
                    String last_id_string = "last_id=" + URLEncoder.encode(last_id, "UTF-8");
                    String parms_string = query_string + "&" + page_string + "&" + last_id_string;
                    out.writeBytes(parms_string);
                    //流用完記得關
                    out.flush();
                    out.close();
                }
                connection.connect();
    
                int code = connection.getResponseCode();
                System.out.println("第" + page + "頁POST網頁解析連線響應碼:" + code);
                if (code == 200) {
                    InputStream in = connection.getInputStream();
                    InputStreamReader isr = new InputStreamReader(in, "utf-8");
                    BufferedReader reader = new BufferedReader(isr);
                    String line;
                    while ((line = reader.readLine()) != null) {
                        content += line;
                    }
                }
            } catch (MalformedURLException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (connection != null) {
                    connection.disconnect();
                }
            }
            return content;
        }
    複製程式碼

    如上程式碼就是偽造的請求方法,返回當前請求的結果原始碼HTML,拿到原始碼以後,我們需要提取最後一頁引數標識。如果是最後一頁,則更新標識,將不再請求。

     public boolean isLastPage(String html) {
            //採用Jsoup解析
            Document doc = Jsoup.parse(html);
            //獲取Js內容,判斷是否最後一頁
            Elements jsEle = doc.getElementsByTag("script");
            for (Element element : jsEle) {
                String js_string = element.data().toString();
                if (js_string.contains(""False" == "True"")) {
                    return false;
                } else if (js_string.contains(""True" == "True"")) {
                    return true;
                }
            }
            return false;
        }
    複製程式碼

    拿到列表原始碼,我們還需要解析出列表中的圖片的詳情連結。測試發現列表的詳情href的div格式又是可變的,這裡遇到兩種格式,不知道有沒有第三種,但是兩種已經可以適應絕大部分了。

            //獲取html標籤中的img的列表資料
            Elements elements = doc.select("li[class=image]");//第一種格式
            if (elements == null || elements.size() == 0) {
                elements = doc.select("ul[class=photos]").select("li[class=image]");//第二種格式
            }
            if (elements == null) return imageModels;
            int size = elements.size();
            for (int i = 0; i < size; i++) {
                Element ele = elements.get(i);
                Elements hrefEle = ele.select("a[href]");
                if (hrefEle == null || hrefEle.size() == 0) {
                    System.out.println("第" + page + "頁第" + (i + 1) + "個檔案hrefEle為空");
                    continue;
                }
                String img_detail_href = hrefEle.attr("href");
    複製程式碼

    拿到圖片詳情頁的超連結後,再請求一次詳情頁面連結,拿到詳情頁面的原始碼,

    String img_detail_entity = HttpRequestUtil.requestGet(img_detail_href, page, (i + 1));//獲取詳情原始碼
    複製程式碼

    然後就可以對原始碼進行解析,拿到圖片的源地址,測試發現圖片源地址的格式也有多種,經過實踐發現大概分為四種,獲取到圖片源地址,我們可以對這個源地址url進行提取檔名作為下載儲存的檔名,然後儲存到圖片模型中,所以根據詳情頁原始碼提取出圖片源地址的程式碼如下:

    public ImageModel getModel(String img_detail_html) throws Exception {
            if (TextUtil.isNullOrEmpty(img_detail_html)) return null;
            //採用Jsoup解析
            Document doc = Jsoup.parse(img_detail_html);
            //獲取html標籤中的內容
            String image_url = doc.select("div[class=img-col]").select("img[itemprop=url]").attr("src");//第一種
            if (TextUtil.isNullOrEmpty(image_url)) {
                image_url = doc.select("div[class=image-section__photo-wrap-width]").select("a[href]").attr("href");//第二種
            }
            if (TextUtil.isNullOrEmpty(image_url)) {
                image_url = doc.select("span[itemprop=image]").select("img").attr("src");//第三種
            }
            if (TextUtil.isNullOrEmpty(image_url)) {
                image_url = doc.select("div[id=download-image]").select("img").attr("src");//第四種
            }
            if (TextUtil.isNullOrEmpty(image_url)) return null;
    
            ImageModel imageModel = new ImageModel();
            String image_name = TextUtil.getFileName(image_url);
            imageModel.setImage_url(image_url);
            imageModel.setImage_name(image_name);
            return imageModel;
        }
    複製程式碼

    綜上上面的程式碼,從網頁列表原始碼中提取出多個圖片模型的程式碼如下:

     public Vector<ImageModel> getImgModelsData(String html, int page) throws Exception {
            //獲取的資料,存放在集合中
            Vector<ImageModel> imageModels = new Vector<>();
            //採用Jsoup解析
            Document doc = Jsoup.parse(html);
            //獲取html標籤中的img的列表資料
            Elements elements = doc.select("li[class=image]");
            if (elements == null || elements.size() == 0) {
                elements = doc.select("ul[class=photos]").select("li[class=image]");
            }
            if (elements == null) return imageModels;
            int size = elements.size();
            for (int i = 0; i < size; i++) {
                Element ele = elements.get(i);
                Elements hrefEle = ele.select("a[href]");
                if (hrefEle == null || hrefEle.size() == 0) {
                    System.out.println("第" + page + "頁第" + (i + 1) + "個檔案hrefEle為空");
                    continue;
                }
                String img_detail_href = hrefEle.attr("href");
                if (TextUtil.isNullOrEmpty(img_detail_href)) {
                    System.out.println("第" + page + "頁第" + (i + 1) + "個檔案img_detail_href為空");
                    continue;
                }
                String img_detail_entity = HttpRequestUtil.requestGet(img_detail_href, page, (i + 1));
                if (TextUtil.isNullOrEmpty(img_detail_entity)) {
                    System.out.println("第" + page + "頁第" + (i + 1) + "個檔案網頁實體img_detail_entity為空");
                    continue;
                }
                ImageModel imageModel = getModel(img_detail_entity);
                if (imageModel == null) {
                    System.out.println("第" + page + "頁第" + (i + 1) + "個檔案模型imageModel為空");
                    continue;
                }
                imageModel.setPage(page);
                imageModel.setPostion((i + 1));
                //將每一個物件的值,儲存到List集合中
                imageModels.add(imageModel);
            }
            //返回資料
            return imageModels;
        }
    複製程式碼

    獲取到圖片的源地址後,接下來就是下載到本地了,一個頁面有多個圖片,所以下載用執行緒池比較合適。因為一個列表頁是24張圖片,所以這裡執行緒池的大小就設為24,解析完一個頁面的列表,就把這個頁面的圖片列表傳給下載器, 當這個列表的任務完成以後,就去解析下一頁的資料,然後重複迴圈這個過程,直到判斷是最後一頁了,就結束此次爬取。

    public void startDownloadList(Vector<ImageModel> downloadList, String keyword) {
            HttpURLConnection connection = null;
            //迴圈下載
            try {
                for (int i = 0; i < downloadList.size(); i++) {
                    pool = Executors.newFixedThreadPool(24);
    
                    ImageModel imageModel = downloadList.get(i);
                    if (imageModel == null) continue;
                    final String download_url = imageModel.getImage_url();
                    final String filename = imageModel.getImage_name();
                    int page = imageModel.getPage();
                    int postion = imageModel.getPostion();
    
                    Future<HttpURLConnection> future = pool.submit(new Callable<HttpURLConnection>() {
                        @Override
                        public HttpURLConnection call() throws Exception {
                            URL url;
                            url = new URL(download_url);
                            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                            //設定超時間為3秒
                            connection.setConnectTimeout(3 * 1000);
                            //防止遮蔽程式抓取而返回403錯誤
                            connection.setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
                            return connection;
                        }
                    });
                    connection = future.get();
                    if (connection == null) continue;
                    int responseCode = connection.getResponseCode();
                    System.out.println("正在下載第" + page + "頁第" + postion + "個檔案,地址:" + download_url + "響應碼:" + connection.getResponseCode());
                    if (responseCode != 200) continue;
                    InputStream inputStream = connection.getInputStream();
                    if (inputStream == null) continue;
                    writeFile(inputStream, "d:\ImageCrawler\" + keyword + "\", URLDecoder.decode(filename, "UTF-8"));
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (null != connection)
                    connection.disconnect();
                if (null != pool)
                    pool.shutdown();
                while (true) {
                    if (pool.isTerminated()) {//所有子執行緒結束,執行回撥
                        if (downloadCallBack != null) {
                            downloadCallBack.allWorksDone();
                        }
                        break;
                    }
                }
            }
        }
    複製程式碼

    儲存到本地的程式碼如下,儲存到的是自定義資料夾的目錄,目錄的名稱是輸入的爬取的關鍵詞,下載的圖片的名字是根據源地址的url提取得到

    public void writeFile(InputStream inputStream, String downloadDir, String filename) {
            try {
                //獲取自己陣列
                byte[] buffer = new byte[1024];
                int len = 0;
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                while ((len = inputStream.read(buffer)) != -1) {
                    bos.write(buffer, 0, len);
                }
                bos.close();
    
                byte[] getData = bos.toByteArray();
    
                //檔案儲存位置
                File saveDir = new File(downloadDir);
                if (!saveDir.exists()) {
                    saveDir.mkdir();
                }
                File file = new File(saveDir + File.separator + filename);
                FileOutputStream fos = new FileOutputStream(file);
                fos.write(getData);
                if (fos != null) {
                    fos.close();
                }
                if (inputStream != null) {
                    inputStream.close();
                }
            } catch (MalformedURLException e) {
                e.printStackTrace();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    複製程式碼

    好了,所有的工作都完成了,讓我們跑起來看看效果吧 ~ 輸入wallpaper為查詢的關鍵詞,然後回車,可以看到控制檯輸出了資訊(對於我這個強迫症來首,看起來很舒適),資料夾也生成了對應的圖片檔案,OK,大功告成!

    Java爬蟲之批量下載LibreStock圖片(可輸入關鍵詞查詢下載)

    以上就是整個爬取的流程,最後,完整的程式碼已經上傳到了github,歡迎各位小夥伴fork。

  • 相關文章