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学习-抓取前端渲染的页面

相关文章
相关标签/搜索