1. 程式人生 > >教您使用java爬蟲gecco抓取JD全部商品資訊

教您使用java爬蟲gecco抓取JD全部商品資訊

轉自:http://www.geccocrawler.com/demo-jd/

gecco爬蟲

如果對gecco還沒有了解可以參看一下gecco的github首頁。gecco爬蟲十分的簡單易用,JD全部商品資訊的抓取9個類就能搞定。

JD網站的分析

要抓取JD網站的全部商品資訊,我們要先分析一下網站,京東網站可以大體分為三級,首頁上通過分類跳轉到商品列表頁,商品列表頁對每個商品有詳情頁。那麼我們通過找到所有分類就能逐個分類抓取商品資訊。

入口地址

新建開始頁面的HtmlBean類AllSort

  1. @Gecco(matchUrl="http://www.jd.com/allSort.aspx"
    , pipelines={"consolePipeline", "allSortPipeline"})
  2. public class AllSort implements HtmlBean {
  3. private static final long serialVersionUID = 665662335318691818L;
  4. @Request
  5. private HttpRequest request;
  6. //手機
  7. @HtmlField(cssPath=".category-items > div:nth-child(1) > div:nth-child(2) > div.mc > div.items > dl"
    )
  8. private List<Category> mobile;
  9. //家用電器
  10. @HtmlField(cssPath=".category-items > div:nth-child(1) > div:nth-child(3) > div.mc > div.items > dl")
  11. private List<Category> domestic;
  12. public List<Category> getMobile() {
  13. return mobile;
  14. }
  15. public void setMobile
    (List<Category> mobile)
    {
  16. this.mobile = mobile;
  17. }
  18. public List<Category> getDomestic() {
  19. return domestic;
  20. }
  21. public void setDomestic(List<Category> domestic) {
  22. this.domestic = domestic;
  23. }
  24. public HttpRequest getRequest() {
  25. return request;
  26. }
  27. public void setRequest(HttpRequest request) {
  28. this.request = request;
  29. }
  30. }

可以看到,這裡以抓取手機和家用電器兩個大類的商品資訊為例,可以看到每個大類都包含若干個子分類,用List\表示。gecco支援Bean的巢狀,可以很好的表達html頁面結構。Category表示子分類資訊內容,HrefBean是共用的連結Bean。

  1. public class Category implements HtmlBean {
  2. private static final long serialVersionUID = 3018760488621382659L;
  3. @Text
  4. @HtmlField(cssPath="dt a")
  5. private String parentName;
  6. @HtmlField(cssPath="dd a")
  7. private List<HrefBean> categorys;
  8. public String getParentName() {
  9. return parentName;
  10. }
  11. public void setParentName(String parentName) {
  12. this.parentName = parentName;
  13. }
  14. public List<HrefBean> getCategorys() {
  15. return categorys;
  16. }
  17. public void setCategorys(List<HrefBean> categorys) {
  18. this.categorys = categorys;
  19. }
  20. }

獲取頁面元素cssPath的小技巧

上面兩個類難點就在cssPath的獲取上,這裡介紹一些cssPath獲取的小技巧。用Chrome瀏覽器開啟需要抓取的網頁,按F12進入發者模式。選擇你要獲取的元素,如圖:輸入圖片說明在瀏覽器右側選中該元素,滑鼠右鍵選擇Copy--Copy selector,即可獲得該元素的cssPath

body > div:nth-child(5) > div.main-classify > div.list > div.category-items.clearfix > div:nth-child(1) > div:nth-child(2) > div.mc > div.items

如果你對jquery的selector有了解,另外我們只希望獲得dl元素,因此即可簡化為:

.category-items > div:nth-child(1) > div:nth-child(2) > div.mc > div.items > dl

編寫AllSort的業務處理類

