Reid

vuePress-theme-reco Reid    2018 - 2024
Reid Reid

Choose mode

  • dark
  • auto
  • light
TimeLine
分类
  • 后端
  • AI
  • 英文翻译
  • 运维
标签
我的GitHub (opens new window)
author-avatar

Reid

16

文章

25

标签

TimeLine
分类
  • 后端
  • AI
  • 英文翻译
  • 运维
标签
我的GitHub (opens new window)
  • 快速逆地址解析

    • 需求
      • 解决方案
        • 撸码
          • 确定数据结构
          • 添加地理索引
          • 解析行政区轮廓
          • 存储行政区轮廓到 MongoDB
          • 查询
        • 总结

        快速逆地址解析

        vuePress-theme-reco Reid    2018 - 2024

        快速逆地址解析


        Reid 2022-05-22 MongoDB 腾讯地图 Node.js

        # 需求

        最近公司给了一个任务,需要对大批的经纬度进行逆地址解析(解析出对应的城市即可)。本来觉得也还好,无非就调个腾讯地图或者百度地图的接口,类似这样的:

        但后面发现给的数据量太大了,初期的数据量就有 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)即可。