摘要:方法,首先判斷是否有這是在中配置的,如果有,直接調(diào)用的將相應(yīng)內(nèi)容轉(zhuǎn)化成對(duì)應(yīng)編碼字符串,否則智能檢測(cè)響應(yīng)內(nèi)容的字符編碼。
Downloader是負(fù)責(zé)請(qǐng)求url獲取返回值(html、json、jsonp等)的一個(gè)組件。當(dāng)然會(huì)同時(shí)處理POST重定向、Https驗(yàn)證、ip代理、判斷失敗重試等。
接口:Downloader 定義了download方法返回Page,定義了setThread方法來(lái)請(qǐng)求的設(shè)置線程數(shù)。
抽象類:AbstractDownloader。 定義了重載的download方法返回Html,同時(shí)定義了onSuccess、onError狀態(tài)方法,并定義了addToCycleRetry來(lái)判斷是否需要進(jìn)行重試。
實(shí)現(xiàn)類:HttpClientDownloader。負(fù)責(zé)通過(guò)HttpClient下載頁(yè)面
輔助類:HttpClientGenerator。負(fù)責(zé)生成HttpClient實(shí)例。
1、AbstractDownloader
public Html download(String url, String charset) { Page page = download(new Request(url), Site.me().setCharset(charset).toTask()); return (Html) page.getHtml(); }
這里download邏輯很簡(jiǎn)單,就是調(diào)用子類實(shí)現(xiàn)的download下載。
protected Page addToCycleRetry(Request request, Site site) { Page page = new Page(); Object cycleTriedTimesObject = request.getExtra(Request.CYCLE_TRIED_TIMES); if (cycleTriedTimesObject == null) { page.addTargetRequest(request.setPriority(0).putExtra(Request.CYCLE_TRIED_TIMES, 1)); } else { int cycleTriedTimes = (Integer) cycleTriedTimesObject; cycleTriedTimes++; if (cycleTriedTimes >= site.getCycleRetryTimes()) { return null; } page.addTargetRequest(request.setPriority(0).putExtra(Request.CYCLE_TRIED_TIMES, cycleTriedTimes)); } page.setNeedCycleRetry(true); return page; }
判斷重試邏輯:先判斷CYCLE_TRIED_TIMES是否為null,如果不為null,循環(huán)重試次數(shù)+1,判斷是否超過(guò)最大允許值(默認(rèn)為3次),然后設(shè)置needCycleRetry標(biāo)志說(shuō)明需要被重試。這在我們Spider分析篇提到過(guò)這個(gè),我們?cè)賮?lái)看看Spider中的代碼片段加深理解
// for cycle retry if (page.isNeedCycleRetry()) { extractAndAddRequests(page, true); sleep(site.getRetrySleepTime()); return; }
2、HttpClientDownloader
繼承了AbstractDownloader.負(fù)責(zé)通過(guò)HttpClient下載頁(yè)面.
實(shí)例變量
httpClients:是一個(gè)Map型的變量,用來(lái)保存根據(jù)站點(diǎn)域名生成的HttpClient實(shí)例,以便重用。
httpClientGenerator:HttpClientGenerator實(shí)例,用來(lái)生成HttpClient
主要方法:
a、獲取HttpClient實(shí)例。
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; }
主要思路是,通過(guò)Site獲取域名,然后通過(guò)域名判斷是否在httpClients這個(gè)map中已存在HttpClient實(shí)例,如果存在則重用,否則通過(guò)httpClientGenerator創(chuàng)建一個(gè)新的實(shí)例,然后加入到httpClients這個(gè)map中,并返回。
注意為了確保線程安全性,這里用到了線程安全的雙重判斷機(jī)制。
b、download方法:
public Page download(Request request, Task task) { Site site = null; if (task != null) { site = task.getSite(); } SetacceptStatCode; String charset = null; Map headers = null; if (site != null) { acceptStatCode = site.getAcceptStatCode(); charset = site.getCharset(); headers = site.getHeaders(); } else { acceptStatCode = WMCollections.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("get page {} error, status code {} ",request.getUrl(),statusCode); return null; } } catch (IOException e) { logger.warn("download page {} error", request.getUrl(), 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); } } }
注意,這里的Task入?yún)?,其?shí)就是Spider實(shí)例。
首先通過(guò)site來(lái)設(shè)置字符集、請(qǐng)求頭、以及允許接收的響應(yīng)狀態(tài)碼。
之后便是設(shè)置代理:首先判斷site是否有設(shè)置代理池,以及代理池是否可用。可用,則隨機(jī)從池中獲取一個(gè)代理主機(jī),否則判斷site是否設(shè)置過(guò)直接代理主機(jī)。
然后獲取HttpUriRequest(它是HttpGet、HttpPost的接口),執(zhí)行請(qǐng)求、判斷響應(yīng)碼,并將響應(yīng)轉(zhuǎn)換成Page對(duì)象返回。期間還調(diào)用了狀態(tài)方法onSuccess,onError,但是這兩個(gè)方法都是空實(shí)現(xiàn)。(主要原因可能是在Spider中已經(jīng)通過(guò)調(diào)用Listener來(lái)處理狀態(tài)了)。
如果發(fā)生異常,調(diào)用addToCycleRetry判斷是否需要進(jìn)行重試。
如果這里返回的Page為null,在Spider中就不會(huì)調(diào)用PageProcessor,所以我們?cè)赑ageProcessor中不用擔(dān)心Page是否為null
最后的finally塊中進(jìn)行資源回收處理,回收代理入池,回收HttpClient的connection等(EntityUtils.consume(httpResponse.getEntity());)。
c、具體說(shuō)說(shuō)怎么獲取HttpUriRequest
protected HttpUriRequest getHttpUriRequest(Request request, Site site, Mapheaders,HttpHost proxy) { RequestBuilder requestBuilder = selectRequestMethod(request).setUri(request.getUrl()); if (headers != null) { for (Map.Entry 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(); }
首先調(diào)用selectRequestMethod來(lái)獲取合適的RequestBuilder,比如是GET還是POST,同時(shí)設(shè)置請(qǐng)求參數(shù)。之后便是調(diào)用HttpClient的相關(guān)API設(shè)置請(qǐng)求頭、超時(shí)時(shí)間、代理等。
關(guān)于selectRequestMethod的改動(dòng):預(yù)計(jì)在WebMagic0.6.2(目前還未發(fā)布)之后由于作者合并并修改了PR,設(shè)置POST請(qǐng)求參數(shù)會(huì)大大簡(jiǎn)化。
之前POST請(qǐng)求設(shè)置參數(shù)需要
request.putExtra("nameValuePair",NameValuePair[]);然后這個(gè)NameValuePair[]需要不斷add BasicNameValuePair,而且還需要UrlEncodedFormEntity,設(shè)置參數(shù)過(guò)程比較繁瑣,整個(gè)過(guò)程如下:
Listformparams = new ArrayList (); formparams.add(new BasicNameValuePair("channelCode", "0008")); formparams.add(new BasicNameValuePair("pageIndex", i+"")); formparams.add(new BasicNameValuePair("pageSize", "15")); formparams.add(new BasicNameValuePair("sitewebName", "廣東省")); request.putExtra("nameValuePair",formparams.toArray());
之后我們只需要如下就可以了:
request.putParam("sitewebName", "廣東省"); request.putParam("xxx", "xxx");
d、說(shuō)說(shuō)下載的內(nèi)容如何轉(zhuǎn)換為Page對(duì)象:
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; }
這個(gè)方法沒(méi)什么好說(shuō)的,唯一要說(shuō)的就是它調(diào)用getContent方法。
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); } }
getContent方法,首先判斷是否有charset(這是在Site中配置的),如果有,直接調(diào)用ApacheCommons的IOUtils將相應(yīng)內(nèi)容轉(zhuǎn)化成對(duì)應(yīng)編碼字符串,否則智能檢測(cè)響應(yīng)內(nèi)容的字符編碼。
protected String getHtmlCharset(HttpResponse httpResponse, byte[] contentBytes) throws IOException { return CharsetUtils.detectCharset(httpResponse.getEntity().getContentType().getValue(), contentBytes); }
getHtmlCharset是調(diào)用CharsetUtils來(lái)檢測(cè)字符編碼,其思路就是,首先判斷httpResponse.getEntity().getContentType().getValue()是否含有比如charset=utf-8
否則用Jsoup解析內(nèi)容,判斷是提取meta標(biāo)簽,然后判斷針對(duì)html4中html4.01 和html5中分情況判斷出字符編碼。
當(dāng)然,你懂的,如果服務(wù)端返回的不是完整的html內(nèi)容(不包含head的),甚至不是html內(nèi)容(比如json),那么就會(huì)導(dǎo)致判斷失敗,返回默認(rèn)jvm編碼值.
所以說(shuō),如果可以,最好手動(dòng)給Site設(shè)置字符編碼。
3、HttpClientGenerator
用于生成HttpClient實(shí)例,算是一種工廠模式了。
public HttpClientGenerator() { Registryreg = RegistryBuilder. create() .register("http", PlainConnectionSocketFactory.INSTANCE) .register("https", buildSSLConnectionSocketFactory()) .build(); connectionManager = new PoolingHttpClientConnectionManager(reg); connectionManager.setDefaultMaxPerRoute(100); }
構(gòu)造函數(shù)主要是注冊(cè)http以及https的socket工廠實(shí)例。https下我們需要提供自定義的工廠以忽略不可信證書(shū)校驗(yàn)(也就是信任所有證書(shū)),在webmagic0.6之前是存在不可信證書(shū)校驗(yàn)失敗這一問(wèn)題的,之后webmagic合并了一個(gè)關(guān)于這一問(wèn)題的PR,目前的策略是忽略證書(shū)校驗(yàn)、信任一切證書(shū)(這才是爬蟲(chóng)該采用的嘛,我們爬的不是安全,是寂寞。)
private CloseableHttpClient generateClient(Site site, Proxy proxy) { CredentialsProvider credsProvider = null; HttpClientBuilder httpClientBuilder = HttpClients.custom(); if(proxy!=null && StringUtils.isNotBlank(proxy.getUser()) && StringUtils.isNotBlank(proxy.getPassword())) { credsProvider= new BasicCredentialsProvider(); credsProvider.setCredentials( new AuthScope(proxy.getHttpHost().getAddress().getHostAddress(), proxy.getHttpHost().getPort()), new UsernamePasswordCredentials(proxy.getUser(), proxy.getPassword())); httpClientBuilder.setDefaultCredentialsProvider(credsProvider); } if(site!=null&&site.getHttpProxy()!=null&&site.getUsernamePasswordCredentials()!=null){ credsProvider = new BasicCredentialsProvider(); credsProvider.setCredentials( new AuthScope(site.getHttpProxy()),//可以訪問(wèn)的范圍 site.getUsernamePasswordCredentials());//用戶名和密碼 httpClientBuilder.setDefaultCredentialsProvider(credsProvider); } httpClientBuilder.setConnectionManager(connectionManager); if (site != null && site.getUserAgent() != null) { httpClientBuilder.setUserAgent(site.getUserAgent()); } else { httpClientBuilder.setUserAgent(""); } if (site == null || site.isUseGzip()) { httpClientBuilder.addInterceptorFirst(new HttpRequestInterceptor() { public void process( final HttpRequest request, final HttpContext context) throws HttpException, IOException { if (!request.containsHeader("Accept-Encoding")) { request.addHeader("Accept-Encoding", "gzip"); } } }); } //解決post/redirect/post 302跳轉(zhuǎn)問(wèn)題 httpClientBuilder.setRedirectStrategy(new CustomRedirectStrategy()); SocketConfig socketConfig = SocketConfig.custom().setSoTimeout(site.getTimeOut()).setSoKeepAlive(true).setTcpNoDelay(true).build(); httpClientBuilder.setDefaultSocketConfig(socketConfig); connectionManager.setDefaultSocketConfig(socketConfig); if (site != null) { httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(site.getRetryTimes(), true)); } generateCookie(httpClientBuilder, site); return httpClientBuilder.build(); }
前面是設(shè)置代理代理及代理的用戶名密碼
這里主要需要關(guān)注的兩點(diǎn)是
1、post/redirect/post 302跳轉(zhuǎn)問(wèn)題:這是是通過(guò)設(shè)置一個(gè)自定義的跳轉(zhuǎn)策略類來(lái)實(shí)現(xiàn)的。(這在0.6版本之前是存在問(wèn)題的,0.6版本之后合并了PR)
httpClientBuilder.setRedirectStrategy(new CustomRedirectStrategy());
CustomRedirectStrategy在繼承HttpClient自帶額LaxRedirectStrategy(支持GET,POST,HEAD,DELETE請(qǐng)求重定向跳轉(zhuǎn))的基礎(chǔ)上,對(duì)POST請(qǐng)求做了特殊化處理,如果是POST請(qǐng)求,代碼處理如下:
HttpRequestWrapper httpRequestWrapper = (HttpRequestWrapper) request; httpRequestWrapper.setURI(uri); httpRequestWrapper.removeHeaders("Content-Length");
可以看到,POST請(qǐng)求時(shí)首先會(huì)重用原先的request對(duì)象,并重新設(shè)置uri為新的重定向url,然后移除新請(qǐng)求不需要的頭部。重用request對(duì)象的好處是,post/redirect/post 302跳轉(zhuǎn)時(shí)會(huì)攜帶原有的POST參數(shù),就防止了參數(shù)丟失的問(wèn)題。
否則默認(rèn)實(shí)現(xiàn)是這樣的
if (status == HttpStatus.SC_TEMPORARY_REDIRECT) { return RequestBuilder.copy(request).setUri(uri).build(); } else { return new HttpGet(uri); }
SC_TEMPORARY_REDIRECT是307狀態(tài)碼,也就是說(shuō)只有在307狀態(tài)碼的時(shí)候才會(huì)攜帶參數(shù)跳轉(zhuǎn)。
2、HttpClient的重試: 這是是通過(guò)設(shè)置一個(gè)默認(rèn)處理器來(lái)實(shí)現(xiàn)的,同時(shí)設(shè)置了重試次數(shù)(也就是Site中配置的retryTimes)。
httpClientBuilder.setRetryHandler(newDefaultHttpRequestRetryHandler(site.getRetryTimes(), true));
之后便是配置Cookie策略。
private void generateCookie(HttpClientBuilder httpClientBuilder, Site site) { CookieStore cookieStore = new BasicCookieStore(); for (Map.EntrycookieEntry : site.getCookies().entrySet()) { BasicClientCookie cookie = new BasicClientCookie(cookieEntry.getKey(), cookieEntry.getValue()); cookie.setDomain(site.getDomain()); cookieStore.addCookie(cookie); } for (Map.Entry > domainEntry : site.getAllCookies().entrySet()) { for (Map.Entry cookieEntry : domainEntry.getValue().entrySet()) { BasicClientCookie cookie = new BasicClientCookie(cookieEntry.getKey(), cookieEntry.getValue()); cookie.setDomain(domainEntry.getKey()); cookieStore.addCookie(cookie); } } httpClientBuilder.setDefaultCookieStore(cookieStore); }
首先創(chuàng)建一個(gè)CookieStore實(shí)例,然后將Site中的cookie加入到cookieStore中。并配置到httpClientBuilder中。那么在這個(gè)HttpClient實(shí)例執(zhí)行的所有請(qǐng)求中都會(huì)用到這個(gè)cookieStore。比如登錄保持就可以通過(guò)配置Site中的Cookie來(lái)實(shí)現(xiàn)。
4、關(guān)于Page對(duì)象說(shuō)明:
Page對(duì)象代表了一個(gè)請(qǐng)求結(jié)果,或者說(shuō)相當(dāng)于頁(yè)面(當(dāng)返回json時(shí)這種說(shuō)法有點(diǎn)勉強(qiáng))。
public Html getHtml() { if (html == null) { html = new Html(UrlUtils.fixAllRelativeHrefs(rawText, request.getUrl())); } return html; }
通過(guò)它得到的頁(yè)面,原始頁(yè)面中的鏈接是不包含域名的情況下會(huì)被自動(dòng)轉(zhuǎn)換為http[s]開(kāi)頭的完整鏈接。
關(guān)于Downloader就分析到這,后續(xù)會(huì)進(jìn)行補(bǔ)充,下篇主題待定。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/66880.html
摘要:獲取正在運(yùn)行的線程數(shù),用于狀態(tài)監(jiān)控。之后初始化組件主要是初始化線程池將到中,初始化開(kāi)始時(shí)間等。如果線程池中運(yùn)行線程數(shù)量為,并且默認(rèn),那么就停止退出,結(jié)束爬蟲(chóng)。 本系列文章,針對(duì)Webmagic 0.6.1版本 一個(gè)普通爬蟲(chóng)啟動(dòng)代碼 public static void main(String[] args) { Spider.create(new GithubRepoPageP...
摘要:有一個(gè)模塊其中實(shí)現(xiàn)了一個(gè)。但是感覺(jué)靈活性不大。接口如下它會(huì)獲得一個(gè)實(shí)例,你可以在里面進(jìn)行任意的操作。本部分到此結(jié)束。 webmagic有一個(gè)selenium模塊,其中實(shí)現(xiàn)了一個(gè)SeleniumDownloader。但是感覺(jué)靈活性不大。所以我就自己參考實(shí)現(xiàn)了一個(gè)。 首先是WebDriverPool用來(lái)管理WebDriver池: import java.util.ArrayList; im...
摘要:爬蟲(chóng)框架源碼分析之爬蟲(chóng)框架源碼分析之爬蟲(chóng)框架源碼分析之爬蟲(chóng)框架源碼分析之爬蟲(chóng)框架源碼分析之之進(jìn)階 爬蟲(chóng)框架Webmagic源碼分析之Spider爬蟲(chóng)框架WebMagic源碼分析之Scheduler爬蟲(chóng)框架WebMagic源碼分析之Downloader爬蟲(chóng)框架WebMagic源碼分析之Selector爬蟲(chóng)框架WebMagic源碼分析之SeleniumWebMagic之Spider進(jìn)階
摘要:所以我模仿這些爬蟲(chóng)框架的優(yōu)勢(shì),以盡量簡(jiǎn)單的原則,搭配實(shí)際上是開(kāi)發(fā)了這套輕量級(jí)爬蟲(chóng)框架。將下載器,解析器,調(diào)度器,數(shù)據(jù)處理器注入核心成為對(duì)象。提供對(duì)爬蟲(chóng)進(jìn)行管理監(jiān)控。每個(gè)腳本被認(rèn)為是一個(gè),確定一個(gè)任務(wù)。 ??現(xiàn)在有很多爬蟲(chóng)框架,比如scrapy、webmagic、pyspider都可以在爬蟲(chóng)工作中使用,也可以直接通過(guò)requests+beautifulsoup來(lái)寫(xiě)一些個(gè)性化的小型爬蟲(chóng)腳本...
摘要:主要用于選擇器抽象類,實(shí)現(xiàn)類前面說(shuō)的兩個(gè)接口,主要用于選擇器繼承。多個(gè)選擇的情形,每個(gè)選擇器各自獨(dú)立選擇,將所有結(jié)果合并。抽象類,定義了一些模板方法。這部分源碼就不做分析了。這里需要提到的一點(diǎn)是返回的不支持選擇,返回的對(duì)象支持選擇。 1、Selector部分:接口:Selector:定義了根據(jù)字符串選擇單個(gè)元素和選擇多個(gè)元素的方法。ElementSelector:定義了根據(jù)jsoup ...
閱讀 1794·2021-10-12 10:12
閱讀 2551·2021-09-29 09:42
閱讀 2728·2021-09-03 10:28
閱讀 2262·2019-08-30 15:54
閱讀 1168·2019-08-30 15:53
閱讀 1400·2019-08-30 11:26
閱讀 3366·2019-08-30 11:02
閱讀 2149·2019-08-30 11:02