網路爬蟲技術Jsoup——爬到一切你想要的

201216323發表於2022-02-02

本文由我的微信公眾號(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爬蟲技術你需要的能力有:

  1. 我們是用安卓開發的,首先肯定要有一定的安卓開發能力,會寫簡單的頁面。
  2. Jsoup中用到了Javascript語言,沒有此語言能力在獲取資料的時候就比較吃力,這是此爬蟲技術的重中之重。
  3. 查閱文件與解決問題的能力和技巧(有點廢話)

上面三條中對於一個安卓開發者來說,最難的就是熟練使用Javascript語言,小編就遇到了這個問題,小編還有一定的javascript基礎,系統的學習過此語言,但是在使用中還是很吃力的,問同學、問朋友、問同事,最後還是靠自己來獲取自己想要的資料。

爬蟲技術沒那麼難,思路就是這麼的簡單

  1. 得到自己想要爬取資料的url.
  2. 通過Jsoup的jar包中的方法將Html解析成Document,
  3. 使用Document中的一些列get、first、children等方法獲取自己想要的資料,如圖片地址、名稱、時間。
  4. 將得到的資料封裝成自己的實體類。
  5. 將實體中的資料在頁面載入出來。

實戰,獲取**點評網站中的場館資料:

先奉上效果圖,沒有圖不說話:

image

這就是今天要實現的效果,左邊圖片是場館的logo,右邊上方是場館的名稱,下邊是場館的地址資訊,點選進去可以根據超連結地址跳轉新的頁面,頁面的Url地址小編已經拿到,但可能是因為重定向的問題,webview沒有載入出來,有興趣的可以輸入連結地址來驗證。

首先:新建一個空的專案.

上面的效果,只要接觸過安卓開發的都能寫出來,所以不是本篇文章的重點,這裡就不過多說明,大家可以使用ListView或者RecyclerView來實現,我這裡用ListView。

小編這裡是為了加入側邊欄所以使用的是DrawerLayout,但後來沒有用到,所以也就沒有側邊欄的效果,不過後期如有時間會加上去的,上一頁下一頁是為了簡單的模仿瀏覽器中的操作,此效果只能顯示前9頁資料,網頁連結中有50頁的資料,為什麼沒有實現呢?

很簡單,因為50頁的連結地址不是一次性返回的,小編為了方便,只獲取了前9頁資料的url,畢竟是為了抓取資料顯示而已。

其次:主程式設計

  1. 通過網頁得到**點評健身場館的url地址是:http://www.dianping.com/search/category/2/45
  2. 抓取資料是一個耗時的操作,需要在一個執行緒中完成,這裡使用 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. 通過Jsoup.connect()方法,根據目標地址url來得到Connection物件,
  2. 將我們的app偽裝成瀏覽器,防止人家後臺發現我們在爬取人家的資料,這需要修改修改http包中的header,來設定User-Agent,此值可以在谷歌瀏覽器中輸入“about:version”來檢視,也可以訪問此地址檢視
  3. 通過Connection物件的get()方法來獲得整個頁面原始碼所在的Document
  4. 通過分析原始碼,使用Document的物件來得到我們想要的資料,上面程式中img待變場館logo的url,radiumName是小編得到的場館的名稱,address0和address1是小編得到的場館地址的資訊,這裡通過組合來使用。
  5. 構造我們ListView所用到的資料
  6. 通過Handle來更新頁面資訊,curPage(當前頁)稍後說明。
  1. 在得到資料後頁面載入顯示
if (!list.isEmpty()) {
            MyAdapter adapter = new MyAdapter(list, MainActivity.this);
            info_list_view.setAdapter(adapter);
        }

4.點選跳轉到場館的詳情頁,這裡本想用Webview載入的,但是可能是網頁重定向的問題,webview也能載入出來,但一會就顯示無法連線網路,所以場館詳情頁就顯示出了我們得到的場館詳情頁的url。

基本的抓取資料、載入資料流程就是這樣的,但是僅僅靠上面的資料還是不能完全實現我們的效果的。

完善頁面,實現上下頁翻頁功能。

  1. 頁面在爬取資料的時候顯示一個ProgressDialog來提示使用者。
ProgressDialog dialog = new ProgressDialog(this);
            dialog.setMessage("正在抓取資料...");
            dialog.setCancelable(false);
            dialog.show();

資料載入完畢,關閉此dialog。

 dialog.dismiss();

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;
    }

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;

因為在網頁中,第一次進入返回了前9頁和第50頁的資料,這裡只取前9頁的資料,firstLoad代表第一次載入,mMapList用來存放頁碼和頁面跳轉時候的url,對js中的程式碼不明白的朋友們,要好好學學js,這裡小編就不介紹js了,至於我為什麼知道取這些欄位,那是小編盯著網頁源程式程式碼看了半天看出來的。

  1. 這個時候就用到了之前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);
                }

上一頁事件:

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);
                }

經過小編測試,在點選下一頁的時候沒有bug,在點選上一頁的時候,會出現doc為null,從而奔潰的bug,小編在努力解決中,但還沒解決掉。

  1. 附上完整的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);
        }
    };

有不明白的可以對照完整的runnable程式碼來理解。

通過上面的步驟,我們已經完成了抓取、載入、上下頁切換的效果。但但請看下面。

通過小編的切身體驗,發現jsoup爬蟲獲取資料時候的幾個需要注意的地方。
1. 個人要會js,再強調一遍,不會js,上面我寫的js的程式應該會非常的迷糊,即便會的人,因為每個人寫的也不一樣,也是不好看懂的。
2. 我們在爬取資料的時候所用的class id 等欄位一旦發生變化,那就得不到相應的標籤了,頁面就會發生奔潰,這一點也是致命的一點把。
3. 要想非常逼真的實現網頁中的效果,那你就要好好的看看網頁的原始碼了,網頁程式碼有很大的靈活性,需要你仔細分析記錄規律。

測試程式已經上傳到了github,有需要的可以下載源程式。

下載地址:點我點我點我

歡迎訪問201216323.tech來檢視我的CSDN部落格。

歡迎關注我的個人技術公眾號,快速檢視我的最新文章。

我的公眾號圖片

相關文章