更新提示(2013-03-13):最新版本更新:
- 支持定向抓取某频道
<!-- | name:目标名称 --> <target name="travel" isForceUseXmlParser="1"> <!-- | 限制目标URL的来源为网易旅游子频道,在spiderman里面把频道页叫做"来源url" --> <sourceRules policy="and"> <rule type="regex" value="http://travel\.163\.com/special/cjgat(_\d+)?/"> <!-- | 定义如何在来源页面上挖掘新的 URL --> <digUrls> <field name="source_url" isArray="1"> <parsers> <parser xpath="//div[@class='list-page']//a[@href]" attribute="href"/> </parsers> </field> <!-- | 在spiderman里面把详细文章页叫做"目标url" --> <field name="target_url" isArray="1"> <parsers> <parser xpath="//div[@class='list-item clearfix']//div[@class='item-top']//h2//a[@href]" attribute="href"/> <parser exp="$this.replace('#p=891JUOOO17KK0006','')" /> </parsers> </field> </digUrls> </rule> </sourceRules>
- 支持分页抓取单篇文章
<!-- | 目标URL的规则 --> <urlRules policy="and"> <rule type="regex" value="http://travel\.163\.com/\d{2}/\d{4}/\d{2}/\w[^_]+\.html"> <!-- | 递归抓取详细页的分页,单篇文章的分页会按顺序抓取保证内容抓取的顺序跟页码一致 --> <nextPage> <field name="next_url"> <parsers> <!-- | 正如field的name=next_url意思一样,这里的规则主要是来解析"当前"页的下一页url是什么,我们都知道分页页面里面肯定都有"下一页"入口的,抓到这个,然后递归即可 --> <parser xpath="//div[@class='ep-pages']//a[@class='ep-pages-ctrl']" attribute="href" /> </parsers> </field> </nextPage> </rule> </urlRules> <!-- | 另外还需要在<model>下的<field>多添加一个参数 isAlsoParseInNextPage="1" 告诉爬虫该字段需要在分页里继续解析的,比如下面这个content字段,是需要在“下一页”里继续解析的 --> <model name="travel-article"> <field name="content" isArray="1" isAlsoParseInNextPage="1">
- 支持站点内多host
<!-- | 告诉爬虫仅抓取以下这些host的链接,多数是应对二级或多级域名的情况 --> <validHosts> <validHost value="travel.163.com" /> <validHost value="wwww.163.com" /> </validHosts>
- 支持多个种子链接
<!-- | 配置多个种子链接 | url:种子链接 --> <seeds> <seed url="" /> </seeds>
- HTML页面也可以强制使用XPath轴、XPath各种函数解析
<!-- | isForceUseXmlParser 当解析的页面是HTML时,除了XPath基本功能外很多XPath功能都不支持,例如XPath轴、其他高级函数等,将此参数设置为 1 即可让其支持,但是会带来某些不确定的问题【暂时未发现】 --> <target name="travel" isForceUseXmlParser="1">
- 其他
<!-- | skipStatusCode:设置哪些状态码需要忽略,多个用逗号隔开,一旦设置了,那么爬虫将会忽略掉一些错误的statusCode,并且继续解析返回的内容 | userAgent:设置爬虫标识 | includeHttps:是否抓取https页 --> <site skipStatusCode="500,501" userAgent="Spiderman[https://gitcafe.com/laiweiwei/Spiderman]" includeHttps="0">
<target>
<model>
<field name="title">
<parsers>
<parser xpath="//div[@class='QTitle']/h1/text()"/>
</parsers>
</field>
</model>
</target>
核心提示:本文介绍了如何使用垂直类网络爬虫#Spiderman#抓取目标网站 “感兴趣” 的数据,这里简单地演示了如何抓取OSC【本站】的问答数据,引出后文对另外一个复杂的团购网站内容的抓取,该网站的团购信息中,我们需要在JS代码里抓取过期时间、需要过滤团购的一些描述信息【保留一些标签,去掉一些标签,去掉属性等】、需要获取好几个地方的图片、需要获取团购的价格、购买人数等。关键的地方在于前面所述的这一切都将通过一个配置文件解决,无需编写一句代码。
所使用的爬虫工具介绍:
#Spiderman#,Java开源垂直类网络爬虫,使用XPath、正则、表达式引擎让你轻松地抓取任何目标网站你“感兴趣”的内容。基于多线程、微内核、插件式的架构。
Spiderman的正式版本还没有发布,但是在github里面有最新的代码可以取下来并且使用maven构建。
Spiderman依赖于EWeb4J的xml读写功能,因此还需要把最新的EWeb4J源码从github拉下来构建。
下面介绍如何抓取OSC的问答数据:
- 首先,我们来看看目标网页长什么样子的:)
图中红色区域就是我们“感兴趣”的内容,从上到下依次为:标题,作者,问题内容,问题关联的标签,答案列表 一共五个属性。 - 然后,从spiderman-sample里拷贝一份xml配置文件按照上述需求编辑之后:
先看没有注释的“简洁版”:
下面这个是加了注释的版本,便于理解:)<?xml version="1.0" encoding="UTF-8"?> <beans> <site name="oschina" url="http://www.oschina.net/question" reqDelay="1s" enable="1" charset="utf-8" schedule="1h" thread="2" waitQueue="10s"> <queueRules policy="and"> <rule type="!regex" value="^.*\.(jpg|png|gif).*$" /> </queueRules> <targets> <target name="deal"> <urls policy="and"> <rule type="regex" value="http://www\.oschina\.net/question/\d+_\d+" /> </urls> <model> <field name="title"> <parser xpath="//div[@class='QTitle']/h1/text()"/> </field> <field name="content"> <parser xpath="//div[@class='Content']//div[@class='detail']" exp="$Tags.xml($output($this)).rm('div').Attrs().rm('style').ok()" /> </field> <field name="author"> <parser xpath="//div[@class='stat']//a[@target='_blank']/text()"/> </field> <field name="tags" isArray="1"> <parser xpath="//div[@class='Tags']//a/text()"/> </field> <field name="answers" isArray="1"> <parser xpath="//li[@class='Answer']//div[@class='detail']/text()" /> </field> </model> </target> </targets> <plugins> <plugin enable="1" name="spider_plugin" version="0.0.1" desc="这是一个官方实现的默认插件,实现了所有扩展点。"> <extensions> <extension point="task_poll"> <impl type="" value="spiderman.plugin.impl.TaskPollPointImpl" sort="0"/> </extension> <extension point="begin"> <impl type="" value="spiderman.plugin.impl.BeginPointImpl" sort="0"/> </extension> <extension point="fetch"> <impl type="" value="spiderman.plugin.impl.FetchPointImpl" sort="0"/> </extension> <extension point="dig"> <impl type="" value="spiderman.plugin.impl.DigPointImpl" sort="0"/> </extension> <extension point="dup_removal"> <impl type="" value="spiderman.plugin.impl.DupRemovalPointImpl" sort="0"/> </extension> <extension point="task_sort"> <impl type="" value="spiderman.plugin.impl.TaskSortPointImpl" sort="0"/> </extension> <extension point="task_push"> <impl type="" value="spiderman.plugin.impl.TaskPushPointImpl" sort="0"/> </extension> <extension point="target"> <impl type="" value="spiderman.plugin.impl.TargetPointImpl" sort="0"/> </extension> <extension point="parse"> <impl type="" value="spiderman.plugin.impl.ParsePointImpl" sort="0"/> </extension> <extension point="end"> <impl type="" value="spiderman.plugin.impl.EndPointImpl" sort="0"/> </extension> </extensions> <providers> <provider> <orgnization name="" website="" desc=""> <author name="weiwei" website="" email="l.weiwei@163.com" weibo="http://weibo.com/weiweimiss" desc="一个喜欢自由、音乐、绘画的IT老男孩" /> </orgnization> </provider> </providers> </plugin> </plugins> </site> </beans>
<?xml version="1.0" encoding="UTF-8"?> <!-- | Spiderman Java开源垂直网络爬虫 | author: l.weiwei@163.com | blog: http://laiweiweihi.iteye.com | qq: 493781187 | time: 2013-01-08 16:12 --> <beans> <!-- | name:名称 | url:种子链接 | reqDelay:{n}s|{n}m|{n}h|n每次请求之前延缓时间 | enable:0|1是否开启本网站的抓取 | charset:网站字符集 | schedule:调度时间,每隔多长时间重新从种子链接抓取 | thread:分配给本网站爬虫的线程数 | waitQueue:当任务队列空的时候爬虫等待多长时间再索取任务 --> <site name="oschina" url="http://www.oschina.net/question" reqDelay="1s" enable="1" charset="utf-8" schedule="1h" thread="2" waitQueue="10s"> <!-- | HTTP Header <headers> <header name="" value="" /> </headers>--> <!-- | HTTP Cookie <cookies> <cookie name="" value="" domain="" path="" /> </cookies>--> <!-- | 进入任务队列的URL规则 | policy:多个rule的策略,暂时只实现了and,未来会有or --> <queueRules policy="and"> <!-- | 规则 | type:规则类型,包括 regex | equal | start | end | contains 所有规则可以在前面添加 "!" 表示取反 | value:值 --> <rule type="!regex" value="^.*\.(jpg|png|gif).*$" /> </queueRules> <!-- | 抓取目标 --> <targets> <!-- | name:目标名称 --> <target name="deal"> <!-- | 目标URL匹配规则 --> <urls policy="and"> <!-- | 同前面的队列规则 --> <rule type="regex" value="http://www\.oschina\.net/question/\d+_\d+" /> </urls> <!-- | 目标网页的数据模型 --> <model> <!-- | 属性的配置 | name:属性名称 | parser:针对该属性的解析规则 --> <field name="title"> <!-- | xpath: XPath规则,如果目标页面是XML,则可以使用2.0语法,否则HTML的话暂时只能1.0 | attribute:当使用XPath解析后的内容不是文本而是一个Node节点对象的时候,可以给定一个属性名获取其属性值例如<img src="" /> | regex:当使用XPath(包括attribute)规则获取到的文本内容不满足需求时,可以继续设置regex正则表达式进行解析 | exp:当使用XPath获取的文本(如果获取的不是文本则会先执行exp而不是regex否则先执行regex)不满足需求时,可以继续这是exp表达式进行解析 | exp表达式有几个内置对象和方法: | $output(Node): 这个是内置的output函数,作用是输出某个XML节点的结构内容。参数是一个XML节点对象,可以通过XPath获得 | $this: 当使用XPath获取到的是Node节点时,这个表示节点对象,否则表示Java的字符串对象,可以调用Java字符串API进行处理 | $Tags: 这个是内置的用于过滤标签的工具类 | $Tags.xml($output($this)).rm('p').ok() | $Tags.xml($this).rm('p').empty().ok() | $Attrs: 这个是内置的用于过滤属性的工具类 | $Attrs.xml($this).rm('style').ok() | $Attrs.xml($this).tag('img').rm('src').ok() | | $Tags和$Attrs可以一起使用: | $Tags.xml($this).rm('p').Attrs().rm('style').ok() | $Attrs.xml($this).rm('style').Tags().rm('p').ok() --> <parser xpath="//div[@class='QTitle']/h1/text()"/> </field> <field name="content"> <parser xpath="//div[@class='Content']//div[@class='detail']" exp="$Tags.xml($output($this)).rm('div').Attrs().rm('style').ok()" /> </field> <field name="author"> <parser xpath="//div[@class='stat']//a[@target='_blank']/text()"/> </field> <field name="tags" isArray="1"> <parser xpath="//div[@class='Tags']//a/text()"/> </field> <field name="answers" isArray="1"> <parser xpath="//li[@class='Answer']//div[@class='detail']/text()" /> </field> </model> </target> </targets> <!-- | 插件 --> <plugins> <!-- | enable:是否开启 | name:插件名 | version:插件版本 | desc:插件描述 --> <plugin enable="1" name="spider_plugin" version="0.0.1" desc="这是一个官方实现的默认插件,实现了所有扩展点。"> <!-- | 每个插件包含了对若干扩展点的实现 --> <extensions> <!-- | point:扩展点名它们包括 task_poll, begin, fetch, dig, dup_removal, task_sort, task_push, target, parse, pojo, end --> <extension point="task_poll"> <!-- | 扩展点实现类 | type: 如何获取实现类 ,默认通过无参构造器实例化给定的类名,可以设置为ioc,这样就会从EWeb4J的IOC容器里获取 | value: 当时type=ioc的时候填写IOC的bean_id,否则填写完整类名 | sort: 排序,同一个扩展点有多个实现类,这些实现类会以责任链的方式进行执行,因此它们的执行顺序将变得很重要 --> <impl type="" value="spiderman.plugin.impl.TaskPollPointImpl" sort="0"/> </extension> <extension point="begin"> <impl type="" value="spiderman.plugin.impl.BeginPointImpl" sort="0"/> </extension> <extension point="fetch"> <impl type="" value="spiderman.plugin.impl.FetchPointImpl" sort="0"/> </extension> <extension point="dig"> <impl type="" value="spiderman.plugin.impl.DigPointImpl" sort="0"/> </extension> <extension point="dup_removal"> <impl type="" value="spiderman.plugin.impl.DupRemovalPointImpl" sort="0"/> </extension> <extension point="task_sort"> <impl type="" value="spiderman.plugin.impl.TaskSortPointImpl" sort="0"/> </extension> <extension point="task_push"> <impl type="" value="spiderman.plugin.impl.TaskPushPointImpl" sort="0"/> </extension> <extension point="target"> <impl type="" value="spiderman.plugin.impl.TargetPointImpl" sort="0"/> </extension> <extension point="parse"> <impl type="" value="spiderman.plugin.impl.ParsePointImpl" sort="0"/> </extension> <extension point="end"> <impl type="" value="spiderman.plugin.impl.EndPointImpl" sort="0"/> </extension> </extensions> <providers> <provider> <orgnization name="" website="" desc=""> <author name="weiwei" website="" email="l.weiwei@163.com" weibo="http://weibo.com/weiweimiss" desc="一个喜欢自由、音乐、绘画的IT老男孩" /> </orgnization> </provider> </providers> </plugin> </plugins> </site> </beans>
- 编写代码启动爬虫:
原谅我写的比较啰嗦的代码 :)import java.io.File; import java.util.List; import java.util.Map; import org.eweb4j.config.EWeb4JConfig; import org.eweb4j.spiderman.spider.SpiderListener; import org.eweb4j.spiderman.spider.SpiderListenerAdaptor; import org.eweb4j.spiderman.spider.Spiderman; import org.eweb4j.spiderman.task.Task; import org.eweb4j.util.CommonUtil; import org.eweb4j.util.FileUtil; import org.junit.Test; public class TestSpider { private final Object mutex = new Object(); @Test public void test() throws Exception { //启动EWeb4J框架 String err = EWeb4JConfig.start(); if (err != null) throw new Exception(err); SpiderListener listener = new SpiderListenerAdaptor(){ public void onInfo(Thread thread, Task task, String info) { System.out.print("[SPIDERMAN] "+CommonUtil.getNowTime("HH:mm:ss")+" [INFO] ~ "); System.out.println(info); } public void onError(Thread thread, Task task, String err, Exception e) { e.printStackTrace(); } public void onParse(Thread thread, Task task, List<Map<String, Object>> models) { // System.out.print("[SPIDERMAN] "+CommonUtil.getNowTime("HH:mm:ss")+" [INFO] ~ "); // System.out.println(CommonUtil.toJson(models.get(0))); synchronized (mutex) { String content = CommonUtil.toJson(models.get(0)); try { File dir = new File("d:/jsons/"+task.site.getName()); if (!dir.exists()) dir.mkdirs(); File file = new File(dir+"/count_"+task.site.counter.getCount()+"_"+CommonUtil.getNowTime("yyyy_MM_dd_HH_mm_ss")+".json"); FileUtil.writeFile(file, content); System.out.print("[SPIDERMAN] "+CommonUtil.getNowTime("HH:mm:ss")+" [INFO] ~ "); System.out.println(file.getAbsolutePath() + " create finished..."); } catch (Exception e) { e.printStackTrace(); } } } }; //启动爬虫 Spiderman.me() .init(listener)//初始化 .startup()//启动 .keep("15s");//存活时间,过了存活时间后马上关闭 //------拿到引用后你还可以这样关闭------------------------- //spiderman.shutdown();//等待正在活动的线程都死掉再关闭爬虫 //spiderman.shutdownNow();//马上关闭爬虫 } }
大概解释下上述代码的意义
首先,因为依赖了EWeb4J框架的XML读写模块以及Properties模块,因此需要先启动EWeb4J:
然后,编写一个爬虫监听器,这里我们使用了内置的监听适配器选择性的实现了其中三个方法,第一个是打印INFO的,第二个是打印异常的,第三个比较重要://启动EWeb4J框架 String err = EWeb4JConfig.start(); if (err != null) throw new Exception(err);
这个方法在爬虫成功的抓取并解析了一个目标网页内容之后被回调,从代码可以看到一个List<Map>对象被传递了进来,这个对象就是我们想要的数据。因此我们将它格式化为JSON串后写入到D盘的文件里。public void onParse(Thread thread, Task task, List<Map<String, Object>> models) { synchronized (mutex) { String content = CommonUtil.toJson(models.get(0)); try { File dir = new File("d:/jsons/"+task.site.getName()); if (!dir.exists()) dir.mkdirs(); File file = new File(dir+"/count_"+task.site.counter.getCount()+"_"+CommonUtil.getNowTime("yyyy_MM_dd_HH_mm_ss")+".json"); FileUtil.writeFile(file, content); } catch (Exception e) { e.printStackTrace(); } } }
准备好了监听器之后,接下来需要启动爬虫:
不知道各位客观是否喜欢这种链式API,俺倒是挺喜欢的:)//启动爬虫 Spiderman.me() .init(listener)//初始化 .startup()//启动 .keep("15s");//存活时间,过了存活时间后马上关闭
PS:那个keep("15s") 是对OSC的一种敬重,虽然OSC不怎么怕“测试” :) @红薯
如果你不想等15s,可以这样关闭爬虫://------拿到引用后你还可以这样关闭------------------------- spiderman.shutdown();//等待正在活动的线程都死掉再关闭爬虫 spiderman.shutdownNow();//马上关闭爬虫
接下来,运行这个Test,观察文件夹以及控制台: - 补充
因为使用了reqDelay="1s"的配置,相当于一秒一次请求的频率,所以可以看到15秒抓取的页面【经过匹配后的】不是特别多 :)
- 好了,最后看看抓取出来的JSON进行格式化后的效果:
以上是“抛砖”之举 :) (红薯别介意哈,OSC一直都很优秀,绝没有“砖”的意思),下面就是“引玉”之时了!
突然尿急,这个“引玉”看来还得放到后面来做......【待续 :)】
来源:oschina
链接:https://my.oschina.net/u/146149/blog/100866