目录
一、背景
最近,碰到了一个业务,是将数据库中所有的地址信息请求百度接口获取经纬度保存起来。有38万多个地址,想到的方案就是查出所有的地址字段加上主键字段,然后导出csv文件,读取这个文件,遍历请求百度api接口,获取经纬度信息,生成一个新的文件,作为一张表导入数据库,使用sql给地址刷一遍经纬度。
二、前期准备
1、生成需要转换的地址数据
(1)示例:查询sql需要筛选出经纬度字段为空的地址数据,之后的刷经纬度需要主键字段,所有也需要获取,然后导出一个文件。
select external_id,address from customer where longitude is null and latitude is null and address is not null
(2)导出这条sql查出的记录,像下面这样,一个csv文件。
三、百度接口介绍
1、百度地址转经纬度接口支持返回json格式和xml格式
(1)get方式请求下面地址将返回json格式,key为自己在百度上申请的开发者密钥。
http://api.map.baidu.com/geocoding/v3/?address={address}&output=json&key=SkSf
(2)成功的返回格式如下:
{ "status":"OK", "result":{ "location":{ "lng":123.473237, "lat":41.833995 }, "precise":1, "confidence":80, "level":"\u95e8\u5740" } }
(3)get方式请求下面地址将返回xml格式
http://api.map.baidu.com/geocoding/v3/?address={address}&output=json&key=SkSf
(4)成功的格式如下:
<?xml version="1.0" encoding="utf-8" ?> <GeocoderSearchResponse> <status>OK</status> <result> <location> <lat>40.148852</lat> <lng>117.125265</lng> </location> <precise>0</precise> <confidence>75</confidence> <level>购物</level> </result> </GeocoderSearchResponse>
(5)请求上面两个url,都可能返回失败内容,失败内容都是像下面这样,返回html页面。
<!DOCTYPE html> <!--STATUS OK--> <html> <head> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta http-equiv="content-type" content="text/html;charset=utf-8"> <meta content="always" name="referrer"> <script src="https://ss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/nocache/imgdata/seErrorRec.js"></script> <title>页é¢ä¸åå¨_ç¾åº¦æç´¢</title> <style data-for="result"> body {color: #333; background: #fff; padding: 0; margin: 0; position: relative; min-width: 700px; font-family: arial; font-size: 12px } p, form, ol, ul, li, dl, dt, dd, h3 {margin: 0; padding: 0; list-style: none } input {padding-top: 0; padding-bottom: 0; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box } img {border: none; } .logo {width: 117px; height: 38px; cursor: pointer } ...
注意:无论想返回json格式还是xml格式,当请求返回这种html类型数据,就获取不到经纬度,需要收集下来重新请求。
四、功能实现
1、先来实现百度接口返回为xml格式并解析获取经纬度,最后附完整代码
(1)为了记录读取的csv文件的原始地址数据和请求百度接口获取经纬度数据,原始文件中有主键(external_id)和地址(address),请求接口返回我们需要的经度(longitude)维度(latitude),这四个字段都需要最终保存到生成的结果文件中,所以我们声明ResultBean类如下,来记录数据(省略setget方法)。
static class ResultBean { private String external_id; //百度经纬度 private String longitude; private String latitude; //address private String address; public ResultBean(String external_id, String address, String longitude, String latitude) { this.external_id = external_id; this.longitude = longitude; this.latitude = latitude; this.address = address; } }
(2)读取导出的原始csv地址文件方法如下:通过CSVReader的write方法读取文件中的每条记录,保存到ResultBean,执行请求后面的经纬度方法。
public static void readCSV(List<ResultBean> datas, String sourcePath) { List<ResultBean> failData = new ArrayList<>(); try (CSVReader csvReader = new CSVReaderBuilder(new BufferedReader(new InputStreamReader(new FileInputStream(new File(sourcePath)), "utf-8"))).build()) { Iterator<String[]> iterator = csvReader.iterator(); //导出文件有标题行,去掉标题行,没有就不需要 iterator.next(); while (iterator.hasNext()) { String[] next = iterator.next(); String address = next[1].replaceAll("\\s*", ""); ResultBean resultBean = new ResultBean(next[0], address, null, null); //百度接口地址转换经纬度方法 getLngLat(datas, failData, resultBean); } //失败数据再次请求百度接口,最多循环一千次,防止失败数据出现程序永不停止 int i = 1000; while (failData.size() > 0 && i > 0) { List<ResultBean> tempFailData = new ArrayList<>(failData); failData.clear(); for (ResultBean resultBean : tempFailData) { getLngLat(datas, failData, resultBean); } i--; } } catch (Exception e) { e.printStackTrace(); } finally { System.out.println("fail record:" + failData.size()); } }
(3)我们使用restTemplate的getForObject方法请求百度接口,得到响应的结果,从上面可以看出返回的正常数据都是String类型的,肯定有"GeocoderSearchResponse",会基于这个字符串判断是否返回了xml数据,防止返回上面所说的html类型的数据,导致xml转换为bean对象获取经纬度报错。请求百度接口方法如下:
/** * 封装的获取经纬度方法 * @param datas * @param failData * @param resultBean */ private static void getLngLat(List<ResultBean> datas, List<ResultBean> failData, ResultBean resultBean) { Map<String, String> map = Maps.newHashMap(); map.put("address", resultBean.getAddress()); String response = restTemplate.getForObject(URL, String.class, map); if (response.contains("GeocoderSearchResponse")) { GeocoderSearchResponse g = JAXB.unmarshal(new StringReader(response), GeocoderSearchResponse.class); if (g.status.equals("OK")) { resultBean.setLatitude(g.result.location.lat); resultBean.setLongitude(g.result.location.lng); datas.add(resultBean); } else { failData.add(resultBean); } } else { failData.add(resultBean); } }
(4)对于偶尔返回html类型的错误数据,会收集相应的ResultBean到failData集合中,执行完csv文件中的所有数据后,遍历失败的集合再次请求百度接口,重复拿到失败数据集合请求百度,直到没有失败数据,或者已经重复了1000次,结束请求百度接口,将百度的所有转换成功的数据写入结果文件中。部分代码如下:
//失败数据再次请求百度接口,最多循环一千次,防止失败数据出现程序永不停止 int i = 1000; while (failData.size() > 0 && i > 0) { List<ResultBean> tempFailData = new ArrayList<>(failData); failData.clear(); for (ResultBean resultBean : tempFailData) { getLngLat(datas, failData, resultBean); } i--; }
(5)当请求百度api返回正确xml数据以后, 需要将xml转换为bean,然后获取经纬度,很多博客说使用dom4j进行转换,但是我发现公司pom里没有dom4j这个依赖,加这个依赖需要向上申请,所以就使用了JAXB(Java Architecture for XML Binding) ,他是一个业界的标准,是一项可以根据XML Schema产生Java类的技术。通过分析上面返回的xml,我们需要建立三个类,一个是GeocoderSearchResponse,Result,Location,他们都需要加上@XmlRootElement注解。类声明如下:
@XmlRootElement(name = "GeocoderSearchResponse") static class GeocoderSearchResponse { private String status; private Result result; public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } public Result getResult() { return result; } public void setResult(Result result) { this.result = result; } } @XmlRootElement static class Result { private Location location; public Location getLocation() { return location; } public void setLocation(Location location) { this.location = location; } } @XmlRootElement static class Location { private String lat; private String lng; public String getLat() { return lat; } public void setLat(String lat) { this.lat = lat; } public String getLng() { return lng; } public void setLng(String lng) { this.lng = lng; } }
注意:①类名的首字母会自动变为小写去对应xml中的字段,由于xml中GeocoderSearchResponse直接是大写的,所以需要在注解上加name属性,否则可能报错:unexpected element (uri:"", local:"GeocoderSearchResponse"). Expected elements are <{}geocoderSearchResponse ②每个类的变量只能有一个获取方式,需要声明变量私有,通过getset方法获取,否则会报错:Class has two properties of the same name "result"。
(6)当获取所有已经转换成功的经纬度信息后,将数据写入结果csv文件中,通过CsvWriter的write方法如下: