Как извлечь члены отношения из XML-файлов .osm с помощью Python

Все,

Я пытался создать веб-сайт (в Django), который должен быть индексом всех маршрутов MTB в мире. Я питонианец, поэтому везде, где могу, я стараюсь использовать Python.

Я успешно извлек данные из OSM API (отображение отношения (след) в листовке), но обнаружил, что делать это для всех трасс MTB (tag: route=mtb) - это слишком много данных (обработка занимает очень много времени). Итак, я попытался сделать все локально, загрузив торрент со всем набором данных OpenStreetMap (из файла XML Latest Weekly Planet) и отфильтровав tag: route=mtb с помощью osmfilter (часть osmctools в Ubuntu 20.04), например:

      osmfilter $unzipped_osm_planet_file --keep="route=mtb" -o=$osm_planet_dir/world_mtb_routes.osm

В результате получается файл размером около 1,2 ГБ, и при ближайшем рассмотрении кажется, что он содержит все необходимые мне данные. Моя цель состояла в том, чтобы преобразовать файл в pandas.DataFrame(), чтобы я мог выполнить дополнительную фильтрацию и преобразование, прежде чем вставлять соответствующие аспекты в мою базу данных Django. Я попытался загрузить файл как обычный XML-файл с помощью Python Pandas, но это привело к сбою ядра записной книжки Jupyter. Думаю, данных слишком много.

Моим вторым подходом было решение: как извлечь и визуализировать данные из файла OSM в Python . У меня это сработало, по крайней мере, я могу получить некоторую информацию, например, теги отношений в файле (и другие указанные детали). Что мне не хватает, так это члены отношения (пути), а затем члены пути (узлы) и их широта / долгота. Они мне нужны для достижения того, что я здесь сделал: построение отношений OpenStreetMap не создает непрерывных линий.

Я открыт для многих решений, например, можно разбить файл на множество разных файлов, содержащих одно отношение и его членов на файл, используя скрипт на основе osmium. Возможно, тогда я смогу перейти к pandas.read_xml(). Это было бы хорошо для пакетной обработки при заполнении базы данных. Было бы неплохо загрузить весь XML-файл OSM в pd.DataFrame, но я думаю, что это действительно много данных. Возможно, это также можно сделать на основе отношения с пиосмием?

Любая помощь приветствуется.

1 ответ

Хорошо, я понял, как получить то, что я хочу (вся информация по отношению типа "route=mtb" хранится в доступном виде), это многоэтапный процесс, я опишу его здесь.

Сначала я скачал файл мира (зашел на wiki.openstreetmap.org/wiki/Planet.osm, открыл xml файла pbf и скачал файл мира как .pbf (все в Linux, и этот файл называется $osm_planet_file ниже).

Я преобразовал этот файл в o5m, используя osmconvert (доступно в Ubuntu 20.04, выполнив apt install osmctools, на линуксовом клиенте:

      osmconvert --verbose --drop-version $osm_planet_file -o=$osm_planet_dir/planet.o5m

Следующий шаг — отфильтровать все интересующие отношения из этого файла (в моем случае мне нужны были все MTB-маршруты: route=mtb) и сохраните их в новом файле, например:

      osmfilter $osm_planet_dir/planet.o5m --keep="route=mtb" -o=$osm_planet_dir/world_mtb_routes.o5m

Это создает гораздо меньший файл, который содержит всю информацию об отношениях, являющихся маршрутами MTB.

С этого момента я переключился на блокнот Jupyter и использовал Python3 для дальнейшего разделения файла на полезные, управляемые фрагменты. Сначала я установил osmium с помощью conda (в env, который я создал первым, но это можно пропустить):

      conda install -c conda-forge osmium

Затем я сделал рекомендуемый класс osm.SimpleHandle, этот класс перебирает большой файл o5m и при этом может выполнять действия. Это способ работы с этими файлами, потому что они слишком велики для памяти. Я решил перебрать файл и сохранить все, что мне нужно, в отдельных файлах json. Это создает более 12000 файлов json, но это можно сделать на моем ноутбуке с 8 ГБ памяти. Это класс:

      import osmium as osm
import json
import os

data_dump_dir = '../data'

class OSMHandler(osm.SimpleHandler):
    def __init__(self):
        osm.SimpleHandler.__init__(self)
        self.osm_data = []

    def tag_inventory(self, elem, elem_type):
        for tag in elem.tags:
            data = dict()
            data['version'] = elem.version,
            data['members'] = [int(member.ref) for member in elem.members if member.type == 'w'], # filter nodes from waylist => could be a mistake
            data['visible'] = elem.visible,
            data['timestamp'] = str(elem.timestamp),
            data['uid'] = elem.uid,
            data['user'] = elem.user,
            data['changeset'] = elem.changeset,
            data['num_tags'] = len(elem.tags),
            data['key'] = tag.k,
            data['value'] = tag.v,
            data['deleted'] = elem.deleted
            with open(os.path.join(data_dump_dir, str(elem.id)+'.json'), 'w') as f:
                json.dump(data, f)

    def relation(self, r):
        self.tag_inventory(r, "relation")

Запустите класс следующим образом:

      osmhandler = OSMHandler()
osmhandler.apply_file("../data/world_mtb_routes.o5m")

Теперь у нас есть файлы json с номером отношения в качестве имени файла и со всеми метаданными, а также список способов. Но нам нужен список путей, а затем также все узлы для каждого пути, чтобы мы могли построить полные отношения (маршруты MTB). Для этого мы снова анализируем файл o5m (используя класс, созданный на основе класса osm.SimpleHandler), и на этот раз мы извлекаем все элементы пути (узлы) и создаем словарь:

      class OSMHandler(osm.SimpleHandler):
    def __init__(self):
        osm.SimpleHandler.__init__(self)
        self.osm_data = dict()

    def tag_inventory(self, elem, elem_type):
        for tag in elem.tags:
            self.osm_data[int(elem.id)] = dict()
#             self.osm_data[int(elem.id)]['is_closed'] = str(elem.is_closed)
            self.osm_data[int(elem.id)]['nodes'] = [str(n) for n in elem.nodes]

    def way(self, w):
        self.tag_inventory(w, "way")

Выполните класс:

      osmhandler = OSMHandler()
osmhandler.apply_file("../data/world_mtb_routes.o5m")
ways = osmhandler.osm_data

Это дает dict (называемый путями) всех путей в качестве ключей и идентификаторов узлов (! Это означает, что нам нужно еще несколько шагов!) В качестве значений.

      len(ways.keys())
>>> 337597

На следующем (и почти последнем) шаге мы добавляем идентификаторы узлов для всех путей в jsons нашего отношения, чтобы они стали частью файлов:

      all_data = dict()
for relation_file in [
    os.path.join(data_dump_dir,file) for file in os.listdir(data_dump_dir) if file.endswith('.json')
    ]:
    with open(relation_file, 'r') as f:
        data = json.load(f)
    if 'members' in data: # Make sure these steps are never performed twice
        try:
            data['ways'] = dict()
            for way in data['members'][0]:
                data['ways'][way] = ways[way]['nodes']
            del data['members']
            with open(relation_file, 'w') as f:
                json.dump(data, f)
        except KeyError as err:
            print(err, relation_file) # Not sure why some relations give errors?

Итак, теперь у нас есть отношения jsons со всеми путями, и все пути имеют все идентификаторы узлов, последнее, что нужно сделать, это заменить идентификаторы узлов их значениями (широтой и долготой). Я также сделал это в 2 этапа: сначала я создал словарь nodeID:lat/lon, снова используя класс на основе osmium.SimpleHandler:

      import osmium

class CounterHandler(osmium.SimpleHandler):
    def __init__(self):
        osmium.SimpleHandler.__init__(self)
        self.osm_data = dict()

    def node(self, n):
        self.osm_data[int(n.id)] = [n.location.lat, n.location.lon]

Выполните класс:

      h = CounterHandler()
h.apply_file("../data/world_mtb_routes.o5m")
nodes = h.osm_data

Это дает нам dict с парой широта/долгота для каждого идентификатора узла. Мы можем использовать это в наших файлах json, чтобы заполнить пути координатами (где сейчас все еще есть только идентификаторы узлов), я создаю эти окончательные файлы json в новом каталоге (data/with_coords в моем случае), потому что, если есть ошибка, мой исходный (входной) файл json не затронут, и я могу попробовать еще раз:

      import os
relation_files = [file for file in os.listdir('../data/') if file.endswith('.json')]
for relation in relation_files:
    relation_file = os.path.join('../data/',relation)
    relation_file_with_coords = os.path.join('../data/with_coords',relation)
    with open(relation_file, 'r') as f:
        data = json.load(f)
    try:
        for way in data['ways']:
            node_coords_per_way = []
            for node in data['ways'][way]:
                node_coords_per_way.append(nodes[int(node)])
            data['ways'][way] = node_coords_per_way
        with open(relation_file_with_coords, 'w') as f:
            json.dump(data, f)
    except KeyError:
        print(relation)

Теперь у меня есть то, что мне нужно, и я могу начать добавлять информацию в свою базу данных Django, но это выходит за рамки этого вопроса.

Кстати, есть некоторые отношения, которые дают ошибку, я подозреваю, что для некоторых отношений пути были помечены как узлы, но я не уверен. Я обновлю здесь, если узнаю. Я также должен выполнять этот процесс регулярно (когда обновляется файл мира или время от времени), поэтому я, вероятно, напишу что-то более краткое позже, но пока это работает, и шаги понятны для меня, после много хотя бы думать.

Вся сложность связана с тем, что данных недостаточно для памяти, иначе я бы создал pandas.DataFrame на первом этапе и покончил с этим. Я мог бы также загрузить данные в базу данных за один раз, но я пока не очень хорошо разбираюсь в базах данных.

Другие вопросы по тегам