1. 程式人生 > >使用Jsoup技術獲取`阿里拍賣`中法院拍賣的所有拍賣品

使用Jsoup技術獲取`阿里拍賣`中法院拍賣的所有拍賣品

前言

最近在學習過程中接到一個任務,要求爬取阿里拍賣中法院拍賣的所有拍賣品。用了點時間完成了任務,並分享出來作為經驗供學習、交流。若文中有任何不妥之處請提出。

最終效果

效果演示

爬取所有記錄

  • 控制檯列印記錄
    爬取所有內容
  • 資料庫記錄
    在這裡插入圖片描述

根據條件爬取

  • 控制檯列印記錄
    根據條件爬取內容
  • 資料庫記錄
    在這裡插入圖片描述

專案倉庫

專案前準備

技術選型

  • HTML解析技術:Jsoup
    jsoup 是一款Java 的HTML解析器,可直接解析某個URL地址、HTML文字內容。它提供了一套非常省力的API,可通過DOM,CSS以及類似於jQuery的操作方法來取出和操作資料。

開發工具

  • IDEA 2018.2
  • MYSQL 5.7
  • Chrome

用到的jar包

  • easy-poi:3.0.3
  • mysql-connector-java:5.1.40
  • jsoup:1.8.3
  • fastjson:1.2.47

爬取所有資料

頁面分析

  • 首先我們通過http://sf.taobao.com/court_list.htm?spm=a213w.3065169.sfhead2014.3.3e1f1a333zcUBm,進入要爬取的入口頁,入口頁的結構如下圖所示:
    在這裡插入圖片描述
    頁面的機構是以省份為區分,在省下包含市,市包含法院,而法院中的連結市我們要獲取資訊的下一個入口。為了實現目的,爬蟲的處理邏輯如下:
1.進入入口頁,獲取所有省份的DOM
2.對第一個省份繼續進行處理,獲取市級列表
3.對第一個市進行處理,獲取法院列表和連結地址
4.進入第一個法院,獲得拍賣品列表
5.進入商品詳情頁獲取需要的資訊
6.重複5,知道所有拍賣品爬完
7.重複4-6,直至所有法院爬完
8.重複3-7,直至所有市爬完
9.重複2-8,直至所有省份爬完
10.結束

第一步:獲取所有省份列表

  • 首先用Chrome瀏覽器檢查爬蟲入口頁面元素,這樣能夠清晰直觀看出頁面的佈局,如下圖所示
    爬蟲入口頁面元素
    圖中,1是省份的div,包裹在一個class="provinces clearfix"div中,在1中還包含了2,3,4這些元素,這些都是我們接下來需要的內容。
    圖中,2是省份的名稱,包裹在一個class="province"div中,通過檢視多個省份的內容可以發現,這個模型中只會有省份名稱這個中文,所以取出這個資訊時可以通過正則表示式對中文進行匹配,從而取出值。
    圖中,3是省份下的市列表,包裹在一個class="province"div中。每個市在一個單獨的class="city"
    div中。圖中的4,市該市下的法院列表,法院列表中每個法院的連結才是我們需要的內容,通過訪問連結才能進入該法院的拍賣品列表,每個法院的連結規則如下:
    在這裡插入圖片描述
    獲取也很簡答,只用獲取a標籤中的href屬性即可,但訪問時為了比較明顯,要在前面加個http:
    點選連結,進入拍賣品列表,如下:
    在這裡插入圖片描述
    都這樣的結構也很簡單,第一想法時使用上述相同的方法直接一條一條爬取就行。但如果這樣你會發現,並爬不到資料,後來才發現,所有的資料並不是在頁面請求時就直接載入的,所有的資料會以json的格式放在一個script標籤中,如下圖所示:
    在這裡插入圖片描述
    這樣就更簡單了,直接通過Json解析,然後取出詳情連結就可以了。但需要注意的是,資料只是這一頁中的,事實上資料可能有很多頁。所以這時的想法就是看下每頁的請求路徑有什麼規律,通過對比就發現翻頁時的路徑類似於http://sf.taobao.com/court_item.htm?spm=a213w.7398554.pagination.1.5edd2fc2Vn735Z&user_id=2364124517&auction_start_seg=-1&page=2,而頁碼對應的就是page的值,所以我們只需要從頁面上獲取總計頁數,通過迴圈就可以得到每頁的資料。
    在這裡插入圖片描述
    方法很簡單,只需要獲取到class="page-total"的em標籤的text內容即可
    接下來就是詳情頁,詳情頁我們只抓三個部分的內容
    在這裡插入圖片描述
    其中標題和變賣價的規則如下:
    在這裡插入圖片描述
    在這裡插入圖片描述
    這裡根據之前的規則就可以取出值了。接下來通過程式碼來實現這一過程

