由于Webmagic使用手册内容实在太过简单,对于新手来说想要更深入的掌握太不友好,所以我打算阅读样本代码sample和源码。
OOSpider
以往我们启动程序,是在Main函数里调用
Spider.create(new PageProcessor()).addUrl("https://www.cnblogs.com/").thread(5).run();
方法,那么OOSpider到底有什么不同呢?
OOSpider是注解式爬虫的入口,这里调用**create()**方法将OschinaBlog这个类加入到爬虫的抽取中,这里是可以传入多个类的,例如:
OOSpider.create(
Site.me().addStartUrl("http://www.oschina.net"),
new ConsolePageModelPipeline(),
OschinaBlog.clas,OschinaAnswer.class).run();
OOSpider会根据TargetUrl调用不同的Model进行解析。
那么是不是说,OOSpider就只支持注解式的爬虫呢?其实还是要看看它的构造函数:
protected OOSpider(ModelPageProcessor modelPageProcessor) {
super(modelPageProcessor);
this.modelPageProcessor = modelPageProcessor;
}
public OOSpider(PageProcessor pageProcessor) {
super(pageProcessor);
}
/**
* create a spider
*
* @param site site
* @param pageModelPipeline pageModelPipeline
* @param pageModels pageModels
*/
public OOSpider(Site site, PageModelPipeline pageModelPipeline, Class... pageModels) {
this(ModelPageProcessor.create(site, pageModels));
this.modelPipeline = new ModelPipeline();
super.addPipeline(modelPipeline);
for (Class pageModel : pageModels) {
if (pageModelPipeline != null) {
this.modelPipeline.put(pageModel, pageModelPipeline);
}
pageModelClasses.add(pageModel);
}
}
可以看到有3种构造函数,前两种分别是注解式和非注解的PageProcessor实现,我们把以前的Spider.create(new PageProcessor()).addUrl("https://www.cnblogs.com/").thread(5).run();
或者Spider.create(new PageProcessor()).addUrl("https://www.cnblogs.com/").addPipeline(new MyPipeline()).thread(5).run();
改为OOSpider也能正常运行的。
所以在本质上OOSpider能够替换原来的Spider。
那么不同处就体现在第三个构造函数上了,这个构造函数指定了Site,指定了PageModelPipeline,最后还通过class反射指定了PageModel。
Site
我们不着急,一点一点剖析。首先从Site下手(Site在我们之前的学习中,完全没有提到过,它是干嘛的呢?),看看它的私有域:
private String domain;
private String userAgent;
private Map<String, String> defaultCookies = new LinkedHashMap<String, String>();
private Map<String, Map<String, String>> cookies = new HashMap<String, Map<String, String>>();
private int sleepTime = 5000;
private int retryTimes = 0;
/**
* new a Site
*
* @return new site
*/
public static Site me() {
return new Site();
}
还包含了domain、userAgent以及cookies,很容易理解。我们以往使用Site,是这样使用的:
private Site site = Site.me().setRetryTimes(3).setSleepTime(100);
public Site getSite() {
return site;
}
我们通过Site.me()
方法获得一个静态的Site,然后设置抓取间隔100ms、重试次数3。我们姑且先把Site是如何控制重试和抓取间隔放在一边,先讨论上面提到的未见过的三个属性domain、userAgent以及cookies。
- cookie
我们的cookies的setter函数是这样的:
/**
* Add a cookie with domain {@link #getDomain()}
*
* @param name name
* @param value value
* @return this
*/
public Site addCookie(String name, String value) {
defaultCookies.put(name, value);
return this;
}
/**
* Add a cookie with specific domain.
*
* @param domain domain
* @param name name
* @param value value
* @return this
*/
public Site addCookie(String domain, String name, String value) {
if (!cookies.containsKey(domain)){
cookies.put(domain,new HashMap<String, String>());
}
cookies.get(domain).put(name, value);
return this;
}
要了解这个类如何使用,首先我们要知道如何获取cookie:
chrome浏览器输入:chrome://settings/content/cookies
,得到的内容如下所示:(我用的QQ浏览器,内核也是chrome,所以输入这个代码也一样)
我们知道,cookie是一个键值对,我们查看:
通过复制,我们就手动的获取到了一对key-value。这样,我们通过:
Site.me().addCookie("RegisteredUserCookie", "sDDDc8dIAgZSq67uJSXhtpQaHEi1XDOH")
就可以加入cookie了。
或者可以通过如下方法得知当前网站的cookie:
在Chrome浏览器中按下F12,打开开发者工具,选择"Console(控制台)"选项卡,输入document.cookie,回车就可以查看当前网站的Cookie了。
用户使用抓包工具将抓到的cookie设置到Spider中就可以了。
- User-Agent
User Agent中文名为用户代理,是Http协议中的一部分,属于头域的组成部分,User Agent也简称UA。它是一个特殊字符串头,是一种向访问网站提供你所使用的浏览器类型及版本、操作系统及版本、浏览器内核、等信息的标识。通过这个标识,用户所访问的网站可以显示不同的排版从而为用户提供更好的体验或者进行信息统计;例如用手机访问谷歌和电脑访问是不一样的,这些是谷歌根据访问者的UA来判断的。UA可以进行伪装。
现在,我们的site可以写为:
site = Site.me().addCookie("RegisteredUserCookie", "sDDDc8dIAgZSq67uJSXhtpQaHEi1XDOH"). setUserAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.65 Safari/537.31");
- domain
可以访问该Cookie的域名。如果设置为“.google.com”,则所有以“google.com”结尾的域名都可以访问该Cookie。注意第一个字符必须为“.”。
ModelPipeline
现在,回到第三个构造函数里来,PageModelPipeline也是为了配合注入的方式设置的一种输出方式,它是一个接口,源码暂且不读,我们看看ModelPipeline。
源码,可以看到class ModelPipeline implements Pipeline
。它实现接口Pipeline。
我们之前也实现过一次,是这样实现的:
public class MyPipeline implements Pipeline {
public void process(ResultItems resultItems, Task task) {
System.out.println("get page: " + resultItems.getRequest().getUrl());
//遍历所有结果,输出到控制台,上面例子中的"author"、"name"、"readme"都是一个key,其结果则是对应的value
for (Map.Entry<String, Object> entry : resultItems.getAll().entrySet()) {
System.out.println(entry.getKey() + ":\t" + entry.getValue());
}
}
}
相当于是输出到控制台的一个Pipeline,我们也对比一下ConsolePipeline吧:
public class ConsolePipeline implements Pipeline {
public ConsolePipeline() {
}
public void process(ResultItems resultItems, Task task) {
System.out.println("get page: " + resultItems.getRequest().getUrl());
Iterator var3 = resultItems.getAll().entrySet().iterator();
while(var3.hasNext()) {
Entry<String, Object> entry = (Entry)var3.next();
System.out.println((String)entry.getKey() + ":\t" + entry.getValue());
}
}
}
默认的pipeline,都会输出一句:System.out.println("get page: " + resultItems.getRequest().getUrl());
然后就是迭代器迭代,其实和自己写的原理一样(写到这里发现自己Java的数据结构还是学的不到位,以后补上)。
回归正题,什么是ModelPipeline?
class ModelPipeline implements Pipeline {
private Map<Class, PageModelPipeline> pageModelPipelines = new ConcurrentHashMap<Class, PageModelPipeline>();
public ModelPipeline() {
}
public ModelPipeline put(Class clazz, PageModelPipeline pageModelPipeline) {
pageModelPipelines.put(clazz, pageModelPipeline);
return this;
}
@Override
public void process(ResultItems resultItems, Task task) {
for (Map.Entry<Class, PageModelPipeline> classPageModelPipelineEntry : pageModelPipelines.entrySet()) {
Object o = resultItems.get(classPageModelPipelineEntry.getKey().getCanonicalName());
if (o != null) {
Annotation annotation = classPageModelPipelineEntry.getKey().getAnnotation(ExtractBy.class);
if (annotation == null || !((ExtractBy) annotation).multi()) {
classPageModelPipelineEntry.getValue().process(o, task);
} else {
List<Object> list = (List<Object>) o;
for (Object o1 : list) {
classPageModelPipelineEntry.getValue().process(o1, task);
}
}
}
}
}
}
关键点在于Map<Class, PageModelPipeline> pageModelPipelines
这样存储的私有变量,这为之后第三个构造函数能够提供多个PageModel起到了至关重要的作用。我们回过头看看之前构造函数里面是怎么构造的:
public OOSpider(Site site, PageModelPipeline pageModelPipeline, Class... pageModels) {
this(ModelPageProcessor.create(site, pageModels));
this.modelPipeline = new ModelPipeline();
super.addPipeline(modelPipeline);
for (Class pageModel : pageModels) {
if (pageModelPipeline != null) {
this.modelPipeline.put(pageModel, pageModelPipeline);
}
pageModelClasses.add(pageModel);
}
}
先不管ModelPageProcessor,先看看后面几句。先New了一个ModelPipeline,再看看for循环,将pagemodel通过put()函数将pageModel和处理它的pageModelPipeline联系到了一起。然后就是更深层次的源码,由于时间限时,就不写在这里了。
总之,从源码可以看出,OOSpider主要是用来处理注解式的爬虫。
之前一直没有好好解释注解式的PageModel,在之后的源码阅读中,再做分析。