很多企业要求利用爬虫去爬取商品信息,一般的开发模型如下:
for i=1;i<=最大页号;i++
列表页面url=商品列表页面url+?page=i(页号)
列表页面=爬取(列表页面url)
商品链接列表=抽取商品链接(列表页面)
for 链接 in 商品链接列表:
商品页面=爬取(链接)
抽取(商品页面);
这样的模型看似简单,但是有一下几个问题:
1)爬虫没有线程池支持。
2)没有断点机制。
3)没有爬取状态存储,爬取商品网站经常会出现服务器拒绝链接(反问次数过多),导致一旦出现
拒绝链接,有部分页面是未爬取状态。而没有爬取状态记录,导致爬虫需要重新爬取,才可获得完整数据。
4)当抽取业务复杂时,代码可读性差(没有固定框架)
很多企业解决上面问题时,并没有选择nutch、crawler4j这样的爬虫框架,因为这些爬虫都是基于广度遍历的,上面的业务虽然是简单的双重循环,但是不是广度遍历。但是实际上这个双重循环,是可以转换成广度遍历的,当广度遍历的的层数为1的时候,等价于基于url列表的爬取(种子列表)。上面业务中的循环,其实就是基于url列表的爬取。上面的伪代码是双重循环,所以可以拆分成2次广度遍历来完成的。
我们设计两个广度遍历器LinkCrawler和ProductCrawler:
1)LinkCrawler负责遍历商品列表页面,抽取每个商品详情页面的url,将抽取出的url注入(inject)到ProductCrawler里
2)ProductCrawler以LinkCrawler注入的url为种子,进行爬取,对每个商品详情页面进行抽取。
这里以WebCollector爬虫框架为例,给出一段爬取大众点评团购的示例:
import java.io.File;
import java.io.IOException;
import java.util.regex.Pattern;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import cn.edu.hfut.dmic.webcollector.crawler.BreadthCrawler;
import cn.edu.hfut.dmic.webcollector.generator.Injector;
import cn.edu.hfut.dmic.webcollector.model.Page;
import cn.edu.hfut.dmic.webcollector.util.Config;
import cn.edu.hfut.dmic.webcollector.util.FileUtils;
/**
* 爬取大众点评团购信息的爬虫Demo 很多精抽取的爬虫,并不是采用简单的广度遍历算法,而是采用两个步骤完成:
* 1.用循环遍历商品列表页面,抽取每个商品详情页面的url 2.对每个商品详情页面进行抽取
* 大多数爬虫往往只支持广度遍历,所以很多人选择自已用循环来进行上面的抽取 操作,这样做往往不能享受到爬虫框架所提供的线程池、异常处理和断点支持等 功能。
*
* 其实上面的抽取任务,是可以通过拆分成2次广度遍历来完成的。 当广度遍历的的层数为1的时候,等价于基于url列表的爬取(种子列表)
* 我们设计两个广度遍历器LinkCrawler和ProductCrawler
* 1)LinkCrawler负责遍历商品列表页面,抽取每个商品详情页面的url,将抽取出的url注 入(inject)到ProductCrawler里
* 2)ProductCrawler以LinkCrawler注入的url为种子,进行爬取,对每个商品详情页面进行 抽取。
*
* @author hu
*/
public class DazhongDemo {
public static class LinkCrawler extends BreadthCrawler {
Injector injector;
public LinkCrawler(String linkPath, String productPath) {
setCrawlPath(linkPath);
/*向ProductCrawler爬虫注入种子的注入器*/
injector = new Injector(productPath);
/*LinkCrawler负责遍历商品列表页面,i是页号*/
for (int i = 1; i < 3; i++) {
addSeed("http://t.dianping.com/list/hefei-category_1?pageno=" + i);
}
addRegex(".*");
}
@Override
public void visit(Page page) {
Document doc = page.getDoc();
Elements links = doc.select("li[class^=floor]>a[track]");
for (Element link : links) {
/*href是从商品列表页面中抽取出的商品详情页面url*/
String href = link.attr("abs:href");
System.out.println(href);
synchronized (injector) {
try {
/*将商品详情页面的url注入到ProductCrawler作为种子*/
injector.inject(href, true);
} catch (IOException ex) {
}
}
}
}
/*Config.topN=0的情况下,深度为1的广度遍历,等价于对种子列表的遍历*/
public void start() throws IOException {
start(1);
}
}
public static class ProductCrawler extends BreadthCrawler {
public ProductCrawler(String productPath) {
setCrawlPath(productPath);
addRegex(".*");
setResumable(true);
setThreads(5);
}
@Override
public void visit(Page page) {
/*判断网页是否是商品详情页面,这个程序里可以省略*/
if (!Pattern.matches("http://t.dianping.com/deal/[0-9]+", page.getUrl())) {
return;
}
Document doc = page.getDoc();
String name = doc.select("h1.title").first().text();
String price = doc.select("span.price-display").first().text();
String origin_price = doc.select("span.price-original").first().text();
String validateDate = doc.select("div.validate-date").first().text();
System.out.println(name + " " + price + "/" + origin_price + validateDate);
}
/*Config.topN=0的情况下,深度为1的广度遍历,等价于对种子列表的遍历*/
public void start() throws IOException {
start(1);
}
}
public static void main(String[] args) throws IOException {
/*
Config.topN表示爬虫做链接分析时,链接数量上限,由于本程序只要求遍历
种子url列表,不需根据链接继续爬取,所以要设置为0
*/
Config.topN = 0;
/*
每个爬虫的爬取依赖一个文件夹,这个文件夹会对爬取信息进行存储和维护
这里有两个爬虫,所以需要设置两个爬取文件夹
*/
String linkPath = "crawl_link";
String productPath = "crawl_product";
File productDir = new File(productPath);
if (productDir.exists()) {
FileUtils.deleteDir(productDir);
}
LinkCrawler linkCrawler = new LinkCrawler(linkPath, productPath);
linkCrawler.start();
ProductCrawler productCrawler = new ProductCrawler(productPath);
productCrawler.start();
}
}