Python-GIS-脚本编程-全-
Python GIS 脚本编程(全)
原文:
zh.annas-archive.org/md5/14eca96eb97dc47d28f8de5385dca806译者:飞龙
前言
随着时间的推移,Python 已经成为空间分析的首选编程语言,产生了许多用于读取、转换、分析和可视化空间数据的包。在如此多的包可用的情况下,为学生和经验丰富的专业人士创建一本包含 Python 3 必需地理空间 Python 库的参考书是有意义的。
这本书也出现在一个激动人心的时刻:新技术正在改变人们处理地理空间数据的方式——物联网、机器学习和数据科学是地理空间数据不断被使用的领域。这解释了为什么包含新的 Python 库,如 CARTOframes 和 MapboxGL,以及 Jupyter 也被包括在内,以探索这些新趋势。同时,基于网页和云的 GIS 正在越来越多地成为新的标准。这在本书第二部分章节中得到了体现,其中介绍了交互式地理空间网络地图和 REST API。
这些较新的库与许多在多年中变得至关重要的旧库相结合,至今仍然非常受欢迎,如 Shapely、Rasterio 和 GeoPandas。对于新进入这个领域的人来说,将给出对流行库的适当介绍,将它们置于适当的视角,并通过使用真实世界数据的代码示例比较它们的语法。
最后,这本书标志着从 Python 2 到 3.x 的过渡。本书涵盖的所有库都是用 Python 3.x 编写的,以便读者可以使用 Jupyter Notebook 访问它们,这也是本书推荐的 Python 编码环境。
本书面向的对象
这本书适合任何与位置信息以及 Python 工作的人。学生、开发人员和地理空间专业人士都可以使用这本参考书,因为它涵盖了 GIS 数据管理、分析技术和用 Python 3 构建的代码库。
本书涵盖的内容
第一章,包安装和管理,解释了如何安装和管理本书中使用的代码库。
第二章,地理空间代码库简介,涵盖了用于处理和分析地理空间数据的主要代码库。
第三章,地理空间数据库简介,介绍了用于数据存储和分析的地理空间数据库。
第四章,数据类型、存储和转换,专注于 GIS 中存在的许多不同数据类型(矢量数据类型和栅格数据类型)。
第五章,矢量数据分析,涵盖了 Shapely、OGR 和 GeoPandas 等 Python 库,这些库用于分析和处理矢量数据。
第六章,栅格数据处理,探讨了使用 GDAL 和 Rasterio 处理栅格数据集以执行地理空间分析。
第七章,使用地理数据库进行地理处理,向读者展示了如何使用包含空间列的数据库表执行空间 SQL 地理处理。
第八章,自动化 QGIS 分析,教导读者如何使用 PyQGIS 在 QGIS 映射套件内自动化分析。
第九章,ArcGIS API for Python 和 ArcGIS Online,介绍了 ArcGIS API for Python,它允许用户使用 Python 3 与 Esri 的云平台 ArcGIS Online 进行交互。
第十章,使用 GPU 数据库进行地理处理,介绍了使用 Python 工具与基于云的数据交互以搜索和处理数据。
第十一章,Flask 和 GeoAlchemy2,描述了如何使用 Flask Python 网络框架和 GeoAlchemy ORM 来执行空间数据查询。
第十二章,GeoDjango,介绍了使用 Django Python 网络框架和 GeoDjango ORM 来执行空间数据查询。
第十三章,地理空间 REST API,教导读者如何为地理空间数据创建 REST API。
第十四章,云地理数据库分析和可视化,向读者介绍了 CARTOframes Python 包,使 Carto 地图、分析和数据服务能够集成到数据科学工作流程中。
第十五章,自动化云制图,介绍了 Jupyter Notebooks 的新位置数据可视化库。
第十六章,使用 Hadoop 进行 Python 地理处理,解释了如何使用分布式服务器进行地理空间分析。
为了充分利用本书
由于本书涵盖 Python,因此假设读者对 Python 语言有基本的了解,可以安装 Python 库,并且知道如何编写和运行 Python 脚本。至于额外的知识,前六章可以很容易地理解,无需任何地理空间数据分析的先验知识。然而,后面的章节假设读者对空间数据库、大数据平台、数据科学、Web API 和 Python 网络框架有一定的了解。
下载示例代码文件
您可以从 www.packtpub.com 的账户下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。
按照以下步骤下载代码文件:
-
在 www.packtpub.com 登录或注册。
-
选择 SUPPORT 选项卡。
-
点击代码下载与勘误。
-
在搜索框中输入本书的名称,并遵循屏幕上的说明。
文件下载后,请确保使用最新版本解压缩或提取文件夹:
-
Windows 版的 WinRAR/7-Zip
-
Mac 版的 Zipeg/iZip/UnRarX
-
Linux 版的 7-Zip/PeaZip
本书代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Mastering-Geospatial-Analysis-with-Python。如果代码有更新,它将在现有的 GitHub 仓库中更新。
我们还有其他来自我们丰富图书和视频目录的代码包可供使用,请访问github.com/PacktPublishing/。查看它们吧!
下载彩色图像
我们还提供了一份包含本书中使用的截图/图表的彩色图像的 PDF 文件。你可以从这里下载:www.packtpub.com/sites/default/files/downloads/MasteringGeospatialAnalysiswithPython_ColorImages.pdf。
使用的约定
本书使用了多种文本约定。
CodeInText: 表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“选择一个文件夹并保存密钥,现在它将有一个.ppk文件扩展名。”
代码块设置如下:
cursor.execute("SELECT * from art_pieces")
data=cursor.fetchall()
data
当我们希望将你的注意力引到代码块的一个特定部分时,相关的行或项目将被设置为粗体:
from pymapd import connect
connection = connect(user="mapd", password= "{password}",
host="{my.host.com}", dbname="mapd")
cursor = connection.cursor()
sql_statement = """SELECT name FROM county;"""
cursor.execute(sql_statement)
任何命令行输入或输出都按照以下方式编写:
conda install -c conda-forge geos
粗体: 表示新术语、重要单词或你在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“要从 EC2 仪表板生成密钥对,请在向下滚动后从左侧面板的 NETWORK & SECURITY 组中选择 Key Pairs。”
警告或重要提示看起来像这样。
技巧和窍门看起来像这样。
联系我们
我们欢迎读者的反馈。
一般反馈: 请通过feedback@packtpub.com发送邮件,并在邮件主题中提及书名。如果你对本书的任何方面有疑问,请通过questions@packtpub.com发送邮件给我们。
勘误: 尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果你在这本书中发现了错误,我们将不胜感激,如果你能向我们报告这个错误。请访问www.packtpub.com/submit-errata,选择你的书,点击勘误提交表单链接,并输入详细信息。
盗版: 如果你在互联网上以任何形式遇到我们作品的非法副本,如果你能提供给我们地址或网站名称,我们将不胜感激。请通过copyright@packtpub.com与我们联系,并附上材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com。
评论
请留下评论。一旦您阅读并使用了这本书,为何不在您购买它的网站上留下评论呢?潜在读者可以查看并使用您的客观意见来做出购买决定,我们 Packt 可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!
想了解更多关于 Packt 的信息,请访问 packtpub.com。
第一章:软件包安装和管理
本书专注于 Python 3 地理空间数据管理和分析的重要代码库。原因很简单——随着 Python 2 接近生命周期的尾声,它正迅速被 Python 3 所取代。这个新的 Python 版本在组织和语法上存在关键差异,这意味着开发者需要调整他们的遗留代码并在代码中应用新的语法。机器学习、数据科学和大数据等领域已经改变了今天地理空间数据的管理、分析和展示方式。在这些所有领域,Python 3 迅速成为新的标准,这也是地理空间社区开始使用 Python 3 的另一个原因。
地理空间社区长期以来一直依赖 Python 2,因为许多依赖项在 Python 3 中不可用或无法正确工作。但现在 Python 3 已经成熟稳定,地理空间社区已经利用了其功能,从而产生了许多新的库和工具。本书旨在帮助开发者理解用 Python 3 编写的地理空间程序的开放源代码和商业模块,提供了一系列主要的地理空间库和工具,用于地理空间数据管理和数据分析。
本章将解释如何安装和管理本书中使用的代码库。它将涵盖以下主题:
-
安装 Anaconda
-
使用 Anaconda Navigator、Anaconda Cloud、
conda和pip管理 Python 包 -
使用 Anaconda、
conda和virtualenv管理虚拟环境 -
运行 Jupyter Notebook
介绍 Anaconda
Anaconda 是 Python 编程语言的免费增值开源发行版,用于大规模数据处理、预测分析和科学计算,旨在简化包管理和部署。它也是世界上最受欢迎的 Python 数据科学平台,拥有超过 450 万用户和 1000 个数据科学包。它不应与conda混淆,conda是一个与 Anaconda 一起安装的包管理器。
对于本书,我们建议安装并使用 Anaconda,因为它为您提供了一切所需——Python 本身、Python 库、管理这些库的工具、Python 环境管理器以及 Jupyter Notebook 应用程序来编写、编辑和运行您的代码。您也可以选择使用 Anaconda 的替代品或通过www.python.org/downloads安装 Python,并使用您选择的任何 IDE 结合包管理器,如pip(在进一步讨论中涵盖)。我们建议使用 Python 版本 3.6。
使用 Anaconda 安装 Python
Continuum Analytics 的主页上提供了适用于 Windows、macOS 和 Linux 的 Anaconda 最新版本的免费下载。在撰写本文时,最新版本是 Anaconda 5.0.1,于 2017 年 10 月发布,提供 32 位和 64 位版本,可在 www.continuum.io/downloads 获取。此页面还提供了针对每个操作系统的详细下载说明、一个 30 分钟的教程,解释如何使用 Anaconda、一个入门指南,以及一个常见问题解答部分。还有一个名为 Miniconda 的 Anaconda 瘦身版,它仅安装 Python 和 conda 包管理器,不包括 Anaconda 标准安装中包含的 1000 多个软件包:conda.io/miniconda.html。如果您决定使用它,请确保下载 Python 3.6 版本。
Anaconda 将将 Python 3.6.2 作为默认的 Python 版本安装到您的机器上。本书所有章节中使用的 Python 版本是 Python 3.6,所以任何以 3.6 或更高版本开始的版本都适用。使用 Anaconda,您将获得 1000 多个 Python 包,以及一些应用程序,例如 Jupyter Notebook,以及各种 Python 控制台和 IDE。
请注意,您在安装后并不强制始终使用 Python 3.6 版本——使用 Anaconda Navigator(一个用于管理本地环境和安装包的 GUI),您还可以选择在虚拟环境中使用 Python 3.5 或 2.7。这为您在切换不同项目之间的不同 Python 版本提供了更多灵活性。
要开始安装,根据您的系统能力下载 32 位或 64 位 Anaconda 安装程序。打开安装程序,按照设置指南在您的本地系统上安装 Anaconda。
运行 Jupyter 笔记本
Jupyter 笔记本是 一个新颖的想法,已被许多公司(包括 Esri 和新的 ArcGIS API for Python)采用。由 Jupyter 项目管理,这是一个开源项目(基于 IPython,一个早期的交互式代码环境),是学习和生产环境中的绝佳工具。虽然代码也可以像其他章节中看到的那样作为脚本运行,但使用 Jupyter 笔记本会让编码变得更加有趣。
代码笔记本的想法是使编码交互式。通过将 Python 终端与代码运行产生的直接输出相结合,这些可保存的笔记本成为分享和比较代码的工具。每个部分都可以稍后编辑,或保存为用于演示目的的单独组件。
在此处查看 Jupyter 笔记本的文档:
运行笔记本
要启动为笔记本提供动力的本地服务器,激活虚拟环境并传递 jupyter notebook 命令:
C:\PythonGeospatial3>cartoenv\Scripts\activate
(cartoenv) C:\PythonGeospatial3>jupyter notebook
[I 17:30:46.338 NotebookApp] Serving notebooks from local directory: C:\PythonGeospatial3
[I 17:30:46.338 NotebookApp] 0 active kernels
[I 17:30:46.339 NotebookApp] The Jupyter Notebook is running at:
[I 17:30:46.339 NotebookApp] http://localhost:8888/?token=5376ed8c704d0ead295a3c0464e52664e367094a9e74f70e
[I 17:30:46.339 NotebookApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).
[C 17:30:46.344 NotebookApp]
Copy/paste this URL into your browser when you connect for the first time,
to login with a token:
http://localhost:8888/?token=5376ed8c704d0ead295a3c0464e52664e367094a9e74f70e
[I 17:30:46.450 NotebookApp] Accepting one-time-token-authenticated connection from ::1
[I 17:30:49.490 NotebookApp] Kernel started: 802159ef-3215-4b23-b77f-4715e574f09b
[I 17:30:50.532 NotebookApp] Adapting to protocol v5.1 for kernel 802159ef-3215-4b23-b77f-4715e574f09b
这将启动一个服务器,该服务器将为笔记本提供动力。这个本地服务器可以通过端口 8888 使用浏览器访问,通过导航到:http://localhost:8888。启动时应自动打开一个类似于这样的标签页:

如果你注销了,请使用在将 jupyter notebook 命令传递时生成的文本中提供的令牌来重新登录,如下例所示:
http://localhost:8888/?token=5376ed8c704d0ead295a3c0464e52664e367094a9e74f70e
创建新的笔记本
要创建一个新的笔记本,请点击右上角的“新建”按钮,并在笔记本部分选择“Python 3”。它将在新标签页中打开笔记本:

添加代码
在 Jupyter 笔记本中,代码是在“输入”部分添加的。代码可以逐行添加,因为代码变量和导入的模块将被保存在内存中,或者可以以块/多行的方式添加,就像脚本一样。可以编辑和运行“输入”部分多次,或者可以将其保留不变,然后开始一个新的部分。这会创建脚本努力的记录,以及交互式输出的记录。
这里有一个 GIST,解释了 Jupyter 笔记本中许多有用的快捷键:
gist.github.com/kidpixo/f4318f8c8143adee5b40
管理 Python 包
安装 Anaconda 后,是时候讨论如何管理不同的 Python 包了。Anaconda 提供了几个选项来完成这项任务——Anaconda Navigator、Anaconda Cloud 和 conda 包管理器。
使用 Anaconda Navigator 管理包
安装 Anaconda 后,你将注意到一个包含各种应用程序的工作文件夹。其中之一是 Anaconda Navigator,它提供了一个 图形用户界面(GUI)。你可以将其与 Windows 文件资源管理器进行比较,即一个用于管理项目、包和环境的平台。术语 环境 指的是一组包和 Python 安装。请注意,这与使用 virtualenv 的方式类似,但这次是使用图形用户界面而不是命令提示符来创建它(virtualenv 在本章后面将更详细地介绍)。
打开 Anaconda Navigator 后,点击屏幕左侧的环境标签,Anaconda Navigator 将提供现有环境和其中包含的包的概览。有一个预定义的环境可供使用,这是一个所谓的根环境,它为您提供了 150 多个预安装的 Python 包。可以通过点击屏幕底部的创建按钮来创建新的环境。这将自动安装五个默认的 Python 包,包括 pip,这意味着您也可以自由地使用它进行包管理。Anaconda Navigator 的有趣之处在于,对于每个新的环境,您都可以选择一个首选的 Python 版本,如果您安装的是默认的 Anaconda 版本而不是 Miniconda,您可以从 1000 多个本地可用的包中进行选择。此列表可通过选择通道按钮旁边的下拉菜单中的“未安装”选项来获取。您可以通过使用搜索包字段并按 Enter 键来轻松搜索和选择您选择的包。标记包并为您选择的 环境 安装它们。安装后,包将以名称的形式列在环境中。如果您点击包名称旁边的带勾选标记的绿色框,您可以选择标记一个包进行升级、删除或特定版本安装。
安装完包后,您可以通过打开终端、Jupyter Notebook 或其他 Anaconda 应用程序来开始使用环境,只需在您选择的环境中的箭头按钮上单击一次鼠标即可。如果您希望使用 IDE 而不是 Anaconda Navigator 提供的选项之一,请确保将您的 IDE 重定向到 Anaconda 使用的正确 python.exe 文件。此文件通常位于以下路径,这是 Anaconda 的默认安装路径:
C:\Users\<UserName>\Anaconda3\python.exe。
使用 Anaconda Cloud 在线搜索包
如果您正在寻找的 Python 包不在本地可用的 Python 包列表中,您可以使用 Anaconda Cloud。此应用程序也是 Anaconda3 的一部分,您可以使用 Anaconda Cloud 应用程序与他人共享包、Notebooks 和环境。在点击 Anaconda Cloud 桌面图标后,将打开一个网页,您可以在其中注册成为注册用户。Anaconda Cloud 与 GitHub 类似,因为它允许您为您的个人工作创建一个私有在线仓库。这些仓库被称为 channels。
如果您创建了一个用户账户,您可以在 Anaconda Navigator 内部使用 Anaconda Cloud。在为 Anaconda Cloud 创建用户账户后,打开 Anaconda Navigator 并使用您的登录详细信息在屏幕右上角(显示为“登录到 Anaconda Cloud”)登录 Anaconda Cloud。现在,您可以将自己的包和文件上传到私有包仓库,并搜索现有的文件或包。
使用 conda 管理 Python 包
除了使用 Anaconda Navigator 和 Cloud 进行包管理外,您还可以使用conda,一个二进制包管理器,作为命令行工具来管理您的软件包安装。conda可以快速安装、运行和更新软件包及其依赖项。conda可以轻松创建、保存、加载并在您的本地计算机上切换环境。安装conda的最佳方式是通过安装 Anaconda 或 Miniconda。第三种选择是通过Python Package Index(PyPI)进行单独安装,但可能不是最新的,因此不建议选择此选项。
使用conda安装软件包非常简单,因为它与pip的语法相似。然而,了解conda不能直接从 Git 服务器安装软件包是有好处的。这意味着许多正在开发的软件包的最新版本无法使用conda下载。此外,conda并不像pip那样覆盖 PyPI 上所有可用的软件包,这就是为什么在创建 Anaconda Navigator 的新环境时您始终可以访问pip(关于pip的更多内容将在后续章节中介绍)。
您可以通过在终端中输入以下命令来验证是否已安装conda:
>> conda -version
如果已安装,conda将显示您已安装的版本号。您可以使用以下终端命令安装您选择的软件包:
>> conda install <package-name>
更新已安装的软件包到最新可用版本可以按照以下方式操作:
>> conda update <package-name>
您还可以通过指定版本号来安装特定版本的软件包:
>> conda install <package-name>=1.2.0
您可以通过使用--all参数简单地更新所有可用的软件包:
>> conda update --all
您也可以卸载软件包:
>> conda remove <package-name>
详细的conda文档可在以下网址找到:conda.io/docs/index.html。
使用 pip 管理 Python 软件包
如前所述,Anaconda 用户在每一个新环境中始终可以使用pip,以及root文件夹——它预安装在 Anaconda 的每个版本中,包括 Miniconda。由于pip是一个用于安装和管理用 Python 编写的软件包的 Python 包管理器,它运行在命令行中,而不是 Anaconda Navigator 和 Cloud。如果您决定不使用 Anaconda 或类似的产品,并使用来自python.org的默认 Python 安装,您可以使用easy_install或pip作为包管理器。由于pip被视为对easy_install的改进,并且是 Python 3 的首选包管理器,因此我们在此仅讨论pip。建议在后续章节中使用pip、conda、Anaconda Navigator 或 Cloud 进行 Python 包管理。
可选地,当您安装 Anaconda 时,三个环境变量将被添加到您的用户变量列表中。这使您能够在打开终端的情况下从任何系统位置访问pip等命令。要检查您的系统上是否已安装pip,请打开终端并输入:
>> pip
如果您没有收到任何错误信息,这意味着pip已正确安装,您可以使用pip通过以下方式从 PyPI 安装您选择的任何包:
>> pip install <package-name>
对于 Anaconda 用户,pip命令文件应存储在以下路径:
C:\Users\<用户名>\Anaconda3\Scripts\pip.exe。
如果您的系统上没有pip,您可以通过以下链接提供的说明来安装pip:pip.pypa.io/en/latest/installing。
使用 pip 升级和卸载包
与 Anaconda Cloud 自动显示已安装包的版本号不同,选择使用默认 Python 安装的用户可以使用pip通过以下命令显示它:
>> import pandas
>> pandas.__version__ # output will be a version number, for example: u'0.18.1'
升级包,例如当您想使用新版本时,可以按照以下步骤进行:
>> pip install -U pandas==0.21.0
将其升级到最新可用版本可以按照以下步骤进行:
>> pip install -U pandas
使用以下命令可以卸载包:
>> pip uninstall <package name>
Python 虚拟环境
通常来说,使用 Python 的推荐方法是项目基础方法。这意味着每个项目使用一个单独的 Python 版本,以及所需的包及其相互依赖。这种方法让您能够在不同的 Python 版本和已安装的包版本之间切换。如果不遵循这种方法,每次您更新包或安装新包时,其依赖项也会更新,导致不同的设置。这可能会导致问题,例如,由于底层的变化而无法正确运行的代码,或者无法正确相互通信的包。虽然这本书主要关注 Python 3,但您不需要切换到不同的 Python 版本,但也许您可以想象为不同的项目使用相同包的不同版本。
在 Anaconda 之前,这种项目基础方法需要使用virtualenv工具来创建隔离的 Python 环境。随着 Anaconda 的出现,这种方法变得更加简单。这两种选项将在我们进一步讨论时详细介绍。
使用 Anaconda 的虚拟环境
如前所述,Anaconda Navigator 有一个名为“环境”的标签页,点击它将显示用户在本地文件系统上创建的所有本地环境的概览。您可以在这样的环境中轻松创建、导入、克隆或删除环境,指定首选的 Python 版本,并按版本号安装包。任何新的环境都将自动安装一些 Python 包,例如pip。从那里,您可以自由地安装更多包。这些环境与您使用virtualenv工具创建的虚拟环境完全相同。您可以通过打开终端或运行 Python 来开始使用它们,这会打开一个终端并运行python.exe。
Anaconda 将所有环境存储在一个单独的root文件夹中,将所有虚拟环境集中在一个地方。请注意,Anaconda Navigator 中的每个环境都被视为虚拟环境,即使是根环境。
使用 conda 管理环境
Anaconda 和 Miniconda 都提供了conda包管理器,它也可以用来管理虚拟环境。打开终端并使用以下命令列出系统上所有可用的环境:
>> conda info -e
使用以下命令创建基于 Python 2.7 版本的虚拟环境:
>> conda create -n python3packt python=2.7
按照以下步骤激活环境:
>> activate python3packt
现在可以使用单个命令安装多个附加包:
>> conda install -n python3packt <package-name1> <package-name2>
此命令直接调用conda。
按照以下步骤注销您正在工作的环境:
>> deactivate
更多关于使用conda管理环境的信息,请参阅:conda.io/docs/user-guide/tasks/manage-environments.html
使用 virtualenv 创建虚拟环境
如果您不想使用 Anaconda,则需要首先安装virtualenv。使用以下命令进行本地安装:
>> pip install virtualenv
接下来,可以通过使用virtualenv命令并跟上新环境的名称来创建一个虚拟环境,例如:
>> virtualenv python3packt
导航到具有相同名称的目录:
>> cd python3packt
接下来,使用activate命令激活虚拟环境:
>> activate
您的虚拟环境现在已准备好使用。使用pip install将包专门安装到该环境中,并在您的代码中使用它们。使用deactivate命令停止虚拟环境的工作:
>> deactivate
如果您安装了多个 Python 版本,请使用-p参数与所需的 Python 版本或您选择的python.exe文件路径一起使用,例如:
>> -p python2.7
您也可以这样做:
>> -p c:\python34\python.exe
此步骤在创建虚拟环境之后,在安装所需包之前进行。有关virtualenv的更多信息,请参阅:virtualenv.readthedocs.io/en/stable
摘要
这章介绍了如何安装和管理本书中将要使用的代码库。我们将主要使用 Anaconda,它是 Python 编程语言的免费增值开源发行版,旨在简化包管理和部署。我们讨论了如何安装 Anaconda,以及使用 Anaconda Navigator、Anaconda Cloud、conda和pip进行 Python 包管理的选项。最后,我们讨论了虚拟环境以及如何使用 Anaconda、conda和virtualenv来管理这些环境。
本书推荐的安装版本是 Anaconda3,它不仅会安装一个可工作的 Python 环境,还会安装一个庞大的本地 Python 包库、Jupyter Notebook 应用程序,以及conda包管理器、Anaconda Navigator 和云服务。在下一章中,我们将介绍用于处理和分析地理空间数据的主要代码库。
第二章:地理空间代码库简介
本章将介绍用于处理和分析地理空间数据的主要代码库。您将了解每个库的特点,它们之间的关系,如何安装它们,在哪里可以找到额外的文档,以及典型用例。这些说明假设用户在其机器上安装了最新的(2.7 或更高版本)Python,并且不涉及 Python 的安装。接下来,我们将讨论所有这些包是如何结合在一起的,以及它们在本书的其余部分是如何被涵盖的。
本章将涵盖以下库:
-
GDAL/OGR
-
GEOS
-
Shapely
-
Fiona
-
Python Shapefile 库 (
pyshp) -
pyproj -
Rasterio
-
GeoPandas
地理空间数据抽象库(GDAL)和 OGR 简单特征库
地理空间数据抽象库(Geospatial Data Abstraction Library)(GDAL)/OGR 简单特征库结合了两个通常一起作为 GDAL 下载的独立库。这意味着安装 GDAL 包也提供了对 OGR 功能的访问,这就是为什么它们在这里一起被介绍。GDAL 被首先介绍的原因是其他包是在 GDAL 之后编写的,所以按时间顺序,它排在前面。您将注意到,本章中涵盖的一些包扩展了 GDAL 的功能或在其底层使用它。
GDAL 是由 Frank Warmerdam 在 1990 年代创建的,并于 2000 年 6 月首次发布。后来,GDAL 的发展被转移到了开源地理空间基金会(Open Source Geospatial Foundation)(OSGeo)。技术上,GDAL 与您平均的 Python 包略有不同,因为 GDAL 包本身是用 C 和 C++ 编写的,这意味着为了能够在 Python 中使用它,您需要编译 GDAL 及其相关的 Python 绑定。然而,使用 conda 和 Anaconda 可以相对容易地快速开始。由于它是用 C 和 C++ 编写的,因此在线 GDAL 文档是用库的 C++ 版本编写的。对于 Python 开发者来说,这可能是一个挑战,但许多函数都有文档,并且可以使用内置的 pydoc 工具或通过在 Python 中使用 help 函数进行查阅。
由于其历史原因,在 Python 中使用 GDAL 感觉更像是在使用 C++ 而不是纯 Python。例如,OGR 中的命名约定与 Python 的不同,因为您使用大写字母而不是小写字母来表示函数。这些差异解释了为什么选择其他一些 Python 库,如 Rasterio 和 Shapely,这些库也包含在本章中,虽然它们是从 Python 开发者的角度编写的,但提供了相同的 GDAL 功能。
GDAL 是一个庞大且广泛使用的栅格数据数据库。它支持读取和写入许多栅格文件格式,最新版本支持多达 200 种不同的文件格式。正因为如此,它在地理空间数据管理和分析中是不可或缺的。与其它 Python 库一起使用,GDAL 能够实现一些强大的遥感功能。它也是行业标准,存在于商业和开源 GIS 软件中。
OGR 库用于读取和写入矢量格式地理空间数据,支持读取和写入多种不同的格式。OGR 使用一个一致的模式来管理许多不同的矢量数据格式。我们将在 第五章 “矢量数据分析” 中讨论这个模型。你可以使用 OGR 进行矢量重投影、矢量数据格式转换、矢量属性数据过滤等操作。
GDAL/OGR 库不仅对 Python 程序员很有用,还被许多 GIS 供应商和开源项目所使用。截至撰写本文时,最新的 GDAL 版本是 2.2.4,该版本于 2018 年 3 月发布。
安装 GDAL
以前,Python 的 GDAL 安装相当复杂,需要你调整系统设置和路径变量。然而,仍然可以通过各种方式安装 GDAL,但我们建议你使用 Anaconda3 或 conda,因为这是最快、最简单的方法来开始。其他选项包括使用 pip 安装,或使用在线仓库,如 gdal.org 或 Tamas Szekeres 的 Windows 二进制文件 (www.gisinternals.com/release.php)。
然而,这可能会比这里描述的选项复杂一些。安装 GDAL 的难点在于,该库的特定版本(以 C 语言编写,并安装在你的本地 Python 文件之外的独立系统目录中)有一个配套的 Python 版本,并且需要编译才能在 Python 中使用。此外,Python 的 GDAL 依赖于一些额外的 Python 库,这些库包含在安装中。虽然可以在同一台机器上使用多个 GDAL 版本,但这里推荐的方法是在虚拟环境中安装它,使用 Anaconda3、conda 或 pip 安装。这将保持你的系统设置干净,避免额外的路径变量或防止某些东西无法正常工作。
使用 Anaconda3 安装 GDAL
如果你使用 Anaconda3,最简单的方法是通过 Anaconda Navigator 创建一个虚拟环境,选择 Python 3.6 作为首选版本。然后,从未安装的 Python 软件包列表中选择 gdal。这将安装 gdal 版本 2.1.0。
安装后,你可以通过进入 Python 命令行并输入以下命令来检查一切是否正常工作:
>> import gdal
>> import ogr
你可以按照以下方式检查 GDAL 的版本号:
>> gdal.VersionInfo() # returns '2010300'
这意味着你正在运行 GDAL 版本 2.1.3。
使用 conda 安装 GDAL
使用conda安装 GDAL 比 Anaconda3 提供了更多选择 Python 版本的自由度。如果您打开终端,可以使用conda search gdal命令打印出可用的gdal版本及其对应的 Python 版本。如果您想了解每个包的依赖项,请输入conda info gdal。GDAL 的特定版本依赖于特定的包版本,如果您已经安装了这些包,例如 NumPy,这可能会成为一个问题。然后,您可以使用虚拟环境安装和运行 GDAL 及其依赖项,并使用相应的 Python 版本,例如:
(C:\Users\<UserName> conda create -n myenv python=3.4
(C:\Users\<UserName> activate myenv # for Windows only. macOS and Linux users type "source activate myenv"
(C:\Users\<UserName> conda install gdal=2.1.0
您将被询问是否继续。如果您确认使用y并按Enter键,将安装一组额外的包。这些被称为依赖项,是 GDAL 运行所需的包。
如您所见,当您输入 conda search gdal 时,conda 并不会列出最新的 GDAL 版本,即 2.2.2。记住,在第一章,包安装与管理中,我们提到conda并不总是提供与其他方式可用的最新测试版本。这是一个例子。
使用 pip 安装 GDAL
Python 包索引(PyPI)也提供了 GDAL,这意味着您可以使用pip在您的机器上安装它。安装过程与前面描述的conda安装过程类似,但这次使用pip install命令。同样,如果您使用 Windows,建议在安装 GDAL 时使用虚拟环境,而不是需要您在系统环境设置中创建路径变量的根安装。
使用 pip 安装第二个 GDAL 版本
如果您有一台 Windows 机器,并且已经在您的机器上安装了一个工作的 GDAL 版本,但想使用pip安装额外的版本,您可以使用以下链接安装您选择的 GDAL 版本,然后从您激活的虚拟环境中运行以下命令来正确安装它:
GDAL 下载仓库:www.lfd.uci.edu/~gohlke/pythonlibs/#gdal
>> pip install path\to\GDAL‑2.1.3‑cp27‑cp27m‑win32.whl
GDAL-2.1.3-cp27m-win32.whl 是下载的 GDAL 仓库的名称。
其他推荐的 GDAL 资源
GDAL/OGR Python API 的完整文档可在:gdal.org/python/找到。
主页,gdal.org,也提供了 GDAL 的下载链接以及针对开发者和用户的详细文档。
GEOS
几何引擎开源(GEOS)是Java 拓扑套件(JTS)和选定功能的 C/C++端口。GEOS 旨在包含 JTS 在 C++中的完整功能。它可以在许多平台上编译,包括 Python。正如你稍后将会看到的,Shapely 库使用了 GEOS 库中的函数。实际上,有许多应用程序使用 GEOS,包括 PostGIS 和 QGIS。在第十二章中介绍的 GeoDjango,GeoDjango,以及其他地理空间库,如 GDAL,也使用了 GEOS。GEOS 还可以与 GDAL 一起编译,为 OGR 提供所有其功能。
JTS 是一个用 Java 编写的开源地理空间计算几何库。它提供了各种功能,包括几何模型、几何函数、空间结构和算法以及输入/输出功能。使用 GEOS,你可以访问以下功能——地理空间函数(如within和contains)、地理空间操作(并集、交集等)、空间索引、开放地理空间联盟(OGC)已知文本(WKT)和已知二进制(WKB)输入/输出、C 和 C++ API 以及线程安全性。
安装 GEOS
GEOS 可以使用pip install、conda和 Anaconda3 进行安装:
>> conda install -c conda-forge geos
>> pip install geos
关于 GEOS 和其他文档的详细安装信息可在此处获取:trac.osgeo.org/geos/
Shapely
Shapely 是一个用于平面特征操作和分析的 Python 包,它使用来自 GEOS 库(PostGIS 的引擎)和 JTS 的端口中的函数。Shapely 不关心数据格式或坐标系,但可以轻松地与这些包集成。Shapely 只处理几何形状的分析,不提供读取和写入地理空间文件的能力。它是由 Sean Gillies 开发的,他也是 Fiona 和 Rasterio 背后的那个人。
Shapely 支持在shapely.geometry模块中以类形式实现的八个基本几何类型——点、多点、线字符串、多线字符串、线环、多边形、几何集合。除了表示这些几何形状外,Shapely 还可以通过多种方法和属性来操作和分析几何形状。
Shapely 在处理几何形状时主要具有与 OGR 相同的类和函数。Shapely 与 OGR 的区别在于,Shapely 具有更 Pythonic 和非常直观的接口,优化得更好,并且拥有完善的文档。使用 Shapely,你将编写纯 Python 代码,而使用 GEOS,你将在 Python 中编写 C++代码。对于数据整理,这是一个用于数据管理和分析术语,你最好使用纯 Python 编写,而不是 C++,这也解释了为什么创建了这些库。
关于 Shapely 的更多信息,请参阅toblerity.org/shapely/manual.html上的文档。此页面还提供了有关在不同平台上安装 Shapely 以及如何从源代码构建 Shapely 以兼容依赖 GEOS 的其他模块的详细信息。这指的是安装 Shapely 可能需要您升级已安装的 NumPy 和 GEOS。
安装 Shapely
Shapely 可以使用 pip 安装、conda 和 Anaconda3 进行安装:
>> pip install shapely
>> conda install -c scitools shapely
Windows 用户也可以从www.lfd.uci.edu/~gohlke/pythonlibs/#shapely获取 wheels。wheel 是 Python 的一个预构建包格式,包含一个 ZIP 格式存档,具有特殊格式的文件名和 .whl 扩展名。Shapely 1.6 需要 Python 版本高于 2.6,以及 GEOS 版本高于或等于 3.3。
还请参阅pypi.python.org/pypi/Shapely以获取有关安装和使用 Shapely 的更多信息。
Fiona
Fiona 是 OGR 的 API。它可以用于读取和写入数据格式。使用它的主要原因之一是它比 OGR 更接近 Python,同时更可靠且错误更少。它利用两种标记语言,WKT 和 WKB,来表示矢量数据的空间信息。因此,它可以很好地与其他 Python 库(如 Shapely)结合使用。您将使用 Fiona 进行输入和输出,而使用 Shapely 创建和操作地理空间数据。
虽然 Fiona 与 Python 兼容且是我们的推荐,但用户也应了解一些缺点。它比 OGR 更可靠,因为它使用 Python 对象来复制矢量数据,而不是 C 指针,这也意味着它们使用更多的内存,这会影响性能。
安装 Fiona
您可以使用 pip 安装、conda 或 Anaconda3 来安装 Fiona:
>> conda install -c conda-forge fiona
>> conda install -c conda-forge/label/broken fiona
>> pip install fiona
Fiona 需要 Python 2.6、2.7、3.3 或 3.4 以及 GDAL/OGR 1.8+。Fiona 依赖于模块 six、cligj、munch、argparse 和 ordereddict(后两个模块是 Python 2.7+ 的标准模块)。
有关更多下载信息,请参阅 Fiona 的自述文件页面toblerity.org/fiona/README.html。
Python shapefile library (pyshp)
Python shapefile 库(pyshp)是一个纯 Python 库,用于读取和写入 shapefiles。pyshp 库的唯一目的是处理 shapefiles——它只使用 Python 标准库。您不能用它进行几何运算。如果您只处理 shapefiles,这个仅包含一个文件的库比使用 GDAL 更简单。
安装 pyshp
您可以使用 pip 安装、conda 和 Anaconda3 来安装 pyshp:
>> pip install pyshp
>> conda install pyshp
更多文档可在 PyPi 上找到:pypi.python.org/pypi/pyshp/1.2.3
pyshp 的源代码可在以下网址找到:github.com/GeospatialPython/pyshp。
pyproj
pyproj 是一个执行地图变换和大地测量计算的 Python 包。它是一个 Cython 包装器,提供 Python 接口到 PROJ.4 函数,这意味着你可以在 Python 中访问现有的 C 代码库。
PROJ.4 是一个投影库,可以在许多坐标系之间转换数据,并且也通过 GDAL 和 OGR 提供。PROJ.4 仍然受欢迎和广泛使用的原因有两个:
-
首先,因为它支持如此多的不同坐标系
-
其次,因为它提供了执行此操作的方法——接下来将要介绍的 Rasterio 和 GeoPandas 两个 Python 库,都使用
pyproj和 PROJ.4 功能。
使用 PROJ.4 单独而不是与 GDAL 等包一起使用,其区别在于它允许你重新投影单个点,而使用 PROJ.4 的包不提供此功能。
pyproj 包提供了两个类——Proj 类和 Geod 类。Proj 类执行地图计算,而 Geod 类执行大地测量计算。
安装 pyproj
使用 pip install、conda 和 Anaconda3 安装 pyproj:
>> conda install -c conda-forge pyproj
>> pip install pyproj
以下链接包含有关 pyproj 的更多信息:jswhit.github.io/pyproj/
你可以在 proj4.org/ 上找到更多关于 PROJ.4 的信息。
Rasterio
Rasterio 是一个基于 GDAL 和 NumPy 的 Python 库,用于处理栅格数据,它以 Python 开发者为出发点,而不是 C 语言,使用 Python 语言类型、协议和习惯用法编写。Rasterio 的目标是使 GIS 数据对 Python 程序员更加易于访问,并帮助 GIS 分析师学习重要的 Python 标准。Rasterio 依赖于 Python 的概念,而不是 GIS。
Rasterio 是来自 Mapbox 卫星团队的开放源代码项目,Mapbox 是网站和应用程序的定制在线地图提供商。这个库的名字应该读作 raster-i-o 而不是 ras-te-rio。Rasterio 是在名为 Mapbox Cloudless Atlas 的项目之后出现的,该项目旨在从卫星图像中创建一个看起来很漂亮的底图。
软件要求之一是使用开源软件和具有方便的多维数组语法的编程语言。尽管 GDAL 提供了经过验证的算法和驱动程序,但使用 GDAL 的 Python 绑定感觉就像是在用 C++ 开发。
因此,Rasterio 被设计成一个顶层 Python 包,中间是扩展模块(使用 Cython),底部是 GDAL 共享库。对于栅格库的其他要求是能够读写 NumPy ndarrays 到和从数据文件,使用 Python 类型、协议和习惯用法而不是 C 或 C++ 来解放程序员,使他们不必用两种语言编码。
对于地理配准,Rasterio 跟随 pyproj 的步伐。在读取和写入的基础上增加了一些功能,其中之一是功能模块。可以使用 rasterio.warp 模块进行地理空间数据的重投影。
Rasterio 的项目主页可在此处找到:github.com/mapbox/rasterio
Rasterio 依赖项
如前所述,Rasterio 使用 GDAL,这意味着它是其依赖之一。Python 包依赖项包括 affine、cligj、click、enum34 和 numpy。
Rasterio 的文档可在此处找到:mapbox.github.io/rasterio/
Rasterio 安装
要在 Windows 机器上安装 Rasterio,你需要下载适用于你系统的 rasterio 和 GDAL 二进制文件,并运行:
>> pip install -U pip
>> pip install GDAL-1.11.2-cp27-none-win32.whl
>> pip install rasterio-0.24.0-cp27-none-win32.whl
使用 conda,你可以这样安装 rasterio:
>> conda config --add channels conda-forge # this enables the conda-forge channel
>> conda install rasterio
conda-forge 是一个额外的通道,可以从该通道安装包。
不同平台的详细安装说明可在此处找到:mapbox.github.io/rasterio/installation.html
GeoPandas
GeoPandas 是一个用于处理矢量数据的 Python 库。它基于 SciPy 堆栈中的 pandas 库。SciPy 是一个流行的数据检查和分析库,但遗憾的是,它不能读取空间数据。GeoPandas 的创建是为了填补这一空白,以 pandas 数据对象为起点。该库还增加了来自地理 Python 包的功能。
GeoPandas 提供了两个数据对象——一个基于 pandas Series 对象的 GeoSeries 对象和一个基于 pandas DataFrame 对象的 GeoDataFrame,为每一行添加一个几何列。GeoSeries 和 GeoDataFrame 对象都可以用于空间数据处理,类似于空间数据库。几乎为每种矢量数据格式提供了读写功能。此外,由于 Series 和 DataFrame 对象都是 pandas 数据对象的子类,你可以使用相同的属性来选择或子集数据,例如 .loc 或 .iloc。
GeoPandas 是一个能够很好地利用新工具(如 Jupyter Notebooks)的库,而 GDAL 则允许你通过 Python 代码与矢量和栅格数据集中的数据记录进行交互。GeoPandas 通过将所有记录加载到 GeoDataFrame 中,以便你可以在屏幕上一起查看它们,采取了一种更直观的方法。数据绘图也是如此。这些功能在 Python 2 中缺失,因为开发者依赖于没有广泛数据可视化能力的 IDE,而现在这些功能可以通过 Jupyter Notebooks 获得。
GeoPandas 安装
安装 GeoPandas 有多种方式。你可以使用 pip install、conda install、Anaconda3 或 GitHub。使用终端窗口,你可以按以下方式安装:
>> pip install geopandas
>> conda install -c conda-forge geopandas
详细安装信息可在此处找到:geopandas.org/install.html
GeoPandas 也可以通过 PyPi 获取:pypi.python.org/pypi/geopandas/0.3.0
GeoPandas 也可以通过 Anaconda Cloud 获取:anaconda.org/IOOS/geopandas
GeoPandas 依赖项
GeoPandas 依赖于以下 Python 库:pandas、Shapely、Fiona、pyproj、NumPy 和 six。这些库在安装 GeoPandas 时会更新或安装。
Geopandas 的文档可在 geopandas.org 查找。
它们是如何协同工作的
我们提供了处理和分析地理空间数据最重要的开源软件包的概述。那么问题随之而来,何时使用某个软件包以及为什么。GDAL、OGR 和 GEOS 对于地理空间处理和分析是必不可少的,但它们不是用 Python 编写的,因此需要为 Python 开发者提供 Python 二进制文件。Fiona、Shapely 和 pyproj 是为了解决这些问题而编写的,以及较新的 Rasterio 库。为了更 Pythonic 的方法,这些较新的软件包比带有 Python 二进制文件的较老 C++ 软件包更受欢迎(尽管它们在底层使用)。
然而,了解所有这些软件包的起源和历史是很好的,因为它们都被广泛使用(并且有很好的理由)。下一章,将讨论地理空间数据库,将基于本章的信息。第五章,矢量数据分析,和 第六章,栅格数据处理,将专门处理这里讨论的库,更深入地探讨使用这些库进行栅格和矢量数据处理的细节。
到目前为止,你应该对处理和分析最重要的软件包有一个全局的了解,包括它们的历史以及它们是如何相互关联的。你应该对特定用例可用的选项有一个概念,以及为什么一个软件包比另一个软件包更可取。然而,正如编程中经常发生的那样,对于特定问题可能有多个解决方案。例如,处理 shapefiles 时,你可以根据你的偏好和问题使用 pyshp、GDAL、Shapely 或 GeoPandas。
摘要
在本章中,我们介绍了用于处理和分析地理空间数据的主要代码库。你学习了每个库的特点,它们是如何相互关联或相互区别的,如何安装它们,在哪里可以找到额外的文档,以及典型用例。GDAL 是一个包含两个独立库的主要库,即 OGR 和 GDAL。许多其他库和软件应用程序在底层使用 GDAL 功能,例如 Fiona 和 Rasterio,这两者都在本章中进行了介绍。这些是为了使与 GDAL 和 OGR 一起以更 Pythonic 的方式工作而创建的。
下一章将介绍空间数据库。这些数据库用于数据存储和分析,例如 SpatiaLite 和 PostGIS。你还将学习如何使用不同的 Python 库来连接这些数据库。
第三章:地理空间数据库简介
在前面的章节中,你学习了如何设置你的 Python 环境,并了解了使用 Python 处理地理数据的不同库。在本章中,你将开始处理数据。
数据库提供了存储大量数据最受欢迎的方式之一,其中最受欢迎的开源数据库之一是 PostgreSQL。PostGIS 扩展了 PostgreSQL,增加了地理对象和空间查询记录的能力。当 PostgreSQL 和 PostGIS 结合使用时,它们创建了一个强大的地理数据存储库。
地理空间数据库通过允许你通过位置或通过数据库中其他特征的位置查询你的数据来改进基本的关系数据库查询。你还可以执行地理空间操作,如特征测量、特征之间的距离以及投影之间的转换。地理空间数据库的另一个特点是能够从现有特征创建新的几何形状,例如通过缓冲区、并集或裁剪操作。
本章将介绍地理空间数据库的基础知识。在本章中,你将学习:
-
如何安装 PostgreSQL 和 PostGIS
-
如何安装和使用
pyscopg2连接到数据库 -
如何向数据库添加数据
-
如何执行基本的空间查询
-
如何查询长度和面积
-
如何查询多边形内的点
在 第七章,使用地理数据库进行地理处理 中,我们将回到地理空间数据库,你将学习更高级的操作以及如何显示你的数据。
在 Windows 上安装 PostgreSQL 和 PostGIS
你可以通过安装 PostgreSQL 然后安装 PostGIS,或者安装 PostgreSQL 然后使用随 PostgreSQL 一起提供的 Stack Builder 来添加 PostGIS。使用 Stack Builder 允许你下载所需的 PostgreSQL 版本,并单击一次即可获取正确的 PostGIS 版本。
当我安装 PostgreSQL 10 时,Stack Builder 没有包含 PostGIS。到出版时,这应该已经添加了。截图可能显示不同的 PostGIS 版本,因为我使用了一个旧的 PostgreSQL 复制来展示 Stack Builder 将如何工作。您可以从 www.postgresql.org/download/ 下载 PostgreSQL。
随着我们继续前进,我将指导你安装 PostgreSQL,然后使用 Stack Builder 添加 PostGIS 和数据库。下载可执行文件后,双击它。你将看到以下向导:

你可以选择安装 PostgreSQL 的位置,但除非你有特定的理由将其放置在其他位置,否则最好将其保留为默认设置:

再次强调,最好将数据存储在默认位置,即与 PostgreSQL 安装相同的根文件夹:

选择你想要运行 PostgreSQL 的端口。应用程序将期望在这个端口上找到 PostgreSQL,所以请自行承担更改端口的风险。更高级的用户可以在安装后修改.config文件中的端口通道配置:

选择你的区域设置,或者选择默认设置。我选择了英语,美国:

这里你将看到启动 Stack Builder 的选项,从那里你可以安装 PostGIS。勾选复选框开始安装。在较新的系统上,安装应该只需要几分钟:

PostgreSQL 安装已完成,Stack Builder 现在应该已经打开。在空间扩展部分,选择 PostGIS 32 位或 64 位的正确版本。请注意,它是一个捆绑包,包括其他包,如pgRouting:

现在,PostGIS 的安装向导将启动。你必须同意许可协议:

你可以随时创建数据库,本章将向你展示如何操作,但是,最好现在就检查创建空间数据库框并处理它。如果你这样做,一旦 PostGIS 安装完成,你的数据库将设置好并准备好使用:

PostGIS 将尝试在 PostgreSQL 安装的位置安装:

输入数据库的用户名、密码和端口。本章中的示例将使用postgres(用户名)和postgres(密码)。如果你选择不同的用户名和密码组合,请记住它。在生产环境中,最好不使用默认的用户名和密码,因为它们众所周知,会使你容易受到黑客攻击:

输入数据库名称。我们将要查看的示例将使用pythonspatial作为数据库名称。你将只使用该名称进行初始连接。示例中的 SQL 查询将使用表名:

在 Mac 上安装 PostgreSQL 和 PostGIS
在 Mac 上安装 PostgreSQL 和 PostGIS,你可以使用Postgres.app。你可以从postgresapp.com/下载文件。文件下载完成后,将其移动到applications文件夹,并双击它。点击初始化。你将有一个在localhost:5432的服务器。用户名和数据库名与你的 Mac 用户名相同。没有密码。
然后,你应该能够使用psql命令创建一个新的数据库并启用 PostGIS。
使用 Python 与 PostgreSQL 和 PostGIS 协同工作
要在 Python 中使用psycopg2库连接和操作您的 PostgreSQL 数据库,您需要一个辅助库。psycopg2就是那个库。它提供了一个官方libpq客户端库的包装。在本节中,我们将介绍如何安装该库、如何连接到数据库以及如何添加表和执行基本的空间查询。
使用 psycopg2 连接到 PostgreSQL
psycopg2是 Python 中处理 PostgreSQL 最流行的库。它完全实现了 Python DB API 2.0 规范,并且与 Python 3 兼容。在接下来的章节中,您将学习如何安装库、建立连接、执行查询和读取结果。您可以在此处阅读完整的文档:initd.org/psycopg/docs/
安装 psycopg2
安装大多数 Python 库需要您打开控制台并输入:
pip install psycopg2
如果这不起作用,并且您使用的是 Anaconda Python 发行版,您可以使用conda命令,如下所示:
conda install -c anaconda psycopg2
大多数 Python 库都可以使用以下方式下载和安装:
python setup.py install
由于psycopg2比这更高级,并且需要您拥有 C 编译器、Python 头文件、libpq头文件和pg_config程序。如果您需要从源代码安装psycopg2,说明链接位于以下提示框中。
要从源代码安装psycopg2,说明位于:initd.org/psycopg/docs/install.html#install-from-source
连接到数据库并创建一个表
您应该在安装 PostGIS 时创建数据库。对于以下提到的示例,我们将使用此数据库。
如果您在安装 PostGIS 时没有创建数据库,您可以使用终端(Windows 中的命令提示符)和以下命令创建:
createdb -U postgres pythonspatial
psql -U postgres -d pythonspatial -c "CREATE EXTENSION postgis;"
您可能需要修改您的路径。在 Windows 上,执行此操作的命令如下所示:
set PATH=%PATH%;C:\Program Files\PostgreSQL\10\bin
要连接到您的数据库,请使用以下代码:
import psycopg2
connection = psycopg2.connect(database="pythonspatial",user="postgres", password="postgres")
cursor = connection.cursor()
cursor.execute("CREATE TABLE art_pieces (id SERIAL PRIMARY KEY, code VARCHAR(255), location GEOMETRY)")
connection.commit()
之前提到的代码首先通过导入psycopg2开始。然后使用connect()函数并通过传递数据库名称、user和password参数来建立连接。然后创建一个cursor,允许您与数据库通信。您可以使用cursor的execute()方法通过字符串形式的 SQL 语句创建表。
该代码执行一个 SQL 命令,创建一个名为art_pieces的表,其中id的类型为SERIAL,并使其成为PRIMARY KEY,code的类型为VARCHAR,长度为255,location的类型为GEOMETRY。SERIAL PRIMARY KEY告诉 PostgreSQL 我们想要一个自动递增的唯一标识符。您也可以使用BIGSERIAL类型。另一种不同的类型是location的GEOMETRY类型。这是将存储我们记录地理部分的列。
最后,您 commit() 以确保更改已保存。您也可以在完成时 close(),但我们将继续前进。
向表中添加数据
在上一节中,我们创建了一个表。在本节中,您将从开放数据网站抓取数据并将其放入您的表中,以便您可以在下一节中进行查询。
大多数城市都有开放数据网站和门户。阿尔伯克基市有几个带有空间数据的 ArcServer 端点。以下代码将使用 requests Python 库抓取公共艺术数据,然后使用 psycopg2 将其发送到 PostgreSQL 数据库,pythonspatial:
import requests
url='http://coagisweb.cabq.gov/arcgis/rest/services/public/PublicArt/MapServer/0/query'
params={"where":"1=1","outFields":"*","outSR":"4326","f":"json"}
r=requests.get(url,params=params)
data=r.json()
data["features"][0]
我们之前提到的代码导入 requests,然后,使用 ArcServer 端点的 URL,它抓取了查询所有数据(where:1=1)和所有字段(outFields:*)在 世界大地测量系统(WGS) 84(outSR:4326)的结果,并将其作为 JSON(f:json)返回。
ArcServer 是由 环境系统研究学院(ESRI)制作的 GIS 服务器。它提供了一种使用 API 提供 GIS 数据并返回 JSON 的方式。许多政府机构将有一个开放数据门户,该门户利用 ArcServer 来提供数据。
结果被加载到 data 变量中。每个记录都在数组 features(data["features"][n])中。单个记录 data["features"][0] 如下所示:
{'attributes': {'ADDRESS': '4440 Osuna NE',
'ARTIST': 'David Anderson',
'ART_CODE': '101',
'IMAGE_URL': 'http://www.flickr.com/photos/abqpublicart/6831137393/',
'JPG_URL': 'http://farm8.staticflickr.com/7153/6831137393_fa38634fd7_m.jpg',
'LOCATION': 'Osuna Median bet.Jefferson/ W.Frontage Rd',
'OBJECTID': 951737,
'TITLE': 'Almond Blossom/Astronomy',
'TYPE': 'public sculpture',
'X': -106.5918383,
'Y': 35.1555,
'YEAR': '1986'},
'geometry': {'x': -106.59183830022498, 'y': 35.155500000061544}}
使用 data,您将遍历 features 数组,将 ART_CODE 作为 code 插入,并为每个点创建一个 已知文本(WKT)表示。
要了解更多关于 WKT 的信息,您可以在其维基百科条目中阅读:en.wikipedia.org/wiki/Well-known_text
以下代码展示了如何插入数据:
for a in data["features"]:
code=a["attributes"]["ART_CODE"]
wkt="POINT("+str(a["geometry"]["x"])+" "+str(a["geometry"] ["y"])+")"
if a["geometry"]["x"]=='NaN':
pass
else:
cursor.execute("INSERT INTO art_pieces (code, location) VALUES ({},
ST_GeomFromText('{}'))".format(code, wkt))
connection.commit()
上述代码遍历每个要素。它将 ART_CODE 分配给 code,然后构建 WKT (Point(-106.5918 35.1555)),并将其分配给 wkt。代码使用 ART_CODE 来展示如何将其他属性加载到数据库中。
数据几乎永远不会干净完美。这些数据也不例外。为了防止在 x 坐标缺失时崩溃,我添加了一个 if, else 语句来跳过缺失的数据。这个概念被称为 错误处理,在构建 requests 时是一种最佳实践。else 语句是数据被插入的地方。使用 cursor.execute(),您可以构建 SQL 查询。
查询将 art_pieces 以及 code 和 location 字段与值一起插入到数据库中。对于 code 的第一个值是一个占位符 {}。对于 location 的第二个值是几何形状,我们将其存储为 WKT。因此,它使用 ST_GeomFromText() 函数和一个占位符 {} 插入。
format() 方法是您传递变量以填充占位符的地方——code,wkt。以下代码展示了当占位符被填充时查询将看起来是什么样子:
INSERT INTO art_pieces (code, location) VALUES (101, ST_GeomFromText('Point(-106.5918 35.1555)'))
在之前提到的代码中,你通过连接字符串创建了 WKT。这可以通过使用 Shapely 库以更简洁和更 Pythonic 的方式完成。
Shapely
Shapely 可以通过以下方式安装:
pip install shapely
或者使用 conda:
conda install -c scitools shapely
Shapely 使得创建和使用几何形状的任务更加容易,并使你的代码更加简洁。在之前的代码中,你通过连接一个字符串来创建一个点的 WKT 表示。使用 Shapely,你可以创建一个点并将其转换为 WKT。以下代码展示了如何操作:
from shapely.geometry import Point, MultiPoint
thepoints=[]
for a in data["features"]:
code=a["attributes"]["ART_CODE"]
p=Point(float(a["geometry"]["x"]),float(a["geometry"]["y"]))
thepoints.append(p)
if a["geometry"]["x"]=='NaN':
pass
else:
cursor.execute("INSERT INTO art_pieces (code, location) VALUES ('{}',
ST_GeomFromText('{}'))".format(code, p.wkt))
connection.commit()
之前的代码从 shapely.geometry 中导入了 Point 和 MultiPoint。代码与上一个版本相同,直到加粗的行。在 Shapely 中创建一个点,你使用 Point(x,y)。它将所有点放入一个名为 thepoints 的数组中,以便在 Jupyter Notebook 中绘制,以下是一个图像。最后,SQL 语句将 p.wkt 传递给 ST_GeomFromText()。
在 Jupyter Notebook 中,你只需输入包含几何形状的变量的名称即可打印 Shapely 几何形状,它将自动绘制。公共 art 点存储在变量 thepoints 中*。可以使用点的数组创建一个 MultiPoint,打印它们将绘制以下图像:

查询数据
你创建了一个表,为代码和位置添加了列,并使用来自另一个源的数据填充了它。现在,你将学习如何查询数据并将其从数据库中导出。
虽然你可以使用空间 SQL 查询,但你总是可以像选择非空间启用数据库中的数据一样选择数据,这样你就可以像以下那样使用它:
SELECT * FROM table
以下代码展示了通用的 SELECT 全查询及其结果:
cursor.execute("SELECT * from art_pieces")
data=cursor.fetchall()
data
结果应该如下所示:
[(1, '101', '010100000025FFBFADE0A55AC06A658B6CE7934140'),
(2, '102', '0101000000CC4E16E181AA5AC0D99F67B3EA8B4140'),
.......,]
第一个数字,1、2、n,是 id(SERIAL PRIMARY KEY)。接下来是 code。几何形状是最后一列。看起来像是随机数字和字母的字符串是一个 已知的二进制(WKB)的十六进制表示。
要转换 WKB,你使用 shapely。以下代码将指导你如何将 WKB 转换为 shapely 的 Point,然后打印 WKT:
from shapely.wkb import loads
aPoint=loads(data[0][2],hex=True)
aPoint.wkt
之前的代码从 shapely.wkb 中导入了 loads() 方法。你必须添加 hex 参数并将其设置为 True,否则你会收到错误。要获取第一条记录的地理列,你可以使用 data[0][2],其中 [0] 是记录,[2] 是列。现在你有了 shapely 的 Point,你可以通过使用 type(aPoint) 来验证它,你可以使用 aPoint.wkt 来打印它。你应该看到以下结果:
POINT (-106.591838300225 35.15550000006154)
如果你想让 PostgreSQL 返回 WKB 格式的数据而不包含十六进制,你可以使用 ST_AsBinary() 来实现。以下代码展示了如何操作:
cursor.execute("SELECT id,code,ST_AsBinary(location) from art_pieces")
data=cursor.fetchall()
data[0][2]
from shapely.wkb import loads
pNoHex=loads(bytes(data[0][2]))
pNoHex.wkt
之前的代码使用 ST_AsBinary() 包裹了位置。要将结果加载到 shapely 的 Point 中,你必须使用 bytes()。然后,你可以使用 pNoHex.wkt 来查看 WKT。你应该看到与上一个示例中相同的点。
二进制可能很有用,但您也可以查询数据并将几何作为 WKT 返回:
cursor.execute("SELECT code, ST_AsText(location) from art_pieces")
data = cursor.fetchone()
之前的代码使用 ST_AsText(geometry column) 将数据作为 WKT 返回。您可以使用 ST_AsText() 在任何时候返回包含几何的列。代码使用 fetchone() 而不是 fetchall() 来获取单个记录。您应该看到以下单个记录:
('101', 'POINT(-106.591838300225 35.1555000000615)')
您可以使用 loads() 将 WKT 载入 shapely 的 Point,但您需要先导入它,就像您之前导入 WKB 一样:
from shapely.wkt import loads
pb=loads(data[1])
pb.coords[:]
之前的代码从 shapely 导入 loads——但这次使用 shapely.wkt 而不是 wkb。否则,您将以与之前示例相同的方式加载数据。您可以使用 pb.coords[:] 查看坐标,或者使用 pb.x 和 pb.y 单独查看它们。
pb.coords[:] 的结果将是一对坐标,如下所示:
[(-106.591838300225, 35.1555000000615)]
改变坐标参考系统
数据库中的数据使用 世界大地测量系统 84 (WGS 84)的纬度和经度。如果您需要将数据输出到 欧洲石油调查组 (EPSG) 3857,您可以使用 ST_Transform() 在查询中更改空间参考。以下代码通过使用 PostGIS 函数展示了如何操作:
cursor.execute("SELECT UpdateGeometrySRID('art_pieces','location',4326)")
cursor.execute("SELECT Find_SRID('public','art_pieces','location')")
cursor.fetchall()
之前的代码对数据库进行了两次查询:
-
首先,它使用
UpdateGeomtrySRID()将空间参考系统标识符分配给表中的几何列。这是因为点被放入表中而没有任何SRID的参考。因此,当我们尝试使用不同的坐标参考系统获取结果时,数据库将不知道如何转换我们的坐标。 -
其次,代码查询数据库以告诉我们表中的几何列的
SRID使用Find_SRID()。如果您没有正确添加几何列,该函数将失败。
现在您已经在表中的列上设置了 SRID,您可以查询数据并转换它:
cursor.execute("SELECT code, ST_AsTexT(ST_Transform(location,3857)) from art_pieces")
cursor.fetchone()
之前的代码是一个基本的 select code 和 location 作为文本从 art_pieces 中选择,但现在有一个 ST_Transform 方法。该方法接受具有几何的列和您希望数据返回的 SRID。现在,使用 3857 返回 (-106.59, 35.155) 的艺术品,如下所示,并显示转换后的坐标:
('101', 'POINT(-11865749.1623 4185033.1034)')
缓冲区
空间数据库允许您存储空间数据,但您还可以对数据进行操作并获取不同的几何结果。这些操作中最常见的是缓冲区。您有一个点表,但使用 ST_Buffer(),您可以要求数据库返回一个具有指定半径的点周围的多边形。以下代码展示了如何操作:
cursor.execute("SELECT ST_AsText(ST_Buffer(a.location,25.00,'quad_segs=2')) from pieces a WHERE a.code='101'")
cursor.fetchall()
之前的代码从表中抓取了一个记录,其中艺术代码字段等于 101,并选择了一个半径为 25 的 location 缓冲区。结果将是一个多边形,如下所示:
当使用地理信息时,如果缓冲区很大,位于两个 UTM 区之间,或者穿过日界线,它可能会出现意外的行为。
'POLYGON((-106.591563918525 35.1555036055616,-106.591568334295 35.1554595740463,-106.59158312469 35.1554170960907,...,-106.591570047094 35.155547498531,-106.591563918525 35.1555036055616))'
如果你使用以下代码将多边形加载到 shapely 中,Jupyter Notebook 将绘制多边形:
from shapely.geometry import Polygon
from shapely.wkt import loads
buff=loads(data[0][0])
buff
ST_Buffer 返回的 shapely 多边形如下所示:

你也可以向 ST_Buffer 传递一个参数来指定绘制四分之一圆所使用的段数。如果你将圆分成四个象限,quad_segs 参数将在每个象限中绘制这么多段。quad_seg 的值为 1 将绘制一个旋转的正方形,如下所示:

而 quad_seg 的值为 2 将绘制一个八边形,如下所示:

距离和邻近
在前面的部分,你让数据库缓冲一个点并返回多边形。在本节中,你将学习如何查询数据库以获取两点之间的距离,并且你将查询数据库并让它根据与指定点的距离返回记录。
PostGIS 的距离函数是 ST_Distance(a,b)。你可以将 a 和 b 作为几何图形或地理图形传递。作为地理图形,结果将以米为单位返回。以下代码将获取数据库中两点之间的距离:
cursor.execute("SELECT ST_Distance(a.location::geography,b.location::geography) FROM art_pieces a, art_pieces b where a.name='101' AND b.name='102'")
dist=cursor.fetchall()
dist
之前的代码执行了 ST_Distance() 的 SQL 查询,传递了 a 和 b 记录的 location 列,其中代码等于 101 和 102。::geography 是在 PostGIS 中将几何图形转换为地理图形的方法。它们相距多远?它们相距 9,560.45428363 米。
要将其转换为英里,请使用:dist[0][0]*0.00062137,这使得它们相距 5.940 英里。
在前面的例子中,你使用了数据库中的两个点,但你也可以像以下代码一样传递一个硬编码的点:
cursor.execute("SELECT ST_Distance(a.location::geography,
ST_GeometryFromText('POINT(-106.5 35.1)')::geography)
FROM art_pieces a where a.name='101'")
cursor.fetchall()
之前的代码是相同的查询,但这次你用硬编码的 WKT 点替换了点 b (code=102)。查询的结果应该声明这些点相距 10,391.40637117 米。
并且,就像之前的例子一样,你也可以使用 shapely 传递点的 WKT,如下面的代码所示:
from shapely.geometry import Point
p=Point(-106.5,35.1)
cursor.execute("SELECT ST_Distance(a.location::geography,
ST_GeometryFromText('{}')::geography)
FROM art_pieces a where a.name='101'".format(p.wkt))
cursor.fetchall()
之前的代码在 shapely 中创建点,然后使用 format(p.wkt) 将 WKT 传递到 {} 占位符。
你可以获取两点之间的距离,但如果你想要多个点与另一个点的距离呢?为此,你可以移除 a.location 并仅使用 location 作为第一个点。以下代码将返回五个点及其与指定点的距离:
cursor.execute("SELECT code, ST_Distance(location::geography,
ST_GeometryFromText('POINT(-106.591838300225
35.1555000000615)')::geography)
as d from art_pieces LIMIT 5")
cursor.fetchall()
结果应该看起来像显示距离(米)的数据:
[('101', 0.0),
('102', 9560.45428362),
('104', 4741.8711304),
('105', 9871.8424894),
('106', 7907.8263995)]
数据库返回了表中前五个点的代码和与指定点的距离。如果你移除 LIMIT,你将得到所有点。
通过添加 ORDER BY 子句和 k-最近邻运算符,你可以扩展这个查询以获取指定点的最近五个点。看看以下代码:
cursor.execute("SELECT code, ST_Distance(location::geography,
ST_GeometryFromText('POINT(-106.591838300225
35.1555000000615)')::geography) as d from art_pieces
ORDER BY location<-
>ST_GeometryFromText('POINT(-106.591838300225
35.1555000000615)') LIMIT 5")
cursor.fetchall()
之前代码中的关键元素是符号 *<->*。这是 k-最近邻运算符。它返回两个几何体之间的距离。使用 ORDER BY location <-> ST_GeometryFromText(),你指定了两个几何体。由于你设置了 LIMIT 为 5,数据库将返回与指定点最近的 5 个点——包括起点。结果应该看起来像以下点:
[('101', 0.0),
('614', 1398.08905864),
('492', 2384.97632735),
('570', 3473.81914218),
('147', 3485.71207698)]
注意代码值不是 101-106 或数据库中的前五个,并且距离从 0.0 开始增加。最近的点,代码 101,是你查询中指定的点,因此它距离 0.0 米。
数据库中的线
本章的第一部分重点介绍了点操作。现在,我们将把注意力转向线。对于以下示例,你将创建一个新的表并插入三条线。以下代码将完成这个任务:
from shapely.geometry import LineString
from shapely.geometry import MultiLineString
connection = psycopg2.connect(database="pythonspatial",user="postgres",
password="postgres")
cursor = c.cursor()
cursor.execute("CREATE TABLE lines (id SERIAL PRIMARY KEY, location GEOMETRY)")
thelines=[]
thelines.append(LineString([(-106.635585,35.086972),(-106.621294,35.124997)]))
thelines.append(LineString([(-106.498309,35.140108),(-106.497010,35.069488)]))
thelines.append(LineString([(-106.663878,35.106459),(-106.586506,35.103979)]))
mls=MultiLineString([((-106.635585,35.086972),(-106.621294,35.124997)),((-106.498309,35.140108),(-106.497010,35.069488)),((-106.663878,35.106459),(-106.586506,35.103979))])
for a in thelines:
cursor.execute("INSERT INTO lines (location) VALUES
(ST_GeomFromText('{}'))".format(a.wkt))
connection.commit()
之前的代码应该很熟悉。它首先连接到 Python 空间数据库,获取一个 cursor,然后创建一个包含 id 和 geometry 类型位置的表。你应该导入 shapely LineString 和 MultiLine,MultiLine 是为了你可以在 Jupyter notebook 中打印出这些线。你应该创建一个 lines 的数组,然后遍历它们,使用 cursor 将每个插入到表中。然后你可以 commit() 这些更改。
要查看是否已将行添加到数据库中,你可以执行以下代码:
cursor.execute("SELECT id, ST_AsTexT(location) from lines")
data=cursor.fetchall()
data
之前的代码在新的表上执行了一个基本的 SELECT 语句。结果集应该有三条记录,如下所示:
[(1, 'LINESTRING(-106.635585 35.086972,-106.621294 35.124997)'),
(2, 'LINESTRING(-106.498309 35.140108,-106.49701 35.069488)'),
(3, 'LINESTRING(-106.663878 35.106459,-106.586506 35.103979)')]
如果你打印 mls 变量(在早期代码中持有多线字符串的变量),你可以看到以下图像中显示的线:

现在你已经有一个包含几条线的数据库表,你可以继续测量它们并找出它们是否相交。
线的长度
点没有长度,如果它们相交,它们有相同的坐标。然而,线有长度,并且可以在表中未指定的点上相交,或者在创建线的两个点之间。
以下代码将返回所有 lines 的长度:
cu.execute("SELECT id, ST_Length(location::geography) FROM lines ")
cu.fetchall()
之前的代码使用了 ST_Length 函数。该函数将接受几何体和地理体。在这个例子中,使用了 ::geography 将几何体转换,以便返回米。
结果如下:
[(1, 4415.21026808109),
(2, 7835.65405408195),
(3, 7059.45840502359)]
你可以在之前的查询中添加一个 ORDER BY 子句,数据库将按从短到长的顺序返回 lines。以下代码添加了该子句:
cu.execute("SELECT id, ST_Length(location::geography)
FROM lines ORDER BY ST_Length(location::geography)")
cu.fetchall()
添加 ORDER BY 将返回记录,交换 2 和 3 的位置,如下所示:
[(1, 4415.21026808109),
(3, 7059.45840502359),
(2, 7835.65405408195)]
相交的线
你知道lines的长度,通过在 Jupyter Notebook 中绘制lines,你知道lines 1和lines 3相交。在 PostGIS 中,你可以使用ST_Intersects()函数,传递几何体或地理体。数据库将返回true或false。
以下代码将在lines 1和lines 3上执行查询并返回True:
cu.execute("SELECT ST_Intersects(l.location::geography,ll.location::geometry)
FROM lines l, lines ll WHERE l.id=1 AND ll.id=3")
cu.fetchall()
之前的代码将返回True,因为lines 1和lines 3相交。但它们在哪里相交?使用ST_Intersection()将返回两条线相遇的点:
cu.execute("SELECT ST_AsText(ST_Intersection(l.location::geography,
ll.location::geometry)) FROM lines l, lines ll
WHERE l.id=1 AND ll.id=3")
cu.fetchall()
通过从ST_Intersects切换到ST_Intersection,你将得到两条线之间的接触点。该点如下:
[('POINT(-106.628684465508 35.1053370957485)',)]
数据库中的多边形
你也可以使用 PostGIS 存储多边形。以下代码将创建一个包含单个多边形的新表:
from shapely.geometry import Polygon
connection = psycopg2.connect(database="pythonspatial",user="postgres", password="postgres")
cursor = conectionn.cursor()
cursor.execute("CREATE TABLE poly (id SERIAL PRIMARY KEY, location GEOMETRY)")
a=Polygon([(-106.936763,35.958191),(-106.944385,35.239293),
(-106.452396,35.281908),(-106.407844,35.948708)])
cursor.execute("INSERT INTO poly (location)
VALUES (ST_GeomFromText('{}'))".format(a.wkt))
connection.commit()
之前的代码几乎与Point和Line示例相同。建立数据库连接,然后获取一个cursor。使用execute()创建表。导入shapely,构建你的几何体并将其插入到表中。最后,commit()更改。
之前的示例从数据库中选择了所有内容,并在 Jupyter Notebook 中绘制了几何体。以下代码将跳过这些步骤,而是返回到多边形的区域:
cur.execute("SELECT id, ST_Area(location::geography) from poly")
cur.fetchall()
使用ST_Area()和将几何体转换为地理体的方法,之前的代码应该返回以下平方米值:
[(1, 3550790242.52023)]
现在你已经知道表中有一个多边形,你可以学习如何在多边形内搜索一个点。
点在多边形内
最常见的问题之一是试图确定一个点是否在多边形内。要使用 PostGIS 解决这个问题,你可以使用ST_Contains或ST_Intersects。
St_Contains接受两个几何体并确定第一个是否包含第二个。
顺序很重要——a 包含 b,这与使用顺序 b,a 的 ST_Within 相反。
使用包含时,几何体 b 的任何部分都不能在几何体 a 之外。以下代码解决了一个点在多边形内(PIP)问题:
isin=Point(-106.558743,35.318618)
cur.execute("SELECT ST_Contains(polygon.location,ST_GeomFromText('{}'))
FROM poly polygon WHERE polygon.id=1".format(isin.wkt))
cur.fetchall()
之前的代码创建了一个点,然后使用ST_Contains(polygon,point)并返回True。该点位于多边形内。你可以使用ST_Contains与任何其他有效几何体。只需记住,它必须包含整个几何体才能为真。
判断一个点是否在多边形内的另一种方法是使用ST_Intersects。如果点或任何其他几何体与多边形重叠、接触或位于多边形内,ST_Intersects将返回True。ST_Intersects可以接受几何体或地理体。
以下代码将使用ST_Intersects执行一个 PIP:
isin=Point(-106.558743,35.318618)
cur.execute("SELECT ST_Intersects(ST_GeomFromText('{}')::geography,polygon.location::geometry) FROM poly polygon WHERE polygon.id=1".format(isin.wkt))
cur.fetchall()
之前的代码仅与ST_Contains示例中的函数不同,使用了几何体,并且返回True。当使用多边形和线时,如果线的任何部分接触到或位于多边形内,ST_Intersects将返回True。这与ST_Contains不同。
使用ST_Intersection,你可以获取表示交集的几何形状。在前面提到的lines例子中,它是一个点。在多边形和线的例子中,我将稍后展示,它将是一条线。以下代码使用ST_Intersection来获取与多边形相交的LineString:
isin=LineString([(-106.55,35.31),(-106.40,35.94)])
cur.execute("SELECT ST_AsText(ST_Intersection(polygon.location,ST_GeomFromText('{}')))
FROM poly polygon WHERE polygon.id=1".format(isin.wkt))
cur.fetchall()
之前的代码几乎与前面的例子相同,只是我们使用了 intersection(交集)与 intersects(相交)的区别。结果是得到一个LINESTRING:
[('LINESTRING(-106.55 35.31,-106.411712640251 35.8908069109443)',)]
摘要
本章涵盖了 PostgreSQL 和 PostGIS 的安装,以及psycogp2和 Shapely。然后,我们简要概述了在处理空间数据库时使用的的主要功能。你现在应该熟悉连接到数据库、执行插入数据的查询以及如何获取你的数据。此外,我们还介绍了返回新几何形状、距离和几何形状面积的功能。了解这些函数的工作原理应该允许你阅读 PostGIS 文档,并熟悉为该函数形成 SQL 语句。
在下一章中,你将学习 GIS 中的主要数据类型以及如何使用 Python 代码库读取和写入地理空间数据。你将学习如何在数据类型之间进行转换,以及如何从地理空间数据库和远程数据源上传和下载数据。
第四章:数据类型、存储和转换
本章将重点介绍 GIS 中存在的多种不同数据类型,并提供 GIS 中主要数据类型的概述以及如何使用之前提到的 Python 代码库来读取和写入地理空间数据。除了读取和写入不同的地理空间数据类型外,你还将学习如何使用这些库在不同数据类型之间进行文件转换,以及如何从地理空间数据库和远程源下载数据。
本章将涵盖以下矢量和栅格数据类型:
-
Shapefiles
-
GeoJSON
-
KML
-
GeoPackages
-
GeoTIFF
以下文件操作也将被涵盖,使用在第二章,地理空间代码库简介中介绍的 Python 地理空间数据库:
-
打开现有文件
-
读取和显示不同的属性(空间和非空间)
-
以不同格式创建和写入新的地理空间数据
-
将一种文件格式转换为另一种格式
-
下载地理空间数据
在我们开始编写读取和写入这些数据类型的代码之前,我们将概述最常用的 GIS 数据类型。接下来,我们将通过一些示例来解释如何使用各种 Python 库来读取、写入、下载和转换地理空间数据。我们将从解释地理空间数据代表什么以及矢量和栅格数据之间的区别开始。
栅格和矢量数据
在深入探讨一些最常用的 GIS 数据类型之前,需要先了解地理数据所代表的信息类型。本书早期提到了栅格数据和矢量数据之间的区别。所有 GIS 数据都是由其中一种或另一种组成,但也可以是两者的组合。在决定使用哪种数据类型时,要考虑数据所代表的地理信息的比例和类型,这反过来又决定了应该使用哪些 Python 数据库。正如以下示例所示,对某个 Python 库的选择也可能取决于个人偏好,并且可能存在多种完成同一任务的方法。
在地理空间领域,栅格数据以航空影像或卫星数据的形式出现,其中每个像素都有一个与之相关的值,该值对应于不同的颜色或阴影。栅格数据用于表示大范围的连续区域,例如区分世界各地的不同温度带。其他流行的应用包括高程、植被和降水图。
栅格数据也可以用作创建矢量地图的输入,例如,可以区分道路和建筑物等对象(例如,在导航到谷歌地图时的标准地图视图)。矢量数据本身由点、线和多边形组成,用于在地理空间中区分特征,如行政边界。这些特征是由具有空间关系的单个点组成的,这些关系在相关数据模型中进行了描述。矢量数据在放大时保持相同的清晰度,而栅格数据则会显得更加粗糙。
既然你已经知道了地理数据代表什么,那么让我们来讨论一下最常用的地理空间数据格式,用于矢量数据和栅格数据。
Shapefiles
Shapefile 可能是今天最常用于地理矢量数据的数据格式。这种文件格式是由 Esri 开发的,基于 Esri 和其他 GIS 软件产品之间数据互操作性的开放规范。尽管已经引入了许多其他文件格式试图取代 shapefile,但它仍然是一种广泛使用的文件格式。如今,许多第三方 Python 编程模块可用于读取和写入 shapefile。
尽管名称shapefile可能暗示与之相关联的只有一个文件,但实际上一个 shapefile 需要至少三个文件,这些文件需要存储在同一个目录中才能正确工作:
-
包含特征几何本身的
.shp文件 -
包含特征几何位置索引的
.shx文件,以便快速向前和向后搜索 -
包含每个形状的列属性
.dbf文件
Shapefiles 有其自身的结构。主文件(.shp)包含几何数据,由一个单一定长头部组成,后面跟着一个或多个变长记录。
GeoJSON
GeoJSON 是一种基于 JSON 的文件格式,在短时间内变得流行。GeoJSON 使用JavaScript 对象表示法(JSON)开放数据标准将地理特征存储为键值对。这些文件易于阅读,可以使用简单的文本编辑器创建,现在在空间数据库、开放数据平台以及商业 GIS 软件中都很常见。你可以使用 GeoJSON 处理各种类型的地理空间矢量数据,如点、线和多边形。GeoJSON 使用.json或.geojson作为文件扩展名。这意味着文件名不一定是.geojson才能是 GeoJSON 文件。
KML
关键孔标记语言 (KML),指的是开发该格式的公司。它可以用来存储地理数据,这些数据可以通过众多应用程序进行可视化,例如谷歌地球、Esri ArcGIS Explorer、Adobe Photoshop 和 AutoCAD。KML 基于 XML,使用基于标签的结构,具有嵌套元素和属性。KML 文件通常以 KMZ 文件的形式分发,这些是带有 .kmz 扩展名的压缩 KML 文件。对于其参考系统,KML 使用经度、纬度和海拔坐标,这些坐标由 1984 年世界大地测量系统 (WGS84) 定义。
GeoPackage
开放地理空间联盟 (OGC) 的 GeoPackage (GPKG) 是一种开放数据格式,用于支持矢量和栅格数据的地理信息系统。该格式由 OGC 定义并于 2014 年发布,此后得到了来自各个政府、商业和开源组织的广泛支持。GeoPackage 数据格式是在考虑到移动用户的情况下开发的——它被设计得尽可能高效,所有信息都包含在一个文件中。这使得它们在云存储和 USB 驱动器上快速共享变得容易,并且它被用于断开连接的移动应用程序中。GeoPackage 文件由一个扩展的 SQLite 3 数据库文件 (*.gpkg) 组成,该文件结合了数据和元数据表。
栅格数据格式
这些是目前用于地理信息的一些最流行的栅格数据格式:
-
ECW (增强压缩小波):ECW 是一种压缩图像格式,通常用于航空和卫星影像。这种 GIS 文件类型以其高压缩比而闻名,同时仍能保持图像中的质量对比度。
-
Esri 网格:一种用于向栅格文件添加属性数据的文件格式。Esri 网格文件可用作整数和浮点网格。
-
GeoTIFF (地理标记图像文件格式):一种用于 GIS 和卫星遥感应用的行业图像标准文件。几乎所有 GIS 和图像处理软件包都支持 GeoTIFF。
-
JPEG 2000:一种开源的压缩栅格格式,允许有损和无损压缩。JPEG 2000 通常具有 JP2 文件扩展名。JPEG 2000 可以实现 20:1 的压缩比,这与 MrSID 格式相似。
-
MrSID (多分辨率无缝图像数据库):一种允许有损和无损压缩的压缩小波格式。LizardTech 的专有 MrSID 格式常用于需要压缩的正射影像。MrSID 图像具有 SID 扩展名,并附带一个文件扩展名为 SDW 的世界文件。
使用 GeoPandas 读取和写入矢量数据
是时候进行一些动手练习了。我们将使用 GeoPandas 库读取和写入一些以 GeoJSON 形式存在的矢量数据,GeoPandas 库是演示所有示例的应用程序,它预安装在 Anaconda3 中。如果您已从第二章,“地理空间代码库简介”中安装了所有地理空间 Python 库,那么您就可以开始了。如果没有,请先这样做。您可能会决定为不同的 Python 库组合创建虚拟环境,因为不同的依赖关系和版本。打开一个新的 Jupyter Notebook 和一个浏览器窗口,转到www.naturalearthdata.com/downloads/并下载 Natural Earth 快速入门套件到方便的位置。我们将在本章的其余部分检查一些数据,以及一些其他地理数据文件。
首先,在具有 GeoPandas 库访问权限的 Jupyter Notebook 中输入以下代码并运行:
In: import geopandas as gpd
df = gpd.read_file(r'C:\data\gdal\NE\10m_cultural
\ne_10m_admin_0_boundary_lines_land.shp')
df.head()
输出如下所示:

代码执行以下操作——第一行导入 GeoPandas 库并缩短其名称,在以后引用它时节省空间。第二行从磁盘读取数据,在本例中是一个包含陆地边界线的 shapefile。它被分配给一个 dataframe 变量,该变量指的是一个pandas dataframe,即一个类似于 Excel 表的二维对象,具有行和列。GeoPandas 的数据结构是pandas子类,名称不同——GeoPandas 中的pandas dataframe 被称为GeoDataFrame。第三行打印属性表,仅限于前五行。运行代码后,单独单元格的输出将列出引用 shapefile 的属性数据。您会注意到 FID 列没有名称,并且已添加了一个作为最后一列的geometry列。
这不是读取数据的唯一命令,您还可以使用read_postgis()命令从 PostGIS 数据库中读取数据。接下来,我们将在 Jupyter Notebook 内部绘制数据:
In: %matplotlib inline
df.plot(color='black')
上一段代码的输出如下:

第一行是一个所谓的魔法命令,仅用于 Jupyter Notebook 内部,告诉它使用 Jupyter Notebook 应用程序单元格内的matplotlib库的绘图功能。这样,您可以直接绘制地图数据,而不是在 IDE 中工作。第二行表示我们想要绘制的 dataframe,颜色为black(默认颜色是蓝色)。输出类似于只有陆地边界的世界地图,这些边界以黑色线条可见。
接下来,我们将研究 GeoPandas 数据对象的一些属性:
In: df.geom_type.head()
Out: 0 LineString
1 LineString
2 MultiLineString
3 LineString
4 LineString
dtype: object
这告诉我们,我们的属性表中的前五个条目由线字符串和多行字符串组成。要打印所有条目,请使用相同的行代码,但不使用 .head():
In: df.crs
Out: {'init': 'epsg:4326'}
crs 属性指的是数据框的 坐标参考系统(CRS),在本例中为 epsg:4326,这是一个由 国际油气生产商协会(IOGP)定义的代码。有关 EPSG 的更多信息,请访问 www.spatialreference.org。CRS 提供了关于您的空间数据集的基本信息。EPSG 4326 也称为 WGS 1984,是地球的标准坐标系。
您可以将 CRS 更改为以下内容,以 Mercator 投影,显示更垂直拉伸的图像:
In: merc = df.to_crs({'init': 'epsg:3395'})
merc.plot(color='black')
上一段代码的输出如下:

假设我们想要将数据框的 shapefile 数据转换为 json。GeoPandas 可以用一行代码完成此操作,输出结果列在新单元格中:
In: df.to_json()
此前命令将数据转换为新的格式,但没有将其写入新文件。将数据框写入新的 geojson 文件可以这样做:
In: df.to_file(driver='GeoJSON',filename=r'C:\data\world.geojson')
不要被 JSON 文件扩展名所迷惑——包含空间数据的 JSON 文件是 GeoJSON 文件,尽管也存在单独的 .geojson 文件扩展名。
对于文件转换,GeoPandas 依赖于 Fiona 库。要列出所有可用的 drivers(一个软件组件,允许操作系统和设备相互通信),请使用以下命令:
In: import fiona; fiona.supported_drivers
使用 OGR 读取和写入矢量数据
现在,让我们转向 OGR 来读取和写入矢量数据,这样您就可以比较 OGR 和 GeoPandas 在执行相同类型任务时的功能。要遵循我们继续提到的说明,您可以下载 MTBS 火灾数据,从: edcintl.cr.usgs.gov/downloads/sciweb1/shared/MTBS_Fire/data/composite_data/fod_pt_shapefile/mtbs_fod_pts_data.zip 并将它们存储在您的电脑上。这里将要分析的是 mtbs_fod_pts_20170501 shapefile 的属性表,该表有 20,340 行和 30 列。
我们将从 ogrinfo 命令开始,该命令在终端窗口中运行,可以用于描述矢量数据。这些不是 Python 命令,但我们将在这里包括它们,因为您可以在 Jupyter Notebook 中通过简单的前缀(在使用的命令前添加感叹号)轻松运行它们。以以下命令为例,该命令类似于 Fiona 驱动器命令:
In: !ogrinfo –-formats
此命令列出ogrinfo可以访问的可用格式,通过使用通用选项--formats。结果还告诉我们 GDAL/OGR 是否只能读取/打开该格式,或者它是否还可以在该格式中写入新层。如输出所示,OGR 支持许多支持的文件格式。查看列表中的 Esri shapefiles,添加的(rw+v)表示 OGR 支持 Esri shapefiles 的读取、写入、更新(意味着创建)和虚拟格式:
In: !ogrinfo -so "pts" mtbs_fod_pts_20170501
之前的命令列出了数据源中所有层的摘要信息,在这种情况下是名为"pts"的文件夹中的所有 shapefile。添加的-so代表摘要选项。您可以看到,此命令列出了与 GeoPandas 中看到的信息类似的信息,例如 CRS。相同的代码行,但如果没有添加-so,将打印所有要素和属性,并且需要一些时间来处理。这与在 GeoPandas 中创建 GeoDataFrame 类似,但所有属性信息都是按特征在新行上打印,而不是保留表格形式:
In: !ogrinfo "pts" mtbs_fod_pts_20170501
如果我们要将此 shapefile 转换为 GeoJSON 文件,我们将使用以下命令:
In: !ogr2ogr -f "GeoJSON" "C:\data\output.json"
"C:\data\mtbs_fod_pts_data\mtbs_fod_pts_20170501.shp"
-f前缀代表格式,后面跟着输出驱动程序名称、输出文件名、位置和输入文件。在文件转换过程中可能会收到错误警告,例如遇到不良特征时,但无论如何都会写入输出文件。
OGR 也支持读取和写入 KML 文件。使用以下代码下载此 KML 样本文件(developers.google.com/kml/documentation/KML_Samples.kml)并运行以下代码来读取其内容:
In: !ogrinfo "C:\Users\UserName\Downloads\KML_Samples.kml" -summary
Out: Had to open data source read-only.INFO: Open of
`C:\Users\UserName\Downloads\KML_Samples.kml' using driver
`KML' successful.
1: Placemarks (3D Point)
2: Highlighted Icon (3D Point)
3: Paths (3D Line String)
4: Google Campus (3D Polygon)
5: Extruded Polygon (3D Polygon)
6: Absolute and Relative (3D Polygon)
对于 OGR 的 Pythonic 方法,让我们看看一些示例,了解如何使用 OGR 读取和写入数据。
以下代码使用 OGR 列出了我们野火 shapefile 的 30 个字段名称:
In: from osgeo import ogr
source = ogr.Open(r"C:\data\mtbs_fod_pts_data\
mtbs_fod_pts_20170501.shp")
layer = source.GetLayer()
schema = []
ldefn = layer.GetLayerDefn()
for n in range(ldefn.GetFieldCount()):
fdefn = ldefn.GetFieldDefn(n)
schema.append(fdefn.name)
print(schema)
Out: ['FIRE_ID', 'FIRENAME', 'ASMNT_TYPE', 'PRE_ID', 'POST_ID', 'ND_T',
'IG_T', 'LOW_T',
'MOD_T', 'HIGH_T', 'FIRE_YEAR', 'FIRE_MON', 'FIRE_DAY', 'LAT',
'LONG', 'WRS_PATH',
'WRS_ROW', 'P_ACRES', 'R_ACRES', 'STATE', 'ADMIN', 'MTBS_ZONE',
'GACC',
'HUC4_CODE','HUC4_NAME', 'Version', 'RevCode', 'RelDate',
'Fire_Type']
如前述代码所示,这比使用 GeoPandas 要复杂一些,在 GeoPandas 中,您可以使用少量代码直接将所有属性数据加载到一个 GeoDataFrame 中。使用 OGR,您需要遍历单个要素,这些要素需要从层定义中引用并附加到空列表中。但首先,您需要使用GetLayer函数——这是因为 OGR 有自己的数据模型,它不会自动适应它所读取的文件格式。
现在我们有了所有字段名称,我们可以遍历单个要素,例如,对于州字段:
In: from osgeo import ogr
import os
shapefile = r"C:\data\mtbs_fod_pts_data\mtbs_fod_pts_20170501.shp"
driver = ogr.GetDriverByName("ESRI Shapefile")
dataSource = driver.Open(shapefile, 0)
layer = dataSource.GetLayer()
for feature in layer:
print(feature.GetField("STATE"))
从最后一个单元的输出来看,显然有很多要素,但确切有多少呢?可以使用以下方式打印总要素计数:
In: import os
from osgeo import ogr
daShapefile = r"C:\data\mtbs_fod_pts_data\
mtbs_fod_pts_20170501.shp"
driver = ogr.GetDriverByName("ESRI Shapefile")
dataSource = driver.Open(daShapefile, 0)
layer = dataSource.GetLayer()
featureCount = layer.GetFeatureCount()
print("Number of features in %s: %d" %
(os.path.basename(daShapefile), featureCount))
Out: Number of features in mtbs_fod_pts_20170501.shp: 20340
正如我们之前看到的,CRS 是关于您空间数据的重要信息。您可以通过两种方式打印此信息——从层和层的几何形状。在以下代码中,两个空间参考变量将打印相同的输出,正如它应该的那样(这里只列出了第一个选项的输出以节省空间):
In: from osgeo import ogr, osr
driver = ogr.GetDriverByName('ESRI Shapefile')
dataset = driver.Open(r"C:\data\mtbs_fod_pts_data\
mtbs_fod_pts_20170501.shp")
# Option 1: from Layer
layer = dataset.GetLayer()
spatialRef = layer.GetSpatialRef()
print(spatialRef)
# Option 2: from Geometry
feature = layer.GetNextFeature()
geom = feature.GetGeometryRef()
spatialRef2 = geom.GetSpatialReference()
print(spatialRef2)
Out: GEOGCS["GCS_North_American_1983",
DATUM["North_American_Datum_1983",
SPHEROID["GRS_1980",6378137.0,298.257222101]],
PRIMEM["Greenwich",0.0],
UNIT["Degree",0.0174532925199433]]
我们可以检查我们是否在处理点,并按照以下方式打印所有单个特征的 x 和 y 值以及它们的质心:
In: from osgeo import ogr
import os
shapefile = r"C:\data\mtbs_fod_pts_data\mtbs_fod_pts_20170501.shp"
driver = ogr.GetDriverByName("ESRI Shapefile")
dataSource = driver.Open(shapefile, 0)
layer = dataSource.GetLayer()
for feature in layer:
geom = feature.GetGeometryRef()
print(geom.Centroid().ExportToWkt())
使用 Rasterio 读取和写入栅格数据
在介绍了如何在 Python 中读取和写入各种矢量数据格式之后,我们现在将做同样的事情来处理栅格数据。我们将从 Rasterio 库开始,看看我们如何读取和写入栅格数据。打开一个新的 Jupyter Notebook,其中包含对 Rasterio 库的访问权限,并输入以下代码:
In: import rasterio
dataset = rasterio.open(r"C:\data\gdal\NE\50m_raster\NE1_50M_SR_W
\NE1_50M_SR_W.tif")
这导入 rasterio 库并打开一个 GeoTIFF 文件。我们现在可以执行一些简单的数据描述命令,例如打印图像带的数量。
栅格图像包含单个或多个带。所有带都包含在单个文件中,每个带覆盖相同的区域。当您的计算机读取图像时,这些带将叠加在一起,因此您将看到一个单独的图像。每个带包含一个二维数组,具有行和列的数据。每个数组的每个数据单元包含一个与颜色值(或可能的高度值)相对应的数值。如果一个栅格图像有多个带,则每个带对应于传感器收集的电磁谱的一部分。用户可以显示一个或多个带,将不同的带组合在一起以创建自己的彩色合成。第九章,ArcGIS API for Python 和 ArcGIS Online 在讨论使用 ArcGIS API for Python 显示栅格数据时提供了一些这些彩色合成的示例。
在这种情况下,有三个不同的带:
In: dataset.count
Out: 3
一个 dataset 带是一个表示二维空间中单个变量部分分布的值数组。列数由 width 属性返回:
In: dataset.width
Out: 10800
行数由 height 属性返回:
In: dataset.height
Out: 5400
以下代码返回以米为单位的空间边界框,因此您可以计算它覆盖的区域:
In: dataset.bounds
Out: BoundingBox(left=-179.99999999999997, bottom=-89.99999999998201,
right=179.99999999996405, top=90.0)
数据集的 CRS 可以按照以下方式打印:
In: dataset.crs
Out: CRS({'init': 'epsg:4326'})
您可以访问并返回一个 NumPy ndarray,该 ndarray 表示栅格带的栅格数组,如下所示:
In: band1 = dataset.read(1)
band1
Out: array([[124, 124, 124, ..., 124, 124, 124], ...
如果您想可视化图像,请使用以下代码:
In: %matplotlib inline
from matplotlib import pyplot
pyplot.imshow(dataset.read(1))
pyplot.show()
输出地图将看起来像这样:

使用 GDAL 读取和写入栅格数据
这里有一些使用 GDAL 读取和写入栅格数据的命令:
In: !gdalinfo --formats
此命令列出了 GDAL 支持的所有文件格式。要包括 CRS 的摘要,请使用 !gdalinfo 而不带任何前缀:
In: !gdalinfo "C:\data\gdal\NE\50m_raster\NE1_50M_SR_W
\NE1_50M_SR_W.tif"
Out: Driver: GTiff/GeoTIFF
Files: C:\data\gdal\NE\50m_raster\NE1_50M_SR_W\NE1_50M_SR_W.tif
Size is 10800, 5400
Coordinate System is:
GEOGCS"WGS 84",
DATUM["WGS_1984", ...
您可以将 GeoTIFF 转换为 JPEG 文件,如下所示:
In: !gdal_translate -of JPEG
"C:\data\gdal\NE\50m_raster\NE1_50M_SR_W\NE1_50M_SR_W.tif"
NE1_50M_SR_W.jpg
Out: Input file size is 10800, 5400
0...10...20...30...40...50...60...70...80...90...100 - done.
输出,NE1_50M_SR_W.jpg,将看起来像这样:
![
现在,让我们使用 GDAL 打开一个 GeoPackage。GeoPackages 可以是基于矢量或栅格的,但在这个例子中,我们将打开一个基于栅格的 GeoPackage,这从下面的输出中可以清楚地看出。对于读取和写入 GeoPackages,我们需要 GDAL 版本 2.2.2,因此以下示例对于较低版本号将不会工作。下载以下 GeoPackage 文件(www.geopackage.org/data/gdal_sample_v1.2_no_extensions.gpkg)并按照以下方式引用:
In: !gdalinfo
"C:\Users\UserName\Downloads\gdal_sample_v1.2_no_extensions.gpkg"
Out: Driver: GPKG/GeoPackageFiles:
C:\Users\UserName\Downloads\gdal_sample_v1.2_no_extensions.gpkg
Size is 512, 512
Coordinate System is''
...
GDAL 的Web 地图服务(WMS)驱动程序允许与在线网络地图服务进行交互。你可以使用它从命令提示符(或在这种情况下,Jupyter Notebook)直接下载各种地理空间数据集、子集或有关可用数据集的信息,而无需使用浏览器导航到网站并手动下载数据。有许多不同的选项,因此请参考在线文档以获取更多信息。以下示例需要 GDAL 版本 2.0 或更高版本。以下命令使用 ArcGIS MapServer 的表示性状态转移(REST)定义的 URL,并返回有关请求的图像服务的信息,例如波段数量、波段名称、CRS、角坐标等:
In: !gdalinfo http://server.arcgisonline.com/ArcGIS/rest/services/
World_Imagery/MapServer?f=json&pretty=true
注意,你向图像服务的 URL 中添加了一些信息,在这种情况下,f=json&pretty=true。这意味着用户请求的文件格式是pretty json,这是一种格式良好的json,更容易供人类阅读。
摘要
本章概述了 GIS 中的主要数据类型。在解释了矢量和栅格数据之间的区别之后,接下来介绍了以下矢量栅格数据类型——Esri 形状文件、GeoJSON、KML、GeoPackages 和 GeoTIFF 文件。然后,我们解释了如何使用一些之前描述的 Python 代码库来读取和写入地理空间数据。特别介绍了以下用于读取和写入栅格和矢量数据的地理空间 Python 库——GeoPandas、OGR、GDAL 和 Rasterio。除了读取和写入不同的地理空间数据类型之外,你还学习了如何使用这些库在不同数据类型之间进行文件转换,以及如何从地理空间数据库和远程源上传和下载数据。
下一章将介绍地理空间分析和处理。涉及到的 Python 库有 OGR、Shapely 和 GeoPandas。读者将学习如何使用这些库编写用于地理空间分析的脚本,并使用实际案例进行学习。
第五章:向量数据分析
本章将涵盖地理空间分析和处理向量数据。以下三个 Python 库将被介绍——Shapely、OGR 和 GeoPandas。读者将学习如何使用这些 Python 库进行地理空间分析,包括编写基本和高级分析脚本。
每个库都将单独介绍,其中适当的地方将概述其数据结构、方法和类。我们将讨论每个库的最佳用例以及如何将它们结合起来用于地理空间工作流程。简短的示例脚本将说明如何执行基本的地理分析。GeoPandas 库为执行数据科学任务和结合地理空间分析提供了更复杂的功能。
在本章中,我们将涵盖以下主题:
-
读取和写入向量数据
-
创建和操作向量数据
-
在地图上可视化(绘图)向量数据
-
处理地图投影和重新投影数据
-
执行空间操作,如空间连接
-
在表格形式中处理向量几何和属性数据
-
分析结果以回答问题,例如在区域 x 中有多少次野火?
在本章之后,你将拥有一个坚实的基础,开始处理地理空间向量数据。你将了解所有三个地理空间库的特点和应用场景,并知道如何进行基本的向量数据处理和分析。
OGR 简单特征库
OGR 简单特征库(属于地理空间数据抽象库(GDAL)的一部分)提供了一套处理向量数据的工具。尽管 GDAL 和 OGR 现在比以前更加集成,但我们仍然可以将 GDAL 区分为向量部分(OGR)和栅格部分(GDAL)。虽然 OGR 是用 C++ 编写的,文档也是用 C++ 编写的,但通过 Python 绑定,我们可以使用 Python 访问 GDAL 的所有功能。
我们可以区分以下 OGR 的组成部分:
-
用于描述和处理向量数据的 OGR 批处理命令
-
ogrmerge,一个用于合并多个向量数据文件的即时 Python 脚本 -
OGR 库本身
在介绍一些如何使用这三个库的示例之前,我们将简要介绍这些组件。
OGR 批处理命令
OGR 提供了一系列批处理命令,可用于描述和转换现有的地理空间向量数据。我们已经在第四章,数据类型、存储和转换中提到了其中两个,ogrinfo和ogr2ogr:
-
ogrinfo可用于对向量数据进行各种报告,例如列出支持的向量格式、可用图层和摘要细节,并且可以与 SQL 查询语法结合,从数据集中选择特征。 -
ogr2ogr用于执行矢量数据转换,例如在不同格式之间转换矢量文件,将多个图层转换为新的数据源,以及根据位置重新投影矢量数据和过滤特征。它也可以像ogrinfo一样使用 SQL 查询语法。
这些是非常强大的命令,可以让您完成大量工作。建议您在工作处理矢量数据时熟悉这些命令。我们很快会提供一些示例。
此外,还有两个用于创建矢量瓦片的批处理命令,ogrtindex和ogr2vrt。这两个命令之间的区别在于,第二个命令比第一个命令更通用。第二个命令需要从在线脚本中导入,因为它不包括在最近的 GDAL 版本中。
ogrmerge
随着 GDAL 的安装,还附带了一套 Python 脚本,可用于专门的地理空间任务。这些脚本可以直接从 Jupyter Notebook 或终端运行,并指定数据集。您可以在本地gdal文件文件夹的scripts目录中找到所有这些脚本,在 Windows 机器上可能类似于以下路径:
C:\Users\Username\Anaconda3\pkgs\gdal-2.2.2-py36_1\scripts
如您从该文件夹中的 Python 脚本列表中看到的,几乎所有的脚本都是针对 GDAL 而不是 OGR 的。所有这些 Python 脚本都可以从 Jupyter Notebook 或终端运行。使用 Jupyter Notebook,您可以使用魔法命令%run来执行 Python 脚本,而使用终端,您将使用python后跟脚本名称和输入/输出数据文件。
魔法命令是扩展核心 Python 语言的命令,并且只能在 Jupyter Notebook 应用程序中使用。它们提供了有用的快捷方式,例如,从外部脚本插入代码,以及从磁盘上的.py文件或shell命令中执行 Python 代码。可以使用以下命令在空单元格中打印出所有魔法命令的完整列表:%lsmagic。
以下示例使用ogrmerge.py,这是 GDAL 2.2.2 及更高版本中可用的一个 Python 脚本。从 Jupyter Notebook 中运行此脚本,它将地球数据集中单个文件夹中的所有 shapefile 合并成一个名为merged.gpkg的单个 GeoPackage 文件:
In: %run "C:\Users\Eric\Anaconda3\pkgs\gdal-2.2.2-
py36_1\Scripts\ogrmerge.py" -f GPKG -o
merged.gpkg "C:\data\gdal\NE\10m_cultural\*.shp"
请注意,为了正确运行 GDAL 目录中的一个 Python 脚本,如果您在运行脚本的不同文件夹中,需要参考它们的文件位置,如果您在使用 Jupyter Notebook 应用程序工作,这种情况很可能是这样。
OGR 库和 Python 绑定
OGR 库及其 Python 绑定结合在一起,是使用 Python 处理矢量数据最重要的部分。有了它,你可以创建点、线和多边形,并对这些元素执行空间操作。例如,你可以计算几何形状的面积,将不同的数据叠加在一起,并使用如缓冲区之类的邻近工具。此外,就像 ogrinfo 和 ogr2ogr 一样,OGR 库提供了读取矢量数据文件、遍历单个元素以及选择和重新投影数据的工具。
OGR 的主要模块和类
OGR 库由两个主要模块组成——ogr 和 osr。这两个都是 osgeo 模块内的子模块。ogr 子模块处理矢量几何,而 osr 则全部关于投影。在 第四章 的 使用 OGR 读取和写入矢量数据 部分,数据类型、存储和转换,我们已经看到了一些如何利用这两个模块的例子。
OGR 提供以下七个类:
-
几何形状 -
空间参考 -
要素 -
要素类定义 -
图层 -
数据集 -
驱动程序
类名大多一目了然,但了解 OGR 的结构概述是很好的。在以下示例中,我们将看到如何访问和使用这些类。OGR 的模块、类和函数在 GDAL 网站上有所记录(www.gdal.org/python),但未提供代码示例,这使得入门变得困难。现在值得知道的是,其他 Python 库填补了这一空白,并提供了更用户友好的方式来处理 GDAL 的功能(如 Fiona 和 GeoPandas)。此外,在某些用例中,ogrinfo 和 ogr2ogr 可能比使用 Python 更可取,例如,在重新投影矢量数据时。
让我们看看几个 OGR 的例子。
使用 OGR 创建多边形几何形状
OGR 允许你写入矢量几何,如点、线、多点、多线字符串、多边形和几何集合。如果你计划稍后进行投影,你可以用坐标或米来给出这些几何值。你创建的所有几何形状都遵循相同的程序,单独的点被定义并连接成线或多边形。你用数字定义单独的实体,用已知二进制(WKB)进行编码,最终的多边形被转换为已知文本(WKT)。Jupyter Notebook 会返回多边形的坐标,但不会自动绘制它,为此,我们将在本章后面使用 Shapely:
In: from osgeo import ogr
r = ogr.Geometry(ogr.wkbLinearRing)
r.AddPoint(1,1)
r.AddPoint(5,1)
r.AddPoint(5,5)
r.AddPoint(1,5)
r.AddPoint(1,1)
poly = ogr.Geometry(ogr.wkbPolygon)
poly.AddGeometry(r)
print(poly.ExportToWkt())
Out: POLYGON ((1 1 0,5 1 0,5 5 0,1 5 0,1 1 0))
从 GeoJSON 创建多边形几何形状
你也可以通过将 GeoJSON 传递给 OGR 来创建几何形状,与第一个例子相比,这样可以节省空间:
In: from osgeo import ogr
geojson = """{"type":"Polygon","coordinates":[[[1,1],[5,1],
[5,5],[1,5], [1,1]]]}"""
polygon = ogr.CreateGeometryFromJson(geojson)
print(polygon)
Out: POLYGON ((1 1,5 1,5 5,1 5,1 1))
基本几何操作
这里有一些基本几何操作,我们可以对我们的多边形执行。我们创建面积、质心、边界、凸包、缓冲区,并检查多边形是否包含某个点:
# 1 create area
In: print("The area of our polygon is %d" % polygon.Area())
Out: The area of our polygon is 16
# 2 calculate centroid of polygon
In: cen = polygon.Centroid()
print(cen)
Out: POINT (3 3)
# 3 Get the boundary
In: b = polygon.GetBoundary()
print(b)
Out: LINESTRING (1 1,5 1,5 5,1 5,1 1)
# 4 convex hull does the same in this case as boundary, as our polygon is a square:
In: ch = polygon.ConvexHull()
print(ch)
Out: POLYGON ((1 1,1 5,5 5,5 1,1 1))
# 5 buffer. A buffer value of 0 (zero) returns the same values as boundary and convex hull in this example:
In: buffer = polygon.Buffer(0)
print(buffer)
Out: POLYGON ((1 1,1 5,5 5,5 1,1 1))# 6 check if a point is inside our polygon
In: point = ogr.Geometry(ogr.wkbPoint)
point.AddPoint(10, 10)
polygon.Contains(point)
Out: False
将多边形数据写入新创建的 shapefile
我们当前的多边形仅存在于内存中。我们可以创建一个新的 shapefile,并将我们之前创建的多边形几何形状写入此 shapefile。脚本包括以下步骤:
-
导入模块并设置空间参考(在这种情况下,世界大地测量系统 1984(WGS1984))。
-
创建 shapefile,然后使用多边形几何创建图层。接下来,将几何形状放入特征中,并将特征放入图层中。注意,脚本直接引用了早期示例中的多边形。
-
关键是要在代码的第一行使用正确的几何类型,在这种情况下应该是
wkbPolygon。 -
我们在早期示例中使用的多边形几何形状在本步骤中被引用并放入了 shapefile 中。
-
在此步骤中将 shapefile 添加为图层。
看看以下代码:
In: import osgeo.ogr, osgeo.osr
# 1 set the spatial reference
spatialReference = osgeo.osr.SpatialReference()
spatialReference.ImportFromProj4('+proj=longlat +ellps=WGS84
+datum=WGS84 +no_defs')
# 2 create a new shapefile
driver = osgeo.ogr.GetDriverByName('ESRI Shapefile')
shapeData = driver.CreateDataSource('my_polygon.shp')
# 3 create the layer
layer = shapeData.CreateLayer('polygon_layer', spatialReference,
osgeo.ogr.wkbPolygon)
layerDefinition = layer.GetLayerDefn()
# 4 geometry is put inside feature
featureIndex = 0
feature = osgeo.ogr.Feature(layerDefinition)
feature.SetGeometry(polygon)
feature.SetFID(featureIndex)
# 5 feature is put into layer
layer.CreateFeature(feature)
我们可以使用ogrInfo来查看文件是否已正确创建:
In: !ogrinfo my_polygon.shp
Out: INFO: Open of `my_polygon.shp'
using driver `ESRI Shapefile' successful.
1: my_polygon (Polygon)
使用空间过滤器选择要素
此示例使用在第四章,“数据类型、存储和转换”部分中介绍的 Natural Earth Dataset。我们将在“使用 GeoPandas 读取和写入矢量数据”小节中使用经纬度坐标来创建一个边界框形式的空问过滤器。这个框只选择框内的数据。这是一种处理数据子集的方法。我们将使用 OGR 的SpatialFilterRec方法,该方法接受四个值——minx、miny、maxx和maxy,来创建一个边界框。我们的(随机)示例是选择边界框内的城市(显示德克萨斯州以及俄克拉荷马州和墨西哥的部分)。为了进一步过滤我们的结果,我们只想选择美国内的城市。这意味着我们必须在for循环中添加额外的if/else语句来过滤我们的搜索结果。
网站 www.mapsofworld.com 为我们提供的示例代码如下四个值:-102 (minx),26 (miny),-94 (maxx),和36 (maxy),用于德克萨斯州。以下是脚本:
In: # import the modules
from osgeo import ogr
import os
# reference the shapefile and specify driver type
shapefile =
r"C:\data\gdal\NE\10m_cultural\ne_10m_populated_places.shp"
driver = ogr.GetDriverByName("ESRI Shapefile")
# open the data source with driver, zero means open in read-only
mode
dataSource = driver.Open(shapefile, 0)
# use the GetLayer() function for referencing the layer that holds
the data
layer = dataSource.GetLayer()
# pass in the coordinates for the data frame to the
SetSpatialFilterRect() function. This filter creates a rectangular
extent and selects the features
inside the extent
layer.SetSpatialFilterRect(-102, 26, -94, 36)
for feature in layer:
# select only the cities inside of the USA
# we can do this through a SQL query:
# we skip the cities that are not in the USA,
# and print the names of the cities that are
if feature.GetField("ADM0NAME") != "United States of
America":
continue
else:
print(feature.GetField("NAME"))
Out: Ardmore
McAlester
Bryan
San Marcos
Longview
…
Shapely 和 Fiona
Shapely 和 Fiona 库在第二章,“地理空间代码库简介”部分中介绍,包括Shapely和Fiona小节。将它们一起介绍是有意义的,因为 Shapely 依赖于其他库来读取和写入文件,而 Fiona 则符合这一要求。正如我们将在示例中看到的那样,我们可以使用 Fiona 打开和读取文件,然后将几何数据传递给 Shapely 对象。
Shapely 对象和类
Shapely 库用于创建和操作 2D 矢量数据,无需空间数据库。它不仅摒弃了数据库,还摒弃了投影和数据格式,只关注几何形状。Shapely 的强大之处在于它使用易于阅读的语法创建各种几何形状,这些形状可用于几何运算。
在其他 Python 包的帮助下,这些几何形状和几何运算的结果可以被写入矢量文件格式,并在必要时进行投影——我们将结合 pyproj 和 Fiona 的功能来介绍示例。一个可能的工作流程示例是使用 Fiona 从 shapefile 中读取矢量几何形状,然后使用 Shapely 简化或清理现有的几何形状,以防内部或与其他几何形状的组合可能正确对齐。清理后的几何形状可以用作其他工作流程的输入,例如创建专题地图或执行数据科学。
Shapely 库使用一组类,这些类是三种基本几何对象类型(点、曲线和表面)的实现。如果你熟悉地理空间数据和它们的几何形状,它们会听起来很熟悉。如果你不熟悉,请使用示例来熟悉它们:
| 几何对象名称 | 类名 |
|---|---|
| 点集合 | Point |
| 曲线集合 | LineString, LinearRing |
| 表面 | Polygon |
| 点集合 | MultiPoint |
| 曲线集合 | MultiLineString |
| 表面集合 | MultiPolygon |
Shapely 的地理空间分析方法
拓扑关系作为几何对象上的方法实现(例如,包含、接触等)。Shapely 还提供了返回新几何对象的分析方法(交集、并集等)。通过知名格式(WKT 和 WKB)、NumPy + Python 数组和 Python 地理接口提供与其他软件的互操作性。
Fiona 的数据模型
虽然 Fiona 是 OGR 的 Python 封装器,但 Fiona 使用的数据模型与 OGR 不同。OGR 使用数据源、层和要素,而 Fiona 使用术语记录来访问存储在矢量数据中的地理要素。这些基于 GeoJSON 特性——使用 Fiona 读取 shapefile 时,你通过其中一个键引用记录,使用 Python 字典对象。记录有一个 ID、几何形状和属性键。
让我们看看一些 Shapely 和 Fiona 的代码示例。
使用 Shapely 创建几何形状
就像 OGR 一样,你可以使用 Shapely 来创建几何形状。在创建几何形状后,Jupyter Notebook 会像 OGR 一样绘制这些几何形状,而不需要使用额外的绘图语句,只需重复用于存储几何形状的变量名即可:
In: from shapely.geometry import Polygon
p1 = Polygon(((1, 2), (5, 3), (5, 7), (1, 9), (1, 2)))
p2 = Polygon(((6,6), (7,6), (10,4), (11,8), (6,6)))
p1
# A new command line is required for printing the second polygon:
In: p2
# Point takes tuples as well as positional coordinate values
In: from shapely.geometry import Point
point = Point(2.0, 2.0)
q = Point((2.0, 2.0))
q
# line geometry
In: from shapely.geometry import LineString
line = LineString([(0, 0), (10,10)])
line
# linear rings
In: from shapely.geometry.polygon import LinearRing
ring = LinearRing([(0,0), (3,3), (3,0)])
ring
# collection of points
In: from shapely.geometry import MultiPoint
points = MultiPoint([(0.0, 0.0), (3.0, 3.0)])
points
# collection of lines
In: from shapely.geometry import MultiLineString
coords = [((0, 0), (1, 1)), ((-1, 0), (1, 0))]
coords
# collection of polygons
In: from shapely.geometry import MultiPolygon
polygons = MultiPolygon([p1, p2,])
polygons
使用 Shapely 应用几何方法
与 OGR 类似,你可以应用几何方法,使用前面示例中的多边形:
In: print(p1.area)
print(p1.bounds)
print(p1.length)
print(p1.geom_type)
Out: 22.0
(1.0, 2.0, 5.0, 9.0)
19.59524158061724
Polygon
使用 Shapely 读取 JSON 几何形状
虽然 Shapely 不读取或写入数据文件,但你可以通过从库外部访问几何形状,例如,通过提供以 json 编写的矢量数据来访问它。以下脚本创建了一个在行中读取到 Shapely 的 json 多边形。接下来,映射命令返回一个新的、独立的几何对象,其坐标来自上下文:
In: import json
from shapely.geometry import mapping, shape
p = shape(json.loads('{"type": "Polygon", "coordinates":
[[[1,1], [1,3 ], [3,3]]]}'))
print(json.dumps(mapping(p)))
p.area
Out: {"type": "Polygon", "coordinates": [[[1.0, 1.0], [1.0, 3.0],
[3.0, 3.0], [1.0, 1.0]]]}
2.0 # result of p.area
使用 Fiona 读取数据
以下代码从我们的 Natural Earth 数据集中读取一个文件并打印其字典键:
In: import fiona
c = fiona.open(r"C:\data\gdal\NE\
110m_cultural\ne_110m_admin_1_states_provinces.shp")
rec = next(iter(c))
rec.keys()
Out: dict_keys(['type', 'id', 'geometry', 'properties'])
使用 Python 标准库中的数据格式化打印库 (pprint),我们可以将数据集中第一个特征对应的值打印到键上:
In: import pprint
pprint.pprint(rec['type'])
pprint.pprint(rec['id'])
pprint.pprint(rec['properties'])
pprint.pprint(rec['geometry'])
Out: 'Feature'
'0'
OrderedDict([('adm1_code', 'USA-3514'),
('diss_me', 3514),
('iso_3166_2', 'US-MN'),
('wikipedia',
'http://en.wikipedia.org/wiki/Minnesota'),
('iso_a2', 'US'),
('adm0_sr', 1),
('name', 'Minnesota'), ….
在数据文件对象上使用以下方法来打印以下信息:
In: print(len(c)) # prints total amount of features
print(c.driver) # prints driver name
print(c.crs) # prints coordinate reference system of data file
Out: 51
ESRI Shapefile
{'init': 'epsg:4326'}
使用 Shapely 和 Fiona 在 shapefile 中访问矢量几何
使用 Fiona,你可以打开一个 shapefile 并访问属性数据,例如几何形状。例如,我们的 Natural Earth 数据集包含一个包含美国所有州及其矢量几何的 shapefile。使用以下代码打开 shapefile 并获取第一个特征的所有矢量几何(从索引号0开始):
In: import pprint, fiona
with fiona.open\
(r"C:\data\gdal\NE\110m_cultural\ne_110m_admin_1_states_provinc
es.shp") as src:
pprint.pprint(src[0])
我们可以使用 shape 方法并传入明尼苏达州的所有坐标:
In: from shapely.geometry import shape
minnesota = {'type': 'Polygon', 'coordinates':
[[(-89.61369767938538, 47.81925202085796), (-89.72800594761503,
47.641976019880644), (-89.84283098016755, 47.464725857119504),
(-89.95765601272012, 47.286907253603175),....]]}
接下来,我们使用 Shapely 绘制几何形状:

关于在 Python 中绘制单独的 shapefile 几何形状的注意事项:
如前文所述,从 shapefile 中引用单独的几何元素,如一个州,并用 Python 绘制它们并不直接。幸运的是,有许多代码示例可供专业人士解决此问题。查看以下免费提供的 Python 用户选项,了解如果你决定直接使用 shapefile 而不是转换为 GeoJSON 格式,如何处理 Python 中的 shapefile 矢量几何绘图:
-
使用 NumPy 数组并
matplotlib:**你可以使用 NumPy 数组将所有坐标挤压到一个一维数组中,然后绘制这些坐标。 -
使用 Shapely 并从现有 shapefile 创建一个新的字典:**如果你知道如何重新组织现有的字典集合,则可以从现有的 shapefile 中创建一个新的字典,该字典使用地理区域的名称作为键,该区域的几何数据作为值。接下来,你可以使用 Shapely 将这些字典的元素传递进去,并在 Python 中绘制它们。
-
使用
pyshp和matplotlib:**pyshp库可以用来读取几何信息,然后可以使用matplotlib进行绘图。 -
使用 GeoPandas 和
matplotlib:**GeoPandas 库可以与 matplotlib 一起使用来读取 shapefile。不仅可以使用 matplotlib 的功能绘制矢量数据,还可以将属性表作为pandas数据框读取。
GeoPandas
GeoPandas 已在第二章的“地理空间代码库简介”部分的GeoPandas小节中介绍,其中也涵盖了其数据结构和方法。
使用 GeoPandas 进行地理空间分析
GeoPandas 是为了向希望使用类似于 pandas 的空间数据的科学家提供数据而创建的,这意味着通过数据结构提供对地理空间属性数据的访问,这些数据结构在 pandas 中不可用。结合一组几何运算、数据叠加功能、地理编码和绘图功能,您就可以了解这个库的功能。在我们继续提到的示例中,我们将涵盖 GeoPandas 的绘图方法,解释如何访问和子集空间数据,并提供使用 GeoPandas 进行地理空间分析的典型工作流程,其中数据处理是正确分析和解释数据的重要条件。
让我们看看一些 GeoPandas 的代码示例。
使用 GeoPandas 和 Matplotlib 选择和绘制几何数据
以下脚本结合了 GeoPandas GeoDataFrame 对象上的 pandas 数据框方法。一起使用,您可以轻松地子集数据并绘制单独的特征几何图形。我们从导入模块开始,这是在 Jupyter Notebook 内部绘制数据的神奇命令,以及输入数据,这是一个包含所有美国州边界的 shapefile:
In: import geopandas as gpd
%matplotlib inline
df = gpd.read_file\
(r"C:\data\gdal\NE\110m_cultural\ne_110m_admin_1_states_provinces.shp" )
df
一些简单的数据检查方法——type(df) 返回对象类型,它是一个 GeoPandas GeoDataFrame,它接受与 pandas 数据框相同的方法。shape 方法返回一个包含行数和列数的元组,而 df.columns 返回列名列表:
In: type(df)
Out: geopandas.geodataframe.GeoDataFrame
In: df.shape
Out: (51, 61)
In: df.columns
Out: Index(['adm1_code', 'diss_me', 'iso_3166_2', 'wikipedia', ...
我们可以使用 pandas、.loc 和 .iloc 方法来子集我们的 GeoDataFrame 的单独行。我们访问第一个特征的属性,如下所示:
In: df.loc[0]
Out: adm1_code USA-3514
diss_me 3514
iso_3166_2 US-MN
Wikipedia http://en.wikipedia.org/wiki/Minnesota
iso_a2 US
adm0_sr 1
name Minnesota
… …
现在,我们将绘制一些州的数据。首先,我们将获取所有州名的列表,因为我们需要州名及其行号:
In: df['name']
Out: 0 Minnesota
1 Montana
2 North Dakota
3 Hawaii
4 Idaho
5 Washington
… …
可以使用 .loc 和一个值来通过 name 而不是行号引用单独的行。重复 name 值将返回所有列和属性数据:
In: california = df.loc[df['name'] == "California"]
california
您可以按照以下方式绘制这个变量的几何图形:
In: california.plot(figsize=(7,7))
下面是这个图表的样子:

您可以通过使用 .iloc 函数并传递一个行号列表来绘制多个项目;在这种情况下,行号分别对应华盛顿、加利福尼亚、内华达和俄勒冈:
In: multipl = df.iloc[[5,7,9,11]]
multipl.plot(cmap="Set1", figsize=(7,7))
输出的图表将看起来像这样:

使用 .cx 方法在 GeoDataFrame 上也可以获得相同的结果,传入边界框的值。此方法使用以下语法:df.cx[xmin:xmax, ymin:ymax]:
In: exp = df.cx[-124:-118,30:50]
exp.plot(cmap="Set1", figsize=(7,7))
使用 GeoPandas 绘制野火数据
以下脚本可以用来创建一个表示 1984-2015 年美国总野火数量的等值线图,基于每个州的计数。我们可以使用在第四章中引入的 MTBS 火数据,数据类型、存储和转换,它给我们 1984-2015 年所有野火发生点的数据。我们可以使用野火数据的州字段来按州映射野火发生。但,我们选择在这里将数据叠加到一个包含州几何形状的单独形状文件上,以说明空间连接的使用。接下来,我们将按州统计总野火数量并绘制结果。GeoPandas 可以用来完成所有这些任务。
我们首先导入模块:
In: import geopandas
接下来,我们导入包含所有州边界的形状文件:
In: states =
geopandas.read_file(r"C:\data\gdal\NE\110m_cultural\ne_110m_admin_
1_states_provinces.shp")
通过重复变量名,可以显示文件的属性表作为一个pandas数据框:
In: states
我们可以看到所有州名都列在 name 列中。我们稍后会需要这个列。矢量数据可以使用 Jupyter Notebook 中的魔法命令和matplotlib的plot方法进行绘制。由于默认地图看起来相当小,我们将通过figsize选项传递一些值来使其看起来更大:
In: %matplotlib inline
states.plot(figsize=(10,10))
你将看到以下地图:

对于我们的野火数据,重复相同的步骤。使用大的figsize选项值可以得到一个显示野火位置的较大地图:
In: fires =
geopandas.read_file(r"C:\data\mtbs_fod_pts_data\mtbs_fod_pts_201705
01.shp")
fires
In: fires.plot(markersize=1, figsize=(17,17))
地图看起来像这样:

查看名为 MTBS Zone 的fires GeoDataFrame列,并验证这个数据集并不包含所有州名以引用数据。然而,我们有一个几何列,我们可以用它来连接这两个数据集。在我们能这样做之前,我们必须确保数据使用相同的地图投影。我们可以如下验证:
In: fires.crs
Out: {'init': 'epsg:4269'}
In: states.crs
Out: {'init': 'epsg:4326'}
有两种地图投影,但两者都需要有相同的 CRS 才能正确对齐。我们可以如下将fires形状文件重新投影到 WGS84:
In: fires = fires.to_crs({'init': 'epsg:4326'})
现在,我们准备执行空间连接,使用sjoin方法,表示我们想知道fires几何形状是否在州几何形状内:
In: state_fires =
geopandas.sjoin(fires,states[['name','geometry']].copy(),op='within' )
state_fires
新的state_fires GeoDataFrame在右侧外边增加了一个名为 name 的列,显示每个火灾所在的州:

我们现在可以按州统计每州的野火总数。结果是显示州名和总数的一个pandas系列对象。为了从最高计数开始,我们将使用sort_values方法:
In: counts_per_state = state_fires.groupby('name').size()
counts_per_state.sort_values(axis=0, ascending=False)
根据我们的数据,佛罗里达州、加利福尼亚州和爱达荷州是 1984-2015 年期间野火最多的三个州:

这些值可以作为新字段输入到原始形状文件中,显示每个州的野火总数:
In: states =
states.merge(counts_per_state.reset_index(name='number_of_fires'))
states.head()
head方法打印了states shapefile 中的前五个条目,并在表格的右端添加了一个新字段。最后,可以创建并绘制每个州的野火计数热力图如下:
In: ax = states.plot(column='number_of_fires', figsize=(15, 6),
cmap='OrRd', legend=True)
输出将类似于以下内容:

将此与应用于相同结果的另一种颜色方案进行比较,去除低值处的浅色:
In: ax = states.plot(column='number_of_fires', figsize=(15, 6),
cmap='Accent', legend=True)
下面是地图的样貌:

使用以下代码进一步微调地图,通过添加标题并删除x轴和y轴:
In: import matplotlib.pyplot as plt
f, ax = plt.subplots(1, figsize=(18,6))
ax = states.plot(column='number_of_fires', cmap='Accent',
legend=True, ax=ax)
lims = plt.axis('equal')
f.suptitle('US Wildfire count per state in 1984-2015')
ax.set_axis_off()
plt.show()
输出如下:

为什么数据检查很重要
在准备数据时,了解你处理的数据是很重要的。例如,列出关于你的数据集的统计数据,显示有多少元素,以及是否有任何缺失值。在进行分析之前,通常需要对数据进行清理。由于 GeoPandas 数据对象是pandas数据对象的子类,你可以使用它们的方法进行数据检查和清理。以我们之前使用的野火数据 shapefile 为例。通过列出我们的 dataframe 对象,它不仅打印了所有属性数据,还列出了总行数和列数,共有 20340 行和 30 列。总行数也可以这样打印出来:
In: len(fires.index)
Out: 20340
这意味着在我们的输入数据集中有20340个单独的野火案例。现在,将这个行值与我们执行空间连接后每个州计数之和进行比较:
In: counts_per_state.sum()
Out: 20266
我们注意到,在空间连接之后,我们的数据集中野火减少了 74 个。虽然目前还不清楚我们的空间连接出了什么问题,为什么会有缺失值,但在执行几何操作之前和之后检查数据集是可能的,也是推荐的,例如检查空字段、非值或简单的空值:
In: fires.empty #checks if there are empty fields in the
dataframe
Out: False
同样的操作也可以通过指定列名来完成:
In: fires['geometry'].empty
Out: False
注意到 GeoPandas 几何列使用文本和值的组合,因此检查 NaN 或零值没有意义。
摘要
本章介绍了三个用于处理矢量数据的 Python 库——OGR、Shapely 和 GeoPandas。特别是,我们展示了如何使用这三个库进行地理空间分析和处理。每个库都单独介绍,包括它们的类、方法、数据结构和常用用例。简短的示例脚本展示了如何开始进行数据处理和分析。整体来看,读者现在知道了如何单独使用每个库,以及如何将这三个库结合起来完成以下任务:
-
读取和写入矢量数据
-
创建和操作矢量数据
-
绘制矢量数据
-
处理地图投影
-
执行空间操作
-
以表格形式处理矢量几何和属性数据
-
展示和分析数据以回答具有空间成分的问题
下一章讨论了光栅数据处理以及如何使用 GDAL 和 Rasterio 库。使用这些库,读者将学习如何执行基于光栅的地理空间搜索和分析,以及如何使用地理定位的文本和图像。
第六章:栅格数据处理
地理信息系统(GIS)通常由点、线和多边形组成。这些数据类型被称为矢量数据。然而,GIS 中还有一种数据类型——栅格。在本章中,你将学习如何处理栅格数据的基础知识。你将学习如何:
-
使用 地理数据抽象库(GDAL)加载和查询栅格
-
使用 GDAL 修改和保存栅格
-
使用 GDAL 创建栅格
-
将栅格加载到 PostgreSQL 中
-
使用 PostgreSQL 对栅格进行查询
安装 GDAL 可能很困难。通过使用虚拟环境和运行 Anaconda,你可以通过使用环境的 GUI 简化此过程。
使用 GDAL 进行栅格操作
GDAL 库允许你读取和写入矢量和栅格数据。要在 Windows 上安装 GDAL,你需要适当的二进制文件:
你可以在 trac.osgeo.org/osgeo4w/ 下载包含二进制的 OSGeo4W。
当你有了二进制文件,你可以使用 conda 安装 gdal,如下所示:
conda install -c conda-forge gdal
在接下来的章节中,你将学习如何加载和使用 .tif 文件。
使用 GDAL 库加载和查询栅格
现在你已经安装了 gdal,使用以下代码导入它:
from osgeo import gdal
GDAL 2 是最新版本。如果你安装了较旧的 gdal 版本,你可能需要使用以下代码导入它:
import gdal
如果是这样,你可能需要考虑升级你的 gdal 版本。一旦你导入了 gdal,你就可以打开一个栅格图像。首先,让我们从网络上获取一个图像。新墨西哥大学的地球数据分析中心维护着 资源地理信息系统(RGIS)。在其中,你可以找到新墨西哥的 GIS 数据。浏览到 rgis.unm.edu/ 并从“获取数据”链接,选择“阴影地形”、“通用”和“新墨西哥”。然后,下载 Color Shaded Relief of New Mexico (Georeferenced TIFF) 文件。
当你解压 ZIP 文件时,你会得到几个文件。我们只对 nm_relief_color.tif 感兴趣。以下代码将使用 gdal 打开 TIF:
nmtif = gdal.Open(r'C:\Desktop\ColorRelief\nm_relief_color.tif')
print(nmtif.GetMetadata())
之前的代码打开 TIF 文件。这和在 Python 中打开任何文件非常相似,只是你使用了 gdal.Open 而不是标准的 Python 库 open。下一行打印 TIF 的元数据,输出如下:
{'AREA_OR_POINT': 'Area', 'TIFFTAG_DATETIME': '2002:12:18 8:10:06', 'TIFFTAG_RESOLUTIONUNIT': '2 (pixels/inch)', 'TIFFTAG_SOFTWARE': 'IMAGINE TIFF Support\nCopyright 1991 - 1999 by ERDAS, Inc. All Rights Reserved\n@(#)$RCSfile: etif.c $ $Revision: 1.9.3.3 $ $Date: 2002/07/29 15:51:11EDT $', 'TIFFTAG_XRESOLUTION': '96', 'TIFFTAG_YRESOLUTION': '96'}
之前的元数据为你提供了一些基本信息,例如创建和修订的日期、分辨率和每英寸像素数。我们感兴趣的数据的一个特点是投影。要找到它,请使用以下代码:
nmtif.GetProjection()
使用 TIF 上的 GetProjection 方法,你会看到我们没有找到任何内容。代码的输出如下:
'LOCAL_CS[" Geocoding information not available Projection Name = Unknown Units = other GeoTIFF Units = other",UNIT["unknown",1]]'
如果您在 QGIS 中打开此 TIF,您将收到一个警告,表示坐标系未定义,它将默认为 epsg:4326。我知道该图像是投影的,我们可以通过查看 nm_relief_color.tif.xml 文件来找出这一点。如果您滚动到页面底部,您将看到 XML 标签 <cordsysn> 下的值,如下所示:
<cordsysn>
<geogcsn>GCS_North_American_1983</geogcsn>
<projcsn>NAD_1983_UTM_Zone_13N</projcsn>
</cordsysn>
如果您在 spatialreference.org 上查找投影,您会发现它是 EPSG:26913。我们可以使用 gdal 来设置投影,如下面的代码所示:
from osgeo import osr
p=osr.SpatialReference()
p.ImportFromEPSG(26913)
nmtif.SetProjection(p.ExportToWkt())
nmtif.GetProjection()
之前的代码导入了 osr 库。然后它使用库创建一个新的 SpatialReference。接下来,它使用 ImportFromEPSG 导入一个已知参考,并传递 26913。然后它使用 SetProjection,传递 EPSG:26913 的 WKT。最后,它调用 GetProjection,这样我们就可以看到代码是否成功。结果如下:
'PROJCS["NAD83 / UTM zone 13N",GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],TOWGS84[0,0,0,0,0,0,0],AUTHORITY["EPSG","6269"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4269"]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",-105],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH],AUTHORITY["EPSG","26913"]]'
之前的输出是 EPSG:26913 的 WKT。
打开 QGIS,TIF 将无警告加载。我可以添加阿尔伯克基街道的副本,它们将出现在正确的位置。这两组数据都在 EPSG:26913。以下图像显示了新墨西哥州阿尔伯克基中心的 TIF 和街道:

新墨西哥州街道的 Tif 文件
现在我们已经添加了投影,我们可以保存 TIF 的新版本:
geoTiffDriver="GTiff"
driver=gdal.GetDriverByName(geoTiffDriver)
out=driver.CreateCopy("copy.tif",nmtif,strict=0)
要查看新文件是否有空间参考,请使用以下代码:
out.GetProjection()
之前的代码将输出 EPSG:26913 的 知名文本(WKT),如下所示:
'PROJCS["NAD83 / UTM zone 13N",GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],TOWGS84[0,0,0,0,0,0,0],AUTHORITY["EPSG","6269"]], PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]], AUTHORITY["EPSG","4269"]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian", -105],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["metre",1, AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH],AUTHORITY["EPSG","26913"]]'
一个彩色栅格数据集有三个波段——红色、绿色和蓝色。您可以使用以下代码单独获取每个波段:
nmtif.RasterCount
之前的代码将返回 3。与数组不同,波段是按 1-n 索引的,因此三波段栅格将具有索引 1、2 和 3。您可以通过传递索引到 GetRasterBand() 来获取单个波段,如下面的代码所示:
band=nmtif.GetRasterBand(1)
现在您已经有一个栅格波段,您可以在其上执行查询,并且可以查找位置处的值。要找到指定行和列的值,您可以使用以下代码:
values=band.ReadAsArray()
现在,values 是一个数组,因此您可以通过索引符号查找值,如下所示:
values[1100,1100]
之前的代码将返回一个值为 216。在单波段数组中,这可能很有用,但在彩色图像中,您可能更想知道位置处的颜色。这需要知道所有三个波段的值。您可以通过使用以下代码来完成:
one= nmtif.GetRasterBand(1).ReadAsArray()
two = nmtif.GetRasterBand(2).ReadAsArray()
three= nmtif.GetRasterBand(3).ReadAsArray()
print(str(one[1100,1100])+","+ str(two[1100,1100])+","+str(three[1100,1100]))
之前的代码返回的值是 216、189、157。这些是像素的 RGB 值。这三个值是合成的——叠加在一起,这应该是以下图像中显示的颜色:

在 [1100,1100] 位置表示的三个波段的颜色
使用乐队,您可以访问获取乐队信息的好几种方法。您可以获取值的平均值和标准差,如下面的代码所示:
one=nmtif.GetRasterBand(1)
two=nmtif.GetRasterBand(2)
three=nmtif.GetRasterBand(3)
one.ComputeBandStats()
two.ComputeBandStats()
three.ComputeBandStats()
输出如下所示:
(225.05771967375847, 34.08382839593031)
(215.3145137636133, 37.83657996026153)
(195.34890652292185, 53.08308166590347)
你还可以从波段获取最小值和最大值,如下面的代码所示:
print(str(one.GetMinimum())+","+str(one.GetMaximum()))
结果应该是 0.0 和 255.0。
你还可以获取波段的描述。以下代码展示了如何获取和设置描述:
two.GetDescription() # returns 'band_2'
two.SetDescription("The Green Band")
two.GetDescription() # returns "The Green Band"
你可能最想对栅格数据集做的事情之一就是在 Jupyter Notebook 中查看栅格。在 Jupyter Notebook 中加载图像有几种方法,其中一种就是使用 HTML 和 <img> 标签。在以下代码中,你将看到如何使用 matplotlib 绘制图像:
import numpy as np
from matplotlib.pyplot import imshow
%matplotlib inline
data_array=nmtif.ReadAsArray()
x=np.array(data_array[0])
# x.shape ---> 6652,6300
w, h =6652, 6300
image = x.reshape(x.shape[0],x.shape[1])
imshow(image, cmap='gist_earth')
之前的代码导入了 numpy 和 matplotlib.pyploy.imshow。
NumPy 是一个用于处理数组的流行库。当处理栅格(数组)时,你将受益于对库的深入了解。Packt 出版了多本关于 NumPy 的书籍,如 NumPy Cookbook、NumPy Beginners Guide 和 Learning NumPy Array,这将是一个学习更多知识的好起点。
然后,它为这个笔记本设置内联绘图。代码接着读取 TIF 作为数组。然后,它从第一个波段创建一个 numpy 数组。
波段索引为 1-n,但一旦作为数组读取,它们就索引为 0。
为了隔离第一个波段,代码使用宽度和高度重塑数组。使用 x.shape,你可以获取它们两个,如果你索引,你可以单独获取每一个。最后,使用 imshow,代码使用 gist_earth 的颜色图绘制图像。图像将在 Jupyter 中如下显示:

在 Jupyter 中使用 imshow 显示 Tif
现在你已经知道了如何加载栅格并执行基本操作,你将在下一节学习如何创建栅格。
使用 GDAL 创建栅格
在上一节中,你学习了如何加载栅格、执行基本查询、修改它并将其保存为新的文件。在本节中,你将学习如何创建栅格。
栅格是一组值。因此,要创建一个,你首先创建一个数组,如下面的代码所示:
a_raster=np.array([
[10,10,1,10,10,10,10],
[1,1,1,50,10,10,50],
[10,1,1,51,10,10,50],
[1,1,1,1,50,10,50]])
之前的代码创建了一个有四行七列的 numpy 数组。现在你有了数据数组,你需要设置一些基本属性。以下代码将值分配给变量,然后你将在以下示例中将它们传递给栅格:
coord=(-106.629773,35.105389)
w=10
h=10
name="BigI.tif"
以下代码为变量 coord 中的栅格设置了左下角、宽度、高度和名称。然后,它以像素为单位设置宽度和高度。最后,它命名了栅格。
下一步是通过组合数据和属性来创建栅格。以下代码将向你展示如何操作:
d=gdal.GetDriverByName("GTiff")
output=d.Create(name,a_raster.shape[1],a_raster.shape[0],1,gdal.GDT_UInt16)
output.SetGeoTransform((coord[0],w,0,coord[1],0,h))
output.GetRasterBand(1).WriteArray(a_raster)
outsr=osr.SpatialReference()
outsr.ImportFromEPSG(4326)
output.SetProjection(outsr.ExportToWkt())
output.FlushCache()
之前的代码将GeoTiff驱动程序分配给变量d。然后,它使用该驱动程序创建栅格。创建方法需要五个参数——name、x的大小、y的大小、波段数和数据类型。要获取x和y的大小,你可以访问a_raster.shape,它将返回(4,7)。对a_raster.shape进行索引将分别给出x和y。
Create() 接受几种数据类型——从 GDT_ 开始。其他数据类型包括未知、字节、无符号 16 位整数、有符号 16 位整数、无符号 32 位整数、有符号 32 位整数、单精度浮点数、双精度浮点数、有符号 16 位整数、有符号 32 位整数、单精度浮点数和双精度浮点数。
接下来,代码使用左上角坐标和旋转设置从地图到像素坐标的转换。旋转是宽度和高度,如果是一个北向上图像,则其他参数为 0。
要将数据写入波段,代码选择栅格波段——在这种情况下,您在调用 Create() 方法时指定了一个单波段,因此将 1 传递给 GetRasterBand() 和 WriteArray() 将获取 numpy 数组。
现在,您需要为 TIF 分配一个空间参考。创建一个空间参考并将其分配给 outsr。然后,您可以从 EPSG 码导入空间参考。接下来,通过将 WKT 传递给 SetProjection() 方法来设置 TIF 上的投影。
最后一步是 FlushCache(),这将写入文件。如果您完成 TIF,可以将 output = None 设置为清除它。然而,您将在下面的代码片段中再次使用它,所以这里将跳过该步骤。
要证明代码有效,您可以检查投影,如下面的代码所示:
output.GetProjection()
输出显示 TIF 在 EPSG:4326:
'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]]'
您可以在 Jupyter 中显示 TIF 并查看它是否如您预期的那样。以下代码演示了如何绘制 image 并检查您的结果:
data=output.ReadAsArray()
w, h =4, 7
image = data.reshape(w,h) #assuming X[0] is of shape (400,) .T
imshow(image, cmap='Blues') #enter bad color to get list
data
之前的代码将栅格读取为数组,并分配宽度和高度。然后,它创建一个 image 变量,将数组重塑为宽度和高度。最后,它将图像传递给 imshow() 并在最后一行打印 data。如果一切正常,您将看到以下图像:

数组的值以及由它们创建的栅格
以下部分将向您介绍如何使用 PostgreSQL 来处理栅格,作为 gdal 的替代品或与其结合使用。
使用 PostgreSQL 进行栅格操作
在本章的第一节中,您能够使用 gdal 加载、显示和查询栅格。在本节中,您将学习如何使用空间数据库——PostgreSQL 来加载和查询栅格。当您开始建模数据时,您很可能会将其存储在空间数据库中。您可以利用数据库对您的栅格执行查询。
将栅格加载到 PostgreSQL 中
要将栅格加载到 PostgreSQL 中,您可以使用 raster2pgsql 二进制文件。如果它不在您的路径中,您可能需要将其添加。您应该能够在 Windows 的 PostgreSQL 安装目录中的 \PostgreSQL\10\bin 找到该二进制文件。
以下命令应从您的操作系统的命令行执行。它将本章中创建的 TIF 加载到现有的 PostgreSQL 数据库中:
>raster2pgsql -I -C -s 4326 C:\Users\Paul\Desktop\BigI.tif public.bigi | psql -U postgres -d pythonspatial
上一条命令使用 raster2pgsql 的 -I(创建索引)、-C(添加栅格约束)和 -s 4326(SRID)参数。在 Windows 上使用管道操作符,将命令发送到 psql。psql 使用 -U postgres(用户名)和 -d pythonspatial(数据库名)参数运行。
如果你以 Postgres 用户登录,你不需要 -U。如果没有它,Windows 将尝试使用已登录的用户账户登录到 PostgreSQL,这可能与 PostgreSQL 用户不同。
现在你已经将数据加载到 PostgreSQL 中,下一节将展示你如何使用 Python 查询它。
使用 PostgreSQL 对栅格进行查询
在 PostgreSQL 中加载了栅格后,你可以使用 Python 查询它。用于与 PostgreSQL 一起工作的 Python 库是 psycopg2。以下代码将连接到你已加载 TIF 的数据库:
import psycopg2
connection = psycopg2.connect(database="pythonspatial",user="postgres", password="postgres")
cursor = connection.cursor()
上一段代码导入 psycopg2。然后通过传递数据库名、用户名和密码建立连接。最后,它获取一个 cursor 对象,以便你可以执行查询。
要在 PostgreSQL 中查看栅格,你可以执行一个选择所有操作,如下面的代码所示:
cursor.execute("SELECT * from bigi")
#Big I is the name of the intersection where I-25 and I-40 meet and split Albuquerque in quadrants.
cursor.fetchall()
上一段代码执行了一个选择所有语句并打印所有结果。表中有两列——rid 和 rast。Rid 是栅格的唯一 ID 字段。如果你在运行 raster2pgsql 时将其分块,则会有更多行。rast 列包含栅格数据:
[(1,
'010000010000000000000024400000000000002440D8B969334EA85AC0D82D02637D8D414000000000000000000000000000000000E61000000700040004000A0A010A0A0A0A010101320A0A320A0101330A0A3201010101320A32')]
查询栅格元数据
使用 PostgreSQL,你可以对你的数据进行各种查询。在本节中,你将学习如何查询栅格的基本元数据和属性。本节将介绍许多可用的 PostgreSQL 函数中的几个。
你可以查询基本文本摘要的数据。以下代码展示了如何使用 ST_Summary() 函数:
cursor.execute("select ST_Summary(rast) from bigi;")
cursor.fetchall()
汇总函数接受栅格数据列作为参数,并返回一个包含栅格大小、边界框、波段数量以及是否任何波段中没有数据值的字符串。以下是从上一段代码输出的内容:
[('Raster of 7x4 pixels has 1 band and extent of BOX(-106.629773 35.105389,-36.629773 75.105389)\n band 1 of pixtype 8BUI is in-db with no NODATA value',)]
从 ST_Summary 中解析出单个信息片段将很困难。你可以通过使用 ST_Metadata 函数以更易于机器读取的格式检索此信息。你可以使用以下代码来完成此操作:
cursor.execute("select ST_MetaData(rast) from bigi")
cursor.fetchall()
上一段代码查询栅格的左上角 X 值、左上角 Y 值、宽度、高度、X 的比例、Y 的比例、X 的倾斜、Y 的倾斜、SRID 和栅格中的波段数量。输出如下所示:
[('(-106.629773,35.105389,7,4,10,10,0,0,4326,1)',)]
输出允许你通过索引符号选择单个元数据片段,这是一个比解析 ST_Summary 提供的字符串更简单的解决方案。
你可以查询栅格的特定和单个属性。要获取单个多边形作为栅格——而不是摘要中描述的两个点框——你可以使用以下代码:
cursor.execute("select ST_AsText(ST_Envelope(rast)) from bigi;")
cursor.fetchall()
之前代码的输出是栅格的向量和多边形的 WKT。如下所示:
[('POLYGON((-106.629773 75.105389,-36.629773 75.105389,-36.629773 35.105389,-106.629773 35.105389,-106.629773 75.105389))',)]
以下代码将查询栅格的高度和宽度:
cursor.execute("select st_height(rast), st_Width(rast) from bigi;") #st_width
cursor.fetchall()
如您从本章前面的内容中可能记得的,栅格是4x7,如下输出所示:
[(4, 7)]
另一个可能很有用的元数据是像素大小。以下代码将展示如何操作:
cursor.execute("select ST_PixelWidth(rast), ST_PixelHeight(rast) from bigi;")
cursor.fetchall()
使用ST_PixelWidth和ST_PixelHeight,您将得到以下输出。这与您在章节早期创建栅格时的高度和宽度相匹配:
[(10.0,10.0)]
您可以查询特定波段中单元格内数据的统计信息。ST_SummaryStats提供了数据值的基本摘要统计信息。以下代码展示了如何查询:
cursor.execute("select ST_SummaryStats(rast) from bigi;")
cursor.fetchall()
之前代码的输出返回了栅格波段的数量、总和、平均值、标准差、最小值和最大值。您可以通过将波段作为整数传递到第二个参数ST_SummaryStats(rast,3)中来传递栅格波段。如果您没有指定波段,默认为1。输出如下所示:
[('(28,431,15.3928571428571,18.5902034218377,1,51)',)]
您还可以查询栅格中值的直方图,如下所示:
cursor.execute("SELECT ST_Histogram(rast,1) from bigi;")
cursor.fetchall()
之前的代码使用了ST_Histogram并传递了栅格列和波段。你可以将 bins 的数量作为第三个参数传递,或者让函数自行决定。结果如下所示:
[('(1,9.33333333333333,10,0.357142857142857)',),
('(9.33333333333333,17.6666666666667,12,0.428571428571429)',),
('(17.6666666666667,26,0,0)',),
('(26,34.3333333333333,0,0)',),
('(34.3333333333333,42.6666666666667,0,0)',),
('(42.6666666666667,51,6,0.214285714285714)',)]
之前的输出是一个 bins 的数组。每个 bin 包含最小值、最大值、计数和百分比。
返回几何形状的查询
之前的查询返回了关于栅格的基本信息,并返回了包含数据的集合。在 PostgreSQL 中,有一系列函数可以从查询中返回几何形状。本节将介绍其中的一些函数。
栅格由一系列单元格和值组成。这些单元格成为我们栅格数据中的地理参照像素。使用 PostgreSQL,您可以查询特定单元格的栅格数据,并返回该单元格的多边形表示。以下代码展示了如何操作:
cursor.execute("select rid, ST_asText(ST_PixelAsPolygon(rast,7,2)) from bigi;")
cursor.fetchall()
使用ST_PixelAsPolygons,您可以传递栅格列、单元格的列和行,并获取该单元格的多边形几何形状。通过将查询包裹在ST_AsText中,您将得到多边形的 WKT 表示而不是二进制表示。
以下为结果:
[(1,
'POLYGON((-46.629773 45.105389,-36.629773 45.105389,-36.629773
55.105389,-46.629773 55.105389,-46.629773 45.105389))')]
之前的输出返回了像素的 rid(栅格 ID)。由于您在将栅格加载到 PostgreSQL 时没有进行瓦片化,所有查询都将返回 rid 为1。
之前的查询返回了一个多边形,但您可以使用函数来返回点。使用ST_PixelAsPoints和ST_PixelAsCentroids,您可以检索栅格数据集中每个像素的点。
使用ST_PixelAsPoints,您可以检索表示每个像素左上角的一个点几何形状。查询还返回了单元格的x和y坐标以及值。以下代码将展示如何操作:
cursor.execute("SELECT x, y, val, ST_AsText(geom) FROM (SELECT (ST_PixelAsPoints(rast, 1)).* FROM bigi) as foo;")
cursor.fetchall()
之前的代码有两个部分的查询。从FROM语句之后开始,查询选择波段1的像素作为点。第一个语句在结果上执行选择并检索点几何形状,以及单元格的x、y和值。默认情况下,ST_PixelAsPoints不返回无值的单元格的数据。您可以将第三个参数传递为 false 以返回无值的单元格。
之前查询的输出是一个数组,每行代表一个单元格。每行包含x、y、值和几何形状。结果如下所示:
[(1, 1, 10.0, 'POINT(-106.629773 35.105389)'),
(2, 1, 10.0, 'POINT(-96.629773 35.105389)'),
(3, 1, 1.0, 'POINT(-86.629773 35.105389)'),
(4, 1, 10.0, 'POINT(-76.629773 35.105389)'),
(5, 1, 10.0, 'POINT(-66.629773 35.105389)'),
(6, 1, 10.0, 'POINT(-56.629773 35.105389)'),
(7, 1, 10.0, 'POINT(-46.629773 35.105389)'),
(1, 2, 1.0, 'POINT(-106.629773 45.105389)'),
(2, 2, 1.0, 'POINT(-96.629773 45.105389)'),
(3, 2, 1.0, 'POINT(-86.629773 45.105389)'),
(4, 2, 50.0, 'POINT(-76.629773 45.105389)'),
(5, 2, 10.0, 'POINT(-66.629773 45.105389)'),
(6, 2, 10.0, 'POINT(-56.629773 45.105389)'),
(7, 2, 50.0, 'POINT(-46.629773 45.105389)'),
(1, 3, 10.0, 'POINT(-106.629773 55.105389)'),
(2, 3, 1.0, 'POINT(-96.629773 55.105389)'),
(3, 3, 1.0, 'POINT(-86.629773 55.105389)'),
(4, 3, 51.0, 'POINT(-76.629773 55.105389)'),
(5, 3, 10.0, 'POINT(-66.629773 55.105389)'),
(6, 3, 10.0, 'POINT(-56.629773 55.105389)'),
(7, 3, 50.0, 'POINT(-46.629773 55.105389)'),
(1, 4, 1.0, 'POINT(-106.629773 65.105389)'),
(2, 4, 1.0, 'POINT(-96.629773 65.105389)'),
(3, 4, 1.0, 'POINT(-86.629773 65.105389)'),
(4, 4, 1.0, 'POINT(-76.629773 65.105389)'),
(5, 4, 50.0, 'POINT(-66.629773 65.105389)'),
(6, 4, 10.0, 'POINT(-56.629773 65.105389)'),
(7, 4, 50.0, 'POINT(-46.629773 65.105389)')]
使用ST_PixelAsCentroids,您可以获取代表像素或单元格重心的点。查询与之前的示例相同,如下所示:
cursor.execute("SELECT x, y, val, ST_AsText(geom) FROM (SELECT (ST_PixelAsCentroids(rast, 1)).* FROM bigi) as foo;")
cursor.fetchall()
之前的查询分为两部分。它首先执行ST_PixelAsCentroids函数,然后从结果集中选择x、y、值和几何形状。输出如下。注意,点与之前的示例不同:
[(1, 1, 10.0, 'POINT(-101.629773 40.105389)'),
(2, 1, 10.0, 'POINT(-91.629773 40.105389)'),
(3, 1, 1.0, 'POINT(-81.629773 40.105389)'),
(4, 1, 10.0, 'POINT(-71.629773 40.105389)'),
(5, 1, 10.0, 'POINT(-61.629773 40.105389)'),
(6, 1, 10.0, 'POINT(-51.629773 40.105389)'),
(7, 1, 10.0, 'POINT(-41.629773 40.105389)'),
(1, 2, 1.0, 'POINT(-101.629773 50.105389)'),
(2, 2, 1.0, 'POINT(-91.629773 50.105389)'),
(3, 2, 1.0, 'POINT(-81.629773 50.105389)'),
(4, 2, 50.0, 'POINT(-71.629773 50.105389)'),
(5, 2, 10.0, 'POINT(-61.629773 50.105389)'),
(6, 2, 10.0, 'POINT(-51.629773 50.105389)'),
(7, 2, 50.0, 'POINT(-41.629773 50.105389)'),
(1, 3, 10.0, 'POINT(-101.629773 60.105389)'),
(2, 3, 1.0, 'POINT(-91.629773 60.105389)'),
(3, 3, 1.0, 'POINT(-81.629773 60.105389)'),
(4, 3, 51.0, 'POINT(-71.629773 60.105389)'),
(5, 3, 10.0, 'POINT(-61.629773 60.105389)'),
(6, 3, 10.0, 'POINT(-51.629773 60.105389)'),
(7, 3, 50.0, 'POINT(-41.629773 60.105389)'),
(1, 4, 1.0, 'POINT(-101.629773 70.105389)'),
(2, 4, 1.0, 'POINT(-91.629773 70.105389)'),
(3, 4, 1.0, 'POINT(-81.629773 70.105389)'),
(4, 4, 1.0, 'POINT(-71.629773 70.105389)'),
(5, 4, 50.0, 'POINT(-61.629773 70.105389)'),
(6, 4, 10.0, 'POINT(-51.629773 70.105389)'),
(7, 4, 50.0, 'POINT(-41.629773 70.105389)')]
之前提到的函数返回了栅格数据集中所有像素的几何形状。这两个函数都有一个相应的函数,允许您指定单个像素。
从重心和点中移除复数形式将允许您指定单个像素,但不会返回x、y和值。以下代码展示了如何将单个像素作为重心进行查询:
cursor.execute("SELECT ST_AsText(ST_PixelAsCentroid(rast,4,1)) FROM bigi;")
cursor.fetchall()
之前的代码使用了ST_PixelAsCentroid并传递了栅格、行和列。结果是为已指定的单元格生成一个单独的重心点几何形状。输出如下:
[('POINT(-71.629773 40.105389)',)]
将查询包裹在ST_AsText中,结果输出为 WKT 格式。
返回值的查询
前两个部分返回了关于栅格和表示栅格数据的几何形状的信息。本节将向您展示如何查询您的栅格数据集的值。
要获取特定单元格的值,您使用ST_Value,如下所示:
cursor.execute("select ST_Value(rast,4,3) from bigi;")
cursor.fetchall()
之前的代码将栅格、列和行传递给ST_Value。如果不想返回任何数据值,可以选择传递 false。之前查询的结果如下所示:
[(51.0,)]
输出是给定单元格的值。
如果您要搜索具有给定值的所有像素,可以使用ST_PixelOfValue,如下所示:
cursor.execute("select ST_PixelOfValue(rast,1,50) from bigi;")
cursor.fetchall()
之前的代码将波段和要搜索的值传递给查询。此查询的结果是一个数组,包含所有(x,y)坐标,其中值为50。输出如下:
[('(4,2)',), ('(5,4)',), ('(7,2)',), ('(7,3)',), ('(7,4)',)]
对于之前显示的每个坐标,其值是50。
要总结栅格中每个值的出现次数,可以使用ST_ValueCount进行查询,如下所示:
cursor.execute("select ST_ValueCount(rast) from bigi;")
cursor.fetchall()
之前的代码将栅格列传递给ST_ValueCount。您可以通过传递整数作为第二个参数来指定栅格波段——ST_ValueCount(raster,2)将是波段2。否则,默认是波段1。输出如下:
[('(10,12)',), ('(1,10)',), ('(50,5)',), ('(51,1)',)]
之前的输出包含值和计数的格式为(值,计数)。
你也可以查询数据中单个值出现的次数。以下代码展示了如何进行查询:
cursor.execute("select ST_ValueCount(rast,1,True,50) from bigi;")
cursor.fetchall()
使用ST_ValueCount并传递搜索值(50),你将收到50在栅格中作为值的出现次数,如下所示:
[(5,)]
之前的输出显示在栅格数据集中50出现了5次。
要返回栅格数据中的所有值,你可以使用ST_DumpValues,如下所示:
cursor.execute("select ST_DumpValues(rast,1) from bigi;")
cursor.fetchall()
之前的代码传递了栅格列和波段。结果是栅格中的所有值作为一个数组。输出如下所示:
[([[10.0, 10.0, 1.0, 10.0, 10.0, 10.0, 10.0],
[1.0, 1.0, 1.0, 50.0, 10.0, 10.0, 50.0],
[10.0, 1.0, 1.0, 51.0, 10.0, 10.0, 50.0],
[1.0, 1.0, 1.0, 1.0, 50.0, 10.0, 50.0]],)]
使用之前的输出,你可以使用标准的 Python 索引符号查询单个单元格。
之前的查询返回了指定单元格或使用指定值返回的值。接下来的两个查询将基于点几何形状返回值。
使用ST_NearestValue,你可以传递一个点并获取离该点最近的像素值。如果栅格数据包含高程值,你将查询离点最近已知的已知高程。以下代码展示了如何进行查询:
cursor.execute("select ST_NearestValue(rast,( select ST_SetSRID( ST_MakePoint(-71.629773,60.105389),4326))) from bigi;".format(p.wkt))
cursor.fetchall()
之前的代码将栅格列和一个点传递给ST_NearestValue。从内部开始,点参数使用了ST_MakePoint从坐标创建一个点。该函数被ST_SetSRID包裹。ST_SetSRID接受两个参数——一个点和空间参考。在这种情况下,点是ST_MakePoint,空间参考是 ESPG 4326。之前查询的结果如下所示:
[(51.0,)]
51的值是离点最近的值。查询中的坐标是之前ST_PixelAsCentroids示例中的单元格(4,3)的重心。在那个示例中,该点的值是51。
要检索给定点附近的多个值,你可以使用ST_Neighborhood,如下面的代码所示:
cursor.execute("select ST_Neighborhood(rast,(select ST_SetSRID( ST_MakePoint(410314,3469015),26913)),1,1) from newmexicoraster;")
cursor.fetchall()
ST_Neighborhood函数接受一个栅格列、一个点和x、y距离值。在之前的代码中,你使用了ST_MakePoint和ST_SetSRID来创建点。然后,你传递了这个点和x、y距离参数的1和1距离值。这将返回一个 3x3 的邻域,如下面的输出所示:
[([[255.0, 255.0, 255.0], [255.0, 255.0, 255.0], [255.0, 255.0, 255.0]],)]
之前的输出显示周围邻域的值都是255。
最后,你可以将矢量几何形状作为栅格选择。当查询包含阿尔伯克基警察区域命令的多边形矢量表时,以下代码将提取单个区域命令作为栅格:
cursor.execute("SELECT ST_AsPNG(ST_asRaster(geom,150,250,'8BUI')) from areacommand where name like 'FOOTHILLS';")
c=cursor.fetchall()
with open('Foothills.png','wb') as f:
f.write(c[0][0])
f.close()
之前的代码是一个选择语句,用于从areacommand表中选取名为FOOTHILLS的几何形状。查询中的几何部分是执行栅格转换的地方。
ST_AsRaster函数接受一个几何形状、x的比例、y的比例以及像素类型。ST_AsRaster函数被ST_AsPNG函数封装。结果是内存中的 PNG 文件。使用标准的 Python 文件操作,代码以写二进制模式打开一个文件,Foothills.png,然后将内存视图c[0][0]写入磁盘。然后关闭文件。
输出结果展示在以下图像中:

展示山麓地带的栅格图像
摘要
在本章中,你学习了如何使用 GDAL 和 PostgreSQL 处理栅格数据。
首先,你学习了如何使用 GDAL 加载和查询栅格。你还学习了如何使用 GDAL 修改和保存栅格。然后,你学习了如何创建自己的栅格数据。你学习了如何使用raster2pgsql工具将栅格数据加载到 PostgreSQL 中。一旦在 PostgreSQL 中,你学习了如何查询元数据、属性、值和几何形状。你学习了 PostgreSQL 中用于栅格数据分析的几个常用函数。
虽然本章只是对处理栅格数据进行了初步探讨,但你现在应该有足够的知识来了解如何学习新的技术和方法来处理栅格数据。在下一章中,你将学习如何在 PostgreSQL 中处理矢量数据。
第七章:使用地理数据库进行地理处理
在第三章,“地理空间数据库简介”中,您学习了如何安装 PostGIS、创建表、添加数据以及执行基本空间查询。在本章中,您将学习如何与地理空间数据库一起工作以回答问题和制作地图。本章将指导您将犯罪数据加载到表中。一旦您的地理数据库用真实世界的数据填充,您将学习如何执行常见的犯罪分析任务。您将学习如何映射查询、按日期范围查询以及执行基本地理处理任务,如缓冲区、点在多边形内和最近邻。您将学习如何将小部件添加到您的 Jupyter 笔记本中,以便查询可以交互式地进行。最后,您将学习如何使用 Python 从您的地理空间查询创建图表。作为一名犯罪分析师,您将制作地图,但并非所有与 GIS 相关的任务都是基于地图的。分析师使用 GIS 数据来回答问题和创建报告。高管通常更熟悉图表和图形。
在本章中,您将学习:
-
如何使用空间查询执行地理处理任务
-
如何向您的表添加触发器
-
如何映射您的地理空间查询结果
-
如何绘制地理空间查询
-
如何使用 Jupyter 与查询交互并将小部件连接到您的查询
犯罪仪表板
要构建一个交互式犯罪仪表板,您需要收集数据来构建数据库。然后,您将查询数据并添加小部件,以便用户无需编写代码即可修改查询。最后,您将绘制和映射查询结果。
构建犯罪数据库
要构建犯罪仪表板的组件,我们将使用阿尔伯克基市的开放数据。阿尔伯克基市有犯罪事件、区域指挥和“beat”的数据集。通过将区域与“事件”结合,您将能够报告两个地理区域。然后,您可以使用邻里协会或任何其他边界——人口普查区块、群体或区域,并获取人口统计信息。
您可以在位于:www.cabq.gov/abq-data/ 的主要开放数据网站上找到数据链接。滚动到页面底部并查找“安全数据集”标题。
创建表
我们需要创建三个表来存储犯罪数据。我们需要一个表来:
-
区域指挥
-
beat
-
事件
要创建表,我们需要导入所需的库:
import psycopg2
import requests
from shapely.geometry import Point,Polygon,MultiPolygon, mapping
import datetime
珍贵的代码导入psycopg2以连接到 PostGIS,导入requests以调用服务以便您可以抓取数据,从shapely.geometry导入Point、Polygon和MultiPolygon以使将GeoJSON转换为对象更容易,以及datetime因为事件有一个date字段。
在第三章,《地理空间数据库简介》中,您创建了一个名为pythonspatial的数据库,并使用名为postgres的用户。我们将在该数据库中创建表。为了填充这些表,我们将从服务中复制一些字段。服务的图层页面底部有一个字段列表。
图层的 URL 链接到服务的根页面或图层编号。对于事件,图层的 URL 是:coagisweb.cabq.gov/arcgis/rest/services/public/APD_Incidents/MapServer/0。
每个字段都有类型和长度,用于incidents图层,如下所示:
-
OBJECTID(类型:esriFieldTypeOID,别名:Object_ID) -
Shape(类型:esriFieldTypeGeometry,别名:Geometry) -
CV_BLOCK_ADD(类型:esriFieldTypeString,别名:Location,长度:72) -
CVINC_TYPE(类型:esriFieldTypeString,别名:Description,长度:255) -
date(类型:esriFieldTypeDate,别名:Date,长度:8)
支持的操作:查询、生成渲染器、返回更新。
使用以下代码创建表:
connection = psycopg2.connect(database="pythonspatial",user="postgres", password="postgres")
cursor = connection.cursor()
cursor.execute("CREATE TABLE areacommand (id SERIAL PRIMARY KEY, name VARCHAR(20), geom GEOMETRY)")
cursor.execute("CREATE TABLE beats (id SERIAL PRIMARY KEY, beat VARCHAR(6), agency VARCHAR(3), areacomm VARCHAR(15),geom GEOMETRY)")
cursor.execute("CREATE TABLE incidents (id SERIAL PRIMARY KEY, address VARCHAR(72), crimetype VARCHAR(255), date DATE,geom GEOMETRY)")
connection.commit()
之前的代码首先创建连接并获取cursor。然后创建areacommand表,包含一个name字段和一个GEOMETRY字段。在ArcServer服务中,区域命令字段长度为20,因此代码创建了一个名为name的字段,类型为VARCHAR(20)。接下来的两行创建了beats和incidents的表,最后,代码提交,使更改永久生效。
数据填充
在创建好表之后,我们需要获取数据并填充它们。以下代码将获取区域命令并将其插入到我们的表中:
url='http://coagisweb.cabq.gov/arcgis/rest/services/public/adminboundaries/MapServer/8/query'
params={"where":"1=1","outFields":"*","outSR":"4326","f":"json"}
r=requests.get(url,params=params)
data=r.json()
for acmd in data['features']:
polys=[]
for ring in acmd['geometry']['rings']:
polys.append(Polygon(ring))
p=MultiPolygon(polys)
name=acmd['attributes']['Area_Command']
cursor.execute("INSERT INTO areacommand (name, geom) VALUES ('{}',
ST_GeomFromText('{}'))".format(name, p.wkt))
connection.commit()
之前的代码使用requests查询 URL,传递参数。参数仅获取所有数据(1=1),并获取所有字段(*),在参考4326和以json格式下。结果使用json()方法加载到变量data中。
要了解环境系统研究学院(ESRI)ArcServer 查询参数,请参阅以下 API 参考:coagisweb.cabq.gov/arcgis/sdk/rest/index.html#/Query_Map_Service_Layer/02ss0000000r000000/
下一段代码是for循环,用于插入数据。服务返回json,我们需要的数据存储在features数组中。对于features数组中的每个区域命令(acmd),我们将获取name和geometry。
geometry由多个rings组成——在本例中,因为我们的数据由多边形组成。我们需要遍历rings。为了做到这一点,代码中有一个另一个for循环,它遍历每个ring,创建一个多边形,并将其添加到polys[]中。当所有rings都被收集为多边形时,代码创建一个名为区域命令的单个MultiPolygon,并使用cursor.execute()将其插入表中。
SQL 是基本的插入命令,但使用了参数化查询和ST_GeometryFromText()。不要被这些附加功能分散注意力。使用以下基本查询构建查询:
INSERT INTO table (field, field) VALUES (value,value)
要传递值,代码使用.format()。它传递字符串名称并使用 Shapely 将坐标转换为 WKT(p.wkt)。
你需要对beats表做同样的事情:
url='http://coagisweb.cabq.gov/arcgis/rest/services/public/adminboundaries/MapServer/9/query'
params={"where":"1=1","outFields":"*","outSR":"4326","f":"json"}
r=requests.get(url,params=params)
data=r.json()
for acmd in data['features']:
polys=[]
for ring in acmd['geometry']['rings']:
polys.append(Polygon(ring))
p=MultiPolygon(polys)
beat = acmd['attributes']['BEAT']
agency = acmd['attributes']['AGENCY']
areacomm = acmd['attributes']['AREA_COMMA']
cursor.execute("INSERT INTO beats (beat, agency,areacomm,geom) VALUES ('{}','{}','{}',
ST_GeomFromText('{}'))".format(beat,agency,areacomm,p.wkt))
connection.commit()
之前的代码与面积命令的代码相同,只是通过多个占位符('{}')传递额外的字段。
最后,我们需要添加incidents:
url='http://coagisweb.cabq.gov/arcgis/rest/services/public/APD_Incidents/MapServer/0/query'
params={"where":"1=1","outFields":"*","outSR":"4326","f":"json"}
r=requests.get(url,params=params)
data=r.json()
for a in data["features"]:
address=a["attributes"]["CV_BLOCK_ADD"]
crimetype=a["attributes"]["CVINC_TYPE"]
if a['attributes']['date'] is None:
pass
else:
date = datetime.datetime.fromtimestamp(a['attributes']['date'] / 1e3).date()
try:
p=Point(float(a["geometry"]["x"]),float(a["geometry"]["y"]))
cursor.execute("INSERT INTO incidents (address,crimetype,date, geom) VALUES
('{}','{}','{}', ST_GeomFromText('{}'))".format(address,crimetype,str(date), p.wkt))
except KeyError:
pass
connection.commit()
之前的代码使用requests获取数据。然后它遍历features。这个代码块有一些错误检查,因为有一些features有空白日期,还有一些没有坐标。如果没有date,代码会通过,并使用try,catch块接受一个KeyError,这将捕获缺失的坐标。
现在数据已加载到表中,我们可以开始查询数据并在地图和图表中展示它。
映射查询
在第三章,《地理空间数据库简介》中,你查询了数据库并返回了文本。geometry以已知文本(WKT)的形式返回。这是我们请求的结果,但我不能通过读取坐标列表来可视化地理数据。我需要在地图上看到它。在本节中,你将使用ipyleaflet和 Jupyter 来映射查询的结果。
要在 Jupyter 中映射查询,你需要安装ipyleaflet。你可以在操作系统的命令提示符中使用pip来完成此操作:
pip install ipyleaflet
然后,你可能需要根据你的环境启用扩展。在命令提示符中输入:
jupyter nbextension enable --py --sys-prefix ipyleaflet
对于代码和ipyleaflet的使用示例,你可以在 GitHub 仓库中查看:github.com/ellisonbg/ipyleaflet
如果你在映射过程中遇到错误,你可能需要启用widgetsnbextension:
jupyter nbextension enable --py --sys-prefix widgetsnbextension
如果你正在运行 Jupyter,你需要重新启动它。
在安装并启用ipyleaflet之后,你可以映射你的查询:
import psycopg2
from shapely.geometry import Point,Polygon,MultiPolygon
from shapely.wkb import loads
from shapely.wkt import dumps, loads
import datetime
import json
from ipyleaflet import (
Map, Marker,
TileLayer, ImageOverlay,
Polyline, Polygon, Rectangle, Circle, CircleMarker,
GeoJSON
)
之前的代码导入了我们需要查询和映射数据的库。让我们建立connection并获取cursor,如下面的代码所示:
connection = psycopg2.connect(database="pythonspatial",user="postgres", password="postgres")
cursor = connection.cursor()
在第三章,《地理空间数据库简介》中,所有的查询都使用了ST_AsText()来返回geometry。现在,由于我们将要映射结果,如果它们以GeoJSON返回将会更容易。在下面的代码中,你将使用ST_AsGeoJSON()来获取geometry:
cursor.execute("SELECT name, ST_AsGeoJSON(geom) from areacommand")
c=cursor.fetchall()
c[0]
上述查询获取了 areacommand 表中的所有记录,包括它们的 name 和作为 GeoJSON 的 geometry,然后打印第一条记录 (c[0])。结果如下:
('FOOTHILLS',
'{"type":"MultiPolygon","coordinates":[[[[-106.519742762931,35.0505292241227],[-106.519741401085,35.0505292211811],[-106.51973952181,35.0505292175042],[-106.518248463965,35.0505262104449],[-106.518299012166,35.0517336649125],[-106.516932057477,35.0537380198153],....]]]}
ST_AsText 和 ST_AsGeoJSON 是从 PostGIS 中获取 几何形状 的 17 种方法中的两种。有关可用返回类型的完整列表,请参阅 PostGIS 参考文档:postgis.net/docs/reference.html#Geometry_Accessors
现在你已经有了一些 GeoJSON,是时候创建一个地图来显示它了。要创建 leaflet 地图,请使用以下代码:
center = [35.106196,-106.629515]
zoom = 10
map = Map(center=center, zoom=zoom)
map
上述代码定义了地图的 中心,对于阿尔伯克基,我总是使用 I-25 和 I-40 的交汇处。这个交汇处将城市分为四个象限。然后代码定义了 缩放 级别——数字越高,缩放 越近。最后,它打印出地图。
你将有一个带有 OpenStreetMap 瓦片的空白底图。在 Jupyter 中,当你向地图添加数据时,你可以滚动回地图的原始打印版以查看数据;你不需要每次都重新打印地图。
区域命令的 GeoJSON 存储在变量 c 中。对于每个项目 c[x],GeoJSON 位于位置 1 (c[x][1])。以下代码将遍历 c 并将 GeoJSON 添加到地图中:
for x in c:
layer=json.loads(x[1])
layergeojson=GeoJSON(data=layer)
map.add_layer(layergeojson)
上述代码使用 json.loads() 将 GeoJSON 分配给一个图层。这将使返回的 GeoJSON 字符串在 Python 中成为一个字典。接下来,代码在图层上调用 ipyleaflet GeoJSON() 方法,并将其传递给变量 layergeojson。最后,在地图上调用 add_layer() 并传递 layergeojson。在 Jupyter 中绘制地图还有其他方法;例如,你可以使用 Matplotlib、Plotly 或 Bokeh 来绘制它们。如果你来自网络制图,你可能已经熟悉了 Leaflet JavaScript 库,这将使使用 ipyleaflet 变得熟悉。此外,ipyleaflet 加载底图并提供交互性。
如果你滚动到地图上方,你应该看到如下截图:

在cursor.execute()中更改 SQL 查询,你可以映射 beats:
cursor.execute("SELECT beat, ST_AsGeoJSON(geom) from beats")
c=cursor.fetchall()
for x in c:
layer=json.loads(x[1])
layergeojson=GeoJSON(data=layer)
map.add_layer(layergeojson)
你应该看到 beats 如下绘制:

你可以为 incidents 做同样的事情,但我们现在先保留这个,因为数据集中有近 30,000 个 incidents,这会使得我们的地图显得过于拥挤。为了在地图上显示 incidents,我们将使用空间查询来限制我们的选择。
按日期统计事件
限制事件查询结果的一种方法是通过 日期。使用 Python 的 datetime 库,你可以指定一个 日期,然后查询该 日期 的事件,并将结果的 几何形状 作为 GeoJSON 添加到你的地图中:
d=datetime.datetime.strptime('201781','%Y%m%d').date()
cursor.execute("SELECT address,crimetype,date,ST_AsGeoJSON(geom) from incidents where date =
'{}' ".format(str(d)))
incidents_date=cursor.fetchall()
for x in incidents_date:
layer=json.loads(x[3])
layergeojson=GeoJSON(data=layer)
map.add_layer(layergeojson)
上一段代码指定了一个日期(YYYYMD)为 2017 年 8 月 1 日。它查询我们使用的“事件”表,其中date = d并将几何返回为GeoJSON。然后它使用您用于区域命令的for循环和beats来映射“事件”。
当您在 Jupyter Notebook 中创建地图时,后续的代码块将修改该地图。您可能需要向上滚动以查看地图以查看更改。
您之前创建的地图现在将看起来像以下截图:

除了指定特定的日期外,您还可以获取所有日期大于特定日期的“事件”:
d=datetime.datetime.strptime('201781','%Y%m%d').date()
cursor.execute("SELECT address,crimetype,date,ST_AsGeoJSON(geom) from incidents where date >
'{}' ".format(str(d)))
或者,您也可以查询早于今天的日期间隔:
cursor.execute("select * from incidents where date >= NOW() - interval '10 day'")
上一段代码使用NOW()方法和 10 天间隔。通过指定>=,您将获得所有 10 天前以及更近期的犯罪。我在 2017 年 11 月 24 日写了这篇文章,所以结果将是 11 月 14 日(th)直到今天的所有“事件”。
多边形内的“事件”
我们的犯罪数据库有一个多边形区域——区域命令和beats——以及事件点。为了构建犯罪仪表板,我们希望能够在特定的区域命令或beat内映射“事件”。我们可以通过使用JOIN和ST_Intersects来实现这一点。以下代码向您展示了如何操作:
cursor.execute("SELECT ST_AsGeoJSON(i.geom) FROM incidents i JOIN areacommand acmd ON ST_Intersects(acmd.geom, i.geom) WHERE acmd.name like'FOOTHILLS' and date >= NOW() - interval '10 day';")
crime=cursor.fetchall()
for x in crime:
layer=json.loads(x[0])
layergeojson=GeoJSON(data=layer)
map.add_layer(layergeojson)
上一段代码从“事件”中选择“几何”作为GeoJSON(从“事件”中的ST_AsGeoJSON(i.geom)),其中事件ST_Intersects与多边形区域命令相交,具体来说,区域命令的名称是FOOTHILLS。代码通过将事件和区域命令表连接起来,其中相交为真来限制结果。代码通过仅选择过去 10 天的犯罪来限制结果。
代码随后遍历结果并将它们映射,就像之前的示例一样。您应该看到以下截图:

上一张截图显示了在“山丘”区域命令上叠加的“事件”。注意所有“事件”都在多边形内部。
您可以通过更改 SQL 查询来为特定的beats做同样的事情。以下代码将映射特定的beats:
cursor.execute("SELECT ST_AsGeoJSON(geom)from beats where beats.beat in ('336','523','117','226','638','636')")
c=cursor.fetchall()
for x in c:
layer=json.loads(x[0])
layergeojson=GeoJSON(data=layer)
map.add_layer(layergeojson)
上一段代码使用beats.beat字段的数组。在 Python 中,数组是[],但在 SQL 语句中,使用括号。结果是指定的beats。然后,代码将它们映射。
使用相同的指定beats,我们可以通过在ST_Intersects()上与beats进行连接来选择“事件”,并按代码所示映射“事件”:
cursor.execute("SELECT ST_AsGeoJSON(i.geom) FROM incidents i JOIN beats b ON ST_Intersects(b.geom, i.geom) WHERE b.beat in ('336','523','117','226','638','636') and date >= NOW() - interval '10 day';")
crime=cursor.fetchall()
for x in crime:
layer=json.loads(x[0])
layergeojson=GeoJSON(data=layer)
map.add_layer(layergeojson)
上一段代码传递了beats数组并再次通过最后 10 天进行过滤。然后它映射了“事件”,如下面的截图所示:

缓冲区
您已经从表中映射了数据,但现在您将映射地理处理任务的输出结果——缓冲区。
要编写一个缓冲区示例代码,我们首先需要创建一个点。以下代码将为我们完成这项工作:
from shapely.geometry import mapping
p = Point([-106.578677,35.062485])
pgeojson=mapping(p)
player=GeoJSON(data=pgeojson)
map.add_layer(player)
之前的代码使用 Shapely 创建一个点,然后使用shapely.geometry.mapping()将其转换为GeoJSON。接下来的两行允许我们在地图上显示它。
PostGIS 允许你将数据发送到数据库并获取数据,这些数据不必都在表中。例如,检查以下代码:
cursor.execute("SELECT ST_AsGeoJSON(ST_Buffer(ST_GeomFromText('{}')::geography,1500));".format(p.wkt))
buff=cursor.fetchall()
buffer=json.loads(buff[0][0])
bufferlayer=GeoJSON(data=buffer)
map.add_layer(bufferlayer)
之前的代码使用ST_Buffer()从 PostGIS 获取一个多边形。ST_Buffer()可以接受一个点地理和半径(以米为单位)来返回多边形。代码将结果包装在ST_AsGeoJSON中,以便我们可以进行映射。在这个例子中,结果集是一个单独的项目,所以我们不需要for循环。代码加载结果buff[0][0]并将其映射。
之前代码的结果显示在下述截图:

现在我们有一个多边形,我们可以用它来选择incidents。以下代码将执行与之前相同的查询,但我们将使用ST_AsText而不是ST_AsGeoJSON。我们不是映射多边形,而是将其用作点在多边形操作中的参数:
cursor.execute("SELECT ST_AsText(ST_Buffer(ST_GeomFromText('{}')::geography,1500));".format(p.wkt))
bufferwkt=cursor.fetchall()
b=loads(bufferwkt[0][0])
在之前的代码中,查询结果通过loads()传递给名为b的shapely多边形。现在,你可以使用ST_Intersects()将这个多边形传递给另一个查询,如下所示:
cursor.execute("SELECT ST_AsGeoJSON(incidents.geom) FROM incidents where ST_Intersects(ST_GeomFromText('{}'), incidents.geom) and date >= NOW() - interval '10 day';".format(b.wkt))
crime=cursor.fetchall()
for x in crime:
layer=json.loads(x[0])
layergeojson=GeoJSON(data=layer)
map.add_layer(layergeojson)
之前的代码选择与buffer(b.wkt)相交的incidents,以及在过去 10 天内的incidents。结果被映射。以下地图显示了之前代码的输出:

最近邻
使用buffer,你可以获取到兴趣点周围指定半径内的所有incidents。但如果你只想获取 5 个、10 个或 15 个最近的incidents呢?为了做到这一点,你可以使用<->运算符或 k-最近邻算法。
你可以使用以下代码选择距离指定点最近的15个点:
p = Point([-106.578677,35.062485])
cursor.execute("SELECT ST_AsGeoJSON(incidents.geom), ST_Distance(incidents.geom::geography,ST_GeometryFromText('{}')::geography) from incidents ORDER BY incidents.geom<->ST_GeometryFromText('{}') LIMIT 15".format(p.wkt,p.wkt))
c=cursor.fetchall()
for x in c:
layer=json.loads(x[0])
layergeojson=GeoJSON(data=layer)
map.add_layer(layergeojson)
之前的代码使用 Shapely 创建一个点,并在 SQL 查询中使用它。查询选择geometry作为GeoJSON的incidents,然后计算每个incidents与指定点的距离。ORDER BY子句、<->运算符和限制子句确保我们按接近程度顺序获取最近的15个点。
代码的最后一段是我们将结果添加到地图中的代码。结果显示在下述截图。截图中心的是指定的点:

现在你已经知道了如何映射空间查询的结果,让我们添加交互式小部件来修改查询并更改地图,而无需编写新代码。
交互式小部件
在本章的开头,你学习了如何根据date查询和映射incidents。在 Jupyter 中,你可以使用交互式小部件来更改值。以下代码将帮助我们了解如何使用ipywidgets导入interact,这将允许你插入一个DatePicker,以便你可以选择一个date与笔记本进行交互:

之前的代码导入interact和DatePicker小部件。在最简单的情况下,之前的截图显示了一个装饰器和函数,允许交互式选择日期并将其作为字符串显示。
当DatePicker改变时,x(DatePicker)被发送到函数theDate(x),并且x作为字符串打印出来。实际的返回值是datetime.date。
使用DatePicker小部件,你可以将date值传递给 SQL 查询,然后映射结果。当DatePicker改变时,你可以擦除地图并显示新的结果。以下代码将向您展示如何操作:
from ipywidgets import interact, interactive, fixed, interact_manual,DatePicker
import ipywidgets as widgets
@widgets.interact(x=DatePicker())
def theDate(x):
if x:
for l in map.layers[1:]:
map.remove_layer(l)
nohyphen=str(x).replace("-","")
d=datetime.datetime.strptime(nohyphen,'%Y%m%d').date()
cursor.execute("SELECT ST_AsGeoJSON(geom) from incidents where date
= '{}' ".format(str(d)))
c=cursor.fetchall()
for x in c:
layer=json.loads(x[0])
layergeojson=GeoJSON(data=layer)
map.add_layer(layergeojson)
return len(c)
else:
pass
之前的代码创建了一个交互式DatePicker小部件。代码中有一个if...else语句,因为第一次遍历时,x将是none。DatePicker没有被选择,所以在第一次遍历时我们pass。
接下来,代码获取地图上的所有图层,并使用map.remove_layer()从第二个([1:])图层开始删除它们。为什么是第二个图层?因为地图上的第一个图层是TileLayer——基础地图。我们希望它保持不变,并且只删除从 SQL 查询中添加的标记。
然后,代码从date字符串中删除连字符,并将其转换为datetime。一旦它成为datetime,你就可以将其传递给 SQL 查询。
下一个代码块是你在本章中用来将查询结果添加到地图上的相同代码块。
选择 2017 年 11 月 2 日的日期,如下截图所示:

当选择 2017 年 11 月 8 日时,地图被重新绘制并显示在以下截图中:

这些截图是在重新选择日期后立即生成的。用户可以使用DatePicker下拉菜单重新查询你的 PostGIS 数据库中的数据。
在 Jupyter 中,如果你将变量的值设置为字符串或整数,你将得到一个数字滑块或文本框。在以下截图中,装饰器有x="None",其中None是一个字符串。文本None是一个占位符,用于创建文本框。这将在文本框中创建一个包含单词None的文本框:

上一张截图中的代码如下所示。该代码将允许你输入区域命令的名称,然后显示该区域命令内的incidents:
@widgets.interact(x="None")
def areaCommand(x):
if x:
for l in map.layers[1:]:
map.remove_layer(l)
cursor.execute("SELECT ST_AsGeoJSON(i.geom) FROM incidents i
JOIN areacommand acmd ON
ST_Intersects(acmd.geom, i.geom) WHERE acmd.name like'{}' and
date >= NOW() - interval '10
day';".format(x))
c=cursor.fetchall()
for x in c:
layer=json.loads(x[0])
layergeojson=GeoJSON(data=layer)
map.add_layer(layergeojson)
return c
else:
pass
之前的代码从装饰器和字符串开始。这将绘制文本框。areaCommand()函数与前面提到的date示例作用相同,但它将字符串传递给 SQL 查询。它返回查询结果,并在地图上绘制incidents。
以下截图显示了NORTHWEST的返回值:

以下截图显示了用户在文本框中输入NORTHWEST时的地图:

在本节中,你学习了如何对空间数据进行查询,以及如何映射结果。在下一节中,你将学习如何绘制查询结果。
图表
地图是出色的数据可视化工具,但有时条形图也能解决问题。在本节中,你将学习如何使用 pandas.DataFrame 绘制你的数据图表。
DataFrame 存储二维表格数据(想想电子表格)。数据帧可以从许多不同的来源和数据结构中加载数据,但我们感兴趣的是它可以从 SQL 查询中加载数据。
以下代码将 SQL 查询加载到DataFrame中:
import pandas as pd
d=datetime.datetime.strptime('2017101','%Y%m%d').date()
cursor.execute("SELECT date, count(date) from incidents where date > '{}' group by date".format(str(d)))
df=pd.DataFrame(cursor.fetchall(),columns=["date","count"])
df.head()
之前的代码选择了“日期”,然后计算在incidents中日期大于 2017 年 10 月 1 日的每个“日期”的出现次数。然后使用 DataFrame(SQL,列)填充DataFrame。在这种情况下,代码传递 cursor.fetchall(), 和 columns=["date","count"]。使用 df.head() 显示了五条记录的结果。你可以使用 df.tail() 来查看最后五条记录,或者使用 df 来查看所有记录。
以下截图显示 df.head():

前面的截图显示,在 2017-10-17,有 175 个“事件”。
你可以通过从 pandas 库调用 plot() 方法来绘制DataFrame。以下代码将绘制df的条形图:
df.sort_values(by='date').plot(x="date",y="count",kind='bar',figsize=(15,10))
之前的代码按“日期”对数据帧进行排序。这样,我们的条形图中的日期就按时间顺序排列。然后使用条形图绘制数据,其中 x 轴是“日期”,y 轴是“计数”。我指定了图形大小,使其适合屏幕。对于较小的数据集,默认图形大小通常效果很好。
以下截图是 plot 的结果:

此图表向我们展示了地图无法展示的内容——犯罪似乎在周五和周六有所减少。
让我们通过使用beats的另一个示例来了解。以下代码将按beat加载犯罪:
cursor.execute("SELECT beats.beat, beats.agency, count(incidents.geom) as crimes from beats left join incidents on ST_Contains(beats.geom,incidents.geom) group by beats.beat, beats.agency")
area=pd.DataFrame(cursor.fetchall(),columns=["Area","Agency","Crimes"])
area.head()
之前的代码从beats表中选择了beat、agency和incidents的计数。注意left join。left join将给我们可能没有incidents的beats。连接基于事件位于beat多边形内。我们按每个选定的字段进行分组。
查询被加载到DataFrame中,并显示head()。结果如下截图所示:

注意,我们没有“无犯罪”的beats,而是缺失的beats。beats太多,无法滚动查看,所以让我们绘制DataFrame。我们将再次使用绘图函数,传递以下 x、y、kind 和 figsize:
area.plot(x="Area",y="Crimes",kind='bar',figsize=(25,10))
绘图的结果如下截图所示:

需要查看的数据量很大,但某些 beats 作为高犯罪率区域而突出。这是数据帧可以帮助的地方。您可以查询 DataFrame 而不是重新查询数据库。以下代码将绘制 beats 的选择:
area[(area['Crimes']>800)].plot(x='Area',y='Crimes',kind='bar')
上述代码将一个表达式传递给区域。该表达式选择 DataFrame 列 Crimes 中的记录,其中值超过 800;Crimes 是 count 列。结果如以下截图所示:

将您的查询加载到 DataFrame 中将允许您绘制数据,但还可以在不重新查询数据库的情况下再次切片和查询数据。您还可以使用交互式小部件允许用户修改图表,就像您使用地图学习的那样。
触发器
在任何数据库中,当数据被插入、更新或删除时,您可以让表启动一个 触发器。例如,如果用户插入一条记录,您可以启动一个触发器以确保记录满足某些指定的标准——没有空值。空间数据库允许您使用相同的触发器。您可以使用多种语言创建这些触发器,包括 Python 和 SQL。以下示例将使用 PL/pgsql。
您可以使用 SQL 表达式创建触发器。以下代码将创建一个触发器以防止输入不完整的事件:
query=('CREATE FUNCTION newcrime()'+'\n'
'RETURNS trigger' +'\n'
'AS $newcrime$' +'\n'
'BEGIN' +'\n'
'IF NEW.crimetype IS NULL THEN'+'\n'
'RAISE EXCEPTION' +" '% Must Include Crime Type', NEW.address;"+'\n'
'END IF;'+'\n'
'RETURN NEW;'+'\n'
'END;'+'\n'
'$newcrime$'+'\n'
'LANGUAGE \'plpgsql\';'
)
cursor.execute(query)
上述代码创建了一个名为 newcrime() 的新函数。该函数是一个 if 语句,用于检查 NEW.crimetype 是否为空。如果是,则不添加记录,并引发异常。异常将指出 NEW.address 必须包含犯罪类型。假设地址不为空。
现在您已经有一个函数,您可以创建一个调用该函数的触发器。以下代码展示了如何操作:
query=('CREATE TRIGGER newcrime BEFORE INSERT OR UPDATE ON incidents FOR EACH ROW EXECUTE PROCEDURE newcrime()')
cursor.execute(query)
connection.commit()
上述代码执行创建触发器的 SQL 语句。它是在 BEFORE INSERT OR UPDATE 时创建的。为了测试触发器,让我们插入一个没有犯罪类型的点。以下代码将尝试输入事件:
p=Point([-106,35])
address="123 Sesame St"
cursor.execute("INSERT INTO incidents (address, geom) VALUES ('{}', ST_GeomFromText('{}'))".format(address, p.wkt))
上述代码创建了一个只有 address 和 geom 的事件。执行上述代码的结果如以下截图所示:

在前面的截图中,内部错误指出 123 芝麻街必须包含犯罪类型。我们的触发器成功阻止了不良数据的输入。为了双重检查,我们可以查询 "123 芝麻街"。结果如以下截图所示:

触发器可以用于在发生更改时防止加载不良数据用于电子邮件或短信。例如,您可以允许用户输入他们感兴趣的多边形及其电话号码。当数据库中添加新事件时,您可以检查它是否在多边形内,如果是,则向与多边形关联的电话号码发送短信。
要安装触发器的其他语言,请打开 Stack Builder 并添加以下截图所示的附加组件:

摘要
在本章中,你学习了如何使用空间查询来执行地理处理任务。你还学习了如何使用 ipyleaflet 和数据框来映射和图表查询结果。你学习了如何使用 Jupyter 中的交互式小部件来修改地图和查询。最后,你了解了触发器的工作原理,并看到了一个使用触发器进行数据检查的快速示例。
在下一章中,你将学习如何使用 QGIS 执行地理处理任务。你将学习如何使用 QGIS 中已包含的工具箱。你将学习如何编写自己的工具箱,以便你可以使用并与其他 QGIS 用户共享,你还将学习如何使用 QGIS 来映射结果。结果可以保存为 QGIS 项目,或作为 QGIS 的许多空间数据格式之一。
第八章:自动化 QGIS 分析
本书已向你介绍了如何从命令行、Jupyter Notebook 和 IDE 中使用 Python 执行地理空间任务。虽然这三个工具将允许你完成任务,但很多时候工作需要使用桌面 GIS 软件来完成。
QGIS,一个流行的开源 GIS 应用程序,提供了桌面 GIS 功能,具有在 Python 控制台中工作的能力,以及使用 Python 编写工具箱和插件的能力。在本章中,你将学习如何使用 Python 操作桌面 GIS 数据,以及如何使用工具箱和插件自动化这些任务。
在本章中,你将学习如何:
-
加载和保存图层
-
从 API 数据源创建图层
-
添加、编辑和删除要素
-
选择特定要素
-
调用地理处理函数
-
编写地理处理工具箱
-
编写插件
在 Python 控制台中工作
QGIS Python 控制台是一个 Python 控制台。你可以执行所有正常的 Python 任务,并且有 QGIS 库的附加好处。从控制台,你可以操作 GIS 数据并在屏幕上显示,或者不显示。
Python 控制台位于 QGIS 工具栏的插件菜单下。你也可以通过按键盘上的Ctrl+Alt+P来访问它。控制台通常会打开在主窗口的底部。你可以通过点击标题栏(显示为 Python 控制台),按住鼠标按钮,并将窗口拖到屏幕上的另一个位置,或者通过点击控制台右上角的窗口按钮来取消停靠:

Python 控制台的截图
控制台有用于清除窗口、导入 GIS 和 QGIS 特定库、运行当前命令(你可以按Enter键而不是点击此按钮)、显示编辑器、修改选项和查看帮助文件的按钮。编辑器启动一个简化的文本编辑器,你可以用它来编写你的 Python 代码。它比命令行有一些优势。你可以使用编辑器打开现有的 Python 文件并运行或编辑它们。当你控制台里编写代码时,你可以将其保存到文件中。在控制台里,你需要选择所有内容,然后复制并粘贴到另一个文件中,移除所有输出。编辑器还允许你搜索文本、剪切文本、添加或删除注释,以及检查对象。
现在你已经了解了控制台和编辑器的基础知识,我们可以开始编写一些 Python 代码。
加载图层
你可能需要做的第一件事是加载一些现有的 GIS 数据。你可以打开几种不同的文件格式。执行此操作的方法是相同的。它是通过创建一个QgsVectorLayer并传递数据源参数、要在图层面板小部件中显示的图层名称以及如下代码中显示的提供者名称来完成的:
import requests
import json
from qgis.core import *
from qgis.PyQt.QtGui import *
from qgis.PyQt.QtWidgets
import *
from qgis.PyQt.QtCore import *
streets = QgsVectorLayer(r'C:\Users\Paul\Desktop\PythonBook\CHP8\Streets.shp', "Streets","ogr")
scf = QgsVectorLayer(r'C:\Users\Paul\Desktop\PythonBook\CHP8\SCF.shp', "SeeClickFix","ogr")
对于大多数矢量图层,你将使用"ogr"作为提供者。然后你可以使用以下代码将图层添加到地图中:
QgsMapLayerRegistry.instance().addMapLayers([scf,streets])
之前的代码将图层添加到地图注册表中。或者,您可以使用 iface 在单行代码中执行之前提到的代码,如下面的代码所示:
streets = iface.addVectorLayer(r'C:\Users\Paul\Desktop\PythonBook\CHP8\Streets.shp', "Streets","ogr")
scf = iface.addVectorLayer(r'C:\Users\Paul\Desktop\PythonBook\CHP8\SCF.shp', "SeeClickFix","ogr")
之前的代码在单个步骤中加载一个矢量图层并将其添加到注册表中。以下截图显示了在 QGIS 中添加的图层和添加到图层面板中的名称:

QGIS 中加载的图层截图
注册表包含地图文档中所有图层的列表。您可以使用以下代码获取已加载图层的列表:
QgsMapLayerRegistry.instance().mapLayers()
之前的代码应显示两个图层 SeeClickFix 和 Streets 已被加载:
{u'SeeClickFix20171129100436571': <qgis._core.QgsVectorLayer object at 0x000000002257F8C8>, u'Streets20171129100433367': <qgis._core.QgsVectorLayer object at 0x000000002257F268>}
您可以使用 removeMapLayer() 并传递要删除的图层的 id 来从地图中删除图层。id 是调用 mapLayers() 的结果中的字符串。在这种情况下,加载的图层的 id 是 'Steets20171129092415901'。以下代码将删除图层:
QgsMapLayerRegistry.instance().removeMapLayer('Streets20171129100433367')
之前的代码将图层 id 传递给 removeMapLayer()。由于数据是在 streets 变量中加载的,因此您也可以传递 streets.id() 而不是输入图层 id,如下面的代码所示:
QgsMapLayerRegistry.instance().removeMapLayer(streets.id())
这两种方法都将导致图层从地图中移除。
处理图层
一旦图层被加载,您将想要检查图层及其中的要素。对于图层,您可能想知道投影、坐标参考系统以及它有多少个要素。
图层属性
要找到坐标参考系统,您可以在图层上使用 crs(),如下面的代码所示:
crs = scf.crs()
之前的代码将坐标参考系统分配给变量 crs。从这里,您可以通过以下代码获取描述来检查它:
crs.description()
之前的代码将返回以下输出:
'WGS 84'
对于坐标参考系统的已知文本(WKT)表示,您可以使用 toWkt() 方法:
crs.toWkt()
这将返回以下结果:
'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]]'
您可以使用以下 extent() 方法获取图层的边界框:
extent = scf.extent()
然后,您可以使用 toString() 获取范围字符串,使用 asWktPolygon() 获取 WKT,或者您可以使用 xMinimum()、xMaximum()、yMinimum() 和 yMaximum() 分别获取每个坐标。方法和它们的输出如下所示:
extent.toString()
u'-106.6649165999999980,35.0744279999999975 : -106.6457526013259951,35.0916344666666973'
extent.asWktPolygon()
'POLYGON((-106.66491659999999797 35.0744279999999975, -106.6457526013259951 35.0744279999999975, -106.6457526013259951 35.09163446666669728, -106.66491659999999797 35.09163446666669728, -106.66491659999999797 35.0744279999999975))'
extent.xMinimum()
-106.6649166
extent.xMaximum()
-106.645752601326
extent.yMinimum()
35.074428
extent.yMaximum()
35.0916344666667
要查看对象上的可用方法,请使用 dir(object)。
要查看范围对象的属性,请使用 dir(extent)。
您可以使用 pendingFeatureCount() 获取图层中的要素数量。以下代码返回 SeeClickFix 图层的要素计数:
scf.pendingFeatureCount()
结果是长数据类型,在这种情况下等于 126。
要素属性
您可以使用以下代码获取第一个要素:
item=scf.getFeatures().next()
之前的代码使用getFeatures().next()获取第一个要素并将其分配给item变量。如果你移除.next(),你将得到一个QgsFeatureIterator,它允许你遍历所有要素。在以下示例中,我们将使用单个要素。
要获取geometry,将其分配给一个变量,如下所示:
g = item.geometry()
要获取type,你可以使用以下代码:
g.type()
0
之前的代码对于点返回0。知道要素是点,我们可以使用asPoint()来查看坐标,如下所示:
item.geometry().asPoint()
(-106.652,35.0912)
item.geometry().asPoint()[0]
-106.65153503418
item.geometry().asPoint()[1]
35.0912475585134
如果我们在streets层上尝试相同的代码,我们将得到类型1和Polyline的坐标,如下所示:
street = streets.getFeatures().next().geometry().type()
1
street.geometry().asPolyline()
[(-106.729,35.1659), (-106.729,35.1659), (-106.729,35.1658), (-106.729,35.1658), (-106.73,35.1658), (-106.73,35.1658), (-106.73,35.1658), (-106.73,35.1658)]
要获取要素中字段的详细信息,使用fields(),如下所示:
item.fields().count()
4
你可以通过为四个字段中的每一个使用.name()和.typeName()方法来获取字段名和类型。使用字段2,以下代码将展示如何获取名称和类型:
item.fields()[2].name()
u'Type'
item.fields()[2].typeName()
u'String'
知道字段的名称,你可以获取第一条记录的字段值。或者,你始终可以使用以下代码中的数值索引:
item["Type"]
u'Other'
item[0]
1572.0
item[1]
3368133L
item[2]
u'Other'
item[3]
u'Acknowledged'
现在你已经知道了如何访问要素的几何和属性,你可以使用getFeatures()方法遍历要素。以下代码将遍历要素并打印所有状态为'Closed'的记录的ID:
for f in scf.getFeatures():
if f["Status"]=='Closed':
print(f["ID"])
之前的代码使用getFeatures()返回一个迭代器。然后它检查Status属性是否等于'Closed',如果是,则打印属性ID。输出如下所示:
3888698
3906283
3906252
3882952
3904754
3904463
3904344
3904289
3903243
3903236
3902993
从 PostGIS 绘制图层
QGIS 允许你使用QgsDataSourceURI类和QgsVectorLayer(URI,名称,提供者(Postgres))加载 PostgreSQL 层。为了使这成为可能,QGIS 需要编译带有 Postgres 支持的版本。在本节中,你将使用在第三章,地理空间数据库简介和第七章,地理数据库处理中学到的psycopg2。本节中添加要素到层和层到地图的方法将在本章后面学习如何编写工具箱时使用。
绘制点
在你学习如何从 PostGIS 加载数据之前,我们首先介绍如何绘制多个点,将它们转换为要素,将它们添加到层中,然后将层加载到地图上。以下代码将引导你完成这个过程。
首先创建一个如代码所示的记忆层:
theLayer=QgsVectorLayer('Point?crs=epsg:4326','SomePoints','memory')
之前的代码创建了一个矢量层并将其分配给变量theLayer。参数包括层的类型和坐标参考系统、层面板的名称,我们指定它是一个memory层。
接下来,你需要创建要素:
from qgis.PyQt.QtCore import *
theFeatures=theLayer.dataProvider()
theFeatures.addAttributes([QgsField("ID", QVariant.Int),QgsField("Name", Qvariant.String)])
之前的代码导入qgis.PyQtCore。您需要这个库来使用QVariant。首先,您调用图层的数据提供者并将其传递给要素。接下来,您将属性及其类型添加到要素中。在以下代码中,您创建了一个point并将其添加到要素中:
p=QgsFeature()
point=QgsPoint(-106.3463,34.9685)
p.setGeometry(QgsGeometry.fromPoint(point))
p.setAttributes([123,"Paul"])
theFeatures.addFeatures([p])
theLayer.updateExtents()
theLayer.updateFields()
之前的代码创建了一个p变量并将其设置为QgsFeature。然后创建了一个点p并传递经纬度坐标。该要素从point分配了几何形状。接下来,您将属性分配给要素。现在您有一个具有几何形状和属性的要素。在下一条线中,您使用addFeature()将要素传递到要素数组中。最后,您更新图层的范围和字段。
将代码块重复第二次,并将point分配不同的坐标,(-106.4540,34.9553),然后按照本章前面的部分,在以下代码中将其添加到地图中:
QgsMapLayerRegistry.instance().addMapLayers([theLayer])
您现在将有一个包含两个点的地图,如下面的屏幕截图所示:

从 Python 控制台加载到 QGIS 中的具有属性的两个点
您可以在图层面板中看到图层被命名为 SomePoints。在属性表中,您可以看到两个字段,ID 和 Name,对应两个要素。现在您知道了如何从几何形状创建要素,添加属性,将要素添加到图层中,并在地图上显示图层,我们将添加 PostGIS 并将之前提到的过程循环一遍。
从 PostGIS 绘制多边形
在本例中,您将从 PostGIS 数据库中绘制阿尔伯克基警察局区域指挥部的多边形。您将使用以下代码,并添加一个 PostGIS 查询,一个循环来添加所有要素,以及一个 WKT 函数来绘制几何形状而不是硬编码坐标。
第一步是连接到 PostGIS 数据库。以下代码与您在第三章,地理空间数据库简介和第七章,使用地理数据库进行地理处理中使用的代码相同:
import psycopg2
connection =
psycopg2.connect(database="pythonspatial",user="postgres",
password="postgres")
cursor = connection.cursor()
cursor.execute("SELECT name, ST_AsTexT(geom) from areacommand")
c=cursor.fetchall()
之前的代码连接到 PostGIS,获取所有带有名称和几何形状的区域指挥部,并将它们分配给c变量。接下来,您将创建与前面示例相同的图层。您将创建一个计数器x并将其设置为要素的ID字段:
APD=QgsVectorLayer('Polygon?crs=epsg:4326','AreaCommands','memory')
APDfeatures=APD.dataProvider()
APDfeatures.addAttributes([QgsField("ID",QVariant.Int),QgsField("Name", QVariant.String)])
x=0
之前的代码创建了一个memory多边形图层,创建了要素并添加了属性。接下来,您将遍历cursor,为每个区域指挥部创建几何形状并添加属性,然后更新图层的范围和字段:
for acmd in c:
g=QgsGeometry()
g=QgsGeometry.fromWkt(acmd[1])
p=QgsFeature()
print(acmd[0])
p.setGeometry(g)
x+=1
p.setAttributes([x,str(acmd[0])])
APDfeatures.addFeatures([p])
APD.updateExtents()
APD.updateFields()
之前的代码与上一节中的点示例相同。主要区别在于您正在使用QgsGeometry.fromWkt(wkt)创建多边形。acmd[1]变量是从 PostGIS 的MultiPolygon WKT 字符串。
最后,按照以下代码将图层添加到地图中:
QgsMapLayerRegistry.instance().addMapLayers([APD])
以下代码将渲染如下截图:

由此,您就拥有了阿尔伯克基警察局的区域命令多边形图层。接下来,您将学习如何向图层添加、编辑和删除特征。
添加、编辑和删除特征
在前面的示例中,您创建了一个空图层并添加了字段,然后添加了数据并显示它。您将需要这样做的时候,大多数情况下,您已经有一个图层,您需要向其中添加数据、编辑数据或从其中删除数据。在本节中,您将学习如何对现有数据执行这些任务。
向现有图层添加特征
要向图层添加数据,您首先需要加载图层。首先,按照以下代码加载一些 SeeClickFix 数据的子集,用于阿尔伯克基:
scf = iface.addVectorLayer(r'C:\Users\Paul\Desktop\PythonBook\CHP8\SCF.shp', "SeeClickFix","ogr")
前面的代码加载并显示地图上的图层。这是本章第一节的相同代码。
您不需要在地图上显示图层即可与之工作。您可以使用 scf = QgsVectorLayer("C:\Users\Paul\Desktop\PythonBook\CHP8\SCF.shp", "SeeClickFix","ogr") 加载图层。
现在您已经加载了图层,可以使用 capabilitiesString() 来查看提供者允许对数据进行哪些操作。以下代码显示了加载图层的结果:
scf.dataProvider().capabilitiesString()
u'Add Features, Delete Features, Change Attribute Values, Add Attributes, Delete Attributes, Rename Attributes, Create Spatial Index, Create Attribute Indexes, Fast Access to Features at ID, Change Geometries'
由于 添加特征 是一种能力,您可以通过以下代码添加一个新特征:
feat = QgsFeature(scf.pendingFields())
feat.setAttribute('fid',911)
feat.setAttribute('ID',311)
feat.setAttribute('Type','Pothole')
feat.setAttribute('Status','Pending')
feat.setGeometry(QgsGeometry.fromPoint(QgsPoint(-106.65897,35.07743)))
scf.dataProvider().addFeatures([feat])
前面的代码创建了一个特征并从加载的图层中获取字段。然后它设置每个属性。接下来,它从点设置几何形状。最后,特征被添加到图层中。当您调用 addFeatures() 时,有两个返回值可以分配给变量——结果和特征。addFeature() 的结果将是真或假。返回的特征是特征列表。如果您需要对该特征执行更多操作,保留该特征可能很方便。
在自动化流程时,您可以在尝试编辑图层之前执行能力检查。
结果如以下截图所示,在属性表中添加了一个新的点和记录:

添加到图层中的特征
您可以通过在单行中传递所有属性来简化前面的代码,使用列表。以下代码展示了如何操作:
feat.setAttributes([912,312,"Other","Closed"])
前面的代码使用列表和 setAttributes() 而不是单个 setAttribute() 来写入属性。如果您想在以后阅读代码时记住字段名,更冗长的版本更清晰。但如果代码的效率是您的目标,后者版本更合适。
如果我们犯了错误,或者有我们不需要的记录怎么办?下一节将向您展示如何删除一个特征。
从现有图层中删除特征
删除特征可以通过一行代码完成,格式如下所示:
LayerName.dataProvider().deleteFeatures([list of id])
在之前的代码中,你使用了deleteFeatures()和层的id。id是feature.id(),它是一个内部持有的数字,而不是用户指定的属性。要获取特定特征的id,你可以像在本章早期学习的那样遍历它们。以下代码展示了如何删除上一节中创建的特征:
for x in scf.getFeatures():
if x["ID"]==311:
scf.dataProvider().deleteFeatures([x.id()])
之前的代码遍历层中的特征,寻找具有ID为311的那个。当找到它时,它使用deleteFeatures()并通过x.id()传递id。在这种情况下,id是216。如果你知道特征的id,你可以不通过循环就删除它。
你也可以传递一个 ID 列表,如下面的代码所示:
for x in scf.getFeatures():
if x["Status"]=='Closed':
key.append(x.id())
scf.dataProvider().deleteFeatures(key)
之前的代码遍历层中的特征,寻找所有的'Closed'案件。当找到其中一个时,它将id放入列表key中。最后,它调用deleteFeatures()并传递列表。
从现有层编辑特征
现在,你可以添加和删除特征,但有时你可能只需要更改一个属性值。例如,将开放案件状态更改为关闭案件状态。在本节中,你将学习如何修改属性。
通过调用changeAttributeValues()来修改属性。以下代码更改了一个单一的特征:
scf.dataProvider().changeAttributeValues({114:{0:123,1:345,2:"ADA",3:"NEW"} })
之前的代码调用changeAttributeValues()并传递一个字典,其键是特征的id,值是一个属性字典—{id:{0:value, 1:value, n:value}}。属性字典的键是字段索引。特征中有四个字段,因此属性字典将有键0到3。以下截图显示了属性表的变化:

单个特征编辑
之前的示例假设你已经知道要修改的特征的id。它还假设你想要修改所有属性值。以下代码将修改多个特征,但每个特征只修改一个属性值——状态:
attributes={3:"Closed"}
for x in scf.getFeatures():
if x["Type"]=='Other':
scf.dataProvider().changeAttributeValues({x.id():attributes})
在之前的代码中,声明了一个具有键3('Status'字段)和值"Closed"的字典。然后代码遍历层中的特征,寻找匹配项。当找到匹配项时,它更改属性值,但这次只更改状态字段的值。结果反映在截图所示的属性表中如下:

所有其他类型的特征现在都具有开放状态
在之前的示例中,你已经通过迭代特征并根据条件选择它们。在下一节中,你将学习如何突出显示所选特征以及如何使用表达式来选择而不是条件。
使用表达式选择特征
使用表达式,你可以遍历特征并评估返回真(1)或假(0)的表达式。在我们深入探讨表达式之前,让我们选择并突出显示一个特征。选择一个特征是通过调用setSelectedFeatures()并传递一个 ID 列表来完成的。以下代码将选择一个单一的特征:
from qgis.PyQt.QtGui import *
from qgis.PyQt.QtWidgets import *
iface.mapCanvas().setSelectionColor( QColor("red") )
scf.setSelectedFeatures([100])
之前的代码导入QtGUI和Qt.Widgets。这些是使用QColor设置颜色所需的。下一行获取地图画布并将部分颜色设置为red。最后,代码选择具有id为100的特征。现在地图上会显示red。
之前的例子假设你想选择一个单一的特征,并且你知道其id。这种情况很少见。大多数情况下,你将想要根据某些条件或使用一个表达式来选择。使用QgsExpression(),你可以传递一个表达式字符串并对其特征进行评估。以下代码展示了如何操作:
closed=[]
exp=QgsExpression("Type='Traffic Signs' and Status='Acknowledged'")
exp.prepare(scf.pendingFields())
for f in scf.getFeatures():
if exp.evaluate(f)==1:
closed.append(f.id())
scf.setSelectedFeatures(closed)
首先,之前的代码创建了一个列表closed来存储表达式评估为真的 ID。接下来声明表达式。表达式检查类型和状态上的两个条件。表达式被准备并传递了图层中的字段。下一行遍历特征。如果表达式为真(1),则id被放入列表中。最后,将选定的特征设置为closed列表中的 ID。
之前代码的结果如下所示:

根据表达式选择的特征
在下一节中,你将学习如何使用 QGIS 附带的工具箱来执行算法和执行地理空间任务。
使用 Python 中的工具箱
QGIS 有一个处理库。如果你在 QGIS 中转到处理菜单并选择工具箱,你会看到一个显示有工具箱分组的窗口小部件。窗口小部件看起来如下所示:

处理窗口小部件
你可以通过导入processing来访问 Python 中的工具箱。你可以通过执行以下代码来查看可用的算法:
import processing
processing.alglist()
之前的代码导入processing并调用alglist()方法。结果是所有安装的工具箱中的可用算法。你应该看到以下类似的输出:
Advanced Python field calculator--------------------->qgis:advancedpythonfieldcalculator
Bar plot--------------------------------------------->qgis:barplot
Basic statistics for numeric fields------------------>qgis:basicstatisticsfornumericfields
Basic statistics for text fields--------------------->qgis:basicstatisticsfortextfields
Boundary--------------------------------------------->qgis:boundary
Bounding boxes--------------------------------------->qgis:boundingboxes
Build virtual vector--------------------------------->qgis:buildvirtualvector
Check validity--------------------------------------->qgis:checkvalidity
Clip------------------------------------------------->qgis:clip
要通过关键字搜索算法,你可以像以下代码一样将字符串传递给alglist():
Processing.alglist("buffer")
之前的代码传递一个字符串来缩小结果。输出将包含包含单词buffer的几个算法。如下所示:
Fixed distance buffer-------------------------------->qgis:fixeddistancebuffer
Variable distance buffer----------------------------->saga:shapesbufferattributedistance
Buffer vectors--------------------------------------->gdalogr:buffervectors
v.buffer.column - Creates a buffer around features of given type.--->grass:v.buffer.column
在本节中,我们将使用Buffer vectors算法。要了解算法的工作原理,你可以运行以下代码:
processing.alghelp("gdalogr:buffervectors")
之前的代码调用alghelp()并传递alglist()的第二个列中找到的算法名称。结果将告诉你执行算法所需的参数及其类型。输出如下所示:
ALGORITHM: Buffer vectors
INPUT_LAYER <ParameterVector>
GEOMETRY <ParameterString>
DISTANCE <ParameterNumber>
DISSOLVEALL <ParameterBoolean>
FIELD <parameters from INPUT_LAYER>
MULTI <ParameterBoolean>
OPTIONS <ParameterString>
OUTPUT_LAYER <OutputVector>
如果你从 GUI 运行算法,然后打开 \.qgis2\processing\processing.log,你将看到执行算法使用的参数。复制它们并在你的 Python 代码中使用。
之前的输出显示了运行算法所需的参数。通过使用 runalg(),你可以执行算法。缓冲矢量在代码中的执行如下:
processing.runalg("gdalogr:buffervectors",r'C:/Users/Paul/Desktop/Projected.shp',"geometry",100,False,None,False,"",r'C:/Users/Paul/Desktop
/ProjectedBuffer.shp')
layer = iface.addVectorLayer(r'C:\Users\Paul\Desktop\
ProjectedBuffer.shp', "Buffer", "ogr")
之前的代码调用 runalg() 并传递我们想要运行的算法的名称,然后是算法所需的参数。在这种情况下:
INPUT_LAYER = Projected.shp
GEOMETRY = geometry
DISTANCE = 100
DISSOLVEALL = False
FIELD = None
MULTI = False
OPTIONS = “”
OUTPUT_LAYER = ProjectedBuffer.shp
然后将输出图层添加到地图上。结果如下截图所示:

缓冲区算法的结果
现在你已经知道了如何使用 Python 控制台和调用算法,让我们编写自己的算法。下一节将向你展示如何创建一个可以使用 runalg() 或通过使用 GUI 调用的工具箱。
编写自定义工具箱
编写工具箱将允许你自动化多个任务,并将该代码作为 GUI 供用户使用,或作为其他开发者可以执行的算法使用。在本节中,你将学习如何创建工具箱并从处理中调用它。
在本章中,你已经学会了如何从文件和 PostGIS 数据库加载数据。在这个例子中,你将学习如何从 SeeClickFix应用程序编程接口(API)将数据引入 QGIS。
SeeClickFix 是一个在美国许多城市使用的 311 报告系统。它包含地理空间数据,并拥有非常详细且用户友好的 API。
要创建一个新的脚本,请在 QGIS 中打开处理工具箱。这将打开一个编辑器窗口。你将在该窗口中编写代码,并使用保存图标保存它。文件名将成为 Tools|User scripts|File name 下的工具箱。保存文件并命名为 SeeClickFix。
现在你有一个空的工具箱,我们可以开始添加代码。在代码之前,你需要创建你想要传递给这个算法的参数。每个参数也将成为一个具有参数名称作为标签的 GUI 小部件。SeeClickFix API 允许你指定一个城市或地区,并且还可以过滤字符串。以下代码将把这些作为参数添加到我们的算法中:
##City_or_Neighborhood= string City
##Filter=string Nothing
##Output=output vector
之前的代码使用双注释符号(##),然后是参数名称,后面跟着参数类型和一个默认值。对于数字和字符串,需要默认值。代码中的第一个参数是城市或地区,它是一个 string,默认为阿尔伯克基。接下来是过滤关键字,它也是一个 string,默认为 Nothing。最后,代码有一个输出,它是一种 output vector 类型。输出将是要添加到地图或保存到磁盘的内容。
在这一点上,你可以在 GUI 中运行工具箱,你将看到以下截图所示的窗口:

工具箱的 GUI。注意每个参数都是一个标签
接下来,您可以导入执行任务所需的库。以下代码将导入SeeClickFix工具箱所需的库:
import requests
import json
from qgis.core import *
from qgis.PyQt.QtGui import *
from qgis.PyQt.QtWidgets import *
from qgis.PyQt.QtCore import *
之前的代码导入了qgis库以及requests和json。requests库将被用来进行 API 调用,而json将解析请求的响应到json。
现在是时候编写一些代码了。首先,您需要获取进行 API 调用所需的参数和变量,并且为用户提供一些关于正在发生的事情的信息也不会有害。以下代码将向您展示如何操作:
scfcity=City_or_Neighborhood
searchterm=Filter
progress.setInfo("Wait while I get data from the API")
progress.setText("Calling API")
if searchterm=="None":
pagesURL="http://seeclickfix.com/api/v2/issues?per_page=100&amp;place_url="+scf city+"&amp;page="
url="http://seeclickfix.com/api/v2/issues?per_page=100&amp;place_url="+scfcity
else:
pagesURL="http://seeclickfix.com/api/v2/issuesper_page=100&amp;place_url="+scfc ity+"&amp;search="+searchterm+"&amp;page="
url="http://seeclickfix.com/api/v2/issues?per_page=100&amp;search="+searchterm+"&amp;place_url="+scfcity
之前的代码将参数传递给变量。然后,我使用了一个全局变量progress,这是由 QGIS 提供的,并调用setInfo()和setText()方法来告诉用户正在发生什么。progress是 QGIS 的一部分。setInfo()方法在 GUI 的文本区域中显示文本。setText()方法更改进度条上的标签文本,并将其添加到 GUI 的文本区域中。
接下来,代码检查过滤器参数是否仍然是None,如果是,它将没有过滤器参数的 API 的统一资源定位符(URL)作为一个字符串分配给 API,并使用城市或区域参数。如果有过滤器,将分配一个不同的 URL 来进行 API 调用。
现在您已经准备好进行一些 GIS 特定的设置。以下代码将为您开始:
crs=QgsCoordinateReferenceSystem("epsg:4326")
scf=QgsVectorLayer('Point?crs=epsg:4326','SeeClickFix','memory')
fields = QgsFields()
fields.append(QgsField("ID", QVariant.Int))
fields.append(QgsField("Type", QVariant.String))
fields.append(QgsField("Status", QVariant.String))
writer = processing.VectorWriter(Output, None, fields.toList(),
QGis.WKBPoint, crs)
之前的代码在 WGS 84 坐标系中设置了一个坐标参考系统。然后,它创建了一个memory图层,并分配了字段。最后,它创建了一个writer矢量并传递了输出参数、编码(None)、字段、几何类型和坐标参考系统。现在您可以根据代码进行 API 调用:
r = requests.get(url).text
rJSON=json.loads(r)
pages=rJSON['metadata']['pagination']['pages']
records=rJSON['metadata']['pagination']['entries']
progress.setInfo("Grabbing "+str(records) +" Records")
count=1
for x in range(1,pages+1):
progress.setText("Reading page "+str(x))
pageData=requests.get(pagesURL+str(x)).text
pageDataJSON=json.loads(pageData)
之前的代码使用requests进行 API 调用。它为pages的数量和返回的records数量分配了一个变量。使用setInfo()方法,代码告诉用户正在处理多少records。然后它遍历每一页,从page中加载项目。它告诉用户当前正在读取哪一页。
现在,代码将解析page上的每个record作为一个要素并发送到矢量writer。您不需要将输出添加到地图上。如果从 GUI 运行,处理将为您处理。如果您从 Python 运行它,您将获得图层的文件路径,可以自行加载。以下代码将向您展示如何操作:
for issue in pageDataJSON['issues']:
try:
p=QgsFeature()
point=QgsPoint(issue['lng'],issue['lat'])
p.setGeometry(QgsGeometry.fromPoint(point))
p.setAttributes([issue["id"],issue["request_type"]
["title"],issue["status"]])
writer.addFeature(p)
progress.setPercentage((count/float(records))*100)
count+=1
except:
pass
del writer
之前的代码创建了一个要素并将 API 中的几何形状传递到一个point。然后,它传递属性并将完成的要素发送到矢量writer。使用progress.setPercentage()更新 GUI 上的进度条。该方法接受一个float。在这个例子中,百分比是处理过的records数量除以总记录数。最后,您删除了writer。
您的工具箱已经完成,请保存它。现在用户可以从 GUI 中运行它,或者您可以从 Python 中调用它。以下代码将向您展示如何从 Python 中调用它并将结果添加到地图中:
processing.alghelp("script:seeclickfix")
ALGORITHM: SeeClickFix
City_or_Neighborhood <ParameterString>
Filter <ParameterString>
Output <OutputVector>
out=processing.runalg("script:seeclickfix","Albuquerque","Juan Tabo",None)
以下代码调用 alghelp() 方法来显示我们的新算法及其参数。接下来,它使用 runalg() 运行算法,并将结果分配给 out 变量。打印 out 变量将显示一个包含 Output 键和指向临时向量的路径的字典,如下所示:
out
{'Output': u'C:\\Users\\Paul\\AppData\\Local\\Temp\\processingca7241c6176e42458ea32e8c7264de1e\\014bc4d4516240028ce9270b49c5fcaf\\Output.shp'}
您可以将向量分配给图层并将其添加到地图中,或者您可以遍历要素并对其进行其他操作,如下所示:
out = iface.addVectorLayer(str(a["Output"]), "SeeClickFix","ogr")
for feature in out.getFeatures():
Do something...
将图层添加到地图的结果将类似于以下截图。所有沿 Juan Tabo 街道报告的 SeeClickFix 事件:

工具箱的结果
摘要
在本章中,您学习了如何在 QGIS 中使用 Python。您从学习加载图层并在地图上显示图层的基本知识开始,然后逐步学习添加、编辑和删除要素。您学习了如何选择要素、突出显示选择,以及如何使用表达式。然后,我们利用预构建的地理处理工具,您学习了如何使用处理调用工具箱算法。最后,您学习了如何编写自己的工具箱。
在下一章中,您将学习如何使用 Python 与 Esri 工具一起使用。您将学习如何在浏览器中使用 Jupyter Notebooks 与基于云的数据集进行交互,以及如何使用 Python 的 Esri API 执行基本的地理空间分析和创建 ArcGIS Online 网上地图。
第九章:ArcGIS API for Python 和 ArcGIS Online
本章将介绍 ArcGIS 应用程序编程接口(API)for Python 和 ArcGIS Online。ArcGIS API for Python 是一个用于处理地图和地理空间数据的 Python 库。此 API 可以使用 conda 本地安装,并与 Esri 的云 GIS 进行交互,无论是 ArcGIS Online(SaaS)还是 Portal for ArcGIS,这是一个提供本地云 GIS 部署的服务器产品。该 API 为使用 Python 进行网络地图脚本化提供了现代解决方案,并且与 Jupyter Notebooks 兼容良好。
本章将涵盖以下主题:
-
介绍 ArcGIS API for Python
-
安装 API
-
使用不同的 Esri 用户账户与 API 交互
-
介绍 API 的某些模块
-
与 API 的地图小部件交互
-
搜索和显示矢量数据
-
显示和地理处理栅格数据
-
为使用 ArcGIS Online 设置个性化账户
-
在 ArcGIS Online 中发布和管理内容
介绍 ArcGIS API for Python 和 ArcGIS Online
Esri,一家以其 ArcGIS 平台而闻名的地理空间软件公司,已将 Python 集成到其 ArcGIS 桌面软件及其继任者 ArcGIS Pro 中。Esri 开发的第一个 Python 站点包是 ArcPy 站点包,它是一组 Python 模块,提供了所有现有的以及扩展的 ArcMap 和 ArcGIS Pro 功能。现在,Python 可以用作脚本和编程语言来自动化涉及大量与 图形用户界面(GUI)交互的重复性任务。通过 ArcPy,这些任务可以通过 Python 脚本、插件或工具箱来执行。
Python 成功地与 ArcGIS 桌面一起引入,而 GIS 本身正转向云端——不仅地理空间数据,还有软件本身。Esri 通过各种云环境提供方案,使用公共、私有或混合云服务,为组织提供了这样做的方式。在本章中,我们将使用 ArcGIS Online,这是一个允许用户创建、存储和管理地图、应用程序和数据的 软件即服务(SaaS)提供方案。在过去的几年里,ArcGIS Online 已成为 Esri ArcGIS 系统的关键组成部分和有机组成部分。其用户可以通过可用于网页、智能手机和平板电脑的现成工具在组织内或全球范围内共享地图。
Pythonic 网络 API
为了让用户能够与其 GIS 数据、服务等进行交互,Esri 开发了一个全新的 Pythonic 网络 API,称为 ArcGIS API for Python,它包含一组用于构建软件和应用程序的子程序定义、协议和工具。它是建立在 ArcGIS 表示状态转移(REST)API 和 ArcGIS API for JavaScript 之上的。此相同的 API 还在 Python API 的后台用于显示 2D 和 3D 网络地图。
GIS 用户可以下载免费提供的 ArcGIS API for Python 并使用它来管理他们的云 GIS 环境,无论是 ArcGIS Online、ArcGIS Portal 还是 ArcGIS Enterprise(以前称为 ArcGIS Server)产品系列。该 API 需要 Python 3.5 或更高版本。你可以与 ArcPy 站点包一起使用该 API,但这不是必需的,API 也可以在没有 ArcPy(或任何基于桌面的 GIS 产品)的情况下工作,甚至在没有 ArcGIS Online 或 Portal 环境的情况下。
该 API 是针对比当前 Python 用户更广泛的受众编写的,这些用户会使用它进行数据处理或地图设计——除了脚本功能外,API 还允许进行 GIS 可视化和分析、空间数据/内容管理以及组织管理。自 2006 年 12 月首次发布以来,API 已经经历了几次更新,截至写作时,当前版本为 1.4。每个新版本都引入了新功能。使用 API 与使用任何其他 Python 库类似——你通过 import 语句导入 API 并立即开始使用它,应用标准的 Python 语法和命令。当你在一个网络环境中使用它来访问网络 GIS 时,最好使用基于浏览器的 Jupyter Notebook 应用程序。
安装 API
API 可以以不同的方式安装。最简单的方法是使用 conda。如果你是第一次安装 API,你可能想通过 Anaconda3 为 API 创建一个单独的虚拟环境,因为它的依赖项很多。安装最新可用的 API 版本很重要,因为它也将确保你安装了最新的 conda 版本和 API 的依赖项。要使用 conda 安装 API,请在终端中运行以下命令:
conda install -c esri arcgis
命令中的 -c 指的是频道(这是一个在线仓库)。当你在终端中运行此命令时,你将被要求安装一系列依赖项。以下截图显示了部分列表。请注意,NumPy 和 pandas 也被安装了,这两个库来自 SciPy 堆栈,用于数据科学。API 本身是列表中的第一个包,称为 arcgis:

测试 API
安装完成后,ArcGIS 软件包可以在 C:\UserName\Anaconda3\pkgs 文件夹内的一个名为 arcgis 的单独文件夹中找到,文件夹名称后面跟着版本号。如果你已经在你的电脑上安装了 API,你可能需要将其更新到最新版本以确保一切正常工作,例如地图小部件:
conda upgrade -c esri arcgis
截至写作时,API 的最新版本是 1.4,需要 Python 3.5 或更高版本。你可以通过以下方式在终端中打开 Jupyter Notebook 应用程序来测试你的安装,或者直接从 Anaconda3 运行应用程序:
jupyter notebook
然后,运行以下代码并检查是否打开地图窗口,并且没有错误信息:
In: from arcgis.gis import GIS
my_gis = GIS()
my_gis.map()
如果您已安装 ArcGIS Pro,则可以通过 Pro 内部的 conda 环境使用 Python 软件包管理器来安装 API。查找 Python 选项卡并点击添加包按钮。搜索 arcgis,点击安装,并接受条款和条件。
故障排除
如果由于某些原因,您无法在本地上安装和使用 API,您还可以尝试 API 的沙盒版本,该版本在云中运行notebooks.esri.com/。通过点击此 URL,将打开一个浏览器窗口,其中包含 Jupyter Notebook,您可以在其中创建自己的笔记本,运行代码示例,并使用 API 的所有功能。
要查看显示所有模块、带有描述的类和示例的在线 API 参考,请参阅esri.github.io/arcgis-python-api/apidoc/html/index.html。
对于 API 更新、发布说明等更多信息,请参阅developers.arcgis.com/python/guide/release-notes/。
所有关于 API 的信息的首页可以在此找到。这是一个极好的资源,包含大量文档、用户指南和 API 参考:developers.arcgis.com/python/。
验证您的 Esri 用户账户
现在我们已经在我们的机器上安装了 API,是时候讨论如何与不同的 Esri 用户账户结合使用了。正如我们之前所说的,API 是为了管理和与位于云环境中的 Web GIS 交互而创建的。为了能够使用 API 并与这个 Web 或云 GIS 交互,我们需要某种额外的 Esri 用户账户来与这个 Web GIS 建立连接。您可以将其与从您的计算机连接到 FTP 服务器或远程 Web 服务器并使用用户名和密码(或令牌)执行登录过程进行比较。此过程确保服务器和客户端之间的安全连接以及访问正确的内容。
不同的 Esri 用户账户
以下 Esri 用户账户可以通过 ArcGIS API for Python 访问 ArcGIS Online:
-
一个匿名用户账户,您可以通过 ArcGIS Online 访问,无需传递任何用户信息。这是一个快速测试一些基本功能的解决方案,但不会提供带有个性化账户的高级功能。我们将在本章的三个动手练习中的两个中介绍此选项。
-
一个 ArcGIS Online 组织账户(或 Portal for ArcGIS 账户)。这需要订阅 ArcGIS Online 或 Portal for ArcGIS(付费)。此选项提供了可能的最大功能,但在此处未介绍。
-
ArcGIS Enterprise 试用账户。此选项免费,并提供您创建地图和发布内容所需的服务积分。此试用账户仅持续 21 天,之后必须转换为付费账户才能继续使用。在后续内容中,我们将介绍如何设置试用账户。
-
免费 Esri 开发者账户。此账户是 ArcGIS 开发者计划的一部分,该计划为您提供 50 个服务积分,用于开发、测试个人应用程序,以及使用 ArcGIS Online 等其他服务。在后续内容中,我们将介绍此选项。
-
最后,您可以选择创建一个公共 ArcGIS 账户,并使用网页浏览器登录 ArcGIS Online。使用这些登录详情,您现在可以使用 API 连接到 ArcGIS Online,但功能有限。此选项是在 API 的 1.3 版本中添加的,此处不做介绍。
总结前面提到的内容,我们介绍了多个不同的用户账户,以便使用 API 访问 ArcGIS Online。个性化账户比匿名账户提供了更多功能。我们将在本章后面的练习中使用这两种类型。现在让我们看看 API 是如何组织成不同的模块以及它们提供哪些功能。
ArcGIS API for Python 的不同模块
就像其他 Python 库一样,API 包含 Python 模块、类、函数和类型,可用于管理和处理 ArcGIS 平台信息模型中的元素。由于 API 针对不同需要自己独特工具的用户群体,因此 API 已组织成 13 个不同的模块。在此处不需要介绍所有模块,但本章最重要的模块如下所述:
-
GIS 模块:这是最重要的模块,是访问托管在 ArcGIS Online 或 ArcGIS Portal 中的 GIS 的入口点。GIS 模块允许您在 GIS 中管理用户、组和内容。在此上下文中,GIS 指的是创建、可视化和共享地图、场景、应用程序、图层、分析和数据的协作环境。
-
特征模块:此模块代表 API 的矢量数据部分。矢量数据通过此模块表示为特征数据、特征图层或特征图层的集合。单个数据元素由特征对象表示,而
FeatureSet、FeatureLayer和FeatureCollection等类则代表特征数据的不同分组。 -
栅格模块:此模块包含用于处理栅格数据和影像图层的类和栅格分析函数。而特征模块代表 API 的矢量数据组件,栅格模块则是栅格数据组件。此模块使用
Imagerylayer类来显示影像服务的数据,并提供实时图像处理的栅格函数。可以使用地图小部件来可视化影像图层。 -
地理处理模块:此模块是导入具有地理处理功能但不是 API 部分而是通过 ArcGIS Online 可用的工具箱所必需的。这些地理处理工具箱被导入为原生 Python 模块,这样您就可以调用导入模块中可用的函数来调用这些工具。API 本身还包括一个丰富的地理处理工具集,这些工具通过定义空间数据类型的其他模块提供。
地理处理工具是一个在 GIS 数据上执行操作的函数,从输入数据集开始。然后对该数据集执行操作,最后将操作的结果作为输出数据集返回。
- 小部件模块:它提供了可视化 GIS 数据和分析的组件,包括 MapView Jupyter Notebook 小部件。我们将使用这个小部件来可视化地图和图层。这不是唯一的可视化模块——独立的映射模块提供了不同的映射层和 2D/3D 映射及可视化组件。
如您所见,API 为不同的任务和用户提供了广泛的模块,从发布映射数据、执行地理空间分析和数据处理。所有模块都使用 Python 作为脚本语言来管理 GIS 数据和功能。现在让我们开始使用 API,在继续进行更高级的任务之前探索一些基本功能。
练习 1 – 导入 API 并使用地图小部件
现在是时候开始使用 API 了。按照说明,在 Jupyter Notebook 应用程序中打开一个新的笔记本,您可以在其中访问 API。输入并运行以下代码。我们将首先导入 API,以便我们可以使用其模块、函数和类:
In: import arcgis
In: from arcgis.gis import GIS
代码的第二行可以分解如下——arcgis.gis 指的是 arcgis 模块中的一个子模块(gis)。被导入的 (GIS) 是一个 GIS 对象,它包括用于显示地理位置、可视化 GIS 内容以及分析结果的地图小部件。接下来,我们将通过将相同名称的变量分配给它来创建一个 GIS 对象,但拼写为小写:
In: gis = GIS()
这是一个匿名登录的示例,因为我们没有在 GIS() 的括号中传递任何登录详情。现在我们将通过创建一个地图对象并将其分配给一个变量来使用地图小部件,然后可以通过查询该变量在笔记本中打开小部件:
In: map1 = gis.map('San Francisco')
map1
注意,您必须在新的一行上重复并运行变量名,才能显示地图。在您的 Jupyter Notebook 应用程序中,将打开一个地图窗口,显示旧金山市的 2D 彩色地图:

您可以通过 zoom 属性和传递一个整数来调整地图的缩放级别:
In: map1.zoom = 5
没有缩放级别值的地图显示会给你一个默认的缩放值 2。更大的值会显示更小的区域,并使用更大的比例尺来显示更多细节。这个地图是 ArcGIS Online 提供的几个基础地图之一,用作映射数据的背景。我们可以查询当前显示的是哪种基础地图:
In: map1.basemap
Out: 'topo'
你可能想知道如何访问所有对象属性。这可以通过输入对象名称,然后按点号并按下Tab键来完成。然后,将显示一个包含所有可用属性的下拉列表的窗口:

我们可以在之前提到的屏幕截图中看到basemap属性被列出。有关给定属性的更多信息,请选择你选择的属性后跟一个问号。然后,一个信息窗口将在屏幕底部打开,显示更多信息:
In: map1.basemaps?
basemaps属性也可以直接查询,并返回一个包括每个值的新行的列表对象:
In: map1.basemaps
Out: 'dark-gray',
'dark-gray-vector',
'gray', ...
我们可以通过更改基础地图,通过在基础地图属性(注意单数形式)中传递一个可用的选项来实现使用这个窗口中的信息,如下所示:
In: map1.basemap = 'satellite'
map1
我们可以看到,我们的基础地图现在显示了旧金山的卫星图像:

接下来,您将收到一封确认邮件,该邮件将发送到您在线输入的电子邮件地址。邮件中包含一个您需要点击以激活账户的 URL。之后,您可以设置账户并选择用户名和密码。这个用户名和密码可以用作通过 ArcGIS API for Python 登录 ArcGIS Online。您还将被分配一个账户 URL 路径,类似于http://firstname-lastname.maps.arcgis.com。此 URL 路径也是使用 ArcGIS API for Python 登录 ArcGIS Online 所必需的。
接下来,我们将解释如何创建一个公开的 ArcGIS 账户。导航到www.arcgis.com并点击橙色免费试用->按钮:

接下来,您将被引导到一个新页面,页面上有一个需要填写您个人信息的表单。填写完毕后,您就可以创建一个 ArcGIS Online 账户。用户名和密码可以用作 ArcGIS Online 的登录详情。
练习 2 – 搜索、显示和描述地理空间内容
在以下练习中,我们将搜索在线内容,将其添加到我们的地图中,并描述数据。最后,我们将直接将你的地图小部件保存到 ArcGIS Online 中你个人内容文件夹内的一个网络地图中。这是 API 1.3 版本的新功能,它使得创建网络地图变得非常容易。我们将使用的内容是一个包含加利福尼亚州索诺马县自行车路径的要素层文件。此内容可通过 ArcGIS Online 获取。我们可以使用 API 来搜索内容、引用它并将其添加到我们的 Jupyter Notebook 应用程序中的地图小部件中。
首先,我们将使用个人账户登录 ArcGIS Online。阅读代码并按照以下说明操作:
In: import arcgis
In: from arcgis.gis import GIS
gis = GIS()
在之前的代码中,你需要在第三行大写GIS后面的括号内输入你自己的个人详细信息,从个人 URL、用户名和密码开始。如果你已经创建了一个免费的 ArcGIS 开发者账户,这将看起来像gis = GIS(“https://firstname-lastname.maps.arcgis.com”, “username”, “password”)。如果你为 ArcGIS Online 注册了试用期间,第一个 URL 将是www.arcgis.com,然后是你的用户名和密码。
接下来,我们将打开我们感兴趣的区域——索诺马县地图:
In: map = gis.map("Solano County, USA")
map.zoom = 10
map
要搜索我们自身组织之外的具体内容,请使用以下包含特定搜索词的查询代码。在这里,我们使用了旧金山及其周边地区的路径:
In: search_result = gis.content.search(query="san francisco trail",
item_type="Feature Layer", outside_org=True)
search_result
在之前的代码中,我们使用 GIS 对象的内容属性来搜索内容。使用个性化账户,我们指定我们想要搜索我们自身组织之外的数据。我们的查询正在寻找类型为"Feature Layer"的旧金山附近的路径。接下来,通过重复变量名返回结果。在这种情况下,输出看起来像以下列表,但可能对读者来说有所不同。为了简洁,这里只显示了前三个搜索结果:
Out: <Item title:"National Park Service - Park Unit Boundaries"
type:Feature
Layer Collection owner:imrgis_nps>,
<Item title:"National Park Service - Park Unit Centroids"
type:Feature Layer
Collection owner:imrgis_nps>,
<Item title:"JUBA_HistoricTrail_ln" type:Feature Layer Collection
owner:bweldon@nps.gov_nps>,…
项目以列表形式返回,每个项目包括其标题、类型和所有者名称。我们还可以使用 Jupyter Notebook 应用程序以不同的方式显示此项目列表:
In: from IPython.display import display
for item in search_result:
display(item)
现在,我们的搜索结果返回了一个缩略图图片、标题和描述。标题也是一个超链接,可以带你去一个 ArcGIS Online 网页,在那里你可以使用查看器显示内容并查看元数据。我们感兴趣的是以下项目,它在一个要素集合中显示了索诺马县的自行车路径。这是一个包含要素层和表的集合,这意味着我们必须找到一种方法来访问正确的要素层并将其添加到我们的地图小部件中:
![图片
现在,我们想要在地图上显示来自这个要素集合的自行车路径数据。为此,我们需要在我们的代码中引用数据源。我们可以这样做:
In: bike_trails_item = search_result[8]
bike_trails_item.layers
代码如下,第一行创建一个变量,该变量引用搜索结果列表中的项目,其中包含自行车道服务图层。接下来,我们将使用此项目的 layers 属性来查看项目包含多少个图层,在这种情况下有两个图层,索引为0和1。
接下来,我们想要两个图层的名称。使用for循环,我们可以print图层名称:
In: for lyr in bike_trails_item.layers:
print(lyr.properties.name)
Out: BikeTrails
Parks
我们可以看到自行车道存储在第一个图层中。接下来,我们将在服务图层中引用此图层,通过将其分配给name bike_trails_layer变量。我们还将通过重复我们新创建的变量来打印要素层 URL:
In: bike_trails_layer = bike_trails_item.layers[0]
In: bike_trails_layer
使用pandas数据框,我们可以可视化图层附带的属性表:
In: bike_df = bike_trails_layer.query().df
In: bike_df.head()
使用head()函数限制输出到前五行,我们得到以下输出:

现在我们可以将图层添加到地图中并查看结果:
In: map.add_layer(bike_trails_layer)
map
自行车道将在地图小部件中的底图上显示。如果您看不到车道,您可能需要放大缩小几次,以及将地图向右平移,直到您看到以下结果,显示底图上的不同自行车道作为线段:

我们现在将从这个地图小部件创建自己的网络地图。这是 API 的一个新功能,它对于在 ArcGIS Online 中创建和托管自己的网络地图非常强大。这不是创建网络地图的唯一方法,但它作为如何使用 API 来完成此操作的示例。通过导入WebMap类,我们可以通过一个名为wm的变量创建此类的一个实例,该变量将存储我们的网络地图。使用save函数,我们可以简单地将其作为网络地图保存到我们自己的组织内容文件夹中:
In: from arcgis.mapping import WebMap
wm = WebMap()
web_map_properties = {'title':'Bike Trails ',
'snippet':'This map service shows bike trails in Solano County',
'tags':'ArcGIS Python API'}
web_map_item = wm.save(item_properties=web_map_properties)
web_map_item
在提供我们的登录凭据以访问我们的 ArcGIS Online 组织页面后,Python 会返回我们可以通过点击下划线蓝色 URL 立即在线访问的项目:费曼学习法的灵感源于诺贝尔物理奖获得者理查德·费曼。

我们可以在概览标签中编辑元数据,同时我们可以在设置标签中删除它(向下滚动以查看标记为红色的此选项)。
返回到我们的 Jupyter Notebook,我们可以使用 Python 显示来自服务 URL 的信息,返回以 JSON 格式显示的输出,这里只显示前三个结果:
In: bike_trails_layer.properties
通过使用for循环,我们可以显示字段名称:
In: for field in bike_trails_layer.properties['fields']:
print(field['name'])
Out: OBJECTID
SHAPESTLen
STRNAME
...
您还可以访问要素层的单个属性,例如extent:
In: bike_trails_layer.properties.extent
Out: {
"xmin": 6485543.788552672,
"ymin": 1777984.1018305123,
"xmax": 6634421.269668501,
"ymax": 1958537.218413841,
"spatialReference": {
"wkid": 102642,
"latestWkid": 2226
}
}
这完成了第二个练习,我们学习了如何搜索内容,将其添加到我们的地图小部件中,并描述我们正在处理的数据。现在我们将查看栅格模块,看看我们如何可视化和处理栅格影像。
练习 3 – 使用栅格数据和 API 的地理处理函数
对于这个练习,我们将查看光栅模块。我们将以 Landsat 8 卫星图像的形式处理光栅数据,并查看描述数据和使用来自 ArcGIS Online 的地理处理功能的方法。像往常一样,我们将从导入arcgis包和gis模块开始,这次使用匿名登录:
In: import arcgis
from arcgis.gis import GIS
from IPython.display import display
gis = GIS()
接下来,我们将搜索内容——我们将指定我们正在寻找的是图像图层,这是用于光栅模块的图像数据类型。我们将结果限制在最多2个:
In: items = gis.content.search("Landsat 8 Views", item_type="Imagery
Layer",
max_items=2)
接下来,我们将按以下方式显示项目:
In: for item in items:
display(item)
输出显示了以下两个项目:

我们对结果中的第一个项目感兴趣。我们可以这样引用它:
In: l8_view = items[0]
现在,让我们通过点击蓝色的 Landsat 8 视图 URL 来更深入地研究这个项目。它将您带到包含数据集描述的网页。查看波段编号及其描述。API 提供了此 landsat 层上的可用光栅函数,我们将在下一分钟讨论。首先,我们将通过项目的图层属性访问图像层:
In: l8_view.layers
接下来,我们可以按以下方式引用和可视化图层:
In: l8_lyr = l8_view.layers[0]
l8_lyr
输出显示了覆盖整个地球的图层:

当图像图层作为 Python 对象可用时,我们可以打印出所有可用的属性,如之前返回的,以 JSON 格式:
In: l8_lyr.properties
可以使用以下代码打印出更具视觉吸引力的图像图层项目描述:
In: from IPython.display import HTML
In: HTML(l8_lyr.properties.description)
使用pandas数据框,我们可以更详细地探索不同的波长波段:
In: import pandas as pd
In: pd.DataFrame(l8_lyr.key_properties()['BandProperties'])
输出现在以pandas数据框对象的形式呈现:

现在我们将进入光栅函数部分。API 提供了一组用于图像图层的光栅函数,这些函数在服务器端渲染并返回给用户。为了最小化输出,用户需要指定一个位置或区域,其屏幕范围将用作光栅函数的输入。光栅函数也可以链接在一起,并在大型数据集上工作。以下for循环显示了可用于此特定数据集的所有可用光栅函数:
In: for fn in l8_lyr.properties.rasterFunctionInfos:
print(fn['name'])
输出显示了所有可用的光栅函数,每个函数占一行:

这些信息也存在于我们之前打印出的完整属性列表中。接下来,我们将在显示西班牙马德里地区的地图上尝试一些这些光栅函数。我们首先创建地图的一个实例:
In: map = gis.map('Madrid, Spain')
然后,我们将我们的卫星图像添加到地图小部件中,我们可以使用它进行各种光栅函数:
In: map.add_layer(l8_lyr)
map
输出将看起来像这样:

我们现在需要从光栅模块导入apply函数,以便应用光栅函数:
In: from arcgis.raster.functions import apply
首先,我们将使用波段 4、3 和 2 创建一个自然色图像,并进行动态范围调整:
In: natural_color = apply(l8_lyr, 'Natural Color with DRA')
按照以下方式更新地图,看看它与之前有何不同:
In: map.add_layer(natural_color)
输出将看起来像这样:

我们将重复这个程序,但这次我们将可视化农业地图:
In: agric = apply(l8_lyr, 'Agriculture')
In: map.add_layer(agric)
这个栅格函数使用了 6、5 和 2 波段,分别代表短波红外-1、近红外和蓝色。我们可以看到,我们的研究区域显示了以下三个类别——茂盛的植被是鲜绿色,受压的植被是暗绿色,裸露区域是棕色。我们可以在我们的地图小部件中验证这些结果:

如你所见,栅格模块能够快速在云中处理栅格影像,并快速返回和显示结果在你的屏幕上。这只是模块的许多栅格功能之一,还有更多值得探索。这结束了这个练习,我们探讨了如何搜索栅格影像,显示它,以及如何使用 ArcGIS API for Python 的栅格模块来利用 ArcGIS Online 的地理处理功能。
摘要
本章介绍了全新的 ArcGIS API for Python,它是基于 Python 3.5 构建的。你学习了如何使用 API、Jupyter Notebooks 以及存储在基于云的 ArcGIS Online 系统中的数据处理。我们涵盖了 API 如何组织成不同的模块,如何安装 API,如何使用地图小部件,如何使用不同的用户账户登录 ArcGIS Online,以及如何处理矢量和栅格数据。使用一些 API 模块,我们学习了如何使用 Python 的 API 进行基本的地理空间分析,并创建 ArcGIS Online 网络地图。
下一章将介绍用于与云数据交互的 Python 工具,用于搜索和快速数据处理。特别是,它侧重于使用基于 AWS 云基础设施的 Elasticsearch 和 MapD GPU 数据库。读者将学习创建云服务以进行地理空间搜索、地理定位数据处理、地理定位数据,以及如何使用 Python 库与这些服务交互。
第十章:使用 GPU 数据库进行地理处理
随着多核 GPU 的出现,新的数据库技术被开发出来以利用这项改进的技术。MapD 是一家位于旧金山的初创公司,是这些公司的一个例子。他们的基于 GPU 的数据库技术在 2017 年开源,并可在云服务上使用,例如 Amazon Web Services (AWS) 和 Microsoft Azure。通过结合 GPU 的并行化潜力与关系数据库,MapD 数据库提高了基于数据的数据库查询和可视化的速度。
MapD 创建了一个 Python 3 模块 pymapd,允许用户连接到数据库并自动化查询。这个 Python 绑定使得地理空间专业人士能够将 GPU 数据库的速度集成到现有的地理空间架构中,从而提高分析和查询的速度。MapD 的两个核心产品(开源社区版本和商业企业版本)都由 pymapd 支持。
除了 Python 模块之外,MapD 还将地理空间功能添加到他们的数据库技术中。现在支持点、线和多边形的存储,以及提供距离和包含功能的地理分析引擎。此外,MapD 开发了一个可视化组件 Immerse,允许快速构建分析仪表板,数据库作为后端。
在本章中,我们将涵盖以下主题:
-
在云中创建 GPU 数据库
-
使用 Immerse 和 SQL 编辑器探索数据可视化
-
使用
pymapd将空间和表格数据加载到数据库中 -
使用
pymapd查询数据库 -
将云数据库集成到 GIS 架构中
云地理数据库解决方案
云存储地理空间数据已成为许多 GIS 架构的常见部分。无论它是作为本地解决方案的备份、替代本地解决方案,还是与本地解决方案结合以提供基于内部网络的系统互联网支持,云都是 GIS 未来的一部分。
使用 ArcGIS Online、CARTO、MapBox 以及现在的 MapD,支持地理空间数据的云数据存储选项比以往任何时候都要多。每个平台都提供可视化组件和不同类型的数据存储,并且将以不同的方式与您的数据和软件集成。
ArcGIS Online 虽然也提供独立选项(即直接数据上传),但与 ArcGIS Enterprise(以前称为 ArcGIS Server)集成,以使用存储在本地地理数据库上的企业 REpresentational State Transfer (REST) 网络服务。ArcGIS Online 基于 Amazon Web Services (AWS) 构建,所有的服务器架构都隐藏在用户视线之外。企业集成需要高级别的许可(成本),这包括大量的云令牌(即信用额度),并且云账户内的存储和分析可以使用这些令牌。
CARTO 提供云 PostGIS 存储,允许上传地理空间数据文件。随着 Python 包 CARTOframes(在第十四章中介绍,云地理数据库分析和可视化)的发布,云数据集可以通过脚本上传和更新。使用 Python,CARTO 账户可以成为企业解决方案的一部分,该解决方案维护最新的数据集,同时允许它们通过构建应用程序快速部署为自定义网络地图。CARTO 提供两个付费账户级别,具有不同的存储级别。
MapBox 专注于为移动应用创建自定义底图的地图工具,但它也提供数据集的云数据存储和地图创建工具,如 MapBox GL,这是一个基于Web 图形库(WebGL)的 JavaScript 库。使用新的 MapBox GL—Jupyter 模块,数据可以通过 Python 访问。
MapD 虽然提供了与所提及的类似解决方案,但在许多方面都有所不同。它有一个开源的数据库版本(MapD Core Community Edition),可以在本地或云上使用,并为大型客户提供企业版。虽然 MapD Core 具有关系数据库模式并使用 SQL 进行查询,就像传统的 RDBMS 一样,但它使用 GPU 来加速查询。MapD Core 可以部署在 AWS、Google Cloud Platform 和 Microsoft Azure 上。MapD 也可以安装在没有 GPU 的服务器上,但这会降低其相对于其他地理数据库的有效速度提升。
所有地理数据库都支持 Jupyter Notebook 环境进行数据查询,但 MapD 将它们集成到 Immerse 可视化平台内的 SQL EDITOR 中。当使用pymapd上传数据时,MapD 使用 Apache Arrow,同时也支持INSERT语句,并允许使用 Immerse 数据导入器(包括 SHPs、GeoJSONs 和 CSVs)加载数据。
大数据处理
对于数据科学分析和地理空间分析,遇到大数据的情况比以往任何时候都更常见。MapD 在检索行和向客户端返回数据时速度极快,这使得它对于驱动实时数据库或对大型数据集进行查询非常有用。
与基于 CPU 的数据库相比,MapD 在处理大型数据集方面提供了惊人的加速。由于每个 GPU 卡都包含大量的核心,并行进程可以运行得更快。这意味着数十亿条数据集可以在毫秒内进行查询和分析。
MapD 架构
MapD 的架构是 MapD Core(基于 GPU 的数据库)、MapD Immerse(数据可视化组件)以及其他支持数据科学操作和地理空间应用的相关技术和 API 的组合:

借助快速的查询速度和 API,以及 pymapd,组件可以一起或单独使用来创建地理数据库和可视化。存在多个数据导入器的驱动程序,以帮助数据迁移,而 Thrift API 可以提供数据用于导出或与软件包和 Immerse 进行通信。
云与本地与结合
由于许多不同类型的组织依赖于地理数据库,架构的选择也相当多样。虽然一些组织已经将所有数据迁移到云中,将数据和工具存储在不同的服务器上,但大多数组织仍然维护一个本地地理数据库作为企业系统。
第三种架构风格,在基于云和本地地理数据库之间取得平衡,也非常受欢迎。这允许数据库备份由始终可用的云服务支持,并且数据服务可以超出组织防火墙的局限,同时限制暴露给互联网的数据集和服务。
这些解决方案之间的平衡取决于对处理速度和存储成本的需求。MapD 可以本地安装和维护,也可以在云中托管,满足各种组织需求。查询和数据处理的速度允许以与本地存储数据集相同的方式使用云数据资源。使用 pymapd,数据集可以轻松地在云中镜像,同时本地维护,并且可以通过比较本地存储的数据与基于云的数据来集成到地理空间分析中。
您组织选择的技术结构将取决于您的需求和产生以及从其他来源摄取的数据集的大小。MapD 可以成为这个结构的一部分,也可以是整个 GIS,支持在本地、云或两者都位于时的快速 Spatial SQL 查询。
在云中创建 MapD 实例
为了探索使用 MapD Core 和 MapD Immerse 的混合本地和云 GIS 的可能性,让我们在云中创建一个实例(虚拟服务器)。这个云数据库将本地访问,使用 pymapd 执行查询和数据管理任务。
使用 AWS,我们可以创建一个支持 GPU 的服务器。虽然我在这里使用 AWS,但 MapD 可以加载到其他云服务中,例如 Google Cloud 和 Microsoft Azure,也可以本地安装。这些其他云服务也提供社区版。
查找 AMI
我将在 p2.xlarge AWS 实例上使用 MapD 社区版,这是平台的开源版本。社区版的预构建 Amazon Machine Images(AMIs)可用。虽然核心数据库技术是免费的,但 p2 实例仍会有与之相关的成本,并且不包含在 AWS 免费层中。我选择了 p2.xlarge 而不是推荐的 p2.8xlarge,将每小时成本从 7 美元降低到 1 美元。对于低成本或免费评估软件,请下载并在虚拟机或专用 Linux 服务器上安装它:

对于本地安装,从本网站下载社区版(编译版和源代码):www.mapd.com/community/.
开通 AWS 账户
创建数据库实例需要 AWS 账户。请访问 aws.amazon.com 并注册一个账户。此账户需要一个与账户关联的信用卡或借记卡。
在此处探索安装 MapD AWS AMI 的官方文档:
www.mapd.com/docs/latest/getting-started/get-started-aws-ami/.
创建密钥对
生成密钥对将允许您使用安全外壳或 SSH 连接远程访问或远程连接 AWS 实例。要从 EC2 控制台生成密钥对,请在向下滚动后从左侧面板的“网络 & 安全”组中选择密钥对:

给密钥对命名并点击创建以将私钥(以 .pem 扩展名)保存在您计算机或 U 盘上的安全位置。每次您使用 SSH 连接到实例时都需要此密钥。相应的公钥(以 .pub 扩展名)保存在您的 AWS 账户中,并在连接到实例时与私钥匹配。
启动实例
账户设置完成后,从 AWS 管理控制台转到 EC2。在 EC2 控制台中,选择启动实例以打开 AWS 实例选择工具:

选择版本
在左侧面板点击 AWS Marketplace 后,在市场中搜索 MapD 数据库。在搜索框中输入 MapD 会显示两个版本。我选择了 MapD 核心数据库社区版,因为 MapD 软件是免费提供的:

通过点击选择按钮选择感兴趣的版本,并转到实例类型菜单。
搜索实例
在可用的实例类型中,只有少数受支持。这些 p2 实例提供不同级别的 CPU、内存和 GPU。我出于成本考虑选择了 p2.xlarge 实例,尽管 p2.8xlarge 更适合生产级计算:

选择实例类型后,有一些菜单描述了实例的详细信息,并允许在 AWS 生态系统中进行备份存储。根据您组织的需要设置这些参数。
设置安全组
安全组设置很重要,因为它们控制谁可以访问实例,以及他们可以从哪里访问。源选项卡允许您设置可以连接到实例的机器,使用 IP 地址来确定谁被允许连接:

为了安全起见,调整 SSH 的源为我的 IP。这可以在以后更新,以允许从任何地方连接,即整个互联网。一旦完成,将现有的密钥对分配给实例,以确保它可以用于直接连接到命令行 MapD Core。
Immerse 环境
实例设置完成后,可以使用浏览器访问已安装的 Immerse 环境。在 Immerse 环境中,可以导入数据,创建仪表板,并执行 SQL 查询:

登录 Immerse
在 EC2 仪表板中,确保 MapD 实例已启动。复制实例的 IP 地址(公共 IP 地址,不是私有 IP)和实例 ID,这些信息位于 EC2 仪表板中的实例列表下方。确保 MapD 实例被高亮显示,以确保实例 ID 正确:

打开浏览器,在 URL 栏中输入公共 IP 地址,以及端口号8443。以下是一个 URL 示例:https://ec2-54-200-213-68.us-west-2.compute.amazonaws.com:8443/。
确保您使用超文本传输安全协议(HTTPS)进行连接,并且包含端口号。如果浏览器警告您连接不安全,请点击页面底部的“高级”链接。一旦建立连接,登录页面将打开,用户和数据库已预先填写。将实例 ID 作为密码并点击“连接”:

阅读 MapD 条件,点击“我同意”,并进入 Immerse 环境。
在此处了解更多关于在 AWS 上使用 MapD 的信息:www.mapd.com/docs/latest/getting-started/get-started-aws-ami/。
默认仪表板
一旦启动 Immerse 环境,探索内置的默认 DASHBOARDS,以了解可能实现的功能:

纽约出租车数据集
纽约出租车行程仪表板使用包含 1300 万行数据点的数据库表来展示数据库的速度。每次地图缩放时,数据库都会重新查询,并在毫秒内重新生成点。探索数据和修改仪表板以包含其他图表和地图类型非常有趣:

导入 CSV 文件
使用内置的数据导入器,导入 CSV 格式的数据集非常简单。转到 DATA MANAGER 并选择“导入数据”。在下一页上,点击“添加文件”按钮,通过拖放加载包含 Juneau 市地址的 CSV 数据集。
数据将被加载,一旦加载完成,将根据上传的数据生成一个 MapD 数据库表。检查数据,添加新的名称或接受默认名称(由电子表格名称生成)。点击“保存表”后,将生成数据库表:

创建图表
现在已经将数据集添加到数据库中,通过选择 DASHBOARDS 选项卡来测试 MapD Immerse。在这里,可以创建并添加到新仪表板中的动态图表、表格、直方图、热图等。在这个例子中,使用从 CSV 文件中加载的数据创建了一个简单的饼图。统计与城市名称相关的记录数量并将其添加到图表中:

使用 SQL 编辑器进行选择
使用内置的 SQL 编辑器,可以执行 SQL 语句。结果将出现在类似 Jupyter Notebook 的交互式表格中的 SQL 编辑器中:

SQL 语句执行得非常快,并将包括可以执行 SQL 选择语句分析的 spatial SQL 命令。
使用地理空间数据
MapD Core 支持几何和地理数据类型,并且可以使用坐标列生成交互式地图,以 x/y 或经纬度对显示数据。点地图、热图和分色地图可以轻松地使用 Immerse 仪表板环境生成和样式化:

这份数据可视化是通过将 OpenAddresses 中的 Calaveras 县地址 CSV 文件加载到我的 MapD Immerse 实例中,使用 DATA MANAGER,然后利用 LON 和 LAT 列创建热图来制作的。
使用终端连接到数据库
使用基于 Java 的集成终端或另一个终端解决方案连接到数据库。由于我的本地机器使用 Windows,并且没有集成到操作系统中的终端,我已经下载并安装了 PuTTY。这款免费的 SSH 软件允许我使用之前生成的密钥对从 Windows 机器连接到 Linux 命令行服务器。
如果您正在使用 Windows 的其他终端解决方案或使用其他操作系统,请使用终端的正确 SSH 程序连接到实例。步骤将类似,但需要转换所需的私钥格式。
在此处下载 PuTTY 终端:www.chiark.greenend.org.uk/~sgtatham/putty/
PuTTYgen
要授权任何连接到 AWS 实例,必须使用为 AWS 账户生成的私钥,并使用相关的程序 PuTTYgen 将其转换为 PuTTY 密钥格式。从开始菜单打开 PuTTYgen,然后点击“转换”菜单。从下拉选项卡中选择“导入密钥”。
将打开一个文件对话框,允许您选择从 AWS 下载的私钥。此私钥将具有.pem扩展名:

点击“打开”,密钥将被导入。要生成 PuTTY 格式的私钥,提供可选的密钥短语(一个或几个进一步识别用户并必须记住的词),然后在“操作”部分点击“保存私钥”按钮。选择一个文件夹并保存密钥,现在它将具有.ppk文件扩展名。
连接配置
使用 PuTTY 连接到实例需要一些配置。要创建连接,将实例的公网 IP 地址粘贴到“主机名”字段中,确保端口设置为 22,连接类型为 SSH。通过点击“保存”按钮在“已保存会话”部分保存设置:

使用私钥
一旦设置加载,点击左侧的 SSH 下拉菜单。在新菜单中,点击 Auth 切换到新菜单,然后浏览到我们转换成 PuTTY 格式的私钥:

一旦找到密钥,请按“打开”以建立连接。要启动服务器上的 MapD,请转到/raidStorage/prod/mapd/bin文件夹并运行以下代码,将{Instance-ID}替换为您的实例 ID:
./mapdql mapd -u mapd -p {Instance-ID}
如果您在建立连接时遇到问题,请检查 AWS 实例的安全组是否设置为允许从当前使用的计算机进行连接。如果安全组设置是“我的 IP”且计算机的 IP 地址不同,则无法建立连接。
安装 pymapd
安装pymapd很简单,并且由conda和pip(Python 中包含的包安装程序)支持。我在本章中使用pip,但使用conda不会引起任何问题,并且可能建议与其他conda支持的软件集成。
conda 安装命令
使用conda install -c conda-forge连接到conda forge,这是存储pymapd模块的存储库。有关conda的更多信息,请参阅第一章,包安装和管理:
conda install -c conda-forge pymapd
pip 安装命令
pymapd模块也可以通过pip(Python 安装程序包)使用。它从PyPi.org(Python 基金会的存储库)获取:
pip install pymapd
一旦运行安装命令,pymapd轮子将下载并安装,以及所需的辅助模块:

通过打开 Python 终端(或 IDLE)并输入 import pymapd 来测试模块是否已安装。如果没有错误发生,则 pymapd 已成功安装。
另一个选项是从 GitHub 下载 pymapd:github.com/mapd/pymapd.
创建连接
pymapd 模块包含一个名为 connect 的类,该类需要连接信息,如用户名、密码、主机服务器 IP/域名和数据库名(用户名和数据库名的默认值均为 mapd)。对于 AWS 实例,MapD Core 和 MapD Immerse 的默认密码是实例 ID,可在 EC2 仪表板中的实例信息部分找到,如前所述。
用户名和密码
如果你正在连接到 AWS AMI MapD 实例,请使用公共 IP 地址作为 host,并将实例 ID 作为 password。以下是一个 connection 模式示例:
from pymapd import connect
connection = connect(user="mapd", password= "{password}",
host="{my.host.com}", dbname="mapd")
cursor = connection.cursor()
下面是一个已填充的 connect 实例化的示例:
connection = connect(user="mapd", password= "i-0ed5ey62se2w8eed3",
host="ec2-54-212-133-87.us-west-2.compute.amazonaws.com", dbname="mapd")
数据游标
要执行 SQL 命令(空间或其他),我们将创建一个数据 cursor。cursor 是 connection 类的一部分,将用于使用 execute 命令执行语句。它还用于访问查询结果,这些结果被转换为列表并使用 for 循环迭代:
from pymapd import connect
connection = connect(user="mapd", password= "{password}",
host="{my.host.com}", dbname="mapd")
cursor = connection.cursor()
sql_statement = """SELECT name FROM county;"""
cursor.execute(sql_statement)
results = list(cursor)
for result in results:
print(result[0])
结果是一个包含元组的列表,其中(在这种情况下)只包含 county 的名称,通过使用零索引从元组中获取它。
创建一个表格
建立连接后,我们现在可以在 Python 脚本中执行 SQL 语句,这将生成 MapD Core 实例中的表格。以下语句将创建一个简单的名为 county 的表格,包含 MULTIPOLYGON 几何类型、一个 integer id 字段和三个 VARCHAR 类型字段(或字符串,如 Python 中的称呼):
from pymapd import connect
connection = connect(user="mapd", password= "{password}",
host="{my.host.com}", dbname="mapd")
cursor = connection.cursor()
create = """CREATE TABLE county ( id integer NOT NULL,
name VARCHAR(50), statefips VARCHAR(3),
stpostal VARCHAR(3), geom MULTIPOLYGON );
"""
cursor.execute(create)
connection.commit()
下一个代码块将创建一个名为 address 的表格,包含 POINT 几何类型、一个 integer id 字段和一个名为 address 的 VARCHAR 类型字段:
from pymapd import connect
connection = connect(user="mapd", password= "{password}",
host="{my.host.com}", dbname="mapd")
cursor = connection.cursor()
create = """CREATE TABLE address ( id integer NOT NULL PRIMARY KEY,
address VARCHAR(50), geom Point );
"""
cursor.execute(create)
connection.commit()
插入语句
向数据库添加数据的一种方法是通过使用 SQL INSERT 语句。这些语句将在上一节中创建的数据库表中生成数据行。使用 pyshp 模块,我们可以读取 shapefile 并将其包含的数据添加到 INSERT 语句模板中。然后,该语句由 cursor 执行:
from pymapd import connect
import shapefile
connection = connect(user="mapd", password= "{password}",
host="{my.host.com}", dbname="mapd")
import shapefile
import pygeoif
cursor = connection.cursor()
insert = """INSERT INTO county
VALUES ({cid},'{name}','12','FL','{geom}');
"""
countyfile = r'FloridaCounties.shp'
county_shapefile = shapefile.Reader(countyfile)
county_shapes = county_shapefile.shapes()
county_records = county_shapefile.records()
for count, record in enumerate(county_records):
name = record[3]
county_geo = county_shapes[count]
gshape = pygeoif.Polygon(pygeoif.geometry.as_shape(county_geo))
geom = gshape.wkt
insert_statement = insert.format(name=name, geom=geom,cid=count+1)
cursor.execute(insert_statement)
这个过程可能很耗时,因此还有其他几种向数据库添加数据的方法。
使用 Apache Arrow 加载数据
使用 pyarrow 模块和 pandas,数据可以写入 MapD Core 数据库:
import pyarrow as pa
import pandas as pd
from pymapd import connect
import shapefile
connection = connect(user="mapd", password= "{password}",
host="{my.host.com}", dbname="mapd")
cursor = connection.cursor()
create = """CREATE TABLE juneau_addresses (
LON FLOAT, LAT FLOAT,
NUMBER VARCHAR(30),STREET VARCHAR(200) );
"""
cursor.execute(create)
df = pd.read_csv('city_of_juneau.csv')
table = pa.Table.from_pandas(df)
print(table)
connection.load_table_arrow("juneau_addresses", table)
包含查询
此代码将测试对county数据库表的数据查询速度,使用ST_Contains,一个空间 SQL 点在多边形分析工具。county表的几何列(称为geom)是ST_Contains的第一个输入,第二个输入是已知文本(WKT)point。一旦执行 SQL 语句,point将与表中的所有行进行比较,以找出county几何中是否包含由 WKT point描述的点:
import pymapd
from pymapd import connect
connection = connect(user="mapd", password= "{password}",
host="{my.host.com}", dbname="mapd")
import time
point = "POINT(-80.896146 27.438610)"
cursor = connection.cursor()
print(time.time())
sql_statement = """SELECT name FROM county where ST_Contains(geom,'{0}');""".format(point)
cursor.execute(sql_statement)
print(time.time())
result = list(cursor)
print(result)
print(time.time())
此脚本的输出结果如下:

地理空间查询运行得非常快,正如您可以从打印的时间戳(以秒为单位)中看到的那样。只需几毫秒就能找到 Okeechobee 多边形包含point位置。
其他可用的空间 SQL 命令
在 MapD Core 数据库中可用的空间 SQL 命令的数量一直在增长。这些包括:
-
ST_Transform(用于坐标系转换) -
ST_Distance(用于距离分析) -
ST_Point(用于生成point对象) -
ST_XMin、ST_XMax、ST_YMin、ST_YMax(用于边界框访问)
每天都在添加更多功能,并将在今年晚些时候达到与 PostGIS 和其他空间数据库的空间 SQL 功能对等。有了这些 SQL 命令,以及独特的 MapD Immerse 前端仪表板发布工具,MapD 是地理数据库部署的一个强大新选择。
摘要
使用像 MapD Core 这样的基于云的 GPU 数据库和 Immerse 可视化工作室,在设计实现 GIS 时将带来回报。它为表格和空间查询提供速度和云可靠性,并允许数据在交互式仪表板(依赖于D3.js和 MapBox GL JavaScript 等 JavaScript 技术)中共享,这些仪表板易于创建和发布。
使用 MapD Python 模块,pymapd,云数据可以成为查询引擎的集成部分。数据可以被推送到云端或下载到本地使用。利用 GPU 并行化的力量,分析可以快速进行。在云中的虚拟服务器上安装 MapD,甚至本地安装,以测试软件的潜力是值得的。
在下一章中,我们将探讨使用 Flask、SQLAlchemy 和 GeoAlchemy2 创建一个具有 PostGIS 地理数据库后端的交互式网络地图。
第十一章:Flask 和 GeoAlchemy2
Python 一直拥有强大的互联网功能。标准库包括 HTTP 处理、STMP 消息和 URL 请求的模型。已经编写了数千个第三方模块来扩展或改进内置的 Web 功能。随着时间的推移,一些模块聚集成了 Python 网络框架——编写来管理复杂和动态网站创建和维护的代码库。
为了更好地理解如何使用 Python 网络框架以及如何添加地理空间功能,我们将实现 Flask 模型-视图-控制器(MVC)框架。一个纯 Python 网络框架,Flask 可以与 SQLAlchemy、GeoAlchemy2 和 Jinja2 HTML 模板系统结合使用,以创建具有地理空间功能的网页。
在本章中,你将了解:
-
Flask 网络框架
-
SQLAlchemy 数据库管理
-
GeoAlchemy2
-
使用对象关系映射(ORM)连接到 PostGIS
-
Jinja2 网页模板系统
Flask 及其组件模块
与 Django 和 GeoDjango(在第十二章中介绍,GeoDjango)不同,Flask 不包含电池。相反,它允许根据需要安装多个支持模块。这给了程序员更多的自由,但也使得必须单独安装所需的组件。
我为这一章选择了一些模块,这些模块将使我们能够创建一个具有地理空间组件的 Flask 应用程序。以下章节将详细介绍如何设置、安装和利用这些模块来生成网站,使用具有 PostGIS 数据库后端的演示网站(在第七章中介绍,使用地理数据库进行地理处理)以及通过基于 Web 的界面执行空间查询的能力。
设置
必须安装一些重要的 Python 模块,以确保 Flask 应用程序及其与 PostgreSQL 和 PostGIS 数据库组件的连接能够按预期运行。这些模块将通过pip下载和安装,pip连接到Python 包索引(PyPI),一个位于pypi.python.org/pypi的在线注册模块仓库。
这些模块包括:
-
Flask,一个纯 Python MVC 网络框架(
flask.pocoo.org/)。 -
Flask-SQLAlchemy,一个可以连接到多种数据库后端的数据库 ORM 模块(
flask-sqlalchemy.pocoo.org/2.3/)。此模块安装 SQLAlchemy。 -
GeoAlchemy2,一个基于 SQLAlchemy 模块和 Postgres/PostGIS 后端(在第七章中介绍,使用地理数据库进行地理处理)的 Python 模块,用于允许地理空间数据列和 ORM 空间查询(
geoalchemy-2.readthedocs.io/en/latest/)。 -
Flask-WTForms,一个基于 WTForms (
wtforms.readthedocs.io/en/latest/) 的 Web 表单模块,允许 Flask 承载每个网页的逻辑并处理输入 (flask-wtf.readthedocs.io/en/stable/)。 -
SQLAlchemy-Utils,用于管理数据库的创建和删除 (
github.com/kvesteri/sqlalchemy-utils/)。 -
psycopg2,用于创建与 PostgreSQL 数据库的连接,并由 SQLAlchemy 模块使用 (initd.org/psycopg/)。 -
pyshapefile(或pyshp),用于读取本例中使用的 shapefiles 并将其添加到数据库表中 (pypi.python.org/pypi/pyshp)。 -
最后,
pygeoif(pypi.python.org/pypi/pygeoif) 用于允许将 shapefile 二进制编码的数据转换为 已知文本(WKT)编码,以便将几何数据插入到数据库中。
其他重要的支持模块会自动与 Flask 及前面的模块一起安装,包括 Jinja2 模板系统 (jinja.pocoo.org/) 和 Werkzeug Web 服务器网关接口(WSGI)模块 (werkzeug.pocoo.org/)。
使用 pip 安装模块
如果你机器上安装了多个版本的 Python,并且没有使用带有 virtualenv 模块的虚拟环境,确保使用命令行调用的是 Python 3 版本的 pip,可以使用 pip -V 选项来检查:
C:\Python36\Scripts>pip -V
pip 9.0.1 from c:\python36\lib\site-packages (python 3.6)
一旦确认命令行中调用的是正确的 pip,就可以安装模块。让我们逐一查看所需的 pip 命令和一些每个命令预期生成的输出示例。
使用 pip 安装 Flask
首先,安装 Flask 模块本身。使用 pip 命令 pip install flask:
C:\Python36\Scripts>pip install flask
pip 将在 PyPI 上找到 Flask 及其所需的依赖项,然后运行包含的 setup.py 指令(或等效指令)来安装模块:

通过 pip 安装 Flask-SQLAlchemy
使用命令 pip install flask-sqlalchemy 安装 flask-sqlalchemy 轮子及其所需的依赖项:
C:\Python36\Scripts>pip install flask-sqlalchemy
安装命令将在 PyPI 上找到 flask-sqlalchemy 轮子文件(pip 安装模块时使用的预构建文件类型),并运行安装过程:

使用 pip 安装 GeoAlchemy2
使用命令 pip install GeoAlchemy2 从 PyPI 调用模块,下载轮子文件,并将其安装到 Python 安装的 Lib/site-packages 文件夹中:
C:\Python36\Scripts>pip install GeoAlchemy2
使用 pip 安装 Flask-WTForms 和 WTForms
使用 WTForms 模块和 Flask-WTF 接口,我们可以创建使网页交互的 Web 表单。使用 pip install flask-wtf 命令安装它:
C:\Python36\Scripts>pip install flask-wtf
使用 pip 安装 psycopg2
pscycopg2 是一个用于连接 PostgreSQL 数据库的 Python 模块。如果尚未安装(见 第七章,使用地理数据库进行地理处理),请使用 pip install psycopg2 命令安装它:
C:\Python36\Scripts>pip install psycopg2
使用 pip 安装 SQLAlchemy-Utils
这些实用工具允许快速创建数据库:
C:\Python36\Scripts>pip install sqlalchemy-utils
使用 pip 安装 pyshapefile(或 pyshp)
pyshapefile 模块可以读取和写入形状文件:
C:\Python36\Scripts>pip install pyshp
使用 pip 安装 pygeoif
pygeoif 模块允许进行地理空间数据格式转换:
C:\Python36\Scripts>pip install pygeoif
编写 Flask 应用程序
为了探索 Flask 和 GeoAlchemy2 的基础知识,我们将构建一个 Flask 网络应用程序,并在本地使用包含的 Web 服务器进行测试和部署。此 Web 应用程序允许用户查找位于全国各地的不同场馆所属的县、州和国会选区。此应用程序将涉及从美国地质调查局(USGS)数据目录下载形状文件,并将包含执行地理空间查询的视图(处理 Web 请求的 Python 函数)使用 GeoAlchemy2 ORM 和使用 SQLAlchemy ORM 进行表关系搜索。
此应用程序需要使用两个脚本创建数据库和数据库表。随着我们进一步进行,这些脚本将详细介绍,并位于书籍代码包的 Chapter11 文件夹中。最终产品将是一个使用基于 ORM 的空间查询和关系查询的 Leaflet JavaScript 地图显示结果的 Web 应用程序。
从数据源下载数据
为了开始此项目,让我们从美国地质调查局数据目录下载数据。此项目将使用四个基于美国的形状文件——NBA 场馆形状文件、州形状文件、国会选区形状文件和县形状文件。
美国地质调查局(USGS)在此处提供了大量可供下载的 USA 形状文件:www.sciencebase.gov/catalog/item/503553b3e4b0d5ec45b0db20.
县、区、州和场馆形状文件
US_County_Boundaries 数据是一个多边形形状文件,可以从美国地质调查局(USGS)数据目录的此地址获取:www.sciencebase.gov/catalog/item/4f4e4a2ee4b07f02db615738.
点击下载 zip 文件链接,如图所示。将文件解压到项目文件夹中(例如,C:\GeospatialPy3\Chapter11),以便在整个章节中访问:

Arenas_NBA 形状文件在此处可用:www.sciencebase.gov/catalog/item/4f4e4a0ae4b07f02db5fb54d.
Congressional_Districts 形状文件可在以下位置获取:www.sciencebase.gov/catalog/item/4f4e4a06e4b07f02db5f8b58。
US_States 形状文件可在以下位置获取:www.sciencebase.gov/catalog/item/4f4e4783e4b07f02db4837ce。
这些形状文件不是最新的(例如,Nets 场地仍然列在新泽西州,而不是布鲁克林),但我们在这里探索的是应用程序技术(以及它们如何处理几何数据类型),而不是数据本身,因此忽略数据的时效性。
创建数据库和数据表
要创建我们的数据库以及将存储应用程序数据的表,我们将使用 SQLAlchemy 和 GeoAlchemy2 类和方法。以下代码位于名为 Chapter11_0.py 的脚本中。此代码将使我们能够连接到 PostgreSQL 数据服务器以创建数据库和数据表,这些表将构成网络应用程序的后端。导入以下库:
from sqlalchemy import create_engine
from sqlalchemy_utils import database_exists, create_database,
drop_database
from sqlalchemy import Column, Integer, String, ForeignKey, Float
from sqlalchemy.orm import relationship
from geoalchemy2 import Geometry
from sqlalchemy.ext.declarative import declarative_base
使用 create_engine 函数和连接字符串格式连接数据库服务器以生成和查询数据表,如下所示:
conn_string = '{DBtype}://{user}:{pword}@{instancehost}:{port}/{database}'
engine = create_engine(conn_string, echo=True)
连接字符串在所有 Python 数据库模块中都会使用。它们通常包括对关系数据库管理系统(RDBMS)类型的指定、用户名、密码、实例主机(即数据库服务器安装的 IP 地址或本地机器上的 localhost)、可选的端口号和数据库名称。例如,连接字符串可能看起来像这样:
connstring = 'postgresql://postgres:bond007@localhost:5432/chapter11'
engine = create_engine(connstring, echo=True)
在本例中,postgresql 是 RDBMS 类型,postgres 是用户名,bond007 是密码,localhost 是实例主机,5432 是端口(这是 PostgreSQL 安装的默认端口;如果安装时没有更改端口,则可以将其从连接字符串中省略),chapter11 是数据库名称。echo=True 语句用于将数据库交互的日志生成到标准输出窗口。要关闭这些消息,将 echo 值更改为 False。
更详细的解释可以在以下位置找到:docs.sqlalchemy.org/en/latest/core/engines.html。
对于我们的数据库,我们可以使用以下格式。将 {user} 和 {pword}(包括括号)替换为您的 PostgreSQL 服务器用户名和密码:
conn_string ='postgresql://{user}:{pword}@localhost:5432/chapter11'
engine = create_engine(conn_string, echo=True)
如果连接字符串有效,create_engine 函数将返回一个对象到 engine 变量,该变量将用于在整个脚本中执行数据库交互。
注释中的代码(#drop_database(engine.url))被注释掉了,但如果需要使用脚本删除并重新创建数据库,可以取消注释。它调用 SQLAlchemy create_engine 的 url 属性,该属性是连接字符串的引用:
# Uncomment the line below if you need to recreate the database.
#drop_database(engine.url)
数据库及其包含的数据表是在一个依赖于database_exists函数的if not条件语句中创建的。如果条件返回True(表示数据库不存在),则将engine变量传递给create_database函数:
# Check to ensure that the database doesn't exist
# If it doesn't, create it and generate the PostGIS extention and tables
if not database_exists(engine.url):
create_database(engine.url)
将 PostGIS 扩展表添加到新数据库中
在create_database函数下方,我们需要使用engine.connect函数连接到数据库,以便直接向数据库传递 SQL 语句。这个 SQL 语句("CREATE EXTENSION postgis*"*)在新数据库中启用空间列和查询:
# Create a direct connection to the database using the engine.
# This will allow the new database to use the PostGIS extension.
conn = engine.connect()
conn.execute("commit")
try:
conn.execute("CREATE EXTENSION postgis")
except Exception as e:
print(e)
print("extension postgis already exists")
conn.close()
在这里使用try/except块是为了防止数据库已经被空间启用。检查print语句的输出以确保没有其他异常发生。
定义数据库表
在 Python MVC Web 框架的世界中,数据库表是模型。网站使用这些模型来存储数据,它们由 Python 类生成和建模。这些类从包含大部分数据库管理代码的超类中继承或继承预写的功能,让我们只需使用基本数据类型(如字符串和整数)以及高级类(如几何形状)来定义表的列。
这些由类定义的表可以在多个关系数据库管理系统(RDBMS)中生成,而无需重新编写模型代码。虽然 GeoAlchemy2 仅适用于 PostgreSQL/PostGIS 之上,但 SQLAlchemy 模型可以用于在包括 SQL Server、Oracle、Postgres、MySQL 等多种数据库中生成表。
声明式基类
对于 SQLAlchemy 数据库类,一个名为declarative_base的基类允许继承数据库方法和属性(这就是 SQLAlchemy 的超类魔法所在,处理多个 SQL 版本的数据库 SQL 语句,从而简化了写入任何关系数据库管理系统所需的代码):
# Define the model Base
Base = declarative_base()
数据库表模型类
一旦调用或实例化了基类,就可以将其传递给模型类。这些类,就像所有 Python 类一样,可以包括函数、属性和方法,这些对于在类内部处理数据非常有用。在本章中,模型类不包含任何内部函数,而是只定义列。
在这里探索 SQLAlchemy 模型及其内部函数:docs.sqlalchemy.org/en/latest/orm/tutorial.html。
表属性
在 RDBMS 数据库中生成的数据表名称将对应于模型类的 __tablename__ 属性。每个表的主键用于关系和查询,必须使用 primary_key 关键字定义。Column 类和 String、Float 以及 Integer 类型类是从 SQLAlchemy 调用的,用于定义要在底层 RDBMS 中生成的表列(从而允许程序员避免为每个主要 RDBMS 使用的各种 SQL 编写 CREATE TABLE 语句)。
例如,Arena 类将用于管理一个具有四列的表——一个 String name 字段,两个 Float 字段(longitude 和 latitude),以及一个具有 SRID 或 EPSG 空间参考系统 ID 为 4326 的 POINT 几何类型,对应于 WGS 1984 坐标系统 (spatialreference.org/ref/epsg/wgs-84/)):
# Define the Arena class, which will model the Arena database table
class Arena(Base):
__tablename__ = 'arena'
id = Column(Integer, primary_key=True)
name = Column(String)
longitude = Column(Float)
latitude = Column(Float)
geom = Column(Geometry(geometry_type='POINT', srid=4326))
与 Arena 类类似,以下类使用一个 String name 列。对于几何类型,它们也使用 SRID 4326,但它们使用 MULTIPOLYGON 几何类型来存储用于建模这些地理的复杂多边形几何。对于具有关系的表,例如 County、District 和 State 类,还有用于管理表关系和表之间查询的特殊类。
这些特殊类包括 ForeignKey 类和 relationship 函数。ForeignKey 类接受一个 id 参数,并将其传递给 Column 类,以关联子行与父行。relationship 函数允许双向查询。backref 关键字生成一个函数,该函数实例化连接表的模型实例:
# Define the County class
class County(Base):
__tablename__ = 'county'
id = Column(Integer, primary_key=True)
name = Column(String)
state_id = Column(Integer, ForeignKey('state.id'))
state_ref = relationship("State",backref='county')
geom = Column(Geometry(geometry_type='MULTIPOLYGON',srid=4326))
# Define the District class
class District(Base):
__tablename__ = 'district'
id = Column(Integer, primary_key=True)
district = Column(String)
name = Column(String)
state_id = Column(Integer, ForeignKey('state.id'))
state_ref = relationship("State",backref='district')
geom = Column(Geometry(geometry_type='MULTIPOLYGON',srid=4326))
County 类和 District 类将与 State 类建立 relationship 关系,允许会话查询调用 State 类。这种 relationship 使得查找县或国会选区所在的美国州变得容易。state_id 列建立 relationship,而 state_ref 字段引用父 State 类。对于 State 类,县和区有自己的 backref 引用,允许父 State 类访问相关的县/区:
# Define the State class
class State(Base):
__tablename__ = 'state'
id = Column(Integer, primary_key=True)
name = Column(String)
statefips = Column(String)
stpostal = Column(String)
counties = relationship('County', backref='state')
districts = relationship('District', backref='state')
geom =
Column(Geometry(geometry_type='MULTIPOLYGON',srid=4326))
创建表
实际生成表时,可以使用两种方法。表模型类有一个内部的 __table__ 方法,该方法有一个 create 函数,可以用来单独创建每个表。还有一个 drop 函数可以用来删除表。
在脚本中,我们使用 try/except 块来生成表。如果发生异常(即,如果表已存在),则删除表并重新创建。以下是一个示例的 State 表创建语句:
# Generate the State table from the State class.
# If it already exists, drop it and regenerate it
try:
State.__table__.create(engine)
except:
State.__table__.drop(engine)
State.__table__.create(engine)
或者,可以使用定义的类通过 Base 方法的 metadata 和其 create_all 函数生成所有数据库表:
Base.metadata.create_all(engine)
将数据插入新的数据表中
一旦创建了数据库并在数据库中定义和创建了数据库表,就可以添加数据。第二个脚本Chapter11_1.py将用于查找和读取下载的 shapefile 中的数据,并使用for循环读取数据并将其写入相应的数据库表。将使用 SQLAlchemy 会话管理器来查询和提交数据到表中。
导入所需的模块
对于需要处理和导入的数据,将使用几个新的模块。pyshapefile模块(或pyshp,作为 shapefile 导入)用于连接到 shapefile 并读取它们包含的几何和属性数据。pygeoif模块是一个纯 Python 模块,实现了名为geo_interface的协议。
此协议允许对地理空间数据进行 Python 对象级别的自省,例如,它将地理空间数据格式转换为 Python 对象。它将被用来将存储在二进制中的 shapefile 几何形状转换为可以插入数据库的 WKT 几何形状,使用 GeoAlchemy2 ORM:
# The pyshapefile module is used to read shapefiles and
# the pygeoif module is used to convert between geometry types
import shapefile
import pygeoif
关于geo_interface协议的更多讨论,请参阅以下链接:gist.github.com/sgillies/2217756。
要连接到数据库和表,导入 SQLAlchemy ORM 和其他 SQLAlchemy 函数:
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, ForeignKey, Float
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import relationship
要将数据添加到数据库表的几何列中,将使用 GeoAlchemy2 的Geometry数据类型:
# The Geometry columns from GeoAlchemy2 extend the SQLAlchemy ORM
from geoalchemy2 import Geometry
要使脚本能够找到下载的 shapefile,使用Tkinter模块及其filedialog方法,因为它内置在 Python 中,且与操作系统无关:
# The built-in Tkinter GUI module allows for file dialogs
from tkinter import filedialog
from tkinter import Tk
将再次使用 SQLAlchemy 的create_engine函数创建数据库连接。本节还使用会话管理器生成一个session,并将其绑定到连接数据库的engine变量:
# Connect to the database called chapter11 using SQLAlchemy functions
conn_string = 'postgresql://postgres:password@localhost/chapter11'
engine = create_engine(conn_string)
Session = sessionmaker(bind=engine)
session = Session()
session将允许对这些表进行查询和提交(即写入数据库)进行管理。我们将在for循环内部查询数据库表,以创建县、区和州之间的数据库关系。
数据库表模型再次在脚本中定义,从declarative_base类派生。这些类定义将与上一个脚本中的定义相匹配。
定位和读取 shapefile
要创建允许用户搜索和定位 shapefile 的文件对话框,Tkinter 的Tk类被实例化并分配给变量root。Tk类创建了一个不必要的迷你控制台窗口,因此使用root.withdraw方法将其隐藏:
# Initiate the Tkinter module and withdraw the console it generates
root = Tk()
root.withdraw()
文件对话框是通过filedialog.askopenfilename方法生成的。该方法接受多个参数,包括文件对话框窗口的title、初始目录以及在使用文件对话框时应可见的文件扩展名。以下是一个示例代码,用于选择竞技场 Shapefile对话框:
# Navigate to the Arena shapefile using the Tkinter file dialog
root.arenafile = filedialog.askopenfilename(initialdir = "/",
title = "Select Arena Shapefile",
filetypes = (("shapefiles","*.shp"),
("all files", "*.*")))
在脚本中,这会为每个下载的 shapefile 重复进行。在文件对话框使用后,每个找到的 shapefile 都将传递一个字符串类型的文件路径给root变量,并且文件路径将保存在一个属性中。
访问 shapefile 数据
要访问 shapefile 中的数据,通过将相应的文件路径属性传递给Reader类来调用pyshp的Reader类。实例化的类将具有records和shapes方法,分别允许访问 shapefile 的属性数据和几何数据:
# Read the Arena shapefile using the Reader class of the pyshp module
import shapefile
arena_shapefile = shapefile.Reader(root.arenafile)
arena_shapes = arena_shapefile.shapes()
arena_records = arena_shapefile.records()
一旦数据被读取并分配给可迭代变量,它们可以使用for循环进行迭代。因为使用pyshp的Reader records方法访问的数据与使用shapes方法访问的数据相对应,所以使用enumerate函数生成的循环计数器用于匹配当前记录和由shapes方法生成的几何数据列表中相应的几何数据索引。
对于Arena形状文件的几何形状,Reader的shapes方法返回一个包含坐标对的列表。由于Arena类的几何列是POINT数据类型,可以使用POINT(X Y) WKT 模板将数据写入数据库表。根据 GeoAlchemy2 扩展 WKT(EWKT)的要求,SRID(4326)包含在字符串的开头。
在这里了解更多关于 GeoAlcheym2 ORM 的信息:geoalchemy-2.readthedocs.io/en/0.4/orm_tutorial.html.
在每次循环中,都会实例化一个新的Arena类并将其分配给变量arena。name字段从位于索引6的Reader record数据项中提取出来,并分配给arena变量,同时几何数据从arena_shapes数据项的count(即当前循环号)中提取出来,并分配给Arena列的arena.longitude和arena.latitude。
这些坐标随后传递给字符串format方法以格式化 EWKT 模板,并分配给arena.geom属性。一旦为arena行分配了数据,它就使用session.add添加到会话中。最后,使用会话的commit方法将数据写入数据库:
# Iterate through the Arena data read from the shapefile
for count, record in enumerate(arena_records):
arena = Arena()
arena.name = record[6]
print(arena.name)
point = arena_shapes[count].points[0]
arena.longitude = point[0]
arena.latitude = point[1]
arena.geom = 'SRID=4326;POINT({0} {1})'.format(point[0],
point[1])
session.add(arena)
session.commit()
对于State类(以及County和District类),使用索引从属性数据中提取名称、联邦信息处理标准(FIPS)代码和邮政编码缩写。pygeoif用于首先将几何数据转换为pygeoif MultiPolygon格式,然后转换为 WKT,并将其传递给字符串模板,写入geom字段作为 EWKT:
# Iterate through the State data read from the shapefile
for count, record in enumerate(state_records):
state = State()
state.name = record[1]
state.statefips = record[0]
state.stpostal = record[2]
state_geo = state_shapes[count]
gshape =
pygeoif.MultiPolygon(pygeoif.geometry.as_shape(state_geo))
state.geom = 'SRID=4326;{0}'.format(gshape.wkt)
session.add(state)
if count % 10 == 0:
session.commit()
session.commit()
由于州几何数据量较大,它们每10次循环提交到数据库。最后的commit捕获任何剩余的数据。
使用查询
对于“地区”和“县”数据表,增加了一个最后的细节,即通过 FIPS 代码查询新添加的“州”数据以找到相关的州。通过使用session.query查询“州”类,并使用filter_by方法(将地区记录中的 FIPS 代码作为“过滤”参数传递)过滤州的数据,然后指定使用“第一个”结果,可以调用正确的“州”。使用变量的id字段来填充地区的state_id列以创建“关系”:
# This uses the STFIPS data to query the State table and find the state
for count, record in enumerate(district_records):
district = District()
district.district = record[0]
district.name = record[1]
state = session.query(State).filter_by(statefips=record[4]).first()
district.state_id = state.id
dist_geo = district_shapes[count]
gshape=pygeoif.MultiPolygon(pygeoif.geometry.as_shape(dist_geo))
district.geom = 'SRID=4326;{0}'.format(gshape.wkt)
session.add(district)
if count % 50 == 0:
session.commit()
session.commit()
“县”表同样被循环遍历,并包括一个“州”查询。查看脚本以查看完整的代码。一旦所有数据都已写入数据表,就“关闭”会话并“处理”连接引擎:
session.close()
engine.dispose()
Flask 应用程序的组件
现在后台数据库和表已经创建并加载数据,表之间的关系也已经建模和生成,是时候编写创建 Flask 应用程序的脚本了。这些脚本将包含处理 Web 请求、查询数据库并返回 HTTP 响应的视图、模型和表单。
该网络应用程序被称为“竞技场应用程序”,因为它在下拉列表中列出存储在“竞技场”表中的所有 NBA“竞技场”,并允许用户在地图上显示位置,同时显示包含关于“竞技场”信息的“弹出窗口”,这些信息来自空间查询和表关系。
网络开发的 MVC 方法允许将网络应用程序的必要组件分开。这些组件包括数据库模型(前面描述的 SQLAlchemy 模型)、接受应用程序输入的网络表单以及一个路由请求的控制对象。组件的分离反映在单独的脚本中。使每个组件独立,可以更容易地调整而不会影响应用程序的其他组件。
数据库模型将包含在一个名为models.py的脚本中,以及所需的模块导入。网络表单(创建网页组件如下拉列表和输入字段的 Python 类)将包含在一个名为forms.py的脚本中。所有视图,包括 URL 端点和处理这些 URL 的 Web 请求,都将包含在一个名为views.py的脚本中。
控制器是从Flask类生成的对象,并分配给名为app的变量。每个 Web 应用程序的 URL 端点都使用app.route定义,并有一个相关的 Python 函数(视图),其中包含处理 Web 请求并返回 HTTP 响应的逻辑。控制器用于将 Web 请求路由到正确的 URL 端点,并且可以区分GET和POST HTTP请求。它在views.py脚本中创建。
使用 HTML 模板来展示 Web 请求的处理结果。通过使用 Jinja2 模板系统,网页表单中的数据将被传递到 HTML 模板中,并以完整的网页形式发送回请求的 Web 浏览器。该应用的模板包含指向 JavaScript 库的链接,包括Leaflet,这使得网页能够在网页内展示地图。
文件夹结构和控制器对象
为了包含应用程序的独立组件,建议使用特定的文件夹结构。这将允许组件在需要时相互引用,同时仍然保持独立性。对组件某一部分的调整不应需要分离组件的重构(至少尽可能少)。
Arena 应用程序包含在一个名为arenaapp的文件夹中:

在arenaapp文件夹中有一个名为app.py的脚本和一个名为application的文件夹:

app.py脚本从application导入app控制器对象,并调用app.run方法来启动 Web 应用程序:
from application import app
app.run()
通过添加 Python 的__init__.py脚本,使得application文件夹可导入,并允许app访问组件脚本中的代码成为可能。这个特殊的脚本会告诉 Python 可执行文件该文件夹是一个模块:

在__init__.py文件中,定义并配置了app对象。app对象包含一个配置字典,允许 Web 应用程序连接到后端('SQLALCHEMY_DATABASE_URI')并执行会话管理和加密。虽然我们在脚本中包含了配置设置,但请注意,较大的应用程序会将配置设置分离到单独的config.py脚本中:
import flask
app = flask.Flask(__name__)
conn_string = 'postgresql://postgres:password@localhost:5432/chapter11'
app.config['SQLALCHEMY_DATABASE_URI'] = conn_string
app.config['SECRET_KEY'] = "SECRET_KEY"
app.config['DEBUG'] = True
import application.views
为了便于调试应用程序,已将DEBUG配置设置为True。在生产环境中将其设置为False。将'SECRET KEY'替换为您的自己的密钥。
在这里了解更多关于配置 Flask Web 应用程序的信息:flask.pocoo.org/docs/latest/config/。
模型
对于 Arena 应用程序,一个名为models.py的脚本包含了将用于应用程序的模型。如前所述,这些模型是包含数据库列定义的 Python 类,并且可以包含内部函数以处理数据。我们的简化模型仅使用 SQLAlchemy 和 GeoAlchemy2 类定义数据列。
要连接到数据库,导入app对象。这使得应用程序配置变量,包括app.config['SQLALCHEMY_DATABASE_URI'](存储数据库连接字符串),对 SQLAlchemy 的create_engine函数可用:
from application import app
# The database connections and session management are managed with SQLAlchemy functions
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, ForeignKey, Float
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import relationship
from geoalchemy2 import Geometry
engine = create_engine(app.config['SQLALCHEMY_DATABASE_URI'])
Session = sessionmaker(bind=engine)
session = Session()
Base = declarative_base()
为了简洁起见,我在这里省略了模型类定义的详细说明,因为它们之前已经解释过了。请查找它们在 arenaapp/application 文件夹的 models.py 脚本中。
表单
网络表单用于在 Web 应用中接受用户数据并将其发送到服务器进行验证和处理。为了生成所需的表单(例如下拉列表、输入字段,甚至隐藏其内容的密码字段),我们使用了 Flask-WTF 模块和 WTForms 模块。这些模块包含创建表单组件并确保输入到其中的数据对该字段有效的类。
对于我们的简单应用,只创建了一个表单。ArenaForm 表单继承自 FlaskFor 类,并包含一个名为 description 的属性和字段 selections。这个字段是一个 SelectField,它将在网页上创建一个下拉列表。它需要一个描述字符串,并使用关键字 choices 生成下拉列表中可用的选项列表。由于下拉列表的成员将在视图中动态生成(如下所述),因此在这里传递给 choices 关键字的是一个空列表:
from flask_wtf import FlaskForm
from wtforms import SelectField
class ArenaForm(FlaskForm):
description = "Use the dropdown to select an arena."
selections = SelectField('Select an Arena',choices=[])
其他字段类,如 TextField、BooleanField、StringField、FloatField、PasswordField 以及许多其他类,都可用于 WTForms 以实现复杂 Web 应用。此外,由于它们是 Python 对象,因此可以在运行时更新表单以包含其他数据属性,正如我们将在进一步操作中看到的那样。
视图
Flask 视图是 Python 函数,当与 app 控制器对象及其 app.route URL 定义配对时,允许我们编写 Python 代码来接受网络请求、处理它并返回响应。它们是 Web 应用的核心,使得将网页及其表单连接到数据库及其表成为可能。
为了创建视图,我们将导入所有应用程序组件以及许多 Flask 函数。表单和模型从各自的脚本中导入,就像 app 对象一样:
from application import app
from flask import render_template,jsonify, redirect, url_for, request
from .forms import *
from .models import *
对于 Arena 应用,我们定义了两个视图,创建了两个应用程序 URL 端点。第一个视图 home 仅用于将请求重定向到 IP 地址根。使用 Flask 函数 redirect 和 url_for,发送到 root 地址的任何网络请求都将重定向到 arenas 视图:
@app.route('/', methods=["GET"])
def home():
return redirect(url_for('arenas'))
第二个视图 arenas 更复杂。它接受 GET 和 POST 请求方法。根据 request 方法,处理和返回的数据将不同,尽管它们都依赖于存储在 application/templates 文件夹(所有 Flask HTML 模板都存储在这里)中的模板 index.html。以下是完整的视图:
@app.route('/arenas', methods=["GET","POST"])
def arenas():
form = ArenaForm(request.form)
arenas = session.query(Arena).all()
form.selections.choices = [(arena.id,
arena.name) for arena in arenas]
form.popup = "Select an Arena"
form.latitude = 38.89517
form.longitude = -77.03682
if request.method == "POST":
arena_id = form.selections.data
arena = session.query(Arena).get(arena_id)
form.longitude = round(arena.longitude,4)
form.latitude = round(arena.latitude,4)
county=session.query(County).filter(
County.geom.ST_Contains(arena.geom)).first()
if county != None:
district=session.query(District).filter(
District.geom.ST_Intersects(arena.geom)).first()
state = county.state_ref
form.popup = """The {0} is located at {4}, {5}, which is in
{1} County, {3}, and in {3} Congressional District
{2}.""".format(arena.name,county.name, district.district,
state.name,
form.longitude, form.latitude)
else:
form.popup = """The county, district, and state could
not be located using point in polygon analysis"""
return render_template('index.html',form=form)
return render_template('index.html',form=form)
视图剖析
视图的 URL 是 http://{localhost}/arenas。 使用一个特殊的 Python 对象,称为 装饰器(例如 @app.route*),我们可以将我们想要使用的 URL 与将接受和处理请求处理的函数连接起来。函数和 URL 不需要具有相同的名称,尽管它们通常是这样的:
@app.route('/arenas', methods=["GET","POST"])
def arenas():
使用表单
在装饰器和函数声明之下,调用了来自 forms.py 的 ArenaForm,并将 request.form 函数作为参数传递。这为 ArenaForm 添加了功能,并允许它根据需要访问请求的自身参数。
一旦将 ArenaForm 对象传递给变量 form,就可以用数据填充它。这些数据将来自对 Arena 模型的 SQLAlchemy 会话 query。此查询请求 Arena 表的所有数据行,并使用 all 方法(而不是 filter_by 方法,后者会限制返回的行数)将其传递给变量 arenas。
由于 ArenaForm 的 selections 字段目前为空,我们将使用列表推导来遍历名为 arenas 的列表中包含的 arena 对象,将它们的 id 和 name 字段添加到列表内部的元组中。这填充了下拉列表,并确保列表中的每个选择项都有一个值(id)和一个标签(name):
form = ArenaForm(request.form)
arenas = session.query(Arena).all()
form.selections.choices = [(arena.id,
arena.name) for arena in arenas]
form.popup = "Select an Arena"
form.latitude = 38.89517
form.longitude = -77.03682
在填充选择项选项后,表单中添加了三个新属性——popup、latitude 和 longitude。最初,这些只是占位符,并不来自 arena 数据。然而,一旦网络应用程序运行,并且用户从下拉列表中选择 arenas,这些占位符值将被来自 arenas 表和其他表的查询得到的数据所替换。
评估请求方法
下一条是使用 request.method 属性的 if 条件语句,以查看 HTTP 请求方法是否为 POST:
if request.method == "POST":
由于对 arenas URL 的初始请求是一个 GET 请求,代码最初评估 if 条件为 False,跳过视图底部缩进的代码部分,以返回模板 index.html 和现在已填充的 form:
return render_template('index.html',form=form)
此函数使用 render_template 函数返回名为 index.html 的模板,并将填充的 ArenaForm 变量 form 传递到模板中,使得 Jinja2 模板系统能够生成完整的网页并发送到请求的网页浏览器。模板中所有双括号变量都填充了来自 form 的对应数据(例如,选择项被添加到下拉列表中)。
POST 请求
如果用户从列表中选择一个 arena 并点击查找数据按钮,HTML 表单会向视图发出 POST 请求。当 if 条件解析为 True 时,视图通过生成 arena 位置坐标对和自定义 popup 来处理请求,而不是使用默认的坐标对和 popup 值:
if request.method == "POST":
arena_id = form.selections.data
arena = session.query(Arena).get(arena_id)
form.longitude = round(arena.longitude,4)
form.latitude = round(arena.latitude,4)
属性 form.selections.data 用于检索从列表中选择的 arena 的 id,并将其传递给一个名为 arena_id 的变量。然后,使用 SQLAlchemy ORM 的 get 方法通过此 id 查询数据库。查询返回的 arena 对象的数据字段可以填充 form.longitude 和 form.latitude 字段。
空间查询
要找到县和国会选区,使用了两种 PostGIS 空间分析技术——ST_Contains 和 ST_Intersects。第一个查询确定 arena 是否包含在县内;如果不是,结果为空(或 Python 中的 None):
county=session.query(County).filter(
County.geom.ST_Contains(arena.geom)).first()
if county != None:
district=session.query(District).filter(
District.geom.ST_Intersects(arena.geom)).first()
虽然 ST_Contains 可以用于两个查询,但我想要展示 GeoAlchemy2 ORM 允许在 Geometry 列中使用所有 PostGIS 函数。这些搜索结合了 SQLAlchemy 的 filter 方法与 GeoAlchemy2 ORM,使其能够根据空间分析返回查询结果。
关系查询
如果 county 查询成功,则执行 district 查询,然后使用关系属性 (state_ref) 来找到 county 所在的 state:
state = county.state_ref
在 County、District 和 State 模型定义中建立的双向关系使得这一点成为可能。这个 state 对象是 State 模型类的一个成员,可以用来检索 state 的 name。
要创建自定义 popup,使用字符串模板格式化来填充 popup,以包含描述请求的 arena 的具体信息。结果被分配给变量 form.popup。
最后,填充的 form 再次传递给 index.html 模板,但这次它包含有关所选 arena 的数据:
return render_template('index.html',form=form)
这里是应用程序查询结果截图,针对的是 The Oracle Arena:

网络地图模板
在 index.html 模板中,通过双括号变量访问 form 数据。这些变量可以位于 JavaScript 中或 HTML 中。在这个例子中,form.latitude 和 form.longitude 变量位于定义地图初始中心点的地图 JavaScript 中:
var themap = L.map('map').setView([{{form.latitude}}, {{form.longitude}}], 13);
要在请求的 arena 位置创建具有自定义 popup 的 marker,需要添加位置坐标和 popup 字段:
L.marker([{{form.latitude}},{{form.longitude}}]).addTo(themap)
.bindPopup("{{form.popup}}").openPopup();
要使 POST 请求成为可能,一个具有 POST 方法的 HTML 表单包含了 form.description 和 form.selection(下拉列表)属性。当按钮被按下时,HTML 表单的按钮生成 POST 请求:
<form method="post" class="form">
<h3>{{form.description}}</h3>
{{form.selections(class_='form-control',placeholder="")}}
<br>
<input type="submit" value="Find Data">
</form>
在本地运行网络应用程序
要在本地运行应用程序,我们可以使用 Python 可执行文件调用位于 arenaapp 文件夹中的 app.py 脚本。打开命令行并传递脚本参数:
C:\Python36>python C:\GeospatialPy3\Chapter11\Scripts\arenaapp\app.py
在本章中,运行此应用程序在 Web 服务器上超出了范围,但这涉及到配置一个带有 WSGI 处理器的 Web 服务器,以便允许通过 Python 可执行文件和 app.py 处理 Web 请求。对于 Apache Web 服务器,mod_wsgi 模块很受欢迎。对于使用 Internet Information Services(IIS)的 Windows 服务器,wfastcgi 模块非常有用,并且可以从 Microsoft Web 平台安装程序中获取。
在此处了解更多关于 Apache 和 mod_wsgi 模块的信息:flask.pocoo.org/docs/latest/deploying/mod_wsgi/.
对于 IIS,以下安装说明非常有用:netdot.co/2015/03/09/flask-on-iis/.
摘要
在本章中,我们学习了如何使用 Flask MVC Web 框架以及一些可用的组件模块,这些模块增加了额外的功能。这些模块包括 SQLAlchemy ORM、用于地理空间查询的 GeoAlchemy2 ORM、用于处理 Web 数据的 WTForms 以及用于创建网页模板的 Jinja2 模板系统。我们创建了数据库表,添加了数据表和表关系,并创建了一个利用地理空间和关系查询生成动态网页的 Web 应用程序。
一个有趣的挑战,基于此处审查的代码,将是探索向 Arena 应用程序添加编辑功能,允许用户在数据过时的情况下将 arenas 移动到正确的位置。探索 GeoAlchemy2 ORM 文档以获取更多高级功能。
在下一章中,我们将回顾一个类似的 MVC Web 框架 Django 及其 GeoDjango 空间组件。Django 以更多内置功能(batteries included)的哲学,以不同的方式解决了与 Web 应用程序固有的相同问题,但与 Flask 相比,模块选择自由度较低。
第十二章:GeoDjango
Django Python 网络框架于 2005 年推出,并在多年来持续得到支持和改进。一个主要改进是增加了对空间数据类型和查询的支持。这一努力产生了 GeoDjango,使得 Django 能够支持地理空间数据库模型和利用地理空间查询的网页视图。
GeoDjango 现在是标准的 Django 组件,可以通过特定的配置来激活。2017 年 12 月,Django 2 作为新的长期支持版本发布。它目前支持 Python 3.4、3.5 和 3.6。
在本章中,我们将学习以下内容:
-
Django 和 GeoDjango 的安装和配置
-
Django 管理面板功能,包括地图编辑
-
如何使用 LayerMapping 将 shapefiles 加载到数据库表中
-
GeoDjango 查询
-
Django URL 模式
-
Django 视图
安装和配置 Django 和 GeoDjango
与 Flask 相比,Django 是一个包含电池的框架。它包括允许数据库后端支持的模块,无需单独的数据库代码包(与 Flask 不同,Flask 依赖于 SQLAlchemy)。Django 还包括一个管理面板,允许通过网页界面轻松地进行数据编辑和管理。这意味着安装的模块更少,包含的代码更多,用于处理数据库交互和网页处理。
Flask 和 Django 之间有一些主要区别。Django 在结构上比 Flask 更好地将 URL 与视图和模型分离。Django 还使用 Python 类来表示数据库表,但它具有内置的数据库支持。对于地理空间数据库,不需要额外的模块。Django 还支持更广泛数据库中的几何列,尽管 PostgreSQL 和 PostGIS 使用得最为频繁。
与许多 Python 3 模块一样,Django 开发侧重于 Linux 开发环境。虽然它支持 Windows 安装,但需要在 Windows 中修改环境变量,需要机器的行政控制权限。配置需要行政级别的权限,允许 Django 访问地理空间数据抽象库(GDAL)和OGR 简单特征库。
从 Django 到 GeoDjango 的步骤
在本节中,我们将安装 Django 和 GeoDjango 配置,并添加所需的库(包括 GDAL 和 OGR),这些库将空间功能引入 Django。安装 Django 2 模块(针对 Python 3)和配置 GeoDjango 组件取决于多个步骤。这些包括:
-
使用
pip安装 Django 2 -
安装和启用空间数据库(如果尚未安装)
-
安装 GDAL/OGR/PROJ4/GEOS
-
配置 Windows 环境变量
-
生成项目
-
打开
settings.py -
将
django.contrib.gis添加到INSTALLED_APPS -
配置数据库设置以指向空间数据库
安装 Django
Django 2 托管在Python 包索引(PyPI)上,因此请使用pip进行安装。它也可以手动下载和安装。使用pip安装 Django 时,也会安装所需的依赖项pytz。Django 将从 PyPI 作为 wheel 下载并安装。
由于 Django 2 是一个最近发布的重大更新,我们必须确保pip安装正确的版本。使用以下命令,我们将安装 Django 2.0:
C:\Python36\Scripts>pip install Django==2.0
该模块将安装,以及相关的支持模块:

本章使用 Django 2.0。使用可用的最新 Django 2 版本开始项目。在此处查看 Django 2.0 文档(以及其他 Django 版本):
如果你正在使用虚拟环境,你可以为每个环境指定 Django 的特定版本。如果没有,并且你安装了多个 Python 版本,请确保使用正确的pip版本在Python 3文件夹结构中安装 Django。
安装 PostGIS 和 psycopg2
本章将使用 PostGIS。如果你在机器上没有安装 PostGIS,请参阅第七章使用地理数据库进行地理处理,因为它解释了如何将空间扩展插件安装到 PostgreSQL。同时,确保使用以下代码安装psycopg2模块:
C:\Python36\Scripts>pip install psycopg2
创建数据库
通过Chapter12_0.py脚本生成数据库表,该脚本创建一个名为chapter12的 PostgreSQL 数据库,并将空间功能添加到新数据库中。在以下连接配置中调整凭据、主机和端口(如有必要)。
使用psycopg2及其connect函数连接到数据库服务器,该函数创建一个connection类。该类有一个cursor函数,可以创建一个cursor对象,该对象能够执行 SQL 语句。本节创建本章的数据库:
import psycopg2
connection = psycopg2.connect(host='localhost', user='{user}',password='{password}', port="5432")
connection.autocommit = True
cursor = connection.cursor()
cursor.execute('CREATE DATABASE chapter12')
要使数据库支持地理空间功能,请确保已经安装了 PostGIS 空间插件。连接到新数据库,并执行以下 SQL 语句,该语句将空间功能表添加到数据库中:
import psycopg2
connection = psycopg2.connect(dbname='chapter12', host='localhost', user='{user}', password='{password}', port="5432")
cursor = connection.cursor()
connection.autocommit = True
cursor.execute('CREATE EXTENSION postgis')
connection.close()
本章的 PostGIS 数据库现已创建并启用空间功能。
GDAL/OGR
Django 内置的地理空间支持需要使用来自开源地理空间基金会(OSGeo)的代码库。GDAL 库包括 OGR,处理矢量数据和栅格数据集。它必须安装(有关使用 GDAL 进行分析的详细信息,请参阅第五章矢量数据分析和第六章栅格数据处理)。
如果尚未安装,请使用可从以下位置获取的 OSGeo4W 安装程序:trac.osgeo.org/osgeo4w/。选择适合您机器的正确安装程序。安装程序还将安装 QGIS 和 GRASS 以及其他开源地理空间程序。下载并运行安装程序,并将输出文件放置在本地驱动器上。此文件路径(例如:C:\OSGeo4w)在修改 Windows 环境变量时将非常重要。
在此处从 Django 项目文档中找到配置 GeoDjango 的 Linux 和 macOS 的安装说明:
docs.djangoproject.com/en/2.0/ref/contrib/gis/install/
修改 Windows 环境变量
在 Windows 中编辑系统路径和其他环境变量需要管理员权限。以下是编辑它们的步骤:
-
使用具有管理员权限的账户登录。
-
打开 Windows 资源管理器,在左侧窗格中右键单击 PC 图标。
-
从上下文菜单中选择属性。
-
点击“高级系统设置”。
-
在下一个菜单中,点击环境变量。
-
从系统变量中选择路径并点击编辑(或双击路径值)。
-
将
OSGeo4W文件夹中bin文件夹的文件路径(例如,C:\OSGeo4W\bin)添加到路径中:

在这个例子中,Python 3.6 文件夹也被添加到路径中,以及 Python 2.7,由于它在路径环境变量值中的位置,所以排在 Python 3.6 之后。这意味着当 Python 传递到命令行时,将运行 Python 3.6 可执行文件。
可能还需要两个其他变量:GDAL_DATA 变量和 PROJ_LIB 变量。如果已经安装了 PostGIS,它将已经创建了一个 GDAL_DATA 变量,如果没有,请点击系统变量框下方的“新建”按钮。添加变量的名称(GDAL_DATA)和变量值(例如,C:\OSGeo4W64\share\gdal)。
以相同的方式添加 PROJ_LIB 变量:

点击“确定”以保存新变量,然后再次点击“确定”以退出第一个设置对话框。关闭系统属性菜单。
创建项目和应用程序
现在 Django 已经安装,让我们创建一个项目。Django 有两个级别,由接受命令行参数的脚本管理。这两个级别是项目和应用程序。一个项目可以有多个应用程序,有时一个应用程序也有多个项目。这种组织方式允许你在相关应用程序之间重用项目级别的代码。
Django 使用一个管理文件 django-admin.py 来控制项目的创建。它安装在 Python 3 文件夹的 Scripts 文件夹中。我通常将 django-admin.py 文件复制到一个新的项目文件夹中,并在项目文件夹中工作的时候传递所需的命令行参数,但如果 Scripts 文件夹包含在路径环境变量中,它也可以从命令行调用。
为您的项目创建一个文件夹;例如 C:\Projects。将 django-admin.py 复制到 C:\Projects。
命令行参数 - startproject
命令行参数与 django-admin.py 结合使用来创建项目——startproject。要创建一个项目,请打开命令提示符并切换到之前创建的文件夹。我们将通过传递 startproject 和我们新项目的名称 (chapter12) 到 django-admin.py 来在这个文件夹中创建项目:

startproject 创建了什么?
通过传递两个参数给 django-admin.py,startproject 和 chapter12(项目的名称),创建了一个包含多个脚本和子文件夹的文件夹。外部的 (root) 文件夹被称为 chapter12,它包含一个重要的脚本 manage.py,以及一个也称为 chapter12 的文件夹,这是项目文件夹:

在项目文件夹中包含一些重要的脚本,包括 settings.py 和 urls.py:

这些文件是默认的占位符,等待我们配置我们的项目和应用程序。随着项目的进行,我们也将编辑 setting.py 和 urls.py,并使用我们项目的具体细节。第三个文件 wsgi.py 用于 Web 应用的生产部署。
使用 manage.py 创建应用程序
现在,root 文件夹、Projects 文件夹和相关脚本已经创建。在 root 文件夹中是 manage.py 文件,它用于配置和管理应用程序和项目。在本节中,我们将使用 manage.py 和命令行参数 startapp 创建一个应用程序。
使用命令提示符,切换到 root 文件夹。与 django-admin.py 不同,我们必须通过将 manage.py 作为参数传递给 Python 可执行文件来运行它。反过来,我们将 startapp 参数和应用程序的名称 arenas 传递给 manage.py。它应该看起来像这样:

manage.py 创建的内容
将 startapp arenas 命令传递给 manage.py 创建了一个名为 arenas 的文件夹。所有应用程序都创建在 root 文件夹内,紧邻项目文件夹旁边:

在文件夹内有一些自动生成的脚本,我们将在稍后进行配置和添加。还有一个名为 migrations 的文件夹,Django 使用它来存储描述对数据库进行编辑的脚本。本章将使用 admin.py、models.py 和 views.py 脚本:

配置 settings.py
在创建项目和新的应用程序后,使用 GeoDjango 的下一步是配置项目文件夹中包含的 settings.py 脚本。我们将添加有关数据库连接(用户、密码、数据库名称等)的详细信息,并调整 INSTALLED_APPS 设置。
添加新的数据库连接
使用 IDLE 或其他 IDE,从 chapter12 项目文件夹中打开 settings.py。滚动到名为 DATABASES 的变量。此变量设置为本地 SQLite 数据库,将调整为带有 PostGIS 扩展的 PostgreSQL 数据库。
这是默认设置:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
将其更改为以下内容,用你的 PostGIS 安装中的 username 和 password 替换(见 第三章,地理空间数据库简介):
DATABASES = {
'default': {
'ENGINE': 'django.contrib.gis.db.backends.postgis',
'NAME': 'chapter12',
'USER': '{username}',
'PASSWORD': '{password}',
'HOST': '127.0.0.1',
'PORT':'5432'
},
}
对于 HOST 选项,也可以使用空字符串来表示 localhost。如果 PostgreSQL 安装在不同的机器上,调整 HOST 选项为数据库服务器的 IP 地址。如果它在不同的端口上,调整 PORT 选项。
保存脚本,但不要关闭它。
添加新的已安装应用程序
在 settings.py 文件中,滚动到变量 INSTALLED_APPS。这个变量列出了用于支持我们应用程序的内置、核心应用程序。我们将向其中添加 django.contrib.gis,即内置的 Django GIS 应用程序,以及我们自己的新应用程序,竞技场。
INSTALLED_APPS 是一个列表,并且可以进行编辑。最初,INSTALLED_APPS 的样子如下:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
编辑它,使其看起来像这样:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.gis',
'arenas',
]
保存 settings.py 文件并关闭脚本。现在我们已经将我们的自定义竞技场应用和 Django 的 GIS 库添加到已安装的应用程序包管理器中,因此 GeoDjango 已经配置完成。接下来,我们将使用 manage.py 和 OGR 来读取 shapefiles 并自动生成数据模型。
创建应用程序
此应用程序将使用数据库表的几何字段执行地理空间分析。为了实现这一点,我们必须使用 shapefiles 和一个名为 LayerMapping 的内置方法创建和填充数据库表。
完成的应用程序将需要 URL 模式匹配来将 URL 与处理请求并返回响应的视图相关联。模板将用于将处理后的数据传递到浏览器。视图将被编写以处理 POST 和 GET 请求,并将重定向到其他视图。
现在,GeoDjango 已经配置完成,可以使用名为 manage.py 的 Django 项目管理脚本创建 NBA 竞技场应用程序。
manage.py
脚本manage.py执行多项任务以帮助设置和管理项目。出于测试目的,它可以创建一个本地 Web 服务器(使用runserver作为参数);它管理数据库模式迁移,从数据模型生成表(使用makemigration和migrate);它甚至内置了一个 Python 3 shell(使用shell),用于测试和其他操作:

在本节中,我们将使用manage.py创建和填充数据库表,使用形状文件作为数据和模式源。
生成数据模型
在配置 GeoDjango 后,manage.py中新增了一个可用功能,即ogrinspect,它可以自动生成具有几何列的数据表模型,这些模型可以放置在models.py中。通过使用 OGR 检查或读取形状文件数据,Django 的内置功能创建了一个 Python 类数据模型和一个字段映射字典,该字典将形状文件字段名和数据库列名之间进行映射。
对于本节,我们将使用在第十一章中下载的形状文件,Flask 和 GeoAlchemy2。它们也包含在代码包中。将四个形状文件(以及所有相关文件)复制到 arenas 应用程序文件夹中的data文件夹内:

打开命令提示符,并将目录切换到项目文件夹。将检查包含四个形状文件(Arenas_NBA.shp、US_States.shp、US_County_Boundaries.shp和Congressional_Districts.shp)的data文件夹,使用manage.py生成数据模型。结果将复制到models.py中。从这些模型中,将生成数据库表,然后使用字段映射字典来填充这些表:
C:\Projects\chapter12>python manage.py ogrinspect arenas\data\Arenas_NBA.shp Arenas --srid=4326 --mapping
此命令将生成一个具有几何列和4326 SRID 的数据模型。由--mapping选项生成的字段映射字典是一个 Python 字典,它将键(数据模型列名)和值(形状文件字段名)之间进行映射。这是输出的一部分:

将输出(包括import行、数据模型和字段映射字典)复制到arenas文件夹下的models.py中。将import行覆盖在数据模型类定义上的自动生成的import行。
当在命令提示符默认设置中开启快速编辑选项时,从命令行复制操作变得简单。一旦开启,可以通过拖动鼠标选择文本。当文本块被选中后,按Enter键。
多边形
对于具有多边形几何类型的其他三个形状文件,我们将传递参数—multi到manage.py和ogrinspect。使用此选项在数据模型中生成一个MultiPolygon几何列。
此命令从美国州形状文件生成数据模型:
C:\Projects\chapter12>python manage.py ogrinspect arenas\data\US_States.shp US_States \
--srid=4326 --mapping --multi
输出将看起来像这样:
# This is an auto-generated Django model module created by ogrinspect.
from django.contrib.gis.db import models
class US_States(models.Model):
stfips = models.CharField(max_length=2)
state = models.CharField(max_length=66)
stpostal = models.CharField(max_length=2)
version = models.CharField(max_length=2)
dotregion = models.IntegerField()
shape_leng = models.FloatField()
shape_area = models.FloatField()
geom = models.MultiPolygonField(srid=4326)
# Auto-generated `LayerMapping` dictionary for US_States model
us_states_mapping = {
'stfips': 'STFIPS',
'state': 'STATE',
'stpostal': 'STPOSTAL',
'version': 'VERSION',
'dotregion': 'DotRegion',
'shape_leng': 'Shape_Leng',
'shape_area': 'Shape_Area',
'geom': 'MULTIPOLYGON',
}
将输出复制到models.py中,包括数据模型和字段映射字典。通过调整manage.py的参数(即 shapefile 名称和表名称)重复此过程,并在模型添加完毕后保存models.py。
数据库迁移
Django 使用数据库迁移的概念来记录和执行对数据库的更改。这些更改包括表创建和模式变更。现在我们已经生成了数据模型,我们需要迁移数据库,这涉及到检查models.py中的更改,计算生成数据库变更的 SQL 语法,然后运行所需的迁移以使数据库表列与models.py代码定义匹配。这些迁移也可以回滚。
makemigrations
要开始迁移,将makemigrations传递给manage.py。此参数将通过检查models.py的内容来启动迁移过程。所有 Python 类数据模型将被读取,并生成相应的 SQL:
C:\Projects\chapter12>python manage.py makemigrations
Migrations for 'arenas':
arenas\migrations\0001_initial.py
- Create model Arenas
- Create model Counties
- Create model Districts
- Create model US_States
A new script has been generated and added to the migrations folder. This initial database migration script creates a Migration class and includes a number of migration operations using the CreateModel method. Each of these migrations creates operations will generate a new (empty) table in the chapter12 database. Migration classes also have methods for performing table alterations, when you need to add or remove fields.
sqlmigrate
使用sqlmigrate命令查看从makemigration操作生成的 SQL 语句。将sqlmigrate、应用程序标签(arenas)和迁移名称(0001)传递给manage.py以生成输出:

所有数据模型都已转换为 SQL,并自动添加了主键和字段长度的定义。
migrate
使用生成的迁移脚本,我们最终可以执行数据库迁移。此操作将在settings.py中指定的数据库内生成表。
将migrate参数传递给manage.py:
C:\Projects\chapter12>python manage.py migrate
操作的结果应该如下所示:

数据库表已在数据库中创建。打开 pgAdmin4(或另一个数据库 GUI 工具)检查数据库中的表,或打开 psql 并使用命令行界面。
探索 Django 文档,了解django-admin.py和manage.py的所有可用参数:
LayerMapping
要填充由 shapefile 创建的数据库表,Django 有一个内置的概念称为 LayerMapping。通过使用 manage.py 生成的字段映射字典,以及 django.contrib.gis.utils 中的 LayerMapping 类,可以从 shapefile 中提取数据并将其加载到数据库表中。要实例化一个 LayerMapping 对象,我们将数据模型、相关的字段映射和 shapefile 的位置传递给该类。
创建一个名为 load.py 的新文件,并将其保存在 Arenas 应用程序中。向文件中添加以下行:
import os
from django.contrib.gis.utils import LayerMapping
from .models import US_States, Counties, Arenas, Districts
打开 models.py 文件,并将所有字段映射字典复制到 load.py 文件中。然后,使用 os 模块将 shapefile 路径分配给一个变量。以下是 US_County_Boundary.shp 的字典和路径变量:
us_counties_mapping = {
'stfips' : 'STFIPS', 'ctfips' : 'CTFIPS', 'state' : 'STATE', 'county' : 'COUNTY',
'version' : 'VERSION', 'shape_leng' : 'Shape_Leng', 'shape_area' : 'Shape_Area', 'geom' : 'MULTIPOLYGON'
}
counties_shp = os.path.abspath(os.path.join(os.path.dirname(__file__), 'data','US_County_Boundaries.shp'),
)
重复此步骤,对所有 shapefile 进行操作,如代码包中提供的 load.py 所示。这些路径变量和映射字典是执行层映射所必需的。
运行层映射
在 load.py 的底部创建一个名为 run 的函数,包含以下代码。注意,映射的名称(例如,us_states_mapping)必须与字典的名称匹配:
def run(verbose=True):
lm = LayerMapping(
US_States, states_shp, us_states_mapping,
transform=False, encoding='iso-8859-1',
)
lm.save(strict=True, verbose=verbose)
lm = LayerMapping(
Counties, counties_shp, us_counties_mapping,
transform=False, encoding='iso-8859-1',
)
lm.save(strict=True, verbose=verbose)
lm = LayerMapping(
Districts, districts_shp, districts_mapping,
transform=False, encoding='iso-8859-1',
)
lm.save(strict=True, verbose=verbose)
lm = LayerMapping(
Arenas, arenas_shp, arenas_mapping,
transform=False, encoding='iso-8859-1',
)
lm.save(strict=True, verbose=verbose)
要运行脚本,我们将使用 manage.py shell 参数来调用 Python shell,然后在此本地 shell 中导入 load.py 文件并执行 run 函数:
>>> from arenas import load
>>> load.run()
一旦调用并执行了 run 函数,shapefile 中的数据行将被导入到数据库表中:

一旦函数成功完成,数据库表将被填充。我们现在可以探索 Django 的一个非常有用的功能——内置的行政面板。
管理面板
Django 框架是在繁忙的新闻编辑室环境中开发的,从一开始就需要一个内置的行政面板,以便记者和编辑可以访问他们的故事。这个概念一直得到支持,因为大多数网站都需要一个用于行政任务的界面。这是一个非常实用和方便的界面,使用它不需要对网站有任何技术知识。
GeoDjango 行政面板
使用 GeoDjango 配置构建的网站没有不同,GeoDjango 网站的行政面板甚至支持显示和编辑几何数据。OpenLayers JavaScript 库包含在面板模板中,以允许数据可视化。它还允许执行常规的行政任务,例如编辑组或用户及其权限。
admin.py
要通过行政面板访问 models.py 中存储的数据模型,必须在 Arenas 应用程序内自动生成的 admin.py 脚本中进行更新。在 IDE 中打开文件,并添加以下行,复制原始代码:
from django.contrib.gis import admin
from .models import US_States, Counties, Arenas, Districts
admin.site.register(US_States, admin.GeoModelAdmin)
admin.site.register(Counties, admin.GeoModelAdmin)
admin.site.register(Arenas, admin.GeoModelAdmin)
admin.site.register(Districts, admin.GeoModelAdmin)
保存脚本并关闭它。
创建超级用户
第一步是创建一个超级用户。此用户将能够访问管理面板。为此,我们将向 manage.py 传递 createsuperuser 参数,并按照出现的指示逐一执行:
C:\Projects\chapter12>python manage.py createsuperuser
Username: loki
Email address: email@server.com
Password:
Password (again):
Superuser created successfully.
现在超级用户可以使用提供的密码和用户名登录管理面板。
runserver
创建超级用户后,将 runserver 参数传递给 manage.py 以启动本地开发网络服务器:

这将使 localhost 默认在端口 8000 上打开(http://127.0.0.1:8000)。管理面板可在:http://127.0.0.1:8000/admin 上找到。打开网络浏览器并导航到管理面板 URL。输入超级用户凭据:

一旦输入,管理面板将列出可用的模型,以及认证和授权部分。这些模型最初以复数形式显示,其名称末尾带有 s(默认情况下进行复数化)。虽然这种行为可以(并且应该)被覆盖,但在这里我们不会关注这个任务:

在“场”下点击 U_s_statess 模型,然后点击状态对象列表中的第一个对象。它应该看起来像这样:

这些字段可以通过这个管理面板进行编辑,甚至可以使用包含的 OpenLayers 编辑插件编辑状态(或在此情况下,波多黎各)的几何形状。编辑后点击保存。也可以从该界面删除数据行。
在这里探索完整的管理面板文档:
docs.djangoproject.com/en/2.0/ref/contrib/admin/
网址
最后,在 HTML 表单部分,我们指定描述和下拉列表的位置,并包含一个隐藏的令牌(CSRF),这是进行认证所必需的。
在生成模型并将数据添加到相关表后,是时候生成一些视图了,这些视图将处理我们的网络请求并返回完成请求所需的数据。
为了正确路由我们的请求,我们首先必须创建一些将与视图配对的 URL。这需要项目级和应用级配置。与 Flask 不同,URL 不是通过 Python 装饰器附加到视图上的。相反,它们包含在单独的脚本中,这些脚本将映射到应用程序或视图。
URL 模式
Django URL 模式非常简洁简单,使得网址短小且易于记忆的网站成为可能。为了实现这一点,需要将请求的 URL 与视图(或与应用程序级 URL 匹配的视图)进行匹配。URL 和它们的目的是在名为 urlpatterns 的列表中进行匹配。
在项目文件夹(C:\Projects\chapter12\chapter12)中,有一个名为 urls.py 的脚本,位于 settings.py 下方。此脚本控制项目级别的 URL 路由。对于此应用程序,我们还将向 arenas 文件夹中添加应用级别的 URL,并将项目级别的 URL 路由指向应用级别的 URL。
打开项目级别的 urls.py 文件,并将以下代码复制到现有代码之上:
from django.urls import include, path
from django.contrib.gis import admin
urlpatterns = [
path('', include('arenas.urls')),
path('arena/', include('arenas.urls')),
path('admin/', admin.site.urls),
]
此代码将请求重定向到应用级别 urls.py 文件中的两个不同 URL,在那里可以进行进一步排序。发送到管理 URL 的任何请求都由管理代码处理。path 函数接受两个必需参数:URL 路径(例如,'arenas/',它指向 http://127.0.0.1:8000/arenas),以及将接受请求的视图或应用级别代码。include 函数用于将来自“场地”应用的可用 URL 添加到项目级别 URL 中。
要创建应用级别的 URL,在“场地”应用文件夹内创建一个名为 urls.py 的脚本。复制以下代码:
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
path('arena', views.arena, name='arena'),
]
这次,函数 path 将请求重定向到 views.py 脚本内的视图(将接受请求的视图)。基本 URL 和场地 URL 都被重定向到一个视图。还包含了一个可选参数 name。
注意,Django URL 模式在 Django 2.0 中引入了重大变化。早期版本的 Django 不使用 path 函数,而是使用一个类似的功能 url。请确保您使用的是最新版本的 Django,以匹配此处所示的代码。
视图
视图是应用程序的核心,在 Django 中以 Python 函数的形式存在。它们接受 GET 和 POST 网络请求,允许在同一个函数内执行多个操作,并产生各种响应。在视图函数中,我们设计如何解析请求,如何查询数据库表,如何处理查询结果(Django 中的 QuerySets),以及将哪些表单和模板与处理后的数据一起发送到浏览器。
现在 URL 模式已经就绪,我们需要编写一些视图来接受和处理发送到 URL 的网络请求。这些视图将查询 models.py 中的数据库表模型类,以找到与 Arenas 类中包含的每个 NBA 场地相关联的位置数据。
必需的文件夹和文件
第一步是创建必要的文件夹,包含表单和模板文件,因为视图的 Web 响应需要预先生成的模板来显示请求的数据(在这种情况下,请求的 NBA 场地 的位置)。
forms.py
在 Django 中,Web 表单用于捕获用户输入并将其提交到视图。为了能够从下拉列表中选择 NBA 场地 名称,并使 Web 地图缩放到该位置,必须创建一个新的脚本 forms.py。打开一个 IDE,并将以下代码复制到一个新文件中:
from django import forms
from .models import Arenas
class ArenaForm(forms.Form):
name = ""
description = "Use the dropdown to select an arena."
selections =
forms.ChoiceField(choices=Arenas.objects.values_list('id','name1'),
widget=forms.Select(),required=True)
此部分通过从 forms.Form 继承创建一个表单类。它有一个 name 字段、一个 description 字段和一个 ChoiceField。ChoiceField 将创建一个下拉列表,由 arenas 的 ID 和名称填充。其他字段将在视图中的 ArenaForm 类中添加,此处未定义。此表单及其字段将插入到下一节中创建的模板中。将此文件保存为 forms.py 到 Arenas 应用程序文件夹中。
模板文件夹
将完成代码包中的 templates 文件夹复制到 Arenas 应用程序文件夹中。在 templates 文件夹中有一个名为 arenas 的文件夹,其中包含一个名为 index.html 的模板 HTML 文件。此文件包含生成网页地图的 JavaScript 部分。在该地图上,NBA arena 的位置被显示出来。
Django 模板使用占位符(用 {{form.field }} 格式标记)允许在运行时将数据传递到模板中,提供请求的详细信息。这些占位符位于 index.html 的各个部分。Django 有自己的内置模板语言,我们在这里将使用它,还包括 Jinja2,这是 Flask 也使用的(参见第十一章,Flask 和 GeoAlchemy2)。
index.html 的第一部分需要强调的是,当前 NBA arena 的 longitude 和 latitude 已添加到 Leaflet JavaScript 中,该地图窗口在 13 级别缩放时将中心定位在该位置:
var themap = L.map('map').setView([ {{form.latitude}}, {{form.longitude}}], 13);
下一个需要强调的部分是,将 longitude、latitude 和关于当前 NBA arena 的自定义 popup 添加到一个标记中:
L.marker([ {{form.latitude}},{{form.longitude}}]).addTo(themap)
.bindPopup("{{form.popup}}").openPopup();
最后,在 HTML form 部分中,我们指定 description 和下拉列表的位置,并包含一个隐藏的令牌(CSRF),这是 POST 请求认证所必需的。按钮由输入 HTML 生成:
<form method="post" class="form">
<h3>{{form.name}}</h3>
<h4>{{form.description}}</h4>
{{form.selections}}
<br>
<input type="submit" value="Find Data">
{% csrf_token %}
</form>
当视图被处理并且数据返回给请求的浏览器时,所有这些占位符都将被填充。
编写视图
最后,我们将设置编写我们的视图。在 IDE 中打开位于 Arenas 应用程序文件夹内的 views.py 文件。导入所需的库、模型、表单和模块:
from django.shortcuts import render, redirect
from django.http import HttpResponse, HttpResponseNotFound
from .models import US_States, Counties, Districts, Arenas
from .forms import ArenaForm
from django.views.decorators.http import require_http_methods
import random
接下来,我们将创建两个视图——index 和 arena*,以及一个非视图函数 queryarena。这些与我们在 urls.py 中添加的 URL 相匹配。index 函数的返回值非常简单——它将重定向到 arena 函数。对于视图,使用装饰器来确定允许的 HTTP 请求方法。
index 视图
index 视图是一个接受请求数据并将其重定向到 arena 视图的 Python 函数,在限制允许的 HTTP 请求之前使用装饰器(require_http_methods):
@require_http_methods(["GET", "POST"])
def index(request):
return redirect(arena)
queryarena 函数
下面的 arena 函数用于为初始 GET 请求选择一个随机的 arena,从数据库模型中获取所选 NBA arena 的数据。查询本身由 queryarena 函数处理。
在这个函数中,接受所选arena的名称作为参数。它用于查询(或filter)所有的Arenas模型对象。这个对象关系映射(ORM)filter方法需要一个字段作为参数;在这种情况下,该字段称为name1。作为一个filter操作的例子,如果arena的名称是Oracle Arena,那么翻译成英文的filter将是查找所有名称为 Oracle Arena 的 NBA 场馆。filter方法的结果以列表形式返回,因此使用零索引从列表中检索第一个结果。一个结果是表示从Arenas类中满足filter参数的数据行的对象:
def queryarena(name):
arena = Arenas.objects.filter(name1=name)[0]
state = US_States.objects.filter(geom__intersects=arena.geom)
if state:
state = state[0]
county = Counties.objects.filter(geom__contains=arena.geom)[0]
district = Districts.objects.filter(geom__contains=arena.geom)[0]
popup = "This arena is called " + arena.name1 + " and it's
located at "
popup += str(round(arena.geom.x,5))+ "," +
str(round(arena.geom.y,5) )
popup += "It is located in " +state.state + " and in the county
of " + county.county
popup += " and in Congressional District " + district.district
return arena.name1, arena.geom.y, arena.geom.x, popup
else:
return arena.name1, arena.geom.y, arena.geom.x, arena.name1 + "
is not in the United States"
一旦实例化了arena对象,其几何字段就用于filter操作。然而,这个filter不是使用字段来filter,而是使用地理空间分析。将arena.geom传递给 GeoDjango 提供的geom__intersects方法执行交集操作,以找到arena所在的州。一个if/else条件检查确保arena位于美国(例如,不是多伦多的arena),以确定返回的正确值。
如果arena位于美国境内,则再次使用arena几何来确定包含arena的county和国会district。这次,地理空间操作是geom_contains。filters返回一个county对象和一个district对象。它们被用来生成将添加到 leaflet 地图上的地图标记的定制popup。这个popup包含arena的经纬度、arena的名称、其county、state的名称以及其state内的国会district编号。
arena视图
arena视图接受request对象,然后实例化一个ArenaForm对象来收集响应request所需的数据。对Arenas模型对象及其values_list方法的查询创建了一个包含每个arena的 ID 和名称的元组的 Python 列表。在条件中使用request方法(无论是GET还是POST)来确定适当的响应。
如果收到一个GET请求(即,网页首次打开),则生成一个随机的arena对象并将其传递给模板,该模板在包含的地图上显示arena。要获取一个随机的arena,我们使用arena名称和 ID 的列表(值)。一旦生成了列表,就使用列表推导式生成一个包含arena名称的新列表。
使用random模块和列表中名称的数量(length)生成一个随机index,用于从列表中选择一个arena名称。然后,这个name被传递给queryarena函数,该函数用arena的name、位置和popup填充form。
这些值通过render函数返回给浏览器。这个函数用于将forms与request一起传递到模板中,并且知道templates文件夹位于 Arenas 应用程序内部的位置:
@require_http_methods(["GET", "POST"])
def arena(request):
values = Arenas.objects.values_list('id','name1')
if request.method=="GET":
form= ArenaForm(request.GET)
names = [name for id, name in values]
length = len(names)
selectname = names[random.randint(0, length-1)]
form.name, form.latitude, form.longitude, form.popup = queryarena(selectname)
return render(request, "arena/index.html", {"form":form})
else:
form= ArenaForm(request.POST)
if form.is_valid():
selectid = int(request.POST['selections'])
selectname = [name for ids, name in values if ids == selectid][0]
form.name, form.latitude, form.longitude, form.popup =
queryarena(selectname)
return render(request, "arena/index.html", {"form":form})
如果收到POST请求(即选择了arena),则通过将POST数据传递给类来调用ArenaForm类,并验证form。所选arena的 ID 用作列表推导中的条件,使我们能够检索arena的name。然后,将name传递给queryarena,查询其位置详情,并在使用render返回之前将其添加到form中。
视图已经完成,脚本可以保存。下一步是运行应用程序。
运行应用程序
打开命令提示符,切换到root文件夹(C:\Projects\chapter12)。使用以下命令启动本地开发服务器:
C:\Projects\chapter12>python manage.py runserver
结果应该看起来像这样:

打开浏览器并访问:http://127.0.0.1:8000。初始的GET请求将被重定向到arenas视图并处理,返回一个随机的arena。从列表中选择另一个arena并点击查找数据按钮将执行POST请求并定位所选的arena。每次选择arena时,arena名称的文本都会改变,同时地图位置和显示的弹出窗口也会改变。
下面是一个POST请求结果的示例:

通过选择不同的 NBA 场馆来测试应用程序,并且为了加分,可以更改弹出消息。
摘要
Django 以其内置电池的哲学,只需要非常少的第三方库就能创建完整的应用程序。此应用程序仅使用 Django 内置工具和 GDAL/OGR 库进行数据管理和数据分析。启用 GeoDjango 功能是一个相对无缝的过程,因为它本是 Django 项目的一部分。
使用 Django 创建 Web 应用程序可以提供很多即时功能,包括管理面板。LayerMapping使得从 shapefiles 导入数据变得容易。ORM 模型使得执行地理空间过滤或查询变得容易。模板系统使得添加网络地图以及位置智能到网站变得容易。
在下一章中,我们将使用一个 Python 网络框架来创建一个地理空间 REST API。这个 API 将接受请求并返回表示地理空间特征的 JSON 编码数据。
第十三章:地理空间 REST API
在网络上发布数据以供消费是现代 GIS 的一个重要组成部分。为了将数据从远程服务器传输到远程客户端,大多数地理空间发布软件堆栈都使用 表示状态转移(REST)网络服务。对于特定数据资源的网络请求,REST 服务将 JavaScript 对象表示法(JSON)编码的数据返回给请求的客户端机器。这些网络服务组合成一个应用程序编程接口,或 API,其中将包含代表每个可查询数据资源的端点。
通过结合 Python 网络框架、对象关系映射(ORM)和 PostGIS 后端,我们可以创建一个自定义的 REST API,该 API 将以 JSON 格式响应网络请求。在这个练习中,我们将使用 Flask 网络框架和 SQLAlchemy 模块,以及 GeoAlchemy2 提供的空间 ORM 功能。
在本章中,我们将学习以下内容:
-
REST API 组件
-
JSON 响应格式化
-
如何处理
GET、POST、PUT和DELETE请求方法 -
使用 API 执行地理空间操作
-
如何使用 IIS 部署 Flask 网站
在 Python 中编写 REST API
为了理解具有 JSON 响应的 REST API 的组件,我们将利用 Flask 网络框架、PostgreSQL/PostGIS 数据库、SQLAlchemy 和 GeoAlchemy2 进行 ORM 查询。Flask 将用于创建 API 的 URL 端点。PostGIS 将将数据存储在由 SQLAlchemy 模型定义的表中,这些模型定义了所有列的类型,除了由 GeoAlchemy2 列类型定义的几何列。
REST
REST 是一种用于网络服务的标准,旨在接受请求和参数,并返回数据的表示,通常以 JSON 格式,但有时也以 XML 或 HTML 格式。使用 REST 架构的 API 必须满足以下架构约束:
-
客户端-服务器交互
-
无状态
-
缓存能力
-
统一接口
-
分层系统
客户端(一个网页浏览器或远程计算机)将向指定 URL 端点的服务器发送请求。请求可以包含参数,这些参数限制了返回的数据对象,就像 SQL 语句中的条件语句一样。它是无状态的,意味着每个请求都必须包含请求参数,并且不能引用另一个请求的结果。返回的数据必须明确标记为可缓存或不可缓存,以便客户端决定数据是否可以存储,或者是否在需要时请求。当请求数据时,所有与数据相关的可用 API 端点(包括添加或删除数据的链接,如果有的话)都作为链接与数据表示一起返回。API 的底层架构不会通过 API 揭示,并且可以在不改变 API 结构的情况下进行操作(添加或删除机器)。
JSON
JSON 旨在被人类和机器 alike 理解。JavaScript 数据对象可以轻松地从 Python 字典生成,因为它们使用相同的键值结构和花括号表示法。Python 包含一个用于生成 JSON 的内置库(json模块),而 Web 框架如 Flask 也包含生成 JSON 响应的代码。
对于地理空间数据,存在多个 JSON 标准,包括 GeoJSON 和 Esri JSON。在本章中,REST API 将使用 GeoJSON 格式来响应请求。
在这里了解更多关于 GeoJSON 的信息:geojson.org/。
Python 用于 REST API
Python 是编写 REST API 的绝佳语言。它包含允许进行数据库查询的模块,以及其他将 HTTP Web 请求处理成 URL 和参数组件的模块。使用这些模块,可以从数据库中检索请求的资源,并使用在 Python 字典和 JSON 对象之间进行转换的模块将数据作为 JSON 返回。
虽然可以使用标准库构建基于 Python 的 API,但使用 Web 框架来构建 API 将加快开发速度,并允许根据需要添加组件模块。
Flask
Flask 是 Python Web 框架构建 REST API 的一个好选择。与 SQLAlchemy 和 GeoAlchemy2(见第十一章,Flask 和 GeoAlchemy2,了解更多关于这两个库的信息)配合使用,它允许将 REST URL 端点与视图(一个 Python 函数)配对,该视图将根据请求方法(例如GET和POST,仅举两个例子)以不同的方式处理请求并返回 JSON 数据。
REST 模块
由于 Flask 旨在可扩展,因此有许多旨在简化 REST API 创建的附加模块。这些包括:
-
Flask-RESTful (
flask-restful.readthedocs.io/en/latest/) -
Eve (
python-eve.org/),它建立在 Flask 和 Cerberus 之上 -
Flask-REST-JSONAPI (
github.com/miLibris/flask-rest-jsonapi)
本章将使用纯 Flask 功能,结合 SQLAlchemy 和 GeoAlchemy2 进行数据库查询,以说明 API 创建的基本原理。
其他框架
Django 和 GeoDjango(在第十二章,GeoDjango中介绍)被广泛用于创建 REST API。遵循其“内置电池”的设计理念,Django 允许轻松地进行 API 开发。Django REST 框架简化了代码库中的 API 发布。
在这里探索 Django REST 框架:www.django-rest-framework.org/。
Flask URL 中的变量
当使用 Flask 进行 URL 处理时,了解如何将变量添加到 URL 中是有用的,因为每个资源可能使用 ID 或字符串标识符(例如,州名)进行请求。Flask URL 使用占位符将数据传递到函数参数中,并在每个端点的视图中将其作为变量使用。使用转换器,可以在占位符中为数值数据分配类型;默认类型是字符串类型。
数字转换器
在此示例中,在 URL 末尾添加了一个带有整数 ID 转换器的占位符。通过在占位符变量(arena_id)之前添加int:,可以使用 ID 通过session.query的get(id)方法查询Arena模型/数据库表,该方法期望一个整数。如果占位符中没有指定数据类型转换器,arena_id变量将包含字符串字符,并且不会被get(id)方法使用:
@app.route('/nba/api/v0.1/arena/<int:arena_id>', methods=['GET'])
def get_arena(arena_id):
arena = session.query(Arena).get(arena_id)
在指定了参数数据类型后,ORM 查询将返回请求的arena对象,并可以对其进行处理以生成响应。
其他数据转换器
除了整数,可以使用float转换浮点数据,使用path转换 URL 数据。字符串(使用转换器string)是默认的。在这种情况下,捕获并使用一个float值来与county几何体面积进行比较。由于此数据的 SRID 在 WKID 中,面积以奇特的格式表示,但此查询将有效:
@app.route('/nba/api/v0.1/county/query/size/<float:size>', methods=['GET'])
def get_county_size(size):
counties = session.query(County).filter(County.geom.ST_Area() > size).all()
data = [{"type": "Feature",
"properties":{"name":county.name,"id":county.id ,"state":county.state.name},
"geometry":{"type":"MultiPolygon",
"coordinates":[shapely.geometry.geo.mapping(to_shape(county.geom))["coordinates"]]},
} for county in counties]
return jsonify({"type": "FeatureCollection","features":data})
在此示例中,从 URL 变量捕获的值使用ST_Area函数与county几何体进行比较,该函数借鉴了 PostGIS 空间 SQL。
在这里了解更多关于 GeoAlchemy2 空间功能及其使用空间 SQL 的信息:geoalchemy-2.readthedocs.io/en/latest/spatial_functions.html。
请求方法
当使用 REST API 时,可以充分利用多种 HTTP 请求方法。GET方法用于请求数据,POST方法用于添加新数据,PUT方法用于更新数据,而DELETE方法用于从数据库中删除数据。
GET
对于 Flask URL 端点,使用GET请求指定方法GET。数据可以作为参数传递,并通过request.args访问:
from flask import requests, jsonify
@app.route('/nba/api/v0.1/arenas', methods=['GET'])
def get_arenas():
if 'name' in request.args:
arenas = session.query(Arena).filter(name=request.args['name'])
else:
arenas = session.query(Arena).all()
data = [{"type": "Feature", "properties":{"name":arena.name, "id":arena.id},
"geometry":{"type":"Point","coordinates":[round(arena.longitude,6), round(arena.latitude,6)]},
} for arena in arenas]
return jsonify({"type": "FeatureCollection","features":data})
将响应数据通过列表推导式处理成 Python 字典列表,然后添加到另一个 Python 字典中,并使用 Flask 的jsonify将其转换为 JSON。
POST
POST请求携带的数据可以处理以添加到数据库中。为了区分POST请求,Flask 请求对象有一个method属性,可以检查请求方法是否为GET或POST。如果我们创建一个form(称为AddForm)来向Arenas表添加新的arenas,我们可以处理作为POST请求提交的数据,并使用会话管理器将其添加到数据库中:
from flask import request
from .forms import AddForm
@app.route('/nba/api/v0.1/arena/add', methods=['GET', 'POST'])
def add_arenas():
form = AddForm(request.form)
form.name.data = "New Arena"
form.longitude.data = -121.5
form.latitude.data = 37.8
if request.method == "POST":
arena = Arena()
arena.name = request.form['name']
arena.longitude =float(request.form['longitude'])
arena.latitude = float(request.form['latitude'])
arena.geom = 'SRID=4326;POINT({0} {1})'.format(arena.longitude, arena.latitude)
session.add(arena)
data = [{"type": "Feature", "properties":{"name":arena.name},
"geometry":{"type":"Point",
"coordinates":[round(arena.longitude,6), round(arena.latitude,6)]},}]
return jsonify({'added':'success',"type": "FeatureCollection","features":data})
return render_template('addarena.html', form=form)
由于此方法将接受 GET 和 POST 请求,因此它根据每个请求方法发送不同的响应。
其他可用的请求方法
虽然 GET 和 POST 是主要请求方法,但还有其他方法可用于处理数据。对于示例 API,我们只会使用 GET、POST 和 DELETE。
PUT
与 POST 请求类似,PUT 请求将携带数据以更新或添加到数据库中。它将尝试多次更新数据以确保更新完全传输。
DELETE
DELETE 方法将从指定的端点删除资源,例如,从 Arenas 表中删除一个 arena。它需要一个记录标识符来指定要删除的资源:
@app.route('/nba/api/v0.1/arena/delete/<int:arena_id>', methods=['DELETE'])
def delete_arena(arena_id):
arena = session.query(Arena).delete(arena_id)
这是一个 REST API 应用程序
为了能够访问 NBA 场馆、美国州、美国县和美国国会选区的数据库,我们将构建一个 REST API。该 API 将允许查询表和特定表资源,即数据行。它还将允许进行地理空间查询。
应用程序组件
此应用程序的组件包括:
-
在 第十一章 中创建的数据库,Flask 和 GeoAlchemy2,其中包含 NBA 场馆、美国州、美国县和美国国会选区的表
-
app.py文件,当通过 Python 可执行文件调用时,它将启动应用程序 -
application文件夹,它包含应用程序代码和文件夹 -
__init__.py文件,它使application文件夹成为一个模块,定义 Flask 对象并连接到数据库 -
views.py文件,它定义了 API 端点、视图函数和返回的响应 -
models.py文件,它定义了数据库表模型作为从 SQLAlchemy 继承的 Python 类 -
forms.py文件,它定义了 HTML 表单 -
static和templates文件夹,它们包含模板和数据
应用程序文件夹和文件结构
示例 REST API 需要创建特定的文件和文件夹。外部文件夹,称为 arenaapp,将包含 app.py 文件和名为 application 的文件夹。创建名为 arenaapp 的文件夹。在其内部,创建一个名为 application 的文件夹。在 application 内部,创建 static 和 templates 文件夹:

其他文件,views.py、models.py 和 forms.py,将位于 application 内部。两个文件夹 static 和 templates 将存储应用程序数据和 HTML 表单:

app.py
使用 IDE 或文本编辑器,在 arenaapp 内创建一个名为 app.py 的文件。打开此文件并添加以下行;此文件将由 Python 可执行文件运行以启动 REST API 应用程序:
from application import app
app.run()
__init__.py 文件允许 application 文件夹被 app.py 导入,从而允许调用 Flask 对象 app 及其 app.run() 方法。
init.py
在application文件夹内,创建一个名为__init__.py的文件。在文件内,添加以下代码(同时调整用户名和密码以匹配您的特定数据库凭据):
import flask
app = flask.Flask(__name__)
conn_string = 'postgresql://{user}:{password}@localhost:5432/chapter11'
app.config['SQLALCHEMY_DATABASE_URI'] = conn_string
app.config['SECRET_KEY'] = "SECRET_KEY"
import application.views
在此文件中,创建了 Flask 对象app并进行了配置。为了连接到数据库,使用了一个连接字符串并将其存储在app.config字典中的'SQLALCHEMY_DATABASE_URI'。请记住将用户名和密码添加到连接字符串中。
数据库
这将连接到在第十一章中创建的数据库,Flask 和 GeoAlchemy2。它是由导入并结构化以匹配我们逐步描述的模型的 shapefiles 生成的。为了确保应用程序能够工作,请确保已创建数据库并且已导入 shapefiles。
models.py
在models.py中,导入了 SQLAlchemy 和 GeoAlchemy2 模块,并初始化了数据库会话。数据库模型以其 Python 类形式定义了模式,允许查询和数据更新。
导入所需的模块
这些模块使应用程序能够定义模型并连接到数据库:
# The database connections and session management are managed with SQLAlchemy functions
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, ForeignKey, Float
from sqlalchemy.orm import sessionmaker, relationship
# The Geometry columns of the data tables are added to the ORM using the Geometry data type
from geoalchemy2 import Geometry
声明会话
从app.config字典中,将数据库连接字符串传递给create_engine函数。一旦engine绑定到sessionmaker,就可以启动一个session:
from application import app
# Connect to the database called chapter11 using SQLAlchemy functions
engine = create_engine(app.config['SQLALCHEMY_DATABASE_URI'])
Session = sessionmaker(bind=engine)
session = Session()
Base = declarative_base()
从declarative_base()函数创建了一个名为Base的 Python 类。然后使用Base类来子类化所有应用程序类。
声明模型
对于模型,所有字段类型(例如,Integer、String和Float)都使用 SQLAlchemy ORM 列类定义,除了几何列,它们使用 GeoAlchemy2 的Geometry类。Geometry类需要一个几何类型和 SRID:
# Define the Arena class, which will model the Arena database table
class Arena(Base):
__tablename__ = 'arena'
id = Column(Integer, primary_key=True)
name = Column(String)
longitude = Column(Float)
latitude = Column(Float)
geom = Column(Geometry(geometry_type='POINT', srid=4326))
County类有一个主键字段和一个name字段,以及定义与State类多对一关系的字段。它使用MULTIPOLYGON而不是POINT几何类型:
# Define the County class, which will model the County database table
class County(Base):
__tablename__ = 'county'
id = Column(Integer, primary_key=True)
name = Column(String)
state_id = Column(Integer, ForeignKey('state.id'))
state_ref = relationship("State",backref='county')
geom = Column(Geometry(geometry_type='MULTIPOLYGON', srid=4326))
District类代表美国国会选区。以MULTIPOLYGON几何类型和 SRID 为4326存储,它与State类有一个多对一的关系。每个存储的district都与它所在的州相关联:
# Define the District class, which will model the District database table
class District(Base):
__tablename__ = 'district'
id = Column(Integer, primary_key=True)
district = Column(String)
name = Column(String)
state_id = Column(Integer, ForeignKey('state.id'))
state_ref = relationship("State",backref='district')
geom = Column(Geometry(geometry_type='MULTIPOLYGON', srid=4326))
State类分别与County和District类具有一对一和一对多关系,使用relationship函数定义。它还有一个MULTIPOLYGON几何列,SRID 为4326:
# Define the State class, which will model the State database table
class State(Base):
__tablename__ = 'state'
id = Column(Integer, primary_key=True)
name = Column(String)
statefips = Column(String)
stpostal = Column(String)
counties = relationship('County', backref='state')
districts = relationship('District', backref='state')
geom = Column(Geometry(geometry_type='MULTIPOLYGON', srid=4326))
定义了字段和关系后,下一步是创建 REST API 端点和编写查询数据库并返回 GeoJSON 响应的视图。
forms.py
为了捕获用户数据,例如一个新的arena,将使用表单。在application文件夹内创建一个名为forms.py的文件,并添加以下代码:
from flask_wtf import FlaskForm
from wtforms import TextField, FloatField
class AddForm(FlaskForm):
name = TextField('Arena Name')
longitude = FloatField('Longitude')
latitude = FloatField('Latitude')
此代码将字段添加到模板中,这将在使用POST方法的章节中讨论。它将允许从 HTML 模板中输入代码并将其传递到服务器以添加新的arena。
views.py
API 端点和处理包含在views.py中。视图在__init__.py中导入,以便它们对app对象可用。打开 IDE 并在application文件夹中保存一个名为views.py的文件。
导入模块
为了启用 Web 请求的处理,我们需要从 Flask、GeoAlchemy2 和 Shapely(一个用于创建和处理地理空间数据的 Python 模块)导入功能。我们还将导入模型和表单:
from application import app
from flask import render_template,jsonify, redirect, url_for, request, Markup
from .forms import *
from .models import *
import geoalchemy2,shapely
from geoalchemy2.shape import to_shape
基础 URL
每个 API 模式可能不同,但通常应包括一个基础 URL,该 URL 指示 API 版本,并应链接到 API 中可用的其他端点。此应用程序将使用基础 URL 模式nba/api/v0.1。在这种情况下,主页 URL('/')将重定向到 API 的基础 URL:
@app.route('/', methods=['GET'])
def get_api():
return redirect('/nba/api/v0.1')
@app.route('/nba/api/v0.1', methods=['GET'])
def get_endpoints():
data= [{'name':"Arena", "endpoint":"/arena"},
{'name':"State", "endpoint":"/state"},
{'name':"County", "endpoint":"/county"},
{'name':"District", "endpoint":"/district"},]
return jsonify({"endpoints":data})
下述各节中的端点均从基础 URL 提供。每个资源 URL 可以通过将资源特定的端点添加到基础 URL 来构建。
Arenas
要从Arenas表请求数据,我们将定义 API 端点并使用视图函数查询Arenas模型。每个响应都将作为一个 GeoJSON 包返回。此端点('/arena')将返回一个 GeoJSON 响应,该响应将根据添加到 URL 中的变量的存在而变化。这些变量包括 arenas ID 和名称。
获取所有 arenas
要生成包含所有arenas表示的响应,使用 SQLAlchemy ORM 进行查询。为了将查询结果转换为 GeoJSON,使用列表推导来生成一个字典列表,这些字典描述了从 ORM 查询返回的每个arena。然后,将生成的列表(data)添加到一个字典中,使用jsonify函数将该 Python 字典转换为 JSON 对象:
@app.route('/nba/api/v0.1/arena', methods=['GET'])
def get_arenas():
arenas = session.query(Arena).all()
data = [{"type": "Feature", "properties":{"name":arena.name, "id":arena.id},
"geometry":{"type":"Point", "coordinates":[round(arena.longitude,6), round(arena.latitude,6)]},
} for arena in arenas]
return jsonify({"type": "FeatureCollection","features":data})
返回name和id字段,以及longitude和latitude。为了限制传输的数据量,将latitude和longitude四舍五入到6位小数。描述arena位置所需的高精度要求使得这种限制是合理的。由于点数据类型仅由两个点组成,产生的数据较少,因此更容易返回。而多边形和多段线数据则更大,需要更高的精度。
与循环相比,列表推导减少了迭代列表所需的处理时间。在此处了解更多关于列表推导的信息:
docs.python.org/3/tutorial/datastructures.html#list-comprehensions.
通过 ID 获取 arenas
通过向arena端点添加一个数字 ID,可以定位并返回特定的arena。使用session.query方法的get来检索请求的arena对象:
@app.route('/nba/api/v0.1/arena/<int:arena_id>', methods=['GET'])
def get_arena(arena_id):
arena = session.query(Arena).get(arena_id)
data = [{"type": "Feature", "properties":{"name":arena.name, "id":arena.id}, "geometry":{"type":"Point", "coordinates":[round(arena.longitude,6), round(arena.latitude,6)]},
return jsonify({"type": "FeatureCollection","features":data})
选定的arena被添加到列表中的一个字典内,然后该字典被添加到另一个字典中,并以 JSON data的形式返回。
通过名称获取竞技场
可以通过name在此端点请求arena。通过利用称为filter的查询条件,可以检索到与提供的name匹配的arena。为了增加灵活性,使用了一个like运算符(以及一个"%"通配符运算符),使得输入的arena name可以是完整的。相反,输入的字符串将被用于filter查询,并仅返回名称以该字符串开头的arena对象:
@app.route('/nba/api/v0.1/arena/<arena_name>', methods=['GET'])
def get_arena_name(arena_name):
arenas = session.query(Arena).filter(Arena.name.like(arena_name+"%")).all()
data = [{"type": "Feature", "properties":{"name":arena.name,"id":arena.id},
"geometry":{"type":"Point", "coordinates":[round(arena.longitude,6), round(arena.latitude,6)]},
} for arena in arenas]
return jsonify({"type": "FeatureCollection","features":data})
使用列表推导式生成arena字典。以下是对arena端点进行字符串查询的响应示例:

地理空间查询
通过添加一个额外的 URL 组件,API 被启用空间查询。传递一个arena ID 并添加"/intersect"将使用空间查询来查找描述请求的 NBA 竞技场的数据。在此视图函数中,使用intersect filter(即,使用点在多边形函数中识别包含arena的县)查询县和区表。使用县和州之间的表关系检索基础州。返回所有几何形状和所选字段:
@app.route('/nba/api/v0.1/arena/<int:arena_id>/intersect', methods=['GET'])
def arena_intersect(arena_id):
arena = session.query(Arena).get(arena_id)
county = session.query(County).filter(County.geom.ST_Intersects(arena.geom)).first()
district=session.query(District).filter(District.geom.ST_Intersects(arena.geom))
district = district.first()
if county != None:
data = [{"type": "Feature", "properties": {"name":arena.name, "id":arena.id,} ,
"geometry":{"type":"Point", "coordinates":[round(arena.longitude,6), round(arena.latitude,6)]},
},{"type": "Feature", "properties": {"name":county.name, "id":county.id,} ,
"geometry":{"type":"MultiPolygon",
"coordinates":[shapely.geometry.geo.mapping(to_shape(county.geom))]},
},{"type": "Feature", "properties": {"name":district.district, "id":district.id,},
"geometry":{"type":"MultiPolygon",
"coordinates":[shapely.geometry.geo.mapping(to_shape(district.geom))]},
},{"type": "Feature", "properties": {"name":county.state_ref.name, "id":county.state_ref.id,}, "geometry":{"type":"MultiPolygon",
"coordinates":[shapely.geometry.geo.mapping(to_shape(county.state_ref.geom))]},
}]
return jsonify({"type": "FeatureCollection","features":data})
else:
return redirect('/nba/api/v0.1/arena/' + str(arena_id))
为了确保函数有效,if条件检查arena是否在美县内;如果不是,则不使用县、区和州对象。相反,请求被重定向到非地理空间查询视图函数。
州
由于每个州由许多顶点组成,因此美国州数据可能很大。在states端点中,我们将添加一些 URL 参数,使我们能够决定返回的每个请求州的几何形状。
获取所有州
通过检查request.args字典中的 URL 参数,然后检查该参数是否评估为真,我们可以确定是否应返回所有州的几何形状。GeoJSON 响应是通过使用to_shape函数和shapely.geometry.geo.mapping(简称为smapping)函数从州的几何形状生成的:
@app.route('/nba/api/v0.1/state', methods=['GET'])
def get_states():
smapping = shapely.geometry.geo.mapping
states = session.query(State).all()
data = [{"type": "Feature",
"properties":{"state":state.name,"id":state.id},
"geometry":{"type":"MultiPolygon",
"coordinates":"[Truncated]"},
} for state in states]
if "geometry" in request.args.keys():
if request.args["geometry"]=='1' or request.args["geometry"]=='True':
data = [{"type": "Feature",
"properties":{"state":state.name,"id":state.id},
"geometry":{"type":"MultiPolygon",
"coordinates":[smapping(to_shape(state.geom))["coordinates"]]},
} for state in states]
return jsonify({"type": "FeatureCollection","features":data})
如果不包括geometry参数或参数,几何形状将表示为截断。
通过 ID 获取州
要使用州的主键 ID 获取特定的州,我们可以添加一个 URL 变量来检查整数 ID。它以geojson的形式返回,包括geometry:
@app.route('/nba/api/v0.1/state/<int:state_id>', methods=['GET'])
def get_state(state_id):
state = session.query(State).get(state_id)
geojson = shapely.geometry.geo.mapping(to_shape(state.geom))
data = [{"type": "Feature", "properties":{"name":state.name},
"geometry":{"type":"MultiPolygon", "coordinates":[geojson["coordinates"]]},
}]
return jsonify({"type": "FeatureCollection","features":data})
通过名称获取州
使用filter将允许使用 URL 变量作为query filter。字符串变量将与数据库表中的state name字段进行比较,并使用like运算符进行模糊比较(即,如果state_name变量是'M',则将获取所有以'M'开头的states):
@app.route('/nba/api/v0.1/state/<state_name>', methods=['GET'])
def get_state_name(state_name):
states = session.query(State).filter(State.name.like(state_name+"%")).all()
geoms = {state.id:smapping(to_shape(state.geom)) for state in states}
data = [{"type": "Feature", "properties":{"state":state.name},
"geometry":{"type":"MultiPolygon",
"coordinates":[shapely.geometry.geo.mapping(to_shape(state.geom)["coordinates"]]},
} for state in states]
return jsonify({"type": "FeatureCollection","features":data})
此函数没有 URL 参数,并将返回所选州的指定字段和geometry。
通过州获取场馆
此函数使用空间分析来查找所有被state包含的arenas。state通过 ID 识别,URL 组件将选择所有geometry在state geometry内的arenas:
@app.route('/nba/api/v0.1/state/<int:state_id>/contains', methods=['GET'])
def get_state_arenas(state_id):
state = session.query(State).get(state_id)
shp = to_shape(state.geom)
geojson = shapely.geometry.geo.mapping(shp)
data = [{"type": "Feature", "properties":{"name":state.name},
"geometry":{"type":"MultiPolygon", "coordinates":[geojson]},
}]
arenas = session.query(Arena).filter(state.geom.ST_Contains(arena.geom))
data_arenas =[{"type": "Feature",
"properties":{"name":arena.name}, "geometry":{"type":"Point",
"coordinates":[round(arena.longitude,6), round(arena.latitude,6)]},
} for arena in arenas]
data.extend(data_arenas)
return jsonify({"type": "FeatureCollection","features":data})
返回的数据将包括州的data和所有arenas的data,因为 GeoJSON 允许将多个数据类型打包为一个要素集合。
县
与State数据库表类似,这将检索所有county数据。它接受一个geometry参数来决定是否返回每个county的geometry:
@app.route('/nba/api/v0.1/county', methods=['GET'])
def get_counties():
counties = session.query(County).all()
geoms = {county.id:smapping(to_shape(county.geom)) for county in counties}
if 'geometry' in request.args.keys():
data = [{"type": "Feature",
"properties":{"name":county.name, "state":county.state.name},
"geometry":{"type":"MultiPolygon",
"coordinates":[shapely.geometry.geo.mapping(to_shape(state.geom)["coordinates"]]},
} for county in counties]
else:
data = [{"type": "Feature",
"properties":{"name":county.name, "state":county.state.name},
"geometry":{"type":"MultiPolygon",
"coordinates":["Truncated"]},
} for county in counties]
return jsonify({"type": "FeatureCollection","features":data})
通过 ID 获取县
在使用get_counties函数检索所有县之后,可以将特定county的 ID 传递给此函数。使用session.query.County.get(county_id)可以检索感兴趣的county:
@app.route('/nba/api/v0.1/county/<int:county_id>', methods=['GET'])
def get_county(county_id):
county = session.query(County).get(county_id)
shp = to_shape(county.geom)
geojson = shapely.geometry.geo.mapping(shp)
data = [{"type": "Feature",
"properties":{"name":county.name, "state":county.state.name},
"geometry":{"type":"MultiPolygon",
"coordinates":[geojson]},
}]
return jsonify({"type": "FeatureCollection","features":data})
通过名称获取县
再次,我们可以使用 URL 变量来收集一个字符串,并使用提供的字符串作为查询过滤器。如果使用Wash作为 URL 变量county_name,查询将找到所有以Wash开头的counties:
@app.route('/nba/api/v0.1/county/<county_name>', methods=['GET'])
def get_county_name(county_name):
counties = session.query(County).filter(County.name.like(county_name+"%")).all()
data = [{"type": "Feature",
"properties":{"name":county.name, "state":county.state.name},
"geometry":{"type":"MultiPolygon",
"coordinates":[shapely.geometry.geo.mapping(to_shape(county.geom))["coordinates"]]},
} for county in counties]
return jsonify({"type": "FeatureCollection","features":data})
filter方法可以用于空间字段以及非空间字段。
区域
区域可以被类似地添加到 API 中。在这种情况下,我们将添加一个几何参数来决定是否应该返回几何信息。这允许请求的机器或浏览器获取所有区域及其 ID,这些 ID 可以用于在下一节中获取单个区域,或者根据需要一次性获取所有数据。
获取所有区域
此端点('/district')将使用session.query(District).all()对District模型进行查询:
@app.route('/nba/api/v0.1/district', methods=['GET'])
def get_districts():
districts = session.query(District).all()
if 'geometry' in request.args.keys() and request.args['geometry'] in ('1','True'):
data = [{"type": "Feature",
"properties":{"representative":district.name, "district":district.district,
"state": district.state_ref.name, "id":district.id},
"geometry":{"type":"MultiPolygon",
"coordinates":shapely.geometry.geo.mapping(to_shape(district.geom))["coordinates"]},
} for district in districts]
else:
data = [{"type": "Feature",
"properties":{"representative":district.name, "district":district.district,
"state": district.state_ref.name, "id":district.id},
"geometry":{"type":"MultiPolygon",
"coordinates":["Truncated"]},
} for district in districts]
return jsonify({"type": "FeatureCollection","features":data})
通过 ID 获取区域
通过传递整数district ID,将仅返回请求的district表示。geometry使用shapely和geoalchemy2.shape中的to_shape方法转换为 GeoJSON 格式:
@app.route('/nba/api/v0.1/district/<int:district_id>', methods=['GET'])
def get_district(district_id):
district = session.query(District).get(district_id)
shp = to_shape(district.geom)
geojson = shapely.geometry.geo.mapping(shp)
data = [{"type": "Feature",
"properties":{"district":district.district,"id":district.id},
"geometry":{"type":"MultiPolygon",
"coordinates":[geojson['coordinates']]},
}]
return jsonify({"type": "FeatureCollection","features":data})
通过名称获取区域
在这种情况下,区域的name是国会区域编号。存在一个name字段,但它包含该区域的当选代表的姓名:
@app.route('/nba/api/v0.1/district/<dist>', methods=['GET'])
def get_district_name(dist):
districts = session.query(District).filter(District.district.like(dist+"%")).all()
data = [{"type": "Feature",
"properties":{"district":district.district,"id":district.id,
"representative":district.name}, "geometry":{"type":"MultiPolygon",
"coordinates":shapely.geometry.geo.mapping(to_shape(district.geom))["coordinates"]},
} for district in districts]
return jsonify({"type": "FeatureCollection","features":data})
所有这些方法都可以调整以包含更多参数。尝试添加检查返回字段的条件或另一个条件。所有 URL 参数都添加在查询后的问号('?')之后。
API POST 端点
使用 JSON 数据和 HTML 表单都可以添加arena。在本节中,我们将创建一个 HTML 模板,使用forms.py中的AddForm,并使用它从第十二章,GeoDjango代码包中包含的Leaflet.js地图收集数据。它还使用 jQuery 库允许用户点击地图上的任何位置,从而更新地图的longitude和latitude数据:

新场馆
要将新的 arena 添加到数据库的 Arena 表中,将创建一个用于处理请求的视图函数和一个 Jinja2 HTML 模板,并使用它们。该函数将确定请求方法,并将适当的响应发送给请求。如果是一个 GET 请求,它将发送一个包含 AddForm 表单的 HTML 模板。从 HTML 模板中,填写数据并点击按钮将提交一个 POST 请求,该请求将转到相同的视图函数,并使用提交的数据在 Arena 表中添加一行新数据。
视图函数
将处理请求的视图函数接受 GET 和 POST 请求方法。在这种情况下使用端点 '/add',尽管它可以是任何与 arena 端点区分开来的东西:
@app.route('/nba/api/v0.1/arena/add', methods=['GET', 'POST'])
def add_arenas():
form = AddForm(request.form)
form.name.data = "New Arena"
form.longitude.data = -121.5
form.latitude.data = 37.8
if request.method == "POST":
arena = Arena()
arena.name = request.form['name']
arena.latitude = float(request.form['latitude'])
arena.longitude = float(request.form['longitude'])
arena.geom = 'SRID=4326;POINT({0} {1})'.format(arena.longitude, arena.latitude)
session.add(arena)
data = [{"type": "Feature", "properties":{"name":arena.name},
"geometry":{"type":"Point",
"coordinates":[round(arena.longitude,6), round(arena.latitude,6)]},}]
return jsonify({'added':'success',"type": "FeatureCollection","features":data})
return render_template('addarena.html', form=form)
一旦按钮被按下,数据就会被提交。视图函数将根据请求方法确定要执行的操作——如果是一个 POST 请求,则 form 中提交的数据将用于创建一个新的 arena 对象,并且会话管理器将保存该对象,并将其添加到数据库中。
addarena.html 的头部
接下来,让我们创建一个名为 addarena.html 的模板,该模板将被添加到 application 文件夹内的 templates 文件夹中。在 HTML 文件的顶部,在 head 部分,添加 CSS、JavaScript 和 jQuery 库:
<!DOCTYPE html>
<html>
<head>
<title>Arena Map</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.2.0/dist/leaflet.css" />
<script src="img/leaflet.js"></script>
<script src="img/jquery.min.js"></script>
</head>
addarena.html 脚本
创建地图 <div> 部分,并添加将启用地图交互性的 JavaScript。如果地图被点击,JavaScript 函数 showMapClick(它接受一个事件 *e* 作为参数)将移动标记。在函数内部,使用 jQuery 设置 latitude 和 longitude form 元素的值,从事件参数的 e.latlng 方法获取值:
<body>
<div id="map" style="width: 600px; height: 400px;"></div>
<script>
var themap = L.map('map').setView([ {{form.latitude.data}},{{form.longitude.data}}], 13);
L.tileLayer('https://stamen-tiles-{s}.a.ssl.fastly.net/toner/{z}/{x}/{y}.{ext}', {
subdomains: 'abcd',
minZoom: 1,
maxZoom: 18,
ext: 'png'
}).addTo(themap);
marker = L.marker([ {{form.latitude.data}},{{form.longitude.data}}]).addTo(themap)
.bindPopup("Click to locate the new arena").openPopup();
var popup = L.popup();
function showMapClick(e) {
$('#longitude').val(e.latlng.lng);
$('#latitude').val(e.latlng.lat);
marker
.setLatLng(e.latlng)
.bindPopup("You added a new arena at " + e.latlng.toString())
.openPopup();
}
themap.on('click', showMapClick);
</script>
addarena.html 表单
form 数据将以 POST 方法提交。一旦按下“添加 Arena”按钮,表单中的数据就会被提交:
<form method="post" class="form">
Name: {{form.name}}<br>
Longitude: {{ form.longitude(class_ = 'form-control first-input last-input', placeholder = form.longitude.data, ) }} <br>
Latitude: {{ form.latitude(class_ = 'form-control first-input last-input', placeholder = form.latitude.data, ) }} <br>
<input type="submit" value="Add Arena">
</form>
</body>
</html>
点击按钮将数据提交给视图函数。数据将被处理,并返回一个成功的 JSON 消息:

使用 requests 库发送 POST 请求
可以使用 Web 请求添加一个新的 arena,避免使用 HTML 模板。以下是一个使用 requests 库进行请求的演示:
>>> form = {'longitude':'-109.5', 'latitude':'40.7', 'name':'Test Arena'}
>>> requests.post('http://127.0.0.1:5000/nba/api/v0.1/arena/add', form)
<Response [200]>
将 POST 请求发送到 '/add' 端点,同时附带所需的 form 参数,作为一个 Python 字典。
删除 arena
删除一个 arena(或另一个资源)也可以通过视图函数和特定的端点来完成:
@app.route('/nba/api/v0.1/arena/delete/<int:arena_id>', methods=['DELETE'])
def delete_arena(arena_id):
arena = session.query(Arena).delete(arena_id)
return jsonify({"deleted":"success"})
要删除一个 arena,发送一个使用 delete 方法的请求:
>>> import requests
>>>requests.delete('http://127.0.0.1:5000/nba/api/v0.1/arena/delete/30')
在本地运行 REST API
要在本地运行此 API 应用程序,将 app.py 脚本传递给 Python 可执行文件。这将启动本地机器上的内置 Web 服务器:
C:\Projects\Chapter13\arenaapp>python app.py
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
一旦服务器启动,导航到 API 端点以从视图函数获取响应。然而,如果应用程序是完整的,本地的服务器可能不足以处理 API 请求。相反,需要在生产 Web 服务器上进行部署。
将 Flask 部署到 IIS
在安装了互联网信息服务(IIS)的 Microsoft 服务器上部署新的 API 应用程序,我们需要下载一些 Python 代码,以及一个名为FastCGI的 IIS 模块。一旦配置完成,应用程序将能够响应来自任何允许的机器的 Web 请求。
Flask 和 Web 服务器
虽然 Flask 包含一个用于测试目的的本地 Web 服务器,但它并不是为生产部署而设计的。Flask 与 Apache 或 IIS 等 Web 服务器配合得最好。虽然关于如何使用 Apache 部署 Flask 的文献很多,但关于如何使用 IIS 部署它的良好说明却很少见。由于大多数 GIS 专业人员使用 Windows 服务器或可以访问它们,这些说明将专注于使用 IIS 7 进行部署。
WSGI
Web 服务器网关接口(WSGI)是一个 Python 规范,它允许 Python 可执行文件用于响应 Web 请求。WSGI 内置在 Flask 和 Django 等 Python Web 框架中。
要启用 Flask Web 框架用于服务 Web 页面,需要对 IIS 进行一些配置,包括安装一个名为FastCGI的 IIS公共网关接口(CGI)模块,以及安装一个名为WFastCGI的 Python 模块。有了这两个添加项,IIS Web 服务器将连接到 API 应用程序背后的代码。
安装 WFastCGI 模块和 FastCGI
使用此处可用的 Web 平台安装程序:www.microsoft.com/web/downloads/platform.aspx(如果尚未安装)。使用右上角的搜索栏,输入WFastCGI。搜索结果将出现,并列出适用于 Python 2.x 和 Python 3.x 的可用 WFastCGI 版本。选择 Python 3.6 的版本并运行安装程序。
此安装向所需的技栈添加了两个重要组件。FastCGI 模块被添加到 IIS 中,WFastCGI Python 代码被添加到一个新的 Python 安装中。这个新安装将添加到C:\Python36,除非该位置已存在版本(不包括 ArcGIS10.X Python 安装中的 Python 版本)。
在这个新安装中,C:\Python36\Scripts(或等效)文件夹中添加了一个名为wfastcgi.py的文件。这个文件应该被复制到站点文件夹中,紧挨着app.py文件。
配置 FastCGI
打开 IIS,点击默认网站。在内容窗格的功能视图中,选择处理器映射图标。双击打开它。从右侧窗格中选择添加模块映射。当添加模块映射界面出现时,输入以下内容:
-
在请求路径条目中添加一个星号(
*)。 -
从模块选择列表中选择 FastCGI 模块。
-
如果您已将
wfastcgi.py文件复制到代码路径,并且代码位于C:\website,请在可执行条目中输入以下内容:C:\Python36\python.exe|C:\website\wfastcgi.py。 -
可选地,可以使用
Scripts文件夹中的wfastcgi.py文件。设置如下:C:\Python36\python.exe|C:\Python36\Scripts\wfastcgi.py。 -
点击请求限制,并取消选中仅当请求映射到时调用处理器的复选框(如果已选中)。点击确定。
-
在添加模块映射界面中点击确定。
-
在确认对话框中点击是。
根服务器设置和环境变量
前往根服务器设置,并点击 FastCGI 设置图标。双击与上一节中添加的路径匹配的参数。将打开编辑 FastCGI 应用程序界面。
-
点击环境变量(集合)条目。将出现省略号 (...)。双击省略号以编辑环境变量。
-
点击添加按钮以添加一个新变量。
-
将
PYTHONPATH添加到名称条目中。 -
将网站代码的路径(例如
C:\website\)添加到值条目中。 -
点击添加按钮以添加第二个变量。
-
将
WSGI_HANDLER添加到名称条目中。 -
如果网站由名为
app.py的文件控制,将app.app添加到值条目中(将.py替换为.app)。 -
一旦添加了变量,请点击确定。在编辑 FastCGI 应用程序时也请点击确定。
网站现在应该已经上线。使用浏览器导航到 REST 端点以确认网站按预期加载。
摘要
使用 Python 网络框架创建具有 REST 规范的 API 很简单。Flask 使得协调 URL 端点与请求方法和响应类型变得简单。凭借内置的 JSON 功能,以及使用 SQLAlchemy 和 GeoAlchemy2 ORM,Flask 是创建地理空间 REST API 的完美框架。
在下一章中,我们将介绍使用 CARTOframes 模块进行地理空间数据的云可视化。
第十四章:云地理数据库分析和可视化
本章将介绍 CARTOframes,这是一个由位置智能软件公司 CARTO 于 2017 年 11 月发布的 Python 包。它提供了一个 Python 接口来处理 CARTO 栈,使 CARTO 地图、分析和数据服务能够集成到数据科学工作流程中。
本章将涵盖以下主题:
-
CARTOframes Python 库的详细信息
-
熟悉 CARTO 栈以及 CARTOframes 如何与它的不同部分交互
-
如何安装 CARTOframes、其包依赖项和文档
-
CARTOframes 的不同包依赖项
-
如何获取 CARTO API 密钥
-
设置 CARTO Builder 账户
-
虚拟环境
-
使用 Jupyter Notebook
-
安装 GeoPandas
考虑到数据科学家,CARTOframes 是一种数据科学工具,它将 CARTO 的 SaaS 提供和 Web 地图工具与 Python 数据科学工作流程相结合。由 CARTO(www.carto.com)于 2017 年末发布,可通过 GitHub 和 Python 包索引(PyPI)存储库下载。
该包可以被视为一种将 CARTO 元素与数据科学工作流程集成的方式,使用 Jupyter Notebooks 作为工作环境。这不仅使其对数据科学家具有吸引力,还允许您通过 Jupyter Notebooks 保存和分发代码和工作流程。这些数据科学工作流程可以通过使用 CARTO 的服务进行扩展,例如托管、动态或静态地图和来自 CARTO 数据观测站的数据集——所有这些都可以通过 CARTO 的云平台获得。该平台通过 API 密钥访问,当在 Jupyter Notebook 中使用 CARTOframes 时需要使用该密钥。我们将简要描述如何获取 API 密钥以及如何安装 CARTOframes 包。
该包提供了读取和写入不同类型空间数据的功能。例如,您可以将 pandas 数据框写入 CARTO 表,也可以将 CARTO 表和查询读入 pandas 数据框。CARTOframes 包将 CARTO 的外部数据位置数据服务引入 Jupyter Notebook,例如位置数据服务、基于云的数据存储、CARTOColors(一套基于地图上颜色使用标准的自定义调色板)、PostGIS 和动画地图。
使用 CARTOframes 的一个很好的理由是其绘图功能。它是对 GeoPandas、matplotlib、Folio 和 GeoNotebook 等其他地图绘图包的良好替代品。所有这些包都有其优点和缺点。例如,matplotlib 是一个不易学习的包,制作基本地图需要大量代码。而 CARTOframes 则不是这样,其结果看起来令人印象深刻,尤其是在使用颜色、结合动态图像(时间流逝)和易于阅读、编写、查询、绘图和删除数据的命令方面。
如何安装 CARTOframes
CARTOframes 库的最佳安装方式是通过启动 Anaconda Navigator 并创建一个新的环境。从那里,你可以打开一个终端并使用 pip install,这将为你安装库。这是目前安装它的唯一方式(目前还没有 conda 支持)。使用以下命令:
>>pip install cartoframes
其他资源
CARTOframes 文档可以在以下网址找到:CARTOframes.readthedocs.io/en/latest/.
CARTOframes 的当前版本是 0.5.5。CARTOframes 的 PyPi 仓库可以通过以下网址访问:pypi.python.org/pypi/CARTOframes。
此外,还有一个包含额外信息的 GitHub 仓库,作为许多 CARTO GitHub 仓库之一:github.com/CARTODB/CARTOframes.
Jupyter Notebooks
建议在 Jupyter Notebooks 中使用 CARTOframes。在本章后面的示例脚本中,我们将使用 CARTOframes 包与其他地理空间包一起使用,因此你可能希望与 GeoPandas 一起在虚拟环境中安装它,这样你就可以访问其依赖项。请参考 第二章 中 GeoPandas 和其他库的安装指南,地理空间代码库简介。你可以在单独的 Python 环境中使用以下命令安装 Jupyter Notebook 应用程序,在终端窗口中:
>>pip install jupyter
CARTO API 密钥
安装 CARTOframes 后,我们需要创建一个 CARTO API 密钥,以便能够使用库中的功能。该库与 CARTO 基础设施交互,类似于第九章 ArcGIS API for Python 和 ArcGIS Online 中的 ArcGIS API for Python。API 密钥可用于将数据帧写入账户、从私有表中读取以及将数据可视化在地图上。CARTO 为教育和非营利用途等提供了 API 密钥。如果你是学生,可以通过注册 GitHub 的学生开发者包来获取 API 密钥:education.github.com/pack.
另一个选择是成为 CARTO 使节:
carto.com/community/ambassadors/.
包依赖项
CARTOframes 依赖于一些 Python 库,一旦运行 pip 安装命令,这些库就会自动安装。以下 Python 库会被安装:
-
ipython:提供用于交互式使用 Python 的丰富工具包 -
appdirs:一个用于确定适当平台特定目录的小型 Python 模块 -
carto:提供围绕 CARTO API 的 SDK -
chardet:Python 2 和 3 的通用编码检测器 -
colorama:在 MS Windows 中启用彩色终端文本和光标定位 -
decorator:在 Python 各个版本中一致地保留装饰函数的签名 -
future:提供 Python 2 和 Python 3 之间的兼容层 -
idna:为应用程序中的 国际化域名(IDNA)提供支持 -
ipython-genutils:来自 IPython 的残余实用工具 -
jedi:一个用于文本编辑器的 Python 自动完成工具 -
numpy:执行数字、字符串、记录和对象的数组处理 -
pandas:提供强大的数据分析、时间序列和统计数据结构 -
parso:一个支持不同 Python 版本错误恢复的 Python 解析器,以及更多 -
pickleshare:一个具有并发支持的类似 shelve 的小型数据存储库 -
prompt-toolkit:一个用于在 Python 中构建强大交互式命令行的库 -
pygments:一个用 Python 编写的语法高亮包 -
pyrestcli:一个通用的 Python REST 客户端 -
python-dateutil:为标准的 Pythondatetime模块提供扩展 -
pytz:提供现代和历史的世界时区定义 -
requests:一个 HTTP 请求库 -
simplegeneric:让您定义简单的单分派泛型函数 -
six:一个 Python 2 和 3 兼容库 -
tqdm:提供快速、可扩展的进度条 -
traitlets:一个用于 Python 应用程序的配置系统 -
urllib3:一个具有线程安全连接池、文件上传等功能的 HTTP 库 -
wcwidth:测量宽字符代码的终端列单元格数 -
webcolors:一个用于处理 HTML 和 CSS 定义的名称和颜色值格式的库
CARTO 数据观测站
您可以通过使用 CARTO 数据观测站来增强 CARTOframes 库,CARTO 数据观测站是 CARTO 提供的在线数据服务。它提供三件事——即插即用的位置数据、访问分析数据方法的目录,以及基于快速 API 构建位置智能应用的机会。这个数据服务是在这样的想法下创建的,即网络上的数据必须是可搜索的,因此需要良好的标签。为了能够找到这些数据,提供数据上下文,并使用它进行空间分析,这是这个服务所能实现的。
CARTO 数据观测站对 CARTO 企业用户可用,这需要付费订阅。在本章中,我们不会介绍这个选项,但在这里提及它,以便让您了解 CARTOframes 库可以实现的功能。
注册 CARTO 账户
要使用 CARTOframes 并与 CARTO 提供的基于云的 PostGIS 数据库服务中存储的数据交互,您需要注册一个 CARTO 账户。虽然提供免费账户,但存储容量有限,且对现有数据资源的访问有限,要使用 CARTOframes,您需要有一个付费账户,因为这些账户提供了 API 密钥。API 密钥将由 CARTOframes 用于识别账户,每个数据请求都将发送到用户的云地理数据库。
CARTO 的免费试用
通过注册,账户最初是一个带有访问所有 CARTO 功能的付费账户。付费账户提供免费 30 天试用期,可用于评估目的。请访问网站carto.com/signup并创建账户:

一旦创建账户,30 天的试用期就开始了。这将允许您将数据添加到云数据库,或从 CARTO 库访问公开数据。它还允许您轻松发布地图。点击 NEW MAP 按钮开始:

添加数据集
使用 DATA LIBRARY 标签页,将波特兰建筑足迹添加到地图中。从列表中选择数据集,然后点击 Create Map。数据集将被添加到账户数据集标签页和称为 Builder 的地图创建界面中:

数据集被添加为地图的一层。可以在地图编辑器中操纵层的所有方面,包括层的颜色、显示的属性、弹出窗口等。也可以调整底图。
可以添加表示属性实时数据的控件。我已经将来自数据库的 US Census Tracts 层添加到地图中,并添加了一个显示所选属性字段值的图形控件。此图形是动态的,并将根据地图窗口中显示的具体人口普查区调整显示的值:

查看 Builder 中的其他标签页,包括 DATA(数据)、ANALYSIS(分析)、STYLE(样式)、POP-UP(弹出)和 LEGEND(图例),以进一步自定义地图。有许多调整和小部件可以使数据交互式。地图也可以设置为公开或私有,并且可以通过点击 PUBLISH 按钮发布到网络上。CARTO 的编辑器和数据导入界面使得创建和共享地图变得非常容易。
API 密钥
要使用 CARTOframes 连接到 CARTO 账户,需要一个 API 密钥。要访问它,请转到账户仪表板,点击右上角的图片,然后从下拉菜单中选择 Your API keys 链接:

API 密钥是一长串用于确保我们将编写的脚本可以访问账户及其相关数据集的文本。当编写脚本时,复制密钥文本并将其作为 Python 字符串分配给脚本中的变量:

添加数据集
有一种方便的方法可以将您电脑上的数据添加到账户中。然而,当添加 shapefiles 时,构成 shapefile 的所有数据文件都必须在一个 ZIP 文件中。我们将从第十一章,Flask 和 GeoAlchemy2,将 NBA 场馆 shapefile 作为 ZIP 文件添加到账户中。点击仪表板数据集区域中的“新建数据集”按钮:

一旦按下“新建数据集”按钮,并出现“连接数据集”界面,点击“浏览”并导航到压缩文件以上传它:

上传过程完成后,数据将被分配一个 URL,并可以使用 Builder 进行编辑。也可以使用 CARTOframes 进行编辑。
现在账户已经设置好,并且已经从本地文件以及数据库中添加了一个数据集,我们需要在我们的本地机器上设置 Python 环境,以便能够连接到账户中存储的数据。
虚拟环境
为了管理 CARTOframes 和其他相关 Python 3 模块的安装,我们将使用虚拟环境包 virtualenv。这个 Python 模块使得在同一台计算机上设置完全独立的 Python 安装变得非常容易。使用 virtualenv,会创建一个 Python 的副本,当激活后,所有安装的模块都将与主 Python 安装分开(换句话说,虚拟环境中安装的模块不会添加到主 site-packages 文件夹中)。这大大减少了包管理的麻烦。
安装 virtualenv
使用 PyPI 中的 pip 安装 virtualenv 包非常简单 (pypi.org):
pip install virtualenv
此命令将添加 virtualenv 及其支持模块。请确保主 Python 安装已添加到 Windows 环境变量中的路径,以便可以从命令行调用 virtualenv。
运行 virtualenv
要创建虚拟环境,打开命令行并输入以下命令结构,virtualenv {环境名称}. 在这个例子中,环境的名称是 cartoenv:

在创建 virtualenv 的文件夹内,会生成一系列文件夹,包含支持 Python 所需的代码文件。还有一个 Lib 文件夹,其中包含将存放所有在此 Python 虚拟版本中安装的模块的 site-packages 文件夹:

激活虚拟环境
要从命令行开始使用新的虚拟环境,在虚拟环境所在的文件夹内传递以下参数。这将运行 activate 批处理文件,并启动虚拟环境:
C:\PythonGeospatial3>cartoenv\Scripts\activate
一旦激活了虚拟环境,环境名称将出现在文件夹名称之前,表示命令是在环境中运行的,并且任何执行的操作(如安装模块)都不会影响主 Python 安装:
(cartoenv) C:\PythonGeospatial3>
在 Linux 环境中,使用命令 source {environment}/bin/activate 代替。在 Linux 中编程时,终端中的命令看起来会是这样:
silas@ubuntu16:~$ mkdir carto
silas@ubuntu16:~$ cd carto/
silas@ubuntu16:~/carto$ virtualenv cartoenv
New python executable in /home/silas/carto/cartoenv/bin/python
Installing setuptools, pip, wheel...done.
silas@ubuntu16:~/carto$ source cartoenv/bin/activate
(cartoenv) silas@ubuntu16:~/carto$
在任何操作系统上,要退出虚拟环境,输入 deactivate 命令。这将结束虚拟会话:
C:\PythonGeospatial3>cartoenv\Scripts\activate
(cartoenv) C:\PythonGeospatial3>deactivate
C:\PythonGeospatial3>
在 virtualenv 中安装模块
由于每个虚拟环境都与主 Python 安装分开,每个环境都必须安装所需的模块。虽然这可能会让人感到烦恼,但 pip 使得这个过程变得相当简单。在设置第一个虚拟环境之后,一个名为 freeze 的 pip 命令可以让你生成一个名为 requirements.txt 的文件。这个文件可以被复制到一个新的虚拟环境中,并使用 pip install 命令,所有列出的模块都将从 PyPI 添加。
要在当前文件夹中生成 requirements.txt 文件,使用以下命令:
(cartoenv) C:\Packt\Chapters>pip freeze > requirements.txt
在文件被复制到新的虚拟环境文件夹之后,激活环境并输入以下命令进行读取:
(newenv) C:\Packt\Chapters>pip install -r requirements.txt
要使用的模块
对于这个虚拟环境,我们将安装 CARTOframes 和 Jupyter 这两个模块。第二个模块将允许我们运行 Jupyter Notebook,这是一种基于浏览器的专业编码环境。
激活虚拟环境,并使用以下命令在虚拟环境中安装模块:
(cartoenv) C:\Packt\Chapters>pip install cartoframes
(cartoenv) C:\Packt\Chapters>pip install jupyter
所需的所有模块也将被下载并安装,包括我们直接安装的两个模块。使用 pip 和 virtualenv 使得包安装和管理变得简单快捷。
使用 Jupyter Notebook
我们已经在 第一章,包安装和管理 以及前一章的多个实例中介绍了 Jupyter Notebook 的基本安装,以运行代码并获得所需的输出。
在这里,我们将使用 Jupyter Notebook 为 CARTOframes 连接到账户并分析地理空间数据,并将其显示出来。
连接到账户
在第一个代码框中,我们将导入 CARTOframes 模块,并传递 API 密钥字符串以及从你的 CARTO 用户名生成的基 URL,即 https://{username}.carto.com。在这种情况下,URL 是 https://lokiintelligent.carto.com:

在这个代码块中,API 密钥和 URL 被传递给 CartoContext 类,并返回一个 CartoContext 连接对象,将其分配给变量 cc。有了这个对象,我们现在可以与我们的账户关联的数据集进行交互,将数据集加载到账户中,甚至可以直接在 Jupyter Notebook 中生成地图。
一旦代码被输入到部分,点击运行按钮以执行当前部分的代码。任何输出都将出现在代码运行的下方 Out 部分。这部分可以包括地图、表格,甚至图表——Jupyter Notebooks 经常用于科学计算,因为它们能够即时生成图表并在 Notebook 中保存它们。
保存凭据
可以使用Credentials库保存和稍后访问 CARTO 账户凭据:
from cartoframes import Credentials
creds = Credentials(username='{username}', key='{password}')
creds.save()
访问数据集
要访问我们已加载到账户中的 NBA 场馆数据集,我们将使用CartoContext的read方法,将我们想要交互的数据集名称作为字符串传递。在 Jupyter Notebook 部分,运行以下代码:
import cartoframes
APIKEY = "{YOUR API KEY}"
cc = cartoframes.CartoContext(base_url='https://{username}.carto.com/', api_key=APIKEY)
df = cc.read('arenas_nba')
print(df)
使用CartoContext,访问账户。使用cc对象,read方法从 NBA arenas数据集创建一个DataFrame对象。DataFrame对象是查询或更新的对象。
print语句将生成一个包含已加载到 CARTOframe 对象中的 NBA arenas数据集值的表格:

可以使用点符号(例如,df.address1)或使用键(例如,df['address1'])访问单个列:

选择单个行
要选择来自 CARTO 账户数据集的 Pandas 数据框中的特定行,可以将条件语句传递到括号中的对象。在这里,通过传递一个 NBA 球队的名称作为参数,查询 NBA arenas数据集的球队列:
df[df.team=='Toronto Raptors']
加载 CSV 数据集
要使用 CARTOframes 将数据集加载到账户中,我们再次使用pandas库,该库与 Jupyter 模块一起安装。Pandas 允许我们从 CSV(以及其他文件格式)中读取数据,将其加载到 Pandas 数据框(一个特殊的数据对象,允许进行多种数据操作,并生成输出)。然后,使用CartoContext,数据框(作为一个表)被写入账户:
import pandas as pd
APIKEY = "{YOUR API KEY}"
cc = cartoframes.CartoContext(base_url='https://{username}.carto.com/', api_key=APIKEY)
df = pd.read_csv(r'Path\to\sacramento.csv')
cc.write(df, 'sacramento_addresses')
这将把作为数据框导入的 CSV 表格写入 CARTO 账户的 DATASETS 部分:

导入的数据集将不会是一个地理空间表,而是一个可以查询和与空间数据连接的表。
加载 shapefile
手动将地理空间数据加载到 CARTO 中非常简单,正如我们之前所探讨的。当使用 CARTOframes 时,它甚至更容易,因为它使得自动化数据管理成为可能。新的、更新的数据文件或来自 REST API 的数据可以转换为数据框并写入 CARTO 账户。
Shapefiles 需要安装 GeoPandas 库,因为几何形状需要 GeoPandas DataFrame对象进行数据管理。
安装 GeoPandas
如 第五章 所述,GeoPandas 是 Pandas 的地理空间补充。为了能够从 shapefiles 创建数据框对象,我们必须确保 GeoPandas 已安装并添加到虚拟环境中。使用 pip install 添加 GeoPandas 库:
(cartoenv) C:\PythonGeospatial3>pip install geopandas
如果在 Windows 上存在安装问题,GeoPandas 和 Fiona(GeoPandas 的动力)的预构建二进制文件在此处可用,还有许多其他 Python 库:www.lfd.uci.edu/~gohlke/pythonlibs。通过下载它们,将它们复制到一个文件夹中,并使用 pip install 从 wheel 安装 Fiona 和 GeoPandas。例如,在这里,Fiona 是从 wheel 文件安装的:
C:\PythonGeospatial3>pip install Fiona-1.7.11.post1-cp36-cp36m-win_amd64.whl
写入 CARTO
将 shapefile 写入 CARTO 账户只需要一个 CartoContext 对象、一个文件路径以及通常的 URL 和 API 密钥组合。现在 GeoPandas 已安装,MLB Stadiums shapefile 可以被加载到 GeoPandas DataFrame 中,然后使用 CartoContext 的 write 方法写入 CARTO 账户:
import geopandas as gdp
import cartoframes
APIKEY = "{API KEY}"
cc = cartoframes.CartoContext(base_url='https://{username}.carto.com/',
api_key=APIKEY)
shp = r"C:\Data\Stadiums_MLB\Stadiums_MLB.shp"
data = gdp.read_file(shp)
cc.write(data,"stadiums_mlb")
登录 CARTO 账户以确认数据集已被添加。
加载带有几何形状的 CSV
为了确保具有纬度和经度列的表(在这种情况下是从 OpenAddresses 导入的地址数据)作为地理空间数据集导入,我们必须使用 Shapely 库的 Point 类。每个 Point 几何对象都是从已导入的地址数据集的 LON 和 LAT 字段生成的:
import geopandas as gdp
import cartoframes
import pandas as pd
from shapely.geometry import Point
APIKEY = "{API KEY}"
cc = cartoframes.CartoContext(base_url='https://{username}.carto.com/',
api_key=APIKEY)
address_df = pd.read_csv(r'data/city_of_juneau.csv')
geometry = [Point(xy) for xy in zip(address_df.LON, address_df.LAT)]
address_df = address_df.drop(['LON', 'LAT'], axis=1)
crs = {'init': 'epsg:4326'}
geo_df = gdp.GeoDataFrame(address_df, crs=crs, geometry=geometry)
cc.write(geo_df, 'juneau_addresses')
确保在导入 CARTOframes 之前导入 GeoPandas 库,以避免从 Fiona 库中导入错误。
地理空间分析
要执行地理空间分析,使用云数据集,我们可以通过 CARTOframes 连接并使用 GeoPandas 和 Shapely 的组合执行空间查询。在这个示例中,NBA arenas 数据集使用相交空间查询与 US States shapefile 进行比较。如果 arena 对象与州对象相交,则打印出 arena 和州的名称:
import geopandas as gdp
import cartoframes
import pandas as pd
APIKEY = "1353407a098fef50ec1b6324c437d6d52617b890"
cc = cartoframes.CartoContext(base_url='https://lokiintelligent.carto.com/',
api_key=APIKEY)
from shapely.geometry import Point
from shapely.wkb import loads
arenas_df = cc.read('arenas_nba')
shp = r"C:\Data\US_States\US_States.shp"
states_df = gdp.read_file(shp)
for index, orig in states_df.iterrows():
for index2, ref in arenas_df.iterrows():
if loads(ref['the_geom'], hex=True).intersects(orig['geometry']):
print(orig['STATE'], ref['team'])
编辑和更新数据集
因为 CARTOframes 集成了 Pandas 的数据框对象,这些对象可以在内存中进行编辑,并将数据写入存储在 CARTO 账户中的数据集,我们可以创建脚本来自动上传地理空间数据。数据集可以完全更新,或者可以使用 Pandas 的数据方法(如 replace)更新单个行和值。这,加上 Builder,CARTO 网络地图部署工具,使得创建具有网络地图前端和云数据存储的 GIS 变得容易,这些数据可以通过脚本进行管理。
在这个示例代码中,使用 intersect 查询找到包含 NBA arena 的州的名字。这些名字被添加到一个列表中,然后这个列表被添加到 arena 数据框中作为名为 states 的新列。存储在 arenas 数据集中的几何数据需要使用 loads 模块转换为 Shapely 对象:
import geopandas as gdp
import cartoframes
import pandas as pd
from shapely.wkb import loads
APIKEY = "API KEY"
cc = cartoframes.CartoContext(base_url='https://{username}.carto.com/',
api_key=APIKEY)
arenas_df = cc.read('arenas_nba')
shp = r"C:\Data\US_States\US_States.shp"
states_df = gdp.read_file(shp)
data = []
for index, ref in arenas_df.iterrows():
check = 0
for index2, orig in states_df.iterrows():
if loads(ref['the_geom'], hex=True).intersects(orig['geometry']):
data.append(orig['STATE'])
check = 1
if check == 0:
data.append(None)
arenas_df['state'] = data
cc.write(arenas_df,'arenas_nba', overwrite=True)
overwrite=True
随着数据集的每次更新,必须将更改写入 CARTO 账户。要使用新数据覆盖云数据库中的数据,必须将 overwrite 参数设置为 True:
cc.write(data,"stadiums_mlb",'overwrite=True')
创建地图
由于 Jupyter Notebooks 的交互性,代码和代码输出是同时存在的。当处理地理空间数据时,这非常棒,因为它使得创建数据地图变得容易。在这个例子中,NBA arenas 和 MLB 场馆数据集被添加到一个 BaseMap 对象上的地图中:
from cartoframes import Layer, BaseMap, styling
cc.map(layers=[BaseMap('light'),
Layer('arenas_nba',),
Layer('stadiums_mlb')], interactive=True)
生成的输出如下:

摘要
本章涵盖了以下主题。首先,我们介绍了 CARTOframes Python 库,并讨论了它与 CARTO 堆栈的其他部分(如 CARTO Builder 和 CARTO Data Observatory)的关系。接下来,我们解释了如何安装 CARTOframes 库,它依赖于哪些其他 Python 包,以及在哪里查找文档。由于 CARTOframes 使用来自 CARTO Builder 的数据,我们解释了如何设置 CARTO Builder 账户。在构成本章其余部分的示例脚本中,我们看到了如何将库与 pandas 数据框集成,如何处理表格,以及如何制作地图并将它们与其他地理空间库(如 Shapely 和 GeoPandas)结合。
在下一章中,我们将介绍另一个利用 Jupyter Notebooks 和地图视觉化的模块,MapboxGL—Jupyter。
第十五章:自动化云制图
Mapbox 已成为移动地图和数据可视化的同义词。除了被应用程序开发者和制图师采用的底图样式工具集外,他们还在生产用 Python 和 JavaScript 编写的有趣的制图工具。
将这两种有用的语言结合到一个包中,Mapbox 最近发布了新的 MapboxGL—Jupyter Python 模块。这个新模块允许在 Jupyter Notebook 环境中即时创建数据可视化。与允许 API 访问账户服务的 Mapbox Python SDK 模块一起,Python 使得向企业地理空间应用程序添加 Mapbox 工具和服务变得容易。
本章我们将学习:
-
如何创建 Mapbox 账户以生成访问令牌
-
如何设计自定义底图样式
-
对云数据和底图的读写访问
-
如何创建分级圆可视化
-
如何创建分级圆可视化
所有制图相关内容
Mapbox 成立于 2010 年,由 Eric Gunderson 创立,迅速扩张并超越了初创公司的根基,成为制图复兴的领导者。他们的 MapboxGL JavaScript API 是一个用于创建交互式网络地图和数据可视化的有用库。他们向地理空间社区贡献了多个开放制图规范,包括矢量瓦片。
Mapbox 专注于为地图和应用程序开发者提供定制底图瓦片,将自己定位为领先的网络地图和移动应用软件公司。本章中使用的两个 Python 模块允许 GIS 管理人员和开发者将他们的服务和工具集成到企业地理信息系统生态中。
如何将 Mapbox 集成到您的 GIS 中
通过他们的 JavaScript 库和新的 MapboxGL—Jupyter Python 模块,Mapbox 工具的使用比以往任何时候都更加容易。地理空间开发者和程序员可以将他们的工具集成到现有的 GIS 工作流程中,或者创建利用 Mapbox 提供的套件的新地图和应用程序。
与 CARTO 类似,Mapbox 允许基于账户的云数据存储。然而,他们的重点更少在分析工具上,更多在制图工具上。对于大小不同的制图团队,使用 Mapbox 工具可以降低创建和支持自定义底图以用于交互式网络地图的成本,并且与其他地图瓦片选项(如 Google Maps API)相比,可以节省更多。
Mapbox Studio 使得创建具有制图外观和感觉的地图变得容易,可以与公司或部门的品牌相匹配。底图可以使用现有样式构建,并叠加您的组织层,或者设计一个全新的底图。它甚至允许基于被拖入工作室的图像进行样式设计,根据从图像像素生成的直方图为要素分配颜色。
Mapbox 工具
Mapbox 采用了地理空间领域的领导者(如 Mapbox 开源负责人 Sean Gillies,Shapely、Fiona 和 Rasterio 的重要开发者),为开源许可下的分析和地图 Python 库做出了贡献。他们新的 MapboxGL—Jupyter 库代表了一种利用其工具套件的新方法,结合其他 Python 模块(如 Pandas/GeoPandas)和多种数据类型(如 GeoJSON、CSVs 以及甚至 shapefiles)。
除了新的 Python 模块外,Mapbox 的开源工具还包括基于 Web 图形库(WebGL)的 MapboxGL JavaScript 库,以及 Mapbox Python SDK。
MapboxGL.js
MapboxGL 是建立在著名的 JavaScript 地图库 Leaflet.js 之上的。Leaflet 于 2011 年发布,支持各种知名的 Web 地图应用,包括 Foursquare、Craigslist 和 Pinterest。Leaflet 的开发者 Vladimir Agafonkin 自 2013 年以来一直在 Mapbox 工作。
基于 Leaflet 开发工作的原始努力,MapboxGL.js 集成了 WebGL 库,利用 HTML 5 的 canvas 标签支持无需插件的 Web 图形。MapboxGL.js 支持矢量瓦片,以及平滑缩放和平移的 3D 环境。它支持 GeoJSON 叠加以及标记和形状。可以使用包括点击、缩放和平移在内的事件来触发数据处理函数,使其非常适合交互式网络地图应用。
Mapbox Python SDK
Mapbox Python SDK 用于访问大多数 Mapbox 服务,包括路线、地理编码、分析和数据集。对支持数据编辑和上传、行政管理和基于位置查询的云服务进行低级访问,允许与本地 GIS 进行企业集成和扩展。
安装 Python SDK
使用 pip 安装 Python SDK,允许访问 Mapbox 服务 API。此模块不是使用 MapboxGL—Jupyter 工具所必需的,但对于上传和查询很有用:
C:\Python3Geospatial>pip install mapbox
在此处下载 Mapbox Python SDK:
github.com/mapbox/mapbox-sdk-py.
开始使用 Mapbox
要开始使用 Mapbox 工具和 Mapbox Studio,您需要注册一个账户。这将允许您生成 API 密钥,这些密钥对于将 Mapbox 底图瓦片添加到网络地图以及创建区分您地图的定制底图是必需的。有了这个账户,您还可以将数据加载到云中,以便在您的地图中使用。
注册 Mapbox 账户
要使用 Mapbox 工具和底图,您必须注册一个账户。这是一个简单的过程,需要提供用户名、电子邮件和密码:

注册后,您将被带到账户仪表板,在那里可以生成 API 访问令牌并访问 Mapbox Studio。仪表板还显示了您的账户统计信息,包括对各种服务(如路线、地理编码和数据集)的 API 调用次数。
创建 API 令牌
新账户附带账户仪表板,默认提供 API 访问令牌。这个公开访问令牌或密钥以 pk 开头,是一长串字符。此 API 访问令牌用于验证使用此账户构建的所有地图和应用程序。复制字符串并将其添加到您的地图中:

要创建新的 API 访问令牌,请点击“创建令牌”按钮并选择它将允许的访问级别:

在 JavaScript 代码中,API 访问令牌被传递给 MapboxGL 对象以启用对瓦片和工具的访问。以下是一个简单的使用 HTML/JavaScript 的 Web 地图示例,展示了如何使用访问令牌创建地图。请将以下代码中提到的访问令牌替换为您自己的公开访问令牌:
<html><head>
<script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.44.1/mapbox-gl.js'></script>
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.44.1/mapbox-gl.css' rel='stylesheet' />
</head><body>
<div id='map' style='width: 400px; height: 300px;'></div>
<script>
mapboxgl.accessToken = 'pk.eyJ1IjoibG9raXByZXNpZGVud0.8S8l9kH4Ws_ES_ZCjw2i8A';
var map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v9'
});
</script></body></html>
将此代码保存为"index.html",然后使用浏览器打开以查看简单的地图。请确保将早期示例中的 API 访问令牌替换为您自己的密钥,否则地图将不会显示。
查阅文档了解 API 访问令牌的各种配置:
www.mapbox.com/help/how-access-tokens-work/.
将数据添加到 Mapbox 账户
Mapbox 支持使用您自己的数据。您不仅可以样式化底图瓦片,甚至可以将自己的数据添加到瓦片中,使其对客户或用户更加相关。这可以使用 Mapbox Python SDK 和上传以及数据集 API 进行程序化管理。
要上传数据,您必须创建一个秘密 API 访问令牌。这些是通过之前详细说明的创建令牌过程创建的,但包括秘密范围。选择以下范围以允许数据集和瓦片集的读写能力:
-
DATASETS:WRITE
-
UPLOADS:READ
-
UPLOADS:WRITE
-
TILESETS:READ
-
TILESETS:WRITE
在这里了解更多关于将数据加载到您的 Mapbox 账户的信息:
www.mapbox.com/help/how-uploads-work/.
瓦片集
瓦片集是经过分块处理的栅格数据,用于创建可滑动地图,允许它们叠加在底图上。它们可以从矢量数据生成,以使用您自己的数据创建自定义底图。使用 Mapbox Python SDK 中的Uploader类,可以将 GeoJSON 文件和 shapefile 以瓦片集的形式程序化地加载到您的云账户中。
在这里了解更多关于瓦片集的信息:
www.mapbox.com/api-documentation/#tilesets.
数据集
数据集是 GeoJSON 层,可以比瓦片集更频繁地编辑。虽然您可以使用账户仪表板上传数据集,但要加载大于 5 MB 的数据集,您必须使用数据集 API。
在此处了解更多关于数据集的信息:
www.mapbox.com/api-documentation/#datasets.
示例 - 上传 GeoJSON 数据集
mapbox 模块有一个 Datasets 类,用于在账户中创建和填充数据集。此演示代码将从邮编 GeoJSON 文件中读取,并将一个邮编 GeoJSON 对象加载到新的数据集中。将秘密访问令牌传递给 Datasets 类:
from mapbox import Datasets
import json
datasets = Datasets(access_token='{secrettoken}')
create_resp = datasets.create(name="Bay Area Zips",
description = "ZTCA zones for the Bay Area")
listing_resp = datasets.list()
dataset_id = [ds['id'] for ds in listing_resp.json()][0]
data = json.load(open(r'ztca_bayarea.geojson'))
for count,feature in enumerate(data['features'][:1]):
resp = datasets.update_feature(dataset_id, count, feature)
这将向图层添加一个邮编,可以从账户仪表板中查看:

示例 - 将数据作为瓦片集上传
可以将瓦片集添加到自定义底图样式,这使得快速加载数据层成为可能。此演示代码使用具有读写能力的秘密令牌,通过 Mapbox Python SDK 将 GeoJSON 文件作为瓦片集上传:
token = 'sk.eyJ1IjoibG9oxZGdqIn0.Y-qlJfzFzr3MGkOPPbtZ5g' #example secret token
from mapbox import Uploader
import uuid
set_id = uuid.uuid4().hex
service = Uploader(access_token=token)
with open('ztca_bayarea.geojson', 'rb') as src:
response = service.upload(src, set_id)
print(response)
如果返回的响应是 201 响应,则上传成功。
在此处了解更多关于上传 API 的信息:
www.mapbox.com/api-documentation/?language=Python.
Mapbox Studio
创建自定义底图可能对经验丰富的制图员来说也是一个耗时过程。为了帮助简化这个过程,Mapbox 工程师使用了 Open Street Map (OSM)数据来生成预构建的自定义底图,这些底图可用于商业和非商业应用。使用 Mapbox Studio,还可以调整这些样式以添加更多自定义细节。此外,可以从头开始构建底图,以创建应用程序的特定外观:

要访问 Mapbox Studio,请登录账户仪表板并点击 Mapbox Studio 链接。在这个 Studio 环境中,您可以管理底图、瓦片集和数据集。
自定义底图
点击新建样式按钮并选择卫星街道主题:

一份快速教程解释了自定义选项。已添加了各种可用的图层,并且可以通过在目录表中点击图层来调整它们的标签和样式。还可以添加新图层,包括账户瓦片集:

可以调整地图缩放级别、方位角、俯仰角和初始坐标。使用地图位置菜单,可以更改这些地图参数,并使用底部的锁定按钮将其锁定为默认位置:

探索其他样式选项,例如标签颜色和图层缩放级别。完成自定义后,通过点击发布样式按钮发布样式。样式 URL 将添加到 MapboxGL 可视化中的这些 Jupyter Notebook 练习或网络地图中。
添加瓦片集
要将您的数据添加到基础地图样式,请点击图层按钮并从可用选项中选择一个瓦片集。之前使用 Mapbox Python SDK 加载的邮编瓦片集应该可用,并可以添加到基础地图并对其进行样式化:

虚拟环境
使用 virtualenv(参见上一章中的安装说明)启动虚拟环境,并使用 pip 安装以下列出的模块。如果您有一个文件夹路径为 C:\Python3Geospatial,virtualenv 将创建一个虚拟环境文件夹,这里称为 mapboxenv,它可以按以下方式激活:
C:\Python3Geospatial>virtualenv mapboxenv
Using base prefix 'c:\\users\\admin\\appdata\\local\\programs\\python\\python36'
New python executable in C:\Python3Geospatial\mapboxenv\python.exe
Installing setuptools, pip, wheel...done.
C:\Python3Geospatial>mapboxenv\Scripts\activate
安装 MapboxGL – Jupyter
您可以通过 pip 从 PyPI.org 仓库安装 MapboxGL—Jupyter 库:
(mapboxenv) C:\Python3Geospatial>pip install mapboxgl
所有支持模块都将与 Mapbox 创建的核心库一起定位和安装。
安装 Jupyter 笔记本
在虚拟环境中安装 Jupyter Notebooks 库:
(mapboxenv) C:\Python3Geospatial>pip install jupyter
安装 Pandas 和 GeoPandas
Pandas 应已安装,因为它是与 GeoPandas 一起安装的,但如果尚未安装,请使用 pip 在 PyPI.org 仓库中查找它:
(mapboxenv) C:\Python3Geospatial>pip install geopandas
如果您在 Windows 计算机上安装这些模块时遇到任何问题,请在此处探索预构建的 wheel 二进制文件(下载后使用 pip 安装它们):
www.lfd.uci.edu/~gohlke/pythonlibs/.
使用 Jupyter Notebook 服务器
启动 Jupyter Notebook 服务器非常简单。当使用虚拟环境时,您需要首先激活环境,然后启动服务器。如果不这样做,请确保 Python 和 Notebook 服务器位置在路径环境变量中。
打开命令提示符并输入 jupyter notebook 以启动服务器:
(mapboxenv) C:\Python3Geospatial>jupyter notebook
服务器将启动并显示其端口号和可以用于重新登录网页浏览器的令牌:

启动服务器将在系统浏览器中打开一个浏览器窗口。服务器地址是 localhost,默认端口是 8888。浏览器将在 http://localhost:8888/tree 打开:

点击新建按钮创建一个新的笔记本。从笔记本部分选择 Python 版本,新的笔记本将在第二个标签页中打开。这个笔记本应该重命名,因为它很快就会变得难以组织未命名的笔记本:

一旦窗口打开,编码环境就处于活动状态。在这个例子中,我们将使用 GeoPandas 导入人口普查区数据,将其转换为点数据,选择特定列,并使用 MapboxGL—Jupyter 进行可视化。
使用 GeoPandas 导入数据
导入所需的模块并将 API 密钥分配给一个变量。以下命令应添加到 Jupyter Notebook 单元中:
import geopandas as gpd
import pandas as pd
import os
from mapboxgl.utils import *
from mapboxgl.viz import *
token = '{user API Key}'
API 密钥也可以分配给 Windows 路径环境变量(例如,"MAPBOX_ACCESS_TOKEN"),并使用os模块调用:
token = os.getenv("MAPBOX_ACCESS_TOKEN")
从多边形创建点数据
旧金山地区的普查区 GeoJSON 文件包含具有多边形geometry的人口数据。为了创建第一个可视化,我们需要将几何类型转换为点:
tracts = gpd.read_file(r'tracts_bayarea.geojson')
tracts['centroids'] = tracts.centroid
tract_points = tracts
tract_points = tract_points.set_geometry('centroids')
tract_points.plot()
之前代码的输出如下:

数据清理
这份数据可视化将比较旧金山地区的男性和女性人口。为了生成圆形可视化,我们可以使用 Geopandas 的数据框操作重命名和删除不必要的列:
tract_points['Total Population'] = tract_points['ACS_15_5YR_S0101_with_ann_Total; Estimate; Total population']
tract_points['Male Population'] = tract_points['ACS_15_5YR_S0101_with_ann_Male; Estimate; Total population']
tract_points['Female Population'] = tract_points['ACS_15_5YR_S0101_with_ann_Female; Estimate; Total population']
tract_points = tract_points[['Total Population',
'Male Population','Female Population',
'centroids' ]]
这段代码通过传递新列的名称并将数据值赋给现有列,从三个现有列创建了三个新列。然后,整个 GeoDataFrame(在内存中)被重写,只包含三个新列和中心点列,消除了不想要的列。探索新的 GeoDataFrame 的前五行,我们可以看到新的数据结构:

将点保存为 GeoJSON
保存新清理的 GeoDataFrame 是将其加载到 Mapbox CircleViz类所必需的。必须指定 GeoJSON 驱动程序,因为默认输出文件格式是 shapefile:
tract_points.to_file('tract_points.geojson',driver="GeoJSON")
将点添加到地图上
要简单地查看地图上的点,我们可以提供一些参数并调用CircleViz对象的show属性:
viz = CircleViz('tract_points.geojson', access_token=token,
radius = 2, center = (-122, 37.75), zoom = 8)
viz.show()
之前的代码将产生以下输出:

要对数据进行分类,我们可以为特定字段设置颜色停止点,传递一个包含相关颜色信息的类断点列表:
color_stops = [
[0.0, 'rgb(255,255,204)'], [500.0, 'rgb(255,237,160)'],
[1000.0, 'rgb(252,78,42)'], [2500.0, 'rgb(227,26,28)'],
[5000.0, 'rgb(189,0,38)'],
[max(tract_points['Total Population']),'rgb(128,0,38)']
]
viz.color_property = 'Total Population'
viz.color_function_type = 'interpolate'
viz.color_stops = color_stops
viz.radius = 1
viz.center = (-122, 37.75)
viz.zoom = 8
viz.show()
输出将看起来像这样:

向tract_points GeoDataFrame 添加一些新字段并重新保存:
tract_points['Percent Male'] = tract_points['Male Population']/tract_points['Total Population']
tract_points['Percent Female'] = tract_points['Female Population']/tract_points['Total Population']
tract_points.to_file("tract_points2.geojson", driver="GeoJSON")
创建渐变色可视化
这段代码将手动为数据的具体部分分配颜色,将数据分为类别。这还将为数据分配特定的半径大小,以便可视化可以通过颜色和圆的大小传达信息:
color_stops = [
[0.0, 'rgb(107,174,214)'], [3000.0, 'rgb(116,196,118)'],
[8000.0, 'rgb(254,153,41)'],
[max(tract_points['Total Population']), 'rgb(222,45,38)'],
]
minmax = [min(tract_points['Percent Male']),
max(tract_points['Percent Male'])]
diff = minmax[1] - minmax[0]
radius_stops = [
[round(minmax[0],2), 4.0],
[round(minmax[0]+(diff/6.0),2), 7.0],
[round(minmax[1]-(diff/2.0),2), 10.0],
[minmax[1], 15.0],]
在设置好这些半径大小和颜色范围后,它们可以应用于新的 GeoJSON 中的两个字段:总人口和男性百分比。对于这个可视化,圆的大小将表示人口的男性百分比,颜色将表示总人口:
vizGrad = GraduatedCircleViz('tract_points2.geojson', access_token=token)
vizGrad.color_function_type = 'interpolate'
vizGrad.color_stops = color_stops
vizGrad.color_property = 'Total Population'
vizGrad.color_default = 'grey'
vizGrad.opacity = 0.75
vizGrad.radius_property = 'Percent Male'
vizGrad.radius_stops = radius_stops
vizGrad.radius_function_type = 'interpolate'
vizGrad.radius_default = 1
vizGrad.center = (-122, 37.75)
vizGrad.zoom = 9
vizGrad.show()
这将生成一个交互式地图,如下所示:

自动设置颜色、大小和断点
与手动设置颜色、半径大小和断点不同,MapboxGL—Jupyter 包含一些实用工具(如 create_color_stops),它们在颜色(或大小)和断点值之间创建匹配。颜色方案通过传递 YlOrRd 关键字(表示 黄橙红)来设置。此外,我们可以通过设置可视化样式为样式 URL 来调整底图,使用另一个预置样式或我们自己的自定义样式:
measure_color = 'Percent Male'
color_breaks = [round(tract_points[measure_color].quantile(q=x*0.1),3) for x in range(1, 11,3)]
color_stops = create_color_stops(color_breaks, colors='YlOrRd')
measure_radius = 'Total Population'
radius_breaks = [round(tract_points[measure_radius].quantile(q=x*0.1),1) for x in range(2, 12,2)]
radius_stops = create_radius_stops(radius_breaks, 5.0, 20)
vizGrad = GraduatedCircleViz('tract_points2.geojson',
access_token=token,
color_property = measure_color,
color_stops = color_stops,
radius_property = measure_radius,
radius_stops = radius_stops,
stroke_color = 'black',
stroke_width = 0.5,
center = (-122, 37.75),
zoom = 9,
opacity=0.75)
vizGrad.style='mapbox://styles/mapbox/dark-v9'
vizGrad.show()
深色底图使得渐变圆可视化更加清晰可见:

在此处探索文档中可用的可视化选项:
github.com/mapbox/mapboxgl-jupyter/blob/master/docs-markdown/viz.md.
在此处探索可用的数据工具:
github.com/mapbox/mapboxgl-jupyter/blob/master/docs-markdown/utils.md.
在此处探索可用的颜色渐变:
github.com/mapbox/mapboxgl-jupyter/blob/master/mapboxgl/colors.py.
创建渐变色地图
使用渐变色地图,我们可以显示一个多边形 GeoJSON 文件。使用 tracts GeoDataFrame,我们将创建另一个具有多边形 geometry 和一个表格字段的 GeoDataFrame,并将其保存为 GeoJSON 文件:
tract_poly = tracts
tract_poly['Male Population'] = tract_poly['ACS_15_5YR_S0101_with_ann_Male; Estimate; Total population']
tract_poly = tract_poly[['Male Population','geometry' ]]
tract_poly.to_file('tracts_bayarea2.geojson', driver="GeoJSON")
可视化是通过 ChoroplethViz 类创建的。底图样式是 MapBox Studio 章节中先前创建的卫星影像样式的 URL:
vizClor = ChoroplethViz('tracts_bayarea2.geojson',
access_token=API_TOKEN,
color_property='Male Population',
color_stops=create_color_stops([0, 2000, 3000,5000,7000, 15000],
colors='YlOrRd'),
color_function_type='interpolate',
line_stroke='-',
line_color='rgb(128,0,38)',
line_width=1,
opacity=0.6,
center=(-122, 37.75),
zoom=9)
vizClor.style='mapbox://styles/lokipresident/cjftywpln22sp9fcpqa8rl'
vizClor.show()
生成的输出如下:

保存地图
要保存渐变色地图,请使用可视化的 create_html 方法:
with open('mpop.html', 'w') as f:
f.write(vizClor.create_html())
要在本地查看保存的 HTML 文件,请打开命令提示符,并在与保存的 HTML 文件相同的文件夹中使用 Python 启动本地 HTTP 服务器。然后,在 http://localhost:8000/mpop.html 打开浏览器查看地图:
C:\Python3Geospatial>python -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
创建热图
使用 HeatmapViz 类从数据生成热图:
measure = 'Female Population'
heatmap_color_stops = create_color_stops([0.01, 0.25, 0.5, 0.75, 1], colors='PuRd')
heatmap_radius_stops = [[0, 3], [14, 100]]
color_breaks = [round(tract_poly[measure].quantile(q=x*0.1), 2) for x in range(2,10)]
color_stops = create_color_stops(color_breaks, colors='Spectral')
heatmap_weight_stops = create_weight_stops(color_breaks)
vizheat = HeatmapViz('tracts_points2.geojson',
access_token=token,
weight_property = "Female Population",
weight_stops = heatmap_weight_stops,
color_stops = heatmap_color_stops,
radius_stops = heatmap_radius_stops,
opacity = 0.8,
center=(-122, 37.78),
zoom=7,
below_layer='waterway-label'
)
vizheat.show()
使用 Mapbox Python SDK 上传数据
使用 MapboxGL—Jupyter 和 Mapbox Python SDK 在账户中存储数据集并将它们与其他表格数据连接起来是可能的。加载 GeoJSON 文件需要仅分配给秘密 API 访问令牌的特定权限。为了确保使用的 API 令牌具有正确的范围,您可能需要生成一个新的 API 令牌。转到您的账户仪表板并生成一个新的令牌,并确保您检查了 入门 Mapbox 部分中显示的上传和数据集的读写能力。
创建数据集
第一步是创建一个数据集,如果您还没有创建,此代码将在账户中生成一个空数据集,该数据集将具有 datasets.create 方法提供的名称和描述:
from mapbox import Datasets
import json
datasets = Datasets(access_token={secrettoken})
create_resp = datasets.create(name="Bay Area Zips",
description = "ZTCA zones for the Bay Area")
将数据加载到数据集中
要将数据加载到新数据集中,我们将遍历包含在邮编 GeoJSON 中的特征,将它们全部写入数据集(而不是像之前演示的那样只写入一个)。由于此文件大于 5MB,必须使用 API 加载,该 API 通过mapbox模块访问。update_feature方法的所有参数都是必需的:数据集的 ID(使用datasets.list方法检索)、行 ID 和feature:
listing_resp = datasets.list()
dataset_id = [ds['id'] for ds in listing_resp.json()][0]
data = json.load(open(r'ztca_bayarea.geojson'))
for count,feature in enumerate(data['features']):
resp = datasets.update_feature(dataset_id, count, feature)
完成后的数据集在 Mapbox Studio 中看起来如下所示:

从数据集中读取数据
要读取数据集中存储的 JSON 数据,请使用read_dataset方法:
datasets.read_dataset(dataset_id).json()
删除行
要从数据集中删除特定行,请将数据集 ID 和行 ID 传递给datasets.delete_feature方法:
resp = datasets.delete_feature(dataset_id, 0)
摘要
在本章中,我们学习了如何使用 MapboxGL—Jupyter 和 Mapbox Python SDK 创建数据可视化,并将数据上传到 Mapbox 账户。我们创建了点数据可视化、面状图、热图和渐变圆可视化。我们学习了如何定制底图样式,如何将其添加到 HTML 地图中,以及如何将自定义瓦片集添加到底图中。我们还学习了如何使用 GeoPandas 将多边形数据转换为点数据,以及如何可视化结果。
在下一章中,我们将探讨使用 Python 模块和 Hadoop 进行地理空间分析的使用。
第十六章:使用 Hadoop 进行 Python 地理处理
本书中的大多数示例都使用单个计算机上的相对较小的数据集进行工作。但随着数据量的增加,数据集甚至单个文件可能会分布在机器集群中。处理大数据需要不同的工具。在本章中,你将学习如何使用 Apache Hadoop 处理大数据,以及 Esri GIS 工具用于在空间上处理大数据。
本章将教你如何:
-
安装 Linux
-
安装和运行 Docker
-
安装和配置 Hadoop 环境
-
在 HDFS 中处理文件
-
使用 Hive 进行基本查询
-
安装 Esri GIS 工具用于 Hadoop
-
在 Hive 中执行空间查询
什么是 Hadoop?
Hadoop 是一个开源框架,用于处理分布在单台计算机到数千台计算机上的大量数据。Hadoop 由四个模块组成:
-
Hadoop 核心组件
-
Hadoop 分布式文件系统(HDFS)
-
另一个资源协商者(YARN)
-
MapReduce
Hadoop 核心组件包括运行其他三个模块所需的组件。HDFS 是一个基于 Java 的文件系统,它被设计成分布式,并且能够在多台机器上存储大量文件。当我们说大量文件时,我们指的是千兆字节。YARN 管理你的 Hadoop 框架中的资源和调度。MapReduce 引擎允许你并行处理数据。
有几个其他项目可以安装以与 Hadoop 框架一起使用。在本章中,你将使用 Hive 和 Ambari。Hive 允许你使用 SQL 读取和写入数据。你将在本章末尾使用 Hive 对你的数据进行空间查询。Ambari 为 Hadoop 和 Hive 提供了一个网页用户界面。在本章中,你将使用它上传文件并输入你的查询。
现在你已经对 Hadoop 有了一个概述,下一节将展示如何设置你的环境。
安装 Hadoop 框架
在本章中,你将不会自己配置 Hadoop 框架的每个组件。你将运行一个 Docker 镜像,这需要你安装 Docker。目前,Docker 在 Windows 10 专业版或企业版上运行,但在 Linux 或 Mac 上运行得更好。Hadoop 也可以在 Windows 上运行,但需要你从源代码构建,因此它将在 Linux 上运行得更容易。此外,你将使用的 Docker 镜像正在运行 Linux,因此熟悉 Linux 可能会有所帮助。在本节中,你将学习如何安装 Linux。
安装 Linux
设置 Hadoop 框架的第一步是安装 Linux。你需要获取一个 Linux 操作系统的副本。Linux 有很多版本。你可以选择你喜欢的任何版本,然而,本章是使用 CentOS 7 编写的,因为大多数你将要安装的工具也已经在 CentOS 上进行了测试。CentOS 是基于 Red Hat 的 Linux 版本。你可以在以下网址下载 ISO:www.centos.org/。选择“立即获取 CentOS”。然后,选择“DVD 图像”。选择一个镜像下载 ISO。
下载镜像后,您可以使用 Windows 将其烧录到磁盘上。一旦烧录完成,将磁盘放入将要运行 Linux 的机器中并启动它。安装过程中会有提示。需要关注的两个步骤是软件选择步骤和分区。在软件选择步骤中,选择 GNOME 桌面。这将提供一个带有流行 GUI 的足够的基础系统。如果您在计算机上还有其他文件系统,您可以在分区屏幕上选择覆盖它或选择分区上的空闲空间。
对于如何安装 Linux 的更详细解释,谷歌是你的朋友。有许多优秀的教程和 YouTube 视频会引导你完成安装过程。不幸的是,看起来 CentOS 网站没有 CentOS 7 的安装手册。
安装 Docker
Docker 提供了软件,以便您可以运行容器。容器是一个包含运行其包含的软件所需所有内容的可执行文件。例如,如果我有一个配置为运行 Hadoop、Hive 和 Ambari 的 Linux 系统,并且我从它创建了一个容器,我可以给你这个容器,当你运行它时,它将包含运行该系统所需的所有内容,无论你的计算机配置或安装了什么软件。如果我把这个容器镜像给任何其他人,它也会始终以相同的方式运行。容器不是虚拟机。虚拟机是硬件层面的抽象,而容器是应用层面的抽象。容器包含运行软件所需的所有内容。对于本章,这就是你需要知道的所有内容。
现在您已经安装了 Linux 并了解了 Docker 是什么,您可以安装 Docker 的一个副本。使用您的终端,输入以下命令:
curl -fsSL https://get.docker.com/ | sh
前面的命令使用 curl 应用程序下载并安装 Docker 的最新版本。参数告诉 curl 在服务器错误发生时静默失败,不显示进度,报告任何错误,并在服务器表示位置已更改时进行重定向。curl 命令的输出通过管道 - | 传递到 sh(Bash shell)以执行。
当 Docker 安装完成后,你可以通过执行以下命令来运行它:
sudo systemctl start docker
上一条命令使用 sudo 以管理员(root)身份运行命令。想象一下在 Windows 中右键点击并选择“以管理员身份运行”选项。下一条命令是 systemctl。这是在 Linux 中启动服务的方式。最后,start docker 正好做了这件事,它启动了 docker。如果您在执行前面提到的命令时收到提及 sudoers 的错误,那么您的用户可能没有权限以 root 身份运行应用程序。您需要以 root 身份登录(或使用 su 命令)并编辑 /etc/sudoers 中的文本文件。请添加以下行:
your username ALL=(ALL) ALL
上一行将授予您使用 sudo 的权限。您的 /etc/sudoers 文件应类似于以下截图:

现在你已经运行了 docker,你可以下载我们将要加载的包含 Hadoop 框架的镜像。
安装 Hortonworks
与安装 Hadoop 和所有其他组件相比,你将使用预配置的 Docker 镜像。Hortonworks 有一个数据平台沙盒,它已经包含了一个可以在 Docker 中加载的容器。要下载它,请访问 hortonworks.com/downloads/#sandbox 并选择“为 Docker 下载”。
你还需要安装 start_sandox_hdp_version.sh 脚本。这将简化在 Docker 中启动容器。你可以从 GitHub 下载脚本:gist.github.com/orendain/8d05c5ac0eecf226a6fed24a79e5d71a.
现在你需要在 Docker 中加载这个镜像。以下命令将展示如何操作:
docker load -i <image name>
之前的命令将镜像加载到 Docker 中。镜像名称将类似于 HDP_2.6.3_docker_10_11_2017.tar,但会根据你的版本而变化。要查看沙盒是否已加载,请运行以下命令:
docker images
如果没有其他容器,输出应该看起来如下截图所示:

为了使用基于 Web 的 GUI Ambari,你将需要一个为沙盒设置的域名。为此,你需要容器的 IP 地址。你可以通过运行两个命令来获取它:
docker ps docker inspect <container ID>
第一个命令将包含 container ID,第二个命令将使用 container ID 返回大量信息,其中 IP 地址位于末尾。或者,你可以利用 Linux 命令行,只需使用以下命令即可获取 IP 地址:
docker inspect $(docker ps --format "{{.ID}}") --format="{{json .NetworkSettings.IPAddress}}"
之前的命令将之前提到的命令包装成一个单独的命令。docker inspect 命令将 docker ps 的输出作为 container ID。它是通过将输出放在 $() 中来实现的,但它还传递了一个过滤器,以便只返回 ID。然后,inspect 命令还包含一个过滤器,只返回 IP 地址。{{}} 之间的文本是一个 Go 模板。此命令的输出应该是一个 IP 地址,例如,172.17.0.2。
现在你已经得到了镜像的 IP 地址,你应该使用以下命令更新你的主机文件:
echo '172.17.0.2 sandbox.hortonworks.com sandbox-hdp.hortonworks.com sandbox-hdf.hortonworks.com' | sudo tee -a /etc/hosts
之前的命令将 echo 命令的输出(你希望在 /etc/hosts 文件中的文本)重定向到 sudo tee -a /etc/hosts 命令。这个第二个命令使用 sudo 以 root 用户身份运行。tee 命令将输出发送到文件和终端(STDOUT)。-a 参数告诉 tee 向文件追加内容,而 /etc/hosts 是你想要追加的文件。现在,在你的浏览器中,你将能够使用名称而不是 IP 地址。
现在您已准备好启动镜像并浏览到您的 Hadoop 框架。
Hadoop 基础
在本节中,您将启动您的 Hadoop 镜像,并学习如何使用 ssh 和 Ambari 进行连接。您还将移动文件并执行一个基本的 Hive 查询。一旦您了解了如何与框架交互,下一节将向您展示如何使用空间查询。
首先,从终端使用提供的 Bash 脚本启动 Hortonworks Sandbox。以下命令将展示如何操作:
sudo sh start_sandbox-hdp.sh
之前的命令执行了您下载的包含沙盒的脚本。同样,它使用了 sudo 以 root 身份运行。根据您的机器,完全加载并启动所有服务可能需要一些时间。完成之后,您的终端应该看起来像以下截图所示:

通过 Secure Shell 连接
现在沙盒正在运行,您可以使用 Secure Shell (SSH) 进行连接。安全壳允许您远程登录到另一台机器。打开一个新的终端并输入以下命令:
ssh raj_ops@127.0.0.1 -p2222
之前的命令使用 ssh 连接到用户 raj_ops 的 localhost (127.0.0.1) 端口 2222。您将收到一个警告,表示无法验证主机的真实性。我们没有为 ssh 创建任何密钥。只需键入 yes,然后您将被提示输入密码。用户 raj_ops 的密码是 raj_ops。您的终端提示符现在应该看起来像以下行:
[raj_ops@sandbox-hdp ~]$
如果您的终端与之前的代码一样,您现在已登录到容器中。
关于用户、权限和配置沙盒的更多信息,请访问以下页面:hortonworks.com/tutorial/learning-the-ropes-of-the-hortonworks-sandbox/
您现在可以使用大多数 Linux 命令在容器中导航。您现在可以下载和移动文件,运行 Hive,以及从命令行运行所有其他工具。由于本节已经对 Linux 做了足够的介绍,所以您在本章中不会仅使用命令行。相反,下一节将向您展示如何在 Ambari 中执行这些任务,Ambari 是一个基于 Web 的 GUI,用于执行任务。
Ambari
Ambari 是一个用于简化 Hadoop 管理的用户界面。在前一节中,您学习了如何将 ssh 实现到容器中。从那里,您可以管理 Hadoop,运行 Hive 查询,下载数据,并将其添加到 HDFS 文件系统中。Ambari 使得所有这些操作都变得更加简单,尤其是如果您不熟悉命令行的话。要打开 Ambari,浏览到以下 URL:sandbox.hortonworks.com:8080/
Ambari 的 URL 依赖于您的安装。如果您遵循了本章中的说明,那么这将将是您的 URL。您还必须从 Docker 镜像中启动了服务器。
您将被重定向到 Ambari 登录页面。输入 raj_ops/raj_ops 的用户/密码组合,如下面的截图所示:

登录后,您将看到 Ambari 仪表板。它看起来如下面的截图所示:

在左侧,您有一个服务列表。窗口的主要部分包含指标,顶部菜单栏有不同功能的标签页。在本章中,您将使用由九个小方块组成的方形。将鼠标悬停在方形图标上,您将看到一个文件视图的下拉菜单。
这是 HDFS 文件系统的 root 目录。
通过 ssh 连接到容器后,运行 hdfs dfs -ls / 命令,您将看到相同的目录结构。
从这里,您可以上传文件。为了尝试一下,打开一个文本编辑器并创建一个简单的 CSV。本例将使用以下数据:
40, Paul
23, Fred
72, Mary
16, Helen
16, Steve
保存 CSV 文件,然后在 Ambari 中点击上传按钮。您可以将 CSV 文件拖放到浏览器中。Ambari 将文件添加到容器上的 HDFS 文件系统:

现在您已经将数据加载到容器中,您可以使用 SQL 在 Hive 中查询它。再次使用方形图标,选择 Hive View 2.0 的下拉菜单。您应该会看到一个如下所示的工作区:

在 Hive 中,您有工作表。在工作表中,您连接到的数据库,在本例中是默认数据库。下面是主要的查询窗口。在右侧,您有一个现有表的列表。最后,向下滚动,您将看到执行按钮,下面是结果加载的位置。
在查询面板中,输入以下 SQL 查询:
SELECT * FROM sample_07
之前的查询是一个基本的 SQL 全选查询。结果将如下所示:

Esri 为 Hadoop 优化的 GIS 工具
在您的环境设置完成,并对 Ambari、HDFS 和 Hive 有一些基本了解之后,您现在将学习如何将空间组件添加到您的查询中。为此,我们将使用 Esri 为 Hadoop 优化的 GIS 工具。
第一步是下载位于 GitHub 仓库中的文件,该仓库位于:github.com/Esri/gis-tools-for-hadoop。您将使用 Ambari 将文件移动到 HDFS,而不是容器,因此请将这些文件下载到您的本地机器上。
Esri 提供了一个教程,说明如何使用 ssh 连接到容器,然后使用 git 克隆仓库来下载文件。您可以在以下位置找到这些说明:github.com/Esri/gis-tools-for-hadoop/wiki/GIS-Tools-for-Hadoop-for-Beginners。
您可以通过使用存储库右侧的 GitHub Clone 或下载按钮来下载文件。要解压存档,请使用以下命令之一:
unzip gis-tools-for-hadoop-master.zip
unzip gis-tools-for-hadoop-master.zip -d /home/pcrickard
第一个命令将在当前目录中解压文件,这很可能是您家目录的“下载”文件夹。第二个命令将解压文件,但通过传递 -d 和一个路径,它将解压到该位置。在这种情况下,这是我的家目录的“根”。
现在您已经解压了文件,可以通过在 Ambari 中选择图标下拉菜单中的“文件视图”来打开它。选择“上传”,将弹出一个模态窗口,允许您拖放文件。在您的本地机器上,浏览到 Esri Java ARchive (JAR) 文件的位置。如果您将压缩文件移动到您的家目录,路径将类似于 /home/pcrickard/gis-tools-for-hadoop-master/samples/lib。您将拥有三个 JAR 文件:
-
esri-geometry-api-2.0.0.jar -
spatial-sdk-hive-2.0.0.jar -
spatial-sdk-json-2.0.0.jar
将这三个文件中的每一个移动到 Ambari 的“根”文件夹。这是 / 目录,这是您启动文件视图时默认打开的位置。
接下来,您通常会也将数据移动到 HDFS,然而,您在之前的示例中已经这样做过了。在这个例子中,您将保留数据文件在您的本地机器上,您将学习如何在不位于 HDFS 的情况下将它们加载到 Hive 表中。
现在您已经准备好在 Hive 中执行空间查询。从图标下拉菜单中选择 Hive View 2.0。在查询面板中,输入以下代码:
add jar hdfs:///esri-geometry-api-2.0.0.jar;
add jar hdfs:///spatial-sdk-json-2.0.0.jar;
add jar hdfs:///spatial-sdk-hive-2.0.0.jar;
create temporary function ST_Point as 'com.esri.hadoop.hive.ST_Point';
create temporary function ST_Contains as 'com.esri.hadoop.hive.ST_Contains';
drop table earthquakes;
drop table counties;
CREATE TABLE earthquakes (earthquake_date STRING, latitude DOUBLE, longitude DOUBLE, depth DOUBLE, magnitude DOUBLE,magtype string, mbstations string, gap string, distance string, rms string, source string, eventid string)
ROW FORMAT DELIMITED FIELDS TERMINATED BY ','
STORED AS TEXTFILE;
CREATE TABLE counties (Area string, Perimeter string, State string, County string, Name string, BoundaryShape binary)
ROW FORMAT SERDE 'com.esri.hadoop.hive.serde.EsriJsonSerDe'
STORED AS INPUTFORMAT 'com.esri.json.hadoop.EnclosedEsriJsonInputFormat'
OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat';
LOAD DATA LOCAL INPATH '/gis-tools-for-hadoop-master/samples/data/earthquake-data/earthquakes.csv' OVERWRITE INTO TABLE earthquakes;
LOAD DATA LOCAL INPATH '/gis-tools-for-hadoop-master/samples/data/counties-data/california-counties.json' OVERWRITE INTO TABLE counties;
SELECT counties.name, count(*) cnt FROM counties
JOIN earthquakes
WHERE ST_Contains(counties.boundaryshape, ST_Point(earthquakes.longitude, earthquakes.latitude))
GROUP BY counties.name
ORDER BY cnt desc;
运行前面的代码将花费一些时间,具体取决于您的机器。最终结果将看起来像以下图像:

之前的代码和结果未加解释,以便您能够运行示例并查看输出。之后,代码将逐块进行解释。
以下代码块的第一部分如下所示:
add jar hdfs:///esri-geometry-api-2.0.0.jar;
add jar hdfs:///spatial-sdk-json-2.0.0.jar;
add jar hdfs:///spatial-sdk-hive-2.0.0.jar;
create temporary function ST_Point as 'com.esri.hadoop.hive.ST_Point';
create temporary function ST_Contains as 'com.esri.hadoop.hive.ST_Contains';
此块将 HDFS 位置的 JAR 文件添加到其中。在这种情况下,它是 / 文件夹。一旦代码加载了 JAR 文件,它就可以通过调用 JAR 文件中的类来创建函数 ST_Point 和 ST_Contains。JAR 文件可能包含许多 Java 文件(类)。add jar 语句的顺序很重要。
以下块删除了两个表——earthquakes 和 counties。如果您从未运行过此示例,您可以跳过这些行:
drop table earthquakes;
drop table counties;
接下来,代码为 earthquakes 和 counties 创建表。earthquakes 表被创建,并将每个字段和类型传递给 CREATE。行格式指定为 CSV——','。最后,它是一个文本文件:
CREATE TABLE earthquakes (earthquake_date STRING, latitude DOUBLE, longitude DOUBLE, depth DOUBLE, magnitude DOUBLE,magtype string, mbstations string, gap string, distance string, rms string, source
string, eventid string)
ROW FORMAT DELIMITED FIELDS TERMINATED BY ','
STORED AS TEXTFILE;
通过将字段名称和类型传递给CREATE来以类似的方式创建counties表,但数据是 JSON 格式,并将使用你导入的 JAR 文件spatial-sdk-json-2.0.0中的com.esri.hadoop.hive.serde.EsriJSonSerDe类。STORED AS INPUTFORMAT和OUTPUTFORMAT对于 Hive 来说,是必须的,以便知道如何解析和处理 JSON 数据:
CREATE TABLE counties (Area string, Perimeter string, State string, County string, Name string, BoundaryShape binary)
ROW FORMAT SERDE 'com.esri.hadoop.hive.serde.EsriJsonSerDe'
STORED AS INPUTFORMAT 'com.esri.json.hadoop.EnclosedEsriJsonInputFormat'
OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat';
接下来的两个块将数据加载到创建的表中。数据存在于你的本地机器上,而不是 HDFS 上。为了使用本地数据而不先将其加载到 HDFS,你可以使用LOCAL命令与LOAD DATA INPATH一起指定数据的本地路径:
LOAD DATA LOCAL INPATH '/gis-tools-for-hadoop-master/samples/data/earthquake-data/earthquakes.csv' OVERWRITE INTO TABLE earthquakes;
LOAD DATA LOCAL INPATH '/gis-tools-for-hadoop-master/samples/data/counties-data/california-counties.json' OVERWRITE INTO TABLE counties;
在加载了 JAR 文件并创建了表并填充了数据后,你现在可以使用两个定义好的函数——ST_Point和ST_Contains——运行空间查询。这些函数的使用方式与第三章中的示例相同,地理数据库简介:
SELECT counties.name, count(*) cnt FROM counties
JOIN earthquakes
WHERE ST_Contains(counties.boundaryshape,
ST_Point(earthquakes.longitude, earthquakes.latitude))
GROUP BY counties.name
ORDER BY cnt desc;
之前的查询通过将县几何形状和每个地震的位置作为点传递给ST_Contains来选择县的name和地震的count。结果如下所示:

Python 中的 HDFS 和 Hive
本书是关于 Python 地理空间开发的,因此在本节中,你将学习如何使用 Python 进行 HDFS 操作和 Hive 查询。有几个 Python 和 Hadoop 的数据库包装库,但似乎没有单个库成为突出的首选库,而像 Snakebite 这样的库似乎还没有准备好在 Python 3 上运行。在本节中,你将学习如何使用两个库——PyHive 和 PyWebHDFS。你还将学习如何使用 Python 的子进程模块来执行 HDFS 和 Hive 命令。
要获取 PyHive,你可以使用conda和以下命令:
conda install -c blaze pyhive
你可能还需要安装sasl库:
conda install -c blaze sasl
之前的库将赋予你从 Python 运行 Hive 查询的能力。你还将想要能够将文件移动到 HDFS。为此,你可以安装pywebhdfs:
conda install -c conda-forge pywebhdfs
上述命令将安装库,并且像往常一样,你也可以使用pip install或使用其他任何方法。
在安装了库之后,让我们首先看看pywebhdfs。
pywebhdfs的文档位于:pythonhosted.org/pywebhdfs/
在 Python 中建立连接,你需要知道你的 Hive 服务器位置。如果你已经跟随了本章,特别是/etc/hosts中的配置更改——你可以使用以下代码来完成:
from pywebhdfs.webhdfs import PyWebHdfsClient as h
hdfs=h(host='sandbox.hortonworks.com',port='50070',user_name='raj_ops')
之前的代码将PyWebHdfsClient导入为h。然后它创建与在容器中运行的 HDFS 文件系统的连接。该容器映射到sandbox.hortonworks.com,HDFS 运行在端口50070。由于示例一直使用raj_ops用户,代码也是如此。
现在可用于hdfs变量的函数类似于您的标准终端命令,但名称不同——mkdir现在是make_dir,ls现在是list_dir。要删除文件或目录,您将使用delete_file_dir。make和delete命令在成功时将返回True。
让我们使用 Python 查看我们的 HDFS 文件系统的root目录:
ls=hdfs.list_dir('/')
之前的代码发出了list_dir命令(与ls等效)并将其分配给ls。结果是包含目录中所有文件和文件夹的字典。
要查看单个记录,您可以使用以下代码:
ls['FileStatuses']['FileStatus'][0]
之前的代码通过使用字典键FileStatuses和FileStatus来获取单个记录。
要获取字典中的键,您可以使用.keys(),即ls.keys()返回[FileStatuses],以及ls['FileStatuses'].keys()返回['FileStatus']。
之前代码的输出如下所示:
{'accessTime': 0, 'blockSize': 0, 'childrenNum': 1, 'fileId': 16404, 'group': 'hadoop', 'length': 0, 'modificationTime': 1510325976603, 'owner': 'yarn', 'pathSuffix': 'app-logs', 'permission': '777', 'replication': 0, 'storagePolicy': 0, 'type': 'DIRECTORY'}
每个文件或目录都包含多个数据项,但最重要的是类型、所有者和权限。
运行 Hive 查询示例的第一步是将我们的数据文件从本地机器移动到 HDFS。使用 Python,您可以使用以下代码来完成此操作:
hdfs.make_dir('/samples',permission=755)
f=open('/home/pcrickard/sample.csv')
d=f.read()
hdfs.create_file('/samples/sample.csv',d)
之前的代码创建了一个名为samples的目录,权限为755。在 Linux 中,权限基于三种用户的读取(4)、写入(2)和执行(1)权限——所有者、组和其它。因此,755的权限意味着所有者有读取、写入和执行权限(4+2+1=7),而组和其它有读取和执行权限(4+1=5)。
接下来,代码打开并读取我们想要传输到 HDFS 的 CSV 文件,并将其分配给变量d。然后,代码在samples目录中创建名为sample.csv的文件,传递d的内容。
要验证文件是否已创建,您可以使用以下代码来读取文件的内容:
hdfs.read_file('/samples/sample.csv')
之前代码的输出将是 CSV 文件的字符串。它已成功创建。
或者,您可以使用以下代码来获取文件的状态和详细信息:
hdfs.get_file_dir_status('/samples/sample.csv')
之前的代码将返回以下详细信息,但仅当文件或目录存在时。如果不存在,前面的代码将引发FileNotFound错误。您可以将前面的代码包裹在try...except块中:
{'FileStatus': {'accessTime': 1517929744092, 'blockSize': 134217728, 'childrenNum': 0, 'fileId': 22842, 'group': 'hdfs', 'length': 47, 'modificationTime': 1517929744461, 'owner': 'raj_ops', 'pathSuffix': '', 'permission': '755', 'replication': 1, 'storagePolicy': 0, 'type': 'FILE'}}
数据文件已传输到 HDFS 后,您可以继续使用 Hive 查询数据。
PyHive 的文档位于:github.com/dropbox/PyHive
使用pyhive,以下代码将创建一个表:
from pyhive import hive
c=hive.connect('sandbox.hortonworks.com').cursor()
c.execute('CREATE TABLE FromPython (age int, name string) ROW FORMAT DELIMITED FIELDS TERMINATED BY ","')
之前的代码将pyhive导入为hive。它创建了一个连接并获取了游标。最后,它执行了一个 Hive 语句。一旦您有了连接和游标,您就可以通过将它们包裹在.execute()方法中来执行 SQL 查询。要将 HDFS 中的 CSV 数据加载到表中并选择所有内容,您将使用以下代码:
c.execute("LOAD DATA INPATH '/samples/sample.csv' OVERWRITE INTO TABLE FromPython")
c.execute("SELECT * FROM FromPython")
result=c.fetchall()
之前的代码使用了 execute() 方法两次来加载数据,然后执行了全选操作。使用 fetchall(),结果被传递到 result 变量中,其外观将类似于以下输出:
[(40, ' Paul'), (23, ' Fred'), (72, ' Mary'), (16, ' Helen'), (16, ' Steve')]
使用 pyhive 就像使用 psycopg2 一样——这是连接到 PostgreSQL 的 Python 库。大多数数据库包装库都非常相似,你只需建立连接,获取一个 cursor,然后执行语句。结果可以通过获取所有、一个或下一个(可迭代)来检索。
摘要
在本章中,你学习了如何设置 Hadoop 环境。这需要你安装 Linux 和 Docker,从 Hortonworks 下载镜像,并学习该环境的使用方法。本章的大部分内容都花在了环境和如何使用提供的 GUI 工具执行空间查询上。这是因为 Hadoop 环境很复杂,如果没有正确的理解,就很难完全理解如何使用 Python 来操作它。最后,你学习了如何在 Python 中使用 HDFS 和 Hive。用于与 Hadoop、Hive 和 HDFS 一起工作的 Python 库仍在开发中。本章为你提供了一个基础,这样当这些库改进时,你将拥有足够的关于 Hadoop 和相关技术的知识,以实现这些新的 Python 库。


浙公网安备 33010602011771号