Python获取高德POI(关键词搜索法)
胎教级爬取poi教程!尝试用通俗易懂的话来谈谈高德poi爬取这件事儿
文章目录
高德POI的获取
改进代码
该篇文章是关键词搜索法获取高德poi,但鉴于无法突破900条记录的上限,因此重写了矩形搜索法的文章,具体可参考以下文章:
(建议没有python基础的朋友先阅读该篇再看矩形搜索法!)
前言
首先我们需要明白一些常识
- poi是兴趣点,它本身除了经纬度,还记录了一些信息,如名称、地址、联系方式、所属行政区
- 高德poi是指从高德地图上获取poi,所以我们借助的平台是高德地图
- 高德地图知道有很多人需要用到poi,因此它十分良心地推出了若干个接口(API),借助这些API我们就可以各取所需。
- 最容易被忽略的一点,通过高德地图获取得到的坐标会有偏移,具体详看3.2节,因此我们获取得到的坐标还需要经过坐标系转换才能得到正确的坐标,
API的理解
我们把整个poi的获取理解成以下几个步骤:
(1)找“高德地图”这个人申请要数据
(2)高德地图他说你申请数据需要先填一下这张数据需求表
(3)你拿到这张表(API),开始填写表(API)里的信息,包括POI的类别,POI的区域,需要数据的格式
(4)高德地图看了你填写的需求表,就从它的数据库里找到并且交回给你
(5)你拿到数据之后,筛选出关心的信息,之后就找各种方式把数据保存起来,例如存成一份excel表,或者一份txt文件,抑或是csv表格等等
所以通俗地讲,API就是一份表,通过这个表可以让对方返回你需要的东西,只不过实际上,这个表是用链接的形式发给你,在链接里填入信息就相当于填表的行为了。
查看API接口
在高德开放平台上,我们获取poi所需的接口位于[开发支持]-[Web服务]-[Web服务API]下,点击即可进到主页。我们需要用到的接口如下图所示:
从图上可以知道,高德地图说想使用这个API,你必须有我家的密钥(key),因此我们首先需要去申请一下这个key。具体怎么申请可以参照以下这篇文章(https://zhuanlan.zhihu.com/p/96838566),接下来默认大家都有这个key了。
然后呢,我们从适用场景那里得知,这个搜索poi有四种方法可以获取poi,分别是关键字搜索、周边搜索、多边形搜索和ID查询,我们这里使用最简单的关键字搜索。
在关键字搜索下面,我们可以看到有个请求参数的表格,这个请求参数其实就是API里需要我们填写的信息,我们看到那么多参数,其实不用慌,分清哪些是必填的,哪些是可填的,接下来就好办了。
从我框选的地方可以知道,其实必填的只有两三项。
1.key,就是我们上面申请的密钥
2.keywords或者types,注意是两者至少填一个,我示例里选择的是types
3.city,这个如果不填的话默认是帮你搜索全国的,但我们一般都会指定一个区域
4.page,你可以想象成一页页的纸,高德地图默认每页20条信息,默认返回一页给你,也就是20条。那么为了获取全部的poi,我们需要高德地图返回尽可能多页给我们,直到搜不到poi为止,因此这个参数在控制循环的时候尤为重要
5.output,返回的数据格式类型,我们一般都返回JSON格式,方便我们处理
接下来,我们查看一下返回的参数,这部分内容比较多,因此我录了个视频来讲解。
核心参数:
1. status
2. pois(核心参数:location、name等)
查看完返回结果参数之后,我们就已经明白了我们需要填写的信息和最后得到的信息有哪些,那么距离开始操作就只差最后一步了,就是拿表(API)填写。
高德地图里给我们举了个栗子:
不难发现,这个API里面有一些参数是我们熟悉的,例如keywords、city、key,并且这些关键词都是通过“&”符号进行连接,因此我们只要能够替换掉它的这些信息,就可以完成API的构建啦。
例如我需要获取东莞市的,就把 city=beijing
改成city = 东莞市
需要获取公园类型的poi,就把keywords换成types,然后types = 公园
即可。
实现思路
这是我的一些想法:
-
我们现在已经学会怎么通过修改链接里的关键词来构建我们所需的API,那么下一步其实只需要向高德地图发出申请即可
-
但仔细思考,我们可以把整个过程划分成以下三个部分:
- 发出一次申请,高德地图返回一页数据给我,我们可以将这一次的申请想象成举手向老师拿东西(但注意,老师不会一次性把所有东西都给你)
- 我们通过某个指标,保证区域内的poi都获取完,这个过程理解为如果老师没有把东西全都给我,那我就一直举手,直到给完为止。
- 获取完之后我们需要将数据存到电脑上,因此这个过程需要我们想方设法地存成某种文件。
-
通过上面过程的划分,我们可以抽象成三个函数,一个举手函数,一个反复举手函数,一个保存结果函数。
-
虽然我在这次实验中需要获取高德地图poi,但如果下次我需要获取另外一个地方的其他类型poi,我能否用这次的方法来直接获取而不是重新写一次代码?
-
因此我想将上述的过程都封装成函数,下次再使用的时候只需要改改地区、poi类别即可完成所有工作。
实现过程
这里我们引入所需要的库
#库的说明
1.requests --爬虫常用库
2.json --该库能够帮我们处理高德返回的JSON格式的数据
3.xlwt --名字很奇怪,但它是xlsx、xls writer的意思,就是存储为excel表格库
4.Coordin_transformlat -- 这个是自己写的一个坐标转换的库,其目的是为了解决开头说的坐标偏移的问题
5.urlib -- 主要使用该库里的quote函数,因为当我们向电脑输入汉字时,计算机其实并不认识汉字,因为它只认识0101这些二进制代码,那么quote函数就可以将汉字翻译成计算机能够懂的语言。
#引入所需的库
import requests
import json
from urllib.parse import quote
import xlwt
from Coordin_transformlat import gcj02towgs84
1.构建一个申请的函数(举手函数)
回想一下刚刚的api,申请必须填写的几个参数:key,city,types或keywords,同时,容易忽略的一个参数是page,在请求函数里page是个可选参数,表示当前页数,我们可以通过递增page的值来反复地提交申请,例如第一次先申请第一页,第二次申请第二页这样实现,所以这里至少需要四个参数
代码理解
url: 是一个链接,通过访问这个链接,我们可以获取得到所需的数据
User-Agent:用户代理头,因为我们编写的是爬虫程序,通过爬虫去获取数据,而网站其实是不太支持爬虫爬取数据,因为爬虫的访问次数太快太多了,后台容易崩溃。那怎么让对方认不出来这是个爬虫呢?
通过添加User-Agent来给爬虫戴上用户(指正常浏览网站的人)的面具,这样瞒过对方视线,一般设置在headers里。
request.get():我们通过get(url,header)来爬取数据
get()方法,只要你输入一个网址就可以模拟人浏览网站的行为,相当于访问url这个链接
def Get_poi(key,city,types,page):
'''
这是一个能够从高德地图获取poi数据的函数
key:为用户申请的高德密钥
city:目标城市
types:POI数据的类型
page:当前页数
'''
#设置header
header = {'User-Agent': "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50"}
#构建url
#{}在链接里表示占位符,也就是占住位置先不填写,.format()里就是往刚刚占位符的地方填写变量,按照顺序一一对应,第一个{}里是key,第二个{}里是types
url = 'https://restapi.amap.com/v3/place/text?key={}&types={}&city={}&page={}&output=josn'.format(key,types,quote(city),page)
#用get函数请求数据
r = requests.get(url,headers=header)
#设置数据的编码为'utf-8'
r.encoding = 'utf-8'
#将请求得到的数据按照'utf-8'编码成字符串
data = r.text
return data
2.构建反复申请的函数(多次举手)
这里只需要重复调用上面构造好的申请函数,直到最后一页的数据条数(Count)为0
因此这里只要当前页数(page)的记录条数(count)不为0,就一直执行循环
def Get_times(key,city,types):
'''
这是一个控制申请次数的函数
'''
page = 1
#创建一个poilist的空列表
poilist = []
#执行以下代码,直到count为0的时候跳出循环
while True:
#调用第一个函数来获取数据
result = Get_poi(key,city,types,page)
#json.loads可以对获取回来JSON格式的数据进行解码
content = json.loads(result)
#content的样子其实跟返回结果参数是一样的,可以想象成有很多个字段的excel表格,下面这个语句就是提取出pois那个字段
pois = content['pois']
#pois的信息写入空列表里,这里由于不知道返回的数据长什么样子,所以会难以理解些
for i in range(len(pois)):
poilist.append(pois[i])
#递增page
page = page + 1
#判断当前页下的count是否等于0
if content['count'] == '0':
break
#将写好poi信息的列表返回
return poilist
content的样子:
这图里,我们可以看到有很多条数据,这些数据就是高德地图返回给我们的,但类似于info、suggestion这些字段我们是不关心的,我们只关心pois这个字段,所以我们用pois = content['pois']
这行代码来提取出pois这个字段。
但如果观察得仔细,你会发现pois字段里面还有很多的信息,除了我们关心的location、name以外,还有id、parent、email等,所以第三步需要我们再一次筛选信息
3.构建保存函数
通过上述的函数,我们得到了只包含pois字段的列表,但这个pois字段里还有很多无关的信息,因此我们这里单独提取出经纬度(location)、名称(name)、地址(address)
(下面这段代码是直接搬了别人的过来,现成的就不用自己再写一遍了,意义不大)
借鉴博客:(https://blog.csdn.net/john_ashley/article/details/114196683)
def write_to_excel(poilist, city,types):
'''
这是一个可以将列表写入excel的函数
poilist -- 上面得到的poilist
city -- 城市名,这个参数是保存excel文件的名字用的
types -- poi类别,这个也是为了保存excel文件,可直接看代码最后一行
'''
#我们可以把这两行代码理解成新建一个excel表,第一句是新建excel文件
book = xlwt.Workbook(encoding='utf-8', style_compression=0)
#往这个excel文件新建一个sheet表格
sheet = book.add_sheet(types, cell_overwrite_ok=True)
# 第一行(列标题)
sheet.write(0, 0, 'x')
sheet.write(0, 1, 'y')
sheet.write(0, 2, 'count')
sheet.write(0, 3, 'name')
sheet.write(0, 4, 'address')
sheet.write(0, 5, 'adname')
#最难理解的地方应该是这里了,放到代码后面讲解
for i in range(len(poilist)):
name = poilist[i]['name']
location = poilist[i]['location']
address = poilist[i]['address']
adname = poilist[i]['adname']
lng = str(location).split(",")[0]
lat = str(location).split(",")[1]
#这里是坐标系转换,也放到代码后面详解
result = gcj02towgs84(location)
lng = result[0]
lat = result[1]
# 每一行写入
sheet.write(i + 1, 0, lng)
sheet.write(i + 1, 1, lat)
sheet.write(i + 1, 2, 1)
sheet.write(i + 1, 3, name)
sheet.write(i + 1, 4, address)
sheet.write(i + 1, 5, adname)
# 最后,将以上操作保存到指定的Excel文件中
book.save(city + "_" + types + '.xls')
第三个文件其实难以理解的地方只有for循环跟坐标系转换,第一个是技术问题,第二个是背景问题,我们先来将坐标系。
3.1 高德的坐标系
1.为什么高德地图得到的坐标系不能直接使用?
其实是因为国家规定,中国大陆所有公开地理数据都需要至少用GCJ-02进行加密,也就是说我们从国内公司的产品中得到的数据,一定是经过了加密的。绝大部分国内互联网地图提供商都是使用GCJ-02坐标系,包括高德地图,谷歌地图中国区等。
2.关于这个GCJ-02坐标系
GCJ-02(G-Guojia国家,C-Cehui测绘,J-Ju局),又被称为火星坐标系,是一种基于WGS-84制定的大地测量系统,由中国国测局制定。此坐标系所采用的混淆算法会在经纬度中加入随机的偏移。所以这就是我们为什么需要对获取得到的经纬度进行坐标系转换的原因了。
3.“Coordin_transformlat – 这个是自己写的一个坐标转换的库” 这个怎么理解呢?
其实呢,就是自己写了一个python文件,只不过在这个文件里有一些方法,我希望在另外的文件里也能用上,所以通过import的方法引入,例如我在A文件里写了某个方法,然后我在B文件里也想使用这个方法,那么我在B文件里import A即可。
当B文件运行的时候,系统读到了这个方法,就会去A文件里看看这个方法是怎样的,大致如此。
4.Coordin_transformlat这个模块有什么用呢?
从上面标注其实可以发现,这个模块里头有许多函数,如bd09todcj02,gcj02tobd09,wgs84togcj02等等,其实这些都是坐标转换的函数,目的不就是实现坐标转换吗?所以在代码中的那句result = gcj02towgs84(location)
就不难理解了,是从火星坐标系(gcj02)转向WGS84坐标系。
Coordin_transformlat源代码
# * 百度坐标系 (BD-09) 与 火星坐标系 (GCJ-02)的转换
# * 即 百度 转 谷歌、高德
# * @param bd_lon
# * @param bd_lat
# * @returns {*[]}
# */
import math
def bd09togcj02(bd_lon, bd_lat):
x_pi = 3.14159265358979324 * 3000.0 / 180.0
x = bd_lon - 0.0065
y = bd_lat - 0.006
z = math.sqrt(x * x + y * y) - 0.00002 * math.sin(y * x_pi)
theta = math.atan2(y, x) - 0.000003 * math.cos(x * x_pi)
gg_lng = z * math.cos(theta)
gg_lat = z * math.sin(theta)
return [gg_lng, gg_lat]
# * 火星坐标系 (GCJ-02) 与百度坐标系 (BD-09) 的转换
# * 即谷歌、高德 转 百度
# */
def gcj02tobd09(lng, lat):
x_PI = 3.14159265358979324 * 3000.0 / 180.0
z = math.sqrt(lng * lng + lat * lat) + 0.00002 * math.sin(lat * x_PI)
theta = math.atan2(lat, lng) + 0.000003 * math.cos(lng * x_PI)
bd_lng = z * math.cos(theta) + 0.0065
bd_lat = z * math.sin(theta) + 0.006
return [bd_lng, bd_lat]
# wgs84转高德
def wgs84togcj02(lng, lat):
PI = 3.1415926535897932384626
ee = 0.00669342162296594323
a = 6378245.0
dlat = transformlat(lng - 105.0, lat - 35.0)
dlng = transformlng(lng - 105.0, lat - 35.0)
radlat = lat / 180.0 * PI
magic = math.sin(radlat)
magic = 1 - ee * magic * magic
sqrtmagic = math.sqrt(magic)
dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * PI)
dlng = (dlng * 180.0) / (a / sqrtmagic * math.cos(radlat) * PI)
mglat = lat + dlat
mglng = lng + dlng
return [mglng, mglat]
# GCJ02/谷歌、高德 转换为 WGS84 gcj02towgs84
def gcj02towgs84(localStr):
lng = float(localStr.split(',')[0])
lat = float(localStr.split(',')[1])
PI = 3.1415926535897932384626
ee = 0.00669342162296594323
a = 6378245.0
dlat = transformlat(lng - 105.0, lat - 35.0)
dlng = transformlng(lng - 105.0, lat - 35.0)
radlat = lat / 180.0 * PI
magic = math.sin(radlat)
magic = 1 - ee * magic * magic
sqrtmagic = math.sqrt(magic)
dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * PI)
dlng = (dlng * 180.0) / (a / sqrtmagic * math.cos(radlat) * PI)
mglat = lat + dlat
mglng = lng + dlng
return [lng * 2 - mglng,lat * 2 - mglat]
def transformlat(lng, lat):
PI = 3.1415926535897932384626
ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * \
lat + 0.1 * lng * lat + 0.2 * math.sqrt(abs(lng))
ret += (20.0 * math.sin(6.0 * lng * PI) + 20.0 *
math.sin(2.0 * lng * PI)) * 2.0 / 3.0
ret += (20.0 * math.sin(lat * PI) + 40.0 *
math.sin(lat / 3.0 * PI)) * 2.0 / 3.0
ret += (160.0 * math.sin(lat / 12.0 * PI) + 320 *
math.sin(lat * PI / 30.0)) * 2.0 / 3.0
return ret
def transformlng(lng, lat):
PI = 3.1415926535897932384626
ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + \
0.1 * lng * lat + 0.1 * math.sqrt(abs(lng))
ret += (20.0 * math.sin(6.0 * lng * PI) + 20.0 *
math.sin(2.0 * lng * PI)) * 2.0 / 3.0
ret += (20.0 * math.sin(lng * PI) + 40.0 *
math.sin(lng / 3.0 * PI)) * 2.0 / 3.0
ret += (150.0 * math.sin(lng / 12.0 * PI) + 300.0 *
math.sin(lng / 30.0 * PI)) * 2.0 / 3.0
return ret
3.2 循环体代码的理解
for i in range(len(poilist)):
name = poilist[i]['name']
location = poilist[i]['location']
address = poilist[i]['address']
adname = poilist[i]['adname']
lng = str(location).split(",")[0]
lat = str(location).split(",")[1]
如果你想理解上面的循环体,那么必须要了解以下的知识:
- poilist是长什么样的
- for循环里的i
- 多维列表的遍历。
那么下面我就一一讲解一下。
poilist的样子大概是这样的:
这里需要注意的是,poilist它本身是一个列表,这个大列表里面包含着295个子项,每一个子项又是字典形式的。
画出示意图大概就是下面这样:
name = poilist[i]['name']
location = poilist[i]['location']
address = poilist[i]['address']
adname = poilist[i]['adname']
所以你再看这几行代码的时候,是不是就没那么陌生了
而在for循环里的i,它其实是个变量。
你可以想象一下"有十只羊,按顺序排好队,一只只地送上屠宰场"这个情景,我们假设一个准备送上屠宰场的羊 的变量,那么随着羊一只只地走了,这个变量也在不断地指向下一只羊,它所指代的内容是不断变化的。
再比如,在for i in range(1,10)
这个代码里,第一次循环i是1,然后下一次循环的时候i就变成了2,下一次变成了3,只到所有的都轮过一遍才结束。
因此,不难理解,在我们的代码 for i in range(len(poilist)):
里,i是一个变化着的数值,从0开始,直到变成poilist的长度那么大的数字结束。然后刚好这个数字放在了poilist[i]里,可以不断地指向poilist的每个子项。
完整代码:
import requests
import json
from urllib.parse import quote
import xlwt
from Coordin_transformlat import gcj02towgs84
def Get_poi(key, city, types, page):
'''
这是一个能够从高德地图获取poi数据的函数
key:为用户申请的高德密钥
city:目标城市
types:POI数据的类型
page:当前页数
'''
# 设置header
header = {
'User-Agent': "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50"}
# 构建url
# {}在链接里表示占位符,也就是占住位置先不填写,.format()里就是往刚刚占位符的地方填写变量,按照顺序一一对应,第一个{}里是key,第二个{}里是types
url = 'https://restapi.amap.com/v3/place/text?key={}&types={}&city={}&page={}&output=josn'.format(key, types,
quote(city), page)
# 用get函数请求数据
r = requests.get(url, headers=header)
# 设置数据的编码为'utf-8'
r.encoding = 'utf-8'
# 将请求得到的数据按照'utf-8'编码成字符串
data = r.text
return data
def Get_times(key, city, types):
'''
这是一个控制申请次数的函数
'''
page = 1
# 创建一个poilist的空列表
poilist = []
# 执行以下代码,直到count为0的时候跳出循环
while True:
# 调用第一个函数来获取数据
result = Get_poi(key, city, types, page)
# json.loads可以对获取回来JSON格式的数据进行解码
content = json.loads(result)
# content的样子其实跟返回结果参数是一样的,可以想象成有很多个字段的excel表格,下面这个语句就是提取出pois那个字段
pois = content['pois']
# pois的信息写入空列表里,这里由于不知道返回的数据长什么样子,所以会难以理解些
for i in range(len(pois)):
poilist.append(pois[i])
# 递增page
page = page + 1
# 判断当前页下的count是否等于0
if content['count'] == '0':
break
# 将写好poi信息的列表返回
return poilist
def write_to_excel(poilist, city,types):
'''
这是一个可以将列表写入excel的函数
poilist -- 上面得到的poilist
city -- 城市名,这个参数是保存excel文件的名字用的
types -- poi类别,这个也是为了保存excel文件,可直接看代码最后一行
'''
#我们可以把这两行代码理解成新建一个excel表,第一句是新建excel文件
book = xlwt.Workbook(encoding='utf-8', style_compression=0)
#往这个excel文件新建一个sheet表格
sheet = book.add_sheet(types, cell_overwrite_ok=True)
# 第一行(列标题)
sheet.write(0, 0, 'x')
sheet.write(0, 1, 'y')
sheet.write(0, 2, 'count')
sheet.write(0, 3, 'name')
sheet.write(0, 4, 'address')
sheet.write(0, 5, 'adname')
#最难理解的地方应该是这里了,放到代码后面讲解
for i in range(len(poilist)):
name = poilist[i]['name']
location = poilist[i]['location']
address = poilist[i]['address']
adname = poilist[i]['adname']
lng = str(location).split(",")[0]
lat = str(location).split(",")[1]
#这里是坐标系转换,也放到代码后面详解
result = gcj02towgs84(location)
lng = result[0]
lat = result[1]
# 每一行写入
sheet.write(i + 1, 0, lng)
sheet.write(i + 1, 1, lat)
sheet.write(i + 1, 2, 1)
sheet.write(i + 1, 3, name)
sheet.write(i + 1, 4, address)
sheet.write(i + 1, 5, adname)
# 最后,将以上操作保存到指定的Excel文件中
book.save(city + "_" + types + '.xls')
#这里修改为自己的高德密钥
key = '**********'
#这里修改自己的poi类型
types = ['公园广场','汽车服务']
#建议将大区域切分成几个小区域来获取,保证获取的数据齐全
city_list = ['白云区','天河区','越秀区','黄埔区']
#先遍历city_list里面的每个区域
for city in city_list:
#再遍历types里的每个类别
for type in types:
poi = Get_times(key,city,type)
print('当前城市:' + str(city) + ', 分类:' + str(type) + ", 总的有" + str(len(poi)) + "条数据")
write_to_excel(poi,city,type)
print('*'*50+'分类:' + str(type) + "写入成功"+'*'*50)
print('====爬取完成====')
运行结果
汽车服务的有几条数据都是897,应该是区域内的poi数量超过1000了,因此这个通过关键字搜索poi的方法还有待改善~码不动了,改日再战!
总的来说,过程还是挺顺利的!
一些关键词搜索法的问题
在2022年7月6日高德POI进行了一次更新之后,关键词搜索法也出现了一些新的问题,例如获取到的POI数量最多为200条,这里我也尝试获取某个区的酒店poi,发现从11页开始POI的数量就变为0了。
这个问题也困扰着我,暂时无法用该方法解决。所幸的是矩形搜索法目前能够很好地解决这个问题,所以建议大家将该方法作为熟悉高德POI的一种方式,实战中用矩形搜索法去获取齐全的POI数据。
更多推荐
所有评论(0)