精通-Python-地理空间分析-全-

精通 Python 地理空间分析(全)

原文:zh.annas-archive.org/md5/619a59a0e10973fe1be4c6aa361430b4

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

随着时间的推移,Python 已成为空间分析的首选编程语言,导致出现了许多读取、转换、分析和可视化空间数据的包。在这么多包可用的情况下,为学生和经验丰富的专业人士创建一本包含 Python 3 必需地理空间 Python 库的参考书是有意义的。

本书发布在激动人心的时刻:新技术正在改变人们处理地理空间数据的方式——物联网、机器学习和数据科学是地理空间数据不断被使用的领域。这也解释了为什么包含新的 Python 库,如 CARTOframes 和 MapboxGL,以及 Jupyter,以探索这些新趋势。同时,基于 Web 和云的 GIS 正日益成为新的标准。这在本书第二部分的章节中得到了体现,其中介绍了交互式地理空间网络地图和 REST API。

这些较新的库与一些在多年中已成为必需且至今仍非常流行的旧库相结合,例如 Shapely、Rasterio 和 GeoPandas。对于新进入这个领域的人来说,将给出对流行库的适当介绍,通过使用真实世界数据的代码示例将它们置于适当的背景中,并比较它们的语法。

最后,本书标志着从 Python 2 到 3.x 的过渡。本书涵盖的所有库都是用 Python 3.x 编写的,以便读者可以使用 Jupyter Notebook 访问它们,这也是本书推荐的 Python 编码环境。

本书面向对象

本书面向任何与位置信息及 Python 工作的人。学生、开发者和地理空间专业人士都可以使用这本参考书,因为它涵盖了 GIS 数据管理、分析技术和使用 Python 3 构建的代码库。

为了充分利用本书

由于本书涵盖 Python,因此假设读者对 Python 语言有基本的了解,可以安装 Python 库,并且知道如何编写和运行 Python 脚本。至于额外的知识,前六章可以很容易地理解,无需任何地理空间数据分析的先验知识。然而,后面的章节假设读者对空间数据库、大数据平台、数据科学、Web API 和 Python Web 框架有一定的了解。

下载示例代码文件

您可以从 www.packtpub.com 的账户下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. www.packtpub.com 登录或注册。

  2. 选择“支持”选项卡。

  3. 点击代码下载与勘误表。

  4. 在搜索框中输入本书的名称,并遵循屏幕上的说明。

下载文件后,请确保使用最新版本解压缩或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

本书代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Mastering-Geospatial-Analysis-with-Python。如果代码有更新,它将在现有的 GitHub 仓库中更新。

我们还有其他来自我们丰富图书和视频目录的代码包可供使用,网址为github.com/PacktPublishing/。查看它们吧!

下载彩色图像

我们还提供了一份包含本书中使用的截图/图表的彩色图像的 PDF 文件。您可以从这里下载:www.packtpub.com/sites/default/files/downloads/MasteringGeospatialAnalysiswithPython_ColorImages.pdf

Conventions used

本书使用了多种文本约定。

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

Bold: 表示新术语、重要单词或你在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“要从 EC2 仪表板生成密钥对,请在滚动到左侧面板的 NETWORK & SECURITY 组后选择 Key Pairs。”

Warnings or important notes appear like this.

Tips and tricks appear like this.

Get in touch

我们读者的反馈总是受欢迎的。

General feedback: 通过feedback@packtpub.com发送电子邮件,并在邮件主题中提及书名。如果你对本书的任何方面有疑问,请通过questions@packtpub.com发送电子邮件给我们。

Errata: 尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然可能发生。如果你在这本书中发现了错误,我们将不胜感激,如果你能向我们报告这个错误。请访问www.packtpub.com/submit-errata,选择你的书,点击 Errata Submission Form 链接,并输入详细信息。

Piracy: 如果你在互联网上以任何形式遇到我们作品的非法副本,如果你能提供给我们地址或网站名称,我们将不胜感激。请通过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、condapip 管理 Python 包

  • 使用 Anaconda、condavirtualenv 管理虚拟环境

  • 运行 Jupyter Notebook

介绍 Anaconda

Anaconda 是 Python 编程语言的免费增值开源发行版,适用于大规模数据处理、预测分析和科学计算,旨在简化包管理和部署。它也是全球最受欢迎的 Python 数据科学平台,拥有超过 4.5 百万用户和 1,000 个数据科学包。它不应与 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)采用。由 Project Jupyter 管理,这是一个开源项目(基于 IPython,一个早期的交互式代码环境),它是一个学习和生产环境中的绝佳工具。虽然代码也可以像其他章节中看到的那样作为脚本运行,但使用 Jupyter 笔记本会让编码变得更加有趣。

代码笔记本的想法是使编码交互式。通过将 Python 终端与代码运行产生的直接输出相结合,笔记本(可保存)成为共享和比较代码的工具。每个部分都可以稍后编辑,或作为单独的组件保存以供演示目的。

在此处查看 Jupyter 笔记本的文档:

jupyter.org/documentation.

运行笔记本

要启动为笔记本提供动力的本地服务器,请激活虚拟环境并传递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

创建新的笔记本

要创建一个新的笔记本,请点击右上角的“New”按钮,并在笔记本部分选择“Python 3”。它将在新标签页中打开笔记本:

添加代码

在 Jupyter 笔记本中,代码是在“In”部分添加的。代码可以逐行添加,因为代码变量和导入的模块将被保存在内存中,或者可以以块/多行的方式添加,就像脚本一样。可以编辑和运行“In”部分多次,或者可以将其保留不变,然后开始一个新的部分。这会创建脚本努力的记录,以及交互式输出。

这里有一个 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 版本,并从本地可用的 1000 多个包列表中进行安装,如果你安装的是默认的 Anaconda 版本而不是 Miniconda。此列表可通过选择“频道”按钮旁边的下拉菜单中的“未安装”选项来获取。你可以通过使用“搜索包”字段并按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,因为它允许你为你的个人工作创建一个私有在线仓库。这些仓库被称为频道

如果你创建了一个用户账户,你可以在 Anaconda Navigator 内部使用 Anaconda Cloud。创建 Anaconda Cloud 的用户账户后,打开 Anaconda Navigator 并使用你的登录详细信息在屏幕右上角(显示为“登录到 Anaconda Cloud”)登录 Anaconda Cloud。现在,你可以将你自己的包和文件上传到私有包仓库,并搜索现有的文件或包。

使用 conda 管理 Python 包

除了使用 Anaconda Navigator 和 Cloud 进行包管理外,你还可以将 conda(一个二进制包管理器)用作命令行工具来管理你的包安装。conda 可以快速安装、运行和更新包及其依赖项。conda 可以轻松创建、保存、加载并在你的本地计算机上切换环境。安装 conda 的最佳方式是通过安装 Anaconda 或 Miniconda。第三种选择是通过 Python 包索引PyPI)进行单独安装,但可能不是最新的,因此不建议选择此选项。

使用 conda 安装包非常简单,因为它与 pip 的语法相似。然而,了解 conda 不能直接从 Git 服务器安装包是有好处的。这意味着许多正在开发的包的最新版本无法使用 conda 下载。此外,conda 并不像 pip 那样覆盖 PyPI 上所有可用的包,这就是为什么在用 Anaconda Navigator 创建新环境时,你总是可以访问 pip(关于 pip 的更多内容将在后续章节中介绍)。

你可以通过在终端中输入以下命令来验证 conda 是否已安装:

>> conda -version

如果已安装,conda 将显示你已安装的 version 的数量。你可以在终端中使用以下命令安装你选择的包:

>> 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_installpip 作为包管理器。由于 pip 被视为对 easy_install 的改进,并且是 Python 3 的首选包管理器,因此我们在此仅讨论 pip。建议在接下来的章节中使用 pipconda、Anaconda Navigator 或 Cloud 进行 Python 包管理。

选项性地,当你安装 Anaconda 时,三个环境变量将被添加到你的用户变量列表中。这使你能够在打开终端时从任何系统位置访问 pip 等命令。要检查你的系统上是否已安装 pip,请打开终端并输入:

>> pip

如果您没有收到任何错误消息,这意味着 pip 已正确安装,并且您可以使用 pip 通过以下方式从 PyPI 安装您选择的任何包:

>> pip install <package-name>

对于 Anaconda 用户,pip 命令文件应存储在以下路径:

C:\Users\<用户名>\Anaconda3\Scripts\pip.exe

如果您的系统上没有 pip,您可以按照以下说明安装 pippip.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、condapip进行 Python 包管理的选项。最后,我们讨论了虚拟环境以及如何使用 Anaconda、condavirtualenv来管理这些环境。

本书推荐的安装版本是 Anaconda3,它不仅会安装一个可工作的 Python 环境,还会安装一个庞大的本地 Python 包库、Jupyter Notebook 应用程序,以及conda包管理器、Anaconda Navigator 和云服务。在下一章中,我们将介绍用于处理和分析地理空间数据的主要代码库。

第二章:地理空间代码库简介

本章将介绍用于处理和分析地理空间数据的主要代码库。您将学习每个库的特点,它们之间的关系,如何安装它们,在哪里可以找到额外的文档,以及典型用例。这些说明假设用户在其机器上安装了较新的(2.7 或更高版本)的 Python,并且不涉及 Python 的安装。接下来,我们将讨论所有这些包是如何相互关联的,以及它们在本书的其余部分是如何被涵盖的。

本章将介绍以下库:

  • GDAL/OGR

  • GEOS

  • Shapely

  • Fiona

  • Python Shapefile Library (pyshp)

  • pyproj

  • Rasterio

  • GeoPandas

地理空间数据抽象库(GDAL)和 OGR 简单特征库

地理空间数据抽象库(Geospatial Data Abstraction Library)GDAL)和OGR 简单特征库(OGR Simple Features Library)结合了两个通常一起作为 GDAL 下载的独立库。这意味着安装 GDAL 包也提供了对 OGR 功能的访问,这就是为什么它们在这里一起被介绍。GDAL 被首先介绍的原因是其他包是在 GDAL 之后编写的,所以按时间顺序,它排在前面。您将注意到,本章中介绍的一些包扩展了 GDAL 的功能或在其底层使用它。

GDAL 是在 20 世纪 90 年代由 Frank Warmerdam 创建的,并于 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 安装相当复杂,需要你调整系统设置和路径变量。然而,尽管如此,我们仍然建议你使用 Anaconda3 或conda,因为这是开始的最快和最简单的方式。其他选项包括使用pip安装,或者使用在线仓库,如gdal.org或 Tamas Szekeres 的 Windows 二进制文件(www.gisinternals.com/release.php)。

然而,这可能会比这里描述的选项复杂一些。安装 GDAL 的难点在于,库的特定版本(以 C 语言形式提供,并安装在你的本地 Python 文件之外的单独系统目录中)有一个相应的 Python 版本,并且需要编译才能在 Python 中使用。此外,Python 版本的 GDAL 依赖于一些额外的 Python 库,这些库包含在安装中。虽然可以在同一台机器上使用多个 GDAL 版本,但这里推荐的方法是在虚拟环境中安装它,使用 Anaconda3、condapip安装。这将保持你的系统设置干净,避免额外的路径变量或防止某些东西无法正常工作。

使用 Anaconda3 安装 GDAL

如果你使用 Anaconda3,安装 GDAL 最简单的方法是通过 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,这可能会成为一个问题。然后,你可以创建一个虚拟环境来使用相应的 Python 版本安装和运行 GDAL 及其依赖项,例如:

(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,也使用了 GEOS,以及其他地理空间库,如 GDAL。GEOS 还可以与 GDAL 一起编译,为 OGR 提供所有功能。

JTS 是一个用 Java 编写的开源地理空间计算几何库。它提供了各种功能,包括几何模型、几何函数、空间结构和算法以及输入/输出能力。使用 GEOS,你可以访问以下功能——地理空间函数(如withincontains)、地理空间操作(并集、交集等)、空间索引、开放地理空间联盟OGC)的已知文本WKT)和已知二进制WKB)输入/输出、C 和 C++ API 以及线程安全性。

安装 GEOS

可以使用pip install、conda和 Anaconda3 来安装 GEOS:

>> 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 的预构建包格式,包含一个具有特殊格式文件名和 .whl 扩展名的 ZIP 格式存档。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 依赖于模块 sixcligjmunchargparseordereddict(后两个模块是 Python 2.7+ 的标准模块)。

更多下载信息,请参阅 Fiona 的 README 页面 toblerity.org/fiona/README.html

Python shapefile 库(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

pyproj 的安装可以通过 pip installconda 和 Anaconda3 完成:

>> 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 包依赖项包括affinecligjclickenum34numpy

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安装、conda安装、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 中连接并操作你的 PostgreSQL 数据库,你需要一个库来帮助你。psyscopg2 就是那个库。它提供了一个官方 libpq 客户端库的包装。在本节中,我们将介绍如何安装库、如何连接到数据库以及如何添加表和执行基本的地理空间查询。

使用 psycopg2 连接到 PostgreSQL

pscycopg2 是在 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() 函数并传递数据库名称、用户密码 参数来建立连接。然后创建一个 cursor,这允许你与数据库进行通信。你可以使用 cursorexecute() 方法通过字符串形式的 SQL 语句创建表。

代码执行一个 SQL 命令,创建一个名为 art_pieces 的表,其中 id 类型为 SERIAL 并将其设置为 PRIMARY KEYcode 类型为 VARCHAR 且长度为 255,以及 locationGEOMETRY 类型。SERIAL PRIMARY KEY 告诉 PostgreSQL 我们想要一个自动递增的唯一标识符。你也可以使用 BIGSERIAL 类型。另一种不同的类型是 locationGEOMETRY 类型。这是将包含我们记录的地理部分的列。

最后,你commit()以确保更改被保存。当你完成时,你也可以close(),但我们将继续前进。

向表中添加数据

在上一节中,我们创建了一个表。在本节中,你将从开放数据网站抓取数据并将其放入你的表中,以便你可以在下一节中查询它。

大多数城市都有开放数据网站和门户。阿尔伯克基市有几个带有空间数据的 ArcServer 端点。以下代码将使用requestsPython 库抓取公共艺术数据,然后使用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:*)的结果,在世界大地测量系统WGS84outSR: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坐标缺失时崩溃,我添加了一个ifelse语句来跳过缺失的数据。这个概念被称为错误处理,在构建requests时是一个最佳实践。else语句是数据被插入的地方。使用cursor.execute(),你可以构建 SQL 查询。

查询将art_pieces以及带有值的codelocation字段插入到数据库中。对于code的第一个值是一个占位符{}。对于location的第二个值是几何形状,我们将其存储为 WKT。因此,它使用ST_GeomFromText()函数和一个占位符{}插入。

format()方法是你传递变量以填充占位符的地方——codewkt。以下代码展示了当占位符被填充时查询将看起来是什么样子:

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中导入了PointMultiPoint。代码与上一个版本相同,直到加粗的行。在 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'),
 .......,]

第一个数字,12n,是id(即SERIAL PRIMARY KEY)。接下来是code。几何信息是最后一列。看起来像是随机数字和字母的字符串是一个已知的二进制WKB)的十六进制表示。

