Jsoup 實現的基於列表-詳情結構的網頁爬蟲

問題闡述: 

對於很多應用而言,都需要蒐集一些資訊內容充實自己的內容,這樣可以豐富站點內容,增加用戶停留的時間。

最原始的辦法,莫過於複製粘貼,但是,當如果目標網站是幾個,甚至幾十個的時候,複製粘貼並不是長久之計,勞心勞力,又容易搞錯。所以基於程序的數據爬取就十分重要。但是幾乎每個網站,都有他獨特的結構,看起來要針對每個網站獨特的結構,來寫一套東西,但是這樣拓展性也很差。

這裏我介紹一下,我所實現的資訊爬取程序。我的程序主要針對列表和詳情結構這樣的資訊內容。舉個例子


如下網頁是一個有列表目錄的網頁。當然這個頁面可能是一個類型的資訊內容



點擊後,會進入詳情頁面




在實踐中,我發現很多資訊內容網站都是這樣的結構,所以我在想,是否,可以寫一個通用的程序處理這一類的資訊爬取。當新的網站成爲我們爬取的目標時,只需要在數據配置的地方,多幾條配置數據,就可以爬到我需要多數據,那不是很贊 !


1. 公告數據的抽象

如何理解一個網站的結構,首先還是要抽象它的數據,在這裏,我簡單列出以下我抽象的數據。

public class NoticeCrawler implements Serializable{

	/**
	 * 
	 */
	private static final long serialVersionUID = 6562741735153521775L;

	private Long noticeCrawlerId;
	
	private String urlToCrawl; //要爬取的URL,以爲一個類型的公告,可能存放在不同的頁面,所以我這裏對應的是用“;”分開的URL
	
	private int noticeType;  //公告類型
	
	private String homeUrl;  //Host 這個可以從 <span style="font-family: Arial, Helvetica, sans-serif;">urlToCrawl分析出來,不過爲了方便,我直接指定了</span>

	private String noticesUrlSelector; //獲取 list 內容所需要的 selector,一般來說 select 列表裏所有的<a 標籤
	
	private String titleSelector;//如何從 noticeUrlSelector中獲取標題
	
	private String contentSelector; //如何從細節頁面 獲取內容的selector
	
	private String pageEncode; //網站編碼,這個很關鍵,如果編碼錯誤,會得不到正確的內容
	
	private String adSelectors; //網站的內容中的廣告selector,會被移除
	
	private String dateSelector; //文章發佈時間,
 }

當然這些數據,可以被定義在數據庫中,程序啓動的時候,從數據庫中加載所有的數據,就可以開始數據爬取了。


2. 準備工作

我是用Jsoup做獲取網頁和解析網頁的第三方庫。Jsoup的是用入門,可以參考其他資料,在這裏我就不多囉嗦了。

不過在是用Jsoup獲取頁面後,雖然我們定義了內部列表的Selector,但是卻並不能保證我們正確獲得URL,因爲URL的格式可能是http開頭,也可能是絕對目錄,也可能是相對目錄。所以還要做一些前處理,我的做法是把所有的URL替換成http開頭的絕對路徑,這樣有助於我接下來解析。

        public static void prepareAnalysis(Document document, String currentUrl, String homeUrl) {

		Elements elements = document.select("[href]"); //找到所有img或者其他有href屬性的標籤
		for (Element element : elements) {
			String url = element.attr("href");
			if (url != null && !url.startsWith("http")) { //URL已經是可以處理的URL
				if (url.startsWith("/")) { // 如果使用絕對路徑
					url = homeUrl + url;
				} else { // 如果使用相對路徑
					url = replaceUrl(currentUrl, url); 
				}
				element.attr("href", url);
			}
		}

		elements = document.select("[src]");
		for (Element element : elements) {
			String url = element.attr("src");
			if (url != null && !url.startsWith("http")) { //http開頭的目錄可以直接解析,跳過				
				if (url.startsWith("/")) { // 絕對目錄,則直接用 host 拼接起來
					url = homeUrl + url;
				} else { // 相對目錄的處理要稍微複雜,要回退目錄處理。
					url = replaceUrl(currentUrl, url);
				}
				element.attr("src", url);
			}
		}
	}
        //當使用相對路徑的時候,把路徑中的 根據路徑中的 ../來處理  爬取目標的URL,每遇到一個../則 回退到一個上級目錄,最後得到真實的URL
	public static final String replaceUrl(String toCrawl, String url) {

		String base = toCrawl.substring(0, toCrawl.lastIndexOf('/'));

		while (url.startsWith("../")) {
			url = url.substring(3);
			base = base.substring(0, base.lastIndexOf("/"));
		}

		return base + '/' + url;
	}