完成對AllSort的注入後,我們需要對AllSort進行業務處理,這裡我們不做分類資訊持久化等處理,只對分類連結進行提取,進一步抓取商品列表資訊。看程式碼:

  1. @PipelineName("allSortPipeline")
  2. public class AllSortPipeline implements Pipeline<AllSort> {
  3. @Override
  4. public void process(AllSort allSort) {
  5. List<Category> categorys = allSort.getMobile();
  6. for(Category category : categorys) {
  7. List<HrefBean> hrefs = category.getCategorys();
  8. for(HrefBean href : hrefs) {
  9. String url = href.getUrl()+"&delivery=1&page=1&JL=4_10_0&go=0";
  10. HttpRequest currRequest = allSort.getRequest();
  11. SchedulerContext.into(currRequest.subRequest(url));
  12. }
  13. }
  14. }
  15. }

@PipelinName定義該pipeline的名稱,在AllSort的@Gecco註解裡進行關聯,這樣,gecco在抓取完並注入Bean後就會逐個呼叫@Gecco定義的pipeline了。為每個子連結增加"&delivery=1&page=1&JL=4100&go=0"的目的是隻抓取京東自營並且有貨的商品。SchedulerContext.into()方法是將待抓取的連結放入佇列中等待進一步抓取。

抓取商品列表資訊

AllSortPipeline已經將需要進一步抓取的商品列表資訊的連結提取出來了,可以看到連結的格式是:http://list.jd.com/list.html?cat=9987,653,659&delivery=1&JL=4100&go=0。因此我們建立商品列表的Bean——ProductList,程式碼如下:

  1. @Gecco(matchUrl="http://list.jd.com/list.html?cat={cat}&delivery={delivery}&page={page}&JL={JL}&go=0", pipelines={"consolePipeline", "productListPipeline"})
  2. public class ProductList implements HtmlBean {
  3. private static final long serialVersionUID = 4369792078959596706L;
  4. @Request
  5. private HttpRequest request;
  6. /**
  7. * 抓取列表項的詳細內容,包括titile,價格,詳情頁地址等
  8. */
  9. @HtmlField(cssPath="#plist .gl-item")
  10. private List<ProductBrief> details;
  11. /**
  12. * 獲得商品列表的當前頁
  13. */
  14. @Text
  15. @HtmlField(cssPath="#J_topPage > span > b")
  16. private int currPage;
  17. /**
  18. * 獲得商品列表的總頁數
  19. */
  20. @Text
  21. @HtmlField(cssPath="#J_topPage > span > i")
  22. private int totalPage;
  23. public List<ProductBrief> getDetails() {
  24. return details;
  25. }
  26. public void setDetails(List<ProductBrief> details) {
  27. this.details = details;
  28. }
  29. public int getCurrPage() {
  30. return currPage;
  31. }
  32. public void setCurrPage(int currPage) {
  33. this.currPage = currPage;
  34. }
  35. public int getTotalPage() {
  36. return totalPage;
  37. }
  38. public void setTotalPage(int totalPage) {
  39. this.totalPage = totalPage;
  40. }
  41. public HttpRequest getRequest() {
  42. return request;
  43. }
  44. public void setRequest(HttpRequest request) {
  45. this.request = request;
  46. }
  47. }

currPage和totalPage是頁面上的分頁資訊,為之後的分頁抓取提供支援。ProductBrief物件是商品的簡介,主要包括標題、預覽圖、詳情頁地址等。

  1. public class ProductBrief implements HtmlBean {
  2. private static final long serialVersionUID = -377053120283382723L;
  3. @Attr("data-sku")
  4. @HtmlField(cssPath=".j-sku-item")
  5. private String code;
  6. @Text
  7. @HtmlField(cssPath=".p-name> a > em")
  8. private String title;
  9. @Image({"data-lazy-img", "src"})
  10. @HtmlField(cssPath=".p-img > a > img")
  11. private String preview;
  12. @Href(click=true)
  13. @HtmlField(cssPath=".p-name > a")
  14. private String detailUrl;
  15. public String getTitle() {
  16. return title;
  17. }
  18. public void setTitle(String title) {
  19. this.title = title;
  20. }
  21. public String getPreview() {
  22. return preview;
  23. }
  24. public void setPreview(String preview) {
  25. this.preview = preview;
  26. }
  27. public String getDetailUrl() {
  28. return detailUrl;
  29. }
  30. public void setDetailUrl(String detailUrl) {
  31. this.detailUrl = detailUrl;
  32. }
  33. public String getCode() {
  34. return code;
  35. }
  36. public void setCode(String code) {
  37. this.code = code;
  38. }
  39. }

