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

前言(廢話)

公司產品新版本剛剛上線,因此也終於得空休息一下了,有了一點時間。因爲以前看到過爬蟲,能夠把網頁上的數據經過代碼自動提取出來,以爲挺有意思的,因此也想接觸一下,可是網上不少爬蟲不少都是基於Python寫的,本人以前也學了一點Python基礎,可是尚未那麼熟練和自信能寫出東西來。因此就想試着用Java寫一個爬蟲,提及立刻開幹!爬點什麼好呢,一開始還糾結了一下,究竟是文本仍是音樂仍是什麼呢,忽然想起最近本身開始練習寫文章,文章須要配圖,由於文字太枯燥,看着密密麻麻的文字,誰還看得下去啊,俗話說好圖配文章,閱讀很清爽 ~ 哈哈哈ヾ(◍°∇°◍)ノ゙」,對,個人名字就叫俗話,皮了一下嘻嘻~。因此要配一個高質量的圖片才能賞心悅目,因此就想要不爬個圖片吧,這樣之後媽媽不再用擔憂個人文章配圖了。加上我以前看到過一個國外的圖片網站,質量絕對高標準,我還常常在上面找壁紙呢,並且支持各類尺寸高清下載,還能夠自定義尺寸啊,最重要的是免費哦 ~ 簡直不要太方便,在這裏也順便推薦給你們,有須要的能夠Look一下,名字叫LibreStock。其實這篇文章的配圖就是從這上面爬下來的哦~好了,說了這麼多其實都是廢話,下面開始進入正題。html

概述

爬蟲,顧名思義,是根據網頁上的數據特徵進行分析,而後編寫邏輯代碼對這些特徵數據進行提取加工爲本身可用的信息。ImageCrawler是一款基於Java編寫的爬蟲程序,能夠爬取LibreStock上的圖片數據並下載到本地,支持輸入關鍵詞爬取,運行效果以下。git

分析

首先打開LibreStock網站,點擊F12查看源碼,以下圖github

從圖中能夠看出每一個圖片對應的一個
  • 元素,
  • 下有一個圖片的超連接,這個就是對應圖片的詳情地址,因此這裏還不能拿到圖片的源地址,咱們再點進去再看

    能夠看到,在多層的div有一個href超連接,這個就是圖片的源地址,可是好像下面還有href誒,並且也是圖片的地址,這裏不用管,咱們取一個就能夠。這個href是在image-section__photo-wrap-width的這個div裏面的,因此大概特徵咱們就找到了。此處你認爲就完成了就太天真了,通過我屢次測試,踩了一些坑以後才發現並無那麼簡單。數組

    其實最開始個人作法是經過比較列表頁的bash

  • 下的src屬性和圖片詳情頁的源地址href屬性,而後將src中的值進行提取拼接成一個固定格式的連接,這個連接就是這張圖片的源地址(後面發現圖片的源地址連接能夠有不少,其中能夠配置不一樣的圖片參數,連接就是對應參數的圖片),而後進行下載。後來發現此方法並不穩定,由於測試發現並非全部圖片源地址都支持這種格式,並且這種方法也只能獲取到頁面第一次加載的圖片數,由於下拉會加載更多,這個時候是處理不了這種狀況的,此時就很尷尬了 ~ 既然要爬取,確定是要針對大量數據的,因此首先是要解決不能爬取下拉加載更多的這種狀況,既然有數據加載,確定會設計到網絡訪問,因而經過Fiddler進行抓包發現下拉到底部的時候會觸發一個異步加載。

    會請求一次接口,而後返回下一頁的列表數據,既然知道了數據的獲取方式,咱們就能夠僞造一個如出一轍的數據請求,而後拿到下一頁的數據。可是何時加載完呢,經過觀察發現每次接口的返回數據裏有一個js的部分,以下圖: cookie

    這個last_page就是標識,當加載到最後一頁時,last_page就會爲true,可是咱們只能獲取到返回的數據的字符串,怎麼對這個js的函數進行判斷呢,測試發現加載到最後一頁時,False==True這個會變成True==True,因此能夠經過判斷這個字符串來做爲爬取的頁數標識。好了,至此就解決了加載更多的問題。網絡

    咱們能夠拿到每一頁的圖片列表數據,可是圖片列表裏面沒有圖片的源地址,接下來就是解決這個問題,我以前一直都是想直接經過爬取列表頁的數據就拿到源地址,可是發現經過拼接的源地址並不適用於全部的圖片,因而我試着改變思路,經過異步

  • 中的數據拿到圖片的詳細地址,再進行一次源碼爬取,這樣就能夠拿到對應圖片的詳細頁面的源碼了,經過詳情頁的源碼就能夠獲取到圖片的源地址了。OK,大致流程就是這樣了。

    實施

    經過分析已經清楚大體的流程了,接下來就是編碼實現了。因爲本人從事的是Android開發,因此項目就建在了一個Android項目裏,可是能夠單獨運行的Java程序。 首先須要僞造一個如出一轍的異步網絡請求,觀察上面圖中的數據能夠看出,請求包含一些頭部的設置和token參數等配置信息,照着寫下來就能夠了,另外,請求是一個Post,還帶有三個參數(querypagelast_id),query則是咱們查詢的圖片的關鍵詞,page是當前頁數,last_id不清楚,不用管,設置爲固定的和模板請求同樣的便可。ide

    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,大功告成!

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

  • 相關文章
    相關標籤/搜索