公司產品新版本剛剛上線,因此也終於得空休息一下了,有了一點時間。因爲以前看到過爬蟲,能夠把網頁上的數據經過代碼自動提取出來,以爲挺有意思的,因此也想接觸一下,可是網上不少爬蟲不少都是基於Python寫的,本人以前也學了一點Python基礎,可是尚未那麼熟練和自信能寫出東西來。因此就想試着用Java寫一個爬蟲,提及立刻開幹!爬點什麼好呢,一開始還糾結了一下,究竟是文本仍是音樂仍是什麼呢,忽然想起最近本身開始練習寫文章,文章須要配圖,由於文字太枯燥,看着密密麻麻的文字,誰還看得下去啊,俗話說好圖配文章,閱讀很清爽
~ 哈哈哈ヾ(◍°∇°◍)ノ゙」,對,個人名字就叫俗話,皮了一下嘻嘻~。因此要配一個高質量的圖片才能賞心悅目,因此就想要不爬個圖片吧,這樣之後媽媽不再用擔憂個人文章配圖了。加上我以前看到過一個國外的圖片網站,質量絕對高標準,我還常常在上面找壁紙呢,並且支持各類尺寸高清下載,還能夠自定義尺寸啊,最重要的是免費哦 ~ 簡直不要太方便,在這裏也順便推薦給你們,有須要的能夠Look一下,名字叫LibreStock。其實這篇文章的配圖就是從這上面爬下來的哦~好了,說了這麼多其實都是廢話,下面開始進入正題。html
爬蟲,顧名思義,是根據網頁上的數據特徵進行分析,而後編寫邏輯代碼對這些特徵數據進行提取加工爲本身可用的信息。ImageCrawler是一款基於Java編寫的爬蟲程序,能夠爬取LibreStock上的圖片數據並下載到本地,支持輸入關鍵詞爬取,運行效果以下。git
首先打開LibreStock網站,點擊F12查看源碼,以下圖github
從圖中能夠看出每一個圖片對應的一個能夠看到,在多層的div有一個href超連接,這個就是圖片的源地址,可是好像下面還有href誒,並且也是圖片的地址,這裏不用管,咱們取一個就能夠。這個href是在image-section__photo-wrap-width的這個div裏面的,因此大概特徵咱們就找到了。此處你認爲就完成了就太天真了,通過我屢次測試,踩了一些坑以後才發現並無那麼簡單。數組
其實最開始個人作法是經過比較列表頁的bash
會請求一次接口,而後返回下一頁的列表數據,既然知道了數據的獲取方式,咱們就能夠僞造一個如出一轍的數據請求,而後拿到下一頁的數據。可是何時加載完呢,經過觀察發現每次接口的返回數據裏有一個js的部分,以下圖: cookie
這個last_page就是標識,當加載到最後一頁時,last_page就會爲true,可是咱們只能獲取到返回的數據的字符串,怎麼對這個js的函數進行判斷呢,測試發現加載到最後一頁時,False==True
這個會變成True==True
,因此能夠經過判斷這個字符串來做爲爬取的頁數標識。好了,至此就解決了加載更多的問題。網絡
咱們能夠拿到每一頁的圖片列表數據,可是圖片列表裏面沒有圖片的源地址,接下來就是解決這個問題,我以前一直都是想直接經過爬取列表頁的數據就拿到源地址,可是發現經過拼接的源地址並不適用於全部的圖片,因而我試着改變思路,經過異步
經過分析已經清楚大體的流程了,接下來就是編碼實現了。因爲本人從事的是Android開發,因此項目就建在了一個Android項目裏,可是能夠單獨運行的Java程序。 首先須要僞造一個如出一轍的異步網絡請求,觀察上面圖中的數據能夠看出,請求包含一些頭部的設置和token參數等配置信息,照着寫下來就能夠了,另外,請求是一個Post,還帶有三個參數(query
,page
,last_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。