国土数値情報の鉄道データを活用してみる

##目的
国土数値情報　ダウンロードサービスでは、国土交通省が管理するデータが取得できます。
今回は、鉄道データを用いて、その座標をGoogleMapにプロットしてみます。


 **デモ：** 
http://needtec.sakura.ne.jp/railway_location/railway

 **GIT：** 
https://github.com/mima3/railway_location

#データについて
鉄道データは下記のページからダウンロードできます。

 **国土数値情報　鉄道データ** 
http://nlftp.mlit.go.jp/ksj/gml/datalist/KsjTmplt-N02-v2_2.html


ダウンロードしたファイル中のXMLの使用については下記を参考にしてください。
http://nlftp.mlit.go.jp/ksj/gml/product_spec/KS-PS-N02-v2_1.pdf

簡単に説明すると、鉄道データには、路線の形を示す情報と、駅情報が格納されています。
ここで、重要な要素は以下の通りです。
・gml:Curve　曲線の情報
・ksj:railroadSection 鉄道区間の情報
・ksj:Station 駅情報

Curveには座標情報が格納されています。
railroadSectionとStationのlocation要素にて、Curveへのリンクが格納されています。

##データベースへの格納
大量のデータをXMLで取り扱うのは困難なので、一旦リレーショナルデータベースに格納しています。

この時、大きなサイズのXMLを解析しますが、XMLファイル全体を一旦、メモリに格納してパースするというやりかたをすると、メモリ使用量が激増し、処理しきれなくなります。
そのため、lxml.etree.iterparseを用いて、順次処理するようにします。

しかしながら、N02-XX.xmlをlxml.etree.itreparseにて解析するとエラーが発生します。
これは、XML中に以下のような行があるからです。

```
 xmlns:schemaLocation="http://nlftp.mlit.go.jp/ksj/schemas/ksj-app KsjAppSchema-N02-v2_0.xsd">
```

lxmlは、ここで指定しているURIを不正なURIとみなし、エラーを出力します。
これを回避するにはlxmlにて、XMLを解析する際に、recover=Trueを指定する必要があります。
http://stackoverflow.com/questions/18692965/how-do-i-skip-validating-the-uri-in-lxml

 **回避例：** 

```py
        context = etree.iterparse(
            xml,
            events=('end',),
            tag='{http://www.opengis.net/gml/3.2}Curve',
            recover=True
        )
```

iterparseにおける、この引数は、lxml==3.4.1以降に導入されているため、バージョンを指定してlxmlをインストールする必要があります。

```
easy_install lxml==3.4.1
```

以上のことを踏まえて、鉄道データのXMLをデータベースにインポートする処理は以下のようになります。


