使用Jsoup技術獲取`阿里拍賣`中法院拍賣的所有拍賣品
阿新 • • 發佈:2018-12-13
前言
最近在學習過程中接到一個任務,要求爬取
阿里拍賣
中法院拍賣的所有拍賣品。用了點時間完成了任務,並分享出來作為經驗供學習、交流。若文中有任何不妥之處請提出。
最終效果
效果演示
爬取所有記錄
- 控制檯列印記錄
- 資料庫記錄
根據條件爬取
- 控制檯列印記錄
- 資料庫記錄
專案倉庫
專案前準備
技術選型
- 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);
剩下的內容和爬取所有內容相同在此不做贅述
總結
- 總體上來說功能實現了,但還有很多細節需要優化
- 如果有任何問題歡迎留言探討