前言(廢話)
公司產(chǎn)品新版本剛剛上線贝室,所以也終于得空休息一下了,有了一點(diǎn)時(shí)間仿吞。由于之前看到過爬蟲滑频,可以把網(wǎng)頁上的數(shù)據(jù)通過代碼自動(dòng)提取出來,覺得挺有意思的唤冈,所以也想接觸一下峡迷,但是網(wǎng)上很多爬蟲很多都是基于Python寫的,本人之前也學(xué)了一點(diǎn)Python基礎(chǔ),但是還沒有那么熟練和自信能寫出東西來绘搞。所以就想試著用Java寫一個(gè)爬蟲彤避,說起馬上開干!爬點(diǎn)什么好呢夯辖,一開始還糾結(jié)了一下琉预,到底是文本還是音樂還是什么呢,突然想起最近自己開始練習(xí)寫文章蒿褂,文章需要配圖圆米,因?yàn)槲淖痔菰铮粗苊苈槁榈奈淖肿乃ǎl還看得下去啊娄帖,俗話說好圖配文章,閱讀很清爽
~ 哈哈哈ヾ(?°?°?)??”昙楚,對(duì)屋彪,我的名字就叫俗話愿题,皮了一下嘻嘻~嘉冒。所以要配一個(gè)高質(zhì)量的圖片才能賞心悅目辛润,所以就想要不爬個(gè)圖片吧,這樣以后媽媽再也不用擔(dān)心我的文章配圖了崎场。加上我之前看到過一個(gè)國外的圖片網(wǎng)站,質(zhì)量絕對(duì)高標(biāo)準(zhǔn)遂蛀,我還經(jīng)常在上面找壁紙呢谭跨,而且支持各種尺寸高清下載,還可以自定義尺寸啊李滴,最重要的是免費(fèi)哦 ~ 簡直不要太方便螃宙,在這里也順便推薦給大家,有需要的可以Look一下所坯,名字叫LibreStock谆扎。其實(shí)這篇文章的配圖就是從這上面爬下來的哦~好了,說了這么多其實(shí)都是廢話芹助,下面開始進(jìn)入正題堂湖。
概述
爬蟲,顧名思義状土,是根據(jù)網(wǎng)頁上的數(shù)據(jù)特征進(jìn)行分析无蜂,然后編寫邏輯代碼對(duì)這些特征數(shù)據(jù)進(jìn)行提取加工為自己可用的信息。ImageCrawler是一款基于Java編寫的爬蟲程序蒙谓,可以爬取LibreStock上的圖片數(shù)據(jù)并下載到本地斥季,支持輸入關(guān)鍵詞爬取,運(yùn)行效果如下。
分析
首先打開LibreStock網(wǎng)站酣倾,點(diǎn)擊F12查看源碼舵揭,如下圖
從圖中可以看出每個(gè)圖片對(duì)應(yīng)的一個(gè)<li/>元素,<li/>下有一個(gè)圖片的超鏈接躁锡,這個(gè)就是對(duì)應(yīng)圖片的詳情地址午绳,所以這里還不能拿到圖片的源地址,我們再點(diǎn)進(jìn)去再看
可以看到稚铣,在多層的div有一個(gè)href超鏈接箱叁,這個(gè)就是圖片的源地址,但是好像下面還有href誒惕医,而且也是圖片的地址耕漱,這里不用管,我們?nèi)∫粋€(gè)就可以抬伺。這個(gè)href是在image-section__photo-wrap-width的這個(gè)div里面的螟够,所以大概特征我們就找到了。此處你認(rèn)為就完成了就太天真了峡钓,經(jīng)過我多次測試妓笙,踩了一些坑之后才發(fā)現(xiàn)并沒有那么簡單。
其實(shí)最開始我的做法是通過比較列表頁的<li/>下的src屬性和圖片詳情頁的源地址href屬性能岩,然后將src中的值進(jìn)行提取拼接成一個(gè)固定格式的鏈接寞宫,這個(gè)鏈接就是這張圖片的源地址(后面發(fā)現(xiàn)圖片的源地址鏈接可以有很多,其中可以配置不同的圖片參數(shù)拉鹃,鏈接就是對(duì)應(yīng)參數(shù)的圖片)辈赋,然后進(jìn)行下載。后來發(fā)現(xiàn)此方法并不穩(wěn)定膏燕,因?yàn)闇y試發(fā)現(xiàn)并不是所有圖片源地址都支持這種格式钥屈,而且這種方法也只能獲取到頁面第一次加載的圖片數(shù),因?yàn)橄吕瓡?huì)加載更多坝辫,這個(gè)時(shí)候是處理不了這種情況的篷就,此時(shí)就很尷尬了 ~ 既然要爬取,肯定是要針對(duì)大量數(shù)據(jù)的近忙,所以首先是要解決不能爬取下拉加載更多的這種情況竭业,既然有數(shù)據(jù)加載,肯定會(huì)設(shè)計(jì)到網(wǎng)絡(luò)訪問及舍,于是通過Fiddler進(jìn)行抓包發(fā)現(xiàn)下拉到底部的時(shí)候會(huì)觸發(fā)一個(gè)異步加載永品。
會(huì)請求一次接口,然后返回下一頁的列表數(shù)據(jù)击纬,既然知道了數(shù)據(jù)的獲取方式鼎姐,我們就可以偽造一個(gè)一模一樣的數(shù)據(jù)請求,然后拿到下一頁的數(shù)據(jù)。但是什么時(shí)候加載完呢炕桨,通過觀察發(fā)現(xiàn)每次接口的返回?cái)?shù)據(jù)里有一個(gè)js的部分饭尝,如下圖:
這個(gè)last_page就是標(biāo)識(shí),當(dāng)加載到最后一頁時(shí)献宫,last_page就會(huì)為true钥平,但是我們只能獲取到返回的數(shù)據(jù)的字符串,怎么對(duì)這個(gè)js的函數(shù)進(jìn)行判斷呢姊途,測試發(fā)現(xiàn)加載到最后一頁時(shí)涉瘾,False==True
這個(gè)會(huì)變成True==True
,所以可以通過判斷這個(gè)字符串來作為爬取的頁數(shù)標(biāo)識(shí)捷兰。好了立叛,至此就解決了加載更多的問題。
我們可以拿到每一頁的圖片列表數(shù)據(jù)贡茅,但是圖片列表里面沒有圖片的源地址秘蛇,接下來就是解決這個(gè)問題,我之前一直都是想直接通過爬取列表頁的數(shù)據(jù)就拿到源地址顶考,但是發(fā)現(xiàn)通過拼接的源地址并不適用于所有的圖片赁还,于是我試著改變思路,通過<li/>中的數(shù)據(jù)拿到圖片的詳細(xì)地址驹沿,再進(jìn)行一次源碼爬取艘策,這樣就可以拿到對(duì)應(yīng)圖片的詳細(xì)頁面的源碼了,通過詳情頁的源碼就可以獲取到圖片的源地址了渊季。OK朋蔫,大體流程就是這樣了。
實(shí)施
通過分析已經(jīng)清楚大致的流程了梭域,接下來就是編碼實(shí)現(xiàn)了。由于本人從事的是Android開發(fā)搅轿,所以項(xiàng)目就建在了一個(gè)Android項(xiàng)目里病涨,但是可以單獨(dú)運(yùn)行的Java程序。
首先需要偽造一個(gè)一模一樣的異步網(wǎng)絡(luò)請求璧坟,觀察上面圖中的數(shù)據(jù)可以看出既穆,請求包含一些頭部的設(shè)置和token參數(shù)等配置信息,照著寫下來就可以了雀鹃,另外幻工,請求是一個(gè)Post,還帶有三個(gè)參數(shù)(query
黎茎,page
囊颅,last_id
),query
則是我們查詢的圖片的關(guān)鍵詞,page
是當(dāng)前頁數(shù)踢代,last_id
不清楚盲憎,不用管,設(shè)置為固定的和模板請求一樣的即可胳挎。
`
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());
// 正文饼疙,正文內(nèi)容其實(shí)跟get的URL中 '? '后的參數(shù)字符串一致
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);
//流用完記得關(guān)
out.flush();
out.close();
}
connection.connect();
int code = connection.getResponseCode();
System.out.println("第" + page + "頁P(yáng)OST網(wǎng)頁解析連接響應(yīng)碼:" + 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;
}
`
如上代碼就是偽造的請求方法,返回當(dāng)前請求的結(jié)果源碼HTML慕爬,拿到源碼以后窑眯,我們需要提取最后一頁參數(shù)標(biāo)識(shí)。如果是最后一頁医窿,則更新標(biāo)識(shí)磅甩,將不再請求。
public boolean isLastPage(String html) {
//采用Jsoup解析
Document doc = Jsoup.parse(html);
//獲取Js內(nèi)容留搔,判斷是否最后一頁
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;
}
拿到列表源碼更胖,我們還需要解析出列表中的圖片的詳情鏈接。測試發(fā)現(xiàn)列表的詳情href的div格式又是可變的隔显,這里遇到兩種格式却妨,不知道有沒有第三種,但是兩種已經(jīng)可以適應(yīng)絕大部分了括眠。
//獲取html標(biāo)簽中的img的列表數(shù)據(jù)
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) + "個(gè)文件hrefEle為空");
continue;
}
String img_detail_href = hrefEle.attr("href");
拿到圖片詳情頁的超鏈接后彪标,再請求一次詳情頁面鏈接,拿到詳情頁面的源碼掷豺,
String img_detail_entity = HttpRequestUtil.requestGet(img_detail_href, page, (i + 1));//獲取詳情源碼
然后就可以對(duì)源碼進(jìn)行解析捞烟,拿到圖片的源地址,測試發(fā)現(xiàn)圖片源地址的格式也有多種当船,經(jīng)過實(shí)踐發(fā)現(xiàn)大概分為四種题画,獲取到圖片源地址,我們可以對(duì)這個(gè)源地址url進(jìn)行提取文件名作為下載保存的文件名德频,然后保存到圖片模型中苍息,所以根據(jù)詳情頁源碼提取出圖片源地址的代碼如下:
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標(biāo)簽中的內(nèi)容
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;
}
綜上上面的代碼壹置,從網(wǎng)頁列表源碼中提取出多個(gè)圖片模型的代碼如下:
public Vector<ImageModel> getImgModelsData(String html, int page) throws Exception {
//獲取的數(shù)據(jù),存放在集合中
Vector<ImageModel> imageModels = new Vector<>();
//采用Jsoup解析
Document doc = Jsoup.parse(html);
//獲取html標(biāo)簽中的img的列表數(shù)據(jù)
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) + "個(gè)文件hrefEle為空");
continue;
}
String img_detail_href = hrefEle.attr("href");
if (TextUtil.isNullOrEmpty(img_detail_href)) {
System.out.println("第" + page + "頁第" + (i + 1) + "個(gè)文件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) + "個(gè)文件網(wǎng)頁實(shí)體img_detail_entity為空");
continue;
}
ImageModel imageModel = getModel(img_detail_entity);
if (imageModel == null) {
System.out.println("第" + page + "頁第" + (i + 1) + "個(gè)文件模型imageModel為空");
continue;
}
imageModel.setPage(page);
imageModel.setPostion((i + 1));
//將每一個(gè)對(duì)象的值盖喷,保存到List集合中
imageModels.add(imageModel);
}
//返回?cái)?shù)據(jù)
return imageModels;
}
獲取到圖片的源地址后难咕,接下來就是下載到本地了距辆,一個(gè)頁面有多個(gè)圖片惦界,所以下載用線程池比較合適。因?yàn)橐粋€(gè)列表頁是24張圖片沾歪,所以這里線程池的大小就設(shè)為24,解析完一個(gè)頁面的列表灾搏,就把這個(gè)頁面的圖片列表傳給下載器, 當(dāng)這個(gè)列表的任務(wù)完成以后狂窑,就去解析下一頁的數(shù)據(jù)媳板,然后重復(fù)循環(huán)這個(gè)過程,直到判斷是最后一頁了泉哈,就結(jié)束此次爬取蛉幸。
public void startDownloadList(Vector<ImageModel> downloadList, String keyword) {
HttpURLConnection connection = null;
//循環(huán)下載
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();
//設(shè)置超時(shí)間為3秒
connection.setConnectTimeout(3 * 1000);
//防止屏蔽程序抓取而返回403錯(cuò)誤
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 + "個(gè)文件,地址:" + download_url + "響應(yīng)碼:" + 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()) {//所有子線程結(jié)束丛晦,執(zhí)行回調(diào)
if (downloadCallBack != null) {
downloadCallBack.allWorksDone();
}
break;
}
}
}
}
保存到本地的代碼如下奕纫,保存到的是自定義文件夾的目錄,目錄的名稱是輸入的爬取的關(guān)鍵詞烫沙,下載的圖片的名字是根據(jù)源地址的url提取得到
public void writeFile(InputStream inputStream, String downloadDir, String filename) {
try {
//獲取自己數(shù)組
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
為查詢的關(guān)鍵詞锌蓄,然后回車升筏,可以看到控制臺(tái)輸出了信息(對(duì)于我這個(gè)強(qiáng)迫癥來首,看起來很舒適)瘸爽,文件夾也生成了對(duì)應(yīng)的圖片文件您访,OK,大功告成剪决!
以上就是整個(gè)爬取的流程灵汪,最后,完整的代碼已經(jīng)上傳到了github昼捍,歡迎各位小伙伴fork识虚。