程式碼實現

本文前面已經附帶了github的地址,原專案使用gradle構建,需要的朋友可以參考。所以本部分以關鍵部分程式碼講解為主。

進入主頁

Document document = Jsoup.connect(ConValues.SF_URL_ENTRANCE).timeout(5000).get();

這一步比較簡單,需要注意的是最好加上timeout()方法設定超時時間,不然可能會出現java.net.SocketTimeoutException:Read timed out異常。通過這一步就可以得到文件物件,來完成下列操作。

解析頁面

獲得所有省的文件模型

Elements select = document.select("div[class=provinces clearfix]");

迴圈,獲取各省的資訊

Elements elements = element.select("div[class=province"); Matcher valueByReg = DataUtil.getValueByReg(ConValues.ZHONG_WEN_REG, elements.toString());
if(valueByReg.find()){
    //省
   province = valueByReg.group(0);
 }

獲得市列表

 Elements citys = var1.select("dl[class=city]");

獲得總頁數和翻頁時的路徑規則

 Elements var4 = var3.select("a");
 for(Element var5:var4){
        if(var5.hasAttr("rel")){
               basePageUrl = "http:"+var5.attr("href").trim();
               break;
        }
    }
 String pageText = var3.select("span[class=page-skip]").select("em[class=page-total]").text();
 if(pageText.length()>0){
         totalPage = Integer.parseInt(pageText);
 }

進入拍賣品列表頁並取出值

//對每一頁進行處理
String pageUrl = basePageUrl.substring(0,basePageUrl.length()-1)+i;
Document document2 = Jsoup.connect(pageUrl).timeout(5000).get();
 //獲得該頁所有商品資訊,該頁的所有商品資訊是以json的格式存放在<script>標籤中的
 String oriData = document2.getElementById("sf-item-list-data").toString();
 //接下來對資料進行處理
 //1.找到“>”標籤的位置
 int start = oriData.indexOf(">");
 //2.找到"</"標籤的位置
 int end = oriData.indexOf("</");
 //截取出值
 String data = oriData.substring(start+1, end);

解析json資料獲得詳情路徑,並提取資料

JSONArray data1 = (JSONArray) JsonUtil.convertJsonStrToMap(data).get("data");
Iterator<Object> iterator = data1.iterator();
while(iterator.hasNext()){
    JSONObject next = (JSONObject)iterator.next();
    //需要記錄的url
    String detailUrl = "http:"+next.get("itemUrl");
    //獲取標題
    Document document3 = Jsoup.connect(detailUrl).timeout(5000).get();
    String detailTitle = document3.select("div[class=pm-main clearfix]").select("h1").text();
    //獲取變賣價
    String detailPrice = document3.select("span[class=J_Price]").first().text();
    System.out.println("抓取資訊:");
    System.out.println(province+"  "+city+"  "+countryName+"  "+detailTitle+"  "+detailPrice+"   "+detailUrl);   
}

持久化資料庫

定義POJO

public class AuctionItem {
    private String province;//省份
    private String city;//城市
    private String countryName;//法院
    private String detailTitle;//標題
    private String detailPrice;//變賣價
    private String detailUrl;//資源路徑

  //getter and setter
  .............
}

構造資料庫操作工廠類

public class DbOpFactory {
    // JDBC 驅動名及資料庫 URL
    static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";
    static final String DB_URL = "jdbc:mysql://localhost:3306/spider-data";
    private static DbOpFactory instance;
    public static DbOpFactory getInstance(){
        if(instance==null){
            return new DbOpFactory();
        }else{
            return instance;
        }
    }

    private DbOpFactory(){
        init();
    }
    // 資料庫的使用者名稱與密碼,需要根據自己的設定
    static final String USER = "root";
    static final String PASS = "xda265856";
    Connection conn = null;
    Statement stmt = null;
    ResultSet rs = null;

