WebMagic學習-抓取前端渲染的頁面

寫在前面:

     參考:官方文檔http://webmagic.io/docs/zh/posts/chx-cases/js-render-page.htmlhtml

 

兩種作法:

  1. 在抓取階段,在爬蟲中內置一個瀏覽器內核,執行js渲染頁面後,再抓取。這方面對應的工具備SeleniumHtmlUnit或者PhantomJs。可是這些工具都存在必定的效率問題,同時也不是那麼穩定。好處是編寫規則同靜態頁面同樣。
  2. 由於js渲染頁面的數據也是從後端拿到,並且基本上都是AJAX獲取,因此分析AJAX請求,找到對應數據的請求,也是比較可行的作法。並且相對於頁面樣式,這種接口變化可能性更小。缺點就是找到這個請求,並進行模擬,是一個相對困難的過程,也須要相對多的分析經驗。

 

兩種作法的適用場景:

  1. 一次性或者小規模的需求,用第一種方式省時省力。
  2. 長期性的、大規模的需求,仍是第二種會更靠譜一些。

 

 

內置瀏覽器法:

      第一種方法,webmagic-selenium 就是這樣的一個嘗試,它實現了一個Downloader,在下載頁面時,就是用瀏覽器內核進行渲染。selenium的配置比較複雜,並且跟平臺和版本有關,沒有太穩定的方案。感興趣的能夠參考博客:使用Selenium來抓取動態加載的頁面前端

 

拼接Ajax請求法:

      以AngularJS中文社區http://angularjs.cn/爲例。java

1 如何判斷前端渲染

    判斷頁面是否爲js渲染的方式比較簡單,在瀏覽器中直接查看源碼(Windows下Ctrl+U,Mac下command+alt+u),若是找不到有效的信息,則基本能夠確定爲js渲染。git

 

2 分析請求

     找到ajax數據的請求angularjs

                    以Chome爲例,咱們打開「開發者工具」(Windows下是F12,Mac下是command+alt+i),而後從新刷新頁面。github

                    首先能幫助咱們的是上方的分類篩選(All、Document等選項)。若是是正常的AJAX,會在XHR標籤下顯示,而JSONP請求會在Scripts標籤下顯示,這是兩個比較常見的數據類型。web

                    根據數據大小來判斷: 通常返回數據體積較大的更有多是返回數據的接口ajax

                    看一下響應體是什麼內容了: 咱們把URL http://angularjs.cn/api/article/latest?p=1&s=20複製到地址欄,從新請求一次(若是用Chrome推薦裝個jsonviewer,查看AJAX結果很方便)正則表達式

                    一樣的辦法,咱們進入到詳情頁,找到了具體內容的請求:http://angularjs.cn/api/article/A0y2json

 

3 分析ajax返回json數據內容

     經過分析,看到列表頁的ajax請求返回數據,是

{

    "ack": true,

    "error": null,

    "timestamp": 1476088599560,

    "data": [

        {

            "_id": "A2BE",     //經過後面的分析知這個就是每一個文章的id。

            "author": {

                "_id": "Uaeeml",

                "name": "破曉",

                "avatar": "http://www.gravatar.com/avatar/3844ec617c0e2c5b0b9b948ca3876ebc",

                "score": "49"

            },

            "date": 1476008186342,

            "display": 0,

            "status": 0,

            "refer": {

                "_id": null,

                "url": "http://www.icketang.com/"

            },

            "title": "張容銘2016年Angular.JS從入門到上手企業開發(完整版)",

            "cover": "",

            "content": "張容銘2016年Angular.JS從入門到上手企業開發(完整版)內容簡介:\n\n張容銘:愛創課堂由前百度工程師,《JavaScript設計模式》做者張容銘老師創立,公司秉承純乾貨,不忽悠的態度專一前端培訓,讓每一個學員都能真正的從入門到精通。\n\nAngularJS是爲了克服HTML在構建應用上的不足而設計的。HTML是一門很好的爲靜態文本展現…",

            "hots": 65,

            "visitors": 314,

            "updateTime": 1476014275212,

            "tagsList": [

                {

                    "_id": "T001",

                    "tag": "AngularJS",

                    "articles": 301,

                    "users": 48

                },

                {

                    "_id": "T004",

                    "tag": "JavaScript",

                    "articles": 113,

                    "users": 32

                },

                {

                    "_id": "T008",

                    "tag": "AngularJS 開發指南",

                    "articles": 66,

                    "users": 15

                },

                {

                    "_id": "T006",

                    "tag": "AngularJS 入門教程",

                    "articles": 36,

                    "users": 12

                },

                {

                    "_id": "T01l",

                    "tag": "開發經驗",

                    "articles": 16,

                    "users": 0

                }

            ],

            "comments": 0

        },    //列表數據,這裏是個數組

        ],

    "pagination": {

        "total": 2100,

        "pageSize": 15,

        "pageIndex": 1

    }

}

 

     一樣的方式找到了博客詳情也的ajax返回的json數據:

