「原創」如何快速獲取銀行、聯行號的數據?

前言

  通過一段時間的加班,終因而把項目熬上線了。本覺得能夠輕鬆一點,但每每事與願違,出現了各類各樣的問題。因爲作的是POS前置交易系統,涉及到和商戶進件以及交易相關的業務,須要向上遊支付機構上送「聯行號」,可是因爲系統內的數據不全,常常出現找不到銀行或者聯行號有誤等狀況,致使沒法進件。html

  爲了解決這個問題,我找上游機構要了一份支行信息。好傢伙,足足有14w條記錄。在導入系統時,發現有一些異常的數據。有些是江西的銀行,地區碼居然是北京的。通過一段時間排查,發現這樣的數據還挺多的。這可愁死我了,原本偷個懶,等客服反饋的時候,出現一條修一條。前端

  通過2分鐘的思考,想到之後天天都要修數據,那不得煩死。因而長痛不如短痛,還不如一次性修了。而後我反手就打開了百度,通過一段時間的遨遊。發現下面3個網站的支行信息比較全,準備用來跟系統內數據做對比,而後進行修正。java

分析網站

  輸入聯行號,而後選擇查詢方式,點擊開始查詢就能夠。可是呢,結果頁面一閃而過,而後被廣告頁面給覆蓋了,這個時候就很是你的手速了。對於這樣的,天然是難不倒我。從前端的角度分析,很明顯展現結果的table標籤被隱藏了,用來顯示廣告。因而反手就是打開控制檯,查看源代碼。git

通過一頓搜尋,終因而找到了詳情頁的地址。github

  經過上面的操做,咱們要想爬到數據,須要作兩步操做。先輸入聯行號進行查詢,而後進去詳情頁,才能取到想要的數據。因此第一步須要先獲取查詢的接口,因而我又打開了熟悉的控制檯。json

  從上圖能夠發現這些請求都是在獲取廣告,並無發現咱們想要的接口,這個是啥狀況,難道憑空變出來的嘛。並非,主要是由於這個網站不是先後端分離的,因此這個時候咱們須要從它的源碼下手。segmentfault

<html>
 <body>
  <form id="form1" class="form-horizontal" action="/banknum/" method="post"> 
   <div class="form-group"> 
    <label class="col-sm-2 control-label"> 關鍵詞:</label> 
    <div class="col-sm-10"> 
     <input class="form-control" type="text" id="keyword" name="keyword" value="102453000160"  placeholder="請輸入查詢關鍵詞,例如:中關村支行" maxlength="50" /> 
    </div> 
   </div> 
   <div class="form-group"> 
    <label class="col-sm-2 control-label"> 搜索類型:</label> 
    <div class="col-sm-10"> 
     <select class="form-control" id="txtflag" name="txtflag"> 
             <option value="0">支行關鍵詞</option>
          <option value="1" selected="">銀行聯行號</option>
          <option value="2">支行網點地址</option> 
      </select> 
    </div> 
   </div> 
   <div class="form-group"> 
    <label class="col-sm-2 control-label"> </label> 
    <div class="col-sm-10"> 
     <button type="submit" class="btn btn-success"> 開始查詢</button> 
     <a href="/banknum/" class="btn btn-danger">清空輸入框</a> 
    </div> 
   </div> 
  </form>
 </body>
</html>

經過分析代碼能夠得出:後端

咱們能夠用PostMan來驗證一下接口是否有效,驗證結果以下圖所示:app

  剩下的兩個網站相對比較簡單,只須要更改相應的聯行號,進行請求就能夠獲取到相應的數據,因此這裏不過多贅述。

爬蟲編寫

  通過上面的分析了,已經取到了咱們想要的接口,可謂是萬事俱備,只欠代碼了。爬取原理很簡單,就是解析HTML元素,而後獲取到相應的屬性值保存下來就行了。因爲使用Java進行開發,因此選用Jsoup來完成這個工做。

<!-- HTML解析器 -->
<dependency>
  <groupId>org.jsoup</groupId>
  <artifactId>jsoup</artifactId>
  <version>1.13.1</version>
