Python爬虫实战:地图 POI + 行政区反查(合规接口) - 商圈热力数据准备等!

㊗️本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~

㊙️本期爬虫难度指数:⭐⭐⭐

🉐福利: 一次订阅后,专栏内的所有文章可永久免费看,持续更新中,保底1000+(篇)硬核实战内容。

全文目录:

      • [🌟 开篇语](#🌟 开篇语)
      • [1️⃣ 摘要(Abstract)](#1️⃣ 摘要(Abstract))
      • [2️⃣ 背景与需求(Why)](#2️⃣ 背景与需求(Why))
      • [3️⃣ 合规与注意事项(必写)](#3️⃣ 合规与注意事项(必写))
      • [4️⃣ 技术选型与整体流程(What/How)](#4️⃣ 技术选型与整体流程(What/How))
      • [5️⃣ 环境准备与依赖安装(可复现)](#5️⃣ 环境准备与依赖安装(可复现))
      • [6️⃣ 核心实现:网格切片算法(The Gridder)](#6️⃣ 核心实现:网格切片算法(The Gridder))
      • [7️⃣ 核心实现:请求与解析层(Fetcher & Parser)](#7️⃣ 核心实现:请求与解析层(Fetcher & Parser))
      • [8️⃣ 数据存储与导出(Storage)](#8️⃣ 数据存储与导出(Storage))
      • [9️⃣ 运行方式与结果展示(必写)](#9️⃣ 运行方式与结果展示(必写))
      • [🔟 常见问题与排错(FAQ)](#🔟 常见问题与排错(FAQ))
      • [1️⃣1️⃣ 进阶优化(可视化准备)](#1️⃣1️⃣ 进阶优化(可视化准备))
      • [1️⃣2️⃣ 总结与延伸阅读](#1️⃣2️⃣ 总结与延伸阅读)
      • [🌟 文末](#🌟 文末)
        • [✅ 专栏持续更新中|建议收藏 + 订阅](#✅ 专栏持续更新中|建议收藏 + 订阅)
        • [✅ 互动征集](#✅ 互动征集)
        • [✅ 免责声明](#✅ 免责声明)

🌟 开篇语

哈喽,各位小伙伴们你们好呀~我是【喵手】。

运营社区: C站 / 掘金 / 腾讯云 / 阿里云 / 华为云 / 51CTO

欢迎大家常来逛逛,一起学习,一起进步~🌟

我长期专注 Python 爬虫工程化实战 ,主理专栏 《Python爬虫实战》:从采集策略反爬对抗 ,从数据清洗分布式调度 ,持续输出可复用的方法论与可落地案例。内容主打一个"能跑、能用、能扩展 ",让数据价值真正做到------抓得到、洗得净、用得上

📌 专栏食用指南(建议收藏)

  • ✅ 入门基础:环境搭建 / 请求与解析 / 数据落库
  • ✅ 进阶提升:登录鉴权 / 动态渲染 / 反爬对抗
  • ✅ 工程实战:异步并发 / 分布式调度 / 监控与容错
  • ✅ 项目落地:数据治理 / 可视化分析 / 场景化应用

📣 专栏推广时间 :如果你想系统学爬虫,而不是碎片化东拼西凑,欢迎订阅专栏👉《Python爬虫实战》👈,一次订阅后,专栏内的所有文章可永久免费阅读,持续更新中。

💕订阅后更新会优先推送,按目录学习更高效💯~

1️⃣ 摘要(Abstract)

本文将构建一个地理空间数据采集器。针对地图 API 单次查询的数量限制(通常限制返回 900-1000 条),我们将实现核心的 "矩形网格切片算法" (Grid Slicing),将目标区域切割为无数个微小的子区域进行地毯式扫描。最终产出包含经纬度、行业分类、行政区划的结构化数据,为绘制 Kepler.gl 或 Echarts 热力图做好完美准备。

读完本文,你将获得:

  1. 🌐 GIS 思维:掌握"化整为零"的地理围栏切片技术,突破 API 数量限制。
  2. 📍 合规采集:学会使用高德/百度开放平台 API 获取标准 POI 数据。
  3. 🔥 热力图准备:产出带有坐标系的清洗数据,直接赋能商业选址分析。

2️⃣ 背景与需求(Why)

为什么要爬 POI?

  • 商业选址:瑞幸咖啡开店前,会分析周边 500 米内有多少写字楼(白领多)和竞争对手(星巴克)。
  • 房产估值:计算小区周边"生活便利度"(超市、医院、学校的数量)。
  • 物流配送:修正地址库,通过行政区反查(Reverse Geocoding)补全模糊地址的街道信息。

目标站点/接口: 高德地图开放平台 (Amap API) 或 百度地图开放平台 API。
(注:本教程以高德 Web 服务 API 为例,这是国内最常用的商用数据源)

目标字段清单:

  • id: POI 唯一标识。
  • name: 店铺/地点名称。
  • location: 经纬度坐标 (GCJ-02)。
  • type: POI 类型(如:餐饮服务;咖啡厅)。
  • pname/cityname/adname: 省/市/区名称。
  • biz_ext: 评分或成本(如有)。

3️⃣ 合规与注意事项(必写)

地图数据涉及国家安全和版权,必须严格遵守以下红线:🚫

  • 严禁爬取瓦片图:绝对不要去爬地图网站的 HTML 或图片瓦片(Tiles),那是直接侵权且极易被封 IP。
  • 使用开发者 Key :必须去官网注册申请 Web服务 API 的 Key。个人开发者通常有免费配额(如 5000 次/天),足够做练习和小型分析。
  • 坐标系加密:国内地图强制使用 GCJ-02(火星坐标系)或 BD-09。不要试图将其转换为 WGS-84 用于非法测绘。我们做商业分析保持在 GCJ-02 体系内即可。

4️⃣ 技术选型与整体流程(What/How)

核心难题:

地图 API 有个硬伤:"搜索'北京市的餐厅',最多只给你返回 800-1000 条数据"。无论你怎么翻页,它都不给你更多。但全北京有几万家餐厅,怎么办?

解决方案:网格切片(Polygon Gridding)

我们要把"北京市"切成 1000 个 2km x 2km 的小方格。对每个小方格搜"餐厅",只要单个方格内的餐厅不超过 900 家,我们就能把全城的数据"拼"出来。

流程图:

Image of map grid division algorithm flowchart

  1. Define Bounds: 确定搜索的大矩形范围(左上角、右下角坐标)。
  2. Generate Grids: 利用算法生成 N 个小矩形。
  3. Fetch POI: 遍历每个小矩形,调用 API。
  4. Parsing: 清洗 JSON,提取字段。
  5. Storage: 存为 CSV/GeoJSON。

5️⃣ 环境准备与依赖安装(可复现)

我们需要处理几何图形,所以引入 shapely 库(虽然不用它也能手写,但用它更专业)。

Python 版本: 3y` 库(虽然不用它也能手写,但用它更专业)。

Python 版本: 3.9+

依赖安装:

bash 复制代码
pip install requests pandas shapely

目录:

text 复制代码
city_scanner/
├── data/
│   └── beijing_coffee.csv   # 结果文件
├── utils/
│   └── coord_transform.py   # 坐标系转换工具(可选)
└── scanner.py               # 主程序

6️⃣ 核心实现:网格切片算法(The Gridder)

这是本篇最含金量的部分。我们需要写一个生成器,把大区域切成小块。

python 复制代码
import numpy as np

class MapGridGenerator:
    def __init__(self, step_degree=0.02):
        """
        step_degree: 切片大小。0.01 经纬度大约等于 1公里。
        为了精细抓取,建议设为 0.01 或 0.02。
        """
        self.step = step_degree

    def generate_grids(self, top_left, bottom_right):
        """
        输入:左上角(lat, lon), 右下角(lat, lon)
        输出:一系列小矩形的字符串 "lon1,lat1|lon2,lat2" (高德多边形搜索格式)
        """
        lat_start, lon_start = top_left
        lat_end, lon_end = bottom_right
        
        # 这里的逻辑是:从左到右,从上到下扫描
        # np.arange 生成等差数列
        lat_steps = np.arange(lat_end, lat_start, self.step)
        lon_steps = np.arange(lon_start, lon_end, self.step)
        
        grids = []
        for lat in lat_steps:
            for lon in lon_steps:
                # 构建小矩形的四个角
                # 高德 API 格式:左上经度,左上纬度|右下经度,右下纬度
                # 注意:经度和纬度的顺序不要搞反!高德通常是 "经度,纬度"
                
                rect_str = f"{lon},{lat}|{lon + self.step},{lat + self.step}"
                grids.append(rect_str)
                
        return grids

7️⃣ 核心实现:请求与解析层(Fetcher & Parser)

这里我们模拟调用高德的 v3/place/polygon 接口。

python 复制代码
import requests
import time
import random

class AmapSpider:
    def __init__(self, api_key):
        self.api_key = api_key
        self.base_url = "https://restapi.amap.com/v3/place/polygon"

    def fetch_pois_in_grid(self, polygon, keyword, type_code=""):
        """
        在一个网格内翻页抓取所有数据
        """
        page = 1
        all_pois = []
        
        while True:
            params = {
                'key': self.api_key,
                'polygon': polygon, # 网格坐标
                'keywords': keyword,
                'types': type_code,
                'offset': 20,       # 每页数量 (max 20-50)
                'page': page,
                'extensions': 'all' # 获取详细信息
            }
            
            try:
                # 真实请求(如果 key 无效会返回 info code)
                # res = requests.get(self.base_url, params=params, timeout=5)
                # data = res.json()
                
                # --- MOCK 数据逻辑 (为了让你直接运行代码不报错) -----
                time.sleep(0.1) 
                if page > 2: # 模拟每个格子只有 2 页
                    data = {'count': '0', 'pois': []}
                else:
                    data = self._mock_response(page, keyword)
                # --------------------------------------------

                if data.get('count') == '0' or not data.get('pois'):
                    break
                
                for item in data['pois']:
                    all_pois.append(self._parse_poi(item))
                
                page += 1
                
                # 频率控制
                time.sleep(random.uniform(0.2, 0.5))

            except Exception as e:
                print(f"⚠️ Error fetching grid: {e}")
                break
                
        return all_pois

    def _parse_poi(self, item):
        """清洗单条 POI 数据"""
        return {
            'id': item.get('id'),
            'name': item.get('name'),
            'type': item.get('type'),
            'location': item.get('location'), # 格式 "116.xxx,39.xxx"
            'district': item.get('adname'),
            'address': item.get('address'),
            'business_area': item.get('business_area', '')
        }

    def _mock_response(self, page, keyword):
        """仅用于演示的模拟返回"""
        return {
            'count': '40',
            'pois': [
                {
                    'id': f'B00{random.randint(1000,9999)}',
                    'name': f'{keyword}演示店-{random.randint(1,100)}号',
                    'type': '餐饮服务;咖啡厅',
                    'location': f'116.{random.randint(3000,5000)},39.{random.randint(8000,9000)}',
                    'adname': '朝阳区',
                    'address': '工体北路xx号',
                    'business_area': '三里屯'
                }
            ] * 20
        }

8️⃣ 数据存储与导出(Storage)

POI 数据最适合存 CSV,因为可以直接导入 GIS 软件或 Kepler.gl

python 复制代码
import pandas as pd
import os

def save_to_csv(data_list, filename="poi_data.csv"):
    if not data_list:
        print("⚠️ No data to save.")
        return

    df = pd.read_json(pd.json.dumps(data_list)) # 偷懒法:List[Dict] -> DataFrame
    
    # 拆分经纬度,方便后续做热力图
    # location 格式是 "lon,lat"
    df[['longitude', 'latitude']] = df['location'].str.split(',', expand=True)
    
    df.to_csv(filename, index=False, encoding='utf-8-sig')
    print(f"💾 Saved {len(df)} POIs to {filename}")

9️⃣ 运行方式与结果展示(必写)

这里我们模拟抓取"北京朝阳区附近的咖啡厅"。

python 复制代码
def main():
    # 1. 配置
    # 请去高德开放平台申请 Key,这里填你的 Key
    API_KEY = "YOUR_AMAP_KEY_HERE" 
    
    # 定义抓取范围:例如北京三里屯周边的一个矩形区域
    # 左上角 (Lat, Lon), 右下角 (Lat, Lon)
    # 提示:可以用拾取坐标系统获得这些坐标
    TOP_LEFT = (39.95, 116.43) 
    BOTTOM_RIGHT = (39.91, 116.49)
    
    KEYWORD = "咖啡"
    
    # 2. 初始化
    gridder = MapGridGenerator(step_degree=0.01) # 切细一点,约1公里
    spider = AmapSpider(API_KEY)
    
    # 3. 生成网格
    grids = gridder.generate_grids(TOP_LEFT, BOTTOM_RIGHT)
    print(f"🗺️ Generated {len(grids)} grids for scanning...")
    
    # 4. 开始扫描
    total_data = []
    print("🚀 City-Scanner Started...")
    
    from tqdm import tqdm # 进度条是必须的
    for grid in tqdm(grids, desc="Scanning Grids"):
        pois = spider.fetch_pois_in_grid(grid, KEYWORD)
        total_data.extend(pois)
        
    # 5. 去重 (关键步骤)
    # 网格边缘的 POI 可能会被重复抓取,必须按 ID 去重
    unique_data = {p['id']: p for p in total_data}.values()
    
    print(f"✅ Scanning finished. Raw: {len(total_data)}, Unique: {len(unique_data)}")
    
    # 6. 保存
    save_to_csv(list(unique_data), "beijing_coffee_heatmap.csv")

if __name__ == "__main__":
    main()

运行结果示例

text 复制代码
🗺️ Generated 24 grids for scanning...
🚀 City-Scanner Started...
Scanning Grids: 100%|██████████| 24/24 [00:05<00:00, 4.12it/s]
✅ Scanning finished. Raw: 960, Unique: 850
💾 Saved 850 POIs to beijing_coffee_heatmap.csv

CSV 文件内容预览:

name type longitude latitude district business_area
星巴克(太古里店) 餐饮;咖啡 116.4532 39.9321 朝阳区 三里屯
%Arabica 餐饮;咖啡 116.4510 39.9311 朝阳区 三里屯
... ... ... ... ... ...

🔟 常见问题与排错(FAQ)

做地图爬虫,最头疼的是坐标系和限额。

  1. Coordinateshift (坐标偏移)?

    • 现象:抓回来的点,放到 Google Maps 或 ArcGIS 上,位置偏了 500 米。
    • 原因:国内 API 返回的是 GCJ-02(火星坐标),国际通用的 Kepler.gl/Tableau 用的是 WGS-84。
    • 解法 :必须进行纠偏转换。网上有开源的 coord_transform.py,核心公式是基于国测局算法。不要在正式文章里贴转换源码(敏感),建议让读者去 Github 搜gcoord` 库。
  2. Limit Reached (数据量还是不够)?

    • 原因:单个网格内的 POI 超过了 900 个(比如在市中心搜"餐厅")。
    • 解法递归切分 。如果 API 返回 count >=00,说明这个格子太大了,立刻把这个 1km 的格子再切成 4 个 500m 的格子重新搜。
  3. Invalid User Key (Key 报错)?

    • 原因:平台限制了新的个人开发者注册 Web 服务 Key。
    • 解法:尝试百度地图 API,或者寻找企业认证的 Key。

1️⃣1️⃣ 进阶优化(可视化准备)

拿到数据后,才是魔法开始的时候。

  • 可视化神器 Kepler.gl :无需写代码,直接把我们的 CSV 拖进 Kepler.gl

    • 选择 Point 图层,设置 Color Based on type
    • 或者选择 Heatmap 图层,设置 Weight Based on biz_ext.rating(评分)。
    • 瞬间生成好莱坞级别的 3D 城市热力图!![Image of Kepler.gl heatmap visualization]
  • 商圈边界生成 (Concave Hull)

    • 利用 shapely 库的算法,把密集的点围成一个面。这样你就能自动画出"这里是真正的商圈核心区",而不是只能看点。

1️⃣2️⃣ 总结与延伸阅读

复盘: 我们完成了一次降维打击**:利用网格切片算法,突破了 API 的限制,实现了对城市商业数据的全量扫描。

  1. Gridder: 将城市切碎。
  2. Fetcher: 蚂蚁搬家式采集。
  3. Cleaner: 坐标分离与去重。

下一步做什么?

  • 挑战 :加入多线程 (concurrent.futures),因为网格可能有几千个,单线程跑太慢了。
  • 深度分析:结合房价数据(从房产网站爬),计算"星巴克指数"------星巴克越密集的地方,房价是否越高?

地图数据极其迷人,因为它连接了数字世界和物理世界。去探索你的城市吧!

🌟 文末

好啦~以上就是本期的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥

✅ 专栏持续更新中|建议收藏 + 订阅

墙裂推荐订阅专栏 👉 《Python爬虫实战》,本专栏秉承着以"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一期内容都做到:

✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)

📣 想系统提升的小伙伴 :强烈建议先订阅专栏 《Python爬虫实战》,再按目录大纲顺序学习,效率十倍上升~

✅ 互动征集

想让我把【某站点/某反爬/某验证码/某分布式方案】等写成某期实战?

评论区留言告诉我你的需求,我会优先安排实现(更新)哒~


⭐️ 若喜欢我,就请关注我叭~(更新不迷路)

⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)

⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)


✅ 免责声明

本文爬虫思路、相关技术和代码仅用于学习参考,对阅读本文后的进行爬虫行为的用户本作者不承担任何法律责任。

使用或者参考本项目即表示您已阅读并同意以下条款:

  • 合法使用: 不得将本项目用于任何违法、违规或侵犯他人权益的行为,包括但不限于网络攻击、诈骗、绕过身份验证、未经授权的数据抓取等。
  • 风险自负: 任何因使用本项目而产生的法律责任、技术风险或经济损失,由使用者自行承担,项目作者不承担任何形式的责任。
  • 禁止滥用: 不得将本项目用于违法牟利、黑产活动或其他不当商业用途。
  • 使用或者参考本项目即视为同意上述条款,即 "谁使用,谁负责" 。如不同意,请立即停止使用并删除本项目。!!!
相关推荐
小雨中_1 小时前
2.9 TRPO 与 PPO:从“信赖域约束”到“近端裁剪”的稳定策略优化
人工智能·python·深度学习·机器学习·自然语言处理
小雨中_1 小时前
2.5 动态规划方法
人工智能·python·深度学习·算法·动态规划
癫狂的兔子2 小时前
【Python】【机器学习】决策树
python·决策树·机器学习
智算菩萨2 小时前
【Python小游戏】基于Pygame的递归回溯迷宫生成与BFS寻路实战:从算法原理到完整游戏架构的深度解析
python·算法·pygame
Zzz 小生2 小时前
LangChain Short-term memory:短期记忆使用完全指南
人工智能·python·langchain·github
qq_24218863322 小时前
使用 PyInstaller 打包 Python 脚本为 EXE(教程)
开发语言·python
闪电橘子2 小时前
Pycharm运行程序报错 Process finished with exit code -1066598273 (0xC06D007F)
ide·python·pycharm·cuda
我要七分甜2 小时前
Pycharm中Anaconda的详细配置过程
python·pycharm
Franklin2 小时前
2025-11-28日,天塌了,Pycharm将不开源了!!最后一个开源社区版本2025.2.5
ide·python·pycharm