上篇:Java-网络爬虫(一)
之前有介绍过传统实现爬虫的技术 HttpClient
和 Jsoup
,并提供了一些案例,但是作为企业级的应用,还是远远不够的,竟然如此就需要一些更深入的技术 WebMagic
。
官网:https://webmagic.io/
WebMagic
是一款基于 Java
的开源网络爬虫框架,底层是 HttpClient
和 Jsoup
,它提供了简单、灵活、强大的爬取功能,可以用于抓取网页数据、图片、文件等。WebMagic
的设计参考了 Scapy
,但是实现方式更 Java
化一些。
该框架分为核心和扩展两个部分,核心部分是一个精简、模块化的爬虫实现,而扩展部分则包括一些便利的、实用性的功能。
webmagic core
):提供非常简单、灵活的 API
,在基本不改变开发模式的情况下,编写一个爬虫webmagic extension
):提供一些便捷的功能,例如注解模式编写爬虫等,同时内置了一些常用的组件,便于开发优点:
Python
和 Scala
等语言的版本,能够适应不同开发者的需求Quartz
等任务调度框架结合使用,实现定时爬取数据的功能CPU
提高爬虫效率Cookie
、代理等功能,能够模拟登录、避免反爬等操作MySQL
、Redis
、Elasticsearch
等,方便后续数据处理404
、解析错误等,提高爬虫的健壮性WebMagic
的核心非常简单,但是覆盖了爬虫的整个流程,也是很好的学习爬虫开发的材料。它提供简单灵活的 API
,只需少量代码即可实现一个爬虫UA/cookie
等功能js
动态渲染的页面架构介绍:
WebMagic
的结构分为 Downloader
、PageProcessor
、Scheduler
、Pipeline
四大组件,这四大组件对应爬虫生命周期中的下载、处理、管理和持久化等功能。
Downloader
:
PageProcessor
:
Scheduler
:
Pipeline
:
用于数据流转的对象:
Request
:
Page
:
ResultItems
:
而 Spider
则将这几个组件组织起来,让它们可以互相交互,流程化的执行,可以认为 Spider
是一个大的容器,它也是 WebMagic
逻辑的核心。
工作原理:
从架构图中可以得知:
http
请求(其实 http
请求之后也会转换为 Request
)进入到 Downloader
之后会进行页面下载,输出 Page
Page
经过 PageProcessor
之后开始解析页面,会有两种输出类型:Request
和 ResultItems
,对应两种情况
Request
url
地址,则产生一些新的 Request
进入 Scheduler
等待进一步抓取Downloader
会从 Scheduler
拉取待处理的 Request
ResultItems
ResultItems
中,再流转至 Pipeline
Pipeline
对抽取的结果进行处理如果有一个 Maven
工程的项目,可跳过
打开 IDEA
工具,点击 File
-> New
-> Project...
创建一个项目
选择 Maven
设置项目保存地址,点击 Finish
创建完成
不过使用 WebMagic
一般会将爬取到的结果数据持久到数据中,所以这里建议是搭建 SpringBoot
或者 SpringCloud
项目,但是搭建这些项目不是本文的重点,如果想要搭建简单的 SpringBoot
项目可参见 SpringBoot - 快速搭建
WebMagic
分为两个部分:核心和扩展,可在 Maven 仓库 中查询这这两个依赖
<dependency>
<groupId>us.codecraftgroupId>
<artifactId>webmagic-coreartifactId>
<version>0.10.0version>
dependency>
<dependency>
<groupId>us.codecraftgroupId>
<artifactId>webmagic-extensionartifactId>
<version>0.10.0version>
dependency>
还是使用 WebMagic
爬取一个网站为例作为入门,在上篇博客中我们爬取了 https://www.rgbku.com/chaxun.html(rgb颜色查询器) 这个网站的表格信息,现在我们使用 WebMagic
获取底部链接的信息
查看 代码编写: 运行结果如下: 可以看到是能够成功获取得到想要的数据。 如果查看过 在上述案例中有使用过 可以认为 获取 例如上述入门案例中就是使用 从源码上也可以看到 所以才说它是一个大的容器,同时 从源码上可以看到 之前说 进入到 通过设置 通过合理配置 之前提到过 在 上篇:Java-网络爬虫(一) 博客中有通过 在 该对象最重要的两个作用就是获取 常用方法: 总的来说, 通过入门案例中使用 在 抽取元素的四种方式: XPath 的教程可参考:https://www.w3cschool.cn/xpath/ 正则表达式-基础教程:https://blog.csdn.net/xhmico/article/details/126729869 正则表达式是一种通用的文本抽取语言,一般会用这种方式获取 入门案例中使用的就是这种方式获取元素, 关于它的使用可参考:JsonPath完全介绍及详细使用教程 API: 以下是 从这些 输出: 前面就有提到过 默认情况下 查看 如果希望下载页面时进行一些其它的操作,可以自定义 如果想要自定义的 PageProcessor 的作用就是负责解析页面的 它的输入是 而页面的解析是开发者根据需求去编写,也就是说要开发者去实现, 而 之前说过 也可以从进入入门案例 同时 默认情况下, 通常情况下开发者会自定义 上篇:Java-网络爬虫(一) 参考博客: WebMagic:https://blog.csdn.net/weixin_40055163/article/details/123541437 JsonPath完全介绍及详细使用教程:https://blog.csdn.net/software_test010/article/details/125427926html
源码可知只需要获取到.zh
-> ->
,然后再拿到
标签的
href
属性内容即可
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.processor.PageProcessor;
import us.codecraft.webmagic.selector.Html;
import java.util.ArrayList;
import java.util.List;
public class TestProcessorDemo implements PageProcessor {
@Override
public void process(Page page) {
// 通过 page 获取 Html 对象
Html html = page.getHtml();
// 通过 Html 可以获取到 Document
Document document = html.getDocument();
// 有了 Document 就能够通过 Jsoup 的一些操作进行解析,比如说获取 标签元素
Elements aElements = document.select("div.zh > div > span > a");
// 创建 List 集合用于存放 标签的超链接信息
List<String> links = new ArrayList<>();
for (Element aElement : aElements) {
// 获取 标签中的链接
String href = aElement.attr("href");
// 添加到集合中
links.add(href);
}
System.err.println("links = " + links);
/*
* 不过一般也不会用 Jsoup 的方式来解析 Html
* WebMagic 有一些解析器可以较为方便的拿到这些元素,比如使用 css 解析器
*/
List<String> href = html.css("div.zh > div > span > a", "href").all();
// 在将结果封装到 ResultItems(默认设置下会打印在控制台上)
page.putField("href", href);
}
@Override
public Site getSite() {
return PageProcessor.super.getSite();
}
public static void main(String[] args) {
Spider.create(new TestProcessorDemo())
// 设置起始的 URL
.addUrl("https://www.rgbku.com/chaxun.html")
// 在当前线程中执行爬虫
//.run();
// 在新线程中执行爬虫
.start();
}
}
WebMagic
的核心包中的源码,可以发现其实有几个现成的 demo
案例
四、核心对象&组件
PageProcessor
、Page
、Site
、ResultItems
、Spider
等对象,以下内容我会结合源码对这些对象和组件的一些概要说明1. 核心对象
Sipder
Spider
是一个大的容器,它也是 WebMagic
逻辑的核心。它的作用是:将各个组件组织起来,使它们能够相互协作,形成一个完整的爬虫系统。它负责管理和调度各个组件的运行,以确保整个爬虫过程的顺利进行。Spider
对象的方法有两个,要么通过构造方法 new
出来,要么使用静态方法 create(PageProcessor pageProcessor)
创建出来:public class Spider implements Runnable, Task {
...
public static Spider create(PageProcessor pageProcessor) {
return new Spider(pageProcessor);
}
public Spider(PageProcessor pageProcessor) {
this.newUrlCondition = this.newUrlLock.newCondition();
this.pageCount = new AtomicLong(0L);
this.emptySleepTime = 30000L;
this.pageProcessor = pageProcessor;
this.site = pageProcessor.getSite();
}
...
}
create
的方式Spider spider = Spider.create(new TestProcessorDemo());
Spider
中包含了 webMaigc
的四大组件的影子Spider
中有各个组件的配置以及线程相关的方法,是 webMagic
的核心
Request
Request
是对 URL
地址的一层封装,一个 Request
对象对应一个 URL
地址。Request
对象中包含了发送一个 http
请求的所有要素,包括 url
地址、请求类型、请求参数、cookie
和 header
等等,还包含一个 Key-Value
结构的字段 extra
。可以在 extra
中保存一些特殊的属性,然后在其他地方读取,以完成不同的功能。http
在进入到下载组件 Download
是便会转化成 Request
对象,可以通过入门案例中的代码spider.addUrl("https://www.rgbku.com/chaxun.html")
addUrl(String... urls)
方法中看到public class Spider implements Runnable, Task {
...
public Spider addUrl(String... urls) {
String[] var2 = urls;
int var3 = urls.length;
for(int var4 = 0; var4 < var3; ++var4) {
String url = var2[var4];
// 将 url 转化成 Request 对象,加载到调度器 Scheduler 中
this.addRequest(new Request(url));
}
this.signalNewUrl();
return this;
}
...
}
Site
Site
对象用于配置站点本身的一些配置信息,例如编码、HTTP
头、超时时间、重试策略等、代理等。其中待解析的域名存放在 domain
这个属性中Site
对象,可以对爬虫的行为进行详细配置,以满足不同的需求。具体来说,Site
对象可以设置以下配置信息:
HTTP
头:用于设置请求头信息,以模拟浏览器行为,增加爬虫的隐蔽性Site
对象提供的配置方法如下:
方法
说明
setCharset(String charset)
设置字符编码方式,以确保爬虫能够正确解析页面内容
setUserAgent(String userAgent)
设置用户代理,用于标识发送请求的客户端应用或设备,更好的模拟游览器发送请求
Site setDomain(String domain)
设置域名,需要设置域名后,
addCookie()
才会生效
setSleepTime(int sleepTime)
设置爬虫在抓取下一个页面之前等待的时间,以避免过于频繁的请求导致被目标站点封禁
setTimeOut(int timeOut)
设置爬虫请求的超时时间,以避免因网络延迟等原因导致请求等待过长时间
setRetrySleepTime(int retrySleepTime)
设置爬虫在遇到请求失败时的重试间隔时间
setRetryTimes(int retryTimes)
设置爬虫在遇到请求失败时的重试次数
addHeader(String key, String value)
添加请求头信息,以模拟浏览器行为,增加爬虫的隐蔽性
addCookie(String key, String value)
添加
Cookie
信息,以模拟浏览器会话信息,增加爬虫的隐蔽性Site
对象的参数,可以优化爬虫的性能,提高爬虫的效率和成功率,增加爬虫的隐蔽性。domian
存放的是待解析域名,从入门案例中的 addUrl(String... urls)
-> addRequest(Request request)
可以看到这条逻辑,当然不止这一处public class Spider implements Runnable, Task {
...
private void addRequest(Request request) {
if (this.site.getDomain() == null && request != null && request.getUrl() != null) {
// 将清洗过的 url 存放到 Site 的 domain 属性中
this.site.setDomain(UrlUtils.getDomain(request.getUrl()));
}
this.scheduler.push(request, this);
}
...
}
HttpClient
的 execute(HttpUriRequest var1)
方法来发送请求(HttpGet、HttpPost...
均实现了 HttpUriRequest
) // 创建 httpClient 对象
CloseableHttpClient httpClient = HttpClients.createDefault();
// 创建 httpGet 对象,设置访问 URL
HttpGet httpGet = new HttpGet("https://www.rgbku.com/chaxun.html");
// 发送请求
response = httpClient.execute(httpGet);
webMagic
中底层也是使用 HttpClient
来发送请求的,不够首先要通过 Site
和 Request
获取到 HttpUriRequest
和 HttpContext
对象,进而就可以执行HttpClient
的 execute(HttpUriRequest var1, HttpContext var2)
方法发送请求获取响应信息了
Page
Page
对象是用于处理和封装从目标网站下载得到的 HTML
页面内容的一种对象Html
对象,而 Html
对象是解析网页十分重要的对象,其次是该 Page
对象能够能够将封装好的 ResultItems
对象传送给存储器 Pipeline
做持久化处理
方法
说明
getHtml()
获取当前页面的
Html
对象
String getRawText()
获取当前页面的文本内容
putField(String key, Object field)
以
key-value
的形式封装信息到 ResultItems
中
ResultItems
ResultItems
是一个 Map
对象,用于保存 PageProcessor
处理的结果,供Pipeline使用。它的 API
与 Map
类似,可以保存各种类型的数据,包括字符串、列表、字典等ResultItems
的主要作用是作为 PageProcessor
和 Pipeline
之间的数据传输媒介。当 PageProcessor
处理完一个页面后,可以将处理结果保存到 ResultItems
中,然后由 Pipeline
进行处理。这样可以方便地实现数据的提取、清洗、过滤等操作,并将结果持久化到文件、数据库等地方ResultItems
还提供了一些额外的方法来控制结果的输出和处理。例如,可以通过 setSkip(true)
方法来跳过当前的结果,不进行后续的处理和输出。此外,ResultItems
还提供了 getExtra()
方法,用于获取一些自定义的数据和属性WebMagic
中的 ResultItems
是一个重要的组件,它充当了 PageProcessor
和 Pipeline
之间的桥梁,使得数据的处理和输出更加灵活和方便。
Html(Selectable)
page.getHtml()
能够获取到一个 Html
对象,解析相关的操作都与该对象有关,而这个对象实现了 Selectable
WebMagic
中,Selectable
是一个重要的接口,它定义了一系列链式 API
调用方式Selectable
里主要支持了四种抽取技术:XPath
、正则表达式和 CSS
选择器。另外,对于 JSON
格式的内容,可使用 JsonPath
进行解析。
List<String> href = html.xpath("//div[@class=zh]/div/span/a/@href").all();
url
地址等page.putField("links", page.getHtml().regex("[a-zA-z]+://[^\\s][^\" ]*").all());
CSS
选择器是与 XPath
类似的语言,但相对于 XPath
而言简单一点,只要对 css 语法熟悉,写起来应该是比较简单的。比方说入门案例标签中的链接的代码还可以改成:
List<String> href = html.css(".zh a", "href").all();
JsonPath
是于 XPath
很类似的一个语言,它用于从 Json
中快速定位一条内容Selectable
提供了一系列链式 API
调用方式,支持多种选择器,包括 xpath
、css
、regex
和 jsonPath
等。用户可以使用这些选择器方便地获取所需元素的信息,同时还提供了诸如获取链接等便利方法。简单来说就是根据特定的方法抽取 html
页面的信息。使用 Selectable
接口,可以直接完成页面元素的链式抽取,也无需去关心抽取的细节。public interface Selectable {
Selectable xpath(String var1);
Selectable $(String var1);
Selectable $(String var1, String var2);
Selectable css(String var1);
Selectable css(String var1, String var2);
Selectable smartContent();
Selectable links();
Selectable regex(String var1);
Selectable regex(String var1, int var2);
Selectable replace(String var1, String var2);
String toString();
String get();
boolean match();
List<String> all();
Selectable jsonPath(String var1);
Selectable select(Selector var1);
Selectable selectList(Selector var1);
List<Selectable> nodes();
}
Selectable
常用 API
的介绍
方法
说明
示例
Selectable xpath(String var1)
使用
XPath
选择器html.xpath(“//div[@class=‘title’]”)
Selectable regex(String var1)
使用正则表达式抽取
html.regex(“(.*?)”)
Selectable regex(String var1, int var2)
使用正则表达式抽取,并指定捕获组
html.regex(“(.*?)”,1)
Selectable $(String var1)
使用
Css
选择器选择html.$(“div.title”)
Selectable $(String var1, String var2)
使用
Css
选择器选择html.$(“div.title”,“text”)
Selectable css(String var1)
功能同
$()
,使用 Css
选择器选择html.css(“div.title”)
Selectable css(String var1, String var2)
功能同
$()
,使用Css选择器选择html.css(“div.title”,“text”)
Selectable jsonPath(String var1)
使用
JsonPath
选择器选择html.jsonPath(“$.*”)
Selectable links()
获取所有链接,如果链接为相对地址会自动拼接
html.links()
Selectable replace(String regex, String replacement)
替换内容
html.replace(“”,“”)
String get()
返回一条String类型的结果
String link= html.links().get()
String toString()
功能同
get()
,返回一条 String
类型的结果String link= html.links().toString()
List all()
返回所有抽取结果
List links= html.links().all()
boolean match()
是否有匹配结果
boolean result = html.links().match()
Html
除了 Selectable
这些常用的 API
外还有如下几个方法用得也比较多
方法
说明
Document getDocument()
获取
Document
对象
static Html create(String text)
通过文本获取
Html
对象API
中可以看出,那些抽取元素的方法返回的都是 Selectable
对象,也就是说抽取是支持链式调用的, List<String> href = html
// css 选择类为 zh 的标签
.css(".zh")
// 获取其下所有的链接
.links() // 使用这种方式获取链接,如果链接是相对地址的形式会自动进行拼接
.all();
2. 四大组件
Downloader
Downloader
的作用就是负责从互联网上下载页面Downloader
的输入是 Request
输出是 Pag
,可见 Downloader.java
源码:public interface Downloader {
Page download(Request var1, Task var2);
void setThread(int var1);
}
Spider
配置的 Downloader
为 HttpClientDownloader
,可以从 initComponent()
方法中得出HttpClientDownloader.java
源码中的 download()
方法public class HttpClientDownloader extends AbstractDownloader {
...
public Page download(Request request, Task task) {
if (task != null && task.getSite() != null) {
CloseableHttpResponse httpResponse = null;
// 获取 HttpClient 对象
CloseableHttpClient httpClient = this.getHttpClient(task.getSite());
Proxy proxy = this.proxyProvider != null ? this.proxyProvider.getProxy(request, task) : null;
// 通过 Request 和 Site 得到 HttpUriRequest、HttpContext
HttpClientRequestContext requestContext = this.httpUriRequestConverter.convert(request, task.getSite(), proxy);
Page page = Page.fail(request);
Page var9;
try {
// 发起请求
httpResponse = httpClient.execute(requestContext.getHttpUriRequest(), requestContext.getHttpClientContext());
// 解析响应体并封装成 Page 返回
page = this.handleResponse(request, request.getCharset() != null ? request.getCharset() : task.getSite().getCharset(), httpResponse, task);
this.onSuccess(page, task);
this.logger.info("downloading page success {}", request.getUrl());
Page var8 = page;
return var8;
} catch (IOException var13) {
this.onError(page, task, var13);
this.logger.info("download page {} error", request.getUrl(), var13);
var9 = page;
} finally {
if (httpResponse != null) {
EntityUtils.consumeQuietly(httpResponse.getEntity());
}
if (this.proxyProvider != null && proxy != null) {
this.proxyProvider.returnProxy(proxy, page, task);
}
}
return var9;
} else {
throw new NullPointerException("task or site can not be null");
}
}
...
}
Downloader
,要么实现 Downloader
接口,要么继承实现了 Downloader
接口的子类,比如:HttpClientDownloader
Downloader
生效,就需要在 spider.setDownloader()
方法中进行设置,比如: Spider.create(new TestProcessorDemo())
// 设置起始的 URL
.addUrl("https://www.rgbku.com/chaxun.html")
// 设置自定义 Downloader
.setDownloader(new MyDownload())
// 在当前线程中执行爬虫
.run();
// 在新线程中执行爬虫
//.start();
PageProcessor
Page
对象,process() 方法中实现页面解析的逻辑,PageProcessor.java
源码如下:public interface PageProcessor {
void process(Page var1);
default Site getSite() {
return Site.me();
}
}
webMaic
也没办法提供默认的 PageProcessor
, 所以使用 webMaigc
编写爬虫的时候都需要去实现 PageProcessor
接口或者继承实现了 PageProcessor
接口的类,比如:SimplePageProcessor
。而解析的结果最好是封装到 ResultItems
中交给 Pipeline
进行处理,Page 对象中可以通过 putField(String key, Object field)
方法直接将对象封装到 ResultItems
中public class Page {
...
public void putField(String key, Object field) {
this.resultItems.put(key, field);
}
...
}
Scheduler
Scheduler
负责管理待抓取的 URL
Scheduler.java
源码:public interface Scheduler {
void push(Request var1, Task var2);
Request poll(Task var1);
}
Scheduler
默认是 QueueScheduler
,可查看 Spider.java
的源码得出QueueScheduler
的底层就如同它的类名一样,是个队列Downloader
下载页面时需要 Request
对象,而这些 Request
对象都是从 Scheduler
中拉取而来的,包括起始的 url
也是会先放到 Scheduler
,可以从入门案例中的 spider.addUrl()
方法去追溯源码证实sipder.run()
方法中看到传入 Downloader
的 Request
是从 Scheduler
中获取的Scheduler
也可以自定义,只需要实现 Scheduler
接口或者继承其实现类,比如:QueueScheduler
即可,然后通过 spider.setScheduler()
方法去设置,例如: Spider.create(new TestProcessorDemo())
// 设置起始的 URL
.addUrl("https://www.rgbku.com/chaxun.html")
// 设置自定义 Scheduler
.setScheduler(new MyScheduler())
// 在当前线程中执行爬虫
.run();
// 在新线程中执行爬虫
//.start();
Pipeline
Pipeline
负责抽取结果的处理,包括计算、持久化到文件、数据库等Pipeline
.java 源码:public interface Pipeline {
void process(ResultItems var1, Task var2);
}
webMaigc
的使用的 Pipeline
是 ConsolePipeline
,可查看 Spider.initComponent()
的方法源码得知:ConsolePipeline
的处理就是将封存在 ResultItems
里的内容打印到控制台上ConsolePipeline.java
源码: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());
}
}
}
Spider
中可以配置多个 Pipeline
Pipeline
,将爬取的数据存放在数据库中,自定义的方式就是实现 Pipeline
接口重写 process()
方法,通过 spider.setPipelines()
来设置,例如: List<Pipeline> pipelines = new ArrayList<>();
// 添加输出到控制台的 Pipeline:ConsolePipeline
pipelines.add(new ConsolePipeline());
// 添加保存到文本的 Pipeline:FilePipeline
pipelines.add(new FilePipeline());
// 添加自定义自定义 Pipeline
pipelines.add(new MyPipeline());
Spider.create(new TestProcessorDemo())
// 设置起始的 URL
.addUrl("https://www.rgbku.com/chaxun.html")
// 设置 Pipeline
.setPipelines(pipelines)
// 在当前线程中执行爬虫
.run();
// 在新线程中执行爬虫
//.start();