參考:官方文檔http://webmagic.io/docs/zh/posts/chx-cases/js-render-page.htmlhtml
Selenium
、HtmlUnit
或者PhantomJs
。可是這些工具都存在必定的效率問題,同時也不是那麼穩定。好處是編寫規則同靜態頁面同樣。
第一種方法,webmagic-selenium
就是這樣的一個嘗試,它實現了一個Downloader
,在下載頁面時,就是用瀏覽器內核進行渲染。selenium的配置比較複雜,並且跟平臺和版本有關,沒有太穩定的方案。感興趣的能夠參考博客:使用Selenium來抓取動態加載的頁面前端
以AngularJS中文社區http://angularjs.cn/爲例。java
判斷頁面是否爲js渲染的方式比較簡單,在瀏覽器中直接查看源碼(Windows下Ctrl+U,Mac下command+alt+u),若是找不到有效的信息,則基本能夠確定爲js渲染。git
找到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/A0y2。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.*";
以前,咱們分析的列表頁是一個url,詳情頁是一個url;程序分析時,分析的也是對應的url;但頁面是使用js渲染數據時,列表頁就對應了一個ajax請求(得到列表);每一個詳情頁也都對應一個ajax請求(詳情數據)
在列表頁,咱們須要找到有效的信息,來幫助咱們構建詳情頁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); } } }
有了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
實際上,動態頁面抓取,最大的區別在於:它提升了發現目標連接的難度。咱們對比一下兩種開發模式:
後端渲染的頁面
下載輔助頁面=>發現連接=>下載並分析目標HTML
前端渲染的頁面
發現輔助數據=>構造連接=>下載並分析目標AJAX對於不一樣的站點,這個輔助數據多是在頁面HTML中已經預先輸出,也多是經過AJAX去請求,甚至多是屢次數據請求的過程,可是這個模式基本是固定的。
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(); } }
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、暫時沒有想到
印象筆記連接: