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文件中,后面可以改为其它操作。

posted @ 2018-11-24 11:24  点点爱梦  阅读(1333)  评论(0编辑  收藏  举报