Webmagic源码分析之运行流程
来源:互联网 发布:java @before 编辑:程序博客网 时间:2024/06/05 03:33
Webmagic是Java中的一个爬虫开源框架,主要有四大核心组件,分别是:Downloader、PageProcessor、Scheduler、Pipeline,并有Spider进行管理。这四个组件分别对应了爬虫生命周期中的下载、处理、管理、持久化。同时还支持XPath、Jsoup、CSS选择器,方便我们对抓取的页面进行解析。
Webmagic的源码可以从github上pull下来:https://github.com/code4craft/webmagic
Webmagic的入门文档可以查看:http://webmagic.io/docs/zh/
以下是一张从文档中截下来的Webmagic架构图:
接下来将会以Webmagic中的一个例子,来跟踪一下webmagic运行流程的源码。这个例子位于webmagic-core这个核心模块中的us.codecraft.webmagic.processor.example包下,类名为GithubRepoPageProcessor,这是一个关于Github爬虫的代码。
首先从main函数入手,它创建了一个Spider对象,GithubRepoPageProcessor对象是对抓去结果进行解析的类。addUrl() 函数可以添加我们需要爬去的连接,这个函数的参数是可变的,可以传入多个URL并使用逗号隔开。thread() 函数用于设置Spider线程的数量,表明抓取时启动的线程数,支持多线程并发抓取。run方法用于启动Spider,因为Spider类实现了Runnable接口。这里也可以调用Spider的start()方法用于启动Spider。
public static void main(String[] args) { Spider.create(new GithubRepoPageProcessor()).addUrl("https://github.com/code4craft").thread(5).run(); }thread()函数中首先会调用checkIfRunning()函数来检查Spider的运行状态,如果状态为已运行,那么将会抛出异常。在Spider类中定义了三个常量来表示爬虫的运行状态,分别是初始化、运行、停止。第三行中将参数复制给类变量,用于保存爬虫的线程数。
public Spider thread(int threadNum) { checkIfRunning(); this.threadNum = threadNum; if (threadNum <= 0) { throw new IllegalArgumentException("threadNum should be more than one!"); } return this; } protected void checkIfRunning() { if (stat.get() == STAT_RUNNING) { throw new IllegalStateException("Spider is already running!"); } }
由上面checkIfRunning函数中可以看待,stat用来保存爬虫的状态,从它的定义中,我发现它的类型是AtomicInteger,这是JDK1.5之后,在java.util.concurrent.atomic包下新增的原子处理类,主要用于在多线程环境下保证数据操作的准确性,能保证并发访问下的线程安全。
protected AtomicInteger stat = new AtomicInteger(STAT_INIT); protected final static int STAT_INIT = 0; protected final static int STAT_RUNNING = 1; protected final static int STAT_STOPPED = 2;
接下来我们来看看run方法的运行流程,当线程启动时,就会调用run方法。第3行调用函数用于检测Spider的状态,第4行的函数用于初始化爬虫的一些组件,这些组件我们可以在创建Spider的时候进行设置,如果没有设置,那么该函数会使用默认的组件进行初始化,其中也包含了对线程池的初始化。
第6行中用while循环,判断如果当前线程不中断,且Spider的状态为运行状态,也就是在checkRunningStat()方法中成功设置成了运行状态,那么就开始执行爬虫。第7行先从请求队列中拿出一个请求,Scheduler是用于管理爬虫请求的类,如果没有指定,Spider默认使用的是QueueScheduler,即基于内存的队列,其内部的数据结构是使用一个LinkedBlockingQueue来存放我们要爬取的请求。
当从队列中取出的请求为null,则判断如果线程池的活跃线程数为0,且exitWhenComplete设置为了true,那么就退出while循环。exitWhenComplete是一个boolean类型的值,表示当没有新的爬虫请求时,是否退出。否则的话,将执行waitNewUrl()等待新的请求被加入队列,再继续执行。
当Request不为null,则执行到第16行,创建一个匿名内部类,实现Runnable()接口,并添加到线程池中去执行。第20行,调用processRequest()函数,将会创建HttpClient去执行请求,并抓取界面进行解析等一系列操作。如果这个过程中没有发生异常,那么一次爬虫的生命周期就成功了,此时会执行第21行onSuccess()。这里涉及到了为Spider添加一个爬虫的监听器,当爬虫执行成功时,在onSuccess()中就会调用这个监听器中的onSuccess()方法,我们可以把对于爬虫执行成功做的操作,写在onSuccess()方法内。而当发生异常时,会执行onError()方法,实际上也是执行监听器中的onError()方法,我们可以对爬虫失败做一些处理。
当跳出while循环时,执行第33行,将Spider的状态设置为停止,并进行资源的释放。
@Override public void run() { checkRunningStat(); initComponent(); logger.info("Spider " + getUUID() + " started!"); while (!Thread.currentThread().isInterrupted() && stat.get() == STAT_RUNNING) { Request request = scheduler.poll(this); if (request == null) { if (threadPool.getThreadAlive() == 0 && exitWhenComplete) { break; } // wait until new url added waitNewUrl(); } else { final Request requestFinal = request; threadPool.execute(new Runnable() { @Override public void run() { try { processRequest(requestFinal); onSuccess(requestFinal); } catch (Exception e) { onError(requestFinal); logger.error("process request " + requestFinal + " error", e); } finally { pageCount.incrementAndGet(); signalNewUrl(); } } }); } } stat.set(STAT_STOPPED); // release some resources if (destroyWhenExit) { close(); } }
在checkRunningStat()函数中,是一个while的死循环。第3行会得到当前Spider的状态,当状态为运行态时,会抛出异常,否则就设置当前状态为运行态,并退出循环。
private void checkRunningStat() { while (true) { int statNow = stat.get(); if (statNow == STAT_RUNNING) { throw new IllegalStateException("Spider is already running!"); } if (stat.compareAndSet(statNow, STAT_RUNNING)) { break; } } }
在initComponent()函数中,当你没有指定Downloader时,默认使用HttpClientDownloader,当你没有指定Pipeline时,默认使用ConsolePipeline。注意,一个Spider只对应有一个Downloader,一个Scheduler,一个PageProcessor,但可以有多个Pipeline,在Spider中使用List集合来保存多个的Pipeline。第9-15行是对线程池的初始化,在创建Spider的时候,可以使用setExecutorService(ExecutorService executorService)来设置我们自己的线程池,如果没有设置,那么Spider会给我们创建一个默认的线程池,即Executors.newFixedThreadPool(threadNum)。
第16-21行,遍历初始设置的请求,添加到Scheduler请求队列当中,并清空初始请求。startRequest是一个Request列表,可以在创建Spider的时候,通过startRequest(List<Request> startRequests)函数进行设置。
protected void initComponent() { if (downloader == null) { this.downloader = new HttpClientDownloader(); } if (pipelines.isEmpty()) { pipelines.add(new ConsolePipeline()); } downloader.setThread(threadNum); if (threadPool == null || threadPool.isShutdown()) { if (executorService != null && !executorService.isShutdown()) { threadPool = new CountableThreadPool(threadNum, executorService); } else { threadPool = new CountableThreadPool(threadNum); } } if (startRequests != null) { for (Request request : startRequests) { scheduler.push(request, this); } startRequests.clear(); } startTime = new Date(); }processRequest()是执行爬虫生命周期的函数,其参数为Request对象,封装了请求的相关信息,如URL,请求方式等。第二行调用download()方法去下载我们要抓去的界面,其内部实现是基于HttpClinet实现的,抓取返回一个Page对象,封装了抓取到的界面的信息。第12行调用了PageProcessor中的process()方法,此方法用于解析抓取的界面,并添加新的抓取请求,通常我们需要自己实现PageProcessor,在process()方法中写我们自己的解析代码。第14-18行,遍历所有的Pipeline,并执行process()方法,来对解析出的数据信息处理,可以打印也可以保存到数据库中。这部分的代码执行与否可以通过ResultItems类中的skip来控制,如果skip设置为true,那么则会执行Pipeline,如果skip设置为false,则不执行Pipeline。
protected void processRequest(Request request) { Page page = downloader.download(request, this); if (page == null) { throw new RuntimeException("unaccpetable response status"); } // for cycle retry if (page.isNeedCycleRetry()) { extractAndAddRequests(page, true); sleep(site.getRetrySleepTime()); return; } pageProcessor.process(page); extractAndAddRequests(page, spawnUrl); if (!page.getResultItems().isSkip()) { for (Pipeline pipeline : pipelines) { pipeline.process(page.getResultItems(), this); } } //for proxy status management request.putExtra(Request.STATUS_CODE, page.getStatusCode()); sleep(site.getSleepTime()); }上图中第13行所调用的函数实现如下,我们在PageProcessor的process函数中对界面进行解析,可以将解析得到的新的URL添加到Page的targetRequests列表中。而下面代码的3-5行,则是遍历targetRequests列表,将所有的请求加入到Scheduler的请求队列当中,以此来让Spider继续进行爬取。
protected void extractAndAddRequests(Page page, boolean spawnUrl) { if (spawnUrl && CollectionUtils.isNotEmpty(page.getTargetRequests())) { for (Request request : page.getTargetRequests()) { addRequest(request); } } } private void addRequest(Request request) { if (site.getDomain() == null && request != null && request.getUrl() != null) { site.setDomain(UrlUtils.getDomain(request.getUrl())); } scheduler.push(request, this); }
Site对象保存的是爬虫的一些配置信息,如请求头、Cookie、代理信息、字符编码、可以接收的服务器状态吗等。第3-6行,判断task如果不为空,那么就得到site。Task是一个接口,里面有两个方法:一个是getUUID(),另一个是getSite(),而我们的Spider就是实现了Task接口的类。第11-13行,当site不为null时,分别获得site中设置的信息,有可接收的状态码、字符编码、请求头,这些信息是我们在创建Site对象时,可以自己设置进去的。第14行,如果site为null,那么就创建一个包含状态码200的Set集合,表示默认只接收服务器返回的带有200状态码的响应。
第23-28行,与获取代理有关。如果Site中有设置代理IP池,并且启用,那么则从代理IP池中获取一个代理Proxy对象,如果没有设置代理IP池,但Site中设置了HttpHost对象,则直接从Site中获取。第30行,调用getHttpUriRequest()来的一个Http请求对象,这个函数的具体实现我们之后会详细说明。第31行,通过getHttpClient()方法得到用于执行HTTP请求的HttpClient对象,并调用execute()执行请求,得到Response对象。第32-41行,获取服务器响应的状态码,判断这个状态码是否是当前Spider所支持的状态码,如果支持,则使用handleResponse()方法处理响应的界面,得到了封装响应界面的Page对象,如果不支持,则会打出警告日志。在finally中,会将代理IP归还给IP池,并关闭响应流,即EntityUtils.consume()。
@Override public Page download(Request request, Task task) { Site site = null; if (task != null) { site = task.getSite(); } Set<Integer> acceptStatCode; String charset = null; Map<String, String> headers = null; if (site != null) { acceptStatCode = site.getAcceptStatCode(); charset = site.getCharset(); headers = site.getHeaders(); } else { acceptStatCode = Sets.newHashSet(200); } logger.info("downloading page {}", request.getUrl()); CloseableHttpResponse httpResponse = null; int statusCode=0; try { HttpHost proxyHost = null; Proxy proxy = null; //TODO if (site.getHttpProxyPool() != null && site.getHttpProxyPool().isEnable()) { proxy = site.getHttpProxyFromPool(); proxyHost = proxy.getHttpHost(); } else if(site.getHttpProxy()!= null){ proxyHost = site.getHttpProxy(); } HttpUriRequest httpUriRequest = getHttpUriRequest(request, site, headers, proxyHost); httpResponse = getHttpClient(site, proxy).execute(httpUriRequest); statusCode = httpResponse.getStatusLine().getStatusCode(); request.putExtra(Request.STATUS_CODE, statusCode); if (statusAccept(acceptStatCode, statusCode)) { Page page = handleResponse(request, charset, httpResponse, task); onSuccess(request); return page; } else { logger.warn("code error " + statusCode + "\t" + request.getUrl()); return null; } } catch (IOException e) { logger.warn("download page " + request.getUrl() + " error", e); if (site.getCycleRetryTimes() > 0) { return addToCycleRetry(request, site); } onError(request); return null; } finally { request.putExtra(Request.STATUS_CODE, statusCode); if (site.getHttpProxyPool()!=null && site.getHttpProxyPool().isEnable()) { site.returnHttpProxyToPool((HttpHost) request.getExtra(Request.PROXY), (Integer) request .getExtra(Request.STATUS_CODE)); } try { if (httpResponse != null) { //ensure the connection is released back to pool EntityUtils.consume(httpResponse.getEntity()); } } catch (IOException e) { logger.warn("close response fail", e); } } }
我们来看看是如何获取到HttpUriRequest对象的。getHttpUriRequest()函数有四个参数,第一个是Request对象,封装了请求的信息;第二个是Site对象,封装了Spider的配置信息;第三个是Map集合,保存了请求头;第四个是HttpHost对象,保存了代理的信息。第1行,调用selectRequestMethod() 来进行请求方式的选择,返回一个RequestBuilder对象,该对象是用于产生HttpUriRequest的。第22行,从request中获得请求的方式,然后进行判断,根据不同的请求方式构建出不同的RequestBuilder对象。这里要说的一个是,如果请求为POST,则需要获取表单参数,POST请求的表单参数是以key-value的形式封装在NameValuePair数组中的。需要特别注意的是,当我们使用Request对象的post请求时,如果要传递表单参数,则要将封装了表单参数NameValuePair数组,以key为nameValuePair的形式,加入到Request对象里的Map集合中。
第3-7行,当请求头不为null时,则为RequestBuilder添加请求头。第8-12行,创建配置请求信息的对象,设置了连接超时时间、socket超时时间等信息。第13-16行,对代理信息进行设置。
protected HttpUriRequest getHttpUriRequest(Request request, Site site, Map<String, String> headers,HttpHost proxy) { RequestBuilder requestBuilder = selectRequestMethod(request).setUri(request.getUrl()); if (headers != null) { for (Map.Entry<String, String> headerEntry : headers.entrySet()) { requestBuilder.addHeader(headerEntry.getKey(), headerEntry.getValue()); } } RequestConfig.Builder requestConfigBuilder = RequestConfig.custom() .setConnectionRequestTimeout(site.getTimeOut()) .setSocketTimeout(site.getTimeOut()) .setConnectTimeout(site.getTimeOut()) .setCookieSpec(CookieSpecs.BEST_MATCH); if (proxy !=null) {requestConfigBuilder.setProxy(proxy);request.putExtra(Request.PROXY, proxy);} requestBuilder.setConfig(requestConfigBuilder.build()); return requestBuilder.build(); } protected RequestBuilder selectRequestMethod(Request request) { String method = request.getMethod(); if (method == null || method.equalsIgnoreCase(HttpConstant.Method.GET)) { //default get return RequestBuilder.get(); } else if (method.equalsIgnoreCase(HttpConstant.Method.POST)) { RequestBuilder requestBuilder = RequestBuilder.post(); NameValuePair[] nameValuePair = (NameValuePair[]) request.getExtra("nameValuePair"); if (nameValuePair != null && nameValuePair.length > 0) { requestBuilder.addParameters(nameValuePair); } return requestBuilder; } else if (method.equalsIgnoreCase(HttpConstant.Method.HEAD)) { return RequestBuilder.head(); } else if (method.equalsIgnoreCase(HttpConstant.Method.PUT)) { return RequestBuilder.put(); } else if (method.equalsIgnoreCase(HttpConstant.Method.DELETE)) { return RequestBuilder.delete(); } else if (method.equalsIgnoreCase(HttpConstant.Method.TRACE)) { return RequestBuilder.trace(); } throw new IllegalArgumentException("Illegal HTTP Method " + method); }
下面我们来看看如何产生HttpClient对象的,如第11行,是通过HttpClientGenerator对象中的getClient()方法来获取HttpClient对象。其内部实现就是使用HttpClientBuilder来生产HttpClient,需要设置代理IP的信息,以及用户名和密码。
private CloseableHttpClient getHttpClient(Site site, Proxy proxy) { if (site == null) { return httpClientGenerator.getClient(null, proxy); } String domain = site.getDomain(); CloseableHttpClient httpClient = httpClients.get(domain); if (httpClient == null) { synchronized (this) { httpClient = httpClients.get(domain); if (httpClient == null) { httpClient = httpClientGenerator.getClient(site, proxy); httpClients.put(domain, httpClient); } } } return httpClient; }
得到了服务器的响应,就需要解析这些响应,handleResponse() 方法就实现了这个getContent()方法,用于获得服务器响应的HTML代码。这是设计到一个字符编码的问题,如果有在site中设置了charset,就使用这个charset,将服务器响应的byte字节数组转成String类型返回。否则,则需要调用getHtmlCharset() 根据响应内容获取字符编码,有一下几种方式:第一,解析响应头中Content-Type属性,Content-Type中设置的编码;第二,解析HTML中meta标签中的字符编码,例如
protected Page handleResponse(Request request, String charset, HttpResponse httpResponse, Task task) throws IOException { String content = getContent(charset, httpResponse); Page page = new Page(); page.setRawText(content); page.setUrl(new PlainText(request.getUrl())); page.setRequest(request); page.setStatusCode(httpResponse.getStatusLine().getStatusCode()); return page; } protected String getContent(String charset, HttpResponse httpResponse) throws IOException { if (charset == null) { byte[] contentBytes = IOUtils.toByteArray(httpResponse.getEntity().getContent()); String htmlCharset = getHtmlCharset(httpResponse, contentBytes); if (htmlCharset != null) { return new String(contentBytes, htmlCharset); } else { logger.warn("Charset autodetect failed, use {} as charset. Please specify charset in Site.setCharset()", Charset.defaultCharset()); return new String(contentBytes); } } else { return IOUtils.toString(httpResponse.getEntity().getContent(), charset); } }自此,一个简单example的爬虫运行流程就走完了,有些细节的地方,还需要自己慢慢去体会。
- Webmagic源码分析之运行流程
- [源码学习][知了开发]WebMagic-总体流程源码分析
- Monkey源码分析之运行流程
- Monkey源码分析之运行流程
- WhatWeb源码分析之运行流程
- 源码-spark运行流程分析
- spark源码分析:spark运行总流程
- 结合源码分析Struts2运行流程
- 【源码分析】storm拓扑运行全流程源码分析
- 【源码分析】storm拓扑运行全流程源码分析
- 爬虫分析之WebMagic框架篇:牛刀小试
- HDFS源码分析心跳汇报之BPServiceActor工作线程运行流程
- Hadoop-2.4.1源码分析--HDFS HeartBeat(心跳检测)之BPServiceActor工作线程运行流程(上)
- Hadoop-2.4.1源码分析--HDFS HeartBeat(心跳检测)之BPServiceActor工作线程运行流程(下)
- WebMagic爬取论坛链接实现流程分析
- [gevent源码分析] 深度分析gevent运行流程
- Struts2源码分析(二)Struts2运行流程分析
- caffe之具体运行流程分析
- 第二章
- java 值传递和引用传递
- C语言的fopen函数(文件操作/读写)
- SVN版本服务器搭配全过程详解(含服务端、客户端)
- Android 圆形头像 带阴影 带边界 完整代码
- Webmagic源码分析之运行流程
- Centreon+Nagios实战第二篇——监控端安装Nagios
- 【2016.10.4NOIP普及模拟】Bill
- HDU5920 Ugly Problem (大模拟)
- Java 集合框架分析:LinkedList
- UVA12186 Another Crisis
- hadoop :mkdir: 'input': No such file or directory问题
- Leetcode 226. Invert Binary Tree
- 【2016.10.4NOIP普及模拟】Exam