</dependency>

  因爲單個網站的數據可能不全,因此咱們須要逐個進行抓取。先抓取第一個,若是抓取不到,則抓取下一個網站,這樣依次進行下去。這樣的業務場景,咱們可使用變種的責任鏈設計模式來進行代碼的編寫。

BankBranchVO支行信息

@Data
@Builder
public class BankBranchVO {

    /**
     * 支行名稱
     */
    private String bankName;

    /**
     * 聯行號
     */
    private String bankCode;

    /**
     * 省份
     */
    private String provName;

    /**
     * 市
     */
    private String cityName;

}

BankBranchSpider抽象類

public abstract class BankBranchSpider {

    /**
     * 下一個爬蟲
     */
    private BankBranchSpider nextSpider;

    /**
     * 解析支行信息
     *
     * @param bankBranchCode 支行聯行號
     * @return 支行信息
     */
    protected abstract BankBranchVO parse(String bankBranchCode);

    /**
     * 設置下一個爬蟲
     *
     * @param nextSpider 下一個爬蟲
     */
    public void setNextSpider(BankBranchSpider nextSpider) {
        this.nextSpider = nextSpider;
    }

    /**
     * 使用下一個爬蟲
     * 根據爬取的結果進行斷定是否使用下一個網站進行爬取
     *
     * @param vo 支行信息
     * @return true 或者 false
     */
    protected abstract boolean useNextSpider(BankBranchVO vo);

    /**
     * 查詢支行信息
     *
     * @param bankBranchCode 支行聯行號
     * @return 支行信息
     */
    public BankBranchVO search(String bankBranchCode) {
        BankBranchVO vo = parse(bankBranchCode);
        while (useNextSpider(vo) && this.nextSpider != null) {
            vo = nextSpider.search(bankBranchCode);
        }
        if (vo == null) {
            throw new SpiderException("沒法獲取支行信息:" + bankBranchCode);
        }
        return vo;
    }

}

  針對不一樣的網站解析方式不太同樣,簡言之就是獲取HTML標籤的屬性值,對於這步能夠有不少種方式實現,下面貼出個人實現方式,僅供參考。

JsonCnSpider

@Slf4j
public class JsonCnSpider extends BankBranchSpider {

    /**
     * 爬取URL
     */
    private static final String URL = "http://www.jsons.cn/banknum/";


    @Override
    protected BankBranchVO parse(String bankBranchCode) {

        try {
            log.info("json.cn-支行信息查詢:{}", bankBranchCode);

            // 設置請求參數
            Map<String, String> map = new HashMap<>(2);
            map.put("keyword", bankBranchCode);
            map.put("txtflag", "1");

            // 查詢支行信息
            Document doc = Jsoup.connect(URL).data(map).post();


            Elements td = doc.selectFirst("tbody")
                    .selectFirst("tr")
                    .select("td");

            if (td.size() < 3) {
                return null;
            }

            // 獲取詳情url
            String detailUrl = td.get(3)
                    .selectFirst("a")
                    .attr("href");

            if (StringUtil.isBlank(detailUrl)) {
                return null;
            }

            log.info("json.cn-支行詳情-聯行號:{}, 詳情頁:{}", bankBranchCode, detailUrl);

            // 獲取詳細信息
            Elements footers = Jsoup.connect(detailUrl).get().select("blockquote").select("footer");

            String bankName = footers.get(1).childNode(2).toString();
            String bankCode = footers.get(2).childNode(2).toString();
            String provName = footers.get(3).childNode(2).toString();
            String cityName = footers.get(4).childNode(2).toString();

            return BankBranchVO.builder()
                    .bankName(bankName)
                    .bankCode(bankCode)
                    .provName(provName)
                    .cityName(cityName)
                    .build();

        } catch (IOException e) {
            log.error("json.cn-支行信息查詢失敗:{}, 失敗緣由:{}", bankBranchCode, e.getLocalizedMessage());
            return null;
        }
    }

    @Override
    protected boolean useNextSpider(BankBranchVO vo) {
        return vo == null;
    }

}

FiveCmSpider

@Slf4j
public class FiveCmSpider extends BankBranchSpider {

    /**
     * 爬取URL
     */
    private static final String URL = "http://www.5cm.cn/bank/%s/";

