文档章节

webmagic的设计机制及原理-如何开发一个Java爬虫

黄亿华
 黄亿华
发布于 2013/07/20 14:19
字数 3932
阅读 67557
收藏 263

image

此文章是webmagic 0.1.0版的设计手册,后续版本的入门及用户手册请看这里:https://github.com/code4craft/webmagic/blob/master/user-manual.md

之前就有网友在博客里留言,觉得webmagic的实现比较有意思,想要借此研究一下爬虫。最近终于集中精力,花了三天时间,终于写完了这篇文章。之前垂直爬虫写了一年多,webmagic框架写了一个多月,这方面倒是有一些心得,希望对读者有帮助。

webmagic的目标

一般来说,一个爬虫包括几个部分:

  • 页面下载

    页面下载是一个爬虫的基础。下载页面之后才能进行其他后续操作。

  • 链接提取

    一般爬虫都会有一些初始的种子URL,但是这些URL对于爬虫是远远不够的。爬虫在爬页面的时候,需要不断发现新的链接。

  • URL管理

    最基础的URL管理,就是对已经爬过的URL和没有爬的URL做区分,防止重复爬取。

  • 内容分析和持久化

    一般来说,我们最终需要的都不是原始的HTML页面。我们需要对爬到的页面进行分析,转化成结构化的数据,并存储下来。

不同的爬虫,对这几部分的要求是不一样的。

<!--more-->

对于通用型的爬虫,例如搜索引擎蜘蛛,需要指对互联网大部分网页无差别进行抓取。这时候难点就在于页面下载和链接管理上--如果要高效的抓取更多页面,就必须进行更快的下载;同时随着链接数量的增多,需要考虑如果对大规模的链接进行去重和调度,就成了一个很大的问题。一般这些问题都会在大公司有专门的团队去解决,比如这里有一篇来自淘宝的快速构建实时抓取集群。对Java来说,如果你要研究通用爬虫,那么可以看一下heritrix或者nutch

而垂直类型的爬虫要解决的问题则不一样,比如想要爬取一些网站的新闻、博客信息,一般抓取数量要求不是很大,难点则在于如何高效的定制一个爬虫,可以精确的抽取出网页的内容,并保存成结构化的数据。这方面需求很多,webmagic就是为了解决这个目的而开发的。

使用Java语言开发爬虫是比较复杂的。虽然Java有很强大的页面下载、HTML分析工具,但是每个都有不小的学习成本,而且这些工具本身都不是专门为爬虫而生,使用起来也没有那么顺手。我曾经有一年的时间都在开发爬虫,重复的开发让人头痛。Java还有一个比较成熟的框架crawler4j,但是它是为通用爬虫而设计的,扩展性差一些,满足不了我的业务需要。我也有过自己开发框架的念头,但是终归觉得抽象的不是很好。直到发现python的爬虫框架scrapy,它将爬虫的生命周期拆分的非常清晰,我参照它进行了模块划分,并用Java的方式去实现了它,于是就有了webmagic。

代码已经托管到github,地址是https://github.com/code4craft/webmagic,Javadoc:http://code4craft.github.io/webmagic/docs/

webmagic的实现还参考了另一个Java爬虫SpiderMan。SpiderMan是一个全栈式的Java爬虫,它的设计思想跟webmagic稍有不同,它希望将Java语言的实现隔离,仅仅让用户通过配置就完成一个垂直爬虫。理论上,SpiderMan功能更强大,很多功能已经内置,而webmagic则比较灵活,适合熟悉Java语法的开发者,可以比较非常方便的进行扩展和二次开发。


webmagic的模块划分

webmagic目前的核心代码都在webmagic-core中,webmagic-samples里有一些定制爬虫的例子,可以作为参考。而webmagic-plugin目前还不完善,后期准备加入一些常用的功能。下面主要介绍webmagic-core的内容。

前面说到,webmagic参考了scrapy的模块划分,分为Spider(整个爬虫的调度框架)、Downloader(页面下载)、PageProcessor(链接提取和页面分析)、Scheduler(URL管理)、Pipeline(离线分析和持久化)几部分。只不过scrapy通过middleware实现扩展,而webmagic则通过定义这几个接口,并将其不同的实现注入主框架类Spider来实现扩展。

image

Spider类-核心调度