這裡需要說明一下@Href(click=true)的click屬性,click屬性形象的說明了,這個連結我們希望gecco繼續點選抓取。對於增加了click=true的連結,gecco會自動加入下載佇列中,不需要在手動呼叫SchedulerContext.into()增加。

編寫ProductList的業務邏輯

ProductList抓取完成後一般需要進行持久化,也就是將商品的基本資訊入庫,入庫的方式有很多種,這個例子並沒有介紹,gecco支援整合spring,可以利用spring進行pipeline的開發,大家可以參考gecco-spring這個專案。本例子是進行了控制檯輸出。ProductList的業務處理還有一個很重要的任務,就是對分頁的處理,列表頁通常都有很多頁,如果需要全部抓取,我們需要將下一頁的連結入抓取佇列。

  1. @PipelineName("productListPipeline")
  2. public class ProductListPipeline implements Pipeline<ProductList> {
  3. @Override
  4. public void process(ProductList productList) {
  5. HttpRequest currRequest = productList.getRequest();
  6. //下一頁繼續抓取
  7. int currPage = productList.getCurrPage();
  8. int nextPage = currPage + 1;
  9. int totalPage = productList.getTotalPage();
  10. if(nextPage <= totalPage) {
  11. String nextUrl = "";
  12. String currUrl = currRequest.getUrl();
  13. if(currUrl.indexOf("page=") != -1) {
  14. nextUrl = StringUtils.replaceOnce(currUrl, "page=" + currPage, "page=" + nextPage);
  15. } else {
  16. nextUrl = currUrl + "&" + "page=" + nextPage;
  17. }
  18. SchedulerContext.into(currRequest.subRequest(nextUrl));
  19. }
  20. }
  21. }

JD的列表頁通過page引數來指定頁碼,我們通過替換page引數達到分頁抓取的目的。至此,所有的商品的列表資訊都已經可以正常抓取了。

詳情頁抓取

  1. @Gecco(matchUrl="http://item.jd.com/{code}.html", pipelines="consolePipeline")
  2. public class ProductDetail implements HtmlBean {
  3. private static final long serialVersionUID = -377053120283382723L;
  4. /**
  5. * 商品程式碼
  6. */
  7. @RequestParameter
  8. private String code;
  9. /**
  10. * 標題
  11. */
  12. @Text
  13. @HtmlField(cssPath="#name > h1")
  14. private String title;
  15. /**
  16. * ajax獲取商品價格
  17. */
  18. @Ajax(url="http://p.3.cn/prices/get?skuIds=J_[code]")
  19. private JDPrice price;
  20. /**
  21. * 商品的推廣語
  22. */
  23. @Ajax(url="http://cd.jd.com/promotion/v2?skuId={code}&area=1_2805_2855_0&cat=737%2C794%2C798")
  24. private JDad jdAd;
  25. /*
  26. * 商品規格引數
  27. */
  28. @HtmlField(cssPath="#product-detail-2")
  29. private String detail;
  30. public JDPrice getPrice() {
  31. return price;
  32. }
  33. public void setPrice(JDPrice price) {
  34. this.price = price;
  35. }
  36. public String getTitle() {
  37. return title;
  38. }
  39. public void setTitle(String title) {
  40. this.title = title;
  41. }
  42. public JDad getJdAd() {
  43. return jdAd;
  44. }
  45. public void setJdAd(JDad jdAd) {
  46. this.jdAd = jdAd;
  47. }
  48. public String getDetail() {
  49. return detail;
  50. }
  51. public void setDetail(String detail) {
  52. this.detail = detail;
  53. }
  54. public String getCode() {
  55. return code;
  56. }
  57. public void setCode(String code) {
  58. this.code = code;
  59. }
  60. }

@RequestParameter可以獲取@Gecco裡定義的url變數{code}。

@Ajax是頁面中的ajax請求,JD的商品價格和推廣語都是通過ajax請求非同步獲取的,gecco支援非同步ajax請求,指定ajax請求的url地址,url中的變數可以通過兩種方式指定。

