【最新更新支持頻道分頁、文章分頁】【拋磚引玉】抓取OSC的問答數據展示垂直爬蟲的能力

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

  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代碼裏抓取過時時間、須要過濾團購的一些描述信息【保留一些標籤,去掉一些標籤,去掉屬性等】、須要獲取好幾個地方的圖片、須要獲取團購的價格、購買人數等。關鍵的地方在於前面所述的這一切都將經過一個配置文件解決,無需編寫一句代碼。 java

所使用的爬蟲工具介紹: git

#Spiderman#,Java開源垂直類網絡爬蟲,使用XPath、正則、表達式引擎讓你輕鬆地抓取任何目標網站你「感興趣」的內容。基於多線程、微內核、插件式的架構。 github

Spiderman的正式版本尚未發佈,可是在github裏面有最新的代碼能夠取下來而且使用maven構建。 web

Spiderman依賴於EWeb4J的xml讀寫功能,所以還須要把最新的EWeb4J源碼從github拉下來構建。 正則表達式

下面介紹如何抓取OSC的問答數據: json


  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一直都很優秀,絕沒有「磚」的意思),下面就是「引玉」之時了! cookie


忽然尿急,這個「引玉」看來還得放到後面來作......【待續 :)】 網絡

相關文章
相關標籤/搜索