Spider是爬虫的入口类,Spider的接口调用采用了链式的API设计,其他功能全部通过接口注入Spider实现,下面是启动一个比较复杂的Spider的例子。

<!-- lang: java -->
Spider.create(sinaBlogProcessor)
.scheduler(new FileCacheQueueScheduler("/data/temp/webmagic/cache/"))
.pipeline(new FilePipeline())
.thread(10).run();	

Spider的核心处理流程非常简单,代码如下:

<!-- lang: java -->
private void processRequest(Request request) {
    Page page = downloader.download(request, this);
    if (page == null) {
        sleep(site.getSleepTime());
        return;
    }
    pageProcessor.process(page);
    addRequest(page);
    for (Pipeline pipeline : pipelines) {
        pipeline.process(page, this);
    }
    sleep(site.getSleepTime());
}

Downloader-页面下载

页面下载是一切爬虫的开始。

大部分爬虫都是通过模拟http请求,接收并分析响应来完成。这方面,JDK自带的HttpURLConnection可以满足最简单的需要,而Apache HttpClient(4.0后整合到HttpCompenent项目中)则是开发复杂爬虫的不二之选。它支持自定义HTTP头(对于爬虫比较有用的就是User-agent、cookie等)、自动redirect、连接复用、cookie保留、设置代理等诸多强大的功能。

webmagic使用了HttpClient 4.2,并封装到了HttpClientDownloader。学习HttpClient的使用对于构建高性能爬虫是非常有帮助的,官方的Tutorial就是很好的学习资料。目前webmagic对HttpClient的使用仍在初步阶段,不过对于一般抓取任务,已经够用了。

下面是一个使用HttpClient最简单的例子:

<!-- lang: java -->
HttpClient httpClient = new DefaultHttpClient();
HttpGet httpGet = new HttpGet("http://youhost/xxx");
HttpResponse httpResponse = httpClient.execute(httpGet);
System.out.println(EntityUtils.toString(httpResponse.getEntity().getContent())); 

对于一些Javascript动态加载的网页,仅仅使用http模拟下载工具,并不能取到页面的内容。这方面的思路有两种:一种是抽丝剥茧,分析js的逻辑,再用爬虫去重现它(比如在网页中提取关键数据,再用这些数据去构造Ajax请求,最后直接从响应体获取想要的数据); 另一种就是:内置一个浏览器,直接获取最后加载完的页面。这方面,js可以使用PhantomJS,它内部集成了webkit。而Java可以使用Selenium,这是一个非常强大的浏览器模拟工具。考虑以后将它整理成一个独立的Downloader,集成到webmagic中去。

一般没有必要去扩展Downloader。

PageProcessor-页面分析及链接抽取

这里说的页面分析主要指HTML页面的分析。页面分析可以说是垂直爬虫最复杂的一部分,在webmagic里,PageProcessor是定制爬虫的核心。通过编写一个实现PageProcessor接口的类,就可以定制一个自己的爬虫。

页面抽取最基本的方式是使用正则表达式。正则表达式好处是非常通用,解析文本的功能也很强大。但是正则表达式最大的问题是,不能真正对HTML进行语法级别的解析,没有办法处理关系到HTML结构的情况(例如处理标签嵌套)。例如,我想要抽取一个<div>里的内容,可以这样写:"<div>(.*?)</div>"。但是如果这个div内部还包含几个子div,这个时候使用正则表达式就会将子div的"</div>"作为终止符截取。为了解决这个问题,我们就需要进行HTML的分析。

HTML分析是一个比较复杂的工作,Java世界主要有几款比较方便的分析工具:

####Jsoup

Jsoup是一个集强大和便利于一体的HTML解析工具。它方便的地方是,可以用于支持用jquery中css selector的方式选取元素,这对于熟悉js的开发者来说基本没有学习成本。

<!-- lang: java -->
String content = "blabla";
Document doc = JSoup.parse(content);
Elements links = doc.select("a[href]");

Jsoup还支持白名单过滤机制,对于网站防止XSS攻击也是很好的。

####HtmlParser

HtmlParser的功能比较完备,也挺灵活,但谈不上方便。这个项目很久没有维护了,最新版本是2.1。HtmlParser的核心元素是Node,对应一个HTML标签,支持getChildren()等树状遍历方式。HtmlParser另外一个核心元素是NodeFilter,通过实现NodeFilter接口,可以对页面元素进行筛选。这里有一篇HtmlParser的使用文章:使用 HttpClient 和 HtmlParser 实现简易爬虫

####Apache tika

tika是专为抽取而生的工具,还支持PDF、Zip甚至是Java Class。使用tika分析HTML,需要自己定义一个抽取内容的Handler并继承org.xml.sax.helpers.DefaultHandler,解析方式就是xml标准的方式。crawler4j中就使用了tika作为解析工具。SAX这种流式的解析方式对于分析大文件很有用,我个人倒是认为对于解析html意义不是很大。

<!-- lang: java -->
InputStream inputStream = null;
HtmlParser htmlParser = new HtmlParser();
htmlParser.parse(new ByteArrayInputStream(page.getContentData()), 
contentHandler, metadata, new ParseContext());

####HtmlCleaner与XPath

HtmlCleaner最大的优点是:支持XPath的方式选取元素。XPath是一门在XML中查找信息的语言,也可以用于抽取HTML元素。XPath与CSS Selector大部分功能都是重合的,但是CSS Selector专门针对HTML,写法更简洁,而XPath则是通用的标准,可以精确到属性值。XPath有一定的学习成本,但是对经常需要编写爬虫的人来说,这点投入绝对是值得的。

学习XPath可以参考w3school的XPath 教程。下面是使用HtmlCleaner和xpath进行抽取的一段代码:

<!-- lang: java -->
HtmlCleaner htmlCleaner = new HtmlCleaner();
TagNode tagNode = htmlCleaner.clean(text);
Object[] objects = tagNode.evaluateXPath("xpathStr");

几个工具的对比

在这里评价这些工具的主要标准是“方便”。就拿抽取页面所有链接这一基本任务来说,几种代码分别如下:

XPath:

<!-- lang: java -->
tagNode.evaluateXPath("//a/@href")

CSS Selector:

<!-- lang: java -->
//使用类似js的实现
$("a[href]").attr("href")

HtmlParser:

<!-- lang: java -->
Parser p = new Parser(value);
NodeFilter aFilter = new TagNameFilter("a");
NodeList nodes = p.extractAllNodesThatMatch(aFilter);
for (int i = 0; i < nodes.size(); i++) {
    Node eachNode = nodes.elementAt(i);
    if (eachNode instanceof LinkTag) {
        LinkTag linkTag = (LinkTag) eachNode;
        System.out.println(linkTag.extractLink());
    }
}

XPath是最简单的,可以精确选取到href属性值;而CSS Selector则次之,可以选取到HTML标签,属性值需要调用函数去获取;而HtmlParser和SAX则需要手动写程序去处理标签了,比较麻烦。

webmagic的Selector

Selector是webmagic为了简化页面抽取开发的独立模块,是整个项目中我最得意的部分。这里整合了CSS Selector、XPath和正则表达式,并可以进行链式的抽取,很容易就实现强大的功能。即使你使用自己开发的爬虫工具,webmagic的Selector仍然值得一试。

例如,我已经下载了一个页面,现在要抽取某个区域的所有包含"blog"的链接,我可以这样写:

<!-- lang: java -->
//content是用别的爬虫工具抽取到的正文
String content = "blabla";
List<String> links = Html.create(content)
.$("div.title")  //css 选择,Java里虽然很少有$符号出现,不过貌似$作为方法名是合法的
.xpath("//@href")  //提取链接
.regex(".*blog.*") //正则匹配过滤
.all(); //转换为string列表

另外,webmagic的抓取链接需要显示的调用Page.addTargetRequests()去添加,这也是为了灵活性考虑的(很多时候,下一步的URL不是单纯的页面href链接,可能会根据页面模块进行抽取,甚至可能是自己拼凑出来的)。

补充一个有意思的话题,就是对于页面正文的自动抽取。相信用过Evernote Clearly都会对其自动抽取正文的技术印象深刻。这个技术又叫Readability,webmagic对readability有一个粗略的实现SmartContentSelector,用的是P标签密度计算的方法,在测试oschina博客时有不错的效果。

Scheduler-URL管理

URL管理的问题可大可小。对于小规模的抓取,URL管理是很简单的。我们只需要将待抓取URL和已抓取URL分开保存,并进行去重即可。使用JDK内置的集合类型Set、List或者Queue都可以满足需要。如果我们要进行多线程抓取,则可以选择线程安全的容器,例如LinkedBlockingQueue以及ConcurrentHashMap。