一種是花括號{},可以獲取request的引數類似@RequestParameter,例子中獲取推廣語的{code}是matchUrl="http://item.jd.com/{code}.html"中的code;

一種是中括號[],可以獲取bean中的任意屬性。例子中獲取價格的[code]是變數private String code;。

json資料的元素抽取

商品的價格是通過ajax獲取的,ajax一般返回的都是json格式的資料,這裡需要將json格式的資料抽取出來。我們先定義價格的Bean:

  1. public class JDPrice implements JsonBean {
  2. private static final long serialVersionUID = -5696033709028657709L;
  3. @JSONPath("$.id[0]")
  4. private String code;
  5. @JSONPath("$.p[0]")
  6. private float price;
  7. @JSONPath("$.m[0]")
  8. private float srcPrice;
  9. public float getPrice() {
  10. return price;
  11. }
  12. public void setPrice(float price) {
  13. this.price = price;
  14. }
  15. public float getSrcPrice() {
  16. return srcPrice;
  17. }
  18. public void setSrcPrice(float srcPrice) {
  19. this.srcPrice = srcPrice;
  20. }
  21. public String getCode() {
  22. return code;
  23. }
  24. public void setCode(String code) {
  25. this.code = code;
  26. }
  27. }

我們獲取的商品價格資訊的json資料格式為:[{"id":"J_1861098","p":"6488.00","m":"7488.00"}]。可以看到是一個數組,因為這個介面其實可以批量獲取商品的價格。json資料的資料抽取使用@JSONPath註解,語法是使用的fastjson的JSONPath語法。

JDad的抓取類似,下面是Bean的程式碼:

  1. public class JDad implements JsonBean {
  2. private static final long serialVersionUID = 2250225801616402995L;
  3. @JSONPath("$.ads[0].ad")
  4. private String ad;
  5. @JSONPath("$.ads")
  6. private List<JSONObject> ads;
  7. public String getAd() {
  8. return ad;
  9. }
  10. public void setAd(String ad) {
  11. this.ad = ad;
  12. }
  13. public List<JSONObject> getAds() {
  14. return ads;
  15. }
  16. public void setAds(List<JSONObject> ads) {
  17. this.ads = ads;
  18. }
  19. }

學會分析ajax請求

目前爬蟲抓取頁面內容針對ajax請求有兩種主流方式:

  • 一種是模擬瀏覽器將頁面完全繪製出來,比如可以利用htmlunit。這種方式存在一個問題就是效率低,因為頁面中的所有ajax都會被請求,而且需要解析所有的js程式碼。gecco可以通過自定義downloader來實現這種方式
  • 還一種就是需要哪些ajax就執行哪些,這就要開發人員分析網頁中的ajax請求,獲得請求的地址,比如抓取JD的商品價格的地址@Ajax(url="http://p.3.cn/prices/mgets?skuIds=J_[code]")。而且這個地址之後可能會變。

這兩種方式都有各自的優缺點,gecco通過擴充套件都支援,本人還是更傾向於使用第二種方式。

下面說說怎麼分析頁面中的ajax請求,還是要利用chrome的開發者模式,network選項可以看到頁面中的所有請求:輸入圖片說明可以看到請求的地址是:http://p.3.cn/prices/get?type=1&area=128052855&pdtk=&pduid=836516317&pdpin=&pdbp=0&skuid=J1861098&callback=cnp。我們去掉其他引數只留下商品的程式碼,發現一樣可以訪問,http://p.3.cn/prices/get? skuid=J1861098就是我們要請求的地址。

gecco的其他一些有用的特性

  • gecco支援頁面中的定義的全域性javascript變數的提取,如頁面中定義的var變數。
  • gecco支援分散式抓取,通過redis管理startRequest實現分散式抓取。

原始碼

全部原始碼可以在gecco的github上下載,程式碼位於src/test/java/com/geccocrawler/gecco/demo/jd包下。如果使用過程中發現任何bug歡迎Pull request,或者通過Issue提問,當然也可以在部落格中留言。