'bulk_upload.py'에 해당되는 글 1건

  1. 2009.07.09 OSM 작업노트 #13: 도로 업로드 작업 2
OSM의 전 세계 모든 데이터를 다 담고 있는 planet.osm 은 일주일에 한번씩 업데이트된다. planet.osm에서 한반도만 뜯어내려면 원래 그럴 용도로 만든 osmosis 프로그램을 사용한다. 2009.7.2 현재 planet.osm 파일의 크기는 무려 160GB, 압축한 파일의 크기가 6.2GB이다.

win32에서 osmosis를 실행하면 십중팔구 안 된다. 이유는 osmosis.bat 파일에 library path가 잘못 지정되어 있기 때문이다(linux에서는 상관없다). osmosis 0.31 버전 기준으로, osmosis가 설치된 디렉토리의 하위 디렉토리에 있는 ./lib/default/*.jar 를 EXEC 환경 변수에 모두 추가하여 osmosis.bat 파일을 만드는 편이 빠르다.
@ECHO OFF
set JAVACMD=java.exe
set JAVACMD_OPTIONS=-Xmx1024m
set MYAPP_HOME=D:\luke\Documents\private\gps\tool\osmosis-0.31
set LIB=%MYAPP_HOME%\lib\default
set MAINCLASS=org.openstreetmap.osmosis.core.Osmosis
SET EXEC=%JAVACMD% %JAVACMD_OPTIONS%
SET EXEC=%EXEC% -cp %MYAPP_HOME%\osmosis.jar;
SET EXEC=%EXEC%%LIB%\bzip2-20090327.jar;
SET EXEC=%EXEC%%LIB%\commons-logging-1.0.4.jar;
SET EXEC=%EXEC%%LIB%\jpf-1.5.jar;
SET EXEC=%EXEC%%LIB%\mysql-connector-java-5.1.6.jar;
SET EXEC=%EXEC%%LIB%\postgis-1.3.2.jar;
SET EXEC=%EXEC%%LIB%\postgresql-8.3-603.jdbc4.jar;
SET EXEC=%EXEC%%LIB%\stax2-api-3.0.1.jar;
SET EXEC=%EXEC%%LIB%\woodstox-core-lgpl-4.0.3.jar
SET EXEC=%EXEC% %MAINCLASS% %OSMOSIS_OPTIONS% %*
%EXEC%
planet.osm 파일이 워낙 커서 보통 배포되는 형태인 planet-090702.osm.bz2 파일을 압축을 풀지 않고 그대로 작업한다. osmosis의 위키 페이지에는 이런 식으로 명령을 지정하라고 적어 놓았다.
osmosis.bat --read-xml file=planet-090701.osm.bz2 --bounding-polygon file=skorea.poly.txt --write-xml file=skorea.osm
osmosis의 bzip2 라이브러리의 버그 때문에 실행 하면 몇 시간 잘 돌다가 exception이 발생하며 프로그램이 중단된다. 오래 전부터 알려진 문제란다. win32용 bzip을 얻어서, 실행파일인 bzip-xxx.exe 파일을 bzcat.exe로 복사해 놓고 다음처럼 실행한다(리눅스에는 bzcat이 기본적으로 설치되어 있다).
bzcat planet-090701.osm.bz2 | osmosis.bat --read-xml file=- --bounding-polygon file=skorea.poly.txt --write-xml file=skorea.osm
osmosis의 입력 파라메터로 지정하는 bounding-polygon 파일 'skorea.poly.txt'는 osmosis 위키 페이지에서 참고하라는 각 국가별 polygon 파일을 봤는데, 좀 괴상하다. 남북한 국경선도 명확하지 않고 울릉도, 독도 등이 빠져 있다. 해서 확실한 국경선은 아니지만, 하나 만들었다. skorea.poly.txt:
south_korea
1
124.50082 33.88033
126.84860 32.67590
128.86031 34.19477
130.32874 35.89549
132.62271 37.16911
128.90661 38.93043
124.46191 37.55735
124.50082 33.88033
END
END
이걸 어떻게 만들었냐 하면... 궁즉통, Google Earth에서 대충 한국을 포함하는 polygon을 그린 다음 그것을 kml로 저장해서  좌표를 텍스트 에디터로 옮겨 적었다. 남한의 국경선 폴리곤이 혹시 있지 않을까 싶지만 구글질해서 성과(?)를 얻는 것보다 직접 아웃라인을 따내는 시간이 훨씬 적게 걸린다. -_-

osmosis로 160GB짜리 파일을 처리하는데 얼마나 많은 시간이 걸리는지 모르겠다. 밤에 작업을 걸어두고 잤다. 2시간 40분쯤 돌다가 exception이 발생하며 bzip2 라이브러리 문제로 프로그램이 죽어버렸다. 파일이 깨진 것 같아 다른 서버에서 다운로드 받아 다시 돌렸다. 3시간 정도 걸려 skorea.osm 파일을 얻을 수 있었다.

skorea.osm 파일을 얻은 다음, 일단 잘못된 저수지 정보를 모두 삭제했다. 삭제는 bulk_upload.py를 수정해서 만든 조그만 python 프로그램으로 돌렸다. osmChange를 이용해 한꺼번에 여러 개의 node,way에 대한 작업을 수행하는 것인데, changeset중 단 하나라도 entry에 문제가 있으면 업데이트가 되지 않는다. 그 때는 changeset을 새로 만드는 방법 밖에 없다. JOSM도 동일한 문제가 있다. 예를 들어 하나의 changeset에 100개의 노드를 삭제하는 delete element가 있다고 하면,
<osmChange>
     <delete>
          <node id="34132450" ... />
          <node id="34132452" .../>
           ...
    </delete>
</osmChange>
34132452번째 노드를 삭제할 때 에러가 발생하면 100개의 노드에 대한 delete 작업 전체가 수행되지 않는다. OSM에서 프로그래밍을 하는 사람들이라면 반드시 알아야 할 내용인데 OSM development 섹션에서는 언급이 안되어 있어 메모해 둔다:

Error:  409 :  Version mismatch: Provided 0, server had: 1 of Way 36898268
삭제하려는 node의 버전은 1이고 서버의 해당 노드의 버전은 2이면 삭제되지 않는다. 삭제하려는 노드를 누군가 다른 사람이 작업해서 버전 번호가 달라진 것이다.

Error:  410 :  The way with the id 35355795 has already been deleted
이미 삭제된 노드로, 이 경우에는 에러로 간주되지 않았으면 좋겠는데 이런 에러 때문에 전체 changeset이 commit되지 않는다.

Error:  412 :  Precondition failed: Node 414576118 is still used by way 36898266
삭제하려는 node를 사용하는 다른 way가 있을 경우 발생하는 에러. 이게 가장 골치아프다. 왜냐하면 삭제하려는 node가 만약 어떤 way에 소속되어 있는데, 이미 그 way가 삭제되었다 하더라도 다른 way에서 이 node를 사용하고 있다면 이 node를 삭제하면 안 되는 것은 맞다. 그런데 해당 node가 어떤 way에 속해있는지 사전에 알 방법이 애매하다. 전 node, way, relation을 메모리에 갖고 있던가 그 정보를 db에 넣어 해당 node가 다른 way에서 점유하고 있는지 조사해 봐야 하는데, 삭제하려는 node가 300000 이고 한 node 조사에 100msec이 걸린다면 삭제대상 노드 조사에만 30000초(약 500분)이 걸린다.

osmChange를 사용하지 않고, changeset을 만든 다음 http DELETE로 삭제하는 고전적인 방법을 사용할 수도 있다. 이 방법을 사용하면 한 node에서 발생한 에러가 다른 node의 삭제에 영향을 끼치지 않는다. 그런데 그 방법은 시간이 오래 걸려 비효율적이다. 개당 500msec이 걸린다면 300000개를 삭제하는데 41시간이 걸린다.

그래서 python 프로그램은 먼저 한 번에 111개를 한 세트로 하는 changeset을 사용해 11개 element를 삭제하고, 삭제가 정상적으로 이루어진 element의 id를 기록해 둔 다음, 다음번에 python 프로그램을 다시 실행해 이번에는 53개씩 삭제, 다음에는 23개, 그 다음에는 11개, 5개, 3개, 2개, 그리고 마지막으로 http DELETE로 1개씩 삭제한다. 갯수는 100, 50, 25, 13, 6, 3, 2, 1에 근접한 소수를 사용해서 이전 changeset이 다음 changeset의 배수가 되어 같은 에러가 동일한 위치에서 발생하지 않도록 한 것.

손이 많이 가지만 이전에 잘못 올린 저수지및 호수 데이터 160000개를 삭제하는데 4시간 가량 걸렸다. 사실 작업시간 보다는 changeset의 특성을 파악하고 적응하는데, 특히, 412 에러를 회피하려고 갖은 꽁수를 써 보는데 시간이 많이 걸렸다. 처음에는 c++로 작업하다가 python 프로그램이 워낙 간단하고 편해서 python으로 바꿨다.

smoothing이 적용된 저수지및 호수 데이터(ncat=저수지및호수2)를 저번 주 금요일 저녁에 올리기 시작했다. 월요일에 정상적으로 올라간 것을 확인했다.

교통정보센터의 수정된 도로를 올리기 전에, 이전에 내 아이디로 만든 highway, trunk, primary, secondary 도로를 삭제하기로 했다. 진행 전에 한국 OSM 작업자들에게 도로 업로드 작업을 이번 주 중에 진행한다는 메시지를 작성하여 전송했다. 욕도 일찍 먹는게 낫다고, 이번에 도로 데이터를 올리지 못하면 일이 점점 더 커질 것이다. 한 두어 달을 올릴지 말지 망설이며(서울, 부산, 대전, 안동이 그 당시에 작업이 꽤 진척된 상태였다. 그 작업을 다시 해야 하니까 작업자들에게 미안해서 어떻게 하면 작업양을 최소화하면서 도로 업로드의 당위성을 설명할까 망설였다) 한가한 고민이나 하던 사이에 홍의님이 전주시와 인근 도로 작업을 상당히 진척시켰다.

도로는 월, 화요일에 모두 삭제했다. 올리려고 하는 도로 데이터의 검토 역시 대충 마무리 되었다. 고속도로는 양방향 lane을 그대로 유지하기로 했다. 수요일 OSM 서버의 업데이트가 있기 전 고속도로를 먼저 올려 JOSM과 Potlatch로 상태를 확인했다. 고속도로만 올리는데 3시간 가량 걸렸다. XAPI가 맛이 가는 바람에 조금이라도 일이 틀어지면 다음에 그것을 교정할 기회는 1주일 후에나 주어진다. 특히나 대량의 업로드의 경우는 문제가 심각하다. 조심조심 작업해야겠다.

화요일 밤부터 국도를 올리기 시작했고 수요일에는 도심로와 지방도 업로드를 시작했다. 다 합쳐 20시간 가량 걸렸다. 프로그램이 수고가 많다. 이전의 bulk_upload.py를 좀 더 개선한 소스. bu.py:

[code] #!/usr/bin/python # -*- coding: utf-8 -*- import time import socket import xml.etree.cElementTree as ET import sets import optparse import pickle import os import sys import httplib2 import re socket.setdefaulttimeout(300.0) headers = { 'User-Agent' : 'BatchUploader/0.1', } api_host='http://api.openstreetmap.org/api/0.6' userid = "#####" passwd = "#####" cachefn = "" demo = False idMap = {} testid = '1' class UploadFailed(Exception): pass class ImportProcessor: def __init__(self): self.user = userid self.password = passwd self.e = ET.Element("create") createReq = ET.Element('osm',version="0.6") change = ET.Element('changeset') createReq.append(change) if demo: self.cid = '1' print 'Create changeset ', self.cid return resp, content = self.request(api_host + '/changeset/create','PUT', ET.tostring(createReq)) if resp.status != 200: print 'Error Request changeset ', resp.status, ': ', content raise UploadFailed(resp.status, content) self.cid = content print 'Create changeset ', content def request(self, url, cmd, xml): con = httplib2.Http() con.add_credentials(userid, passwd) fc = 0; failed = True while failed: try: resp, content = con.request(url, cmd, xml, headers=headers) return resp, content except socket.error, msg: fc += 1 if fc > 5: raise print 'Request Error:', msg time.sleep(2) def add(self, item): item.attrib['changeset'] = self.cid self.e.append(item) def upload(self): xml = ET.Element('osmChange') xml.append(self.e) if demo: return 1 resp, content = self.request(api_host + '/changeset/' + self.cid + '/upload', 'POST', ET.tostring(xml)) if resp.status == 200: # print "Return:", content er = ET.fromstring(content) for child in er.getchildren(): old_id = child.attrib['old_id'] new_id = child.attrib['new_id'] idMap[old_id] = new_id return 1 # 그외의 에러 print 'Error: ', resp.status, ':', content return 0 def closeSet(self): if demo: print "Closing changeset", self.cid, '\n' return resp, content = self.request(api_host + '/changeset/' + self.cid + '/close', 'PUT', "") if resp.status != 200: print "Error closing changeset", self.cid, ":", resp.status def doJob(osmData, etypes, csize): cnt = 0 ip = None for e in osmData.getiterator(): if e.tag not in ( etypes ): continue # 이미 기록한 것은 다시 하지 않음 eid = e.attrib['id'] if eid in idMap: continue if ip == None: ip = ImportProcessor() # way 또는 relation이면 이전에 기록된 id로 기록함. if e.tag == 'way' or e.tag == 'relation': for child in e.getiterator(): if child.attrib.has_key('ref'): old_id = child.attrib['ref'] if idMap.has_key(old_id): child.attrib['ref'] = idMap[old_id] print 'create [', ip.cid, ':', cnt, '] ', e.tag, eid ip.add(e) cnt += 1 if cnt >= csize: ip.upload() f = open(cachefn, "w") pickle.dump(idMap, f) f.close() ip.closeSet() ip = None cnt = 0 if cnt > 0: ip.upload() f = open(cachefn, "w") pickle.dump(idMap, f) f.close() ip.closeSet() usage = "usage: %prog -i input.osm" parser = optparse.OptionParser(usage) parser.add_option("-i", dest="infile", default="", help="read data from input.osm") (options, args) = parser.parse_args() if options.infile == "": print "specify input osm file" raise cachefn = options.infile + ".db" try: if os.stat(cachefn): print 'Read from cache file:', cachefn f = open(cachefn, "r") idMap = pickle.load(f) f.close() except: print "no cache found" osmData = ET.parse(options.infile) st = time.time() doJob(osmData, ("node", "way", "relation" ), 500) print "\nJob done. %.0f" %(time.time() - st), "secs ellapsed" [/code]

OSM 서버의 느린 응답 때문에 에러가 빈번하게 발생하여 프로그램이 자주 멎는다. 프로그램 탓은 아니다. changeset이 완전히 반영되지 않으면 idMap에 기록하지 않도록 하고, 한 번에 commit하는 element의 갯수는 2000개나 5000개가 아닌 500개 정도로 제한해서, 시간이 더 걸리더라도 안정적으로 작동하는데 촛점을 맞췄다.

아울러, 저 것과 같은 기능을 하는 프로그램을 처음에는 VC++ 2005로 작성하려고 했지만 c++로는 작업량이 상당했다. 무엇보다도 expat SAX xml parser 때문에 로직을 짜기가 어렵다. c++에서도 python처럼 쉽게 작성 하려면 XPP(xml pull parser) + DOM을 사용하는게 마땅하다. python 프로그램은 한 시간도 안 걸리는 것을, 그것과 등가한 VC 프로그램을 작성하려면 1-2일쯤 걸릴 것 같다.

수요일 밤에 작업이 끝났다. 업로드에 15시간 정도 걸렸다.  작업 시간을 잘 맞춘 덕에 업로드된 내용이 그 다음날 바로 반영되었다.

서울 부근의 겹치고, 태그가 불분명하게 설정된 도로를 다시 정리하는 작업을 진행 중.

사용자 삽입 이미지
OSM에서 mapnik renderer로 본 지도. mapnik으로는 그간 무슨 삽질을 했는지 알 수가 없어서,

사용자 삽입 이미지
Osmarender로 렌더러를 바꿨다. 저 촘촘한 도로들이 지난 한 달 동안 작업한 것. 이것으로 한국 지도 작업을 진행하기 위한 기본 뼈대가 어느 정도 갖춰진 셈이다. 갈 길은 여전히 멀다...

,