轉自:http://blog.csdn.net/ccg_201216323/article/details/53576654
本文由我的微信公眾號(bruce常)原創首發,
並同步發表到csdn博客,歡迎轉載,2016年12月11日。
概述:
本周五,接到一個任務,要使用爬蟲技術來獲取某點評網站里面關於健身場館的數據,之前從未接觸過爬蟲技術,於是就從網上搜了一點學習資料,本篇文章就記錄爬蟲技術Jsoup技術,爬蟲技術聽名稱很牛叉,其實沒什么難點,慢慢的用心學習就會了。
Jsoup介紹:
Jsoup 是一個 Java 的開源HTML解析器,可直接解析某個URL地址、HTML文本內容,Jsoup官網jar包下載地址。
Jsoup主要有以下功能:
1. 從一個URL,文件或字符串中解析HTML
2. 使用DOM或CSS選擇器來查找、取出數據
3. 對HTML元素、屬性、文本進行操作
4. 清除不受信任的HTML (來防止XSS攻擊)
使用Jsoup爬蟲技術你需要的能力有:
- 我們是用安卓開發的,首先肯定要有一定的安卓開發能力,會寫簡單的頁面。
- Jsoup中用到了Javascript語言,沒有此語言能力在獲取數據的時候就比較吃力,這是此爬蟲技術的重中之重。
- 查閱文檔與解決問題的能力和技巧(有點廢話)
上面三條中對於一個安卓開發者來說,最難的就是熟練使用Javascript語言,小編就遇到了這個問題,小編還有一定的javascript基礎,系統的學習過此語言,但是在使用中還是很吃力的,問同學、問朋友、問同事,最后還是靠自己來獲取自己想要的數據。
爬蟲技術沒那么難,思路就是這么的簡單
- 得到自己想要爬取數據的url.
- 通過Jsoup的jar包中的方法將Html解析成Document,
- 使用Document中的一些列get、first、children等方法獲取自己想要的數據,如圖片地址、名稱、時間。
- 將得到的數據封裝成自己的實體類。
- 將實體中的數據在頁面加載出來。
實戰,獲取**點評網站中的場館數據:
先奉上效果圖,沒有圖不說話:
這就是今天要實現的效果,左邊圖片是場館的logo,右邊上方是場館的名稱,下邊是場館的地址信息,點擊進去可以根據超鏈接地址跳轉新的頁面,頁面的Url地址小編已經拿到,但可能是因為重定向的問題,webview沒有加載出來,有興趣的可以輸入鏈接地址來驗證。
首先:新建一個空的項目.
上面的效果,只要接觸過安卓開發的都能寫出來,所以不是本篇文章的重點,這里就不過多說明,大家可以使用ListView或者RecyclerView來實現,我這里用ListView。
小編這里是為了加入側邊欄所以使用的是DrawerLayout,但后來沒有用到,所以也就沒有側邊欄的效果,不過后期如有時間會加上去的,上一頁下一頁是為了簡單的模仿瀏覽器中的操作,此效果只能顯示前9頁數據,網頁鏈接中有50頁的數據,為什么沒有實現呢?
很簡單,因為50頁的鏈接地址不是一次性返回的,小編為了方便,只獲取了前9頁數據的url,畢竟是為了抓取數據顯示而已。
其次:主程序設計
- 通過網頁得到**點評健身場館的url地址是:http://www.dianping.com/search/category/2/45
- 抓取數據是一個耗時的操作,需要在一個線程中完成,這里使用 new Thread(runnable).start()方式,在runnable代碼中獲取場館的logo、名稱、地址如下:
Runnable runnable = new Runnable() {
@Override
public void run() {
Connection conn = Jsoup.connect(url); // 修改http包中的header,偽裝成瀏覽器進行抓取 conn.header("User-Agent", userAgent); Document doc = null; try { doc = conn.get(); } catch (IOException e) { e.printStackTrace(); } //獲取場館的數據 Element elementDiv = doc.getElementById("shop-all-list"); Elements elementsUl = elementDiv.getElementsByTag("ul"); Elements elements = elementsUl.first().getElementsByTag("li"); for (Element element : elements) { Elements elements1 = element.children(); String targetUrl = elements1.get(0).getElementsByTag("a").attr("href"); String img = elements1.get(0).getElementsByTag("img").first().attr("data-src"); if (img.contains(".jpg")) { int a = img.indexOf(".jpg"); img = img.substring(0, a + 4); } String radiumName = elements1.get(1).child(0).getElementsByTag("h4").text(); String address0 = elements1.get(1).child(2).getElementsByTag("a").get(1).text(); String address1 = elements1.get(1).child(2).getElementsByClass("addr").text(); RadiumBean radiumBean = new RadiumBean(); radiumBean.setImg(img); radiumBean.setName(radiumName); radiumBean.setAddress(address0 + " " + address1); list.add(radiumBean); } // 執行完畢后給handler發送一個空消息 Message message = new Message(); message.arg1 = Integer.parseInt(curPage); handler.sendMessage(message); } };
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 通過Jsoup.connect()方法,根據目標地址url來得到Connection對象,
- 將我們的app偽裝成瀏覽器,防止人家后台發現我們在爬取人家的數據,這需要修改修改http包中的header,來設置User-Agent,此值可以在谷歌瀏覽器中輸入“about:version”來查看,也可以訪問此地址查看。
- 通過Connection對象的get()方法來獲得整個頁面源代碼所在的Document
- 通過分析源代碼,使用Document的對象來得到我們想要的數據,上面程序中img待變場館logo的url,radiumName是小編得到的場館的名稱,address0和address1是小編得到的場館地址的信息,這里通過組合來使用。
- 構造我們ListView所用到的數據
- 通過Handle來更新頁面信息,curPage(當前頁)稍后說明。
- 在得到數據后頁面加載顯示
if (!list.isEmpty()) { MyAdapter adapter = new MyAdapter(list, MainActivity.this); info_list_view.setAdapter(adapter); }
- 1
- 2
- 3
- 4
4.點擊跳轉到場館的詳情頁,這里本想用Webview加載的,但是可能是網頁重定向的問題,webview也能加載出來,但一會就顯示無法連接網絡,所以場館詳情頁就顯示出了我們得到的場館詳情頁的url。
基本的抓取數據、加載數據流程就是這樣的,但是僅僅靠上面的數據還是不能完全實現我們的效果的。
完善頁面,實現上下頁翻頁功能。
- 頁面在爬取數據的時候顯示一個ProgressDialog來提示用戶。
ProgressDialog dialog = new ProgressDialog(this); dialog.setMessage("正在抓取數據..."); dialog.setCancelable(false); dialog.show();
- 1
- 2
- 3
- 4
數據加載完畢,關閉此dialog。
dialog.dismiss();
- 1
2.ProgresDialog加載前做是否有網絡的判斷,有網的時候才顯示ProgressDialog,無網絡的時候給出提示。
public boolean isNetworkAvailable(Activity activity) { Context context = activity.getApplicationContext(); ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); if (cm == null) return false; else { // 獲取所有NetworkInfo對象 NetworkInfo[] networkInfo = cm.getAllNetworkInfo(); if (networkInfo != null && networkInfo.length > 0) { for (int i = 0; i < networkInfo.length; i++) if (networkInfo[i].getState() == NetworkInfo.State.CONNECTED) return true; // 存在可用的網絡連接 } } return false; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
3.完善runnable,抓取當前頁碼、上一頁、下一頁的鏈接地址。
// 獲取頁數的鏈接
if (firstLoad) {
Elements elementsPages = doc.getElementsByClass("content-wrap"); Elements elementsPageA = elementsPages.first().getElementsByClass("shop-wrap").first().child(1).getElementsByTag("a"); for (int i = 0; i < elementsPageA.size() - 2; i++) { Element element = elementsPageA.get(i); Element element1 = element.getElementsByClass("cur").first(); Map<String, Object> map = new HashMap<>(); if (element1 != null) { curPage = element1.text(); map.put("page", "" + (i + 1)); map.put("url", url); mMapList.add(map); } else { map.put("page", "" + (i + 1)); map.put("url", element.attr("href")); mMapList.add(map); } } } firstLoad = false;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
因為在網頁中,第一次進入返回了前9頁和第50頁的數據,這里只取前9頁的數據,firstLoad代表第一次加載,mMapList用來存放頁碼和頁面跳轉時候的url,對js中的代碼不明白的朋友們,要好好學學js,這里小編就不介紹js了,至於我為什么知道取這些字段,那是小編盯着網頁源程序代碼看了半天看出來的。
- 這個時候就用到了之前runnable中的Message對象中的curPage
curPage代表當前頁碼,從1開始………………在handle接收到消息后顯示此頁碼信息。
tvCurrentPage.setText("" + msg.arg1);
- 1
- 模仿網頁的上一頁下一頁,我們需要處理TextView的點擊事件。
下一頁事件:
if (curPage.equals("" + (mMapList.size()))) { Toast.makeText(this, "末頁", Toast.LENGTH_SHORT).show(); } else { curPage = "" + (Integer.parseInt(curPage) + 1); url = "http://www.dianping.com" + mMapList.get(Integer.parseInt(curPage) - 1).get("url").toString(); switchOver(); tvCurrentPage.setText(curPage); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
上一頁事件:
if (curPage.equals("1")) { Toast.makeText(this, "首頁", Toast.LENGTH_SHORT).show(); } else { curPage = "" + (Integer.parseInt(curPage) - 1); if (curPage.equals(1)) { url = "http://www.dianping.com/search/category/2/45"; } else { url = "http://www.dianping.com" + mMapList.get(Integer.parseInt(curPage) - 1).get("url").toString(); } switchOver(); tvCurrentPage.setText(curPage); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
經過小編測試,在點擊下一頁的時候沒有bug,在點擊上一頁的時候,會出現doc為null,從而奔潰的bug,小編在努力解決中,但還沒解決掉。
- 附上完整的runnable代碼,畢竟這是此程序的關鍵部分。
Runnable runnable = new Runnable() {
@Override
public void run() {
Connection conn = Jsoup.connect(url); // 修改http包中的header,偽裝成瀏覽器進行抓取 conn.header("User-Agent", userAgent); Document doc = null; try { doc = conn.get(); } catch (IOException e) { e.printStackTrace(); } // 獲取頁數的鏈接 if (firstLoad) { Elements elementsPages = doc.getElementsByClass("content-wrap"); Elements elementsPageA = elementsPages.first().getElementsByClass("shop-wrap").first().child(1).getElementsByTag("a"); for (int i = 0; i < elementsPageA.size() - 2; i++) { Element element = elementsPageA.get(i); Element element1 = element.getElementsByClass("cur").first(); Map<String, Object> map = new HashMap<>(); if (element1 != null) { curPage = element1.text(); map.put("page", "" + (i + 1)); map.put("url", url); mMapList.add(map); } else { map.put("page", "" + (i + 1)); map.put("url", element.attr("href")); mMapList.add(map); } } } firstLoad = false; //獲取場館的數據 Element elementDiv = doc.getElementById("shop-all-list"); Elements elementsUl = elementDiv.getElementsByTag("ul"); Elements elements = elementsUl.first().getElementsByTag("li"); for (Element element : elements) { Elements elements1 = element.children(); String targetUrl = elements1.get(0).getElementsByTag("a").attr("href"); String img = elements1.get(0).getElementsByTag("img").first().attr("data-src"); if (img.contains(".jpg")) { int a = img.indexOf(".jpg"); img = img.substring(0, a + 4); } String radiumName = elements1.get(1).child(0).getElementsByTag("h4").text(); String address0 = elements1.get(1).child(2).getElementsByTag("a").get(1).text(); String address1 = elements1.get(1).child(2).getElementsByClass("addr").text(); // StringBuilder stringBuilder = new StringBuilder(); // // if (elements1.get(2).child(0).children().size()>0){ // String youhui = ""; // if (!"".equals(elements1.get(2).child(0).child(0).getElementsByClass("more").text())){ // youhui = elements1.get(2).child(0).getElementsByTag("a").get(1).attr("title"); // }else { // youhui = elements1.get(2).child(0).getElementsByTag("a").get(1).attr("title"); // // } // // stringBuilder.append(youhui+"+++"); // } RadiumBean radiumBean = new RadiumBean(); radiumBean.setTargetUrl("http://www.dianping.com" + targetUrl); radiumBean.setImg(img); radiumBean.setName(radiumName); radiumBean.setAddress(address0 + " " + address1); list.add(radiumBean); } // 執行完畢后給handler發送一個空消息 Message message = new Message(); message.arg1 = Integer.parseInt(curPage); handler.sendMessage(message); } };
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
有不明白的可以對照完整的runnable代碼來理解。
通過上面的步驟,我們已經完成了抓取、加載、上下頁切換的效果。但但請看下面。
通過小編的切身體驗,發現jsoup爬蟲獲取數據時候的幾個需要注意的地方。
1. 個人要會js,再強調一遍,不會js,上面我寫的js的程序應該會非常的迷糊,即便會的人,因為每個人寫的也不一樣,也是不好看懂的。
2. 我們在爬取數據的時候所用的class id 等字段一旦發生變化,那就得不到相應的標簽了,頁面就會發生奔潰,這一點也是致命的一點把。
3. 要想非常逼真的實現網頁中的效果,那你就要好好的看看網頁的源代碼了,網頁代碼有很大的靈活性,需要你仔細分析記錄規律。
測試程序已經上傳到了github,有需要的可以下載源程序。
下載地址:點我點我點我