因为小规模的URL管理非常简单,很多框架都并不将其抽象为一个模块,而是直接融入到代码中。但是实际上,抽象出Scheduler模块,会使得框架的解耦程度上升一个档次,并非常容易进行横向扩展,这也是我从scrapy中学到的。

在webmagic的设计中,除了Scheduler模块,其他的处理-从下载、解析到持久化,每个任务都是互相独立的,因此可以通过多个Spider共用一个Scheduler来进行扩展。排除去重的因素,URL管理天生就是一个队列,我们可以很方便的用分布式的队列工具去扩展它,也可以基于mysql、redis或者mongodb这样的存储工具来构造一个队列,这样构建一个多线程乃至分布式的爬虫就轻而易举了。

URL去重也是一个比较复杂的问题。如果数据量较少,则使用hash的方式就能很好解决。数据量较大的情况下,可以使用Bloom Filter或者更复杂的方式。

webmagic目前有两个Scheduler的实现,QueueScheduler是一个简单的内存队列,速度较快,并且是线程安全的,FileCacheQueueScheduler则是一个文件队列,它可以用于耗时较长的下载任务,在任务中途停止后,下次执行仍然从中止的URL开始继续爬取。

Pipeline-离线处理和持久化

Pipeline其实也是容易被忽略的一部分。大家都知道持久化的重要性,但是很多框架都选择直接在页面抽取的时候将持久化一起完成,例如crawer4j。但是Pipeline真正的好处是,将页面的在线分析和离线处理拆分开来,可以在一些线程里进行下载,另一些线程里进行处理和持久化。

你可以扩展Pipeline来实现抽取结果的持久化,将其保存到你想要保存的地方-本地文件、数据库、mongodb等等。Pipeline的处理目前还是在线的,但是修改为离线的也并不困难。

webmagic目前只支持控制台输出和文件持久化,但是持久化到数据库也是很容易的。

结语

webmagic确实是一个山寨的框架,本身也没有太多创新的东西,但是确实对Java爬虫的实现有了一些简化。在强大便利的功能和较高的灵活性中间,webmagic选择了后者,目标就是要打造一个熟练的Java开发者也用的比较顺手的工具,并且可以集成到自己的业务系统中,这一点我自己开发了不少这样的业务,对其灵活性还是比较有信心的。webmagic目前的代码实现还比较简单(不到2000行),如果有兴趣的阅读代码可能也会有一些收获,也非常欢迎建议和指正。

最后再次附上代码地址:https://github.com/code4craft/webmagic

© 著作权归作者所有

黄亿华

黄亿华

粉丝 2436
博文 131
码字总数 116344
作品 7
程序员
私信 提问
加载中

评论(94)

suanmb
suanmb
支持
g
garfieldkai
同问,黄老师,对于现在版本的webmagic,有没有加入对不同url的processor的管理了么?
藍色季風
藍色季風

引用来自“肖申克救赎”的评论

发现scrapy好为什么不直接用它,而有费劲开发一个java版的?
开发这个版本还需要跟你说理由?
浮生若梦E
浮生若梦E
啥也不说了,必须支持!
乌龟壳
乌龟壳
哈哈,自己做爬虫,只用了jsoup帮忙,其实看了下和你做的事情差不多,也都有用到redis。不过一些细节上实现的方式不同
b
banyue427
请问作者有没有加入IP代理以及验证码识别的打算?
ShieldW
ShieldW
老师您好!我使用过程中有个疑问,我看到ResultItems中fields保存的是该Page的结果,但是如果一个页面包含许多结果的话,这个fields只会保存该页面的最后一个结果啊?前面的结果会被这个Map覆盖掉啊,page.putfield(key,value)不能直接这样用了吧?
wohenkuaihuo
wohenkuaihuo
该框架貌似不支持https页面的下载
wohenkuaihuo
wohenkuaihuo
该框架貌似不支持https页面的下载
nmdxpc
nmdxpc

引用来自“黄亿华”的评论

引用来自“lidongyang”的评论

