前些天,想要用爬蟲抓取點東西,可是網上不少爬蟲都是使用python語言的,本人只會java,所以,只能找相關java的爬蟲資料,在開源中國的看到國內的大神寫的一個開源的爬蟲框架,並下源碼研究了一下,發現跟官網描述的同樣,夠簡單,簡潔易用!有興趣的朋友能夠到官網瞭解下!css
我這個例子也是在查看了官網的《教您使用java爬蟲gecco抓取JD所有商品信息》這篇博客以後,本身動手實現的,而且加入了持久化操做,因爲京東的商品比較具備層次結構,相似一棵樹,所以,傳統的SQL數據庫很顯然不能很好存儲,因而我選用文檔型的NoSQL數據庫MongoDB在Monogo裏存儲相似json的數據,很容易表達出數據之間的層次關係。下面記錄一下個人實現過程,而且向Gecco做者大神致敬,也建議對這方面有興趣的朋友去官網查看做者的博客,會有更大的收穫,畢竟小弟水平有限,這裏寫的也只是我的理解後實現的!html
jdk 我使用的是jdk1.8.0_74java
IDE eclipse4.6python
jar jar包有點多,主要是依賴包,全部的依賴包都在源碼中的lib目錄下。這裏就不一一貼出來了linux
DB mongoDB 3.2.10git
mongo driver 3.3mongodb
程序從cn.succy.geccospider.engine.jd.JDEngine這個類開始數據庫
public class JDEngine { public static void main(String[] args) { String url = "https://www.jd.com/allSort.aspx"; String classpath = "cn.succy.geccospider"; HttpRequest request = new HttpGetRequest(url); request.setCharset("gb2312"); // 若是pipeline和htmlbean不在同一個包,classpath就要設置到他們的共同父包 // 本引擎主要是把京東分類的頁面手機板塊給抓取過來封裝成htmlbean GeccoEngine.create().classpath(classpath).start(request).interval(2000).run(); // 本引擎是負責抓取每個細目對應的頁面的第一頁的全部商品列表的數據,開啓5個線程同時抓取,提高效率 GeccoEngine.create().classpath(classpath).start(AllSortPipeline.cateRequests).thread(5) .interval(2000).run(); } }
在這段代碼裏邊,總共用了兩個引擎,第一個先是抓取京東分類的入口url裏邊的版塊入口 https://www.jd.com/allSort.aspx,在程序啓動的時候,底層會在指定的classpath包路徑下,尋找有> @Gecco註解的類,而且url和註解matchUrl對應,這個類就是抓取到的數據要映射成的HtmlBean,裏邊的屬性能夠經過註解把這個url下符合條件的節點對應起來,從而,能夠把咱們想要的數據經過一個HtmlBean給封裝起來了。要注意一點,classpath應該設置到能包含全部有類註解的類的包路經,若是同時兩個子包裏邊的類都存在類註解,那麼,這個classpath應該就是設置到他們的共同父包下,以此類推……interval(2000)方法是指,隔多長時間執行一次抓取,單位毫秒。下面咱們看一下京東的分類頁面的結構,而且看一下我要抓取的是那一部分!json
在這張圖片裏邊,咱們能夠看到,實際上咱們要抓取的是先把圈出部分抓取出來,把每一塊封裝成一個AllSort對象,下面咱們看一下這個類!數組
@Gecco(matchUrl = "https://www.jd.com/allSort.aspx", pipelines = { "allSortPipeline", "consolePipeline" }) public class AllSort implements HtmlBean { private static final long serialVersionUID = 3422937382621558860L; @Request private HttpRequest request; /** * 抓取手機模塊的數據 */ @HtmlField(cssPath = "div.category-items > div:nth-child(1) > div:nth-child(2) > div.mc > div.items > dl") private List<Category> cellPhone; public HttpRequest getRequest() { return request; } public void setRequest(HttpRequest request) { this.request = request; } public List<Category> getCellPhone() { return cellPhone; } public void setCellPhone(List<Category> cellPhone) { this.cellPhone = cellPhone; } }
先看註解@Gecco ,這個註解裏邊的matchUrl對應的就是引擎開始爬的那個url,pipeline在個人理解是一個管道,玩過linux的朋友應該知道linux的管道是什麼,java裏邊也有管道輸入輸出流,和這些類似,這裏的大體意思是,當這個類裏邊的屬性都裝配好了以後,接着把這個類的對象當成一個輸入條件,傳遞到pipline裏邊配置好的pipleline類處理,pipeline類要實現一個叫作Pipeline的接口,而且經過@PipelineName註解指定這個pipeline叫啥名,關於Pipeline相關的,待會兒再說。實際上,在AllSort這個類裏邊,把對應選擇器選中的內容,直接注入到一個叫作cellPhone的列表,關於這個待會兒再說。咱們先看一下這個List<Category> cellPhone;裝載的究竟是什麼!
實際上,這裏圈出的就是一個Category對象,手機模塊全部的Category就是List<Category> cellPhone!那麼,Category究竟是什麼樣的一個東西呢?咱們看一下這個類是怎麼實現的就明白了!
public class Category implements HtmlBean { private static final long serialVersionUID = -1808704248579938878L; /** * 對應的是大的分類名字,如手機通信,運營商,手機配件等 */ @Text @HtmlField(cssPath = "dt > a") private String typeName; /** * 相對於上面的大的分類下的小類目名字 */ @HtmlField(cssPath = "dd > a") private List<HrefBean> categories; public String getTypeName() { return typeName; } public void setTypeName(String typeName) { this.typeName = typeName; } public List<HrefBean> getCategories() { return categories; } public void setCategories(List<HrefBean> categories) { this.categories = categories; } }
也就是以下圖所示的標籤對應的文本和url
細心的朋友應該會發現,這裏的@HtmlField(cssPath = "dd > a")的選擇器直接從dd開始,那是由於,在gecco裏邊,Category對象是做爲上面AllSort的一部分,所以,選擇器能夠承接上級,也就是說,
@HtmlField(cssPath = "div.category-items > div:nth-child(1) > div:nth-child(2) > div.mc > div.items > dl") private List<Category> cellPhone;
已經到了dl這一層,那麼它裏邊的Category裏的元素就能夠直接從dl下面開始獲取,因此會看到像註解@HtmlField(cssPath = "dd > a")這種樣子的選擇器。
到了這個時候,咱們已經能夠把京東的分類首頁的手機模塊給抓取下來,而且保存成javaBean。好了,下面能夠說說對於註解上面的pipeline究竟是什麼,怎麼用了!
上面咱們也有說到,pipeline是一個管道鏈接,也就是當頁面的HtmlBean解析完成後,再以此執行註解中配置的全部的pipeline,咱們在AllSort中配置有兩個pipeline,分別是"allSortPipeline","consolePipeline",第二個是往控制檯輸出,輸出的格式是json的格式的,這個沒有什麼好講的,這裏面我想說一下的是第一個,這個是我自定義的。好,接下來先上代碼看一下這個類是怎麼實現的。
@PipelineName("allSortPipeline") public class AllSortPipeline implements Pipeline<AllSort> { public static List<HttpRequest> cateRequests = new ArrayList<>(); @Override public void process(AllSort allSort) { List<Category> cellPhones = allSort.getCellPhone(); for (Category category : cellPhones) { // 獲取mongo的集合 MongoCollection<Document> collection = MongoUtils.getCollection(); // 把json轉成Document Document doc = Document.parse(JSON.toJSONString(category)); // 向集合裏邊插入一條文檔 collection.insertOne(doc); List<HrefBean> hrefs = category.getCategories(); // 遍歷HrefBean,取出裏邊保存的url for (HrefBean href : hrefs) { HttpRequest request = allSort.getRequest(); // 把url保存起來,方便後面開啓一個新的引擎進行多線程抓取數據 cateRequests.add(request.subRequest(href.getUrl())); } } } }
最上面的註解就是給這個pipeline起個名字,接着是把數據先入庫,入庫的數據長得像這樣子:
{「typeName":"手機通信","categories":[{"title":"手機","url":"……"},……]}這種樣子,接下來就能夠順着剛剛提取出來的每個小類目的url進行抓取他們對應的頁面的數據了,咱們先看一下手機這個小類目對應的頁面長什麼樣的!以下圖
這裏邊的商品item就是咱們想要抓取的數據,每一頁有60條,對應的每一個頁面封裝成一個ProductList類,具體規則抽取在上面已經說起到,在這裏就再也不說起怎麼提取規則了,相信看到這裏的朋友,應該能夠依葫蘆畫瓢了,下面貼出代碼,看一下ProductList的實現類!
@Gecco(matchUrl = "https://list.jd.com/list.html?cat={cat}", pipelines = { "consolePipeline", "filePipeline" ,"mongoPipeline"}) public class ProductList implements HtmlBean { private static final long serialVersionUID = -6580138290566056728L; /** * 獲取請求對象,從該對象中能夠獲取抓取的是哪一個url */ @Request private HttpRequest request; // #plist > ul > li.gl-item > div.j-sku-item @HtmlField(cssPath = "#plist > ul > li.gl-item") private List<ProductDetail> details; public HttpRequest getRequest() { return request; } public void setRequest(HttpRequest request) { this.request = request; } public List<ProductDetail> getDetails() { return details; } public void setDetails(List<ProductDetail> details) { this.details = details; } }
值得提一下的是,matchUrl裏邊有一個{cat},這個是像url傳遞參數,在HttpRequest裏的url保存的就是填充參數以後url字符串,mongoPipeline就是對這裏邊完成填充HtmlBean以後,執行對應mongoDB操做的pipeline,咱們先看一下這個pipeline!
@PipelineName("mongoPipeline") public class MongoPipeline implements Pipeline<ProductList> { @Override public void process(ProductList productList) { MongoCollection<Document> collection = MongoUtils.getCollection(); HttpRequest req = productList.getRequest(); // 從productList裏邊獲取url,目的是爲了從以前存進數據庫中找到對應url的小類目 String url = req.getUrl(); // 把類目名對應的商品詳情的列表獲取,例如,手機對應到的頁面的60條記錄 List<ProductDetail> details = productList.getDetails(); // 轉成json字符串 String jsonString = JSON.toJSONString(details); // 經過url找到數組裏邊對應url的類目,而後添加一個字段叫作details,而且把details的值 // 給添加進去 collection.updateOne(new Document("categories.url", url), Document.parse("{\"$set\":{\"categories.$.details\":" + jsonString + "}}")); } }
上面代碼就能夠實現經過獲取到請求對象裏邊的url,由於url是惟一的,因此能夠找到對應的類目的信息,以下圖
這樣子就能夠在這個記錄下面把抓取到的商品詳情列表插進去,表示該類目下面有這麼多(60條)商品,保存進去以後,成下面的樣子。
咱們看下ProductDetail怎麼實現的,這也是一個普通的HtmlBean,和上面的沒有什麼區別,都是規則抽取,而後把裏邊想要的數據給注入到bean的屬性便可
public class ProductDetail implements HtmlBean { private static final long serialVersionUID = -6362237918542798717L; @Attr(value = "data-sku") @HtmlField(cssPath = "div.j-sku-item") private String pCode; @Image({ "data-lazy-img", "src" }) @HtmlField(cssPath = "div.j-sku-item > div.p-img > a > img") private String pImg; //#plist > ul > li:nth-child(1) > div > div.p-price > strong:nth-child(1) > i @Text @HtmlField(cssPath = "div.j-sku-item > div.p-price > strong:nth-child(1) > i") private String pPrice; @Text @HtmlField(cssPath = "div.j-sku-item > div.p-name > a > em") private String pTitle; @Text @HtmlField(cssPath = "div.j-sku-item > div.p-comment > strong > a.comment") private String pComment; @Text @HtmlField(cssPath = "div.j-sku-item > div.p-shop > span > a") private String pShop; @Text @HtmlField(cssPath = "div.j-sku-item > div.p-icons > *") private List<String> pIcons; public String getpCode() { return pCode; } public void setpCode(String pCode) { this.pCode = pCode; } public String getpImg() { return pImg; } public void setpImg(String pImg) { this.pImg = pImg; } public String getpPrice() { return pPrice; } public void setpPrice(String pPrice) { this.pPrice = pPrice; } public String getpTitle() { return pTitle; } public void setpTitle(String pTitle) { this.pTitle = pTitle; } public String getpComment() { return pComment; } public void setpComment(String pComment) { this.pComment = pComment; } public String getpShop() { return pShop; } public void setpShop(String pShop) { this.pShop = pShop; } public List<String> getpIcons() { return pIcons; } public void setpIcons(List<String> pIcons) { this.pIcons = pIcons; } }
到這裏,基本上就已經實現了數據的抓取和入庫了,從總體上來看,就只有一條數據,呈一棵樹同樣,這種結構若是用SQL數據庫作的話,會存在大量自鏈接,形成不少數據冗餘,所以,使用文檔數據庫是最好不過的了
若是這個對您有所幫助,請點個Star喲
代碼已經上傳到osc的代碼倉庫,因爲本人網速不是很是好,所以並無使用maven管理項目,全部所需的jar包也都在項目源碼的lib包裏邊,點擊源代碼下載