{

    "ack": true,

    "error": null,

    "timestamp": 1476090568218,

    "data": {

        "_id": "A2BE",     //文章id

        "author": {     //做者信息

            "_id": "Uaeeml",     //做者id

            "name": "破曉",

            "avatar": "http://www.gravatar.com/avatar/3844ec617c0e2c5b0b9b948ca3876ebc",

            "score": "49"

        },

        "date": 1476008186342,

        "display": 0,

        "status": 0,

        "refer": {

            "_id": null,

            "url": "http://www.icketang.com/"

        },

        "title": "張容銘2016年Angular.JS從入門到上手企業開發(完整版)",     //文章的標題

        "cover": "",

        "content": "張容銘2016年Angular.JS從入門到上手企業開發......",     //文章的內容

        "hots": 68,

        "visitors": 328,

        "updateTime": 1476014275212,

        "collection": 0,

        "tagsList": [

            {

                "_id": "T001",

                "tag": "AngularJS",

                "articles": 301,

                "users": 48

            },

            {

                "_id": "T004",

                "tag": "JavaScript",

                "articles": 113,

                "users": 32

            },

            {

                "_id": "T008",

                "tag": "AngularJS 開發指南",

                "articles": 66,

                "users": 15

            },

            {

                "_id": "T006",

                "tag": "AngularJS 入門教程",

                "articles": 36,

                "users": 12

            },

            {

                "_id": "T01l",

                "tag": "開發經驗",

                "articles": 16,

                "users": 0

            }

        ],

        "favorsList": [

            {

                "_id": "Uaeeml",

                "name": "破曉",

                "avatar": "http://www.gravatar.com/avatar/3844ec617c0e2c5b0b9b948ca3876ebc",

                "score": "49"

            }

        ],

        "opposesList": [],

        "markList": [],

        "comment": true,

        "commentsList": [],

        "comments": 0

    },

    "pagination": {

        "total": 0,

        "pageSize": 10,

        "pageIndex": 1

    }

}

 

     此時就能夠根據列表頁的ajax URL和詳情頁的ajax URL分別寫出對應的regex:       

private static final String ARITICALE_URL = "http://angularjs\\.cn/api/article/\\w+";    // \\w+匹配的是文章的_id

private static final String LIST_URL = "http://angularjs\\.cn/api/article/latest.*";

 

4 編寫程序

     以前,咱們分析的列表頁是一個url,詳情頁是一個url;程序分析時,分析的也是對應的url;但頁面是使用js渲染數據時,列表頁就對應了一個ajax請求(得到列表);每一個詳情頁也都對應一個ajax請求(詳情數據)

          4.一、數據列表

     在列表頁,咱們須要找到有效的信息,來幫助咱們構建詳情頁AJAX的URL。這裏咱們看到,這個_id應該就是咱們想要的帖子的id,而帖子的詳情請求,就是由一些固定URL加上這個id組成。因此在這一步,咱們本身手動構造URL,並加入到待抓取隊列中。這裏咱們使用JsonPath這種選擇語言來選擇數據(webmagic-extension包中提供了JsonPathSelector來支持它)

 if (page.getUrl().regex(LIST_URL).match()) {

     //這裏咱們使用JSONPATH這種選擇語言來選擇數據

     List<String> ids = new JsonPathSelector("$.data[*]._id").selectList(page.getRawText());

     if (CollectionUtils.isNotEmpty(ids)) {

         for (String id : ids) {

             page.addTargetRequest("http://angularjs.cn/api/article/"+id);

         }

     }

 }

 

          4.二、目標數據

     有了URL,實際上解析目標數據就很是簡單了,由於JSON數據是徹底結構化的,因此省去了咱們分析頁面,編寫XPath的過程。這裏咱們依然使用JsonPath來獲取標題和內容。