感谢回复啊,再咨询作者一个问题,我的需求是:抓取列表页,抽取出详情页链接,和下一页链接,然后抓取详情页抽取数据。
我这样设计不知是否合理,请作者指点一二那:列表页对应一个spider,它拥有自己的scheduler、pageProcessor等,scheduler管理的是‘下一页’的链接,而详情页的链接则在pageProcessor里边抽取后放入‘详情页链接队列’。详情页对应一个spider,拥有自己的scheduler、pageProcessor等,scheduler从‘详情页链接队列’取url进行抓取。每个spider在自己的线程或者进程中运行,这样应该涉及到生产者消费者的问题,我看webmagic里边对于队列为空的等待是100ms,不知道会不会出问题。

这样可能会有问题,详情队列空之后就停了... 我建议做法是使用同一个scheduler,然后在同一个pageProcessor里根据url确定处理逻辑,当然这样子会有点难看。webmagic0.1.0版本比较简单,在新版本中会对不同url的processor对应关系进行管理。
你好,请问现在版本的webmagic加入了对不同url的processor的管理了么?
webmagic 0.1.0版本发布,Java垂直爬虫框架

第一个正式版本。 修改了若干API,使得可扩展性更强,重写了Pipeline接口,将抽取结果集包装到ResultItems对象,便于逻辑分离。 增加下载的重试机制,支持gzip,支持自定义UA/cookie。 增加多...

黄亿华
2013/07/25
1K
13
webmagic 0.2.0 发布,Java垂直爬虫框架

此次更新的主题是"方便"(之前的主题是"灵活")。 增加了webmagic-extension模块。 增加了注解方式支持,可以通过POJO+注解的方式编写一个爬虫,更符合Java开发习惯。以下是抓取oschina博客的完...

黄亿华
2013/08/12
5.1K
22
WebMagic 0.4.1 发布,Java 爬虫框架

此次更新加强了Ajax抓取的功能,并进行了一些功能改进。同时引入了重要的脚本化功能"webmagic-script",为今后的WebMagic-Avalon计划做准备。 功能增强: 修复了抓取完页面后,Spider偶尔无法...

黄亿华
2013/11/28
3.3K
17
webmagic 0.2.1 发布,Java爬虫框架

此次更新主要是抽取模块的一些改动,使得抽取更加方便。 完成一些国际化的工作,并发布到了maven中央库。0.2.1及以后的版本直接在项目中添加依赖webmagic-core/webmagic-externsion即可。 为...

黄亿华
2013/08/21
2.2K
6
WebMagic使用说明-安装篇

WebMagic使用说明-安装篇 本文是WebMagic文档的一部分。系列文章写完后,会整合到WebMagic新版文档中。 1. 使用WebMagic WebMagic主要包含两个jar包:和。在项目中添加这两个包的依赖,即可使...

黄亿华
2014/04/03
3.3K
13

没有更多内容

加载失败,请刷新页面

加载更多

centos7 linuxdeployqt qt5.13.1 打包程序

原文链接:https://www.cnblogs.com/linuxAndMcu/p/11016322.html 一、简介 linuxdeployqt 是Linux下的qt打包工具,可以将应用程序使用的资源(如库,图形和插件)复制到二进制运行文件所在的...

shzwork
25分钟前
4
0
IDEA 配置Springboot项目热部署

实现的方式概述 注意以下的热部署方式在IDEA是默认没有打开自动编译的,手动编译需要快捷键(Ctrl+Shift+F9),自动编译的修改配置如下:(注意刷新不要太快,会有1-2秒延迟) File-Settings-C...

小强的进阶之路
35分钟前
6
0
免费数据分析工具:secsoso

前段时间思考了理想数据分析平台,之后我们根据这个思路开发了spl语言并提供了一个数据分析平台,这个平台主要用在搜索ES,数据库索引中的数据。但后来发现对文件的事后处理也是个非常重要的...

赛克蓝德
37分钟前
5
0
暗黑2不能正常启动?带你轻松使用WIN10运行游戏

暗黑破坏神2这款游戏由于年代比较久远,所以设置启动这方面与现在的大部分游戏有很大差距,由于当初完美运行暗黑2是当年使用最多的XP系统,在使用现在大多数玩家使用的WIN7到WIN10系统常会出...

太空堡垒185
41分钟前
6
0
maven项目对象模型(二)

1.4.4.传递性依赖 一个传递性依赖就是一个依赖的依赖。如果project-a依赖于project-b,而后者接着依赖于project-c,那么project-c就是被认为是project-a的传递性依赖。如果project-c依赖于p...

万建宁
42分钟前
5
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部