Как извлечь члены отношения из 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 на первом этапе и покончил с этим. Я мог бы также загрузить данные в базу данных за один раз, но я пока не очень хорошо разбираюсь в базах данных.