Python环境下使用OpenStreetMap下载的.osm数据
引言
最近在项目中需要使用地理空间信息来辅助进行聚类工作,除了常规的经纬度信息之外,还需要更重要的地理层级信息,如对于“都江堰”来进行查询,期望获得“都江堰,成都,中国”这样一个完整的地理层级关系。因此,在这两天笔者便研究了一下如何获得这样的信息。
使用geopy包来实现
工程中用的是Python2,而在python中也确实有现有的包可以实现这样的功能,比如一个常用的包是geopy
。
其使用方法如下:
# -*- coding: utf-8 -*-
from geopy.geocoders import Nominatim
geolocator = Nominatim()
location = geolocator.geocode("dujiangyan")
print (location.address)
程序输出:
都江堰市, 都江堰市 / Dujiangyan, 成都市 / Chengdu, 四川省, 中国
可以看到,确实输出了一串地理层级信息,而且其中也确实包含了我们想要的正确的结果,但是结构非常的不标准,这样的结构在之后的操作中,想要处理成方便程序使用的形式是有些困难的,因为有很多地名会输出各种意想不到的结果的形式。
还有一点问题是,这个包是通过在线查询来返回结果的,而每次返回所需的时间大约是数秒级别的,如果是单次的查询,那么这个时间是完全可以接受的,而若大批量的查询,特别是需要实时效果的话,这种方法就难以获得理想的效果了。
而在尝试过几个包之后,发现这个包的效果其实已经相对较好了。在以前曾经用过谷歌的地理信息查询服务,但是谷歌提供的接口目前开始收费了,所以笔者又把目光放向了开源的OpenStreetMap。
下载OpenStreetMap的地图包
OpenStreetMap是一款开源的,由网络大众共同打造的地图服务,而且是知名度最高、应用最为广泛的开源地图之一,在前一部分所介绍的geopy
包里的一部分返回数据就是通过这个开源地图得到的。而且,OpenStreetMap由于是开源地图,是提供地图的下载的。
要下载OSM上的地图,我们可以在这个网址直接下载:
http://download.geofabrik.de/index.html
进入页面之后,可以看到按大洲下载地图的链接,也可以从左边某个大洲点进去,下载某个国家的地图,如我们进入“Asia”,然后下载中国的地图。
可以看到,在下载中,有三个可选项,分别是.osm.pbf、.shp.zip、.osm.bz3,在这里,我们需要的是.osm文件中的信息,而第一项和第三项都是.osm文件的压缩形式,其中,.bz2是可以直接解压缩的,但是大小是.pbf的1.5到2倍左右。需要下载哪一项,大家可以自己斟酌。
如果下载的是.pbf文件,是需要一个专门的工具来将.pbf文件转换成.osm文件的,这个工具可以在这里下载:https://wiki.openstreetmap.org/wiki/Osmconvert
pbf文件转换
工具本身非常小,下载下来之后,放入存储下载数据的文件夹(即存储.pbf文件的文件夹,推荐所在分区留出较多空间,因为可能占用较多空间),然后打开之后是一个命令行操作的界面。这时,可以用如下的命令直接进行转换:
osmconvert syria-latest.osm.pbf --out-osm -o=syria-latest.osm_01.osm
当然,工具本身也比较方便,无需记忆命令,先按照提示键入a
,然后程序会询问要处理哪个文件,这时键入文件的全名,之后程序会询问需要对该文件进行什么操作,这时键入1
,选择要对文件格式进行转换,最后在选择输出格式时,选择1
,选择按照.osm
格式输出,便可以得到我们需要的.osm
格式的文件了。
.osm文件中的数据
.osm
文件是OpenStreetMap专门用来封装自家数据的一种格式,里面可以按照XML格式的文件来进行读取。
关于osm内部的结构,我参考了这篇文章:https://blog.csdn.net/scy411082514/article/details/7484497/
OpenStreetMap的元素主要包括三种:点(Nodes)、路(Ways)和关系(Relations),这三种原始构成了整个地图画面。其中,Nodes定义了空间中点的位置;Ways定义了线或区域;Relations(可选的)定义了元素间的关系。
而我们所需要的地理层级信息便在node字段中,其中也可获取到经纬度等信息,如“都江堰”字段内如下:
{u'k': u'gns:ADM1', u'v': u'32'}
{u'k': u'gns:DSG', u'v': u'ADM3'}
{u'k': u'gns:UFI', u'v': u'-1907309'}
{u'k': u'gns:UNI', u'v': u'10071801'}
{u'k': u'is_in', u'v': u'Chengdu, Sichuan, China'}
{u'k': u'is_in:continent', u'v': u'Asia'}
{u'k': u'is_in:country', u'v': u'China'}
{u'k': u'is_in:country_code', u'v': u'CN'}
{u'k': u'name', u'v': u'\u90fd\u6c5f\u5830\u5e02'}
{u'k': u'name:de', u'v': u'Dujiangyan'}
{u'k': u'name:en', u'v': u'Dujiangyan'}
{u'k': u'name:fr', u'v': u'D\u016bji\u0101ngy\xe0n'}
{u'k': u'name:ja', u'v': u'\u90fd\u6c5f\u5830\u5e02'}
{u'k': u'name:ru', u'v': u'\u0414\u0443\u0446\u0437\u044f\u043d\u044a\u044f\u043d\u044c'}
{u'k': u'name:vi', u'v': u'\u0110\xf4 Giang Y\u1ec3n'}
{u'k': u'name:zh', u'v': u'\u90fd\u6c5f\u5830\u5e02'}
{u'k': u'name:zh_pinyin', u'v': u'D\u016bji\u0101ngy\xe0n Shi'}
{u'k': u'place', u'v': u'city'}
{u'k': u'wikidata', u'v': u'Q1023900'}
{u'k': u'wikipedia', u'v': u'en:Dujiangyan City'}
而其中的is_in
字段,便是我们需要的地理层级信息了,如这一条中的{u'k': u'is_in', u'v': u'Chengdu, Sichuan, China'}
,便说明都江堰属于“中国,四川,成都,都江堰”,将其解析出来便可直接使用。
将.osm文件中的数据转存到json中
由于.osm
中的数据是按照xml的形式存储的,若每次都从中读取数据的话,对于单个就达几个G甚至数十上百G的文件,若按照树的方式来进行解析,不光时间上难以接受,首先面临的就是内存不足的问题。
对于.osm
文件已经有专门的数据库可以来存储其中的信息,而在我们的工程中使用的是MongoDb数据库,为了便于以后的使用,和往我们的数据库里导入数据,这里我准备先将.osm
文件转换到.json
文件中。
需要注意的是,在转换的过程中,我们是不能将整个文件完整地解析出来的,因为会占用极大的内存,在这里,我们可以采用递归的方法来进行处理。
代码如下:
# -*- coding: utf-8 -*-
import json
from lxml import etree
import xmltodict
def iter_element(file_parsed, file_length, file_write):
current_line = 0
try:
for event, element in file_parsed:
current_line += 1
print current_line/float(file_length)
elem_data = etree.tostring(element)
elem_dict = xmltodict.parse(elem_data, attr_prefix="", cdata_key="")
if (element.tag == "node"):
elem_jsonStr = json.dumps(elem_dict["node"])
file_write.write(elem_jsonStr + "\n")
# 每次读取之后进行一次清空
element.clear()
while element.getprevious() is not None:
del element.getparent()[0]
except:
pass
if __name__ == '__main__':
osmfile = r'D:\data\china-latest.osm'
file_length = -1
for file_length, line in enumerate(open(osmfile, 'rU')):
pass
file_length += 1
print "length of the file:\t" + str(file_length)
file_node = open(osmfile+"_node.json","w+")
file_parsed = etree.iterparse(osmfile, tag=["node"])
iter_element(file_parsed, file_length, file_node)
file_node.close()
这样,就可以将其中的node里的信息保存下来了,之后,我们可以将其中包含地理层级信息的部分筛选出来,经过观察,可以发现里面有大量只有一到两行的数据,其中数据是后面不会使用的,这里,我们可以将其筛出,代码如下:
import json
count = 0
file_write = open(r'D:\data\test.txt', mode = 'wb')
with open(r'D:\data\china-latest.osm_node.json', mode='rb') as file_read:
for line in file_read:
line = line.replace('\n', '')
info = json.loads(line)
if 'tag' in info and type(info['tag']) is list and len(info['tag']) > 2:
for item in info['tag']:
print item
file_write.write(str(item) + '\n')
print "-" * 60
file_write.write("-" * 100 + '\n')
count += 1
在上面这段代码中,仅仅是读取之前所得到的.json
文件并将大于两行的数据打印出来并保存到一个txt文件中,后面可以改为其它操作。