要转换 WKB,你使用shapely。以下代码将指导你将 WKB 转换为shapelyPoint,然后打印 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]代表列。现在你有了shapelyPoint,你可以通过type(aPoint)来验证它,你可以使用aPoint.wkt将其打印为 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()将位置包裹起来。要将结果加载到shapelyPoint中,你必须使用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()在任何时候返回包含几何形状的列。而不是fetchall(),代码使用fetchone()来获取单个记录。你应该看到以下单个记录:

('101', 'POINT(-106.591838300225 35.1555000000615)')

你可以使用loads()将 WKT 加载到shapelyPoint中,但你需要首先导入它,就像你之前导入 WKB 一样:

from shapely.wkt import loads
pb=loads(data[1])
pb.coords[:]

之前的代码从shapely导入loads——但这次使用shapely.wkt而不是wkb。否则,你将以与之前示例相同的方式加载数据。你可以使用pb.coords[:]查看shapelyPoint坐标,或者使用pb.xpb.y单独查看它们。

pb.coords[:]的结果将是一个坐标对,如下所示:

[(-106.591838300225, 35.1555000000615)]

改变 CRS

数据库中的数据使用世界大地测量系统 84WGS 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 codelocation(作为文本)从art_pieces中选择的查询,但现在有一个ST_Transform方法。此方法需要几何列和您希望数据返回的SRID。现在,使用3857返回(-106.59, 35.155)处的艺术作品,并以下列转换坐标显示:

('101', 'POINT(-11865749.1623 4185033.1034)')

缓冲区

空间数据库允许你存储空间数据,但你也可以对数据进行操作并获取不同的几何形状。这些操作中最常见的是缓冲区。你有一个点表,但使用ST_Buffer(),你可以让数据库返回一个指定半径的点周围的 polygon。以下代码展示了如何操作:

cursor.execute("SELECT ST_AsText(ST_Buffer(a.location,25.00,'quad_segs=2')) from pieces a WHERE a.code='101'")

cursor.fetchall()

之前的代码从表中获取一个记录,其中艺术代码字段等于101,并选择一个半径为25location缓冲区。结果将是一个多边形,如下所示:

当使用地理体时,如果缓冲区很大,位于两个 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)。你可以将ab作为几何体或地理体传递。作为地理体,结果将以米为单位返回。以下代码将获取数据库中两点之间的距离:

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 查询,传递了ablocation列,这些记录的代码等于101102::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 点替换了点bcode=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(),你指定了两个几何。由于你设置了LIMIT5,数据库将返回与指定点最近的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,然后创建一个包含idgeometry类型位置的表。你应该导入shapely LineStringMultiLineMultiLine是为了你可以在 Jupyter 笔记本中打印线。你应该创建一个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将返回记录,交换23的位置,如下所示:

[(1, 4415.21026808109),
 (3, 7059.45840502359),
 (2, 7835.65405408195)]

相交的线

你知道lines的长度,并且通过在 Jupyter Notebook 中绘制lines,你知道lines 1lines 3相交。在 PostGIS 中,你可以使用ST_Intersects()函数,传递几何图形或地理图形。数据库将返回 true 或 false。

以下代码将在lines 1lines 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 1lines 3相交。但它们在哪里相交?使用ST_Intersection()将返回两条lines相遇的点:

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,你将得到两个lines之间的接触点。该点如下:

[('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()

之前的代码几乎与PointLine示例完全相同。建立数据库连接并获取一个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_ContainsST_Intersects

St_Contains接受两个几何图形并确定第一个是否包含第二个。

顺序很重要——a 包含 b,这与ST_Within相反,它使用顺序 ba

使用包含,几何图形 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()

之前的代码几乎与前面的示例相同,只是我们使用了交集与相交的区别。结果是 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 编程模块可用于读取和写入 shapefiles。

尽管名称shapefile可能暗示与之关联的只有一个文件,但实际上一个 shapefile 实际上至少需要三个文件,并且需要存储在同一个目录中才能正确工作:

  • 一个包含特征几何本身的.shp文件

  • 一个包含特征几何位置索引的.shx文件,以便快速向前和向后搜索

  • 一个包含每个形状的列属性的.dbf文件

Shapefiles 有其自己的结构。主文件(.shp)包含几何数据,由一个单一定长标题组成,后面跟着一个或多个变长记录。

GeoJSON

GeoJSON 是一种基于 JSON 的文件格式,在短时间内变得非常流行。GeoJSON 使用JavaScript 对象表示法JSON)开放数据标准,将地理特征存储为键值对。这些文件易于阅读,可以使用简单的文本编辑器创建,现在在空间数据库、开放数据平台以及商业 GIS 软件中都很常见。您可以使用 GeoJSON 处理各种类型的地理空间矢量数据,例如点、线和多边形。GeoJSON 使用.json.geojson作为文件扩展名。这意味着一个文件不一定要以.geojson结尾才能成为 GeoJSON 文件。

KML

Keyhole Markup Language (KML),指代开发该格式的公司。它可以用来存储地理数据,这些数据可以通过 Google Earth、Esri ArcGIS Explorer、Adobe Photoshop 和 AutoCAD 等众多应用程序进行可视化。KML 基于 XML,使用基于标签的结构,具有嵌套元素和属性。KML 文件通常以 KMZ 文件形式分发,KMZ 是带有.kmz扩展名的压缩 KML 文件。对于其参考系统,KML 使用经度、纬度和高度坐标,这些坐标由1984 年世界大地测量系统WGS84)定义。

GeoPackage

开放地理空间联盟OGC)的GeoPackageGPKG)是一种支持矢量和栅格数据的地信息系统开放数据格式。该格式由 OGC 定义并于 2014 年发布,此后得到了来自政府、商业和开源组织的广泛支持。GeoPackage 数据格式是考虑到移动用户而开发的——它被设计得尽可能高效,所有信息都包含在一个文件中。这使得它们在云存储和 USB 驱动器上快速共享变得容易,并且它被用于断开连接的移动应用程序。GeoPackage 文件由一个扩展名为*.gpkg的扩展 SQLite 3 数据库文件构建,该文件结合了数据和元数据表。

栅格数据格式

这些是目前用于地理信息的一些最流行的栅格数据格式:

  • 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,即一个具有行和列的二维对象。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,到一个墨卡托投影,显示更垂直拉伸的图像:

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

此命令通过使用通用选项--formats列出ogrinfo可以访问的可用格式。结果还告诉我们 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]]

我们可以检查我们是否在处理点,并打印所有单个特征的xy值以及它们的质心,如下所示:

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:

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。GeoPackage 可以是基于矢量或栅格的,但在这个例子中,我们将打开一个基于栅格的 GeoPackage,这从下面的输出中可以清楚地看出。对于读取和写入 GeoPackage,我们需要 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 提供了一系列批处理命令,可用于描述和转换现有的地理空间矢量数据。我们已经在 第四章 中提到了其中两个,ogrinfoogr2ogr数据类型、存储和转换

  • ogrinfo 可以用于对矢量数据进行各种报告,例如列出支持的矢量格式、可用图层和摘要细节,并且可以与 SQL 查询语法结合以从数据集中选择要素。

  • ogr2ogr用于执行矢量数据转换,例如在不同格式之间转换矢量文件,将多个图层转换为新的数据源,以及根据位置重新投影矢量数据和过滤特征。它也可以像ogrinfo一样使用 SQL 查询语法。

这些是非常强大的命令,可以让您完成大量工作。建议您在工作处理矢量数据时熟悉这些命令。我们很快会提供一些示例。

此外,还存在两个用于创建矢量瓦片的批处理命令,ogrtindexogr2vrt。两者之间的区别在于第二个命令比第一个命令更广泛地可用。第二个命令需要从在线脚本中导入,因为它不随最新的 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 处理矢量数据最重要的部分。有了它,你可以创建点、线和多边形,并对这些元素执行空间运算。例如,你可以计算几何的面积,将不同的数据叠加在一起,并使用如缓冲区之类的邻近工具。此外,就像 ogrinfoogr2ogr 一样,OGR 库提供了读取矢量数据文件、遍历单个元素以及选择和重新投影数据的工具。

OGR 的主要模块和类

OGR 库由两个主要模块组成——ogrosr。这两个都是 osgeo 模块内的子模块。ogr 子模块处理矢量几何,而 osr 则全部关于投影。在 第四章 的 使用 OGR 读取和写入矢量数据 部分,数据类型、存储和转换,我们已经看到了如何利用这两个模块的一些示例。

OGR 提供以下七个类:

  • 几何

  • 空间参考

  • 要素

  • 要素类定义

  • 图层

  • 数据集

  • 驱动程序

类名大多一目了然,但了解 OGR 的结构概述是很好的。在以下示例中,我们将看到如何访问和使用这些类。OGR 的模块、类和函数在 GDAL 网站上有文档(www.gdal.org/python),但提供没有代码示例,这使得入门变得困难。现在值得知道的是,其他 Python 库填补了这一空白,并提供了更用户友好的方式来处理 GDAL 的功能(如 Fiona 和 GeoPandas)。此外,在某些用例中,ogrinfoogr2ogr 可能比使用 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。脚本包括以下步骤:

  1. 导入模块并设置空间参考(在这种情况下,世界大地测量系统 1984WGS1984))。

  2. 创建 shapefile,然后使用多边形几何创建图层。接下来,将几何放入要素中,将要素放入图层中。注意,脚本直接引用了早期示例中的多边形。

  3. 关键在于在代码的第一行使用正确的几何类型,在这种情况下应该是wkbPolygon

  4. 在这一步中引用了我们早期示例中的多边形几何,并将其放入 shapefile 中。

  5. 在这一步中,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)

使用空间过滤器选择要素

此示例使用在第四章,“使用 GeoPandas 读取和写入矢量数据”部分中介绍的 Natural Earth Dataset。我们将使用经纬度坐标创建一个边界框形式的空问过滤器。这个框只选择框内的数据。这是一种处理数据子集的方法。我们将使用 OGR 的SpatialFilterRec方法,该方法接受四个值——minxminymaxxmaxy来创建一个边界框。我们的(随机)示例是选择边界框内的城市(这显示了德克萨斯州,以及俄克拉荷马州和墨西哥的部分地区)。为了进一步过滤我们的结果,我们只想选择美国内的城市。这意味着我们必须在for循环中添加额外的if/else语句来过滤我们的搜索结果。

网站 www.mapsofworld.com 为我们示例代码提供了以下四个值:-102minx),26miny),-94maxx),和36maxy)代表德克萨斯州。以下是脚本:

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 库,具体在ShapelyFiona小节。将它们一起介绍是有意义的,因为 Shapely 依赖于其他库来读取和写入文件,而 Fiona 则符合这一要求。正如我们将在示例中看到的那样,我们可以使用 Fiona 打开和读取文件,然后将几何数据传递给 Shapely 对象。

Shapely 对象和类

Shapely 库用于创建和操作 2D 矢量数据,无需空间数据库。它不仅摒弃了数据库,还摒弃了投影和数据格式,只关注几何。Shapely 的优势在于它使用易于阅读的语法创建各种几何形状,这些形状可用于几何运算。

在其他 Python 包的帮助下,这些几何形状和几何运算的结果可以写入矢量文件格式,并在必要时进行投影——我们将结合pyproj和 Fiona 的 Shapely 功能来举例。一个可能的工作流程示例可能是使用 Fiona 从 shapefile 中读取矢量几何形状,然后使用 Shapely 简化或清理现有几何形状,以防它们在内部或与其他几何形状组合时可能对齐正确。清理后的几何形状可以用作其他工作流程的输入,例如创建专题地图或执行数据科学。

Shapely 库使用一组类,这些类是三种基本几何对象类型(点、曲线和表面)的实现。如果你熟悉地理空间数据和它们的几何形状,它们会听起来很熟悉。如果你不熟悉,请使用示例来熟悉它们:

几何对象名称 类名
Point
曲线 LineString, LinearRing
表面 Polygon
点的集合 MultiPoint
曲线的集合 MultiLineString
表面的集合 MultiPolygon

Shapely 地理空间分析方法

拓扑关系作为几何对象上的方法实现(例如,包含、接触等)。Shapely 还提供了返回新几何对象的分析方法(交集、并集等)。创意地使用缓冲方法提供了清理形状的方法。与其他软件的互操作性通过知名格式(WKT 和 WKB)、NumPy + Python 数组以及 Python Geo 接口提供。

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 进行绘图并不直接。幸运的是,有许多代码示例可供专业人士解决此问题。查看以下免费提供的选项,了解如果你决定直接与 shapefile 一起工作而不是转换为 GeoJSON 格式,你如何处理在 Python 中绘制 shapefile 向量几何图形:

  • 使用 NumPy 数组和 matplotlib你可以使用 NumPy 数组将所有坐标挤压到一个一维数组中,并绘制这些坐标。

  • 使用 Shapely 并从现有的 shapefile 创建一个新的字典如果你知道如何重新组织现有的字典集合,你可以从现有的 shapefile 中创建一个新的字典,使用地理区域的名称作为键,该区域的几何数据作为值。接下来,你可以使用 Shapely 将这些字典的元素传递进去,并在 Python 中进行绘图。

  • 使用 pyshpmatplotlibpyshp 库可以用来读取几何信息,然后可以使用 matplotlib 进行绘图。

  • 使用 GeoPandas 和 matplotlibGeoPandas 库可以一起使用来读取 shapefile。不仅可以使用 matplotlib 的功能来绘制矢量数据,还可以读取属性表作为 pandas 数据框。

GeoPandas

GeoPandas 在第二章 地理空间代码库简介GeoPandas 部分中介绍,其中也涵盖了其数据结构和方法。

使用 GeoPandas 进行地理空间分析

GeoPandas 是为了向想要使用类似于 pandas 的空间数据的科学家提供数据而创建的,这意味着通过数据结构提供对地理空间属性数据的访问,这些数据结构在 pandas 中不可用。结合一系列几何运算、数据叠加功能、地理编码和绘图功能,你就有了这个库功能的一个概念。在接下来的例子中,我们将介绍 GeoPandas 的绘图方法,解释如何访问和子集化空间数据,并提供使用 GeoPandas 进行地理空间分析的典型工作流程,其中数据处理是正确分析和解释数据的重要条件。

让我们看看几个 GeoPandas 的代码示例。

使用 GeoPandas 和 Matplotlib 选择和绘制几何数据

以下脚本结合了 pandas 数据框在 GeoPandas GeoDataFrame 对象上的方法。一起使用,你可以轻松地子集化数据并绘制单独的特征几何图形。我们首先导入模块,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))

输出的图表将看起来像这样:

图片

使用 GeoDataFrame 上的 .cx 方法并传入边界框的值,可以得到相同的结果。此方法使用以下语法: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 年所有野火发生点的数据。我们可以使用野火数据的州字段来按州映射野火发生。但在这里,我们选择在具有州几何形状的单独 shapefile 上叠加数据,以说明空间连接的使用。接下来,我们将按州统计总野火数并绘制结果。GeoPandas 可以用来完成所有这些任务。

我们从导入模块开始:

In:   import geopandas

接下来,我们导入包含所有州边界的 shapefile:

In:  states =              
     geopandas.read_file(r"C:\data\gdal\NE\110m_cultural\ne_110m_admin_          
     1_states_provinces.shp")

通过重复变量名,可以显示文件的属性表作为一个pandas数据框:

In:   states

我们可以看到所有州名都列在名称列中。我们稍后会需要这个列。可以使用 Jupyter Notebook 中的魔法命令和来自matplotlibplot方法在内部绘制矢量数据。由于默认地图看起来相当小,我们将使用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))

地图看起来大致如下:

请查看fires GeoDataFrame中的名为 MTBS Zone 的列,并验证该数据集是否包含所有州名以引用数据。然而,我们有一个几何列可以用来连接这两个数据集。在我们这样做之前,我们必须确保数据使用相同的地图投影。我们可以如下验证:

In:    fires.crs
Out:   {'init': 'epsg:4269'}

In:    states.crs
Out:   {'init': 'epsg:4326'}

有两种地图投影,但它们都需要有相同的 CRS 才能正确对齐。我们可以如下将fires shapefile 重新投影到 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 年期间,佛罗里达州、加利福尼亚州和爱达荷州是三个野火最多的州:

这些值可以作为新字段输入到原始 shapefile 中,显示每个州的总野火计数:

In: states =        
    states.merge(counts_per_state.reset_index(name='number_of_fires'))
    states.head()

head 方法打印出 states 形状文件中的前五个条目,并在表格的右端添加了一个新字段。最后,可以创建并绘制每个州的野火计数柱状图如下:

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数据对象的子类,你可以使用它们的方法进行数据检查和清理。以我们之前使用的野火数据形状文件为例。通过列出我们的 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/,然后从“获取数据”链接中选择“阴影地形”、“一般”和“新墨西哥”。然后,下载“新墨西哥彩色阴影地形(地理参考 TIF)”文件。

当你解压 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()

使用GetProjection方法在 TIF 上,你会看到我们没有找到任何。代码的输出如下:

'LOCAL_CS[" Geocoding information not available Projection Name = Unknown Units = other GeoTIFF Units = other",UNIT["unknown",1]]'

如果你在这张 TIF 文件中打开它,QGIS 会显示一个警告,提示 CRS 未定义,并将默认为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 索引的,因此三波段栅格将具有索引123。你可以通过传递索引到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.0255.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') 

之前的代码导入了 numpymatplotlib.pyploy.imshow

NumPy 是一个用于处理数组的流行库。当处理栅格(数组)时,你将受益于对库的深入了解。Packt 出版了多本关于 NumPy 的书籍,如 NumPy CookbookNumPy Beginners GuideLearning 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。然后,它使用该驱动程序创建栅格。创建方法接受五个参数——namex 的大小、y 的大小、波段数量和数据类型。要获取 xy 的大小,你可以访问 a_raster.shape,它将返回 (4,7)。索引 a_raster.shape 将分别给出 xy

Create() 接受几种数据类型——从 GDT_ 开始。其他数据类型包括未知、字节、无符号整型 16 位、整型 16 位、无符号整型 32 位、整型 32 位、单精度浮点数 32 位、双精度浮点数 64 位、C 整型 16 位、C 整型 32 位、C 单精度浮点数 32 位和 C 双精度浮点数 64 位。

接下来,代码使用左上角坐标和旋转设置从地图到像素坐标的转换。旋转是宽度和高度,如果它是北向上图像,则其他参数为 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 上使用管道运算符,您将命令发送到 psqlpsql 使用 -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_PixelWidthST_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并传递栅格列和波段。您可以传递 bin 的数量作为第三个参数,或者让函数自行决定。结果如下所示:

[('(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)',)]

之前的输出是一个包含最小值、最大值、计数和百分比的 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_PixelAsPointsST_PixelAsCentroids,您可以检索栅格数据集中每个像素的点。

使用ST_PixelAsPoints,您可以检索表示每个像素左上角的点几何形状。查询还返回单元格的xy坐标以及值。以下代码将展示如何操作:

cursor.execute("SELECT x, y, val, ST_AsText(geom) FROM (SELECT (ST_PixelAsPoints(rast, 1)).* FROM bigi) as foo;")

cursor.fetchall()

之前的代码有两个部分查询。在FROM语句之后开始,查询选择波段1的像素作为点。第一个语句在结果上执行选择并检索点几何形状,以及单元格的xy和值。默认情况下,ST_PixelAsPoints不会返回没有值的单元格的数据。你可以将第三个参数传递为 false 以返回没有值的单元格。

之前查询的输出是一个数组,每行代表一个单元格。每行包含xy、值和几何形状。结果如下所示:

[(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函数,然后从该结果集中选择xy、值和几何形状。输出如下。注意,点与之前的示例不同:

[(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)')]

之前提到的函数返回了栅格数据集中所有像素的几何形状。这两个函数都有一个相应的函数,允许你指定单个像素。

从重心和点中移除复数形式将允许你指定单个像素,但不会返回xy和值。以下代码展示了如何将单个像素作为重心进行查询:

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()

之前的代码将波段和要搜索的值传递。此查询的结果是所有(xy)坐标的数组,其中值为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函数接受栅格列、一个点和xy距离值。在之前的代码中,你使用了ST_MakePointST_SetSRID来创建点。然后,你传递了点和xy距离参数的11距离。这将返回一个 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/的主开放数据网站上找到数据链接。滚动到页面底部并查找“安全数据集”标题。

创建表格

我们需要创建三个表格来存储犯罪数据。我们需要一个表格来:

  1. 区域指挥

  2. beat

  3. 事件

要创建表格,我们需要导入所需的库:

import psycopg2
import requests
from shapely.geometry import Point,Polygon,MultiPolygon, mapping
import datetime

程序代码导入psycopg2以连接到 PostGIS,requests用于调用服务以便你可以获取数据,从shapely.geometry中的PointPolygonMultiPolygon以使将GeoJSON转换为对象更容易,以及datetime因为事件有日期字段。

在 第三章,地理空间数据库简介 中,你创建了一个名为 pythonspatial 的数据库,用户名为 postgres。我们将在该数据库中创建表。为了填充表,我们将从服务中复制一些字段。服务的层页面底部有一个字段列表。

层的 URL 链接到服务的根页面或层编号。对于事件,层的 URL 为:coagisweb.cabq.gov/arcgis/rest/services/public/APD_Incidents/MapServer/0

每个字段都有 incidents 层的类型和长度,如下所示:

  • OBJECTID(类型:esriFieldTypeOID,别名:对象 ID)

  • Shape(类型:esriFieldTypeGeometry,别名:几何形状)

  • CV_BLOCK_ADD(类型:esriFieldTypeString,别名:位置,长度:72)

  • CVINC_TYPE(类型:esriFieldTypeString,别名:描述,长度:255)

  • date(类型:esriFieldTypeDate,别名:日期,长度: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)。接下来的两行创建了 beatsincidents 表,最后代码提交,使更改永久生效。

填充数据

在表就绪后,我们需要抓取数据并填充它们。以下代码将抓取区域命令并将其插入到我们的表中:

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 数组(data['features'])中的每个区域命令(acmd),我们将抓取 namegeometry

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,代码会通过,并使用trycatch块接受一个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表中的所有记录,包括它们的namegeometry作为GeoJSON,然后打印第一条记录(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_AsTextST_AsGeoJSON是从 PostGIS 中获取geometry的 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

之前的代码定义了地图的center,对于阿尔伯克基,我总是使用 I-25 和 I-40 的交汇点。这个交汇点将城市分为四个象限。然后代码定义了zoom级别——数字越高,缩放越近。最后,它打印了地图。

你将看到一个带有OpenStreetMap瓦片的空白底图。在 Jupyter 中,当你向地图添加数据时,你可以滚动回地图的原始打印版以查看数据;你不需要每次都重新打印地图。

区域命令的GeoJSON存储在变量c中。对于每个项目c[x]GeoJSON位于位置1c[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,我们将使用空间查询来限制我们的选择。

按日期统计事件

限制事件查询结果的一种方法是通过date。使用 Python 的datetime库,你可以指定一个date,然后查询该日期的incidents,并将结果的geometry作为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 日。它查询我们正在使用的 incidents 表,其中 date = d 并将 geometry 作为 GeoJSON 返回。然后,它使用您用于区域命令的 for 循环和 beats 来映射 incidents

当您在 Jupyter Notebook 中创建地图时,进一步的代码块将修改该地图。您可能需要向上滚动以查看地图以查看更改。

您创建的地图现在将看起来像下面的截图:

除了指定一个特定的 date,您还可以获取所有 date 大于特定日期的 incidents

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)))

或者,您可以查询早于今天的 interval 日期:

cursor.execute("select * from incidents where date >= NOW() - interval '10 day'")

之前的代码使用 NOW() 方法和一个 10 天 间隔。通过指定 >=,您将获得所有 10 天前以及更近期的犯罪。我在 2017 年 11 月 24 日写了这篇文章,所以结果将是 11 月 14 日(第 14 天)直到今天的所有 incidents

多边形内的 incidents

我们的犯罪数据库有一个多边形区域——区域命令和 beats——以及事件点。为了构建犯罪仪表板,我们希望能够映射特定区域命令或 beat 内的 incidents。我们可以通过使用 JOINST_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)

之前的代码从 incidents 中选择 geometry 作为 GeoJSON (ST_AsGeoJSON(i.geom) 来自 incidents),其中事件 ST_Intersects 多边形区域命令,具体来说,区域命令的名称是 FOOTHILLS。代码通过将事件和区域命令表连接起来,其中交集为真。代码通过仅选择过去 10 天的犯罪来限制结果。

代码随后遍历结果并将它们映射,您应该看到下面的截图:

上一张截图将 incidents 展示在 Foothills 区域命令上。注意所有 incidents 都在多边形内。

您可以通过更改 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 进行连接来选择 incidents,并映射 incidents,如下面的代码所示:

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 天再次进行筛选。然后,它映射了 incidents,如下面的截图所示:

缓冲区

您已经从表中映射了数据,但现在您将映射地理处理任务的输出结果——缓冲区。

要编写一个缓冲区示例的代码,我们首先必须创建一个点。以下代码将为我们完成这项工作:

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() 传递给名为 bshapely 多边形。现在,您可以将该多边形传递给另一个查询,使用 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 作为 GeoJSON,并且它们在过去的 10 天内。结果被映射。以下地图显示了之前代码的输出:

图片

最近邻

使用 buffer,您可以获取到兴趣点指定半径内的所有 incidents。但如果你只想获取 5、10 或 15 个最近的 incidents 呢?为了做到这一点,您可以使用 <-> 操作符或 k-最近邻算法。

您可以使用以下代码选择到指定点 p15 个最近点:

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,然后计算每个事故与指定点的距离。ORDER BY 子句、<-> 操作符和限制子句确保我们按接近程度顺序获取最近的 15 个点。

最后一块代码是我们添加结果到地图的代码。以下截图显示了结果。截图中心的是指定的点:

图片

现在您已经知道了如何映射空间查询的结果,让我们添加交互式小部件来修改查询并更改地图,而无需编写新代码。

交互式小部件

在本章开头,您学习了如何根据 date 查询和映射 incidents。在 Jupyter 中,您可以使用交互式小部件更改值。代码将帮助我们了解如何使用 ipywidgetsinteract 来插入 DatePicker,这样您就可以选择一个 date 与笔记本进行交互:

图片

之前的代码导入了 interactDatePicker 小部件。在最简单的情况下,之前的截图显示了一个装饰器和函数,允许交互式选择一个 date 并将其显示为字符串。

DatePicker 发生变化时,xDatePicker)被发送到函数 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 将是 noneDatePicker 未被选中,所以我们第一次遍历时 pass

接下来,代码获取地图上的所有图层,并使用 map.remove_layer() 删除它们,从第二个([1:])图层开始。为什么是第二个图层?因为地图上的第一层是 TileLayer——基础地图。我们希望它保持原样,只删除从 SQL 查询中添加的标记。

代码随后从 date 字符串中删除连字符,并将其转换为 datetime。一旦它成为 datetime,就可以将其传递给 SQL 查询。

下一个代码块与您在本章中用于将查询结果添加到地图上的代码块相同。

以下截图显示了选择 2017 年 11 月 2 日的 date

图片

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

图片

这些截图是在重新选择 date 后立即生成的。用户可以使用 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的另一个例子来演示。以下代码将按beats加载犯罪:

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表中选择了beatagencyincidents计数。注意left joinleft join将给我们可能没有incidentsbeats。连接基于一个事件位于beat多边形内。我们按每个选定的字段进行分组。

查询被加载到DataFrame中,并显示head()。结果如下截图所示:

注意,我们没有显示没有犯罪的beats而不是缺失的beatsbeats太多,无法滚动查看,所以让我们图表DataFrame。我们将再次使用绘图函数,传递xykindfigsize如下:

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')

之前的代码将一个表达式传递给区域。该表达式选择DataFrameCrimes列的记录,其中值超过800Crimes是计数列。结果如下所示:

图片

将您的查询加载到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))

之前的代码创建了一个只有addressgeom的事件。执行之前代码的结果如下所示:

图片

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

图片

触发器可以用来防止在发生更改时加载不良数据用于电子邮件或短信发送。例如,您可以允许用户输入他们感兴趣的多边形以及他们的电话号码。当数据库中添加新的事件时,您可以检查该事件是否位于多边形内,如果是,则向与多边形关联的电话号码发送短信。

要为触发器安装其他语言,请打开Stack Builder并添加以下截图所示的附加组件:

图片

摘要

在本章中,你学习了如何使用空间查询来执行地理处理任务。你还学习了如何使用ipyleaflet和数据框将查询结果映射和制图。你学习了如何使用 Jupyter 中的交互式小部件修改地图和查询。最后,你了解了触发器的工作原理,并展示了使用触发器进行数据检查的快速示例。

在下一章中,你将学习如何使用 QGIS 执行地理处理任务。你将学习如何使用 QGIS 中已包含的工具箱。你将学习如何编写自己的工具箱,以便你可以使用并与其他 QGIS 用户共享,你还将学习如何使用 QGIS 来映射结果。结果可以保存为 QGIS 项目,或作为 QGIS 的许多空间数据格式之一。

第八章:自动化 QGIS 分析

本书已向你介绍了如何从命令行、Jupyter 笔记本和 IDE 中使用 Python 执行地理空间任务。虽然这三个工具将允许你完成任务,但很多时候需要使用桌面 GIS 软件来完成工作。

QGIS,一个流行的开源 GIS 应用,提供了桌面 GIS 功能,具有在 Python 控制台工作的能力,以及使用 Python 编写工具箱和插件的能力。在本章中,你将学习如何使用 Python 操作桌面 GIS 数据,以及如何使用工具箱和插件自动化这些任务。

在本章中,你将学习如何:

  • 加载和保存图层

  • 从 API 数据源创建图层

  • 添加、编辑和删除要素

  • 选择特定要素

  • 调用地理处理函数

  • 编写地理处理工具箱

  • 编写插件

在 Python 控制台工作

QGIS Python 控制台是一个 Python 控制台。你可以执行所有正常的 Python 任务,并且有 QGIS 库的附加优势。从控制台,你可以操作 GIS 数据并在屏幕上显示,或者不显示。

Python 控制台位于 QGIS 工具栏的“插件”菜单下。你也可以通过按Ctrl+Alt+P键在键盘上访问它。控制台通常会打开在主窗口的底部。你可以通过点击标题栏(显示为 Python Console),按住鼠标按钮,并将窗口拖到屏幕上的另一个位置或点击控制台右上角的窗口按钮来取消停靠:

图片

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() 

之前的代码应显示加载了两个图层,SeeClickFixStreets

{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 图层上尝试相同的代码,我们会得到类型 1Polyline 的坐标,如下所示:

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() 遍历特征。以下代码将遍历特征并 print 所有具有 '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,正如你在第三章 [42c1ea5a-7372-4688-bb7f-fc3822248562.xhtml] “地理空间数据库简介”和第七章 [4f2388a3-51fc-419f-a827-bbbedbbb6374.xhtml] “地理数据库处理”中学到的。本节中添加特征到图层以及图层到地图的方法将在本章后面学习如何编写工具箱时使用。

绘制点

在你学习如何从 PostGIS 加载数据之前,我们首先介绍如何绘制多个点,将它们转换为特征,将它们添加到图层中,然后将图层加载到地图上。以下代码将引导你完成这个过程。

首先创建一个 memory 图层,如下所示:

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

之前的代码创建了一个 polygon 内存图层,创建了要素并添加了属性。接下来,您将遍历 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 获取的 WKT MultiPolygon 字符串。

最后,按照以下代码将图层添加到地图上:

QgsMapLayerRegistry.instance().addMapLayers([APD])

以下代码将渲染如下截图:

图片

就这样,你有了阿尔伯克基警察局的区域命令多边形,作为一个层在 QGIS 中。接下来,你将学习如何向层中添加、编辑和删除特征。

添加、编辑和删除特征

在之前的示例中,你创建了一个空层并添加了字段,然后添加了数据并显示了它。你将需要这样做的时候,大多数情况下,你已经有了一个层,你需要向其中添加数据、编辑数据或从其中删除数据。在本节中,你将学习如何对现有数据执行这些任务。

向现有层添加特征

要向层添加数据,你首先需要加载层。首先,加载一些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()的结果将是 true 或 false。返回的特征是特征列表。如果你需要对该特征执行更多操作,保留特征可能很方便。

在自动化过程时,你可以在尝试编辑层之前执行功能检查。

结果是一个新的点和属性表中的记录,如以下截图所示:

图片

添加到层的特征

你可以通过使用列表在单行中传递所有属性来简化之前的代码。以下代码显示了如何做到这一点:

feat.setAttributes([912,312,"Other","Closed"])

之前的代码使用列表和setAttributes()而不是单个setAttribute()来写入属性。如果你想记住在以后阅读代码时字段名称,更冗长的版本更清晰。但如果代码的效率是你的目标,后者版本更合适。

如果我们犯了错误,或者有我们不需要的记录怎么办?下一节将向你展示如何删除一个特征。

从现有层中删除特征

删除特征可以通过单行代码完成,格式如下所示:

LayerName.dataProvider().deleteFeatures([list of id])

在之前的代码中,你使用deleteFeatures()函数和层的ididfeature.id(),它是一个内部持有的数字,而不是用户分配的属性。要获取特定特征的id,你可以像在本章前面学到的那样遍历它们。以下代码显示了如何删除上一节中创建的特征:

for x in scf.getFeatures():
    if x["ID"]==311:
        scf.dataProvider().deleteFeatures([x.id()])

之前的代码遍历了层中的特征,寻找具有ID311的那个。当找到它时,它使用deleteFeatures()函数并通过x.id()传递id。在这种情况下,id216。如果你知道特征的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}}。属性字典的键是字段索引。在特征中有四个字段,因此属性字典将有键03。以下截图显示了属性表的变化:

单个特征编辑

之前的示例假设你已经知道要修改的特征的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])

之前的代码导入了QtGUIQt.Widgets。这些是使用QColor设置颜色所需的。下一行获取地图画布并设置部分颜色为红色。最后,代码选择了一个id100的特征。现在地图上会显示为红色

之前的例子假设你想选择一个单一的特征,并且你知道其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 中打开处理工具箱。这将打开一个编辑器窗口。你将在该窗口中编写你的代码,并使用保存图标保存它。文件名将成为工具箱,位于工具|用户脚本|文件名下。保存文件并命名为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库以及requestsjsonrequests库将用于进行 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;amp;place_url="+scf city+"&amp;amp;page=" 
    url="http://seeclickfix.com/api/v2/issues?per_page=100&amp;amp;place_url="+scfcity 
else: 
    pagesURL="http://seeclickfix.com/api/v2/issuesper_page=100&amp;amp;place_url="+scfc ity+"&amp;amp;search="+searchterm+"&amp;amp;page="
    url="http://seeclickfix.com/api/v2/issues?per_page=100&amp;amp;search="+searchterm+"&amp;amp;place_url="+scfcity 

之前的代码将参数传递给变量。然后,我使用了一个全局变量progress,这是 QGIS 提供的,并调用setInfo()setText()方法来告诉用户正在发生什么。progress是 QGIS 的一部分。setInfo()方法在 GUI 的文本区域中显示文本。setText()方法更改进度条上标签的文本,并将其添加到 GUI 的文本区域中。

接下来,代码检查过滤器参数是否仍然是None,如果是,它将统一资源定位符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 调用。它为页数和返回的记录数分配变量。使用setInfo()方法,代码告诉用户正在处理多少records。然后,它遍历每一页并从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 上的progress条。该方法接受一个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

本章将介绍用于 Python 和 ArcGIS Online 的 ArcGIS 应用程序编程接口 (API)。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

  • 使用 API 与不同的 Esri 用户账户

  • 介绍 API 的某些模块

  • 与 API 的地图小部件交互

  • 搜索和显示矢量数据

  • 显示和地理处理栅格数据

  • 为使用 ArcGIS Online 设置个性化账户

  • 在 ArcGIS Online 中发布和管理内容

介绍 ArcGIS API for Python 和 ArcGIS Online

以 ArcGIS 平台而闻名的地理空间软件公司 Esri 将 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 或更高版本。可以使用 API 与ArcPy站点包一起使用,但这不是必需的,API 也可以在没有ArcPy(或任何基于桌面的 GIS 产品)的情况下工作,甚至在没有 ArcGIS Online 或 Portal 环境的情况下。

该 API 是为比当前 Python 用户更广泛的受众编写的,Python 用户可能会用它来进行数据处理或地图设计——除了脚本功能外,该 API 还允许进行 GIS 可视化与分析、空间数据/内容管理以及组织管理。该 API 仍在开发中,自 2006 年 12 月首次发布以来,API 已经经历了几次更新,当前版本为写作时的 1.4 版。每个新版本都引入了新功能。使用 API 与使用任何其他 Python 库类似——您通过import语句导入 API,然后可以立即开始使用,应用标准的 Python 语法和命令。当您在 Web 环境中使用它来访问 Web GIS 时,最好使用基于浏览器的 Jupyter Notebook 应用。

安装 API

API 可以通过不同的方式安装。最简单的方法是使用conda。如果您是第一次安装 API,您可能想通过 Anaconda3 为 API 创建一个单独的虚拟环境,因为 API 有许多依赖项。安装 API 的最新可用版本很重要,因为它还将确保您安装了最新的conda版本以及 API 的依赖项。要使用conda安装 API,请在终端中运行以下命令:

conda install -c esri arcgis

命令中的-c代表一个频道(这是一个在线仓库)。当在终端中运行此命令时,您将被要求安装一系列依赖项。以下截图显示了部分列表。请注意,NumPypandas也被安装了,这两个库来自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:

  1. 一个匿名用户账户,它允许您访问 ArcGIS Online 而无需传递任何用户信息。这是一个快速测试一些基本功能的解决方案,但不会提供带有个性化账户的高级功能。我们将在本章接下来的三个动手练习中的两个中介绍此选项。

  2. 一个 ArcGIS Online 组织账户(或 Portal for ArcGIS 账户)。这需要订阅 ArcGIS Online 或 Portal for ArcGIS(付费)。此选项提供了可能的最大功能,但在此处未涵盖。

  3. ArcGIS Enterprise 试用账户。这个选项是免费的,并提供你创建地图和发布内容所需的服务积分。这个试用账户仅持续 21 天,之后必须转移到付费账户才能继续使用。设置试用账户将在我们进一步进行时进行说明。

  4. 一个免费的 Esri 开发者账户。这个账户是 ArcGIS 开发者计划的一部分,为你提供 50 个服务积分,用于开发、测试个人应用程序,以及使用 ArcGIS Online 等。这个选项将在我们进一步进行时进行说明。

  5. 最后,还有创建公共 ArcGIS 账户并使用网络浏览器登录 ArcGIS Online 的选项。使用这些登录详情,你现在可以使用 API 连接到 ArcGIS Online,但功能有限。这个选项是在 API 的 1.3 版本中添加的,这里没有进行说明。

总结前面提到的内容,我们已经涵盖了多个不同的用户账户,以便使用 API 访问 ArcGIS Online。个性化账户比匿名账户提供了更多的功能。我们将在本章后面的练习中使用这两种类型。现在让我们看看 API 是如何组织成不同的模块以及它们提供的功能。

Python ArcGIS API 的不同模块

就像其他 Python 库一样,API 有 Python 模块、类、函数和类型,可以用来管理和处理 ArcGIS 平台信息模型中的元素。由于 API 旨在满足需要自己独特工具的不同用户群体,因此 API 被组织成 13 个不同的模块。在这里不需要涵盖所有模块,但本章最重要的模块如下所述:

  1. GIS 模块:这是最重要的模块,是访问托管在 ArcGIS Online 或 ArcGIS Portal 中的 GIS 的入口点。GIS 模块允许你在 GIS 中管理用户、组和内容。在这个上下文中,GIS 指的是一个用于创建、可视化和共享地图、场景、应用程序、图层、分析和数据的协作环境。

  2. 特征模块:此模块代表 API 的矢量数据部分。矢量数据通过此模块表示为特征数据、特征层或特征层的集合。单个数据元素由特征对象表示,而FeatureSetFeatureLayerFeatureCollection等类则代表特征数据的不同分组。

  3. 栅格模块:此模块包含用于处理栅格数据和影像层的类和栅格分析函数。而特征模块代表 API 的矢量数据组件,栅格模块则是栅格数据组件。此模块使用Imagerylayer类来显示影像服务的数据,并提供实时图像处理的栅格函数。可以使用地图小部件来可视化影像层。

  4. 地理处理模块:此模块用于导入具有地理处理功能的工具箱,这些功能不是 API 的一部分,但可通过 ArcGIS Online 获得。这些地理处理工具箱作为原生 Python 模块导入,以便您可以调用导入模块中可用的函数来调用这些工具。API 本身还包括丰富的地理处理工具集合,这些工具可通过定义空间数据类型的其他模块获得。

地理处理工具是一个在 GIS 数据上执行操作的函数,从输入数据集开始。然后在该数据集上执行操作,最后将操作的结果作为输出数据集返回。

  1. 小部件模块:它提供了用于可视化 GIS 数据和分析的组件,包括 MapView Jupyter Notebook 小部件。我们将使用此小部件来可视化地图和图层。这不是唯一的可视化模块——独立的映射模块提供了不同的映射层和 2D/3D 映射和可视化组件。

如您所见,该 API 为不同任务和用户提供了广泛的功能模块,包括发布地图数据、执行地理空间分析和数据处理。所有模块都使用 Python 作为脚本语言来管理 GIS 数据和功能。现在,让我们开始使用 API,探索一些基本功能,然后再进行更高级的任务。

练习 1 - 导入 API 和使用地图小部件

现在是时候开始使用 API 了。按照说明操作,在 Jupyter Notebook 应用程序中打开一个新的 Notebook,您可以在其中访问 API。输入并运行以下代码。我们将首先导入 API,以便我们可以使用其模块、函数和类:

In:   import arcgis
In:   from arcgis.gis import GIS

代码的第二行可以分解如下——arcgis.gis指的是arcgis模块中的一个子模块(gis)。被导入的(GIS)是一个包含用于显示地理位置、可视化 GIS 内容和分析结果的地图小部件的GIS对象。接下来,我们将通过将相同名称的变量分配给它来创建一个GIS对象,但拼写为小写:

In:   gis = GIS()

这是一个匿名登录的示例,因为我们没有在GIS()括号中传递任何登录详情。现在,我们将通过创建一个地图对象并将其分配给一个变量来使用地图小部件,然后可以查询该变量以在 Notebook 中显示小部件:

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

我们可以看到我们的底图现在显示了旧金山的卫星图像:

![

接下来,我们将查询我们地图小部件中显示的地图的坐标参考系统CRS)。此信息可以通过范围属性查询,它还会显示我们 extent 的四个坐标:

In: map1.extent

Out: {'spatialReference': {'latestWkid': 3857, 'wkid': 102100},
 'type': 'extent',
 'xmax': -13505086.994526163,
 'xmin': -13658266.799209714,
 'ymax': 4578600.169423444,
 'ymin': 4517450.546795281}

让我们看看输出结果。我们的底图是一个网络地图的例子,以 JavaScript 对象表示法JSON)格式分享 2D 地图。网络地图是 Esri 规范的一个例子,允许不同的应用程序、API 和 SDK 创建、编辑和显示地图。这些网络地图可用于不同的 Esri 应用程序,例如在本例中的 ArcGIS Online。网络地图规范是 JSON这确实是我们通过查看其使用括号和键值对的结构来观察到的输出情况。

回到空间参考,这些信息存储在位于网络地图 JSON 层次结构顶层的 spatialReference 对象中。在我们的例子中,我们可以看到空间参考被设置为 latestWKid: 3857wkid: 102100。通过查阅 Esri 在 developers.arcgis.com 提供的在线网络地图规范,我们可以看到两者都指的是 Web Mercator 投影,这是网络地图应用程序的事实标准,并被大多数主要在线地图提供商使用。

这标志着我们 API 的第一个动手练习的结束,我们学习了如何导入 API、创建地图对象、显示对象属性的信息以及使用地图小部件。在我们下一个练习中,我们将开始使用来自 ArcGIS Online 的内容并将其添加到我们的地图小部件中。我们将使用个性化账户,这使我们能够创建自己的网络地图并在网上托管它们。在我们能够这样做之前,您需要创建一个个性化的 ArcGIS Online 账户,我们将在下一部分介绍。

创建个性化的 ArcGIS Online 账户

在接下来的练习中,您需要一个名为的用户账户用于 ArcGIS Online。这将使您能够创建自己的地图内容,将网络地图保存在 ArcGIS Online 中您自己的内容文件夹中,与他人共享内容,等等。我们将介绍两种免费选项来完成这项任务。最简单、最快的方法是创建一个免费的 ArcGIS 开发者账户,该账户包含使用 ArcGIS Online 一些功能所需的服务积分。还有可能创建一个免费的 ArcGIS Online 组织试用账户,它提供了更多选项。这里将介绍这两种选项。

要创建一个 ArcGIS 开发者账户,打开浏览器窗口并导航到developers.arcgis.com/sign-up。填写左侧的字段(名字、姓氏和电子邮件):

图片

接下来,您将收到一封确认邮件,该邮件将发送到您在线输入的电子邮件地址。邮件中包含一个您需要点击以激活账户的 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

要搜索我们组织之外的具体内容,请使用以下包含特定搜索词查询的代码。在这里,我们使用了“在”和“附近”旧金山的“trails”:

In: search_result = gis.content.search(query="san francisco trail", 
    item_type="Feature Layer", outside_org=True)
    search_result

在前面的代码中,我们正在使用 GIS 对象的 content 属性来搜索内容。使用个性化账户,我们指定我们想要搜索我们组织之外的数据。我们的查询正在寻找类型为"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

代码的工作原理如下,第一行创建了一个变量,该变量引用了我们的搜索结果列表中包含自行车路径服务层的项目。接下来,我们将使用此项目的图层属性来查看该项目包含多少图层,在这种情况下有两个图层,分别索引为01

接下来,我们想要两个图层的名称。使用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

Python 返回我们可以立即通过点击蓝色下划线 URL 在线访问的项目,在提供登录凭据以访问我们的 ArcGIS Online 组织页面后:

图片

我们可以在概述标签中编辑元数据,而在设置标签中可以删除它(向下滚动以查看标记为红色的此选项)。

返回到我们的 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 Online 的地理处理功能,通过 ArcGIS API for Python 的栅格模块。

摘要

本章介绍了全新的 ArcGIS API for Python,它是基于 Python 3.5 构建的。你学习了如何使用 API、Jupyter Notebooks 以及存储在基于云的 ArcGIS Online 系统中的数据处理。我们涵盖了 API 如何组织成不同的模块、如何安装 API、如何使用地图小部件、如何使用不同的用户账户登录 ArcGIS Online,以及如何处理矢量数据和栅格数据。使用一些 API 模块,我们学习了如何使用 Python API 进行基本的地理空间分析和创建 ArcGIS Online 网络地图。

下一章将介绍用于与云数据交互的 Python 工具,用于搜索和快速数据处理。特别是,它侧重于 Elasticsearch 和 MapD GPU 数据库的使用,这两个数据库都基于 AWS 云基础设施。读者将学习如何创建用于地理空间搜索、地理定位数据处理、地理定位数据的云服务,以及如何使用 Python 库与这些服务交互。

第十章:使用 GPU 数据库进行地理处理

随着多核 GPU 的出现,新的数据库技术已被开发出来以利用这项改进的技术。MapD 是一家位于旧金山的初创公司,是这些公司的一个例子。他们的基于 GPU 的数据库技术在 2017 年开源,并可在云服务(如Amazon Web ServicesAWS)和 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 TransferREST) 网络服务。ArcGIS Online 建立在 Amazon Web ServicesAWS) 之上,所有的服务器架构都隐藏在用户视线之外。企业集成需要高级别的许可(成本),这包括许多云令牌(即信用额度),并且云账户内的存储和分析可以使用大量这些令牌。

CARTO 提供云 PostGIS 存储,允许上传地理空间数据文件。随着 Python 包 CARTOframes(在第十四章中介绍,云地理数据库分析和可视化)的发布,云数据集可以通过脚本上传和更新。使用 Python,CARTO 账户可以成为企业解决方案的一部分,该解决方案维护最新的数据集,同时允许它们通过构建应用程序快速部署为定制网络地图。CARTO 提供两个付费账户级别,它们具有不同的存储级别。

MapBox 专注于为移动应用创建定制基础图的地图工具,但它还提供云数据存储和地图创建工具,如 MapBox GL,这是基于Web 图形库WebGL)的 JavaScript 库。使用新的 MapBox GL—Jupyter 模块,数据可以通过 Python 访问。

虽然 MapD 提供与上述类似解决方案,但在许多方面有所不同。它有一个开源的数据库版本(MapD Core 社区版),可以在本地或云上使用,并为大型客户提供企业版。虽然 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 在检索行和将数据返回给客户端时速度极快,这使得它对于驱动实时数据库或对大型数据集进行查询非常有用。

MapD 在处理大数据集方面比 CPU 密集型数据库提供了惊人的加速。由于每个 GPU 卡包含的内核数量众多,并行进程可以运行得更快。这意味着数十亿的数据集可以在毫秒内查询和分析。

MapD 架构

MapD 的架构是 MapD Core(基于 GPU 的数据库)、MapD Immerse(数据可视化组件)和其他支持数据科学操作和地理空间应用的相关技术和 API 的组合:

图片

利用快速的查询速度和 API,以及 pymapd,组件可以一起使用或单独使用来创建地理数据库和可视化。存在多个数据导入器的驱动程序,以帮助数据迁移,而 Thrift API 可以提供数据导出或与软件包和 Immerse 通信。

云端与本地与混合

由于许多不同类型的组织依赖于地理数据库,架构选项也相当多样。虽然一些组织已经将所有数据迁移到云端,将数据和工具存储在不同的服务器上,但大多数组织仍然维护一个本地地理数据库作为企业系统。

第三种架构风格,在基于云和本地地理数据库之间取得平衡,也非常受欢迎。这允许数据库备份始终由可用的云服务支持,并且数据服务可以超出组织防火墙的限制,同时限制暴露给互联网的数据集和服务。

这些解决方案之间的平衡取决于对处理速度和存储成本的需求。MapD 可以本地安装和维护,也可以在云端托管,满足各种组织需求。查询和数据处理的快速速度允许以与本地存储数据集相同的方式使用云数据资源。使用 pymapd,数据集可以轻松地在云端镜像,同时本地维护,并且可以通过比较本地存储的数据与云端数据集成到地理空间分析中。

您组织选择的技术结构将取决于您的需求和产生以及从其他来源摄取的数据集的大小。MapD 可以成为该结构的一部分,也可以是整个 GIS,无论是在本地、云端还是两者兼而有之,都能以极快的速度支持空间 SQL 查询。

在云端创建 MapD 实例

要探索使用 MapD Core 和 MapD Immerse 在本地和云端混合使用 GIS 的可能性,让我们在云端创建一个实例(虚拟服务器)。这个云数据库将本地访问,使用 pymapd 执行查询和数据管理任务。

使用 AWS,我们可以创建一个支持 GPU 的服务器。虽然我在这里使用 AWS,但 MapD 可以加载到其他云服务中,如 Google Cloud 和 Microsoft Azure,也可以本地安装。这些其他云服务也提供社区版。

查找 AMI

我将在 p2.xlarge AWS 实例上使用 MapD 社区版,这是平台的开源版本。社区版的预构建亚马逊机器镜像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

使用 MapD Immerse 内置的数据导入器轻松导入 CSV 格式的数据集。转到数据管理器并选择导入数据。在下一页上,点击添加文件按钮,并使用拖放方式加载包含的 Juneau 市地址 CSV 数据集。

数据将被加载,一旦加载完成,就会从上传的数据中生成一个 MapD 数据库表。请检查数据,并添加一个新名称或接受默认名称(由电子表格名称生成)。点击“保存表”后,数据库表将被生成:

图片

创建图表

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

图片

使用 SQL 编辑器进行选择

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

图片

SQL 语句执行得非常快,并将包括可以执行空间分析的 SQL 选择语句的空间 SQL 命令。

使用地理空间数据

MapD Core 支持几何和地理数据类型,并可以使用坐标列生成交互式地图来显示带有x/y或经纬度对的地理数据。点地图、热力图和分县图可以很容易地使用 Immerse 仪表板环境生成和样式化:

图片

这份数据可视化是通过将 OpenAddresses 中的 Calaveras 县地址 CSV 加载到我的 MapD Immerse 实例中创建的,然后使用数据管理器并使用经度和纬度列创建热力图。

使用终端连接到数据库

使用集成的基于 Java 的终端或另一个终端解决方案连接到数据库。由于我的本地机器使用 Windows,并且没有集成到操作系统中的终端,我已经下载并安装了 PuTTY。这款免费的 SSH 软件允许我从 Windows 机器连接到 Linux 命令行服务器,使用之前生成的密钥对进行身份验证。

如果您正在使用 Windows 的另一个终端解决方案或使用其他操作系统,请使用终端的正确 SSH 过程连接到实例。步骤将类似,但需要将所需的私钥格式转换为。

请在此处下载 PuTTY 终端:www.chiark.greenend.org.uk/~sgtatham/putty/

PuTTYgen

要授权任何连接到 AWS 实例,必须使用关联程序 PuTTYgen 将为 AWS 账户生成的私钥转换为 PuTTY 密钥格式。从开始菜单打开 PuTTYgen,点击“转换”菜单。从下拉选项卡中选择“导入密钥”。

将会打开一个文件对话框,允许您选择从 AWS 下载的私钥。这个私钥将具有 .pem 扩展名:

点击“打开”,并将密钥导入。要生成 PuTTY 格式的私钥,提供一个可选的密钥短语(一个或几个进一步识别用户并必须记住的词),然后在“操作”部分点击“保存私钥”按钮。选择一个文件夹并保存密钥,现在它将具有 .ppk 文件扩展名。

连接配置

使用 PuTTY 连接到实例需要一些配置。要创建连接,将实例的公共 IP 地址粘贴到“主机名”字段中,确保端口设置为 22,并且连接类型是 SSH。通过点击“保存”按钮在“已保存会话”部分保存设置:

使用私钥

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

一旦找到密钥,点击“打开”以建立连接。要启动服务器上的 MapD,请转到 /raidStorage/prod/mapd/bin 文件夹并运行以下代码,将 {Instance-ID} 替换为您的实例 ID:

./mapdql mapd -u mapd -p {Instance-ID}

如果您在建立连接时遇到问题,请检查确保 AWS 实例的安全组设置为允许从当前使用的计算机进行连接。如果安全组设置是“我的 IP”且计算机的 IP 地址不同,则无法建立连接。

安装 pymapd

安装 pymapd 很简单,并且由 condapip 支持,这是 Python 包安装程序。我在本章中使用 pip,但使用 conda 不会引起任何问题,并且可能建议与其他 conda 支持的软件集成。

conda 安装命令

使用 conda install -c conda-forge 连接到 conda forge,这是存储 pymapd 模块的仓库。有关 conda 的更多信息,请参阅 第一章,包安装和管理

conda install -c conda-forge pymapd

pip 安装命令

使用 pip,Python 安装程序包,也可以使用 pymapd 模块。它会从 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 命令(空间或其他类型),我们将创建一个数据 光标光标连接 类的一部分,将用于使用 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])

结果是一个元组列表,其中只包含(在这种情况下) 的名称,通过使用零索引从元组中获取它。

创建一个表

建立连接后,我们现在可以在 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 字段和一个名为 addressVARCHAR 类型的字段:

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 语句模板中。然后,该语句由 光标 执行:

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的第一个输入,第二个输入是已知文本WKTpoint。一旦执行 SQL 语句,point将与表中的所有行进行比较,以找出是否有任何一个county几何包含由 WKT point描述的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_XMinST_XMaxST_YMinST_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 Web 框架——编写用来管理创建和维护复杂和动态网站的代码库。

为了更好地理解如何使用 Python Web 框架以及如何添加地理空间功能,我们将实现 Flask 模型-视图-控制器MVC)框架。Flask 是一个纯 Python Web 框架,可以与 SQLAlchemy、GeoAlchemy2 和 Jinja2 HTML 模板系统结合使用,以创建具有地理空间功能的网页。

在本章中,你将了解:

  • Flask Web 框架

  • SQLAlchemy 数据库管理

  • GeoAlchemy2

  • 使用对象关系映射(ORM)连接到 PostGIS

  • Jinja2 网页模板系统

Flask 及其组件模块

与 Django 和 GeoDjango(在第十二章中介绍,GeoDjango)不同,Flask 不包含电池。相反,它允许根据需要安装多个支持模块。这给了程序员更多的自由,但也使得必须单独安装所需的组件。

我已经为这一章选择了一些模块,这些模块将使我们能够创建一个具有地理空间组件的 Flask 应用程序。以下各节将详细介绍如何设置、安装和利用这些模块来生成网站,使用具有 PostGIS 数据库后端(如第七章中所述,使用地理数据库进行地理处理)的演示网站,以及通过基于 Web 的界面执行空间查询的能力。

设置

为了确保 Flask 应用程序及其与 PostgreSQL 和 PostGIS 数据库组件的连接能够按需运行,必须安装一些重要的 Python 模块。这些模块将通过pip下载和安装,pip连接到Python 包索引PyPI),这是一个位于pypi.python.org/pypi的在线注册模块仓库。

这些模块包括:

其他重要的支持模块会自动与 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 通过 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 服务器在本地进行测试和部署。此 Web 应用程序允许用户查找与全国不同场馆相关的县、州和国会选区。此应用程序将涉及从美国地质调查局USGS)数据目录下载数据,并将有视图(处理 Web 请求的 Python 函数)使用 GeoAlchemy2 ORM 进行地理空间查询,并使用 SQLAlchemy ORM 进行表关系搜索。

此应用程序需要使用两个脚本,用于创建数据库和数据库表。这些脚本在进一步说明中会详细介绍,并位于书籍代码包的Chapter11文件夹中。最终产品将是一个使用Leaflet JavaScript 地图显示基于 ORM 的空间查询结果和关系查询的 Web 应用程序。

从数据源下载数据

要开始此项目,让我们从 USGS 数据目录下载数据。此项目将使用四个基于美国的形状文件——NBA 场馆形状文件、州形状文件、国会选区形状文件和县形状文件。

美国地质调查局在此处提供了大量可供下载的 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_engineurl 属性,该属性是连接字符串的引用:

# 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 网络框架的世界中,数据库表是模型。网站使用这些表来存储数据,它们由 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类和StringFloatInteger类型类从 SQLAlchemy 调用,并用于定义要在底层 RDBMS 中生成的表列(从而允许程序员避免为每个主要 RDBMS 使用的各种 SQL 编写CREATE TABLE语句)。

例如,Arena类将用于管理一个具有四个列的表——一个String name字段,两个Float字段(longitudelatitude),以及一个 SRID 或 EPSG 空间参考系统 ID 为4326POINT几何类型,对应于 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几何类型来存储用于建模这些地理的复杂多边形几何。对于具有关系的表,例如CountyDistrictState类,还有用于管理表关系和表之间查询的特殊类。

这些特殊类包括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将用于查找和读取下载的形状文件中的数据,并使用for循环读取数据并将其写入相应的数据库表。将使用 SQLAlchemy 会话管理器来查询和提交数据到表中。

导入所需的模块

为了处理和导入数据,将使用几个新模块。pyshapefile模块(或作为 shapefile 导入的pyshp)用于连接到形状文件,并读取它们包含的几何形状和属性数据。pygeoif模块是一个纯 Python 模块,实现了名为geo_interface的协议。

此协议允许 Python 对象级别的地理空间数据自省,例如,它将地理空间数据格式转换为 Python 对象。它将被用于将存储在二进制中的形状文件几何形状转换为可以插入数据库的 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

要使脚本能够找到下载的形状文件,请使用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类派生。这些类定义将与上一个脚本中的定义相匹配。

定位和读取形状文件

要创建允许用户搜索和定位形状文件的文件对话框,请实例化 Tkinter 的Tk类并将其分配给变量rootTk类创建了一个不必要的迷你控制台窗口,因此使用root.withdraw方法将其关闭:

# Initiate the Tkinter module and withdraw the console it generates
root = Tk()
root.withdraw()

文件对话框是通过filedialog.askopenfilename方法生成的。该方法接受多个参数,包括文件对话框窗口的title、初始目录以及在使用文件对话框时应可见的文件扩展名。以下是一个示例代码,用于选择竞技场形状文件对话框:

# 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类。实例化的类将具有recordsshapes方法,分别允许访问 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 shapefile 的几何形状,Readershapes方法返回一个包含坐标对的列表。由于Arena类几何列是POINT数据类型,数据可以使用POINT(X Y) WKT 模板写入数据库表。SRID(4326)按照 GeoAlchemy2 扩展 WKT(EWKT)的要求包含在字符串的开头。

在这里了解更多关于 GeoAlcheym2 ORM 的信息:geoalchemy-2.readthedocs.io/en/0.4/orm_tutorial.html

在每次循环中,都会实例化一个新的Arena类并将其分配给变量arenaname字段从位于索引6Reader record数据项中提取出来,并分配给arena变量,同时几何数据从arena_shapes数据项的count(即当前循环次数)中提取出来,并分配给Arena列的arena.longitudearena.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类(以及CountyDistrict类),使用索引从属性数据中提取名称、联邦信息处理标准FIPS)代码和邮政编码缩写。pygeoif用于首先将几何形状转换为pygeoif MultiPolygon格式,然后转换为 WKT,并将其传递给字符串模板,作为 EWKT 写入geom字段:

# 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捕获任何剩余的数据。

使用查询

对于DistrictCounty数据表,添加了一个最后的细节,即查询新添加的state数据以通过 FIPS 代码找到相关州。通过使用session.query查询State类,并使用filter_by方法(将来自区域记录的 FIPS 代码作为filter参数传递)过滤州的数据,然后指定应使用first结果,可以调用正确的state。使用变量的id字段来填充区域的state_id列以创建relationship

# 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()

County表同样被循环遍历,并且还包括一个State查询。查看脚本以查看完整的代码。一旦所有数据都已写入数据表,close会话并dispose连接引擎:

 session.close()
 engine.dispose()

Flask 应用程序的组件

现在后台数据库和表已经创建并加载数据,表之间的关系也已经建模和生成,是时候编写创建 Flask 应用程序的脚本了。这些脚本将包含视图、模型和表单,以处理 Web 请求、查询数据库并返回 HTTP 响应。

该 Web 应用程序被称为 Arena 应用程序,因为它在下拉列表中列出存储在arena表中的所有 NBAarenas,并允许用户在地图上显示位置,同时显示包含关于arena信息的popup,这些信息来自空间查询和表关系。

Web 开发的 MVC 方法允许将 Web 应用程序的必要组件分离。这些组件包括数据库模型(前面描述的 SQLAlchemy 模型)、接受应用程序输入的 Web 表单,以及用于路由请求的控制器对象。组件的分离反映在单独的脚本中。使每个组件独立,可以更容易地调整而不会影响应用程序的其他组件。

数据库模型将包含在名为models.py的脚本中,以及所需的模块导入。Web 表单(创建网页组件,如下拉列表和输入字段的 Python 类)将包含在名为forms.py的脚本中。所有视图,包括 URL 端点和处理这些 URL 的 Web 请求,都将包含在名为views.py的脚本中。

控制器是从Flask类生成的对象,并分配给名为app的变量。每个 Web 应用程序的 URL 端点都使用app.route定义,并关联一个 Python 函数(视图),该函数包含处理 Web 请求并返回 HTTP 响应的逻辑。控制器用于将 Web 请求路由到正确的 URL 端点,并且可以区分GETPOST HTTP请求。它是在views.py脚本中创建的。

使用 HTML 模板来展示 Web 请求的处理结果。通过使用 Jinja2 模板系统,包含在 Web 表单中的数据将被传递到 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 表单用于 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=[])

其他字段类,例如TextFieldBooleanFieldStringFieldFloatFieldPasswordField以及许多其他字段,都可通过 WTForms 库用于实现复杂的 Web 应用程序。此外,由于它们是 Python 对象,表单可以动态更新以包含其他数据属性,正如我们进一步探讨时将看到的。

视图

Flask 视图是 Python 函数,当与app控制器对象及其app.route URL 定义配对时,允许我们编写 Python 代码来接受 Web 请求、处理它并返回响应。它们是 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 函数redirecturl_for,任何发送到root地址的 Web 请求都将被重定向到arenas视图:

@app.route('/', methods=["GET"])
def home():
 return redirect(url_for('arenas'))

第二个视图arenas更为复杂。它接受GETPOST请求方法。根据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.pyArenaForm,并将 request.form 函数作为参数传递。这为 ArenaForm 添加了功能,并允许它根据需要访问请求的自身参数。

一旦 ArenaForm 对象传递给变量 form,它就可以用数据填充。这些数据将来自对 Arena 模型的 SQLAlchemy 会话 query。此查询请求 Arena 表的所有数据行,并使用 all 方法(而不是 filter_by 方法,后者将限制返回的行数)将其传递给变量 arenas

由于 ArenaFormselections 字段目前为空,我们将使用列表推导来遍历名为 arenas 的列表中包含的 arena 对象,将它们的 idname 字段添加到列表内部的元组中。这填充了下拉列表,并确保列表中的每个选择项都有一个值(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  

在填充选择项选项后,表单中添加了三个新属性——popuplatitudelongitude。最初,这些只是占位符,并不来自 arena 数据。然而,一旦网络应用程序运行,并且用户从下拉列表中选择 arenas,这些占位符值将被来自 arenas 表和其他表的查询得到的数据所替换。

评估请求方法

下一个行是一个使用 request.method 属性的 if 条件语句,用于检查 HTTP 请求方法是否为 POST

if request.method == "POST":

由于对 URL arenas 的初始请求是一个 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属性来检索从列表中选择的arenaid,并将其传递给一个名为arena_id的变量。然后使用这个id通过 SQLAlchemy ORM 的get方法查询数据库。form.longitudeform.latitude字段可以从查询返回的arena对象的字段中填充。

空间查询

为了找到县和国会选区,使用了两种 PostGIS 空间分析技术——ST_ContainsST_Intersects。第一个查询确定arena是否包含在county内;如果不是,结果为空(或在 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,使得基于空间分析返回查询结果成为可能。

关系查询

如果县查询成功,则执行区查询,然后使用关系属性(state_ref)来找到county所在的state

        state = county.state_ref

CountyDistrictState模型定义中建立的双向关系使得这一点成为可能。这个state对象是State模型类的一个成员,可以用来检索statename

为了创建自定义的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.latitudeform.longitude变量位于定义地图初始中心点的地图 JavaScript 中:

 var themap = L.map('map').setView([{{form.latitude}},                                                   {{form.longitude}}], 13);

为了在请求的arena位置创建带有自定义popupmarker,添加位置坐标和popup字段:

  L.marker([{{form.latitude}},{{form.longitude}}]).addTo(themap)
    .bindPopup("{{form.popup}}").openPopup();

为了使POST请求成为可能,一个带有POST方法的 HTML 表单包含了form.descriptionform.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 服务器,以便允许 Web 请求由 Python 可执行文件和app.py处理。对于 Apache Web 服务器,mod_wsgi模块很受欢迎。对于使用Internet Information ServicesIIS)的 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 网络框架以及一些可用的组件模块,这些模块增加了额外的功能。这些模块包括 SQLAlchemy ORM、用于地理空间查询的 GeoAlchemy2 ORM、用于处理 Web 数据的 WTForms 以及用于创建 Web 页面模板的 Jinja2 模板系统。我们创建了数据库表,添加了数据表和表关系,并创建了一个利用地理空间和关系查询生成动态网页的 Web 应用程序。

一个有趣的挑战是在这里审查的代码基础上,探索为 Arena 应用程序添加编辑功能,允许用户在数据过时的情况下将arenas移动到正确的位置。探索 GeoAlchemy2 ORM 文档以获取更多高级功能。

在下一章中,我们将回顾一个类似 MVC 网络框架 Django 及其 GeoDjango 空间组件。Django 以更多电池包的哲学解决了与 Web 应用程序固有的相同问题,但与 Flask 相比,模块选择自由度较低。

第十二章:GeoDjango

Django Python Web 框架于 2005 年推出,并在多年来持续得到支持和改进。一个主要改进是增加了对空间数据类型和查询的支持。这一努力产生了 GeoDjango,使得 Django 能够支持地理空间数据库模型和利用地理空间查询的 Web 视图。

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 还包括一个管理面板,允许通过 Web 界面轻松编辑和管理数据。这意味着安装的模块更少,包含的代码更多,用于处理数据库交互和 Web 处理。

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 组件取决于多个步骤。这些包括:

  1. 使用 pip 安装 Django 2

  2. 安装和启用空间数据库(如果尚未安装)

  3. 安装 GDAL/OGR/PROJ4/GEOS

  4. 配置 Windows 环境变量

  5. 生成项目

  6. 打开 settings.py

  7. django.contrib.gis 添加到 INSTALLED_APPS

  8. 配置数据库设置以指向空间数据库

安装 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 版本):

www.djangoproject.com/.

如果你使用虚拟环境,可以为每个环境指定 Django 的特定版本。如果不使用虚拟环境,并且安装了多个 Python 版本,请确保使用正确的 pip 版本在 Python 3 文件夹结构中安装 Django。

安装 PostGIS 和 psycopg2

本章将使用 PostGIS。如果你在机器上没有安装 PostGIS,请参阅第七章 Geoprocessing with Geodatabases,其中解释了如何将空间扩展附加组件安装到 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,处理矢量数据和栅格数据集。它必须被安装(有关如何使用它进行分析的更多详细信息,请参阅第五章 Vector Data Analysis 和第六章 Raster Data Processing)。

如果尚未安装,请使用以下网址提供的 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 中编辑系统路径和其他环境变量需要管理员权限。以下是编辑它们的步骤:

  1. 使用具有管理员权限的账户登录。

  2. 打开 Windows 资源管理器,在左侧窗格中右键单击 PC 图标。

  3. 从上下文菜单中选择“属性”。

  4. 点击“高级系统设置”。

  5. 在下一个菜单中,点击“环境变量”。

  6. 从系统变量中选择“路径”并点击“编辑”(或双击路径值)。

  7. 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.pystartprojectchapter12(项目的名称),创建了一个包含多个脚本和子文件夹的文件夹。外部的 (root) 文件夹被称为 chapter12,它包含一个重要的脚本 manage.py,以及一个也称为 chapter12 的文件夹,这是项目文件夹:

图片

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

图片

这些文件是默认的占位符,等待我们配置项目和应用程序。随着项目的进行,我们还将编辑 setting.pyurls.py,以包含我们项目的具体信息。第三个文件 wsgi.py 用于生产部署网络应用程序。

使用 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.pymodels.pyviews.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 安装的usernamepassword替换(见第三章,地理空间数据库简介):

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 与处理请求并返回响应的视图关联起来。模板将用于将处理后的数据传递到浏览器。视图将被编写以处理POSTGET请求,并将重定向到其他视图。

现在 GeoDjango 已配置,可以使用名为manage.py的 Django 项目管理脚本创建 NBA 竞技场应用程序。

manage.py

manage.py 脚本执行多项任务以帮助设置和管理项目。出于测试目的,它可以创建本地 Web 服务器(使用 runserver 作为参数);它管理数据库模式迁移,从数据模型生成表(使用 makemigrationmigrate);甚至内置了 Python 3 shell(使用 shell),用于测试和其他操作:

图片

在本节中,我们将使用 manage.py 来创建和填充数据库表,使用 shapefile 作为数据和模式源。

生成数据模型

在配置 GeoDjango 后,manage.py 中出现了一个新的可用功能,即 ogrinspect,它可以自动生成具有几何列的数据表模型,这些模型可以放置在 models.py 中。通过使用 OGR 检查或读取 shapefile 数据,Django 的内置功能创建了一个 Python 类数据模型和一个字段映射字典,该字典将 shapefile 字段名称与数据库列名称映射起来。

对于本节,我们将使用在 第十一章 中下载的 shapefile,即 Flask 和 GeoAlchemy2。它们也包含在代码包中。将四个 shapefile(以及所有相关文件)复制到 arenas 应用程序文件夹中的 data 文件夹内:

图片

打开命令提示符,将目录更改为项目文件夹。将包含四个 shapefile(Arenas_NBA.shpUS_States.shpUS_County_Boundaries.shpCongressional_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 字典,它将键(数据模型列名称)与值(shapefile 字段名称)映射起来。以下是输出的一部分:

图片

将输出(包括 import 行、数据模型和字段映射字典)复制到 arenas 目录下的 models.py 中。将 import 行复制到自动生成的 models.py 中的数据模型类定义上方。

当在命令提示符默认设置中开启快速编辑选项时,从命令行复制内容变得容易。一旦开启,通过拖动鼠标选择文本。当文本块被选中时,按 Enter 键。

多边形

对于具有多边形几何类型的其他三个 shapefile,我们将传递参数 multimanage.pyogrinspect。使用此选项在数据模型中生成 MultiPolygon 几何列。

此命令从美国州 shapefile 生成数据模型:

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 的参数(即形状文件名和表名)对县和区形状文件重复此过程,并在模型添加后保存 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

已生成并添加到 migrations 文件夹中的新脚本。此初始数据库迁移脚本创建了一个 Migration 类,并包含了一系列使用 CreateModel 方法进行的迁移操作。每个迁移操作都会在 chapter12 数据库中生成一个新的(空)表。Migration 类还有执行表变更的方法,当你需要添加或删除字段时。

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.pymanage.py 的所有可用参数:

docs.djangoproject.com/en/2.0/ref/django-admin/.

LayerMapping

为了填充由 shapefiles 创建的数据库表,Django 有一个内置的概念称为LayerMapping。通过使用由manage.py生成的字段映射字典,以及来自django.contrib.gis.utilsLayerMapping类,可以从 shapefiles 中提取并加载到数据库表中。要实例化一个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'),
)

如代码包中提供的load.py所示,重复此步骤以处理所有 shapefiles。这些路径变量和映射字典是执行层映射所必需的。

运行层映射

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,然后导入load.py文件,并在该本地 shell 中执行run函数:

>>> from arenas import load
>>> load.run()

一旦调用并执行了run函数,shapefiles 中的数据行将被导入到数据库表中:

图片

一旦函数成功完成,数据库表将被填充。我们现在可以探索 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)

保存脚本并关闭它。

createsuperuser

第一步是创建一个超级用户。此用户将能够访问管理面板。为此,我们将传递createsuperuser参数给manage.py并逐条遵循出现的说明:

C:\Projects\chapter12>python manage.py createsuperuser
Username: loki
Email address: email@server.com
Password:
Password (again):
Superuser created successfully.

现在超级用户可以使用提供的用户名和密码登录管理面板。

运行服务器

超级用户创建后,将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 函数用于将来自 Arenas 应用的可用 URL 添加到项目级别的 URL 中。

要创建应用级别的 URL,在 Arenas 应用文件夹内创建一个名为 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 函数。它们接受 GETPOST Web 请求,允许在同一个函数内发生多个动作,并产生各种响应。在视图函数中,我们设计如何解析请求,如何查询数据库表,如何处理查询结果(Django 中的QuerySets),以及将哪些表单和模板与处理后的数据一起发送到浏览器。

现在 URL 模式已经就绪,我们需要编写一些视图来接受和处理发送到 URL 的 Web 请求。这些视图将查询 models.py 中的数据库表模型类,以找到与 Arenas 类中包含的每个 NBA 场馆相关的位置数据。

必需的文件夹和文件

第一步是创建必要的文件夹,包含表单和模板的文件,因为视图的 Web 响应需要预先生成的模板来显示请求的数据(在这种情况下,请求的 NBA arena的位置)。

forms.py

在 Django 中,Web 表单用于捕获用户输入并将其提交到视图。为了能够从下拉列表中选择 NBA arena名称,并使 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字段,以及一个ChoiceFieldChoiceField将创建一个下拉列表,由arenas的 ID 和名称填充。其他字段将在视图中的ArenaForm类中添加,此处未定义。此表单及其字段将插入到下一节中创建的模板中。将此文件保存为forms.py到 Arenas 应用程序文件夹中。

templates 文件夹

将完成代码包中的templates文件夹复制到 Arenas 应用程序文件夹中。在templates文件夹中有一个名为arenas的文件夹,里面有一个名为index.html的模板 HTML 文件。此文件包含一个 JavaScript 部分,用于生成一个网络地图。在该地图上,NBA arena的位置被显示出来。

Django 模板使用占位符(以{{form.field }}格式分隔)允许在运行时将数据传递到模板中,提供请求的详细信息。这些占位符位于index.html的各个部分。Django 有自己的内置模板语言,我们在这里将使用它,还包括 Jinja2,Flask 也使用(见第十一章,Flask 和 GeoAlchemy2)。

index.html的第一部分需要突出显示的是,当前 NBA arenalongitudelatitude已经被添加到 Leaflet JavaScript 中,在13级别缩放时将地图窗口中心定位在该位置:

 var themap = L.map('map').setView([ {{form.latitude}}, {{form.longitude}}], 13);

下一个需要突出显示的部分是,将当前 NBA arenalongitudelatitude和自定义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

接下来,我们将创建两个视图——indexarena*,以及一个非视图函数queryarena。这些与我们在urls.py中添加的 URL 相匹配。index函数的返回值非常简单——它将重定向到arena函数。对于视图,使用装饰器来确定允许的 HTTP 请求方法。

index 视图

index视图是一个 Python 函数,它接受请求数据并将其重定向到arena视图,在限制允许的 HTTP 请求之前有一个装饰器(require_http_methods):

@require_http_methods(["GET", "POST"])
def index(request):
    return redirect(arena)

queryarena 函数

下面的arena函数选择一个随机的arena用于初始GET请求,从数据库模型中获取所选 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 几何形状来确定包含 arenacounty 和国会 district。这次,地理空间操作是 geom_containsfilters 返回一个 county 对象和一个 district 对象。它们被用来生成将添加到 leaflet 地图上的地图标记上的自定义 popup。这个 popup 包含 arena 的经纬度、arena 的名称、其 countystate 的名称以及其 state 内的国会 district 的编号。

arena 视图

arena 视图接受 request 对象,然后实例化一个 ArenaForm 对象以收集响应 request 所需的数据。对 Arenas 模型对象及其 values_list 方法的查询创建了一个包含每个 arena 的 ID 和名称的元组的 Python 列表。在条件语句中使用 request 方法(无论是 GET 还是 POST)来确定适当的响应。

如果收到一个 GET 请求(即,网页首次打开),则会生成一个随机的 arena 对象并将其传递给模板,模板会在包含的地图上显示 arena。要获取一个随机的 arena,我们使用 arena 名称和 ID(值)的列表。一旦生成了列表,就使用列表推导式生成一个新的列表,其中包含 arena 名称。

使用 random 模块和列表中名称的 #(长度)生成一个随机 index,用于从列表中选择一个 arena 名称。然后,将此 name 传递给 queryarena 函数,该函数用 arena name、位置和 popup 填充 form

这些值通过 render 函数返回给浏览器。此函数用于将 formsrequest 一起传递到模板中,并知道 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 被用作列表推导式中的条件,使我们能够检索 arenaname。然后,将 name 传递给 queryarena,查询其位置详情并将其添加到 form 中,在通过 render 返回之前。

视图已完整,脚本可以保存。下一步是运行应用程序。

运行应用程序

打开命令提示符并切换到 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 创建网络应用程序允许实现许多即时功能,包括管理面板。LayerMapping 使得从 shapefiles 导入数据变得容易。ORM 模型使得执行地理空间过滤或查询变得容易。模板系统使得添加网络地图以及位置智能到网站变得容易。

在下一章中,我们将使用 Python 网络框架创建地理空间 REST API。此 API 将接受请求并返回表示地理空间特征的 JSON 编码数据。

第十三章:地理空间 REST API

在网络上发布数据以供消费是现代 GIS 的一个重要组成部分。为了将数据从远程服务器传输到远程客户端,大多数地理空间发布软件堆栈都使用 表示状态转移REST)Web 服务。对于针对特定数据资源的 Web 请求,REST 服务返回 JavaScript 对象表示法JSON)编码的数据给请求的客户端机器。这些 Web 服务组合成一个应用程序编程接口(API),其中将包含代表每个可查询数据资源的端点。

通过结合 Python 网络框架、对象关系映射ORM)和 PostGIS 后端,我们可以创建一个自定义的 REST API,该 API 将以 JSON 格式响应用户的 Web 请求。在这个练习中,我们将使用 Flask 网络框架和 SQLAlchemy 模块,结合 GeoAlchemy2 提供的空间 ORM 功能。

在本章中,我们将学习以下内容:

  • REST API 组件

  • JSON 响应格式化

  • 如何处理 GETPOSTPUTDELETE 请求方法

  • 使用 API 执行地理空间操作

  • 如何使用 IIS 部署 Flask 网站

使用 Python 编写 REST API

为了理解具有 JSON 响应的 REST API 的组件,我们将使用 Flask 网络框架、PostgreSQL/PostGIS 数据库,以及 SQLAlchemy 和 GeoAlchemy2 进行 ORM 查询。Flask 将用于创建 API 的 URL 端点。PostGIS 将将数据存储在由 SQLAlchemy 模型定义的表中,这些模型定义了所有列的类型,除了几何列,这些列由 GeoAlchemy2 列类型定义。

REST

REST 是一个用于 Web 服务的标准,旨在接受请求和参数,并返回数据的表示,通常以 JSON 格式,但有时以 XML 或 HTML 格式。使用 REST 架构的 API 必须满足以下架构约束:

  • 客户端-服务器交互

  • 无状态

  • 缓存能力

  • 统一接口

  • 分层系统

客户端(一个网络浏览器或远程计算机)将向指定 URL 端点的服务器发送请求。请求可以包含参数,这些参数限制了返回的数据对象,类似于 SQL 语句中的条件。它是无状态的,这意味着每个请求都必须包含请求参数,并且不能引用另一个请求的结果。返回的数据必须明确标记为可缓存或不可缓存,以便客户端决定数据是否可以存储,或者是否在需要时请求。当请求数据时,所有与数据相关的可用 API 端点(包括添加或删除数据的链接,如果有的话)都作为链接与数据表示一起返回。API 不揭示服务器的基础架构,可以在不改变 API 结构的情况下对其进行操作(添加或删除机器)。

JSON

JSON 被设计成既能被人类理解,也能被机器解析。Python 字典可以轻松地生成 JavaScript 数据对象,因为它们使用相同的键值结构和花括号表示法。Python 内置了一个用于生成 JSON 的库(json模块),而像 Flask 这样的 Web 框架也包含了生成 JSON 响应的代码。

地理空间数据存在多个 JSON 标准,包括 GeoJSON 和 Esri JSON。在本章中,REST API 将使用 GeoJSON 格式来响应请求。

在这里了解更多关于 GeoJSON 的信息:geojson.org/

Python 用于 REST API

Python 是编写 REST API 的绝佳语言。它包含允许进行数据库查询的模块,以及其他将 HTTP 网络请求处理成 URL 和参数组件的模块。使用这些模块,可以从数据库中检索请求的资源,并使用在 Python 字典和 JSON 对象之间进行转换的模块将数据作为 JSON 返回。

虽然可以使用标准库构建基于 Python 的 API,但使用 Web 框架构建 API 将加快开发速度,并允许根据需要添加组件模块。

Flask

Flask 是 Python Web 框架中用于 REST API 的一个好选择。与 SQLAlchemy 和 GeoAlchemy2(见第十一章,Flask 和 GeoAlchemy2,了解更多关于这两个库的信息)配合使用,它允许将 REST URL 端点与一个视图(一个 Python 函数)配对,该视图将根据请求方法(例如GETPOST,仅举两个例子)以不同的方式处理请求并返回 JSON 数据。

REST 模块

由于 Flask 被设计成可扩展的,因此有许多附加模块旨在简化 REST API 的创建。这些包括:

本章将使用纯 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:,可以使用 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 对象,可以对其进行处理以生成响应。

其他数据转换器

除了整数,使用 int 转换器外,浮点数据可以使用 float 转换,URL 数据可以使用 path 转换。字符串,使用 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 属性,可以检查请求方法是否为 GETPOST。如果我们创建一个 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)

由于此方法将接受 GETPOST 请求,因此根据每个请求方法发送不同的响应。

其他可用的请求方法

虽然 GETPOST 是主要请求方法,但还有其他方法可用于处理数据。对于示例 API,我们只会使用 GETPOST 以及 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 表单

  • statictemplates 文件夹,其中包含模板和数据

应用程序文件夹和文件结构

示例 REST API 需要创建特定的文件和文件夹。外部文件夹称为 arenaapp,将包含 app.py 文件和名为 application 的文件夹。创建名为 arenaapp 的文件夹。在其内部,创建名为 application 的文件夹。在 application 内部,创建 statictemplates 文件夹:

图片

其他文件,views.pymodels.pyforms.py,将位于 application 内部。两个文件夹 statictemplates 将存储应用程序数据和 HTML 表单:

图片

app.py

使用 IDE 或文本编辑器,在 arenaapp 中创建一个名为 app.py 的文件。打开此文件并添加以下行;此文件将由 Python 可执行文件运行以启动 REST API 应用程序:

from application import app
app.run()

__init__.py 文件允许 app.py 导入 application 文件夹,允许调用 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 函数。一旦 enginesessionmaker 绑定,就可以初始化 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 类来子类化所有应用程序类。

声明模型

对于模型,所有字段类型(例如,IntegerStringFloat)都使用 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 类分别与 CountyDistrict 类有一对多关系,使用 relationship 函数定义。它还有一个 SRID 为 4326MULTIPOLYGON 几何列:

# 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表中请求数据,我们将定义 API 端点并使用视图函数查询Arenas模型。每个响应都将作为一个 GeoJSON 包。此端点('/arena')将返回一个 GeoJSON 响应,其内容将根据 URL 中添加的变量而变化。这些变量包括竞技场 ID 和名称。

获取所有竞技场

要生成包含所有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})

返回的字段包括nameid,以及longitudelatitude。为了限制传输的数据量,latitudelongitude被四舍五入到6位小数。描述arena位置所需的精度较低,这使得这种限制是合理的。由于点数据类型只包含两个点,因此返回的数据较少,而多边形和多线数据则更大,需要更高的精度。

与循环相比,列表推导式可以减少迭代列表所需的处理时间。了解更多关于列表推导式的内容:

docs.python.org/3/tutorial/datastructures.html#list-comprehensions. 

通过 ID 获取竞技场

通过向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 Arena 的数据。在此视图函数中,使用intersect filter(即,使用点在多边形函数中识别包含arenacounty)查询CountyDistrict表。使用countystate之间的表关系检索基础州。返回所有几何形状和选定字段:

@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是否在美county内;如果不是,则不使用countydistrictstate对象。相反,请求被重定向到非地理空间查询视图函数。

由于每个state由许多顶点组成,因此美国州数据可能很大。在states端点的端点中,我们将添加一些 URL 参数,使我们能够决定是否返回每个请求的state的几何形状。

获取所有州

通过在request.args字典中检查 URL 参数,然后检查该参数是否评估为真,我们可以确定是否应返回所有state几何形状。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 获取州

要使用state的 ID 作为主键来获取特定的state,我们可以添加一个 URL 变量,该变量将检查整数 ID。它将geometry作为geojson返回:

@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。字符串变量将与数据库表中的状态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包含的arenasstate通过 ID 识别,并且 URL 组件将选择所有geometrystate 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和所有arenasdata,因为 GeoJSON 允许将多个数据类型打包为特征集合。

State数据库表类似,这将检索所有的county数据。它接受一个geometry参数来决定是否返回每个countygeometry

@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使用shapelygeoalchemy2.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 库允许用户点击地图上的任何位置,从而更新地图的longitudelatitude数据:

图片

新区域

要将新的 arena 添加到数据库的 Arena 表中,将创建一个用于处理请求的视图函数和一个 Jinja2 HTML 模板,并将其使用。该函数将确定请求方法,并将适当的响应发送给请求。如果是一个 GET 请求,它将发送一个包含 AddForm 表单的 HTML 模板。从 HTML 模板中,填写数据并点击按钮将提交一个 POST 请求,该请求将转到相同的视图函数,并使用提交的数据在 Arena 表中添加一行新数据。

视图函数

处理请求的视图函数接受 GETPOST 请求方法。在这种情况下使用的是端点 '/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 文件的顶部,在头部部分,添加 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 设置 latitudelongitude 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 请求

可以使用网络请求添加一个新的 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 请求。因此,需要在生产网络服务器上进行部署。

将 Flask 部署到 IIS

要在安装了 Internet Information Services (IIS) 的 Microsoft 服务器上部署新的 API 应用程序,我们需要下载一些 Python 代码,以及一个名为 FastCGI 的 IIS 模块。一旦配置完成,应用程序将能够响应来自任何允许的机器的网页请求。

Flask 和网络服务器

虽然 Flask 包含一个用于测试目的的本地网络服务器,但它不是为生产部署设计的。Flask 与 Apache 或 IIS 等网络服务器配合使用效果最佳。虽然关于如何使用 Apache 部署 Flask 的文献很多,但关于如何使用 IIS 部署它的良好说明却很少见。由于大多数 GIS 专业人士使用 Windows 服务器或可以访问它们,因此这些说明将侧重于 IIS 7 的部署。

WSGI

Web 服务器网关接口 (WSGI) 是一个 Python 规范,它允许使用 Python 可执行文件来响应网页请求。WSGI 集成在 Flask 和 Django 等网络框架中。

要启用 Flask 网络框架来服务网页,需要对 IIS 进行一些配置,包括安装一个名为 FastCGI 的 IIS 通用网关接口 (CGI) 模块,以及安装一个名为 WFastCGI 的 Python 模块。有了这两个添加项,IIS 网络服务器将连接到 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 Web 框架创建具有 REST 规范的 API 很容易。Flask 使得协调 URL 端点和请求方法以及响应类型变得简单。凭借内置的 JSON 功能,以及使用 SQLAlchemy 和 GeoAlchemy2 ORM,Flask 是创建地理空间 REST API 的完美框架。

在下一章中,我们将介绍使用 CARTOframes 模块进行地理空间数据的云可视化。

第十四章:云地理数据库分析和可视化

本章将介绍 CARTOframes,这是 CARTO 地理智能软件公司在 2017 年 11 月发布的一个 Python 包。它提供了一个用于与 CARTO 堆栈一起工作的 Python 接口,使 CARTO 地图、分析和数据服务能够集成到数据科学工作流程中。

本章将涵盖以下主题:

  • CARTOframes Python 库的详细信息

  • 熟悉 CARTO 堆栈以及 CARTOframes 如何与它的不同部分交互

  • 如何安装 CARTOframes、其包需求和文档

  • CARTOframes 的不同包依赖项

  • 如何获取 CARTO API 密钥

  • 设置 CARTO Builder 账户

  • 虚拟环境

  • 使用 Jupyter Notebook

  • 安装 GeoPandas

CARTOframes 是一个针对数据科学家创建的 Python 包,它将 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 一起在虚拟环境中安装它,这样你就可以访问其依赖项。请参考第二章,地理空间代码库简介中的安装指南。你可以在单独的 Python 环境中使用以下命令安装 Jupyter Notebook 应用,在终端窗口中:

>>pip install jupyter

CARTO API 密钥

安装 CARTOframes 后,我们需要创建一个 CARTO API 密钥,以便能够使用库中的功能。该库与 CARTO 基础设施交互,类似于第九章,Python ArcGIS API 和 ArcGIS Online中的 ArcGIS API for Python。API 密钥可用于将数据帧写入账户、从私有表中读取以及将数据可视化在地图上。CARTO 为教育和非营利用途等提供了 API 密钥。如果你是学生,你可以通过注册 GitHub 的学生开发者包来获取 API 密钥:education.github.com/pack. 

另一个选择是成为 CARTO 大使:

carto.com/community/ambassadors/.

包依赖

CARTOframes 依赖于多个 Python 库,一旦运行pip install命令,这些库就会自动安装。以下 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: 一个用于文本编辑器的自动完成工具

  • numpy: 执行数字、字符串、记录和对象的数组处理

  • pandas: 提供强大的数据分析、时间序列和统计数据结构

  • parso: 一个支持不同 Python 版本错误恢复的 Python 解析器,以及更多

  • pickleshare: 一个具有并发支持的类似 shelve 的小型数据存储库

  • prompt-toolkit: 用于在 Python 中构建强大交互式命令行的库

  • pygments: 一个用 Python 编写的语法高亮包

  • pyrestcli: Python 的通用 REST 客户端

  • python-dateutil: 为标准的 Python datetime 模块提供扩展

  • pytz: 提供现代和历史世界时区定义

  • requests: 一个 HTTP requests

  • 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 库访问公开可用的数据。它还允许您轻松发布地图。点击“新建地图”按钮开始:

图片

添加数据集

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

图片

数据集被添加为地图的一层。地图编辑器中可以操纵层的所有方面,包括层的颜色、显示的属性、弹出窗口等。底图也可以进行调整。

可以添加表示属性实时数据的控件。我已经将来自 DATA LIBRARY 的 US Census Tracts 层添加到地图中,并添加了一个显示所选属性字段值的图形控件。此图形是动态的,并将根据地图窗口中显示的具体人口普查区调整显示的值:

图片

查看 Builder 中的其他选项卡,包括 DATA、ANALYSIS、STYLE、POP-UP 和 LEGEND,以进一步自定义地图。有许多调整和小部件可以使数据交互式。地图也可以设置为公开或私有,并且可以通过点击发布按钮发布到网络上。CARTO 的编辑器和数据导入界面使得创建和共享地图变得非常容易。

API 密钥

要使用 CARTOframes 连接到 CARTO 账户,需要一个 API 密钥。要访问它,请转到账户仪表板,点击右上角的图片,然后从下拉菜单中选择“您的 API 密钥”链接:

图片

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

图片

添加数据集

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

图片

一旦按下“新建数据集”按钮,并出现“连接数据集”界面,点击“浏览”并导航到 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 文件夹,其中包含 site-packages 文件夹,它将包含在这个 Python 版本中安装的所有模块:

图片

激活虚拟环境

要从命令行开始使用新的虚拟环境,在虚拟环境所在的文件夹中传递以下参数。这将运行 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 使得这个过程相当简单。在设置第一个虚拟环境之后,一个名为 freezepip 命令允许你生成一个名为 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 Notebooks,这些是专门的基于浏览器的编码环境。

激活虚拟环境,并使用以下命令在虚拟环境中安装模块:

(cartoenv) C:\Packt\Chapters>pip install cartoframes
(cartoenv) C:\Packt\Chapters>pip install jupyter

所需的所有模块也将被下载并安装,包括我们直接安装的两个模块。使用 pipvirtualenv 使得包安装和管理变得简单快捷。

使用 Jupyter Notebook

我们已经在 第一章,包安装与管理 以及前一章的多个实例中介绍了 Jupyter Notebook 的基本安装。

在这里,我们将使用 Jupyter Notebook for CARTOframes 来连接账户并分析地理空间数据以及展示它。

连接到账户

在第一个代码框中,我们将导入 CARTOframes 模块,并传递 API 密钥字符串以及基础 URL,该 URL 由你的 CARTO 用户名生成,格式为 https://{username}.carto.com。在这种情况下,URL 是 https://lokiintelligent.carto.com

图片

在这个代码块中,API 密钥和 URL 被传递给 CartoContext 类,并返回一个 CartoContext 连接对象,并将其分配给变量 cc。有了这个对象,我们现在可以与我们的账户关联的数据集进行交互,将数据集加载到账户中,甚至可以直接在 Jupyter Notebook 中生成地图。

一旦代码输入到该部分,点击运行按钮以执行当前部分的代码。任何输出都将出现在代码运行的下方 Out 部分。此部分可以包括地图、表格,甚至图表——Jupyter Notebooks 常用于科学计算,因为它们能够即时生成图表并在笔记本中保存。

保存凭据

可以使用 Credentials 库保存并稍后访问 CARTO 账户的凭据:

from cartoframes import Credentials
creds = Credentials(username='{username}', key='{password}')
creds.save()

访问数据集

要访问我们已加载到账户中的 NBA 场馆数据集,我们将使用 CartoContextread 方法,将我们想要交互的数据集名称作为字符串传递。在 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 语句将生成一个包含 NBA arenas 数据集值的表格,这些数据已被加载到 CARTOframe 对象中:

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

选择单个行

要选择 Pandas 数据框中来自 CARTO 账户数据集的特定行,可以将条件语句传递到括号中的对象。在这里,通过传递 NBA 团队名称作为参数,查询 NBA arenas 数据集的团队列:

df[df.team=='Toronto Raptors']

加载 CSV 数据集

要使用 CARTOframes 将数据集加载到账户中,我们将再次使用与 Jupyter 模块一起安装的 pandas 库。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,如第五章 Vector Data Analysis 中讨论的,是 Pandas 的地理空间补充。为了能够从形状文件创建数据帧对象,我们必须确保 GeoPandas 已安装并添加到虚拟环境中。使用pip install添加 GeoPandas 库:

(cartoenv) C:\PythonGeospatial3>pip install geopandas

如果在 Windows 上遇到安装问题,GeoPandas 和 Fiona(GeoPandas 的驱动库)的预构建二进制文件可供在此处使用,以及许多其他 Python 库:www.lfd.uci.edu/~gohlke/pythonlibs。通过下载它们,将它们复制到一个文件夹中,并使用pip install从轮子中安装 Fiona 和 GeoPandas。例如,在这里,Fiona 是从轮子文件安装的:

C:\PythonGeospatial3>pip install Fiona-1.7.11.post1-cp36-cp36m-win_amd64.whl

写入 CARTO

将形状文件写入 CARTO 账户只需要一个CartoContext对象、一个文件路径以及常用的 URL 和 API 密钥组合。现在 GeoPandas 已安装,MLB Stadiums 形状文件可以加载到 GeoPandas DataFrame中,然后使用CartoContextwrite方法写入 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几何对象都是从已导入的地址数据集的LONLAT字段生成的:

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 形状文件使用相交空间查询进行比较。如果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 dataframe 中作为名为 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 环境中即时创建数据可视化。与 Mapbox Python SDK 一起,一个允许 API 访问账户服务的模块,Python 使将 Mapbox 工具和服务添加到企业地理空间应用变得容易。

在本章中,我们将学习:

  • 如何创建 Mapbox 账户以生成访问令牌

  • 如何样式化自定义基础图

  • 对云数据和基础图的读写访问

  • 如何创建面状图

  • 如何创建渐变圆可视化

所有制图相关内容

由埃里克·冈德森于 2010 年创立的 Mapbox 迅速发展,并超越了其初创公司的根基,成为制图复兴的领导者。他们的 MapboxGL JavaScript API 是一个用于创建交互式网络地图和数据可视化的有用库。他们向地理空间社区贡献了多个开放制图规范,包括矢量瓦片。

Mapbox 专注于为地图和应用程序开发者提供定制基础图瓦片,将自己定位为网络制图和移动应用领域的领先软件公司。本章中使用的两个 Python 模块允许 GIS 经理和开发人员将他们的服务和工具集成到企业地理信息生态系统中。

如何将 Mapbox 集成到您的 GIS 中

通过他们的 JavaScript 库和新的 MapboxGL—Jupyter Python 模块,Mapbox 工具的使用比以往任何时候都更容易。地理空间开发者和程序员可以将他们的工具集成到现有的 GIS 工作流程中,或者创建利用 Mapbox 提供的套件的新地图和应用程序。

Mapbox,就像 CARTO 一样,允许基于账户的云数据存储。然而,他们的重点更少在分析工具上,更多在制图工具上。对于大小不同的制图团队,使用 Mapbox 工具可以降低创建和支持自定义基础图的成本,并且比其他地图瓦片选项(如 Google Maps API)提供更大的节省。

Mapbox Studio 使创建具有制图外观和感觉的地图变得容易,可以与公司或部门的品牌相匹配。基础图可以使用现有样式构建,并叠加您的组织层,或者设计一个全新的基础图。它甚至允许基于被拖入工作室的图像进行样式化,根据从图像像素生成的直方图为要素分配颜色。

Mapbox 工具

在地理空间领域(例如 Mapbox 开源项目负责人 Sean Gillies,Shapely、Fiona 和 Rasterio 的主要开发者)的领导下,Mapbox 为开源许可下的分析和地图制作 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 叠加以及标记和形状。包括点击、缩放和平移在内的事件可以用来触发数据处理函数,使其非常适合交互式 Web 地图应用。

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 作为如何使用访问令牌创建地图的示例。将以下代码中提到的访问令牌替换为您自己的公开访问令牌:

<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 MapOSM)数据来生成预构建的自定义底图,这些底图可用于商业和非商业应用。使用 Mapbox Studio,还可以调整这些样式以添加更多定制功能。此外,可以从头开始构建底图,以创建适用于您应用程序的特定外观:

图片

要访问 Mapbox Studio,请登录账户仪表板并点击 Mapbox Studio 链接。在这个 Studio 环境中,您可以管理底图、瓦片集和数据集。

定制底图

点击新建样式按钮并选择卫星街道主题:

图片

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

图片

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

图片

探索其他样式选项,例如标签颜色和图层比例级别。完成定制后,通过点击发布样式按钮来发布样式。样式 URL 将添加到这些 Jupyter Notebook 练习的 MapboxGL 可视化或网络地图中。

添加瓦片集

要将你的数据添加到基图样式,请点击图层按钮并从可用选择中选择一个瓦片集。之前使用 Mapbox Python SDK 加载的 zip 瓦片集应该可用,并可以添加到基图并对其进行样式化:

图片

虚拟环境

使用 virtualenv(参见上一章中的安装说明)启动一个虚拟环境,并使用 pip 安装以下列出的模块。如果你有一个文件夹路径为 C:\Python3Geospatialvirtualenv 将创建一个虚拟环境文件夹,这里称为 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

MapboxGL—Jupyter 库可以通过 pipPyPI.org 仓库获取:

(mapboxenv) C:\Python3Geospatial>pip install mapboxgl

所有支持模块都将与 Mapbox 创建的核心库一起定位和安装。

安装 Jupyter Notebooks

在虚拟环境中安装 Jupyter Notebooks 库:

(mapboxenv) C:\Python3Geospatial>pip install jupyter

安装 Pandas 和 GeoPandas

Pandas 应该已经安装,因为它是与 GeoPandas 一起安装的,但如果尚未安装,请使用 pipPyPI.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 工具用于 Hadoop 以空间方式处理大数据。

本章将教你如何:

  • 安装 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 命令。第二个命令使用 sudoroot 身份运行。tee 命令将输出发送到文件和终端(STDOUT)。-a 告诉 tee 向文件追加,而 /etc/hosts 是你想要追加的文件。现在,在你的浏览器中,你将能够使用名称而不是 IP 地址。

现在你已经准备好启动镜像并浏览到你的 Hadoop 框架。

Hadoop 基础知识

在本节中,你将启动你的 Hadoop 镜像,并学习如何使用ssh和 Ambari 进行连接。你还将移动文件并执行一个基本的 Hive 查询。一旦你了解了如何与框架交互,下一节将展示如何使用空间查询。

首先,从终端使用提供的 Bash 脚本启动 Hortonworks 沙盒。以下命令将展示如何操作:

sudo sh start_sandbox-hdp.sh

之前的命令执行了你下载的沙盒脚本。同样,它使用了sudoroot身份运行。根据你的机器,完全加载并启动所有服务可能需要一些时间。完成之后,你的终端应该看起来像以下截图:

通过安全外壳连接

现在沙盒正在运行,你可以使用安全外壳(SSH)进行连接。安全外壳允许你远程登录到另一台机器。打开一个新的终端并输入以下命令:

ssh raj_ops@127.0.0.1 -p2222

之前的命令使用ssh以用户raj_ops的身份连接到本地的localhost127.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 GIS 工具用于 Hadoop

在设置好环境并具备一些关于 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

第一条命令将在当前目录中解压文件,这很可能是您家目录中的 Downloads 文件夹。第二条命令将解压文件,但通过传递 -d 和一个路径,它将解压到该位置。在这种情况下,这是我的家目录的 root

现在您已经解压了文件,可以通过从图标下拉菜单中选择它来在 Ambari 中打开文件视图。选择上传,将打开一个模态窗口,允许您拖放文件。在您的本地机器上,浏览到 Esri Java ARchive (JAR) 文件的位置。如果您将 zip 文件移动到您的家目录,路径将类似于 /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 的 root 文件夹。这是 / 目录,这是您启动文件视图时默认打开的位置。

接下来,您通常会也将数据移动到 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_PointST_Contains。一个 JAR 文件可能包含许多 Java 文件(类)。add jar 语句的顺序很重要。

下面的代码块将删除两个表——earthquakescounties。如果您从未运行过示例,可以跳过这些行:

drop table earthquakes;
drop table counties; 

接下来,代码将创建 earthquakescounties 表。创建 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;

counties表通过将字段名称和类型传递给CREATE创建,但数据是 JSON 格式,并将使用你导入的 JAR 文件spatial-sdk-json-2.0.0中的com.esri.hadoop.hive.serde.EsriJSonSerDe类。STORED AS INPUTFORMATOUTPUTFORMAT对于 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_PointST_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。结果如下所示:

图片

HDFS 和 Hive 在 Python 中

这本书是关于地理空间开发的 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_dirlslist_dir。要删除文件或目录,你将使用 delete_file_dirmakedelete 命令在成功时将返回 True

让我们用 Python 查看我们的 HDFS 文件系统的 root 目录:

ls=hdfs.list_dir('/')

之前代码发出了 list_dir 命令(与 ls 相当),并将其分配给 ls。结果是包含目录中所有文件和文件夹的字典。

要查看单个记录,你可以使用以下代码:

ls['FileStatuses']['FileStatus'][0]

之前的代码通过使用字典键 FileStatusesFileStatus 来获取单个记录。

要获取字典中的键,你可以使用 .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。它创建了一个连接并获取了 cursor。最后,它执行了一个 Hive 语句。一旦你有了连接和 cursor,你就可以通过将它们包裹在 .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 库。

posted @ 2025-10-26 09:00  绝不原创的飞龙  阅读(4)  评论(0)    收藏  举报