快速逆地址解析
# 需求
最近公司给了一个任务,需要对大批的经纬度进行逆地址解析(解析出对应的城市即可)。本来觉得也还好,无非就调个腾讯地图或者百度地图的接口,类似这样的:
但后面发现给的数据量太大了,初期的数据量就有 400 万左右,而且后续还有更多的数据,可能一天得处理个几千万的数据,这个数据量,接口的配额显然不太够(当然,也可以考虑搞多个企业账号,但这不是本文的重点)。
# 解决方案
后来得知腾讯地图有个接口 (opens new window)可以获取一个行政区的轮廓点,比如说我通过北京的轮廓点,就可以绘制出北京的一个轮廓:
好了,那如果我们能知道某一个点在哪个城市的轮廓里,不就可以知道这个点属于哪个城市了吗?
是的,没错,那有没有什么技术可以做到呢,答案是肯定的,MongoDB
可以存储一个多边形范围,再配合地理索引,我们就可以很快的检索出一个点属于哪个城市。
# 撸码
# 确定数据结构
咱们需要一个表来存储行政区的轮廓,这是对应的数据结构:
{
name: string; // 行政区名称
code: number; // 行政区编码
location: {
type: "Polygon";
coordinates: [[[number]]]; // 轮廓点
}
}
# 添加地理索引
一定要在插入数据前设置地理索引,因为它会帮我们排查出哪些轮廓点是有问题的,这个下面会说到。
db.geos.ensureIndex(
{
location: "2dsphere"
}
);
# 解析行政区轮廓
咱们先要获取一下整个中国的行政区编码,可以在这里下载 (opens new window)表格。
/**
* 解析行政区编码表
*/
public async parseCodeTable() {
let rows: any[] = await parseXlsx("./行政区划编码表_20220118.xlsx");
rows.shift();
rows = rows.filter((row) => {
const code = row[0] + "";
if (code[4] === "0" && code[5] === "0") { // 滤掉市级,只拿区级
return false;
}
return true;
});
let counter = 0;
for (const row of rows) {
const code = row[0];
const name = row[1];
await sleep(500); // 减少接口并发量
const chunks = await this.getDistrictInfo(code);
for (let polygons of chunks) {
await this.saveToMongoDB(name, code, polygons);
}
if (counter === rows.length) {
console.log("完成");
}
}
}
/**
* 获取行政区轮廓点
*/
public async getDistrictInfo(code) {
const res = await axios.get(`https://apis.map.qq.com/ws/district/v1/search?keyword=${code}&key=UBXBZ-BPTEJ-DEOFY-FAK72-P2Q7T-3GFRQ&get_polygon=2&max_offset=100`);
const data: {
result: [[{
id: string,
fullname: string
polygon: [number[]]
}]]
} = res.data;
const result = data.result[0][0]
let chunks = [];
for (const polygon of result.polygon) {
const polygons = [];
for (let i = 0, j = 1, count = 0; count < polygon.length; count += 2, i += 2, j += 2) {
polygons.push([polygon[i], polygon[j]]);
}
chunks.push(polygons);
}
return chunks;
}
# 存储行政区轮廓到 MongoDB
将行政区的轮廓点插入到 MongoDB 的时候,出现几个小问题。
一个是
Edge locations in degrees
,这个问题主要是因为轮廓点有部分点太接近了,所以就出问题了,写个正则把这部分的经纬度解析出来,然后排除掉即可。第二个呢,是
Duplicate vertices:
,这个不用说了,就是有重复的点,同样排除掉即可。最后就是记得数据得闭合,也就是把最开始的轮廓点添加到最后,不然轮廓无法闭合(下面闭环的地方我都有写注释)。
/**
* 保存数据到MongoDB
*/
private async saveToMongoDB(name, code, polygons, counter = 0) {
if (counter !== 0) {
console.log(`${name} ${code} 重试 ${counter}`);
}
// 形成轮廓闭合
polygons.push(polygons[0]);
const model = new GeoModel();
model.name = name;
model.code = code;
model.location = {
type: "Polygon",
coordinates: [polygons],
};
model.save((err) => {
if (err) {
let removeLocations = [];
let rege = /Edge locations in degrees:.\[.+\].and.\[.+\]/g;
let result = rege.exec(err.message);
if (result) {
let matchInfo = result[0];
if (matchInfo) {
matchInfo = matchInfo.replace("Edge locations in degrees:", "");
const chunks = matchInfo.split("and");
for (const chunk of chunks) {
const location: string[] = chunk.split("-");
const locationItem = JSON.parse(location[0].trim());
removeLocations.push(locationItem);
}
}
} else {
rege = /Duplicate vertices:.+.and.+/g;
result = rege.exec(err.message);
let matchInfo = result[0];
matchInfo = matchInfo.replace("Duplicate vertices:", "");
const chunks = matchInfo.split("and");
const index = Number(chunks[0].trim());
const removeItem = polygons[index];
removeLocations.push([removeItem[1], removeItem[0]]);
}
// 移除掉闭合的点,因为递归调用的时候会重新设置
polygons.pop();
const newPolygons = [];
// 移除掉有问题的点
a: for (const polygon of polygons) {
for (const removeLocation of removeLocations) {
const srcLng = polygon[0] + "";
const srcLat = polygon[1] + "";
const lng = removeLocation[1] + "";
const lat = removeLocation[0] + "";
if (
(srcLat.includes(lat) && srcLng.includes(lng)) ||
(Number(srcLng).toFixed(7).includes(lng) &&
Number(srcLat).toFixed(7).includes(lat))
) {
continue a;
}
}
newPolygons.push(polygon);
}
if (counter + 1 === 20) {
// 记录其他情况的异常
fs.appendFileSync(`./geo.log`, `${code}\r`);
console.log("出错了", code, name);
} else {
this.saveToMongoDB(name, code, newPolygons, counter + 1);
}
}
});
}
# 查询
编写查询语句,这里我写一个广州塔的经纬度:
db.geos.find({
location: {
$geoIntersects: {
$geometry: {
type: "Point",
coordinates: [113.32446, 23.10647],
},
},
},
});
这样我们就可以查到广州塔属于广州市海珠区:
# 总结
用这套方案跑下来基本数据没啥问题,偶尔会有一些正常的点没能查出来,这是因为行政区划分的时候我只选了陆地的行政区划分,而海域的点是查不出来的,如果要包含海域,自行更改参数 (opens new window)即可。