當用Jsoup獲取到頁面後,首先做的事情,就是預處理頁面中存在的連接,使得所有連接可以直接通過程序GET到裏面的內容。


3. 獲取列表頁面的詳情頁面地址

首先要自己分析頁面結構,知道這些URL存在什麼地方,如何用JQuery的Selector獲取到,當然這需要一定的前端實踐經驗。

                                        Elements elements = document.select(noticeCrawler.getNoticesUrlSelector());
					List<String> noticeUrls = new ArrayList<>();
					List<String> titles = new ArrayList<>();

					for (Element element : elements) {
						String url = element.attr("href");
						String title = "";  //也可以在詳情頁爬Title,這裏僅僅是參考
						if (StringUtils.isEmpty(noticeCrawler.getTitleSelector())) {
							title = element.text();
						} else {
							Elements titleEles = element.select(noticeCrawler.getTitleSelector());
							if (!titleEles.isEmpty()) {
								title = titleEles.get(0).text();
							}
						}
						titles.add(title);
						noticeUrls.add(url);
					}


當獲取到所有的詳情頁URL和文章標題,就可以對文章內容進行爬取了。


4. 獲取文章內容


其實內容說到這裏,剩餘的東西已經很容易理解了,其實就是加上文章內容的Selector和時間Selector。

貼一份相對完整的代碼吧,僅供參考~


package com.zx.spider.notice;

import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

import com.zx.common.utils.NetworkUtil;
import com.zx.datamodels.content.bean.entity.Notice;
import com.zx.datamodels.content.bean.entity.NoticeCrawler;
import com.zx.datamodels.market.bean.entity.Exchange;
import com.zx.datamodels.utils.DateUtil;
import com.zx.datamodels.utils.StringUtils;
import com.zx.modules.content.service.NoticeService;
import com.zx.modules.content.service.NoticeSpiderService;
import com.zx.modules.content.service.PushService;
import com.zx.modules.market.service.ExchangeService;
import com.zx.spider.jobs.MyDetailQuartzJobBean;

public class NoticeSpider extends MyDetailQuartzJobBean {

	private NoticeService noticeService;

	private NoticeSpiderService noticeSpiderService;

	public void setNoticeService(NoticeService noticeService) {

		this.noticeService = noticeService;
	}

	public void setNoticeSpiderService(NoticeSpiderService noticeSpiderService) {

		this.noticeSpiderService = noticeSpiderService;
	}

	
	protected void executeInternal(JobExecutionContext context) throws JobExecutionException {

		List<NoticeCrawler> noticeCrawlers = noticeSpiderService.getAllOpenNoticeSpiders();

		for (int i = 0; i < noticeCrawlers.size(); i++) {

			try {

				NoticeCrawler noticeCrawler = noticeCrawlers.get(i);
	

				String[] toScrawlUrls = noticeCrawler.getUrlToCrawl().split(";");
				List<Notice> notices = new ArrayList<>();

				for (String toScrawUrl : toScrawlUrls) {

					String homePage = NetworkUtil.get(toScrawUrl, noticeCrawler.getPageEncode());
					Document document = Jsoup.parse(homePage);

					prepareAnalysis(document, toScrawUrl, noticeCrawler.getHomeUrl());

					Elements elements = document.select(noticeCrawler.getNoticesUrlSelector());
					List<String> noticeUrls = new ArrayList<>();
					List<String> titles = new ArrayList<>();

					for (Element element : elements) {
						String url = element.attr("href");
						String title = "";
						if (StringUtils.isEmpty(noticeCrawler.getTitleSelector())) {
							title = element.text();
						} else {
							Elements titleEles = element.select(noticeCrawler.getTitleSelector());
							if (!titleEles.isEmpty()) {
								title = titleEles.get(0).text();
							}
						}
						titles.add(title);
						noticeUrls.add(url);
					}

					for (int j = 0; j < noticeUrls.size(); j++) {
						String noticeUrl = noticeUrls.get(j);
						String title = titles.get(j);

						Notice notice = crawlContent(noticeUrl, noticeCrawler);


	
						Notice toInsert = new Notice(noticeCrawler.getMarket(), title, notice.getNoticeContent(),
								noticeUrl, noticeCrawler.getNoticeType(), from, notice.getCreateDate());

						notices.add(toInsert);

					}
				}
				noticeService.addNewBatch(notices);

			} catch (Exception e) {
				continue;
			}
		}

	}