    public void init() {
        // 註冊 JDBC 驅動
        try {
            Class.forName("com.mysql.jdbc.Driver");
            // 開啟連結
            System.out.println("連線資料庫...");
            conn = DriverManager.getConnection(DB_URL,USER,PASS);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
      /**
     * 持久化拍賣資訊
     * @param auctionItem
     */
    public void insertAuction(AuctionItem auctionItem){
        try{
            // 執行查詢
           // System.out.println(" 例項化Statement物件...");
            stmt = conn.createStatement();
            String sql;
            sql = String.format("INSERT INTO TB_AUCTION_ITEM(AUCTION_PROVINCE,AUCTION_CITY,AUCTION_COUNTRY_NAME,AUCTION_DETAIL_TITLE,AUCTION_PRICE,AUCTION_DETAIL_URL) VALUES('%s','%s','%s','%s','%s','%s')"
                    ,auctionItem.getProvince(),auctionItem.getCity(),auctionItem.getCountryName(),auctionItem.getDetailTitle(),auctionItem.getDetailPrice(),auctionItem.getDetailUrl());
            stmt.executeUpdate(sql);
        }catch(SQLException se){
            // 處理 JDBC 錯誤
            se.printStackTrace();
        }catch(Exception e){
            // 處理 Class.forName 錯誤
            e.printStackTrace();
        }

    public void close(){
        // 完成後關閉
        try {
            stmt.close();
            conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }

    }
}

執行插入

DbOpFactory instance = DbOpFactory.getInstance();
instance.init();
........
AuctionItem auctionItem = new AuctionItem();
auctionItem.setProvince(province);
auctionItem.setCity(city);
auctionItem.setCountryName(countryName);
auctionItem.setDetailTitle(detailTitle);
auctionItem.setDetailPrice(detailPrice);
auctionItem.setDetailUrl(detailUrl);
instance.insertAuction(auctionItem);
System.out.println("存入資料庫成功");
........
instance.close();

至此爬蟲完成,上述程式碼有不詳盡的地方請到github上檢視原始碼。

根據搜尋條件爬取資料

根據條件的爬蟲時根據搜尋欄中輸入的關鍵字進行查詢,這裡將需要查詢的資訊放到EXCEL表格中,方便獲取。

分析

總體思路於爬取所有的內容時相同的。

  • 在搜尋框中輸入關鍵字
    在這裡插入圖片描述
  • 搜尋結果頁面的地址如下 在這裡插入圖片描述
    值為http://sf.taobao.com/list/0.htm?auction_start_seg=-1&q=%CE%E2%BD%AD%CB%C9%C1%EA%D5%F2%BD%AD%D8%C7%C2%B78%BA%C5A16-1&page=1,令人費解的是%CE%E2%BD%AD%CB%C9%C1%EA%D5%F2%BD%AD%D8%C7%C2%B78%BA%C5A16-1這串字串。猜測應該是搜尋的關鍵字但具體是什麼就不得而知。通過了解發現這是URL字元轉義(想了解更多,請自行百度),所以我們通過下面的方法可以實現對URL的字元轉義功能
 /**
     * url字元轉碼
*/
 public static String getURLEncode(String urlValue){
     String urlEncode= null;

     try {
         urlEncode = java.net.URLEncoder.encode(urlValue, "gb2312");
     } catch (UnsupportedEncodingException e) {
         e.printStackTrace();
     }
     return urlEncode;
 }

我們通過對路徑的拆分、拼接,即可得到完整的路徑

 //查詢地址前面部分
public static String SEARCH_ADDRESS_PREFIX = "http://sf.taobao.com/item_list.htm?q=";
//查詢地址後面部分
public static String SEARCH_ADDRESS_SUFFIX = "&spm=a213w.3064813.9001.1";

從EXCEL中獲得查詢關鍵字

這裡使用Easy-poi獲取excel中的內容

  • 定義pojo
public class SearchAttribute {
    @Excel(name = "序號")
    private String id;
    @Excel(name = "地址")
    private String address;
    @Excel(name = "所有人")
    private String owner;
    @Excel(name = "產證號1")
    private String certNum1;
    @Excel(name = "產證號2")
    private String certNum2;
    @Excel(name = "產證號3")
    private String certNum3;
  //getter and setter
  ...................
}
  • 獲取資訊
public class PoiUtils {


    public static <T> List<T> importExcel(String filePath, Integer titleRows, Integer headerRows, Class<T> pojoClass) throws  Exception {
        ImportParams params = new ImportParams();
        params.setTitleRows(titleRows);
        params.setHeadRows(headerRows);
        List<T> list = null;
        try {
            list = ExcelImportUtil.importExcel(new File(filePath), pojoClass, params);
        } catch (NoSuchElementException e) {
            throw new Exception("模板不能為空");
        } catch (Exception e) {
            e.printStackTrace();
            throw new Exception(e.getMessage());
        }
        return list;
    }
}
//得到Excel中的資訊
List<SearchAttribute> allAttributes = PoiUtils.importExcel("file/paimai.xlsx", 0, 1, SearchAttribute.class);

剩下的內容和爬取所有內容相同在此不做贅述

總結

  • 總體上來說功能實現了,但還有很多細節需要優化
  • 如果有任何問題歡迎留言探討