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

gecco爬虫

如果对gecco还没有了解可以参看一下gecco的github首页。gecco爬虫十分的简单易用,JD全部商品信息的抓取9个类就能搞定。

JD网站的分析

要抓取JD网站的全部商品信息,我们要先分析一下网站,京东网站可以大体分为三级,首页上通过分类跳转到商品列表页,商品列表页对每个商品有详情页。那么我们通过找到所有分类就能逐个分类抓取商品信息。

入口地址

http://www.jd.com/allSort.aspx,这个地址是JD全部商品的分类列表,我们以该页面作为开始页面,抓取JD的全部商品信息

新建开始页面的HtmlBean类AllSort

@Gecco(matchUrl="http://www.jd.com/allSort.aspx", pipelines={"consolePipeline", "allSortPipeline"})
public class AllSort implements HtmlBean {

private static final long serialVersionUID = 665662335318691818L;

@Request
private HttpRequest request;

//手机
@HtmlField(cssPath=".category-items > div:nth-child(1) > div:nth-child(2) > div.mc > div.items > dl")
private List<Category> mobile;

//家用电器
@HtmlField(cssPath=".category-items > div:nth-child(1) > div:nth-child(3) > div.mc > div.items > dl")
private List<Category> domestic;

public List<Category> getMobile() {
return mobile;
}

public void setMobile(List<Category> mobile) {
this.mobile = mobile;
}

public List<Category> getDomestic() {
return domestic;
}

public void setDomestic(List<Category> domestic) {
this.domestic = domestic;
}

public HttpRequest getRequest() {
return request;
}

public void setRequest(HttpRequest request) {
this.request = request;
}
}

可以看到,这里以抓取手机和家用电器两个大类的商品信息为例,可以看到每个大类都包含若干个子分类,用List\表示。gecco支持Bean的嵌套,可以很好的表达html页面结构。Category表示子分类信息内容,HrefBean是共用的链接Bean。

public class Category implements HtmlBean {

private static final long serialVersionUID = 3018760488621382659L;

@Text
@HtmlField(cssPath="dt a")
private String parentName;

@HtmlField(cssPath="dd a")
private List<HrefBean> categorys;

public String getParentName() {
return parentName;
}

public void setParentName(String parentName) {
this.parentName = parentName;
}

public List<HrefBean> getCategorys() {
return categorys;
}

public void setCategorys(List<HrefBean> categorys) {
this.categorys = categorys;
}

}

获取页面元素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进行业务处理,这里我们不做分类信息持久化等处理,只对分类链接进行提取,进一步抓取商品列表信息。看代码:

@PipelineName("allSortPipeline")
public class AllSortPipeline implements Pipeline<AllSort> {

@Override
public void process(AllSort allSort) {
List<Category> categorys = allSort.getMobile();
for(Category category : categorys) {
List<HrefBean> hrefs = category.getCategorys();
for(HrefBean href : hrefs) {
String url = href.getUrl()+"&delivery=1&page=1&JL=4_10_0&go=0";
HttpRequest currRequest = allSort.getRequest();
SchedulerContext.into(currRequest.subRequest(url));
}
}
}

}

@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,代码如下:

@Gecco(matchUrl="http://list.jd.com/list.html?cat={cat}&delivery={delivery}&page={page}&JL={JL}&go=0", pipelines={"consolePipeline", "productListPipeline"})
public class ProductList implements HtmlBean {

private static final long serialVersionUID = 4369792078959596706L;

@Request
private HttpRequest request;

/**
* 抓取列表项的详细内容,包括titile,价格,详情页地址等
*/
@HtmlField(cssPath="#plist .gl-item")
private List<ProductBrief> details;
/**
* 获得商品列表的当前页
*/
@Text
@HtmlField(cssPath="#J_topPage > span > b")
private int currPage;
/**
* 获得商品列表的总页数
*/
@Text
@HtmlField(cssPath="#J_topPage > span > i")
private int totalPage;

public List<ProductBrief> getDetails() {
return details;
}

public void setDetails(List<ProductBrief> details) {
this.details = details;
}

public int getCurrPage() {
return currPage;
}

public void setCurrPage(int currPage) {
this.currPage = currPage;
}

public int getTotalPage() {
return totalPage;
}

public void setTotalPage(int totalPage) {
this.totalPage = totalPage;
}

public HttpRequest getRequest() {
return request;
}

public void setRequest(HttpRequest request) {
this.request = request;
}

}

currPage和totalPage是页面上的分页信息,为之后的分页抓取提供支持。ProductBrief对象是商品的简介,主要包括标题、预览图、详情页地址等。

public class ProductBrief implements HtmlBean {

private static final long serialVersionUID = -377053120283382723L;

@Attr("data-sku")
@HtmlField(cssPath=".j-sku-item")
private String code;

@Text
@HtmlField(cssPath=".p-name> a > em")
private String title;

@Image({"data-lazy-img", "src"})
@HtmlField(cssPath=".p-img > a > img")
private String preview;

@Href(click=true)
@HtmlField(cssPath=".p-name > a")
private String detailUrl;

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public String getPreview() {
return preview;
}

public void setPreview(String preview) {
this.preview = preview;
}

public String getDetailUrl() {
return detailUrl;
}

public void setDetailUrl(String detailUrl) {
this.detailUrl = detailUrl;
}

public String getCode() {
return code;
}

public void setCode(String code) {
this.code = code;
}

}

这里需要说明一下@Href(click=true)的click属性,click属性形象的说明了,这个链接我们希望gecco继续点击抓取。对于增加了click=true的链接,gecco会自动加入下载队列中,不需要在手动调用SchedulerContext.into()增加。

编写ProductList的业务逻辑

ProductList抓取完成后一般需要进行持久化,也就是将商品的基本信息入库,入库的方式有很多种,这个例子并没有介绍,gecco支持整合spring,可以利用spring进行pipeline的开发,大家可以参考gecco-spring这个项目。本例子是进行了控制台输出。ProductList的业务处理还有一个很重要的任务,就是对分页的处理,列表页通常都有很多页,如果需要全部抓取,我们需要将下一页的链接入抓取队列。

@PipelineName("productListPipeline")
public class ProductListPipeline implements Pipeline<ProductList> {

@Override
public void process(ProductList productList) {
HttpRequest currRequest = productList.getRequest();
//下一页继续抓取
int currPage = productList.getCurrPage();
int nextPage = currPage + 1;
int totalPage = productList.getTotalPage();
if(nextPage <= totalPage) {
String nextUrl = "";
String currUrl = currRequest.getUrl();
if(currUrl.indexOf("page=") != -1) {
nextUrl = StringUtils.replaceOnce(currUrl, "page=" + currPage, "page=" + nextPage);
} else {
nextUrl = currUrl + "&" + "page=" + nextPage;
}
SchedulerContext.into(currRequest.subRequest(nextUrl));
}
}

}

JD的列表页通过page参数来指定页码,我们通过替换page参数达到分页抓取的目的。至此,所有的商品的列表信息都已经可以正常抓取了。

详情页抓取

商品的基本信息抓取完成后,就要针对每个商品的详情页进行抓取,可以看到详情页的地址格式一般如下:http://item.jd.com/1861098.html。我们建立商品详情页的Bean:

@Gecco(matchUrl="http://item.jd.com/{code}.html", pipelines="consolePipeline")
public class ProductDetail implements HtmlBean {

private static final long serialVersionUID = -377053120283382723L;

/**
* 商品代码
*/
@RequestParameter
private String code;

/**
* 标题
*/
@Text
@HtmlField(cssPath="#name > h1")
private String title;

/**
* ajax获取商品价格
*/
@Ajax(url="http://p.3.cn/prices/get?skuIds=J_[code]")
private JDPrice price;

/**
* 商品的推广语
*/
@Ajax(url="http://cd.jd.com/promotion/v2?skuId={code}&area=1_2805_2855_0&cat=737%2C794%2C798")
private JDad jdAd;

/*
* 商品规格参数
*/
@HtmlField(cssPath="#product-detail-2")
private String detail;

public JDPrice getPrice() {
return price;
}

public void setPrice(JDPrice price) {
this.price = price;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public JDad getJdAd() {
return jdAd;
}

public void setJdAd(JDad jdAd) {
this.jdAd = jdAd;
}

public String getDetail() {
return detail;
}

public void setDetail(String detail) {
this.detail = detail;
}

public String getCode() {
return code;
}

public void setCode(String code) {
this.code = code;
}
}

@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:

public class JDPrice implements JsonBean {

private static final long serialVersionUID = -5696033709028657709L;

@JSONPath("$.id[0]")
private String code;

@JSONPath("$.p[0]")
private float price;

@JSONPath("$.m[0]")
private float srcPrice;

public float getPrice() {
return price;
}

public void setPrice(float price) {
this.price = price;
}

public float getSrcPrice() {
return srcPrice;
}

public void setSrcPrice(float srcPrice) {
this.srcPrice = srcPrice;
}

public String getCode() {
return code;
}

public void setCode(String code) {
this.code = code;
}

}

我们获取的商品价格信息的json数据格式为:[{"id":"J_1861098","p":"6488.00","m":"7488.00"}]。可以看到是一个数组,因为这个接口其实可以批量获取商品的价格。json数据的数据抽取使用@JSONPath注解,语法是使用的fastjson的JSONPath语法。

JDad的抓取类似,下面是Bean的代码:

public class JDad implements JsonBean {

private static final long serialVersionUID = 2250225801616402995L;

@JSONPath("$.ads[0].ad")
private String ad;

@JSONPath("$.ads")
private List<JSONObject> ads;

public String getAd() {
return ad;
}

public void setAd(String ad) {
this.ad = ad;
}

public List<JSONObject> getAds() {
return ads;
}

public void setAds(List<JSONObject> ads) {
this.ads = ads;
}

}

学会分析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提问,当然也可以在博客中留言。


更多相关文章

  1. Android 外部唤起应用跳转指定页面
  2. fragment保存页面不销毁
  3. Android欢迎页面自动跳转和触摸进入首页
  4. 通过ViewPager实现类似微信的页面切换(Fragment篇)
  5. android开发中如何从当前页面返回上一页面
  6. 在Webview上加载脱机更新页面
  7. 自定义BaseAdapter,在主Activity页面调用显示歌曲列表
  8. 提取Launcher中的WorkSapce,可以左右滑动切换屏幕页面的类
  9. 如何将值发送到Ionic中具有条件的其他页面

随机推荐

  1. xml文件介绍及使用
  2. XML的术语
  3. 第2章 包装类
  4. XML轻松学习总节篇
  5. Java对象的强、软、弱和虚引用
  6. XML轻松学习手册(2)XML概念
  7. JS解析XML文件和XML字符串详解
  8. XML文件结构和基本语法
  9. 了解Xml格式
  10. XML轻松学习手册