	public static Notice crawlContent(String url, NoticeCrawler noticeCrawler) {

		String noticeDocString = NetworkUtil.get(url, noticeCrawler.getPageEncode());

		Document noticeDoc = Jsoup.parse(noticeDocString);

		prepareAnalysis(noticeDoc, url, noticeCrawler.getHomeUrl());

		Elements contentElements = new Elements();
		String[] selectors = noticeCrawler.getContentSelector().split(";");

		for (String selector : selectors) {
			Elements contents = noticeDoc.select(selector);
			contentElements.addAll(contents);
		}

		// remove adds inside
		removeAds(contentElements, noticeCrawler);

		StringBuilder contentBuilder = new StringBuilder();
		for (Element contentPatch : contentElements) {
			contentBuilder.append(contentPatch.html());
		}

		Notice notice = new Notice();
		notice.setNoticeContent(contentBuilder.toString());

		String publishDate = null;

		if (!StringUtils.isEmpty(noticeCrawler.getDateSelector())) {
			Elements dateElements = noticeDoc.select(noticeCrawler.getDateSelector());
			String dateString = dateElements.text();

			publishDate = StringUtils.extractLastDate(dateString);
		} else {
			publishDate = StringUtils.extractLastDate(notice.getNoticeContent());
		}

		String hourPart = DateUtil.toString(new Date(), DateUtil.hmsDash.get());

		Date date = publishDate != null ? DateUtil.toDate(publishDate + ' ' + hourPart, DateUtil.ymdHmsDash.get())
				: new Date();

		notice.setCreateDate(new Timestamp(date.getTime()));
		return notice;
	}

	public static void removeAds(Elements elements, NoticeCrawler noticeCrawler) {

		String adSelectors = noticeCrawler.getAdSelectors();
		if (StringUtils.isEmpty(adSelectors)) {

			return;
		}
		String[] adSelectorsArray = adSelectors.split(";");
		for (Element element : elements) {
			for (String selector : adSelectorsArray) {
				Elements ads = element.select(selector);

				ads.empty();
			}
		}

	}

	public static void prepareAnalysis(Document document, String currentUrl, String homeUrl) {

		Elements elements = document.select("[href]");
		for (Element element : elements) {
			String url = element.attr("href");
			if (url != null && !url.startsWith("http")) { // the url is never
															// started by -1
				if (url.startsWith("/")) { // use absolute path
					url = homeUrl + url;
				} else { // use relative path.
					url = replaceUrl(currentUrl, url);
				}
				element.attr("href", url);
			}
		}

		elements = document.select("[src]");
		for (Element element : elements) {
			String url = element.attr("src");
			if (url != null && !url.startsWith("http")) { // the url is never
															// started by -1
				if (url.startsWith("/")) { // use absolute path
					url = homeUrl + url;
				} else { // use relative path.
					url = replaceUrl(currentUrl, url);
				}
				element.attr("src", url);
			}
		}
	}

	public static final String replaceUrl(String toCrawl, String url) {

		String base = toCrawl.substring(0, toCrawl.lastIndexOf('/'));

		while (url.startsWith("../")) {
			url = url.substring(3);
			base = base.substring(0, base.lastIndexOf("/"));
		}

		return base + '/' + url;
	}

}


5. 效果展示


我在數據庫中,配置瞭如下信息


真的幾乎就是配置一下,就立即生效。

抓去到的數據效果如下



詳情頁面:



文件和連接都沒有問題~


傳送門:http://sunrising.me


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章