前言
经过一段时间的加班,终于是把项目熬上线了。本以为可以轻松一点,但往往事与愿违,出现了各种各样的问题。由于做的是POS前置交易系统,涉及到和商户进件以及交易相关的业务,需要向上游支付机构上送“联行号”,但是由于系统内的数据不全,经常出现找不到银行或者联行号有误等情况,导致无法进件。
为了解决这个问题,我找上游机构要了一份支行信息。好家伙,足足有14w条记录。在导入系统时,发现有一些异常的数据。有些是江西的银行,地区码竟然是北京的。经过一段时间排查,发现这样的数据还挺多的。这可愁死我了,本来偷个懒,等客服反馈的时候,出现一条修一条。
经过2分钟的思考,想到以后每天都要修数据,那不得烦死。于是长痛不如短痛,还不如一次性修了。然后我反手就打开了百度,经过一段时间的遨游。发现下面3个网站的支行信息比较全,准备用来跟系统内数据作对比,然后进行修正。
- http://www.jsons.cn/banknum/
- http://www.5cm.cn/bank/支行编号/
- https://www.appgate.cn/branch/bankBranchDetail/支行编号
输入联行号,然后选择查询方式,点击开始查询就可以。但是呢,结果页面一闪而过,然后被广告页面给覆盖了,这个时候就非常你的手速了。对于这样的,自然是难不倒我。从前端的角度分析,很明显展示结果的table标签被隐藏了,用来显示广告。于是反手就是打开控制台,查看源代码。
经过一顿搜寻,终于是找到了详情页的地址。
通过上面的操作,我们要想爬到数据,需要做两步操作。先输入联行号进行查询,然后进去详情页,才能取到想要的数据。所以第一步需要先获取查询的接口,于是我又打开了熟悉的控制台。
从上图可以发现这些请求都是在获取广告,并没有发现我们想要的接口,这个是啥情况,难道凭空变出来的嘛。并不是,主要是因为这个网站不是前后端分离的,所以这个时候我们需要从它的源码下手。
<html>
<body>
<formid="form1"class="form-horizontal"action="/banknum/"method="post">
<divclass="form-group">
<labelclass="col-sm-2control-label">关键词:</label>
<divclass="col-sm-10">
<inputclass="form-control"type="text"id="keyword"name="keyword"value="102453000160"placeholder="请输入查询关键词,例如:中关村支行"maxlength="50"/>
</div>
</div>
<divclass="form-group">
<labelclass="col-sm-2control-label">搜索类型:</label>
<divclass="col-sm-10">
<selectclass="form-control"id="txtflag"name="txtflag">
<optionvalue="0">支行关键词</option>
<optionvalue="1"selected="">银行联行号</option>
<optionvalue="2">支行网点地址</option>
</select>
</div>
</div>
<divclass="form-group">
<labelclass="col-sm-2control-label"></label>
<divclass="col-sm-10">
<buttontype="submit"class="btnbtn-success">开始查询</button>
<ahref="/banknum/"class="btnbtn-danger">清空输入框</a>
</div>
</div>
</form>
</body>
</html>
通过分析代码可以得出:
- 请求地址:http://www.jsons.cn/banknum/
- 请求方式:POST
- 请求参数: keyword: 联行号txtflag :1
我们可以用PostMan来验证一下接口是否有效,验证结果如下图所示:
剩下的两个网站相对比较简单,只需要更改相应的联行号,进行请求就可以获取到相应的数据,所以这里不过多赘述。
爬虫编写经过上面的分析了,已经取到了我们想要的接口,可谓是万事俱备,只欠代码了。爬取原理很简单,就是解析HTML元素,然后获取到相应的属性值保存下来就好了。由于使用Java进行开发,所以选用「Jsoup」来完成这个工作。
<!--HTML解析器-->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.13.1</version>
</dependency>
由于单个网站的数据可能不全,所以我们需要逐个进行抓取。先抓取第一个,如果抓取不到,则抓取下一个网站,这样依次进行下去。这样的业务场景,我们可以使用变种的责任链设计模式来进行代码的编写。
BankBranchVO支行信息
@Data
@Builder
publicclassBankBranchVO{
/**
*支行名称
*/
privateStringbankName;
/**
*联行号
*/
privateStringbankCode;
/**
*省份
*/
privateStringprovName;
/**
*市
*/
privateStringcityName;
}
publicabstractclassBankBranchSpider{
/**
*下一个爬虫
*/
privateBankBranchSpidernextSpider;
/**
*解析支行信息
*
*@parambankBranchCode支行联行号
*@return支行信息
*/
protectedabstractBankBranchVOparse(StringbankBranchCode);
/**
*设置下一个爬虫
*
*@paramnextSpider下一个爬虫
*/
publicvoidsetNextSpider(BankBranchSpidernextSpider){
this.nextSpider=nextSpider;
}
/**
*使用下一个爬虫
*根据爬取的结果进行判定是否使用下一个网站进行爬取
*
*@paramvo支行信息
*@returntrue或者false
*/
protectedabstractbooleanuseNextSpider(BankBranchVOvo);
/**
*查询支行信息
*
*@parambankBranchCode支行联行号
*@return支行信息
*/
publicBankBranchVOsearch(StringbankBranchCode){
BankBranchVOvo=parse(bankBranchCode);
while(useNextSpider(vo)&&this.nextSpider!=null){
vo=nextSpider.search(bankBranchCode);
}
if(vo==null){
thrownewSpiderException("无法获取支行信息:" bankBranchCode);
}
returnvo;
}
}
针对不同的网站解析方式不太一样,简言之就是获取HTML标签的属性值,对于这步可以有很多种方式实现,下面贴出我的实现方式,仅供参考。
JsonCnSpider
@Slf4j
publicclassJsonCnSpiderextendsBankBranchSpider{
/**
*爬取URL
*/
privatestaticfinalStringURL="http://www.jsons.cn/banknum/";
@Override
protectedBankBranchVOparse(StringbankBranchCode){
try{
log.info("json.cn-支行信息查询:{}",bankBranchCode);
//设置请求参数
Map<String,String>map=newHashMap<>(2);
map.put("keyword",bankBranchCode);
map.put("txtflag","1");
//查询支行信息
Documentdoc=Jsoup.connect(URL).data(map).post();
Elementstd=doc.selectFirst("tbody")
.selectFirst("tr")
.select("td");
if(td.size()<3){
returnnull;
}
//获取详情url
StringdetailUrl=td.get(3)
.selectFirst("a")
.attr("href");
if(StringUtil.isBlank(detailUrl)){
returnnull;
}
log.info("json.cn-支行详情-联行号:{},详情页:{}",bankBranchCode,detailUrl);
//获取详细信息
Elementsfooters=Jsoup.connect(detailUrl).get().select("blockquote").select("footer");
StringbankName=footers.get(1).childNode(2).toString();
StringbankCode=footers.get(2).childNode(2).toString();
StringprovName=footers.get(3).childNode(2).toString();
StringcityName=footers.get(4).childNode(2).toString();
returnBankBranchVO.builder()
.bankName(bankName)
.bankCode(bankCode)
.provName(provName)
.cityName(cityName)
.build();
}catch(IOExceptione){
log.error("json.cn-支行信息查询失败:{},失败原因:{}",bankBranchCode,e.getLocalizedMessage());
returnnull;
}
}
@Override
protectedbooleanuseNextSpider(BankBranchVOvo){
returnvo==null;
}
}
@Slf4j
publicclassFiveCmSpiderextendsBankBranchSpider{
/**
*爬取URL
*/
privatestaticfinalStringURL="http://www.5cm.cn/bank/%s/";
@Override
protectedBankBranchVOparse(StringbankBranchCode){
log.info("5cm.cn-查询支行信息:{}",bankBranchCode);
try{
Documentdoc=Jsoup.connect(String.format(URL,bankBranchCode)).get();
Elementstr=doc.select("tr");
Elementstd=tr.get(0).select("td");
if("".equals(td.get(1).text())){
returnnull;
}
StringbankName=doc.select("h1").get(0).text();
StringprovName=td.get(1).text();
StringcityName=td.get(3).text();
returnBankBranchVO.builder()
.bankName(bankName)
.bankCode(bankBranchCode)
.provName(provName)
.cityName(cityName)
.build();
}catch(IOExceptione){
log.error("5cm.cn-支行信息查询失败:{},失败原因:{}",bankBranchCode,e.getLocalizedMessage());
returnnull;
}
}
@Override
protectedbooleanuseNextSpider(BankBranchVOvo){
returnvo==null;
}
}
@Slf4j
publicclassAppGateSpiderextendsBankBranchSpider{
/**
*爬取URL
*/
privatestaticfinalStringURL="https://www.appgate.cn/branch/bankBranchDetail/";
@Override
protectedBankBranchVOparse(StringbankBranchCode){
try{
log.info("appgate.cn-查询支行信息:{}",bankBranchCode);
Documentdoc=Jsoup.connect(URL bankBranchCode).get();
Elementstr=doc.select("tr");
StringbankName=tr.get(1).select("td").get(1).text();
if(Boolean.FALSE.equals(StringUtils.hasText(bankName))){
returnnull;
}
StringprovName=tr.get(2).select("td").get(1).text();
StringcityName=tr.get(3).select("td").get(1).text();
returnBankBranchVO.builder()
.bankName(bankName)
.bankCode(bankBranchCode)
.provName(provName)
.cityName(cityName)
.build();
}catch(IOExceptione){
log.error("appgate.cn-支行信息查询失败:{},失败原因:{}",bankBranchCode,e.getLocalizedMessage());
returnnull;
}
}
@Override
protectedbooleanuseNextSpider(BankBranchVOvo){
returnvo==null;
}
}
@Component
publicclassBankBranchSpiderBean{
@Bean
publicBankBranchSpiderbankBranchSpider(){
JsonCnSpiderjsonCnSpider=newJsonCnSpider();
FiveCmSpiderfiveCmSpider=newFiveCmSpider();
AppGateSpiderappGateSpider=newAppGateSpider();
jsonCnSpider.setNextSpider(fiveCmSpider);
fiveCmSpider.setNextSpider(appGateSpider);
returnjsonCnSpider;
}
}
@RestController
@AllArgsConstructor
@RequestMapping("/bank/branch")
publicclassBankBranchController{
privatefinalBankBranchSpiderbankBranchSpider;
/**
*查询支行信息
*
*@parambankBranchCode支行联行号
*@return支行信息
*/
@GetMapping("/search/{bankBranchCode}")
publicBankBranchVOsearch(@PathVariable("bankBranchCode")StringbankBranchCode){
returnbankBranchSpider.search(bankBranchCode);
}
}
爬取成功
爬取失败的情况
代码地址
- https://gitee.com/huangxunhui/java-spider-data.git
这个爬虫的难点主要是在于Jsons.cn。因为数据接口被隐藏在代码里面,所以想取到需要花费一些时间。并且请求地址和页面地址一致,只是请求方式不一样,容易被误导。比较下来其他的两个就比较简单,直接替换联行号就可以了,还有就是这个三个网站也没啥反扒的机制,所以很轻松的就拿到了数据。
往期回顾- 「实战省市区三级联动数据爬取」
如果觉得对你有帮助,可以多多评论,多多点赞哦,也可以到我的主页看看,说不定有你喜欢的文章,也可以随手点个关注哦,谢谢。
我是不一样的科技宅,每天进步一点点,体验不一样的生活。我们下期见!
,