【最新更新支持频道分页、文章分页】【抛砖引玉】抓取OSC的问答数据展现垂直爬虫的能力

拈花ヽ惹草 提交于 2020-05-08 04:22:28

更新提示(2013-03-13):最新版本更新:

  1. 支持定向抓取某频道
    <!--
      | 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>


  2. 支持分页抓取单篇文章
    <!--
      | 目标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">

  3. 支持站点内多host
    <!--
      | 告诉爬虫仅抓取以下这些host的链接,多数是应对二级或多级域名的情况
    -->
    <validHosts>
        <validHost value="travel.163.com" />
        <validHost value="wwww.163.com" />
    </validHosts>

  4. 支持多个种子链接
    <!--
      | 配置多个种子链接
      | url:种子链接
    -->
    <seeds>
        <seed url="" />
    </seeds>

  5. HTML页面也可以强制使用XPath轴、XPath各种函数解析
    <!--
      | isForceUseXmlParser 当解析的页面是HTML时,除了XPath基本功能外很多XPath功能都不支持,例如XPath轴、其他高级函数等,将此参数设置为 1 即可让其支持,但是会带来某些不确定的问题【暂时未发现】
    -->
    <target name="travel" isForceUseXmlParser="1">

  6. 其他
    <!--
      | skipStatusCode:设置哪些状态码需要忽略,多个用逗号隔开,一旦设置了,那么爬虫将会忽略掉一些错误的statusCode,并且继续解析返回的内容
      | userAgent:设置爬虫标识
      | includeHttps:是否抓取https页
    -->
    <site skipStatusCode="500,501" userAgent="Spiderman[https://gitcafe.com/laiweiwei/Spiderman]" includeHttps="0">

更新提示(2013-01-10):最新版本的spiderman-plugin针对<field>的解析器<parser>配置做了修改,主要是需要增加一个<parsers>父节点包裹,因为支持多个parser的链式解析了,上一个parser的结果作为下一个parser的$this.因此配置文件所有的<field>外面都需要添加<fields>包裹着。例如:


<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的问答数据:


  1. 首先,我们来看看目标网页长什么样子的:)

    图中红色区域就是我们“感兴趣”的内容,从上到下依次为:标题,作者,问题内容,问题关联的标签,答案列表 一共五个属性。
  2. 然后,从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>

  3. 编写代码启动爬虫:
    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:
    //启动EWeb4J框架
    String err = EWeb4JConfig.start();
    if (err != null)
        throw new Exception(err);
    然后,编写一个爬虫监听器,这里我们使用了内置的监听适配器选择性的实现了其中三个方法,第一个是打印INFO的,第二个是打印异常的,第三个比较重要:
    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();
            }
        }
    }
    这个方法在爬虫成功的抓取并解析了一个目标网页内容之后被回调,从代码可以看到一个List<Map>对象被传递了进来,这个对象就是我们想要的数据。因此我们将它格式化为JSON串后写入到D盘的文件里。

    准备好了监听器之后,接下来需要启动爬虫:
    //启动爬虫
    Spiderman.me()
        .init(listener)//初始化
        .startup()//启动
        .keep("15s");//存活时间,过了存活时间后马上关闭
    不知道各位客观是否喜欢这种链式API,俺倒是挺喜欢的:)

    PS:那个keep("15s") 是对OSC的一种敬重,虽然OSC不怎么怕“测试” :) @红薯

    如果你不想等15s,可以这样关闭爬虫:
    //------拿到引用后你还可以这样关闭-------------------------
    spiderman.shutdown();//等待正在活动的线程都死掉再关闭爬虫
    spiderman.shutdownNow();//马上关闭爬虫

    接下来,运行这个Test,观察文件夹以及控制台:



  4. 补充
    因为使用了reqDelay="1s"的配置,相当于一秒一次请求的频率,所以可以看到15秒抓取的页面【经过匹配后的】不是特别多 :) 

  5. 好了,最后看看抓取出来的JSON进行格式化后的效果:

以上是“抛砖”之举 :) (红薯别介意哈,OSC一直都很优秀,绝没有“砖”的意思),下面就是“引玉”之时了!


突然尿急,这个“引玉”看来还得放到后面来做......【待续 :)】

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!