矢量地理数据导入PostgreSQL
前言
前一段时间,用Flask写了一个Web应用,需要用到一些地理空间的数据,为了更有效地管(zhuang)理(bi)起来,就想用前面提到过的PostgreSQL数据库,这里我们简单介绍一下,如何用Python的相关库,将常见的矢量数据(shp)导入到PostgreSQL中。
前期准备
项目描述
我们将从美国地质调查局(USGS)下载shapefiles,将其导入到PostgreSQL数据库中,能够使用GeoAlchemy 2 ORM 执行地理空间查询,使用SQLAlchemy执行多表关联查询,并在前段显示查询结果。
相关Python库
我们用到的相关库包括:
- Flask,一个纯Python的MVC Web框架(http://flask.pocoo.org/ );
- Flask-SQLAlchemy, ORM框架SQLAlchemy的Flask扩展,可以连接到多个数据库后端(http://flask-sqlalchemy.pocoo.org/ );
- GeoAlchemy2,SQLAlchemy在空间数据库的扩展,用于连接PostgreSQL/PostGIS后端(https://geoalchemy-2.readthedocs.io/en/latest/ );
- SQLAlchemy-Utils,对SQLAlchemy功能的扩展(https://github.com/kvesteri/sqlalchemy-utils/ );
- psycopg2, 用于连接PostgreSQL数据库(http://initd.org/psycopg/ );
- pyshapefile(pyshp),用于读取shapefile(https://pypi.python.org/pypi/pyshp );
- pygeoif, 从shapefile二进制文件转换数据编码为一个WKT编码,用于几何数据插入数据库(https://pypi.python.org/pypi/pygeoif )。
此外,在使用Flask搭建Web应用的时候还会用到其他的库,例如Jinja2模板、Werkzeug WSGI模块等,此处不在赘述。
下载数据
从USGS的网站上可以下载许多数据,我们这次用到四种数据:
- 美国各州边界(US States);
- 美国各县边界(US County Boundaries);
- 美国国会选区(Congressional Districts);
- NBA球场位置(NBA Arenas)。
具体的下载地址见 https://www.sciencebase.gov/catalog/item/4f4e4a2ee4b07f02db615738
创建数据库
我们使用SQLAlchemy和GeoAlchemy2创建数据库和数据表来存储Web应用用到的数据。
首先导入需要用的模块和方法:
1 | from sqlalchemy import create_engine |
构造连接的字符串,格式如下:
conn_string = '{DBtype}://{user}:{password}@{instancehost}:{port}/{database}'
在创建PostgreSQL数据库之后,我们需要为新创建的数据库添加postgis扩展:
1 | conn_string = 'postgresql://postgres:postgres@localhost:5432/nba_arenas' |
定义数据库表
根据MVC的架构模式,数据库(表)属于模型:模型(Model) 用于封装与应用程序的业务逻辑相关的数据以及对数据的处理方法。“ Model ”有对数据直接访问的权力,例如对数据库的访问。“Model”不依赖“View”和“Controller”。
在Python中,我们通过继承已经预先封装了大部分数据库管理功能的超类编写新的类来定义数据表。通过封装我们不需要关心模型的实现代码就可以实现不同类型RDBMS的管理。SQLAlchemy模型可以用于产生包括SQL Server、Oracle、Postgres SQL和MySQL等不同的数据库表,而GeoAlchemy2则仅适用于PostgreSQL/PostGIS。
1 | # For SQLAlchemy database classes, declarative_base |
通过继承基类,我们可以创建不同表的模型类,通过这些模型类分别建立相应的数据库表。其中__tablename__
属性定义了数据库表的名称,通过关键字primary_key
定义表的主键。
对于空间数据而言,我们还需要用srid
定义几何形状(Geometry)的空间参考坐标系,例如srid=4326
代表我们使用的是WSG 1984坐标系。
对于具有关系的表,例如County
、District
和Sate
,我们用专门的类和方法来管理表的关系和多表查询。例如,ForeginKey
类为子表提供外键,relationship
实现连表查询,backref
是反向关联。
1 | metadata.clear() |
传统的方法,是在父类中定义一个关系relationship或叫正向引用 Reference,子类只需定义一个外键。单纯定义的relationship(‘子类名’)只是一个正向引用,也就是只能让父类调用子对象。反过来,如果要问children他们的父亲是谁,就不行了。所以,我们还需要一个反向引用 (Back Reference)的声明,让子对象能够知道父对象是谁。
在上面,我们用relationship
建立了两个表的关联,并且用backref
实现了反向关联,以
state_id = Column(Integer, ForeignKey('state.id')) counties = relationship('County', backref='state')
为例, state为主表,county为从表:
- 查询state表,返回state_obj,可以通过state_obj.counties查询到county表中外键关联的数据,查主表返回从表里的数据,这个称之为“正向查询”。
- 查询county表,返回county_obj,可以通过county_obj.state查询到外键关联的state表中数据。查从表返回主表里的数据,这个称之为:“反向查询”。
创建数据库表
我们可以用两种方法来创建数据库表:
- 一种是调用模型类的
__table__
方法的create()
函数,分别创建每一个表; - 另一种是调用基类Base的
metadata
方法的create_all()
方法,一次性创建所有继承自Base的表。
1 | # Generate the District table from the District class. |
导入数据
利用pgAdmin查看PostgreSQL数据库可以看到,我们已经成功创建了数据库及所需要用的数据表,接下来我们就将下载好的数据写入到数据库中。
导入模块
为了从shapefile中读取数据,我们需要用到pyshp
和pygeoif
模块,其中pyshp用来读取shapefile中的空间(属性)数据,pygeoif模块则是将地理空间数据转换为Python对象,我们将用它将shapefile中的二进制数据转换为可以被GeoAlchemy2处理的WKT格式。
1 | # The pyshapefile module is used to read shapefiles and |
读取数据以后,我们还要用SQLAlchemy
和GeoAlchemy2
模块将数据导入到数据库中,除了前面导入的模块以外,我们还需要导入一些新的模块:
1 | from sqlalchemy.orm import sessionmaker |
使用tkinter提供GUI:
1 | # The built-in Tkinter GUI module allows for file dialogs |
我们创建一个session,与engine绑定,用来管理查询和写入。
1 | Session = sessionmaker(bind=engine) |
加载shp数据
打开shapefiles
1 | # Read the Arena shapefile using the Reader class of the pyshp module |
shapefile Reader
29 shapes (type 'POINT')
29 records (30 fields)
对county、district和state执行类似的操作,不再重复。
写入数据库
在这里我们通过循环将shapefile中的每条数据顺序读取出来,对于几何属性数据,采用WKT的格式保存:
- 对于简单的数据(POINT),我们手动构造WKT;
- 对于复杂的数据(POLYGON),使用
pygeoif
先转换为pygeofi MultiPolygon格式,在转换为WKT。
下面是部分代码作为示例:
1 | # Iterate through the Arena data read from the shapefile |
结果测试
1 | engine = create_engine(conn_string) |
简单查询
我们可以从数据库中查询所有的体育馆信息,然后将名字和经纬度存储到pandas的DataFrame中。
1 | import pandas as pd |
1 | arenas = session.query(Arena).all() |
id | name | lon | lat | |
---|---|---|---|---|
0 | 1 | American Airlines Arena | -80.187580 | 25.781170 |
1 | 2 | ARCO Arena | -121.518690 | 38.649320 |
2 | 3 | EnergySolutions Arena | -111.901025 | 40.768206 |
3 | 4 | Fed Ex Forum | -90.051300 | 35.138100 |
4 | 5 | Quicken Loans Arena | -81.688700 | 41.496480 |
空间查询
既然是空间数据,而且我们也为PostgreSQL添加了PostGIS扩展,所以我们测试一下空间查询的结果,在这里我们分别测试空间包含(ST_Contains)和空间相交(ST_Intersects),为了实现这一查询,我们用到了SQLAlchemy的filter
方法和GeoAlchemy2 ORM定义的Geometry
列。
1 | arena = arenas[0] |
Miami-Dade
Alcee L. Hastings
关联查询
前面,我们以state为主表、county/district为从表,建立了关联,下面我们可以看一下如何进行关联查询。我们使用“state”作为backref的参数,所以可以通过{county_obj}.state
实现逆向查询。而正向查询则更为简单,直接调用{state_obj}.counties
即可。
1 | state = county.state |
Florida
['Hardee', 'Okeechobee', 'Hillsborough', 'Indian River', 'St. Lucie', 'Manatee', 'DeSoto', 'Glades', 'Martin', 'Miami-Dade', 'Lake', 'Sumter', 'Seminole', 'Volusia', 'Orange', 'Citrus', 'Hernando', 'Polk', 'Osceola', 'Pasco', 'Brevard', 'Pinellas', 'Highlands', 'Hendry', 'Sarasota', 'Charlotte', 'Lee', 'Palm Beach', 'Broward', 'Collier', 'Monroe', 'Jackson', 'Holmes', 'Washington', 'Gadsden', 'Madison', 'Hamilton', 'Calhoun', 'Liberty', 'Columbia', 'Baker', 'Nassau', 'Suwannee', 'Okaloosa', 'Walton', 'Santa Rosa', 'Escambia', 'Leon', 'Duval', 'Lafayette', 'Bradford', 'Union', 'Jefferson', 'Clay', 'Wakulla', 'Bay', 'Alachua', 'Gilchrist', 'Gulf', 'St. Johns', 'Taylor', 'Franklin', 'Marion', 'Flagler', 'Putnam', 'Dixie', 'Levy']
小结
我们简单介绍了从shapefile中读取数据,然后存储到PostgreSQL中,并且可以实现空间查询和多表关联查询,可以看到只要了解空间数据的结构,同样可以将其他格式的数据例如GeoJSON,存储到PosttgreSQL中。