page.putField("title", new JsonPathSelector("$.data.title").select(page.getRawText()));

page.putField("content", new JsonPathSelector("$.data.content").select(page.getRawText()));

這個例子完整的代碼請看AngularJSProcessor.java

 

5 總結

     實際上,動態頁面抓取,最大的區別在於:它提升了發現目標連接的難度。咱們對比一下兩種開發模式:

  1. 後端渲染的頁面

    下載輔助頁面=>發現連接=>下載並分析目標HTML

  2. 前端渲染的頁面

    發現輔助數據=>構造連接=>下載並分析目標AJAX

     對於不一樣的站點,這個輔助數據多是在頁面HTML中已經預先輸出,也多是經過AJAX去請求,甚至多是屢次數據請求的過程,可是這個模式基本是固定的。

 

 

PS:

WebMagic 0.5.0以後會將Json的支持增長到鏈式API中,之後你可使用:

page.getJson().jsonPath("$.name").get();

這樣的方式來解析AJAX請求了。

同時也支持

page.getJson().removePadding("callback").jsonPath("$.name").get();

這樣的方式來解析JSONP請求。

 

     us.codecraft.webmagic.samples.AngularJSProcessor

public class AngularJSProcessor implements PageProcessor {



       private Site site = Site.me();



       private static final String ARITICALE_URL = "http://angularjs\\.cn/api/article/\\w+";// \\w+匹配的是文章的_id



       private static final String LIST_URL = "http://angularjs\\.cn/api/article/latest.*";



       @Override

       public void process(Page page) {

              if (page.getUrl().regex(LIST_URL).match()) {

                     List<String> ids = new JsonPathSelector("$.data[*]._id").selectList(page.getRawText());

                     if (CollectionUtils.isNotEmpty(ids)) {

                           for (String id : ids) {

                                  page.addTargetRequest("http://angularjs.cn/api/article/" + id);

                           }

                     }

              } else {

                     page.putField("title", new JsonPathSelector("$.data.title").select(page.getRawText()));

                     page.putField("content", new JsonPathSelector("$.data.content").select(page.getRawText()));

              }

       }



       @Override

       public Site getSite() {

              return site;

       }



       public static void main(String[] args) {



              Spider.create(new AngularJSProcessor()).addUrl("http://angularjs.cn/api/article/latest?p=1&s=20").run();

       }

}

 

***本身寫程序時出現的bugs:

java.lang.IllegalArgumentException: Invalid container object

.....

       at com.lacerta.ajax.angularJS.myAngularJSPageProcessor.process(myAngularJSPageProcessor.java:25)

.....

25     List<String> ids = new JsonPathSelector("$.data[*]._id").selectList(page.getRawText());

提示傳入的參數有問題,也就是傳入的參數應該是個json格式的。因而看到了:

Spider.create(new myAngularJSPageProcessor()).addUrl("http://angularjs.cn/?p=1").run();

addURL的這個url,返回是html頁面,因此page.getRawText()就不是json格式的。

因而修改addURL:

     Spider.create(new myAngularJSPageProcessor()).addUrl("http://angularjs.cn/api/article/latest?p=1&s=20").run();     //這個url是列表也對應的ajax請求

F11運行,輸出的數據是:

get page: http://angularjs.cn/api/article/latest?p=1&s=20

title: null

content:      null

================================================================

而後程序就中止了,說明在這個http://angularjs.cn/api/article/latest?p=1&s=20沒有發現新的TargetRequest,這是怎麼回事呢?

運行官方的us.codecraft.webmagic.samples.AngularJSProcessor就能夠正常採集到數據,但個人就不行,對比後發現有地方不一樣:

官方的:

    if (page.getUrl().regex(LIST_URL).match()) {

          .......

    } else {

          .......

    }

我本身寫的:

    if (page.getUrl().regex(DETAIL_REGEX).match()) { //【只是這個if不一樣】

        page.putField("title", new JsonPathSelector("$.data.title").select(page.getRawText()));

        if (page.getResultItems().get("title") == null) {

            page.setSkip(true);

        } else

            page.putField("content", new JsonPathSelector("$.data.content").select(page.getRawText()));

    } else {

        List<String> ids = new JsonPathSelector("$.data[*]._id").selectList(page.getRawText());

        if (!ids.isEmpty()) {

            for (String id : ids) {

                page.addTargetRequest("http://angularjs.cn/api/article/" + id);

            }

        }

    }

debug我本身的代碼發現,當page.getUrl()是列表的url時,page.getUrl().regex(DETAIL_REGEX).match()表達式也判斷爲true(正常狀況下,列表頁url須要page.addTargetRequest();詳情頁須要採集結構化數據。)

有多是我沒有徹底理解page.getUrl().regex(DETAIL_REGEX).match():

              boolean b1 = "http://angularjs.cn/api/article/latest?p=1&s=20".matches(DETAIL_REGEX);

              System.out.println("列表匹配列表:" + b1); //列表匹配列表:false



              System.out.println("===============");



              Html html = new Html("http://angularjs.cn/api/article/latest?p=1&s=20");

              Selectable xpath = html.xpath("//body/text()");

              System.out.println(xpath);               //http://angularjs.cn/api/article/latest?p=1&s=20

              xpath = xpath.regex(DETAIL_REGEX);

8             System.out.println(xpath);               //http://angularjs.cn/api/article/latest

              boolean b = xpath.match();               //true

              System.out.println(b);



              System.out.println("===============");



              Html html2 = new Html("http://angularjs.cn/api/article/A2zD");

              Selectable xpath2 = html2.xpath("//body/text()");

              System.out.println(xpath2);               //http://angularjs.cn/api/article/A2zD

              xpath2 = xpath2.regex(LIST_REGEX);

              System.out.println(xpath2);               //null

              boolean b2 = xpath2.match();              //false

              System.out.println(b2);

對上面的代碼進行分析:

一、第一段:使用列表url匹配詳情的正則,返回false

二、第二段:使用列表url匹配詳情的正則,返回true

三、第三段:使用詳情url匹配列表的正則,返回false

也就是第二段的結果是錯誤的。

(根據regex()方法的用途:正則過濾:能夠看到第8行,留下正則過濾後的結果,說明了regex()不是徹底match正則表達式,而是提取)

其實這的判斷有點混亂,那我改了一下方法:

     if (page.getRequest().getUrl().matches(DETAIL_REGEX)) {//這樣就行了,能夠正常取到數據

             .... 

     }else{

          .....

     }

 

 

***問題:採集完第一個列表頁的詳情數據後如何採集以後的列表頁詳情?

由於詳情頁返回的json中沒有列表的連接,用中方法頁找不到列表頁的連接,那麼,只會獲取到第一個列表頁的targetRequest,以後的第二頁、第三頁...就得到不到了。要解決這個問題啊~

     page.addTargetRequest("http://angularjs.cn/api/article/" + id);//怎麼添加第二頁列表的數據呢

方法:

1、在addUrl()時,就添加全部的列表頁對應的ajax的url

              Spider spider = Spider.create(new myAngularJSPageProcessor());

              for (int i = 1; i <= 106; i++) { //分析出來列表url的規則,先把列表url添加進去。

                     spider.addUrl("http://angularjs.cn/api/article/latest?p=" + i + "&s=20");

              }

              spider.setScheduler(new RedisScheduler("10.2.1.218")).run();

把列表url添加進去後,程序會先抓取列表頁面的url(也就是經過列表頁面,找到全部的詳情頁的url。若是在列表頁url分析完,讓程序結束,就能夠獲得全部detail的url,把這些url,分給不一樣的線程去採集詳情頁)

2、暫時沒有想到

 

 

印象筆記連接:

WebMagic學習-抓取前端渲染的頁面

相關文章
相關標籤/搜索