通過一段時間的加班,終因而把項目熬上線了。本覺得能夠輕鬆一點,但每每事與願違,出現了各類各樣的問題。因爲作的是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>
因爲單個網站的數據可能不全,因此咱們須要逐個進行抓取。先抓取第一個,若是抓取不到,則抓取下一個網站,這樣依次進行下去。這樣的業務場景,咱們可使用變種的責任鏈設計模式來進行代碼的編寫。
@Data @Builder public class BankBranchVO { /** * 支行名稱 */ private String bankName; /** * 聯行號 */ private String bankCode; /** * 省份 */ private String provName; /** * 市 */ private String cityName; }
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標籤的屬性值,對於這步能夠有不少種方式實現,下面貼出個人實現方式,僅供參考。
@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; } }
@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; } }
@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。由於數據接口被隱藏在代碼裏面,因此想取到須要花費一些時間。而且請求地址和頁面地址一致,只是請求方式不同,容易被誤導。比較下來其餘的兩個就比較簡單,直接替換聯行號就能夠了,還有就是這個三個網站也沒啥反扒的機制,因此很輕鬆的就拿到了數據。
若是以爲對你有幫助,能夠多多評論,多多點贊哦,也能夠到個人主頁看看,說不定有你喜歡的文章,也能夠隨手點個關注哦,謝謝。
我是不同的科技宅,天天進步一點點,體驗不同的生活。咱們下期見!