```py:railway_db.py
# -*- coding: utf-8 -*-
import sqlite3
import sys
import os
# easy_install lxml==3.4.1
from lxml import etree
from peewee import *

database_proxy = Proxy()
database = None


class BaseModel(Model):
    """
    モデルクラスのベース
    """
    class Meta:
        database = database_proxy


class Curve(BaseModel):
    """
    曲線情報モデル
    """
    curve_id = CharField(index=True, unique=False)
    lat = DoubleField()
    lng = DoubleField()


class RailRoadSection(BaseModel):
    """
    鉄道区間情報モデル
    """
    gml_id = CharField(primary_key=True)
    # 外部キーは主キーまたは一意制約をもっていないといけないので、
    # 複数あるデータに関しては外部キーとしては指定できない。
    location = CharField(index=True)
    railway_type = IntegerField()
    service_provider_type = IntegerField()
    railway_line_name = CharField(index=True)
    operation_company = CharField(index=True)


class Station(BaseModel):
    """
    駅情報モデル
    """
    gml_id = CharField(primary_key=True)
    # 外部キーは主キーまたは一意制約をもっていないといけないので、
    # 複数あるデータに関しては外部キーとしては指定できない。
    location = CharField(index=True)
    railway_type = IntegerField()
    service_provider_type = IntegerField()
    railway_line_name = CharField(index=True)
    operation_company = CharField(index=True)
    station_name = CharField(index=True)
    railroad_section = ForeignKeyField(
        db_column='railroad_section_id',
        rel_model=RailRoadSection,
        to_field='gml_id',
        index=True
    )


def setup(path):
    """
    データベースのセットアップ
    @param path データベースのパス
    """
    global database
    database = SqliteDatabase(path)
    database_proxy.initialize(database)
    database.create_tables([Curve, RailRoadSection, Station], True)


def import_railway(xml):
    """
    国土数値院のN02-XX.xmlから路線と駅情報をインポートする
    TODO:
      外部キーがらみのインポートの効率がわるい
    @param xml XMLのパス
    """
    commit_cnt = 2000  # ここで指定した数毎INSERTする
    f = None
    contents = None
    namespaces = {
        'ksj': 'http://nlftp.mlit.go.jp/ksj/schemas/ksj-app',
        'gml': 'http://www.opengis.net/gml/3.2',
        'xlink': 'http://www.w3.org/1999/xlink',
        'xsi': 'http://www.w3.org/2001/XMLSchema-instance'
    }

    with database.transaction():
        insert_buff = []
        context = etree.iterparse(
            xml,
            events=('end',),
            tag='{http://www.opengis.net/gml/3.2}Curve',
            recover=True
        )
        for event, curve in context:
            curveId = curve.get('{http://www.opengis.net/gml/3.2}id')
            print (curveId)
            posLists = curve.xpath('.//gml:posList', namespaces=namespaces)
            for posList in posLists:
                points = posList.text.split("\n")
                for point in points:
                    pt = point.strip().split(' ')
                    if len(pt) != 2:
                        continue
                    insert_buff.append({
                        'curve_id': curveId,
                        'lat': float(pt[0]),
                        'lng': float(pt[1])
                    })
                    if len(insert_buff) >= commit_cnt:
                        Curve.insert_many(insert_buff).execute()
                        insert_buff = []
        if len(insert_buff):
            Curve.insert_many(insert_buff).execute()
        insert_buff = []
        context = etree.iterparse(
            xml,
            events=('end',),
            tag='{http://nlftp.mlit.go.jp/ksj/schemas/ksj-app}RailroadSection',
            recover=True
        )
        for event, railroad in context:
            railroadSectionId = railroad.get(
                '{http://www.opengis.net/gml/3.2}id'
            )
            locationId = railroad.find(
                'ksj:location',
                namespaces=namespaces
            ).get('{http://www.w3.org/1999/xlink}href')[1:]
            railwayType = railroad.find(
                'ksj:railwayType', namespaces=namespaces
            ).text
            serviceProviderType = railroad.find(
                'ksj:serviceProviderType',
                namespaces=namespaces
            ).text
            railwayLineName = railroad.find(
                'ksj:railwayLineName',
                namespaces=namespaces
            ).text
            operationCompany = railroad.find(
                'ksj:operationCompany',
                namespaces=namespaces
            ).text
            insert_buff.append({
                'gml_id': railroadSectionId,
                'location': locationId,
                'railway_type': railwayType,
                'service_provider_type': serviceProviderType,
                'railway_line_name': railwayLineName,
                'operation_company': operationCompany
            })
            print (railroadSectionId)
            if len(insert_buff) >= commit_cnt:
                RailRoadSection.insert_many(insert_buff).execute()
                insert_buff = []
        if len(insert_buff):
            RailRoadSection.insert_many(insert_buff).execute()

        insert_buff = []
        context = etree.iterparse(
            xml,
            events=('end',),
            tag='{http://nlftp.mlit.go.jp/ksj/schemas/ksj-app}Station',
            recover=True
        )
        for event, railroad in context:
            stationId = railroad.get('{http://www.opengis.net/gml/3.2}id')
            locationId = railroad.find(
                'ksj:location', namespaces=namespaces
            ).get('{http://www.w3.org/1999/xlink}href')[1:]
            railwayType = railroad.find(
                'ksj:railwayType',
                namespaces=namespaces
            ).text
            serviceProviderType = railroad.find(
                'ksj:serviceProviderType',
                namespaces=namespaces
            ).text
            railwayLineName = railroad.find(
                'ksj:railwayLineName',
                namespaces=namespaces
            ).text
            operationCompany = railroad.find(
                'ksj:operationCompany',
                namespaces=namespaces
            ).text
            stationName = railroad.find(
                'ksj:stationName',
                namespaces=namespaces
            ).text
            railroadSection = railroad.find(
                'ksj:railroadSection',
                namespaces=namespaces
            ).get('{http://www.w3.org/1999/xlink}href')[1:]
            print (stationId)
            insert_buff.append({
                'gml_id': stationId,
                'location': locationId,
                'railway_type': railwayType,
                'service_provider_type': serviceProviderType,
                'railway_line_name': railwayLineName,
                'operation_company': operationCompany,
                'station_name': stationName,
                'railroad_section': RailRoadSection.get(
                    RailRoadSection.gml_id == railroadSection
                )
            })
            if len(insert_buff) >= commit_cnt:
                Station.insert_many(insert_buff).execute()
                insert_buff = []
        if len(insert_buff):
            Station.insert_many(insert_buff).execute()

```

データベースに格納してしまえば、あとは簡単に活用できます。

##使用上の注意
国土数値情報（鉄道データ）を取り扱う場合に気付いた点を以下に記述します。

・路線名だけでは絞りこめない。
　たとえば、「１号線」といった場合に、「横浜市」が保持する場合と、「千葉モノレール」が保持する場合がある。
　そのため、「運行会社」と「路線名」で絞りこむ必要がある。

・いつも利用している名称とことなる名称の場合がある。
　JR東日本は東日本旅客鉄道になるし、東京メトロは東京地下鉄になっている。

・いつも利用している路線と違う路線になっている場合がある。
　たとえば、通常の路線図では「東京」は「中央線」に含まれている。
　しかし、国土数値情報としては「東京」は「中央線」に含まれていない。
　「東京」～「神田」は「東北線」とみなされる。
　これは、東京駅 - 神田駅間は東北本線上に敷設された専用線路を走行しているためだと思われる。