    @Override
    protected BankBranchVO parse(String bankBranchCode) {
        log.info("5cm.cn-查詢支行信息:{}", bankBranchCode);

        try {
            Document doc = Jsoup.connect(String.format(URL, bankBranchCode)).get();
            Elements tr = doc.select("tr");

            Elements td = tr.get(0).select("td");
            if ("".equals(td.get(1).text())) {
                return null;
            }

            String bankName = doc.select("h1").get(0).text();
            String provName = td.get(1).text();
            String cityName = td.get(3).text();

            return BankBranchVO.builder()
                    .bankName(bankName)
                    .bankCode(bankBranchCode)
                    .provName(provName)
                    .cityName(cityName)
                    .build();

        } catch (IOException e) {
            log.error("5cm.cn-支行信息查詢失敗:{}, 失敗緣由:{}", bankBranchCode, e.getLocalizedMessage());
            return null;
        }
    }

    @Override
    protected boolean useNextSpider(BankBranchVO vo) {
        return vo == null;
    }

}

AppGateSpider

@Slf4j
public class AppGateSpider extends BankBranchSpider {

    /**
     * 爬取URL
     */
    private static final String URL = "https://www.appgate.cn/branch/bankBranchDetail/";

    @Override
    protected BankBranchVO parse(String bankBranchCode) {
        try {
            log.info("appgate.cn-查詢支行信息:{}", bankBranchCode);

            Document doc = Jsoup.connect(URL + bankBranchCode).get();
            Elements tr = doc.select("tr");

            String bankName = tr.get(1).select("td").get(1).text();

            if(Boolean.FALSE.equals(StringUtils.hasText(bankName))){
                return null;
            }

            String provName = tr.get(2).select("td").get(1).text();
            String cityName = tr.get(3).select("td").get(1).text();

            return BankBranchVO.builder()
                    .bankName(bankName)
                    .bankCode(bankBranchCode)
                    .provName(provName)
                    .cityName(cityName)
                    .build();

        } catch (IOException e) {
            log.error("appgate.cn-支行信息查詢失敗:{}, 失敗緣由:{}", bankBranchCode, e.getLocalizedMessage());
            return null;
        }
    }

    @Override
    protected boolean useNextSpider(BankBranchVO vo) {


        return vo == null;
    }
}

初始化爬蟲

@Component
public class BankBranchSpiderBean {

    @Bean
    public BankBranchSpider bankBranchSpider() {
        JsonCnSpider jsonCnSpider = new JsonCnSpider();
        FiveCmSpider fiveCmSpider = new FiveCmSpider();
        AppGateSpider appGateSpider = new AppGateSpider();
        jsonCnSpider.setNextSpider(fiveCmSpider);
        fiveCmSpider.setNextSpider(appGateSpider);
        return jsonCnSpider;
    }
}

爬取接口

@RestController
@AllArgsConstructor
@RequestMapping("/bank/branch")
public class BankBranchController {

    private final BankBranchSpider bankBranchSpider;

    /**
     * 查詢支行信息
     *
     * @param bankBranchCode 支行聯行號
     * @return 支行信息
     */
    @GetMapping("/search/{bankBranchCode}")
    public BankBranchVO search(@PathVariable("bankBranchCode") String bankBranchCode) {
        return bankBranchSpider.search(bankBranchCode);
    }

}

演示

爬取成功

爬取失敗的狀況

代碼地址

總結

   這個爬蟲的難點主要是在於Jsons.cn。由於數據接口被隱藏在代碼裏面,因此想取到須要花費一些時間。而且請求地址和頁面地址一致,只是請求方式不同,容易被誤導。比較下來其餘的兩個就比較簡單,直接替換聯行號就能夠了,還有就是這個三個網站也沒啥反扒的機制,因此很輕鬆的就拿到了數據。

往期回顧

結尾

  若是以爲對你有幫助,能夠多多評論,多多點贊哦,也能夠到個人主頁看看,說不定有你喜歡的文章,也能夠隨手點個關注哦,謝謝。

  我是不同的科技宅,天天進步一點點,體驗不同的生活。咱們下期見!

相關文章
相關標籤/搜索