WebMagic学习-抓取前端渲染的页面
2016-10-10 00:00
495 查看
写在前面:
参考:官方文档http://webmagic.io/docs/zh/posts/chx-cases/js-render-page.html两种做法:
在抓取阶段,在爬虫中内置一个浏览器内核,执行js渲染页面后,再抓取。这方面对应的工具有Selenium、
HtmlUnit或者
PhantomJs。但是这些工具都存在一定的效率问题,同时也不是那么稳定。好处是编写规则同静态页面一样。
因为js渲染页面的数据也是从后端拿到,而且基本上都是AJAX获取,所以分析AJAX请求,找到对应数据的请求,也是比较可行的做法。而且相对于页面样式,这种接口变化可能性更小。缺点就是找到这个请求,并进行模拟,是一个相对困难的过程,也需要相对多的分析经验。
两种做法的适用场景:
一次性或者小规模的需求,用第一种方式省时省力。长期性的、大规模的需求,还是第二种会更靠谱一些。
内置浏览器法:
第一种方法,webmagic-selenium就是这样的一个尝试,它实现了一个
Downloader,在下载页面时,就是用浏览器内核进行渲染。selenium的配置比较复杂,而且跟平台和版本有关,没有太稳定的方案。感兴趣的可以参考博客:使用Selenium来抓取动态加载的页面
拼接Ajax请求法:
以AngularJS中文社区http://angularjs.cn/为例。1 如何判断前端渲染
判断页面是否为js渲染的方式比较简单,在浏览器中直接查看源码(Windows下Ctrl+U,Mac下command+alt+u),如果找不到有效的信息,则基本可以肯定为js渲染。2 分析请求
找到ajax数据的请求以Chome为例,我们打开“开发者工具”(Windows下是F12,Mac下是command+alt+i),然后重新刷新页面。
首先能帮助我们的是上方的分类筛选(All、Document等选项)。如果是正常的AJAX,会在
XHR标签下显示,而JSONP请求会在
Scripts标签下显示,这是两个比较常见的数据类型。
根据数据大小来判断: 一般返回数据体积较大的更有可能是返回数据的接口
看一下响应体是什么内容了: 我们把URL http://angularjs.cn/api/article/latest?p=1&s=20复制到地址栏,重新请求一次(如果用Chrome推荐装个jsonviewer,查看AJAX结果很方便)
同样的办法,我们进入到详情页,找到了具体内容的请求:http://angularjs.cn/api/article/A0y2。
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.1、数据列表
在列表页,我们需要找到有效的信息,来帮助我们构建详情页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.2、目标数据
有了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 总结
实际上,动态页面抓取,最大的区别在于:它提高了发现目标链接的难度。我们对比一下两种开发模式:后端渲染的页面
下载辅助页面=>发现链接=>下载并分析目标HTML
前端渲染的页面
发现辅助数据=>构造链接=>下载并分析目标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);
对上面的代码进行分析:
1、第一段:使用列表url匹配详情的正则,返回false
2、第二段:使用列表url匹配详情的正则,返回true
3、第三段:使用详情url匹配列表的正则,返回false
也就是第二段的结果是错误的。
(根据regex()方法的用途:正则过滤:可以看到第8行,留下正则过滤后的结果,说明了regex()不是完全match正则表达式,而是提取)
其实这的判断有点混乱,那我改了一下方法:
if (page.getRequest().getUrl().matches(DETAIL_REGEX)) {//这样就好了,可以正常取到数据 .... }else{ ..... }
***问题:采集完第一个列表页的详情数据后如何采集之后的列表页详情?
因为详情页返回的json中没有列表的链接,用中方法页找不到列表页的链接,那么,只会获取到第一个列表页的targetRequest,之后的第二页、第三页...就获得不到了。要解决这个问题啊~page.addTargetRequest("http://angularjs.cn/api/article/" + id);//怎么添加第二页列表的数据呢
方法:
一、在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,分给不同的线程去采集详情页)
二、暂时没有想到
印象笔记链接:
WebMagic学习-抓取前端渲染的页面
相关文章推荐
- WebMagic抓取前端Ajax渲染的页面
- 抓取前端渲染的页面的技术webmagic
- WebMagic抓取前端Ajax渲染的页面
- 了解html页面的渲染过程以备学习前端的性能优化
- Spring Boot学习(四)之web开发渲染页面 -- Velocity
- 学习用java基于webMagic+selenium+phantomjs实现爬虫Demo爬取淘宝搜索页面
- Web前端学习第十二天·fighting_HTML页面设计技巧总结(一)
- 学习用java基于webMagic+selenium+phantomjs实现爬虫Demo爬取淘宝搜索页面
- WebMagic抓取前端Ajax渲染的页面
- Web前端面试指导(四十五):页面渲染原理是什么?
- Web前端学习第十三天·fighting_HTML页面设计技巧总结(二)
- 了解html页面的渲染过程以备学习前端的性能优化
- 了解html页面的渲染过程以备学习前端的性能优化(续)
- 抓取前端渲染的页面
- webpack前端构建工具学习总结(四)之自动化生成项目中的html页面
- 【web前端学习】-- JS页面跳转
- 了解html页面的渲染过程以备学习前端的性能优化
- EasyDSS高性能流媒体服务器前端重构(三): webpack + vue + AdminLTE 多页面引入 element-ui
- web前端学习(8)
- java学习:Web前端开发学习路线及建议