問題闡述:
對於很多應用而言,都需要蒐集一些資訊內容充實自己的內容,這樣可以豐富站點內容,增加用戶停留的時間。
最原始的辦法,莫過於複製粘貼,但是,當如果目標網站是幾個,甚至幾十個的時候,複製粘貼並不是長久之計,勞心勞力,又容易搞錯。所以基於程序的數據爬取就十分重要。但是幾乎每個網站,都有他獨特的結構,看起來要針對每個網站獨特的結構,來寫一套東西,但是這樣拓展性也很差。
這裏我介紹一下,我所實現的資訊爬取程序。我的程序主要針對列表和詳情結構這樣的資訊內容。舉個例子
如下網頁是一個有列表目錄的網頁。當然這個頁面可能是一個類型的資訊內容
點擊後,會進入詳情頁面
在實踐中,我發現很多資訊內容網站都是這樣的結構,所以我在想,是否,可以寫一個通用的程序處理這一類的資訊爬取。當新的網站成爲我們爬取的目標時,只需要在數據配置的地方,多幾條配置數據,就可以爬到我需要多數據,那不是很贊 !
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);
}
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. 效果展示
我在數據庫中,配置瞭如下信息
真的幾乎就是配置一下,就立即生效。
抓去到的數據效果如下
詳情頁面:
文件和連接都沒有問題~