概要:WebMagicは構造が分かれDownloader
、PageProcessor
、Scheduler
、Pipeline
四つの成分は、スパイダーが互いにによってそれらを整理。これらの4つのコンポーネントは、クローラーのライフサイクルにおけるダウンロード、処理、管理、および永続化の機能に対応しています。
WebmagicのスタートアップクラスはSpiderです。run()メソッドが実行されると、さまざまなコンポーネントが初期化され、ループで実行されます。リクエストはスケジューラから取得され、runnableにパッケージ化され、処理されます。このプロセスでは、Schedulerコンポーネントが重複排除と保留中のリクエストの読み込みを担当し、Downloaderコンポーネントがリクエスト結果のダウンロードとページクラスへのパッケージ化を担当し、pageProcessorコンポーネントがページ結果の処理を担当し、URLを追加できます。ページから収集し、page.resultItemsとともにインストールする必要があります。処理された結果データは、結果を処理するためにPipelineコンポーネントに送信されます。
具体的な使用方法については、公式ドキュメントhttp://webmagic.io/docs/zh/を参照するか、関連するケースを検索してください。この記事では、主にwebmagicのソースコードを分析します。
1.プロセス分析
1.主な方法
public void run() {
// 检查运行状态,并且compareAndSet成运行
checkRunningStat();
// 初始化组件
initComponent();
logger.info("Spider {} started!",getUUID());
// 当前线程非中断,运行状态持续运行。
while (!Thread.currentThread().isInterrupted() && stat.get() == STAT_RUNNING) {
final Request request = scheduler.poll(this);
if (request == null) {
if (threadPool.getThreadAlive() == 0 && exitWhenComplete) {
break;
}
// wait until new url added
waitNewUrl();
} else {
threadPool.execute(new Runnable() {
@Override
public void run() {
try {
// 处理请求,下载page,pageProcess处理page,pipeline处理page
processRequest(request);
onSuccess(request);
} catch (Exception e) {
onError(request);
logger.error("process request " + request + " error", e);
} finally {
pageCount.incrementAndGet();
signalNewUrl();
}
}
});
}
}
// 设置状态为停止
stat.set(STAT_STOPPED);
if (destroyWhenExit) {
close();
}
logger.info("Spider {} closed! {} pages downloaded.", getUUID(), pageCount.get());
}
上記からわかるように、Spiderはさまざまなコンポーネントの実行を調整することです。コアロジックは、スケジューラからの要求を受け取り、それをスレッドプールによって実行されるタスクにパッケージ化することです。いくつかの主要なコンポーネントがそれらを担当します。このプロセスにおけるそれぞれの責任。
2)コンポーネントinitComponentを初期化します
protected void initComponent() {
if (downloader == null) { // 默认Downloader为HttpClientDownloader
this.downloader = new HttpClientDownloader();
}
if (pipelines.isEmpty()) { // 默认Pipeline为ConsolePipeline
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); // 线程池为CountableThreadPool包装
}
}
if (startRequests != null) { // 初始添加请求到scheduler, 默认scheduler为QueueScheduler
for (Request request : startRequests) {
addRequest(request);
}
startRequests.clear();
}
startTime = new Date();
}
いくつかのコアコンポーネントダウンローダーの初期実装-HttpClientDownloader、パイプライン-ConsolePipeline、スケジューラー-QueueSchedulerpageProcessor-手動で作成。
3.リクエストメソッドprocessRequestを処理しています
private void processRequest(Request request) {
Page page = downloader.download(request, this); // 下载-即通过HttpClient下载页面,并且构建Page对象。
if (page.isDownloadSuccess()){ // 下载成功
onDownloadSuccess(request, page);
} else { // 失败循环
onDownloaderFail(request);
}
}
//ダウンロードは正常に処理されました
private void onDownloadSuccess(Request request, Page page) {
if (site.getAcceptStatCode().contains(page.getStatusCode())){
pageProcessor.process(page); // 先用pageProcessor处理page
extractAndAddRequests(page, spawnUrl); // 提取添加的request
if (!page.getResultItems().isSkip()) { // 如果没有设置isSkip
for (Pipeline pipeline : pipelines) {
pipeline.process(page.getResultItems(), this); // 多个pipelines处理结果
}
}
}
sleep(site.getSleepTime());
return;
}
ダウンロードを成功させるための処理ロジックは、pageProcessorを介してページを処理し、addTragetRequestを抽出してスケジューラーに追加し、isSkipが設定されているかどうかを判断し、設定されていない場合は、複数のパイプラインを使用して結果を処理します。
///ダウンロード失敗の処理
private void onDownloaderFail(Request request) {
if (site.getCycleRetryTimes() == 0) { // 如果错误循环重试次数为0,那么只停顿
sleep(site.getSleepTime());
} else {
doCycleRetry(request); // 循环重试---详见下一代码片段
}
}
//ループの再試行
private void doCycleRetry(Request request) {
Object cycleTriedTimesObject = request.getExtra(Request.CYCLE_TRIED_TIMES);
if (cycleTriedTimesObject == null) { // 循环重试次数0时,clone一个新请求添加设置次数1
addRequest(SerializationUtils.clone(request).setPriority(0).putExtra(Request.CYCLE_TRIED_TIMES, 1));
} else {
int cycleTriedTimes = (Integer) cycleTriedTimesObject;
cycleTriedTimes++; // 重试1次是,clone一个新请求,将附加参数递增。
if (cycleTriedTimes < site.getCycleRetryTimes()) {
addRequest(SerializationUtils.clone(request).setPriority(0).putExtra(Request.CYCLE_TRIED_TIMES, cycleTriedTimes));
}
}
sleep(site.getRetrySleepTime());
}
上記のプロセスでは、公式バージョンよりも2つの詳細なフローチャートが描かれています
2、成分分析-スケジューラー(スケジューリング)
スケジューラスケジューラは、クロールされるURLの管理とスケジュール設定、および一部の重複排除作業を担当します。WebMagicは、デフォルトでLinkedBlockingQueueメモリキューを提供してURLを管理し、コレクションを使用して重複を削除します。また、分散管理のためのRedisの使用もサポートしています。主なスケジューラの実装は次のとおりです。
DuplicateRemovedScheduler:スケジューラの基本クラスである組み込みのDuplicatedRemoverは、HashSetDuplicateRemoverとして実装されます。
QueueScheduler:LinkedBlockingQueueを介して収集される接続を格納するキュースケジューラ。
FileCacheQueueScheduler:ファイルキャッシュキュースケジューラ。キューに基づいて、すべてのURLとポーリングの場所がファイルを介して記録されます。一時的に停止した場合は、ファイルを介して収集を再開できます。
RedisScheduler:Redisを使用してクロールキューを保存します。listはキューキューを保存し、setは重複排除、ハッシュ、および要求された追加データの保存を担当します。
2.1 DuplicateRemovedScheduler
public abstract class DuplicateRemovedScheduler implements Scheduler {
// 默认重复器
private DuplicateRemover duplicatedRemover = new HashSetDuplicateRemover();
public DuplicateRemover getDuplicateRemover() {
return duplicatedRemover;
}
public DuplicateRemovedScheduler setDuplicateRemover(DuplicateRemover duplicatedRemover) {
this.duplicatedRemover = duplicatedRemover;
return this;
}
// 是post请求或循环重试或不重复才添加。
@Override
public void push(Request request, Task task) {
logger.trace("get a candidate url {}", request.getUrl());
if (shouldReserved(request) || noNeedToRemoveDuplicate(request) || !duplicatedRemover.isDuplicate(request, task)) {
logger.debug("push to queue {}", request.getUrl());
pushWhenNoDuplicate(request, task);
}
}
// 循环重试的请求不进行判断
protected boolean shouldReserved(Request request) {
return request.getExtra(Request.CYCLE_TRIED_TIMES) != null;
}
// POST请求不进行判断
protected boolean noNeedToRemoveDuplicate(Request request) {
return HttpConstant.Method.POST.equalsIgnoreCase(request.getMethod());
}
// 子类负责实现
protected void pushWhenNoDuplicate(Request request, Task task) {
}
}
2.2QueueSchedulerメモリキューのURLスケジューリング
QueueSchedulerの実装では、内部のLinkedBlockingQueueがURLの取得または取得を担当します。
2.3 RedisSchedulerRedis分散URLスケジューリング
1)メンバー変数
protected JedisPool pool;
private static final String QUEUE_PREFIX = "queue_"; // 构建list队列的key的前缀
private static final String SET_PREFIX = "set_"; // 构建set的key的前缀
private static final String ITEM_PREFIX = "item_"; // 构建hash的key的前缀
public RedisScheduler(JedisPool pool) {
this.pool = pool;
setDuplicateRemover(this); // 设置重复的实现为自身。
}
protected String getSetKey(Task task) {
return SET_PREFIX + task.getUUID();
}
protected String getQueueKey(Task task) {
return QUEUE_PREFIX + task.getUUID();
}
protected String getItemKey(Task task) {
return ITEM_PREFIX + task.getUUID();
}
上記はメンバー変数です。URLキューはキューを介して保存され、セットは重複排除を担当し、ハッシュは補助データの保存を担当します。
さらに、コンストラクターはjedisPoolを渡すかビルドし、DuplicateRemover自体を実現し、このオブジェクトのduplicateRemoverフィールドに設定しました。
2)重複排除の実装
public boolean isDuplicate(Request request, Task task) {
Jedis jedis = pool.getResource();
try {
return jedis.sadd(getSetKey(task), request.getUrl()) == 0;
} finally {
pool.returnResource(jedis);
}
}
3)プッシュ
public void pushWhenNoDuplicate(Request request, Task task) {
Jedis jedis = pool.getResource();
try {
jedis.rpush(getQueueKey(task), request.getUrl());
if (request.getExtras() != null) {
String field = DigestUtils.md5Hex(request.getUrl());
String value = JSON.toJSONString(request);
jedis.hset(getItemKey(task), field, value);
}
} finally {
pool.returnResource(jedis);
}
}
4)プール
public Request poll(Task task) {
Jedis jedis = pool.getResource();
try {
String url = jedis.lpop(getQueueKey(task));
if (url == null) {
return null;
}
String key = getItemKey(task);
String field = DigestUtils.md5Hex(url);
byte[] bytes = jedis.hget(key.getBytes(), field.getBytes());
if (bytes != null) {
Request o = JSON.parseObject(new String(bytes), Request.class);
return o;
}
return new Request(url);
} finally {
pool.returnResource(jedis);
}
}
2.3 FileCacheQueueScheduler
FileCacheQueueSchedulerは、ファイルキャッシュキュースケジューラです。主に2つのファイルを使用して、すべてのURLと使用されたカーソルの数を記録します。initメソッドは、プッシュまたはポーリングの実行時に初期化され、初期化中に2つのファイルが読み取られます。すべてのURLファイルに保存されているURLはURLに追加され、カーソルの数よりも大きいURLはリクエストとしてパッケージ化され、キューに追加されます。
また、FileCacheQueueSchedulerには、初期化時に2つのprintWriterが作成され、プッシュ(インクリメンタル)時に同時にこのライターに追加されたURLを書き込む役割があり、ポーリング時に、ポーリングの数がカーソルを介して記録されるため、回復時にカーソルを介して判断することができます。カーソルよりも大きいURLはポーリングで使用されません。
@Override
public Request poll(Task task) {
if (!inited.get()) {
init(task);
}
// poll时候,需要文件记录cursor角标 (poll过的个数)
fileCursorWriter.println(cursor.incrementAndGet());
return queue.poll();
}
@Override
protected void pushWhenNoDuplicate(Request request, Task task) {
if (!inited.get()) {
init(task);
}
queue.add(request);
// push时候,需要文件记录url
fileUrlWriter.println(request.getUrl());
}
push和poll添加url和cursor到队列和urls时,同步写入到文件中。
private void init(Task task) {
this.task = task;
File file = new File(filePath);
if (!file.exists()) {
file.mkdirs();
}
readFile();
initWriter();
initFlushThread();
inited.set(true);
logger.info("init cache scheduler success");
}
初期化メソッドは主に、URLとリクエストの復元、ファイルファイルからのカーソル値の読み取り、およびURLファイルからのURLの読み取りに使用されます。次に、キューの回復は、カーソルより大きいURLに基づいて構築されたrequest(url)です。
3、成分分析-ダウンローダー
ダウンローダーは、その後の処理のためにインターネットからページをダウンロードする責任があります。WebMagicは、使用のApacheのHttpClientをすることにより、ダウンロードツールとしてデフォルト。
デフォルトのダウンロード実装はHttpClientDownloaderです。コア処理は、httpClientを介してページデータをダウンロードし、それをPageオブジェクトにアセンブルすることです。
public Page download(Request request, Task task) {
if (task == null || task.getSite() == null) {
throw new NullPointerException("task or site can not be null");
}
CloseableHttpResponse httpResponse = null;
CloseableHttpClient httpClient = getHttpClient(task.getSite());
Proxy proxy = proxyProvider != null ? proxyProvider.getProxy(task) : null;
HttpClientRequestContext requestContext = httpUriRequestConverter.convert(request, task.getSite(), proxy);
Page page = Page.fail();
try {
httpResponse = httpClient.execute(requestContext.getHttpUriRequest(), requestContext.getHttpClientContext());
page = handleResponse(request, request.getCharset() != null ? request.getCharset() : task.getSite().getCharset(), httpResponse, task);
onSuccess(request);
logger.info("downloading page success {}", request.getUrl());
return page;
} catch (IOException e) {
logger.warn("download page {} error", request.getUrl(), e);
onError(request);
return page;
} finally {
if (httpResponse != null) {
//ensure the connection is released back to pool
EntityUtils.consumeQuietly(httpResponse.getEntity());
}
if (proxyProvider != null && proxy != null) {
proxyProvider.returnProxy(proxy, page, task);
}
}
}
第四に、成分分析-パイプライン
Pipelineは、計算、ファイル、データベースなどへの永続性など、抽出結果の処理を担当します。デフォルトでは、WebMagicは「コンソールへの出力」と「ファイルへの保存」の2つの結果処理ソリューションを提供します。
1.コンソールパイプライン
public class ConsolePipeline implements Pipeline {
@Override
public void process(ResultItems resultItems, Task task) {
System.out.println("get page: " + resultItems.getRequest().getUrl());
for (Map.Entry<String, Object> entry : resultItems.getAll().entrySet()) {
System.out.println(entry.getKey() + ":\t" + entry.getValue());
}
}
}
2.ファイルパイプライン
public class FilePipeline extends FilePersistentBase implements Pipeline {
public FilePipeline() {
setPath("/data/webmagic/");
}
public FilePipeline(String path) {
setPath(path);
}
@Override
public void process(ResultItems resultItems, Task task) {
String path = this.path + PATH_SEPERATOR + task.getUUID() + PATH_SEPERATOR;
try {
PrintWriter printWriter = new PrintWriter(new OutputStreamWriter(new FileOutputStream(getFile(path + DigestUtils.md5Hex(resultItems.getRequest().getUrl()) + ".html")),"UTF-8"));
printWriter.println("url:\t" + resultItems.getRequest().getUrl());
for (Map.Entry<String, Object> entry : resultItems.getAll().entrySet()) {
if (entry.getValue() instanceof Iterable) {
Iterable value = (Iterable) entry.getValue();
printWriter.println(entry.getKey() + ":");
for (Object o : value) {
printWriter.println(o);
}
} else {
printWriter.println(entry.getKey() + ":\t" + entry.getValue());
}
}
printWriter.close();
} catch (IOException e) {
logger.warn("write file error", e);
}
}
}
webmagic解析作業の2番目の部分
Webmagicページの抽出と分析の作業、webmagic分析ページの要素は、xpath、css、json、regularを介して行うことができます。
抽出と分析の作業は、主に2つのトップレベルのインターフェイスによって実装されます。
選択可能:ページ要素のコンテンツをラップするチェーン解析操作の最上位インターフェース。(一部のAPIは、異なる構文解析メソッドを実装するために異なるセレクターを呼び出します)
セレクター:パーサーのトップレベルインターフェイス
1.実際の操作のセレクターインターフェイスの分析から始めましょう。
トップレベルインターフェイスには、いくつかの一般的な実装クラスがあります。セレクター :
BaseElementSelector:jsoup、jsoup.parse(text)によって実装される基本クラス、および
サブクラスによって実装されるテンプレートメソッドCssSelector:
cssセレクターの実装LinksSelector:実装CSSセレクタリンク抽出実装原理element.select( '')element.attr( "ABS:HREF")またはelement.attr( "HREF")
XpathSelector:Xsoup(拡張jsoup)に基づいて、XPathのセレクタの実装
JsonPathSelector。 jsonPathに基づくjson解析の実装
1、顶级接口Selector
public interface Selector {
// 提取一个结果
public String select(String text);
// 提取所有的结果
public List<String> selectList(String text);
}
2、BaseElementSelector基类
扩展实现了ElementSelector接口,根据返回值是是Element和结果值是String,以及结果List还是single一共有4个未实现
的抽象方法,并且有四个已经实现的方法(通过Jsoup.parse(text) 将参数变成Element)
public abstract Element selectElement(Element element);
public abstract List<Element> selectElements(Element element);
public String select(Element element);
public List<String> selectList(Element element);
3、CssSelector类
元素返回值的实现很简单,就是调用Jsoup本身的节点api操作。
@Override
public Element selectElement(Element element) {
Elements elements = element.select(selectorText);
if (CollectionUtils.isNotEmpty(elements)) {
return elements.get(0);
}
return null;
}
@Override
public List<Element> selectElements(Element element) {
return element.select(selectorText);
}
String返回值的实现,就要提取元素中的text字符串
public List<String> selectList(Element doc) {
List<String> strings = new ArrayList<String>();
List<Element> elements = selectElements(doc);
if (CollectionUtils.isNotEmpty(elements)) {
for (Element element : elements) {
String value = getValue(element); // getValue获取每个元素的值,默认是提取outerHtml
if (value != null) {
strings.add(value);
}
}
}
return strings;
}
4.XpathSelectorクラス--- xpath解析は、
主にXsoupに基づいてセレクターを呼び出すことです。Xsoupはcssセレクターを拡張し、xpathルールに従って選択します。この拡張機能は個別に分析されます。
5、RegexSelector类 --- regex正则解析
private void compileRegex(String regexStr) {
// 大小写不分,点可以代表全部(默认不匹配换行)
this.regex = Pattern.compile(regexStr, Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
this.regexStr = regexStr;
}
// 构造方法
public RegexSelector(String regexStr) {
this.compileRegex(regexStr);
if (regex.matcher("").groupCount() == 0) {
this.group = 0; // 没有捕捉组是全部
} else {
this.group = 1; // 有捕捉组是第一个捕捉组内容
}
}
// select实现
@Override
public String select(String text) {
return selectGroup(text).get(group);
}
// selectGroup所有的
public RegexResult selectGroup(String text) {
Matcher matcher = regex.matcher(text);
if (matcher.find()) {
String[] groups = new String[matcher.groupCount() + 1]; // +1是因为全部结果计算为0.
for (int i = 0; i < groups.length; i++) {
groups[i] = matcher.group(i);
}
return new RegexResult(groups);
}
return RegexResult.EMPTY_RESULT;
}
次に、選択可能なユニバーサルパッケージページ要素であり、APIインターフェイスのさまざまな分析方法を提供します。つまり、さまざまなセレクターを呼び出すことでさまざまな機能を実現できます。
1. Selectableトップレベルインターフェイスの
主な実装:AbstractSelectable
HtmlNode:HTML要素PlainText
:プレーンテキスト
public interface Selectable {
public Selectable xpath(String xpath);
public Selectable $(String selector);
public Selectable $(String selector, String attrName);
public Selectable css(String selector);
public Selectable css(String selector, String attrName);
public Selectable smartContent();
public Selectable links();
public Selectable regex(String regex);
public Selectable regex(String regex, int group);
public Selectable replace(String regex, String replacement);
public String toString();
public String get();
public boolean match();
public List<String> all();
public Selectable jsonPath(String jsonPath);
public Selectable select(Selector selector);
public Selectable selectList(Selector selector);
public List<Selectable> nodes();
}
2.関数の実現。多くのインターフェース方法があるため、分析に一般的に使用されるものをいくつか選択します。
1) css方法 ($()方法)
由HtmlNode实现,HtmlNode的属性有List<Element>,在创建节点时就会进行赋值。
public Selectable $(String selector, String attrName) {
CssSelector cssSelector = Selectors.$(selector, attrName); // 创建了一个css选择器
return selectElements(cssSelector); // 具体解析方法
}
protected Selectable selectElements(BaseElementSelector elementSelector) {
ListIterator<Element> elementIterator = getElements().listIterator();
if (!elementSelector.hasAttribute()) { // 无设置属性
List<Element> resultElements = new ArrayList<Element>();
while (elementIterator.hasNext()) {
Element element = checkElementAndConvert(elementIterator); // 设置第一个节点为Document节点
List<Element> selectElements = elementSelector.selectElements(element);
resultElements.addAll(selectElements);
}
return new HtmlNode(resultElements); // 默认htmlNode最后返回也是包装成htmlNode
} else { // 有设置属性
// has attribute, consider as plaintext
List<String> resultStrings = new ArrayList<String>();
while (elementIterator.hasNext()) {
Element element = checkElementAndConvert(elementIterator);
List<String> selectList = elementSelector.selectList(element);
resultStrings.addAll(selectList);
}
return new PlainText(resultStrings); // 包装成纯文本节点返回
}
}
2)、regex 正则实现
public Selectable regex(String regex, int group) {
RegexSelector regexSelector = Selectors.regex(regex, group); // 正则选择器
return selectList(regexSelector, getSourceTexts()); // getSourceTexts获取文本--由子类实现
}
protected Selectable selectList(Selector selector, List<String> strings) {
List<String> results = new ArrayList<String>();
for (String string : strings) {
List<String> result = selector.selectList(string);
results.addAll(result);
}
return new PlainText(results);
}
//getSourceTexts的子类实现---HtmlNode
protected List<String> getSourceTexts() {
List<String> sourceTexts = new ArrayList<String>(getElements().size());
for (Element element : getElements()) { // 所有的element进行toString操作。
sourceTexts.add(element.toString());
}
return sourceTexts;
}
//getSourceTexts的子类实现---PlainText
protected List<String> getSourceTexts() {
return sourceTexts;
}
3.選択可能なものの使用
3、selectable的使用
这些不同的selectable都挂载在Page对象 的属性下。
private Html html;
private Json json;
private String rawText;
private Selectable url;
public Html getHtml() {
if (html == null) {
html = new Html(rawText, request.getUrl());
}
return html;
}
public Json getJson() {
if (json == null) {
json = new Json(rawText);
}
return json;
}
// 下载阶段设置的url属性包装的Selectable
page.setUrl(new PlainText(request.getUrl()));
// 可自由使用的rawText
page.setRawText(new String(bytes, charset));
第2部の要約: webmagicは、解析対象の要素を選択可能な一般的なインターフェイスにパッケージ化し、解析のさまざまな操作をセレクターにパッケージ化する方法について説明します。選択可能な継承チェーンは、主にテキストノードとhtmlノードに分けられます。インターフェイスのapiメソッドオブジェクト、およびテキストノードのhtmlノードには、ユニバーサルに操作できない部分がある場合があります。このメソッドは実装をサポートしていません。実装できるメソッドは、さまざまなセレクターを呼び出すことによって実装されます。
終わり!