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

image

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

webmagic的目标

通常来讲,一个爬虫包括几个部分:html

  • 页面下载node

    页面下载是一个爬虫的基础。下载页面以后才能进行其余后续操做。python

  • 连接提取mysql

    通常爬虫都会有一些初始的种子URL,可是这些URL对于爬虫是远远不够的。爬虫在爬页面的时候,须要不断发现新的连接。jquery

  • URL管理nginx

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

  • 内容分析和持久化github

    通常来讲,咱们最终须要的都不是原始的HTML页面。咱们须要对爬到的页面进行分析,转化成结构化的数据,并存储下来。web

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

对于通用型的爬虫,例如搜索引擎蜘蛛,须要指对互联网大部分网页无差异进行抓取。这时候难点就在于页面下载和连接管理上–若是要高效的抓取更多页面,就必须进行更快的下载;同时随着连接数量的增多,须要考虑若是对大规模的连接进行去重和调度,就成了一个很大的问题。通常这些问题都会在大公司有专门的团队去解决,好比这里有一篇来自淘宝的快速构建实时抓取集群。对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的例子。

Spider.create(sinaBlogProcessor) .scheduler(new FileCacheQueueScheduler("/data/temp/webmagic/cache/")) .pipeline(new FilePipeline()) .thread(10).run(); 

Spider的核心处理流程很是简单,代码以下:

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最简单的例子:

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的”

“做为终止符截取。为了解决这个问题,咱们就须要进行HTML的分析。

 

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

Jsoup

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

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意义不是很大。

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进行抽取的一段代码:

HtmlCleaner htmlCleaner = new HtmlCleaner(); TagNode tagNode = htmlCleaner.clean(text); Object[] objects = tagNode.evaluateXPath("xpathStr"); 

几个工具的对比

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

XPath:

tagNode.evaluateXPath("//a/@href") 

CSS Selector:

//使用相似js的实现 $("a[href]").attr("href") 

HtmlParser:

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"的连接,我能够这样写:

//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

相关文章
相关标签/搜索