Python-数据工程-全-
Python 数据工程(全)
原文:
zh.annas-archive.org/md5/b4799e44e6bb988bf01a7e9bab89707e译者:飞龙
前言
数据工程为数据科学和分析提供了基础,并构成所有业务的重要方面。本书将帮助你探索用于使用 Python 理解数据工程过程的多种工具和方法。本书将向你展示如何应对数据工程不同方面常见挑战。你将从数据工程的基础知识介绍开始,包括构建用于处理大数据集的数据管道所需的技术和框架。你将学习如何转换和清洗数据,并执行分析以充分利用你的数据。随着你的进步,你将发现如何处理不同复杂性的大数据和数据库,并构建数据管道。通过实际案例,你将构建架构,并学习如何部署数据管道。
在完成这本 Python 书之后,你将清楚地理解数据建模技术,并能够自信地构建用于跟踪数据、运行质量检查以及在生产中进行必要更改的数据工程管道。
这本书面向谁
本书面向数据分析师、ETL 开发人员以及希望开始或过渡到数据工程领域或使用 Python 刷新数据工程知识的人。本书对计划在数据工程或 IT 领域建立职业生涯的学生以及为过渡做准备的专业人士也很有用。不需要具备数据工程的前置知识。
本书涵盖的内容
第一章, 什么是数据工程,定义了数据工程。它将向你介绍数据工程师的技能、角色和责任。你还将了解数据工程如何与其他学科,如数据科学相结合。
第二章, 构建我们的数据工程基础设施,解释了如何安装和配置本书中使用的工具。你将安装两个数据库——ElasticSearch 和 PostgreSQL——以及 NiFi、Kibana,当然还有 Python。
第三章, 读取和写入文件,介绍了在 Python 中读取和写入文件以及 NiFi 中的数据管道。它将重点介绍逗号分隔值(CSV)和JavaScript 对象表示法(JSON)文件。
第四章, 与数据库一起工作,解释了与 SQL 和 NoSQL 数据库一起工作的基础知识。你将查询这两种类型的数据库,并在 Python 和通过使用 NiFi 中查看结果。你还将学习如何读取文件并将其插入到数据库中。
第五章,清洗和转换数据,解释了如何处理文件或数据库查询并执行基本的数据探索性分析。这种分析将使你能够查看常见的数据问题。然后,你将使用 Python 和 NiFi 来清洗和转换数据,以解决这些常见的数据问题。
第六章,项目 - 构建一个 311 数据管道,介绍了一个项目,在这个项目中你将构建一个完整的数据管道。你将学习如何从 API 读取并使用之前章节中获得的所有技能。你将对数据进行清洗和转换,并使用额外的数据对其进行丰富。最后,你将把数据插入仓库并构建仪表板来可视化它。
第七章,生产数据管道的功能,涵盖了数据管道为准备生产所需的内容。你将了解原子事务以及如何使数据管道具有幂等性。
第八章,使用 NiFi Registry 进行版本控制,解释了如何对数据管道进行版本控制。你将安装和配置 NiFi 注册表。你还将学习如何配置注册表以使用 GitHub 作为 NiFi 处理器的源。
第九章,监控和日志数据管道,教你监控和日志数据管道的基础知识。你将了解 NiFi GUI 的监控功能。你还将学习如何使用 NiFi 处理器从你的数据管道内部进行日志记录和监控性能。最后,你将了解 NiFi API 的基础知识。
第十章,部署你的数据管道,提出了一种为 NiFi 构建测试和生产环境的方法。你将学习如何将完成并进行了版本控制的数据管道移动到生产环境中。
第十一章,项目 - 构建生产数据管道,解释了如何构建生产数据管道。你将使用来自第六章的项目并添加一些功能。你将对数据管道进行版本控制,并添加监控和日志功能。
第十二章,构建 Apache Kafka 集群,解释了如何安装和配置一个三节点的 Apache Kafka 集群。你将学习 Kafka 的基础知识——流、主题和消费者。
第十三章,使用 Kafka 进行数据流,解释了如何使用 Python 写入 Kafka 主题以及如何消费这些数据。你将使用第三方 Python 库编写消费者和生产者的 Python 代码。
第十四章,使用 Apache Spark 进行数据处理,将指导您安装和配置一个三节点 Apache Spark 集群。您将学习如何使用 Python 在 Spark 中操作数据。这将让人联想到本书第一部分中与 pandas DataFrames 一起工作的方式。
第十五章,项目 - 实时边缘数据 - Kafka、Spark 和 MiNiFi,介绍了 MiNiFi,这是一个独立的项目,旨在将 NiFi 部署到资源有限的设备上,如物联网设备。您将构建一个数据管道,将数据从 MiNiFi 发送到您的 NiFi 实例。
附录介绍了使用 Apache NiFi 进行聚类的基础知识。您将学习如何分配数据管道以及这样做的一些注意事项。您还将学习如何允许数据管道在单个指定的节点上运行,而不是在集群中分布式运行。
要充分利用本书
您应该对 Python 有一个基本的了解。您不需要了解任何现有的库,只需对变量、函数以及如何运行程序有一个基本理解。您还应该了解 Linux 的基础知识。如果您能在终端中运行命令并打开新的终端窗口,那么这应该就足够了。

如果您使用的是本书的数字版,我们建议您亲自输入代码或通过 GitHub 仓库(下一节中提供链接)访问代码。这样做将帮助您避免与代码复制粘贴相关的任何潜在错误。
下载示例代码文件
您可以从github.com/PacktPublishing/Data-Engineering-with-Python下载本书的示例代码文件。如果代码有更新,它将在现有的 GitHub 仓库中更新。
我们还提供了一些来自我们丰富的图书和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!
下载彩色图像
我们还提供了一份包含本书中使用的截图/图表的彩色图像的 PDF 文件。您可以从这里下载:www.packtpub.com/sites/default/files/downloads/9781839214189_ColorImages.pdf。
使用的约定
本书使用了多种文本约定。
文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“接下来,将参数字典传递给DAG()。”
代码块设置如下:
import datetime as dt from datetime import timedelta
from airflow import DAG from airflow.operators.bash_operator import BashOperator from airflow.operators.python_operator import PythonOperator
import pandas as pd
任何命令行输入或输出都应如下编写:
# web properties #
nifi.web.http.port=9300
粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“点击 DAG 并选择 树视图。”
小贴士或重要注意事项
看起来是这样的。
联系我们
我们欢迎读者反馈。
customercare@packtpub.com。
勘误表:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,如果您能向我们报告这一点,我们将不胜感激。请访问 www.packtpub.com/support/errata,选择您的书籍,点击勘误表提交表单链接,并输入详细信息。
copyright@packt.com 并附上相关材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com。
评论
请留下评论。一旦您阅读并使用过这本书,为何不在购买它的网站上留下评论呢?潜在读者可以查看并使用您的客观意见来做出购买决定,Packt 公司可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!
想了解更多关于 Packt 的信息,请访问 packt.com。
第一部分:构建数据管道 – 提取、转换和加载
本节将向您介绍数据工程的基础知识。在本节中,您将了解数据工程是什么以及它与其他类似领域(如数据科学)的关系。您将学习在 Python 中处理文件和数据库的基本知识,以及使用 Apache NiFi。一旦您熟悉了移动数据,您将学习清洁和转换数据所需的技术。本节以构建数据管道结束,从 SeeClickFix 提取 311 数据,对其进行转换,并将其加载到另一个数据库中。最后,您将学习使用 Kibana 构建仪表板的基本知识,以可视化您已加载到数据库中的数据。
本节包含以下章节:
- 
第一章, 什么是数据工程? 
- 
第二章, 构建我们的数据工程基础设施 
- 
第三章, 读取和写入文件 
- 
第四章, 与数据库交互 
- 
第五章, 清洁和转换数据 
- 
第六章, 构建 311 数据管道 
第一章:第一章: 什么是数据工程?
欢迎来到《Python 数据工程》。虽然数据工程不是一个新兴领域,但它似乎最近从幕后走到了台前,开始成为焦点。本书将向您介绍数据工程的领域。您将了解数据工程师使用的工具和技术,以及如何将它们结合起来构建数据管道。完成本书后,您将能够连接到多个数据源,提取数据,转换数据,并将其加载到新的位置。您将能够构建自己的数据工程基础设施,包括集群应用程序以增加其数据处理能力。
在本章中,您将了解数据工程师的角色和职责以及数据工程如何支持数据科学。您将介绍数据工程师使用的工具,以及您需要精通的不同技术领域,以成为一名数据工程师。
在本章中,我们将涵盖以下主要主题:
- 
数据工程师的工作内容 
- 
数据工程与数据科学的比较 
- 
数据工程工具 
数据工程师的工作内容
数据工程是大数据生态系统的一部分,与数据科学紧密相连。数据工程师在幕后工作,并不像数据科学家那样受到同样的关注,但他们对于数据科学的过程至关重要。数据工程师的角色和职责根据组织的数据处理成熟度和人员配置水平而有所不同;然而,有一些任务,如数据的提取、加载和转换,是数据工程师角色的基础。
在最底层,数据工程涉及将数据从一个系统或格式移动到另一个系统或格式。使用更常见的术语,数据工程师从源(提取)查询数据,他们对数据进行一些修改(转换),然后将这些数据放置在用户可以访问并且知道它是生产质量的地方(加载)。本书中会多次使用提取、转换和加载这些术语,通常缩写为ETL。这种数据工程的定义是宽泛且简化的。通过一个例子,让我们更深入地了解数据工程师的工作内容。
一家在线零售商有一个网站,您可以在网站上购买各种颜色的 widgets。该网站由关系型数据库支持。每次交易都存储在数据库中。零售商在上个季度卖出了多少蓝色的 widgets?
为了回答这个问题,您可以在数据库上运行一个 SQL 查询。这并不需要数据工程师。但随着网站的增长,在生产数据库上运行查询不再实用。此外,可能存在多个记录交易的数据库。可能存在位于不同地理位置的数据库——例如,北美零售商可能有一个与亚洲、非洲和欧洲零售商不同的数据库。
现在,您已经进入了数据工程的领域。为了回答前面的问题,数据工程师会为每个地区的所有交易数据库创建连接,提取数据,并将其加载到数据仓库中。从那里,您现在可以计算所有蓝色 widgets 的销售数量。
与寻找销售了多少蓝色 widgets 相比,公司更希望找到以下问题的答案:
- 
我们如何找出哪些位置销售最多的 widgets? 
- 
我们如何找出销售 widgets 的峰值时间? 
- 
有多少用户将 widgets 放入购物车并在之后移除? 
- 
我们如何找出一起销售的 widgets 组合? 
回答这些问题不仅需要提取数据并将其加载到单个系统中,在提取和加载之间还需要进行转换。不同地区存在时区差异。例如,仅美国就有四个时区。因此,您需要将时间字段转换为标准格式。您还需要一种方法来区分每个地区的销售情况。这可以通过在数据中添加一个位置字段来实现。这个字段应该是空间性的——以坐标或已知文本的形式——还是仅仅是以文本形式,可以在数据工程管道中进行转换?
在这里,数据工程师需要从每个数据库中提取数据,然后通过添加一个额外的位置字段来转换数据。为了比较时区,数据工程师需要熟悉数据标准。对于时间,国际标准化组织(ISO)有一个标准——ISO 8601。
现在我们逐一回答前面列表中的问题:
- 
从每个数据库中提取数据。 
- 
在数据中为每个交易的地点添加一个标记字段 
- 
将日期从本地时间转换为 ISO 8601。 
- 
将数据加载到数据仓库中。 
通过创建数据管道来完成提取、加载和转换数据的过程。数据以原始或脏数据的形式进入管道,即可能存在缺失数据或数据中的错误,然后在管道中流动时被清理。之后,它从管道的另一侧流出进入数据仓库,在那里可以进行查询。以下图表显示了完成此任务所需的数据管道:

图 1.1 – 一个添加位置并修改日期的管道
对数据工程是什么以及数据工程师做什么有更多的了解,你应该开始对数据工程师需要获取的责任和技能有感觉。下一节将详细阐述这些技能。
成为数据工程师所需的专业技能和知识
在前面的例子中,应该很清楚数据工程师需要熟悉许多不同的技术,而我们甚至还没有提到业务流程或需求。
在数据管道的开始阶段,数据工程师需要知道如何从不同格式或不同类型的数据库中的文件中提取数据。这意味着数据工程师需要了解用于执行许多不同任务的几种语言,例如 SQL 和 Python。
在数据管道的转换阶段,数据工程师需要熟悉数据建模和结构。他们还需要了解业务以及他们希望从数据中提取的知识和见解,因为这会影响数据模型的设计。
将数据加载到数据仓库中意味着需要一个具有模式以存储数据的数据库。这通常也是数据工程师的责任。数据工程师需要了解数据仓库设计的基础知识,以及在其构建中使用的数据库类型。
最后,数据管道运行在其上的整个基础设施可能也是数据工程师的责任。他们需要知道如何管理 Linux 服务器,以及如何安装和配置 Apache Airflow 或 NiFi 等软件。随着组织向云迁移,数据工程师现在需要熟悉在组织使用的云平台上启动基础设施——亚马逊、谷歌云平台或 Azure。
通过了解数据工程师的工作示例,我们现在可以更广泛地定义数据工程。
信息
数据工程是在本地或云(或混合或多云)上开发、运营和维护数据基础设施,包括数据库和用于提取、转换和加载数据的管道。
数据工程与数据科学
数据工程是使数据科学成为可能的基础。再次强调,根据组织的成熟度,数据科学家可能需要清理和移动分析所需的数据。这不是数据科学家最佳的时间利用方式。数据科学家和数据工程师使用类似的工具(例如 Python),但他们专注于不同的领域。数据工程师需要了解数据格式、模型和结构,以便有效地传输数据,而数据科学家则利用这些工具来构建统计模型和数学计算。
数据科学家将连接到数据工程师构建的数据仓库。从那里,他们可以提取用于机器学习模型和分析所需的数据。数据科学家的模型可能被纳入数据工程管道中。数据工程师和数据科学家之间应该存在紧密的关系。了解数据科学家在数据中需要什么,只会帮助数据工程师提供更好的产品。
在下一节中,你将了解数据工程师最常用的工具。
数据工程工具
为了构建数据管道,数据工程师需要选择适合工作的正确工具。数据工程是大数据生态系统的一部分,必须考虑到大数据的三个 V:
- 
体积:数据量已经大幅增长。从一个数据库中移动一千条记录需要与移动数百万行或每分钟处理数百万笔交易不同的工具和技术。 
- 
多样性:数据工程师需要能够处理不同位置(数据库、API、文件)中各种数据格式的工具。 
- 
速度:数据速度总是不断增长。跟踪社交网络上数百万用户的活动或全球各地用户的购买行为,要求数据工程师经常在近乎实时的情况下进行操作。 
编程语言
数据工程的通用语言是SQL。无论你使用低代码工具还是特定的编程语言,几乎无法绕过对 SQL 的了解。在 SQL 方面有坚实的基础,可以让数据工程师优化查询以提高速度,并有助于数据转换。SQL 在数据工程中如此普遍,以至于数据湖和非 SQL 数据库都有工具允许数据工程师使用 SQL 查询它们。
大量开源数据工程工具使用Java和Scala(Apache 项目)。Java 是一种流行的、主流的面向对象编程语言。虽然存在争议,但 Java 正逐渐被运行在Java 虚拟机(JVM)上的其他语言所取代。Scala 是这些语言之一。在 JVM 上运行的其他语言包括Clojure和Groovy。在下一章中,你将介绍Apache NiFi。NiFi 允许你使用 Java、Clojure、Groovy 和Jython开发自定义处理器。虽然 Java 是一种面向对象的语言,但已经出现了一种向函数式编程语言转变的趋势,其中 Clojure 和 Scala 是成员。
本书重点关注使用 Python 进行数据工程。它有很好的文档记录,拥有更大的用户基础和跨平台支持。Python 已成为数据科学和数据工程的默认语言。Python 拥有一系列丰富的标准库和第三方库。Python 的数据科学环境在其它语言中是无与伦比的。pandas、matplotlib、numpy、scipy、scikit-learn、tensorflow、pytorch 和 NLTK 等库构成了一个极其强大的数据工程和数据科学环境。
数据库
在大多数生产系统中,数据将存储在 关系数据库 中。大多数专有解决方案将使用 Oracle 或 Microsoft SQL Server,而开源解决方案倾向于使用 MySQL 或 PostgreSQL。这些数据库以行存储数据,非常适合记录交易。表之间也存在关系,利用主键将一个表中的数据与另一个表中的数据连接起来——因此它们是关系型的。以下表格图显示了简单的数据模型和表之间的关系:
![图 1.2 – 在 Region = RegionID 上连接的关系表。
![图 1.2 – 在 Region = RegionID 上连接的关系表。
图 1.2 – 在 Region = RegionID 上连接的关系表。
在数据仓库中使用的最常见数据库是 Amazon Redshift、Google BigQuery、Apache Cassandra 以及其他 NoSQL 数据库,例如 Elasticsearch。Amazon Redshift、Google BigQuery 和 Cassandra 与传统的行式关系数据库不同,它们以列式格式存储数据,如下所示:
![图 1.3 – 以列式格式存储的行
![img/B15739_01_03.jpg]
图 1.3 – 以列式格式存储的行
列式数据库更适合快速查询——因此非常适合数据仓库。这三个列式数据库都可以使用 SQL 进行查询——尽管 Cassandra 使用 Cassandra 查询语言,但它们是相似的。
与列式数据库相反,还有文档型或 NoSQL 数据库,例如 Elasticsearch。Elasticsearch 实际上是一个基于 Apache Lucene 的搜索引擎。它与 Apache Solr 类似,但更易于使用。Elasticsearch 是开源的,但它确实有一些专有组件——最显著的是用于机器学习、图、安全和警报/监控的 X-Pack 插件。Elasticsearch 使用 Elastic 查询 DSL(领域特定语言)。它不是 SQL,而是一种 JSON 查询。Elasticsearch 以文档的形式存储数据,尽管它有父子文档,但它是非关系型的(类似于列式数据库)。
一旦数据工程师从数据库中提取数据,他们就需要对其进行转换或处理。在大数据中,使用数据处理引擎很有帮助。
数据处理引擎
数据处理引擎允许数据工程师将数据转换为批量或流式数据。这些引擎允许并行执行转换任务。最受欢迎的引擎是Apache Spark。Apache Spark 允许数据工程师使用 Python、Java 和 Scala 编写转换。
Apache Spark 与 Python DataFrames 一起工作,使其成为 Python 程序员的理想工具。Spark 还有弹性分布式数据集(RDDs)。RDDs 是一个不可变和分布式的对象集合。你主要通过加载外部数据源来创建它们。RDDs 允许快速和分布式处理。RDD 中的任务在集群中的不同节点上运行。与 DataFrames 不同,它们不会尝试猜测你的数据模式。
其他流行的处理引擎包括Apache Storm,它使用喷泉(spouts)读取数据,使用螺栓(bolts)执行转换。通过连接它们,你可以构建一个处理管道。Apache Flink和Samza是更现代的流式和批量处理框架,允许你处理无界流。无界流是没有已知终点的数据,例如,温度传感器就是一个无界流。它不断地报告温度。如果你使用 Apache Kafka 从系统中流式传输数据,Flink 和 Samza 是绝佳的选择。你将在本书的后面部分了解更多关于 Apache Kafka 的内容。
数据管道
将事务型数据库、编程语言、处理引擎和数据仓库结合起来,就形成了一个管道。例如,如果你从数据库中选择所有小部件销售的记录,通过 Spark 将其数据减少到小部件和计数,然后将结果存入数据仓库,你就拥有了一个管道。但是,如果你每次想要运行它时都必须手动执行,这个管道就不是很实用。数据管道需要一个调度器来允许它们在指定的时间间隔运行。最简单的方法是使用crontab。为你的 Python 文件安排一个 cron 作业,然后坐下来观看它每隔X小时运行一次。
在 crontab 中管理所有管道很快就会变得困难。你如何跟踪管道的成功和失败?你如何知道什么运行了,什么没有运行?你如何处理背压——如果一个任务比下一个任务运行得快,你如何阻止数据,以免它压倒任务?随着你的管道变得更加复杂,你将很快超出 crontab 的范围,需要一个更好的框架。
Apache Airflow
在 Python 中构建数据工程管道最流行的框架是Apache Airflow。Airflow 是由 Airbnb 构建的工作流管理平台。Airflow 由一个网络服务器、一个调度器、一个元存储、一个排队系统和执行器组成。你可以运行 Airflow 作为一个单一实例,或者将其拆分为具有许多执行节点集群——这可能是你在生产中运行它的方式。Airflow 使用有向无环图(DAGs)。
DAG 是指定任务的 Python 代码。图是由关系或依赖连接的一系列节点。在 Airflow 中,它们是定向的,因为它们以方向流动,每个任务在其依赖项之后进行。使用前面的示例管道,第一个节点将执行一个 SQL 语句,获取所有小部件的销售情况。此节点将连接到下游的另一个节点,该节点将汇总小部件和计数。最后,此节点将连接到最终节点,将数据加载到仓库中。管道 DAG 将看起来如下所示:

图 1.4 – 展示节点间数据流的一个 DAG。任务沿着箭头(有方向性)从左到右进行
本书将介绍 Apache Airflow 的基础知识,但主要将使用 Apache NiFi 来演示数据工程的原则。以下是一个 Airflow 中 DAG 的截图:

图 1.5 – 显示 DAG 详细信息的 Airflow 图形用户界面
图形用户界面不如 NiFi 精炼,我们将在下一节讨论。
Apache NiFi
Apache NiFi 是另一个用于构建数据工程管道的框架,它也利用了 DAG。Apache NiFi 由国家安全局构建,并在几个联邦机构中使用。Apache NiFi 更容易设置,对新数据工程师很有用。图形用户界面非常出色,虽然你可以使用 Jython、Clojure、Scala 或 Groovy 来编写处理器,但通过简单配置现有处理器,你可以完成很多事情。以下截图显示了 NiFi 图形用户界面和示例 DAG:

图 1.6 – 从数据库提取数据并发送到 Elasticsearch 的示例 NiFi 流
Apache NiFi 还允许集群和远程执行管道。它具有内置的调度程序,并提供管道的背压和监控。此外,Apache NiFi 使用 NiFi Registry 进行版本控制,并可用于使用 MiNiFi 收集边缘数据。
另一个用于数据工程管道的基于 Python 的工具是 Luigi – 由 Spotify 开发。Luigi 也使用图结构,并允许你连接任务。它有一个类似于 Airflow 的图形用户界面。Luigi 不会在本书中介绍,但它是基于 Python 的数据工程的优秀选择。
摘要
在本章中,你学习了数据工程是什么。数据工程的角色和责任取决于组织数据基础设施的成熟度。但数据工程在最简单的情况下,是创建管道以将数据从一个源或格式移动到另一个源或格式。这可能涉及或不涉及数据转换、处理引擎和基础设施的维护。
数据工程师使用各种编程语言,但最常见的是 Python、Java 或 Scala,以及专有和开源的事务性数据库和数据仓库,无论是在本地还是在云端,或者两者混合。数据工程师需要在许多领域具备知识——编程、运维、数据建模、数据库和操作系统。该领域的广度是使其有趣、激动人心和具有挑战性的部分。对于那些愿意接受挑战的人来说,数据工程是一个有回报的职业。
在下一章中,我们将首先设置一个环境以开始构建数据管道。
第二章:第二章:构建我们的数据工程基础设施
在上一章中,您学习了数据工程师的工作内容以及他们的角色和职责。您还介绍了一些他们使用的工具,主要是不同类型的数据库、编程语言以及数据管道创建和调度工具。
在本章中,您将安装和配置一些工具,这些工具将帮助您完成本书的其余部分。您将学习如何安装和配置两种不同的数据库——PostgreSQL 和 Elasticsearch——两种辅助构建工作流程的工具——Airflow 和 Apache NiFi,以及两种管理工具——PostgreSQL 的 pgAdmin 和 Elasticsearch 的 Kibana。
使用这些工具,您将能够编写数据工程管道,将数据从一个源移动到另一个源,并且还能够可视化结果。随着您学习如何构建管道,看到数据和它的转换情况将有助于您在调试任何错误时。随着您的进步,您可能不再需要这些工具,但您将支持的其它角色和用户可能需要它们,因此对工具的基本理解将是有用的。
本章将涵盖以下主题:
- 
安装和配置 Apache NiFi 
- 
安装和配置 Apache Airflow 
- 
安装和配置 Elasticsearch 
- 
安装和配置 Kibana 
- 
安装和配置 PostgreSQL 
- 
安装 pgAdmin 4 
安装和配置 Apache NiFi
Apache NiFi 是本书中用于构建数据工程管道的主要工具。NiFi 允许您使用预先构建的处理器来构建数据管道,您可以根据自己的需求对其进行配置。您不需要编写任何代码就能让 NiFi 管道工作。它还提供了一个调度器,用于设置您希望管道运行的频率。此外,它还能处理背压——如果某个任务比另一个任务运行得快,您可以减慢该任务的运行速度。
要安装 Apache NiFi,您需要从 nifi.apache.org/download.html 下载它:
- 
通过使用 curl,您可以使用以下命令行下载 NiFi:curl https://mirrors.estointernet.in/apache/nifi/1.12.1/nifi-1.12.1-bin.tar.gz
- 
使用以下命令从 .tar.gz文件中提取 NiFi 文件:tar xvzf nifi.tar.gz
- 
现在,您将有一个名为 nifi-1.12.1的文件夹。您可以通过在文件夹内执行以下命令来运行 NiFi:bin/nifi.sh start
- 
如果您已经安装并配置了 Java,当您运行以下片段中的状态工具时,您将看到为 JAVA_HOME设置的路径:sudo bin/nifi.sh status
- 
如果您没有看到已设置 JAVA_HOME,您可能需要使用以下命令安装 Java:sudo apt install openjdk-11-jre-headless
- 
然后,您应该编辑 .bash_profile以包含以下行,以便 NiFi 可以找到JAVA_HOME变量:export JAVA_HOME=/usr/lib/jvm/java11-openjdk-amd64
- 
最后,重新加载 .bash_profile:source .bash_profile
- 
当您对 NiFi 运行状态时,您现在应该看到 JAVA_HOME的路径:![图 2.1 – NiFi 正在运行![img/B15739_02_01.jpg]() 图 2.1 – NiFi 正在运行 
- 
当 NiFi 准备就绪时,这可能需要一分钟,打开你的网页浏览器并访问 http://localhost:8080/nifi/。你应该看到以下屏幕:

图 2.2 – NiFi 图形用户界面
在后续章节中,你将了解许多可用的 NiFi 配置,但到目前为止,你将只更改 NiFi 运行的端口。在 conf/nifi.properties 文件中,将 web properties 标题下的 nifi.web.http.port=8080 修改为 9300,如下所示:
# web properties #
nifi.web.http.port=9300
如果你的防火墙已开启,你可能需要打开端口:
sudo ufw allow 9300/tcp
现在,你可以重新启动 NiFi 并在 http://localhost:9300/nifi/ 上查看 GUI。
NiFi 快速浏览
NiFi 图形用户界面将空白,因为你还没有添加任何处理器或处理器组。屏幕顶部是组件工具栏和状态栏。组件工具栏包含构建数据流所需的工具。状态栏,正如标题所暗示的,提供了你对 NiFi 实例当前状态的概述:

图 2.3 – NiFi 组件工具栏和状态栏
你将使用最多的工具是 处理器 工具。其他工具,从左到右,如下所示:
- 
输入端口 
- 
输出端口 
- 
处理器组 
- 
远程处理器组 
- 
漏斗 
- 
模板 
- 
标签 
使用这些有限的工具,你能够构建复杂的数据流。
NiFi 数据流由处理器、连接和关系组成。NiFi 有超过 100 个处理器可供你使用。通过点击 处理器 工具并将其拖动到画布上,你将被提示选择你想要使用的处理器,如下面的截图所示:

图 2.4 – 可以添加到画布上的处理器
使用搜索栏,你可以搜索 GenerateFlowFile。选择处理器,它将被添加到画布上。此处理器将允许你创建带有文本的 FlowFiles。拖动 PutFile,然后选择处理器。此处理器将把 FlowFile 保存到磁盘上的文件。你现在应该有一个如下截图所示的画布:

图 2.5 – 添加到画布上的处理器 – 存在错误
当你添加处理器时,框的左上角将有一个警告符号。它们尚未配置,因此你会收到警告和错误。前面的截图显示 PutFile 处理器缺少 Directory 参数,没有上游连接,并且成功和失败的关系尚未处理。
要配置处理器,你可以双击处理器或右键单击并选择 属性。以下截图显示了处理器的属性:

图 2.6 – 配置 GenerateFlowFile 处理器
应遵循以下步骤来配置处理器:
- 
您必须为任何粗体的参数设置值。每个参数都有一个问号图标以帮助您。 
- 
您也可以右键单击进程并选择使用选项。 
- 
对于 GenerateFlowfile,所有必需的参数已经填写完毕。
- 
在前面的截图中,我已经为 自定义文本 参数添加了一个值。要添加自定义属性,您可以在窗口右上角单击加号。您将被提示输入名称和值。我已经添加了我的属性文件名,并将其值设置为 这是一个来自 nifi 的文件。 
- 
一旦配置完成,框中的黄色警告图标将变为方形(停止按钮)。 
现在您已经配置了第一个处理器,您需要创建一个连接并指定一个关系 – 关系通常是成功或失败,但关系类型会根据处理器而变化。
要创建连接,将鼠标悬停在处理器框上,会出现一个圆圈和箭头:
- 
将圆圈拖到下面的处理器下面( PutFile)。它会自动定位,然后提示您指定要为此连接设置哪种关系。唯一的选择将是 成功,并且它已经选中。 
- 
选择 GenerateFlowFile处理器并选择 运行。
红色方块图标将变为绿色播放按钮。现在您应该有一个如图下截图所示的数据流:
![图 2.7 – 数据流半运行
![图片 B15739_02_07.jpg]
图 2.7 – 数据流半运行
在两个处理器框之间,您可以看到队列。它将显示 FlowFiles 的数量和大小。如果您右键单击队列,您将看到一个 FlowFiles 列表,您可以获取每个 FlowFile 的详细信息,查看其内容,并下载它们。以下截图显示了队列中 FlowFiles 的列表视图:
![图 2.8 – 队列中的 FlowFiles 列表
![图片 B15739_02_08.jpg]
图 2.8 – 队列中的 FlowFiles 列表
您可以查看流和内容的详细信息。详细信息视图有两个表格 – 详细信息和属性。从 详细信息 选项卡,您将看到一些 NiFi 元数据,并具有查看或下载 FlowFile 的能力。属性 选项卡包含 NiFi 分配的属性以及您可能在数据管道中创建的任何属性。详细信息 选项卡如图下截图所示:
![图 2.9 – FlowFile 的详细信息
![图片 B15739_02_09.jpg]
图 2.9 – FlowFile 的详细信息
从 详细信息 选项卡,如果您选择查看 FlowFile,您将在窗口中看到内容。这对于基于文本的数据效果最好,但也可以选择以十六进制格式查看 FlowFile。还有选项显示原始或格式化文本。以下截图显示了原始 FlowFile 数据,它只是一个简单的文本字符串:
![图 2.10 – FlowFile 的内容
![图片 B15739_02_10.jpg]
图 2.10 – FlowFile 的内容
PutFile 处理程序将 FlowFile 保存为机器上的文件,位于 opt/nifioutput。位置可以在处理器的配置中指定。如果您没有 root 权限,您可以将其更改为您的家目录。现在您有一个完整的数据流。这不是一个非常好的数据流,但它将每 10 秒生成一个文件并将其写入磁盘,因此会覆盖旧文件。下面的截图显示了在处理器中配置的目录,以及为输出配置的文本文件。它还显示了文件的内容,这些内容将与 GenerateFlowFile 处理器生成的 FlowFiles 的内容相匹配:
![图 2.11 – 数据流输出
![img/B15739_02_11.jpg]
图 2.11 – 数据流输出
NiFi 将是本书的主要焦点,您将在下一章开始学习更多关于构建数据流的知识。您还将学习另一个工具,Apache Airflow,我们将在下一节进行安装。
PostgreSQL 驱动程序
在本章的后面部分,您将安装 PostgreSQL。为了使用 NiFi 的 ExecuteSQL 处理器连接到 PostgreSQL 数据库,您需要一个连接池,而这需要您将要连接到的数据库的 Java 数据库连接(JDBC)驱动程序。本节将向您展示如何下载该驱动程序以供以后使用。要下载它,请访问 jdbc.postgresql.org/download.html 并下载 PostgreSQL JDBC 4.2 驱动程序,版本 42.2.10。
在您的 NiFi 安装目录中创建一个名为 drivers 的新文件夹。将 postgresql-42.2.10.jar 文件移动到该文件夹中。稍后您将在 NiFi 处理器中引用此 jar 文件。
安装和配置 Apache Airflow
Apache Airflow 执行与 Apache NiFi 相同的角色;然而,它允许您使用纯 Python 创建数据流。如果您是一位强大的 Python 开发者,这可能是一个理想的工具。它目前是最受欢迎的开源数据管道工具之一。与 NiFi 相比,它在精炼的 GUI 方面可能有所欠缺,但它以强大的功能和创建任务的自由度来弥补这一点。
使用 pip 可以完成 Apache Airflow 的安装。但在安装 Apache Airflow 之前,您可以通过导出 AIRFLOW_HOME 来更改 Airflow 安装的位置。如果您希望 Airflow 安装到 opt/airflow,请导出 AIRLFOW_HOME 变量,如下所示:
export AIRFLOW_HOME=/opt/airflow
Airflow 的默认位置是 ~/airflow,对于本书,我将使用此位置。在安装 Airflow 之前,您需要考虑的是确定您想要安装哪些子包。如果您没有指定任何子包,Airflow 将仅安装运行所需的内容。如果您知道您将使用 PostgreSQL,那么您应该通过运行以下命令来安装子包:
apache-airflow[postgres]
有一个选项可以使用 all 安装所有内容,或者使用 all_dbs 安装所有数据库。本书将安装 postgreSQL、slack 和 celery。以下表格列出了所有选项:

图 2.12 – 所有包命令选项表
要使用 postgreSQL、slack 和 celery 选项安装 Apache Airflow,请使用以下命令:
pip install 'apache-airflow[postgres,slack,celery]' 
要运行 Airflow,您需要使用以下命令初始化数据库:
airflow initdb
Airflow 的默认数据库是 SQLite。这对于测试和在单台机器上运行是可以接受的,但要在生产环境和集群中运行,您需要将数据库更改为其他类型,例如 PostgreSQL。
无命令 Airflow
如果找不到 airflow 命令,您可能需要将其添加到您的路径中:
export PATH=$PATH:/home/<username>/.local/bin
Airflow Web 服务器运行在端口 8080 上,与 Apache NiFi 的端口相同。您已经在 nifi.properties 文件中将 NiFi 端口更改为 9300,因此可以使用以下命令启动 Airflow Web 服务器:
airflow webserver
如果您没有更改 NiFi 端口,或者有其他进程在端口 8080 上运行,您可以使用 -p 标志指定 Airflow 的端口,如下所示:
airflow webserver -p 8081
接下来,启动 Airflow 调度器,以便您可以在设定的时间间隔运行您的数据流。在另一个终端中运行此命令,以免终止 Web 服务器:
airflow scheduler
Airflow 可以在没有调度器的情况下运行,但如果调度器没有运行,则在启动 Web 服务器时您将收到警告。警告截图如下:

图 2.13 – 错误信息。调度器没有运行
当调度器运行时,您将看到有关由于使用 SQLite 而将并行度设置为 1 的警告。现在您可以忽略此警告,但稍后您将希望能够同时运行多个任务。警告截图如下:

图 2.14 – 调度器正在运行但警告关于 SQLite
在初始化数据库、运行中的 Web 服务器和调度器运行后,您现在可以浏览到 http://localhost:8080 并查看 Airflow GUI。Airflow 在安装期间安装了几个示例数据流(有向无环图(DAG))。您应该在主屏幕上看到它们,如下所示:

图 2.15 – 安装多个示例的 Airflow
Airflow DAG 是使用代码创建的,因此本节不会深入探讨 GUI,但在后续章节中您将更多地探索它。选择第一个 DAG – example_bash_operator – 您将被带到树视图。点击 图形视图 选项卡,您应该会看到以下截图中的 DAG:

图 2.16 – execute_bash_operator DAG 的图形视图
图形视图清楚地显示了 DAG 中的依赖关系以及任务将运行的顺序。要观察 DAG 运行,切换回 树视图。在 DAG 名称的左侧,将 DAG 切换到 开启。选择 触发 DAG,你将被提示是否现在运行它。选择 是,页面将刷新。我已经运行了 DAG 几次,你可以在下面的截图中看到这些运行的状况:


图 2.17 – execute_bash_operator DAG 的多次运行
注意,有两个 DAG 运行成功完成,有三个 DAG 运行中,这些运行中有四个待处理任务。这些示例非常适合学习如何使用 Airflow GUI,但稍后它们会变得杂乱。虽然这不一定造成问题,但如果没有所有额外的信息,更容易找到你创建的任务。
你可以通过编辑 airflow.cfg 文件来删除示例。使用 vi 或你选择的编辑器,找到以下行并将 True 更改为 False:
load_examples = True
以下截图显示了 airflow.cfg 文件,光标位于你需要编辑的行上:


图 2.18 – 设置 load_examples = False
一旦你编辑了 airflow.cfg 文件,你必须关闭 web 服务器。一旦 web 服务器停止,需要将配置更改加载到数据库中。记住,你之前已经设置了数据库,这是在 pip 安装 Airflow 之后的第一个步骤,使用以下命令:
airflow initdb
要更改数据库,这是你在更改 airflow.cfg 文件后想要做的,你需要重置它。你可以使用以下代码片段来完成:
airflow resetdb
这将把 airflow.cfg 中的更改加载到元数据库中。现在,你可以重新启动 web 服务器。当你打开 http://localhost:8080 上的 GUI 时,它应该是空的,如下面的截图所示:


图 2.19 – 清洁的 Airflow。一个 DAG 都看不到
Airflow 清洁且已准备好加载你将在下一章中创建的 DAG。
安装和配置 Elasticsearch
Elasticsearch 是一个搜索引擎。在这本书中,你将把它用作 NoSQL 数据库。你将把数据从 Elasticsearch 移到其他位置,并从其他位置移到 Elasticsearch。要下载 Elasticsearch,请按照以下步骤操作:
- 
使用 curl下载文件,如下所示:curl https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.6.0-darwin-x86_64.tar.gz --output elasticsearch.tar.gz
- 
使用以下命令提取文件: tar xvzf elasticsearch.tar.gz
- 
你可以通过编辑 config/elasticsearch.yml文件来命名你的节点和集群。在这本书的后面部分,你将设置一个具有多个节点的 Elasticsearch 集群。目前,我已经更改了以下属性:cluster.name: DataEngineeringWithPython node.name: OnlyNode
- 
现在,你可以开始启动 Elasticsearch。要启动 Elasticsearch,请运行以下命令: bin/elasticsearch
- 
一旦 Elasticsearch 启动,您可以在 http://localhost:9200看到结果。您应该看到以下输出:

图 2.20 – Elasticsearch 运行中
现在您已经运行了一个 NoSQL 数据库,您还需要一个关系型数据库。
安装和配置 Kibana
Elasticsearch 不附带 GUI,而是提供 API。要向 Elasticsearch 添加 GUI,您可以使用 Kibana。通过使用 Kibana,您可以更好地管理和与 Elasticsearch 交互。Kibana 允许您在 GUI 中访问 Elasticsearch API,但更重要的是,您可以使用它来构建存储在 Elasticsearch 中的数据的可视化仪表板。要安装 Kibana,请按照以下步骤操作:
- 
使用 wget添加以下键:wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -
- 
然后,添加相应的仓库: echo "deb https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee -a /etc/apt/sources.list.d/elastic-7.x.list
- 
最后,更新 apt并安装 Kibana:sudo apt-get update sudo apt-get install kibana
- 
Kibana 的配置文件位于 etc/kibana,应用程序位于/usr/share/kibana/bin。要启动 Kibana,请运行以下命令:bin/kibana
- 
当 Kibana 准备就绪后,浏览到 http://localhost:5601。Kibana 将在localhost的9200端口上查找任何正在运行的 Elasticsearch 实例。这就是您之前安装 Elasticsearch 的位置,也是您为什么没有在配置中更改端口的理由。当 Kibana 打开时,您将需要选择 尝试我们的样本数据 和 自行探索,如下所示:

图 2.21 – 首次启动 Kibana
自行探索 将带您进入主 Kibana 屏幕,但由于您尚未创建 Elasticsearch 索引且未加载任何数据,应用程序将是空的。
要查看 Kibana 中可用的不同工具,请选择 尝试我们的样本数据,并选择电子商务数据。以下截图显示了 加载我们的样本数据 的选项:

图 2.22 – 加载样本数据和可视化
一旦您加载了样本数据,请选择 发现 图标。从 发现 部分,您能够查看数据中的记录。如果有日期,您将看到给定时间范围内的计数条形图。您可以从此标签页选择条形或更改日期范围。选择记录将显示数据作为表格或文档的 JSON 表示。您还可以从此标签页对数据进行查询,并将它们保存为对象以供以后在可视化中使用。以下截图显示了主要的 发现 屏幕:

图 2.23 – 发现标签页
您可以从 发现选项卡中的数据或从保存的查询中创建可视化。可视化包括条形图 – 水平和垂直,饼图/甜甜圈图,计数,Markdown,热图,甚至一个地图小部件来处理地理空间数据。电子商务数据在国家级别包含地理空间数据,但地图也可以处理坐标。以下截图显示了电子商务数据的区域地图:

图 2.24 – 地图可视化
当您创建了许多可视化,来自单个索引或多个 Elasticsearch 索引时,您可以将它们添加到仪表板中。Kibana 允许您使用来自多个索引的数据加载小部件。当您在仪表板中进行查询或筛选时,只要字段名称存在于每个索引中,所有的小部件都将更新。以下截图显示了一个仪表板,由多个电子商务数据的可视化组成:

图 2.25 – 使用来自电子商务数据的多个小部件的仪表板
开发者工具选项卡在快速测试 Elasticsearch 查询并在数据工程管道中实现之前非常有用。从该选项卡,您可以创建索引和数据,执行查询以过滤、搜索或聚合数据。结果将在主窗口中显示。以下截图显示了一个记录被添加到索引中,然后针对特定 ID 进行搜索:

图 2.26 – 对单个测试记录的查询
现在你已经安装了 Elasticsearch 和 Kibana,接下来的两个部分将指导你安装 PostgreSQL 和 pgAdmin 4。之后,你将拥有一个 SQL 和一个 NoSQL 数据库来探索。
安装和配置 PostgreSQL
PostgreSQL 是一个开源的关系型数据库。它与 Oracle 或 Microsoft SQL Server 相当。PostgreSQL 还有一个插件 – postGIS – 它允许在 PostgreSQL 中实现空间功能。在这本书中,它将是首选的关系型数据库。PostgreSQL 可以作为软件包安装在 Linux 上:
- 
对于基于 Debian 的系统,使用 apt-get,如下所示:sudo apt-get install postgresql-11
- 
一旦安装完成,您可以使用以下命令启动数据库: sudo pg_ctlcluster 11 main start
- 
默认用户 postgres没有密码。要添加一个,请连接到默认数据库:sudo -u postgres psql
- 
连接后,您可以更改用户并分配密码: ALTER USER postgres PASSWORD ‚postgres';
- 
要创建数据库,您可以输入以下命令: sudo -u postgres createdb dataengineering
使用命令行速度快,但有时,GUI 会让生活变得更简单。PostgreSQL 有一个管理工具 – pgAdmin 4.
安装 pgAdmin 4
如果您是关系型数据库的新手,pgAdmin 4 将使管理 PostgreSQL 变得更加容易。基于 Web 的 GUI 将允许您查看数据,并允许您直观地创建表。要安装 pgAdmin 4,请按照以下步骤操作:
- 
您需要将存储库添加到 Ubuntu。以下命令应添加到存储库中: wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main" >> /etc/apt/sources.list.d/pgdg.list' sudo apt update sudo apt install pgadmin4 pgadmin4-apache2 -y
- 
您将被提示输入用户名和电子邮件地址,然后输入密码。您应该看到以下屏幕:![图 2.27 – 为 pgAdmin 4 创建用户 ![图片 B15739_02_25.jpg] 图 2.27 – 为 pgAdmin 4 创建用户 
- 
安装完成后,您可以浏览到 http://localhost/pgadmin4并会看到登录屏幕,如下面的截图所示。输入安装过程中创建的用户凭据:
![图 2.28 – 登录 pgAdmin 4
![图片 B15739_02_26.jpg]
图 2.28 – 登录 pgAdmin 4
登录后,您可以从 GUI 管理您的数据库。下一节将为您简要介绍 pgAdmin 4。
pgAdmin 4 导览
登录 pgAdmin 4 后,您将在左侧看到带有服务器图标的主面板。目前尚未配置任何服务器,因此您将想要添加本章中较早安装的服务器。
点击仪表板上的 添加新服务器 图标。您将看到一个弹出窗口。添加您的 PostgreSQL 实例信息,如下面的截图所示:
![图片 B15739_02_29.jpg]
![图片 B15739_02_27.jpg]
图 2.29 – 添加新服务器
添加服务器后,您可以展开服务器图标,应该会看到您之前创建的数据库 – dataengineering。展开 dataengineering 数据库,然后是 schemas,然后是 public。您将能够右键单击 表 来向数据库添加表,如下面的截图所示:
![图 2.30 – 创建表
![图片 B15739_02_28.jpg]
图 2.30 – 创建表
要用数据填充表,请命名表,然后选择 列 选项卡。创建一个包含有关人员信息的表。表如下面的截图所示:
![图 2.31 – 表数据
![图片 B15739_02_29.jpg]
图 2.31 – 表数据
在下一章中,您将使用 Python 和 faker 库来填充此表中的数据。
摘要
在本章中,您学习了如何安装和配置数据工程师使用的许多工具。完成这些操作后,您现在拥有了一个可以构建数据管道的工作环境。在生产环境中,您不会在单个机器上运行所有这些工具,但在接下来的几章中,这将帮助您快速学习和入门。您现在有两个工作数据库 – Elasticsearch 和 PostgreSQL – 以及两个构建数据管道的工具 – Apache NiFi 和 Apache Airflow。
在下一章中,您将开始使用 Apache NiFi 和 Apache Airflow(Python)来连接文件,以及 Elasticsearch 和 PostgreSQL。您将在 NiFi 和 Airflow 中构建您的第一个管道,将 CSV 文件移动到数据库中。
第三章:第三章:读取和写入文件
在上一章中,我们探讨了如何安装各种工具,例如 NiFi、Airflow、PostgreSQL 和 Elasticsearch。在本章中,你将学习如何使用这些工具。数据工程中最基本的一项任务是将数据从文本文件移动到数据库。在本章中,你将读取来自不同基于文本格式的数据,例如 CSV 和 JSON,并将数据写入这些格式。
在本章中,我们将涵盖以下主要内容:
- 
在 Python 中读取和写入文件 
- 
在 Airflow 中处理文件 
- 
NiFi 处理器用于处理文件 
- 
在 Python 中读取和写入数据库中的数据 
- 
Airflow 中的数据库 
- 
NiFi 中的数据库处理器 
在 Python 中编写和读取文件
这个小节的标题可能听起来有些奇怪,因为你可能习惯于看到它被写成读取和写入,但在这个小节中,你将首先写入数据到文件,然后读取它。通过写入,你将了解数据的结构,并且你会知道你试图读取的内容是什么。
要写入数据,你将使用一个名为 faker 的库。faker 允许你轻松地为常见字段创建假数据。你可以通过调用 address() 生成地址,或者使用 name_female() 生成女性名字。这将简化假数据的创建,同时使其更加真实。
要安装 faker,你可以使用 pip:
pip3 install faker
现在已经安装了 faker,你可以开始编写文件了。下一节将从 CSV 文件开始。
编写和读取 CSV 文件
你最常遇到的文件类型是 逗号分隔值(CSV)。CSV 是由逗号分隔的字段组成的文件。因为逗号在文本中相当常见,所以你需要能够在 CSV 文件中处理它们。这可以通过使用转义字符来实现,通常是在可能包含逗号的文本字符串周围使用一对引号,而这个逗号不是用来表示新字段的。这些引号被称为转义字符。Python 标准库简化了处理 CSV 数据的过程。
使用 Python CSV 库编写 CSV 文件
使用 CSV 库编写 CSV 文件时,你需要遵循以下步骤:
- 
以写入模式打开文件。要打开文件,你需要指定一个文件名和模式。写入模式为 w,但你也可以使用r打开文件进行读取,使用a进行追加,或者使用r+进行读写。最后,如果你正在处理非文本文件,你可以在任何前面提到的模式中添加b,以二进制模式写入;例如,wb将允许你以字节形式写入:output = open('myCSV.CSV',mode='w')
- 
创建 CSV_writer。至少,你必须指定一个要写入的文件,但你也可以传递额外的参数,例如方言。方言可以是定义好的 CSV 类型,如 Excel,或者是一些选项,如要使用的分隔符或引号级别。默认值通常是您所需要的;例如,分隔符默认为逗号(毕竞这是一个 CSV 写入器),引号默认为QUOTE_MINIMAL,这意味着只有当字段中有特殊字符或分隔符时才会添加引号。因此,你可以像下面这样创建写入器:mywriter=csv.writer(output)
- 
包含标题。你可能能够记住 CSV 中的字段,但最好还是包含标题。写入标题与写入任何其他行相同:定义值,然后使用 writerow(),如下所示:header=['name','age'] mywriter.writerow(header)
- 
将数据写入文件。现在,你可以通过使用 writerow(0)并传递一些数据来写入一行数据,如下所示:data=['Bob Smith',40] mywriter.writerow(data) output.close()
现在,如果你查看目录,你将有一个名为myCSV.CSV的 CSV 文件,其内容应如下截图所示:

图 3.1 – mycsv.csv 的内容
注意,当你使用cat查看文件时,新行被添加了。默认情况下,CSV_writer使用回车和换行符('\r\n')。
上述示例非常基础。然而,如果你试图写入大量数据,你很可能希望通过某种条件循环或遍历现有数据。在以下示例中,你将使用Faker生成 1,000 条记录:
from faker import Faker
import csv
output=open('data.CSV','w')
fake=Faker()
header=['name','age','street','city','state','zip','lng','lat']
mywriter=csv.writer(output)
mywriter.writerow(header)
for r in range(1000):
    mywriter.writerow([fake.name(),fake.random_int(min=18, 
    max=80, step=1), fake.street_address(), fake.city(),fake.
    state(),fake.zipcode(),fake.longitude(),fake.latitude()])
    output.close()
现在你应该有一个包含 1,000 行姓名和年龄的data.CSV文件。
现在你已经写入了 CSV,下一节将指导你使用 Python 读取它。
读取 CSV
读取 CSV 与写入 CSV 有些类似。遵循相同的步骤,但略有修改:
- 
使用 with打开文件。使用with有一些额外的优势,但就目前而言,你将获得的好处是不必在文件上使用close()。如果你没有指定模式,open默认为读取模式(r)。在open之后,你需要指定要引用的文件名;在这种情况下,你将打开data.CSV文件并将其命名为f:with open('data.csv') as f:
- 
创建读取器。你将使用 DictReader()而不是仅仅使用reader()。通过使用字典读取器,你将能够通过名称而不是位置来调用数据中的字段。例如,你不再需要将行中的第一个项目称为row[0],现在你可以将其称为row['name']。就像写入器一样,默认值通常足够,你只需要指定一个要读取的文件。以下代码使用f变量名打开data.CSV:myreader=CSV.DictReader(f)
- 
通过使用 next()读取一行来获取标题:headers=next(myreader)
- 
现在,你可以使用以下方式遍历其余的行: for row in myreader:
- 
最后,你可以使用以下方式打印名称: print(row['name'])
你应该只看到 1,000 个名字滚动过去。现在你有一个可以按任何需要操作的 Python 字典。在 Python 中处理 CSV 数据还有另一种方法,这需要 pandas。
使用 pandas DataFrames 读取和写入 CSV
pandas DataFrame 是一个强大的工具,不仅用于读取和写入数据,还用于查询和操作数据。它确实需要比内置的 CSV 库更大的开销,但在某些情况下,这可能值得权衡。根据你的 Python 环境,你可能已经安装了 pandas,如果没有,你可以使用以下命令安装它:
pip3 install pandas
你可以将 pandas DataFrame 想象成一个 Excel 表格或表格。你将拥有行、列和索引。要将 CSV 数据加载到 DataFrame 中,必须遵循以下步骤:
- 
导入 pandas(通常作为pd):import pandas as pd
- 
然后,使用 read_csv()方法读取文件。read_csv()方法有几个可选参数和一个必需参数 – 文件或文件缓冲区。可能对你感兴趣的两个可选参数是header,默认情况下尝试推断标题。如果你设置header=0,则可以使用names参数与列名数组一起使用。如果你有一个大文件,只想查看其中的一部分,可以使用nrows来指定要读取的行数,所以nrows=100意味着它只会读取 100 行数据。在以下代码片段中,你将使用默认值加载整个文件:df=pd.read_csv()('data.CSV')
- 
让我们使用以下方式查看前 10 条记录: df.head(10)
因为使用了 Faker 生成数据,所以你将拥有以下截图中的相同模式,但值将不同:

图 3.2 – 将 CSV 读取到 DataFrame 并打印 head()
你可以使用以下步骤在 Python 中创建一个 DataFrame:
- 
创建一个数据字典。字典是一种数据结构,它将数据存储为键:值对。值可以是任何 Python 数据类型 – 例如,一个数组。字典有用于查找 keys()、values()和items()的方法。它们还允许你通过使用括号中的键名来查找键的值 – 例如,dictionary['key']将返回该键的值:data={'Name':['Paul','Bob','Susan','Yolanda'], 'Age':[23,45,18,21]}
- 
将数据传递给 DataFrame: df=pd.DataFrame(data)
- 
列被指定为字典中的键。现在你有了 DataFrame,你可以使用 to_csv()方法并将文件名传递给它来将内容写入 CSV。在示例中,我们没有设置索引,这意味着行名将是从 0 到 n 的数字,其中 n 是 DataFrame 的长度。当你导出为 CSV 时,这些值将被写入文件,但列名将是空的。所以,在你不需要将行名或索引写入文件的情况下,将index参数传递给to_csv(),如下所示:df.to_csv('fromdf.CSV',index=False)
现在,你将有一个包含 DataFrame 内容的 CSV 文件。我们将在下一章中介绍如何使用此 DataFrame 的内容来执行 SQL 查询。它们将成为你工具箱中的重要工具,本书的其余部分将大量依赖它们。
现在,让我们继续下一节,你将学习另一种常见的文本格式 - JSON。
使用 Python 写入 JSON
你可能会处理另一种常见的数据格式 - JSON–JSON。
要使用 Python 和标准库写入 JSON,需要遵循以下步骤:
- 
导入库并打开你要写入的文件。你还需要创建 Faker对象:from faker import Faker import json output=open('data.JSON','w') fake=Faker()
- 
我们将创建 1,000 条记录,就像在 CSV 示例中做的那样,因此你需要创建一个字典来保存数据。如前所述,键的值可以是任何 Python 数据类型 - 包括值的数组。在创建保存记录的字典后,添加一个 'records'键,并用一个空数组初始化,如下所示:alldata={} alldata['records']=[]
- 
要写入记录,你使用 Faker创建一个字典,然后将其追加到数组中:for x in range(1000): data={"name":fake.name(),"age":fake.random_int (min=18, max=80, step=1), "street":fake.street_address(), "city":fake.city(),"state":fake.state(), "zip":fake.zipcode(), "lng":float(fake.longitude()), "lat":float(fake.latitude())} alldata['records'].append(data)
- 
最后,要将 JSON 写入文件,使用 JSON.dump()方法。传递要写入的数据和要写入的文件:json.dump(alldata,output)
现在,你有一个包含 1,000 条记录的数组 data.JSON 文件。你可以通过以下步骤读取此文件:
- 
使用以下方式打开文件: with open("data.JSON","r") as f:
- 
使用 JSON.load()并将文件引用传递给该方法:data=json.load(f)
- 
通过查看第一条记录来检查 json,使用以下方式: data['records'][0]或者直接使用文件名: data['records'][0]['name']
当你使用 loads 和 dumps 时,与 load 和 dump 不同。它们都是 JSON 库的有效方法。区别在于 loads 和 dumps 用于字符串 - 它们不会序列化 JSON。
pandas DataFrame
使用 DataFrame 读取和写入 JSON 与我们处理 CSV 类似。唯一的区别是将 to_csv 改为 to_json(),将 read_csv() 改为 read_json()。
如果你有一个干净、格式良好的 JSON 文件,你可以使用以下代码读取它:
df=pd.read_json('data.JSON')
在 data.JSON 文件的情况下,记录嵌套在一个 records 字典中。因此,加载 JSON 不像前面的代码那样直接。你需要额外的几个步骤,如下所示。要从文件加载 JSON 数据,请执行以下操作:
- 
使用 pandas的JSON库:import pandas.io.json as pd_JSON
- 
使用 pandas版本的JSON.loads()打开文件并加载:f=open('data.JSON','r') data=pd_JSON.loads(f.read())
- 
要创建 DataFrame,你需要规范化 JSON。规范化是将 JSON 展平以适应表格的过程。在这种情况下,你想要获取存储在 records字典中的单个 JSON 记录。将此路径 -records- 传递给json_normalize()的record_path参数:df=pd_JSON.json_normalize(data,record_path='records')
现在,你将有一个包含 data.JSON 文件中所有记录的 DataFrame。你现在可以使用 DataFrame 将它们写回 JSON 或 CSV。
在写入 JSON 时,你可以传递 orient 参数,该参数确定返回的 JSON 格式。默认为列,对于你在上一节中创建的 data.JSON 文件,它看起来如下所示的数据:
>>> df.head(2).to_json()
'{"name":{"0":"Henry Lee","1":"Corey Combs DDS"},"age":{"0":42,"1":43},"street":{"0":"57850 Zachary Camp","1":"60066 Ruiz Plaza Apt. 752"},"city":{"0":"Lake Jonathon","1":"East Kaitlin"},"state":{"0":"Rhode Island","1":"Alabama"},"zip":{"0":"93363","1":"16297"},"lng":{"0":-161.561209,"1":123.894456},"lat":
{"0":-72.086145,"1":-50.211986}}'
通过将 orient 值更改为 records,你将得到 JSON 中的每一行作为一个记录,如下所示:
>>> df.head(2).to_JSON(orient='records')
'[{"name":"Henry Lee","age":42,"street":"57850, Zachary Camp","city":"Lake Jonathon","state":"Rhode Island", "zip":"93363","lng":-161.561209,"lat":72.086145},{"name":"Corey Combs DDS","age":43,"street":"60066 Ruiz Plaza Apt. 752","city":"EastKaitlin","state":"Alabama","zip":"16297","lng":123.894456, "lat":-50.211986}]'
我发现,围绕 records 定向的 JSON 在 Airflow 等工具中处理起来比其他格式(如 split、index、columns、values 或 table)的 JSON 要容易得多。现在你已知道如何在 Python 中处理 CSV 和 JSON 文件,是时候学习如何使用 Airflow 和 NiFi 将任务组合成数据管道了。在下一节中,你将学习如何在 Apache Airflow 中构建管道。
构建 Apache Airflow 中的数据管道
Apache Airflow 使用 Python 函数以及 Bash 或其他操作符来创建可以组合成 有向无环图(DAG)的任务——这意味着每个任务在完成时都朝一个方向移动。Airflow 允许你组合 Python 函数来创建任务。你可以指定任务的运行顺序以及哪些任务依赖于其他任务。这种顺序和依赖性使其成为 DAG。然后,你可以在 Airflow 中安排你的 DAG 以指定 DAG 应该何时以及多久运行一次。使用 Airflow GUI,你可以监控和管理你的 DAG。通过使用前面章节中学到的知识,你现在将在 Airflow 中构建数据管道。
构建 CSV 到 JSON 数据管道
从一个简单的 DAG 开始将帮助你理解 Airflow 的工作原理,并有助于你添加更多函数来构建更好的数据管道。你构建的 DAG 将使用 Bash 打印一条消息,然后读取 CSV 并打印所有名称的列表。以下步骤将指导你构建数据管道:
- 
使用 Python IDE 或任何文本编辑器打开一个新文件。导入所需的库,如下所示: import datetime as dt from datetime import timedelta from airflow import DAG from airflow.operators.bash_operator import BashOperator from airflow.operators.python_operator import PythonOperator import pandas as pd前两个导入引入了 datetime和timedelta。这些库用于安排 DAG。三个 Airflow 导入引入了构建 DAG 和使用 Bash 和 Python 操作符所需的库。这些是你将用于构建任务的操作符。最后,你导入pandas以便你可以轻松地在 CSV 和 JSON 之间进行转换。
- 
接下来,编写一个函数来读取 CSV 文件并打印出名称。通过结合之前章节中读取 CSV 数据和写入 JSON 数据的步骤,你可以创建一个函数,该函数读取 data.CSV文件并将其写入 JSON,如下面的代码所示:def CSVToJson(): df=pd.read_CSV('/home/paulcrickard/data.CSV') for i,r in df.iterrows(): print(r['name']) df.to_JSON('fromAirflow.JSON',orient='records')此函数在 DataFrame 中打开文件。然后,它遍历行,仅打印名称,最后将 CSV 写入 JSON 文件。 
- 
现在,您需要实现管道的 Airflow 部分。指定将传递给 DAG()的参数。在这本书中,您将使用一组最小的参数。本例中的参数分配了一个所有者、开始日期、失败时的重试次数以及重试前的等待时间。它们在以下字典中显示:default_args = { 'owner': 'paulcrickard', 'start_date': dt.datetime(2020, 3, 18), 'retries': 1, 'retry_delay': dt.timedelta(minutes=5), }
- 
接下来,将参数字典传递给 DAG()。创建 DAG ID,设置为MyCSVDAG,参数字典(前述代码中的default_args变量),以及调度间隔(数据管道的运行频率)。调度间隔可以使用timedelta设置,或者您可以使用以下预设或 crontab 格式:a) @onceb) @hourly–0 * * * *c) @daily–0 0 * * *d) @weekly–0 0 * * 0e) @monthly–0 0 1 * *f) @yearly–0 0 1 1 *crontab 使用分钟、小时、月份中的天、月份、星期的格式。 @yearly的值为0 0 1 1 *,这意味着在每年的 1 月 1 日(1 1)午夜运行,在星期的任何一天(*)。with DAG('MyCSVDAG', default_args=default_args, schedule_interval=timedelta(minutes=5), # '0 * * * *', ) as dag:
- 
您现在可以使用运算符来创建任务。Airflow 有几个预构建的运算符。您可以在文档中查看它们,链接为 airflow.apache.org/docs/stable/_api/airflow/operators/index.html。在这本书中,您将主要使用 Bash、Python 和 Postgres 运算符。这些运算符允许您移除执行常见任务所需的大部分样板代码。在下面的代码片段中,您将使用 Bash 和 Python 运算符创建两个任务:print_starting = BashOperator(task_id='starting', bash_command='echo "I am reading the CSV now....."') CSVJson = PythonOperator(task_id='convertCSVtoJson', python_callable=CSVToJson)以下代码片段使用 BashOperator运算符创建了一个任务,该任务打印一条语句以通知您它正在运行。此任务除了允许您看到如何连接多个任务外,没有其他用途。下一个任务CSVJson使用PythonOperator运算符调用文件开头定义的函数(CSVToJson())。该函数读取data.CSV文件并打印每一行的name字段。
- 
定义了任务后,您现在需要建立任务之间的连接。您可以使用 set_upstream()和set_downstream()方法或使用位移运算符来完成此操作。通过使用上游和下游,您可以使用两个代码片段中的任何一个将图从 Bash 任务转换为 Python 任务;以下是最初的代码片段:print_starting .set_downstream(CSVJson)下面的代码片段是第二个: CSVJson.set_upstream(print_starting)使用位移运算符,您可以执行相同的操作;以下是一个选项: print_starting >> CSVJson以下是一个选项: CSVJson << print_starting注意 您选择哪种方法取决于您;然而,您应该保持一致。在这本书中,您将看到位移运算符设置下游。 
- 
要在 GUI 中使用 Airflow 和调度器,您首先需要为您的 DAGs 创建一个目录。在上一章中安装和配置 Apache Airflow 时,我们删除了示例,因此 DAG 目录缺失。如果您查看 airflow.cfg,您将看到dags_folder的设置。它的格式是$AIRFLOW_HOME/dags。在我的机器上,$AIRFLOW_HOME是home/paulcrickard/airflow。这是您将创建dags folder.e配置文件的目录,显示文件夹应该在哪里。
- 
将您的 DAG 代码复制到文件夹中,然后运行以下命令: airflow webserver airflow scheduler
- 
通过打开您的网页浏览器并访问 http://localhost:8080来启动 GUI。您将看到您的 DAG,如下面的截图所示:![图 3.3 – Airflow GUI 主屏幕显示 MyCSVDAG]() 图 3.3 – Airflow GUI 主屏幕显示 MyCSVDAG 
- 
点击 DAGs 并选择 树视图。开启 DAG,然后点击 Go。随着任务的开始运行,您将看到每个运行的状况,如下面的截图所示: ![图 3.4 – DAG 的多次运行和每个任务的状况]() 图 3.4 – DAG 的多次运行和每个任务的状况 
- 
您将看到已经成功运行 – 每个任务都运行成功了。但没有输出或结果。要查看结果,请点击其中一个已完成的方块,如下面的截图所示: ![图 3.5 – 通过悬停在已完成的任务上检查结果]() 图 3.5 – 通过悬停在已完成的任务上检查结果 
- 
您将看到一个带有几个选项的弹出窗口。点击以下截图所示的 查看日志 按钮: ![图 3.6 – 选择查看日志以查看任务中发生的情况]() 图 3.6 – 选择查看日志以查看任务中发生的情况 
- 
您将被重定向到任务的日志屏幕。查看 CSV 任务的成功运行,您应该看到一个类似于以下截图的日志文件: 

图 3.7 – Python 任务的日志显示正在打印的名称
恭喜!您已使用 Python 和 Airflow 构建了一个数据管道并成功运行。您的管道结果是一个 JSON 文件,位于 dags 目录中,该目录是从您的 data.CSV 文件创建的。您可以让它继续运行,它将在指定的 schedule_interval 时间继续运行。构建更高级的管道只需要您编写更多的函数并将它们通过相同的过程连接起来。但在您继续学习更高级的技术之前,您需要学习如何使用 Apache NiFi 来构建数据管道。
使用 NiFi 处理器处理文件
在前面的章节中,您学习了如何使用 Python 读取和写入 CSV 和 JSON 文件。读取文件是一项如此常见的任务,以至于像 NiFi 这样的工具已经预置了处理器来处理它。在本节中,您将学习如何使用 NiFi 处理器处理文件。
在 NiFi 中处理 CSV
在 NiFi 中处理文件比在 Python 中执行相同任务需要更多步骤。使用更多步骤和 NiFi 的好处包括,即使不知道代码的人也能查看您的数据管道并理解您正在做什么。您甚至可能会发现,当您将来回到管道时,更容易记住您当时试图做什么。此外,对数据管道的更改不需要重构大量代码;相反,您可以通过拖放重新排序处理器。
在本节中,您将创建一个数据管道,该管道读取您在 Python 中创建的data.CSV文件。它将运行针对 40 岁以上人群的查询,然后将该记录写入文件。
本节的结果如图所示:

图 3.8 – 本节中您将构建的数据管道
以下章节将指导您构建数据管道。
使用 GetFile 读取文件
您的数据管道的第一步是读取data.csv文件。为此,请执行以下步骤:
- 
从 NiFi 工具栏拖动处理器图标到画布上。搜索GetFile并选择它。 
- 
要配置 GetFile处理器,您必须指定输入目录。在本章前面的 Python 示例中,我将data.CSV文件写入我的家目录,即home/paulcrickard,因此我将使用它作为输入目录。
- 
接下来,您需要指定一个文件过滤器。该字段允许使用 NiFi 表达式语言,因此您可以使用 [^\.].*\.CSV——但在这个例子中,您可以将值设置为data.csv。
- 
最后,将保留源文件属性设置为true。如果您将其保留为false,NiFi 在处理完文件后将其删除。完整的配置如图所示: 

图 3.9 – GetFile 处理器配置
将记录分割成独立的 flowfiles
现在,您可以将GetFile处理器到SplitRecord处理器的成功关系传递:
- 
SplitRecord处理器将允许您将每一行分离成单独的 flowfile。将其拖放到画布上。您需要创建一个记录读取器和记录写入器——NiFi 已经预置了几个您可以选择配置的。点击Record Reader旁边的框,选择创建新服务,如图所示:![图 3.10 – 可用读取器列表]() 图 3.10 – 可用读取器列表 
- 
您需要选择读取器的类型。从下拉菜单中选择CSVReader。选择Record Writer的下拉菜单并选择CSVRecordSetWriter: ![图 3.11 – 可用读取器的列表]() 图 3.11 – 可用读取器的列表 
- 
要配置CSVReader和CSVRecordSetWriter,点击任一右侧的箭头。这将打开CONTROLLER SERVICES选项卡上的文件配置窗口。您将看到以下截图所示的屏幕: 

图 3.12 – 配置读取器和写入器
右侧的三个图标如下:
- 
一个齿轮用于设置 
- 
用于启用和禁用服务的闪电符号(目前处于禁用状态) 
- 
一个垃圾桶用于删除它 
选择CSVReader的齿轮。默认配置将工作,除了Treat First Line as Header属性,应设置为true。点击CSVRecordSetWriter的齿轮,您可以看到可用的属性。在这个例子中,默认值足够。现在,点击闪电符号以启用服务。
使用 QueryRecord 处理器过滤记录
您现在有一个管道,可以读取 CSV 并将行拆分为单独的 flowfile。现在您可以使用QueryRecord处理器处理每一行。此处理器将允许您对 flowfile 执行 SQL 命令。新 flowfile 的内容将是 SQL 查询的结果。在这个例子中,您将选择所有年龄超过 40 岁的人的记录:
- 
将 QueryRecord处理器拖放到画布上。要查询 flowfile,您需要指定一个记录读取器和写入器。您已经创建了一个,现在它们都可在下拉菜单中找到。Include Zero Record FlowFiles属性应设置为false。此属性将不符合条件的记录路由到相同的关系(您不希望这样做)。
- 
最后,点击右上角的加号并在弹出窗口中指定一个属性名称。当您从这个处理器创建连接时,属性名称将成为一个关系。将属性命名为 over.40。然后,将出现值弹出窗口。这就是您将输入 SQL 查询的地方。查询的结果将成为 flowfile 的内容。由于您想要 40 岁以上的人的记录,查询如下:Select * from FlowFile where age > 40Select*查询返回整个 flowfile。如果您只想获取人的姓名并且字段为full_name,您可以运行以下 SQL:Select name as full_name from FlowFile where age > 40
我在这里试图强调的是,您可以执行 SQL 并修改 flowfile 的内容,使其不同于行内容 – 例如,运行聚合和分组。
从 flowfile 中提取数据
下一个处理器将从 flowfile 中提取一个值。该处理器是 ExtractText。该处理器可用于包含文本的任何 flowfile,并使用正则表达式从 flowfile 中提取任何数据并将其分配给属性。
要配置处理器,点击加号并命名属性。你将从 flowfile 中提取人名,因此你可以命名属性为 name。值将是正则表达式,应该如下所示:
\n([^,]*),
没有完整的正则表达式教程,前面的正则表达式语句查找换行符和逗号 – \n 和结尾的逗号 – 并抓取其间的文本。括号表示取文本并返回任何不是 ^ 或逗号的字符。这个正则表达式返回的是人的名字。Flowfile 包含 CSV 字段名的标题,一个换行符,然后是 CSV 中的值。name 字段是第二行的第一个字段 – 在换行符之后,在指定 name 字段结束的第一个逗号之前。这就是为什么正则表达式查找换行符和逗号之间的文本。
修改 flowfile 属性
现在你已经将人名作为属性提取出来,你可以使用 UpdateAttribute 处理器来更改现有属性值。通过使用此处理器,你将修改 NiFi 在 GetFile 处理器一开始提供的默认文件名属性。每个 flowfile 都将具有 data.CSV 的文件名。如果你尝试将 flowfiles 写入 CSV,它们都将具有相同的名称,并且可能会覆盖或失败。
在 UpdateAttribute 处理器的配置中点击加号并命名新属性文件名。值将使用 NiFi 表达式语言。在表达式语言中,你可以使用格式 ${attribute name} 来获取属性的值。因此,要使用 name 属性,将值设置为 ${name}。
将 flowfile 保存到磁盘
使用 PutFile 处理器,你可以将 flowfile 的内容写入磁盘。要配置处理器,你需要指定一个写入文件的目录。我将继续使用我的家目录。
接下来,你可以指定一个冲突解决策略。默认情况下,它将被设置为失败,但它允许你覆盖现有文件。如果你运行这个数据管道,每小时聚合数据并将结果写入文件,你可能将属性设置为覆盖,以便文件始终包含最新的数据。默认情况下,flowfile 将写入磁盘上的文件,文件名为属性文件名。
在处理器之间建立关系
最后一步是为处理器之间的指定关系建立连接:
- 
捕获 GetFile处理器,将箭头拖到SplitRecord处理器,并在弹出窗口中检查关系成功。
- 
从 SplitRecord处理器连接到QueryRecord处理器,并选择关系拆分。这意味着任何被拆分的记录将被发送到下一个处理器。
- 
从 QueryRecord连接到ExtractText处理器。注意你创建的关系被命名为over.40。如果你添加了更多的 SQL 查询,你会得到更多的关系。对于这个例子,使用over.40关系。
- 
将 ExtractText连接到匹配关系的UpdateAttribute处理器。
- 
最后,将 UpdateAttribute连接到PutFile处理器以成功处理关系。
数据管道现在已完成。你可以点击每个处理器并选择运行来启动它 – 或者点击操作窗口中的运行图标来一次性启动所有处理器。
当管道完成时,你将有一个包含所有超过 40 岁的人的行的目录。在 1,000 条记录中,我有 635 个以每个人命名的 CSV 文件。你将根据 Faker 使用的年龄值得到不同的结果。
本节向你展示了如何读取 CSV 文件。你还学习了如何将文件拆分为行并对它们进行查询,以及如何修改流文件的属性并在另一个处理器中使用它。在下一节中,你将使用 JSON 构建另一个数据管道。
在 NiFi 中处理 JSON
尽管结构不同,在 NiFi 中处理 JSON 与处理 CSV 非常相似。然而,有几个处理器专门用于处理 JSON。在本节中,你将构建一个类似于 CSV 示例的流程 – 读取一个文件,将其拆分为行,并将每一行写入一个文件 – 但你将在管道中对数据进行一些修改,以便写入磁盘的行与原始文件中的不同。以下图显示了完成的数据管道:

图 3.13 – 完成的 JSON 数据管道
要构建数据管道,请按照以下步骤操作:
- 
将 GetFile处理器放置到画布上。要配置处理器,指定home/paulcrickard– 和data.JSON。
- 
在 CSV 示例中,你使用了 SplitRecord处理器。这里,对于 JSON,你可以使用SplitJson处理器。你需要配置 JsonPath 表达式属性。此属性正在寻找包含 JSON 元素的数组。JSON 文件格式如下:{"records":[ { } ] }因为每个记录都在一个数组中,你可以将以下值传递给 JsonPath 表达式属性: $.records这将拆分数组内的记录,这是你想要的结果。 
- 
记录现在将成为单独的 flowfile。你将文件传递给 EvaluateJsonPath处理器。此处理器允许你从 flowfile 中提取值。你可以将结果传递到 flowfile 内容或属性中。设置flowfile-attribute的值。然后你可以选择使用加号创建的属性。你需要为属性命名,然后指定值。值是 JSON 路径,你使用格式$.key。配置的处理器如下面的截图所示:![图 3.14 – 从 flowfile 中提取值的配置![img/B15739_03_14.jpg]() 图 3.14 – 从 flowfile 中提取值的配置 这些属性不会随着 flowfile 流经数据管道。 
- 
现在,你可以使用 QueryRecord处理器,就像你在 CSV 示例中所做的那样。与 JSON 的区别在于你需要创建一个新的记录读取器和记录集写入器。选择创建新服务的选项。选择over.40并将值设置为以下内容:Select * from FlowFile where age > 40
- 
下一个处理器是 AttributesToJSON处理器。此处理器允许你用在步骤 3中显示的EvaluateJsonPath处理器中提取的属性替换 flowfile 内容。设置flowfile-content。此处理器还允许你在属性列表属性中指定以逗号分隔的属性列表。如果你只想使用某些属性,这可能会很有用。在这个例子中,你将其留空,并且一些你没有提取的属性将被添加到 flowfile 内容中。NiFi 写入的所有元数据属性现在将成为 flowfile 的一部分。flowfile 现在将看起来如下面的片段所示:### Run it at night ###
- 
再次使用 EvalueJsonPath处理器,你将创建一个名为uuid的属性。现在 NiFi 的元数据已经在 flowfile 中,你有 flowfile 的唯一 ID。请确保设置flowfile-attribute。你现在将提取它,以便将其传递给下一个处理器UpdateAttribute。
- 
在 CSV 示例中,你使用 UpdateAttribute处理器更新了文件名。你在这里也将这样做。点击加号并添加一个名为filename的属性。将值设置为${uuid}。
- 
使用 NiFi 修改 JSON 的一种方法是通过 flowfile 中的 zip字段。
- 
最后,使用 PutFile处理器将每一行写入磁盘。配置目录和冲突解决策略属性。通过将冲突解决策略属性设置为忽略,处理器在已经处理了具有相同名称的文件时不会警告你。
创建处理器之间的连接和关系:
- 
将 GetFile连接到SplitJson以实现关系成功。
- 
将 SplitJson连接到EvaluateJsonPath以实现关系拆分。
- 
将 EvaluateJsonPath连接到QueryRecord以实现关系匹配。
- 
将 QueryRecord连接到AttributesToJSON以实现关系over.40。
- 
将 AttributesToJSON连接到UpdateAttribute以实现关系成功。
- 
将 UpdateAttributes连接到JoltTransformJSON以实现关系成功。
- 
将 JoltTransformJSON连接到PutFile以实现关系成功。
通过启动每个处理器或在操作框中点击运行来运行数据管道。完成后,你将在磁盘上拥有一个包含 1,000 个文件的子集——所有 40 岁以上的人——并且按其唯一 ID 命名。
摘要
在本章中,你学习了如何使用 Python 处理 CSV 和 JSON 文件。通过这项新技能,你通过创建一个 Python 函数来处理 CSV 并将其转换为 JSON,在 Apache Airflow 中创建了一个数据管道。你现在应该对 Airflow GUI 以及如何运行 DAGs 有一个基本的了解。你还学习了如何使用处理器在 Apache NiFi 中构建数据管道。构建更高级数据管道的过程是相同的,你将在本书的其余部分学习完成此任务所需的所有技能。
在下一章中,你将学习如何使用 Python、Airflow 和 NiFi 读取和写入数据库中的数据。你将学习如何使用 PostgreSQL 和 Elasticsearch。使用两者将让你接触到可以使用 SQL 查询的标准关系型数据库,以及允许你存储文档并使用它们自己的查询语言的 NoSQL 数据库。
第四章:第四章:与数据库一起工作
在上一章中,你学习了如何读取和写入文本文件。从数据湖中读取日志文件或其他文本文件并将它们移动到数据库或数据仓库是数据工程师的常见任务。在本章中,你将使用你在处理文本文件时获得的技能,并学习如何将数据移动到数据库中。本章还将教你如何从关系型和 NoSQL 数据库中提取数据。到本章结束时,你将具备使用 Python、NiFi 和 Airflow 与数据库一起工作的技能。你的大多数数据管道很可能以数据库结束,并且很可能以数据库开始。有了这些技能,你将能够构建可以提取和加载,以及开始和结束于关系型和 NoSQL 数据库的数据管道。
在本章中,我们将涵盖以下主要主题:
- 
在 Python 中插入和提取关系型数据 
- 
在 Python 中插入和提取 NoSQL 数据库数据 
- 
在 Airflow 中构建数据库管道 
- 
在 NiFi 中构建数据库管道 
在 Python 中插入和提取关系型数据
当你听到“数据库”这个词时,你可能想象的是一个关系型数据库——也就是说,由包含列和行的表组成,这些表之间存在关系;例如,一个包含库存、采购和客户信息的采购订单系统。关系型数据库已经存在了 40 多年,起源于 20 世纪 70 年代末由 E. F. Codd 开发的关系数据模型。有几个关系型数据库的供应商——包括 IBM、Oracle 和 Microsoft——但所有这些数据库都使用类似的 SQL 方言,它代表 结构化查询语言。在这本书中,你将使用一个流行的开源数据库——PostgreSQL。在下一节中,你将学习如何创建数据库和表。
创建 PostgreSQL 数据库和表
在第二章,构建我们的数据工程基础设施中,你使用 pgAdmin 4 在 PostgreSQL 中创建了一个数据库。该数据库命名为 dataengineering,你创建了一个名为 users 的表,其中包含名称、街道、城市、ZIP 和 ID 列。数据库在以下屏幕截图中显示:

图 4.1 – 数据工程数据库
如果你已经创建了数据库,你可以跳过这一部分,但如果你没有,这一部分将快速带你通过创建数据库的过程。
要使用 pgAdmin 4 在 PostgreSQL 中创建数据库,请按照以下步骤操作:
- 
浏览到 http://localhost/pgadmin4并使用你在第二章,构建我们的数据工程基础设施安装pgAdmin时创建的账户登录。
- 
在 浏览器 面板中展开服务器图标。右键单击 MyPostgreSQL 图标,然后选择 创建 | 数据库。 
- 
将数据库命名为 dataengineering。您可以保留用户为postgres。
- 
展开 dataengineering图标,然后展开 Schemas,然后 public,然后 Tables。右键单击 Tables,然后点击 创建 | 表。
- 
将表命名为 users。点击name:textb) id:integerc) street:textd) city:texte) zip:text
现在,您已在 PostgreSQL 中创建了一个数据库和一个表,可以使用 Python 加载数据。您将在下一节中填充该表。
将数据插入到 PostgreSQL 中
在 Python 中连接到数据库有几个库和方法 – pyodbc、sqlalchemy、psycopg2,以及使用 API 和请求。在这本书中,我们将使用 psycopg2 库来连接到 PostgreSQL,因为它专门用于连接 PostgreSQL。随着您技能的提高,您可能想了解像 SQLAlchemy 这样的工具。SQLAlchemy 是一个用于 Python 的工具包和对象关系映射器。它允许您以更 Pythonic 的方式执行查询 – 不使用 SQL – 并将 Python 类映射到数据库表。
安装 psycopg2
您可以通过运行以下命令来检查您是否已安装 psycopg2:
python3 -c "import psycopg2; print(psycopg2.__version__)"
上述命令以命令标志运行 python3。标志告诉 Python 将命令作为 Python 程序运行。引号中的文本导入 psycopg2 并打印版本。如果您收到错误,则表示未安装。您应该看到一个类似于 2.8.4 的版本,后面跟一些括号中的文本。该库应在安装 Apache Airflow 时已安装,因为您使用了 第二章,构建我们的数据工程基础设施 中提到的所有附加库。
如果它尚未安装,您可以使用以下命令添加它:
pip3 install psycopg2
使用 pip 需要存在额外的依赖项才能正常工作。如果您遇到问题,您还可以使用以下命令安装预编译的二进制版本:
pip3 install psycopg2-binary
这两种方法中的一种将安装库并使其准备好我们开始下一节。
使用 Python 连接到 PostgreSQL
要使用 psycopg2 连接到数据库,您需要创建一个连接、创建一个游标、执行一个命令并获取结果。无论您是查询还是插入数据,您都会采取相同的步骤。让我们按以下步骤进行:
- 
导入库并将其引用为 db:import psycopg2 as db
- 
创建一个包含主机、数据库、用户名和密码的连接字符串: conn_string="dbname='dataengineering' host='localhost' user='postgres' password='postgres'"
- 
通过将连接字符串传递给 connect()方法来创建连接对象:conn=db.connect(conn_string)
- 
接下来,从连接创建游标: cur=conn.cursor()
您现在已连接到数据库。从这里,您可以发出任何 SQL 命令。在下一节中,您将学习如何将数据插入到 PostgreSQL 中。
插入数据
现在你已经打开了连接,你可以使用 SQL 插入数据。要插入单个人员,你需要格式化一个 SQL insert 语句,如下所示:
query = "insert into users (id,name,street,city,zip) values({},'{}','{}','{}','{}')".format(1,'Big Bird','Sesame Street','Fakeville','12345')
要查看此查询的外观,你可以使用 mogrify() 方法。
什么是 mogrify?
根据 psycopg2 文档,mogrify 方法将在参数绑定后返回一个查询字符串。返回的字符串正是将要发送到运行 execute() 方法或类似方法的数据库中的字符串。简而言之,它返回格式化的查询。这很有用,因为你可以看到你发送到数据库的内容,因为你的 SQL 查询通常可能是错误的一个来源。
将你的查询传递给 mogrify 方法:
cur.mogrify(query)
上述代码将创建一个正确的 SQL insert 语句;然而,随着你的进步,你将在单个语句中添加多条记录。要这样做,你需要创建一个元组的元组。要创建相同的 SQL 语句,你可以使用以下代码:
query2 = "insert into users (id,name,street,city,zip) values(%s,%s,%s,%s,%s)"
data=(1,'Big Bird','Sesame Street','Fakeville','12345')
注意,在 query2 中,你不需要在作为参数传递的字符串周围添加引号,就像你在使用 {} 时在 query 中做的那样。使用前面的格式,psycopg2 将处理查询字符串中的类型映射。要查看执行查询时的查询外观,你可以使用 mogrify 并将数据与查询一起传递:
cur.mogrify(query2,data)
mogrify 对 query 和 query2 的结果应该是相同的。现在,你可以执行查询并将其添加到数据库中:
cur.execute(query2,data)
如果你回到 pgAdmin 4,右键点击 insert 语句,你需要通过以下代码提交事务使其永久化:
conn.commit()
现在,在 pgAdmin 4 中,你应该能够看到记录,如下面的截图所示:



记录现在已添加到数据库中,并在 pgAdmin 4 中可见。现在你已经输入了一条记录,下一节将展示如何输入多条记录。
插入多条记录
要插入多条记录,你可以遍历数据并使用前面章节中显示的相同代码,但这需要在数据库中为每条记录进行一次事务。更好的方法是使用单个事务并发送所有数据,让 psycopg2 处理批量插入。你可以通过使用 executemany 方法来完成此操作。以下代码将使用 Faker 创建记录,然后使用 executemany() 插入它们:
- 
导入所需的库: import psycopg2 as db from faker import Faker
- 
创建 faker对象和一个数组来存储所有数据。你将初始化一个变量i来存储 ID:fake=Faker() data=[] i=2
- 
现在,你可以查看、迭代,并将一个假元组追加到你在上一步创建的数组中。为下一条记录递增 i。记住,在前面章节中,你为Big Bird创建了一个 ID 为1的记录。这就是为什么在这个例子中你会从2开始。数据库表中的主键不能相同:for r in range(1000): data.append((i,fake.name(),fake.street_address(), fake.city(),fake.zipcode())) i+=1
- 
将数组转换为元组的元组: data_for_db=tuple(data)
- 
现在,你回到了 psycopg代码,它将类似于上一节的示例:conn_string="dbname='dataengineering' host='localhost' user='postgres' password='postgres'" conn=db.connect(conn_string) cur=conn.cursor() query = "insert into users (id,name,street,city,zip) values(%s,%s,%s,%s,%s)"
- 
你可以使用 data_for_db变量中的一个单个记录打印出代码将发送到数据库的内容:print(cur.mogrify(query,data_for_db[1]))
- 
最后,使用 executemany()而不是execute()来让库处理多个插入。然后,提交事务:cur.executemany(query,data_for_db) conn.commit()
现在,你可以在 pgAdmin 4 中查看 1,000 条记录。你的数据将类似于以下截图所示:

图 4.3 – 数据库中添加了 1,000 条记录
你的表现在应该有 1,001 条记录。现在,你可以在 PostgreSQL 中插入数据,下一节将展示如何在 Python 中查询它。
从 PostgreSQL 提取数据
使用 psycopg2 提取数据遵循与插入完全相同的程序,唯一的区别在于你将使用 select 语句而不是 insert。以下步骤展示了如何提取数据:
- 
导入库,然后设置你的连接和游标: import psycopg2 as db conn_string="dbname='dataengineering' host='localhost' user='postgres' password='postgres'" conn=db.connect(conn_string) cur=conn.cursor()
- 
现在,你可以执行一个查询。在这个例子中,你将选择 users表中的所有记录:query = "select * from users" cur.execute(query)
- 
现在,你有一个包含结果的可迭代对象。你可以像下面这样遍历游标: for record in cur: print(record)
- 
或者,你也可以使用 fetch中的任何一种方法:cur.fetchall() cur.fetchmany(howmany) # where howmany equals the number of records you want returned cur.fetchone()
- 
要获取单个记录,你可以将其分配给一个变量并查看它。请注意,即使你选择了一个记录,游标也会返回一个数组: data=cur.fetchone() print(data[0])
- 
无论你是获取一个还是多个,你都需要知道你在哪里以及有多少条记录。你可以使用以下代码获取查询的行数: cur.rowcount # 1001
- 
你可以使用 rownumber获取当前行号。如果你使用fetchone()然后再次调用rownumber,它应该随着你的新位置递增:cur.rownumber最后要提到的是,你也可以使用 copy_to()方法查询一个表并将其写入 CSV 文件。
- 
创建连接和游标: conn=db.connect(conn_string) cur=conn.cursor()
- 
打开一个文件以将表写入: f=open('fromdb.csv','w')
- 
然后,调用 copy_to并传递文件、表名和分隔符(如果你不包括它,将默认为制表符)。关闭文件,你将拥有所有行的 CSV 文件:cur.copy_to(f,'users',sep=',') f.close()
- 
你可以通过打开文件并打印内容来验证结果: f=open('fromdb.csv','r') f.read()
现在你已经知道如何使用 psycopg2 库读取和写入数据库,你也可以使用 DataFrame 读取和写入数据,你将在下一节中学习。
使用 DataFrame 提取数据
你也可以使用 pandas DataFrame 查询数据。为此,你需要使用 psycopg2 建立连接,然后你可以跳过游标直接进行查询。DataFrame 在过滤、分析和转换数据方面为你提供了很多功能。以下步骤将指导你使用 DataFrame:
- 
设置连接: import psycopg2 as db import pandas as pd conn_string="dbname='dataengineering' host='localhost' user='postgres' password='postgres'" conn=db.connect(conn_string)
- 
现在,你可以在 DataFrame 中使用 pandas的read_sql()方法执行查询。该方法接受一个查询和一个连接:df=pd.read_sql("select * from users", conn)
- 
结果是一个包含完整用户表的 DataFrame, df。现在您可以使用所有 DataFrame 工具来处理数据——例如,您可以使用以下代码将其导出为 JSON:df.to_json(orient='records')
现在您已经知道如何处理关系型数据库中的数据,是时候学习 NoSQL 数据库了。下一节将向您展示如何使用 Python 与 Elasticsearch 一起工作。
在 Python 中插入和提取 NoSQL 数据库数据
当您听到数据库这个词时,可能会想到关系型数据库,但还有几种其他类型的数据库,例如列式、键值和时间序列。在本节中,您将学习如何使用 Elasticsearch,它是一个 NoSQL 数据库。NoSQL 是一个通用术语,指的是不按行和列存储数据的数据库。NoSQL 数据库通常将数据存储为 JSON 文档,并使用除 SQL 之外的其他查询语言。下一节将教您如何将数据加载到 Elasticsearch 中。
安装 Elasticsearch
要安装elasticsearch库,您可以使用pip3,如下所示:
pip3 install elasticsearch
使用pip将安装最新版本,如果您按照第二章中“构建我们的数据工程基础设施”的说明安装了 Elasticsearch,那么这就是您所需要的。您可以为 Elasticsearch 版本 2、5、6 和 7 获取库。要验证安装并检查版本,您可以使用以下代码:
import elasticsearch
elasticsearch.__version__
上述代码应该打印出类似以下内容:
(7.6.0)
如果你有适合您 Elasticsearch 版本的正确版本,您就可以开始导入数据了。
将数据插入 Elasticsearch
在您能够查询 Elasticsearch 之前,您需要将一些数据加载到索引中。在上一个章节中,您使用了psycopg2库来访问 PostgreSQL。要访问 Elasticsearch,您将使用elasticsearch库。要加载数据,您需要创建连接,然后您可以向 Elasticsearch 发出命令。按照以下步骤向 Elasticsearch 添加记录:
- 
导入库。您还可以创建 Faker对象以生成随机数据:from elasticsearch import Elasticsearch from faker import Faker fake=Faker()
- 
创建到 Elasticsearch 的连接: es = Elasticsearch()
- 
上一段代码假设您的 Elasticsearch实例正在localhost上运行。如果不是,您可以指定 IP 地址,如下所示:es=Elasticsearch({'127.0.0.1'})
现在,您可以向您的Elasticsearch实例发出命令。index方法将允许您添加数据。该方法接受索引名称、文档类型和正文。正文是发送到 Elasticsearch 的内容,是一个 JSON 对象。以下代码创建一个 JSON 对象以添加到数据库,然后使用index将其发送到users索引(该索引将在索引操作期间自动创建):
doc={"name": fake.name(),"street": fake.street_address(), "city": fake.city(),"zip":fake.zipcode()}
res=es.index(index="users",doc_type="doc",body=doc)
print(res['result']) #created
上一段代码应在控制台打印出单词 created,这意味着文档已被添加。Elasticsearch 返回一个包含结果键的对象,这将告诉你操作是否失败或成功。在这种情况下,created 表示索引操作成功并在索引中创建了文档。就像本章前面提到的 PostgreSQL 示例一样,你可以迭代并运行 index 命令,或者你可以使用批量操作让库为你处理所有插入。
使用助手插入数据
使用 bulk 方法,你可以一次插入多个文档。这个过程与插入单个记录类似,只是你需要生成所有数据,然后插入。步骤如下:
- 
你需要导入 helpers库来访问bulk方法:from elasticsearch import helpers
- 
数据需要是一个 JSON 对象数组。在之前的例子中,你创建了一个具有属性的 JSON 对象。在这个例子中,对象需要一些额外的信息。你必须指定索引和类型。名称中的下划线用于 Elasticsearch 字段。 _source字段是你要将 JSON 文档插入数据库的地方。JSON 外面有一个for循环。这个循环创建了 999 个文档(你已经添加了一个,索引从 0 到 998):actions = [ { "_index": "users", "_type": "doc", "_source": { "name": fake.name(), "street": fake.street_address(), "city": fake.city(), "zip":fake.zipcode()} } for x in range(998) # or for i,r in df.iterrows() ]
- 
现在,你可以调用 bulk方法并传递elasticsearch实例和数据数组。你可以打印结果以检查是否成功:res = helpers.bulk(es, actions) print(res['result'])
现在,你应该在名为 users 的 Elasticsearch 索引中拥有 1,000 条记录。我们可以在 Kibana 中验证这一点。要将新索引添加到 Kibana,浏览到你的 Kibana 仪表板 http://localhost:5601。在工具栏左下角选择 管理,然后你可以通过点击蓝色 + 创建索引模式 按钮来创建索引模式,如以下截图所示:

图 4.4 – 创建索引模式
将 Elasticsearch 索引模式添加到 Kibana。在下一屏中,输入索引的名称 – users。Kibana 将开始模式匹配以查找索引。从下拉菜单中选择 users 索引并点击 users),如以下截图所示;你应该能看到你的文档:

图 4.5 – 发现标签页中的所有文档
现在你已经可以单独创建记录或使用 bulk 方法,下一节将教你如何查询你的数据。
查询 Elasticsearch
查询 Elasticsearch 的步骤与插入数据完全相同。唯一的区别是,你使用不同的方法 – search – 发送不同的请求体对象。让我们通过一个简单的查询来查看所有数据:
- 
导入库并创建你的 elasticsearch实例:from elasticsearch import Elasticsearch es = Elasticsearch()
- 
创建要发送到 Elasticsearch 的 JSON 对象。该对象是一个查询,使用 match_all搜索:doc={"query":{"match_all":{}}}
- 
使用 search方法将对象传递给 Elasticsearch。传递索引和返回大小。在这种情况下,你将只返回 10 条记录。最大返回大小是 10,000 个文档:res=es.search(index="users",body=doc,size=10)
- 
最后,你可以打印文档: print(res['hits']['hits'])或者,你可以迭代地只获取 _source:for doc in res['hits']['hits']: print(doc['_source'])
你可以将查询结果加载到pandas DataFrame 中——它是 JSON 格式,你已经在第三章中学习了如何读取 JSON,即读取和写入文件。要将结果加载到 DataFrame 中,从pandas的json库中导入json_normalize,并使用它(json_normalize)对 JSON 结果进行操作,如下面的代码所示:
from pandas.io.json import json_normalize
df=json_normalize(res['hits']['hits'])
现在,你将在 DataFrame 中看到搜索结果。在这个例子中,你只是抓取了所有记录,但除了match_all之外,还有其他查询可用。
使用match_all查询,我知道有一个名为Ronald Goodman的文档。你可以使用match查询在字段上进行查询:
doc={"query":{"match":{"name":"Ronald Goodman"}}}
res=es.search(index="users",body=doc,size=10)
print(res['hits']['hits'][0]['_source'])
你还可以使用 Lucene 语法进行查询。在 Lucene 中,你可以指定field:value。在执行此类搜索时,你不需要发送文档。你可以将q参数传递给search方法:
res=es.search(index="users",q="name:Ronald Goodman",size=10)
print(res['hits']['hits'][0]['_source'])
使用City字段,你可以搜索Jamesberg。它将返回两个记录:一个是Jamesberg,另一个是Lake Jamesberg。Elasticsearch 会将包含空格的字符串进行分词,将其拆分为多个字符串进行搜索:
# Get City Jamesberg - Returns Jamesberg and Lake Jamesberg
doc={"query":{"match":{"city":"Jamesberg"}}}
res=es.search(index="users",body=doc,size=10)
print(res['hits']['hits'])
以下代码块中的结果是两个记录:
[{'_index': 'users', '_type': 'doc', '_id': 'qDYoOHEBxMEH3Xr-PgMT', '_score': 6.929674, '_source': {'name': 'Tricia Mcmillan', 'street': '8077 Nancy #Mills Apt. 810', 'city': 'Jamesberg', 'zip': '63792'}}, {'_index': 'users', '_type': 'doc', '_id': 'pTYoOHEBxMEH3Xr-PgMT', '_score': 5.261652, '_source': {'name': 'Ryan Lowe', 'street': '740 Smith Pine Suite 065', 'city': 'Lake Jamesberg', 'zip': '38837'}}]
你可以使用布尔查询来指定多个搜索条件。例如,你可以在查询之前使用must、must not和should。使用布尔查询,你可以过滤掉Lake Jamesberg。使用must匹配城市为Jamesberg(这将返回两个记录),并添加 ZIP 的过滤器,你可以确保只返回 ZIP 为63792的Jamesberg。你还可以在Lake Jameson ZIP 上使用must not查询:
# Get Jamesberg and filter on zip so Lake Jamesberg is removed
doc={"query":{"bool":{"must":{"match":{"city":"Jamesberg"}},
"filter":{"term":{"zip":"63792"}}}}}
res=es.search(index="users",body=doc,size=10)
print(res['hits']['hits'])
现在,你只得到了你想要的单个记录:
[{'_index': 'users', '_type': 'doc', '_id': 'qDYoOHEBxMEH3Xr-
PgMT', '_score': 6.929674, '_source': {'name': 'Tricia 
Mcmillan', 'street': '8077 Nancy #Mills Apt. 810', 'city': 
'Jamesberg', 'zip': '63792'}}]
你的查询只返回了少量文档,但在实际生产中,你可能会遇到返回成千上万文档的大查询。下一节将展示如何处理所有这些数据。
使用滚动处理更大的结果
在第一个例子中,你为搜索使用了 10 的大小。你可以抓取所有 1,000 条记录,但当你有超过 10,000 条记录并且需要所有这些记录时,你该怎么办?Elasticsearch 有一个滚动方法,允许你迭代结果直到获取所有结果。要滚动数据,请遵循以下步骤:
- 
导入库并创建你的 Elasticsearch实例:from elasticsearch import Elasticsearch es = Elasticsearch()
- 
搜索您的数据。由于您没有超过 10,000 条记录,您将大小设置为 500。这意味着您将错过初始搜索中的 500 条记录。您将向搜索方法传递一个新参数 –scroll。此参数指定您希望结果可用多长时间。我使用 20 毫秒。调整此数字以确保您有足够的时间获取数据 – 它将取决于文档大小和网络速度:res = es.search( index = 'users', doc_type = 'doc', scroll = '20m', size = 500, body = {"query":{"match_all":{}}} )
- 
结果将包括 _scroll_id,您稍后需要将其传递给scroll方法。保存滚动 ID 和结果集的大小:sid = res['_scroll_id'] size = res['hits']['total']['value']
- 
要开始滚动,使用 while循环获取记录,直到大小为 0,这意味着没有更多数据。在循环内部,您将调用scroll方法并传递_scroll_id和滚动时间。这将从原始查询中获取更多结果:while (size > 0): res = es.scroll(scroll_id = sid, scroll = '20m')
- 
接下来,获取新的滚动 ID 和大小,以便在数据仍然存在时再次循环: sid = res['_scroll_id'] size = len(res['hits']['hits'])
- 
最后,您可以对滚动结果进行一些操作。在以下代码中,您将打印每条记录的源: for doc in res['hits']['hits']: print(doc['_source'])
现在您已经知道如何在 Elasticsearch 中创建文档以及如何查询它们,即使返回值超过 10,000 个最大值。您也可以使用关系数据库做同样的事情。现在是时候开始将这些技能用于构建数据管道了。接下来的两个部分将教会您如何使用 Apache Airflow 和 NiFi 在数据管道中使用数据库。
在 Apache Airflow 中构建数据管道
在上一章中,您使用 Bash 和 Python 操作符构建了您的第一个 Airflow 数据管道。这次,您将结合两个 Python 操作符从 PostgreSQL 中提取数据,将其保存为 CSV 文件,然后读取并写入 Elasticsearch 索引。完整的管道在以下屏幕截图中显示:

图 4.6 – Airflow DAG
前面的有向无环图(DAG)看起来非常简单;它只有两个任务,您可以将任务合并成一个函数。这不是一个好主意。在第二部分,将管道部署到生产中,您将学习如何修改您的数据管道以适应生产。生产管道的一个关键原则是每个任务应该是原子的;也就是说,每个任务应该能够独立存在。如果您有一个读取数据库并插入结果的单一函数,当它失败时,您必须追踪查询是否失败或插入是否失败。随着您的任务变得更加复杂,调试将需要更多的工作。下一节将引导您构建数据管道。
设置 Airflow 模板
每个 DAG 都将有一些标准的、模板化的代码,以便在 Airflow 中运行。你将始终导入所需的库,然后导入你任务所需的任何其他库。在下面的代码块中,你导入了操作符 DAG 和 Airflow 的时间库。对于你的任务,你导入了 pandas、psycopg2 和 elasticsearch 库:
import datetime as dt
from datetime import timedelta
from airflow import DAG
from airflow.operators.bash_operator import BashOperator
from airflow.operators.python_operator import PythonOperator
import pandas as pd
import psycopg2 as db
from elasticsearch import Elasticsearch
接下来,你将为你的 DAG 指定参数。记住,如果安排任务每天运行,开始时间应该比任务运行时间晚一天:
default_args = {
    'owner': 'paulcrickard',
    'start_date': dt.datetime(2020, 4, 2),
    'retries': 1,
    'retry_delay': dt.timedelta(minutes=5),
}
现在,你可以将参数传递给 DAG,给它命名,并设置运行间隔。你也将在这里定义你的操作符。在这个例子中,你将创建两个 Python 操作符——一个用于从 PostgreSQL 获取数据,另一个用于将数据插入 Elasticsearch。getData 任务将位于上游,而 insertData 任务位于下游,因此你将使用 >> 位移操作符来指定这一点:
with DAG('MyDBdag',
         default_args=default_args,
         schedule_interval=timedelta(minutes=5),      
                           # '0 * * * *',
         ) as dag:
    getData = PythonOperator(task_id='QueryPostgreSQL',
         python_callable=queryPostgresql)
    insertData = PythonOperator
    (task_id='InsertDataElasticsearch',
         python_callable=insertElasticsearch)
getData >> insertData
最后,你将定义任务。在前面的操作符中,你将它们命名为 queryPostgresql 和 insertElasticsearch。这些任务中的代码看起来非常熟悉;它与本章前几节中的代码几乎相同。
要查询 PostgreSQL,你将创建连接,使用 pandas 的 read_sql() 方法执行 sql 查询,然后使用 pandas 的 to_csv() 方法将数据写入磁盘:
def queryPostgresql():
    conn_string="dbname='dataengineering' host='localhost'
    user='postgres' password='postgres'"
    conn=db.connect(conn_string)
    df=pd.read_sql("select name,city from users",conn)
    df.to_csv('postgresqldata.csv')
    print("-------Data Saved------")
要将数据插入 Elasticsearch,你将创建连接到 localhost 的 Elasticsearch 对象。然后,从上一个任务中读取 CSV 到 DataFrame,遍历 DataFrame,将每一行转换为 JSON,并使用 index 方法插入数据:
def insertElasticsearch():
    es = Elasticsearch() 
    df=pd.read_csv('postgresqldata.csv')
    for i,r in df.iterrows():
        doc=r.to_json()
        res=es.index(index="frompostgresql",
                    doc_type="doc",body=doc)
        print(res)	
现在,你已经在 Airflow 中拥有了一个完整的数据管道。在下一节中,你将运行它并查看结果。
运行 DAG
要运行 DAG,你需要将你的代码复制到 $AIRFLOW_HOME/dags 文件夹。移动文件后,你可以运行以下命令:
airflow webserver
airflow scheduler
当这些命令完成后,浏览到 http://localhost:8080 以查看 Airflow GUI。选择 MyDBdag,然后选择 树视图。你可以安排 DAG 运行五次,并点击 Go。运行时,你应该在下面看到结果,如下面的截图所示:
![图 4.7 – 显示成功运行和排队运行的任务]

图 4.7 – 显示成功运行和排队运行的任务
要验证数据管道是否成功,你可以使用 Kibana 查看 Elasticsearch 中的数据。要查看结果,浏览到 Kibana 的 http://localhost:5601。你需要在 Kibana 中创建一个新的索引。你在这个章节的使用辅助工具插入数据部分执行了这个任务。但为了回顾,你将在 Kibana 的左侧工具栏底部选择管理,然后通过点击创建索引模式按钮创建索引模式。开始输入索引的名称,Kibana 将找到它,然后点击创建。然后,你可以转到工具栏上的发现选项卡并查看数据。你应该看到如下所示的记录:

图 4.8 – 显示 Elasticsearch 中记录的 Airflow 数据管道结果
你将看到包含仅名称和城市的文档,正如你在数据管道任务中指定的那样。需要注意的是,我们现在有超过 2,000 条记录。在 PostgreSQL 数据库中之前只有 1,000 条记录,那么发生了什么?数据管道运行了多次,每次都从 PostgreSQL 插入记录。数据管道的第二个原则是它们应该是幂等的。这意味着无论你运行多少次,结果都应该是相同的。在这种情况下,它们并不是。你将在第二部分,在生产中部署管道,在第七章,生产管道的特点中学习如何解决这个问题。现在,本章的下一节将教你如何在 Apache NiFi 中构建相同的数据管道。
使用 NiFi 处理器处理数据库
在前面的章节中,你学习了如何使用 Python 读取和写入 CSV 和 JSON 文件。读取文件是一项如此常见的任务,以至于像 NiFi 这样的工具有预构建的处理器来处理它。在本节中,你将构建与上一节相同的数据管道。在 NiFi 中,数据管道将看起来如下所示:

图 4.9 – 将数据从 PostgreSQL 移动到 Elasticsearch 的 NiFi 数据管道
数据管道比 Airflow 版本多一个任务,但除此之外,它应该看起来很直接。以下几节将指导你构建数据管道。
从 PostgreSQL 提取数据
在 NiFi 中处理关系数据库最常用的处理器是 ExecuteSQLRecord 处理器。拖动 ExecuteSQLRecord 处理器。一旦它被添加到画布上,你需要配置它。
配置 ExecuteSQLCommand 处理器
要配置处理器,你需要创建一个数据库连接池,如下面的截图所示:

图 4.10 – 创建数据库连接池服务
在选择 dataengineering 后,注意我没有将其命名为 PostgreSQL。随着你添加更多服务,你将为不同的数据库添加更多 PostgreSQL 连接。那时,记住哪个 PostgreSQL 数据库对应于哪个服务将变得很困难。
要配置服务,选择处理器配置中的箭头。服务的配置应如下截图所示:

图 4.11 – 配置数据库服务
配置需要你指定连接 URL,这是一个 Java 数据库连接字符串。该字符串指定 PostgreSQL。然后命名主机,localhost,和数据库名称,dataengineering。驱动类指定 postgresql 驱动。驱动程序的位置是你下载它的位置,在 第二章,构建我们的数据工程基础设施。它应该位于你的主目录中的 nifi 文件夹中,在名为 drivers 的子目录中。最后,你需要输入数据库的用户名和密码。
接下来,你需要创建一个记录写入服务。选择 创建新服务…,选择 JSONRecordSetWriter,然后点击箭头进行配置。有一个重要的配置设置你不能跳过 – 输出分组。你必须将此属性设置为 每对象一行。完成的配置将如下截图所示:

图 4.13 – JSONRecordSetWriter 配置
现在你已经为处理器设置了服务,你需要完成配置过程。你需要配置的最后参数是 SQL 选择查询。这是你可以指定运行数据库的 SQL 命令的地方。例如,你可以输入以下内容:
select name, city from users
这将抓取 PostgreSQL 数据库中的所有记录,但只包括 name 和 city 字段。你现在可以继续到管道中的下一个处理器。
配置 SplitText 处理器
现在你已经配置了 ExecuteSQLRecord 处理器,你将收到一个记录数组。要处理这些数据,你需要为每条记录有一个 flowfile。为此,你可以使用 SplitText 处理器。将其拖到画布上,通过双击处理器打开 属性 选项卡 – 或者右键单击并选择 属性。处理器的默认设置有效,但请确保 行分割计数 设置为 1,标题行计数 为 0 – 当数据来自 ExecuteSQLRecord 处理器时,你的数据没有标题,并且 删除尾随换行符 为 true。
这些设置将允许处理器将 flowfile 的每一行分割成一个新的 flowfile。因此,你的一个输入 flowfile 将从这个处理器输出 1,000 个 flowfile。
配置 PutElasticsearchHttp 处理器
数据管道的最后一步是将 flowfiles 插入 Elasticsearch。您可以使用 PutElasticsearchHttp 处理器来完成此操作。有四个不同的 PutElasticsearch 处理器。在这本书中,只有两个是相关的 – PutElasticsearchHttp 和 PutelasticsearchHttpRecord。这些处理器用于插入单个记录或使用批量 API。其他两个处理器 – Putelasticsearch 和 Putelasticsearch5 – 用于 Elasticsearch 的旧版本(2 和 5)。
要配置处理器,您必须指定 URL 和端口。在这个例子中,您将使用 http://localhost:9200。索引将是 fromnifi,但您可以将其命名为任何您想要的名称。类型是 doc,索引操作将是 index。在 第二部分,在生产中部署管道 中,您将使用其他索引操作,并且您将指定您插入的记录的 ID。
运行数据管道
现在您已经配置了所有处理器,您可以通过将箭头从 ExecuteSQLRecord 拖动到 SplitRecord 处理器来连接它们以实现成功。然后,将 SplitRecord 处理器连接到 PutElasticsearchHttp 处理器以进行拆分。最后,终止 PutElasticsearchHttp 处理器以完成所有关系。
运行每个处理器,或在 操作 面板中,选择 运行 以启动它们。您将看到第一个队列中的一个 flowfile,然后它将在第二个队列中拆分为 1,000 个 flowfiles。随着它们被插入 Elasticsearch,队列将以每批 100 个的方式清空。
为了验证结果,您可以使用 elasticsearch API,而不是 Kibana。在您的浏览器中,访问 http://localhost:9200/_cat/indices。这是查看您 Elasticsearch 数据库中索引的 REST 端点。您应该看到您的新索引 fromnifi 和文档总数,如下面的截图所示:

图 4.13 – 索引包含 PostgreSQL 的所有记录
索引中的文档数量将根据您是否让管道运行而变化。就像在 Airflow 示例中一样,这个管道不是幂等的。在运行过程中,它将不断将具有不同 ID 的相同记录添加到 Elasticsearch 中。这不是您在生产中想要的操作,我们将在 第二部分,在生产中部署管道 中修复这个问题。
摘要
在本章中,你学习了如何使用 Python 查询和将数据插入到关系型数据库和 NoSQL 数据库中。你还学习了如何使用 Airflow 和 NiFi 创建数据管道。数据库技能对于数据工程师来说是最重要的技能之一。几乎所有的数据管道都会以某种方式涉及到它们。本章中你学到的技能为你将需要学习的其他技能提供了基础——主要是 SQL。将强大的 SQL 技能与本章中学到的数据管道技能相结合,将使你能够完成你将遇到的大多数数据工程任务。
在示例中,数据管道不具有幂等性。每次运行时,你都会得到新的结果,以及你不想得到的结果。我们将在第二部分,将管道部署到生产中中解决这个问题。但在你到达那里之前,你需要学习如何处理常见的数据问题,以及如何丰富和转换你的数据。
下一章将教你如何使用 Python 在数据管道的提取和加载阶段之间处理你的数据。
第五章:第五章:清理、转换和丰富数据
在前两章中,你学习了如何构建能够从文件和数据库中读取和写入的数据管道。在许多情况下,仅这些技能就足以让你构建生产级的数据管道。例如,你将读取数据湖中的文件并将它们插入到数据库中。你现在有了完成这项任务的能力。然而,有时在提取数据之后但在加载之前,你可能需要对数据进行一些操作。你需要做的是清理数据。清理是一个模糊的术语。更具体地说,你需要检查数据的有效性,并回答以下问题:它是否完整?值是否在适当的范围内?列的类型是否正确?所有列是否都有用?
在本章中,你将学习执行探索性数据分析所需的基本技能。一旦你对数据有了了解,你将利用这些知识来解决你发现的一些常见数据问题——例如删除列和替换空值。你将学习pandas库中许多有用的方法。这些技能将使你能够快速执行探索性数据分析,并允许你在 Python 中清理数据。这些技能将成为 ETL 数据工程流程转换阶段的工具。
在本章中,我们将涵盖以下主要主题:
- 
在 Python 中执行探索性数据分析 
- 
使用 pandas 处理常见数据问题 
- 
使用 Airflow 清理数据 
在 Python 中执行探索性数据分析
在你清理数据之前,你需要了解你的数据看起来是什么样子。作为一个数据工程师,你并不是领域专家,也不是数据的最终用户,但你应该知道数据将用于什么,以及有效的数据应该是什么样子。例如,你不需要是人口统计学家就能知道一个年龄字段不应该为负数,并且超过 100 的值的频率应该很低。
下载数据
在本章中,你将使用来自阿尔伯克基市的真实电动滑板车数据。这些数据包含从 2019 年 5 月到 7 月 22 日使用电动滑板车进行的行程。你需要从github.com/PaulCrickard/escooter/blob/master/scooter.csv下载电动滑板车数据。该存储库还包含原始的 Excel 文件以及阿尔伯克基市提供的其他一些总结文件。
基本数据探索
在你清理数据之前,你必须知道你的数据看起来是什么样子。理解你的数据的过程被称为 探索性数据分析 (EDA)。你将查看数据的形状、行数和列数,以及列中的数据类型和值的范围。你可以执行更深入的分析,例如数据的分布或偏度,但在这个部分,你将学习如何快速理解你的数据,以便在下一部分中你可以清理它。
在前两个章节中,你学习了如何将文件和数据库导入 pandas DataFrame。这部分知识将在本节中扩展,因为 DataFrame 将是用于 EDA 的工具。
首先,你需要导入 pandas 并读取 .csv 文件:
import pandas as pd
df=pd.read_csv('scooter.csv')
在 DataFrame 中有了数据后,你现在可以探索它,然后分析它。
探索数据
现在,你可以开始查看数据了。你可能会做的第一件事就是打印它。但在你做到这一点之前,看看列和数据类型,使用 columns 和 dtypes:
df.columns
Index(['month', 'trip_id', 'region_id', 'vehicle_id', 
'started_at', 'ended_at', 'DURATION', 
'start_location_name', 'end_location_name', 
       'user_id', 'trip_ledger_id'],
      dtype='object')
df.dtypes
month                  object
trip_id                 int64
region_id               int64
vehicle_id              int64
started_at             object
ended_at               object
DURATION               object
start_location_name    object
end_location_name      object
user_id                 int64
trip_ledger_id          int64
你会看到你有 11 列,其中 5 列是整数(所有名称中包含 ID 的列)其余的是对象。对象是 DataFrame 在存在混合类型时用作 dtype 的内容。此外,DURATION 应该很突出,因为它是所有大写字母的唯一列名。在下一节中,你将修复常见的错误,例如列的大小写不统一(全部小写或大写)以及将 dtypes 对象转换为适当的类型,例如 strings 用于文本数据,datetimes 用于日期和时间。
现在你已经知道了列和类型,让我们来看看数据。你可以使用 head() 打印出前五条记录:
df.head()
  month  trip_id  ...   user_id  trip_ledger_id
0   May  1613335  ...   8417864         1488546
1   May  1613639  ...   8417864         1488838
2   May  1613708  ...   8417864         1488851
3   May  1613867  ...   8417864         1489064
4   May  1636714  ...  35436274         1511212
[5 rows x 11 columns]
head() 的对立面是 tail()。这两个方法默认显示 5 行。然而,你可以传递一个整数作为参数来指定要显示的行数。例如,你可以传递 head(10) 来查看前 10 行。
注意在 head() 和 tail() 输出中,第三列是 ...,然后是此之后的两个更多列。显示正在裁剪中间的列。如果你要打印整个 DataFrame,行也会发生相同的事情。要显示所有列,你可以使用 set_options 方法更改要显示的列数:
pd.set_option('display.max_columns', 500)
现在,当你使用 head() 时,你会看到所有列。然而,根据你显示器的宽度,输出可能会被换行以适应。
head 和 tail 方法显示所有列,但如果你只对单个列感兴趣,你可以像在 Python 字典中一样指定它。以下代码打印了 DURATION 列:
df['DURATION']
0        0:07:03
1        0:04:57
2        0:01:14
3        0:06:58
4        0:03:06
          ...   
34221    0:14:00
34222    0:08:00
34223    1:53:00
34224    0:12:00
34225    1:51:00
再次注意,输出被 ... 截断,但这次是针对行的。结果是 head() 和 tail() 的组合。你可以使用 display_max_rows 选项来改变它,就像你之前改变列一样,但在这个探索中,这是不必要的。
就像你可以显示单个列一样,你可以使用双 [] 显示列的列表,如下面的代码块所示:
df[['trip_id','DURATION','start_location_name']]     
       trip_id DURATION     start_location_name
0      1613335  0:07:03       1901 Roma Ave NE, Albuquerque, NM 
                              87106, USA
1      1613639  0:04:57  1 Domenici Center en Domenici Center, 
                           Albuquer...
2      1613708  0:01:14  1 Domenici Center en Domenici Center, 
                           Albuquer...
3      1613867  0:06:58  Rotunda at Science & Technology Park, 
                         801 Univ...
4      1636714  0:03:06          401 2nd St NW, Albuquerque, NM
                                 87102, USA
...        ...      ...                                     ...
34221  2482235  0:14:00     Central @ Broadway, Albuquerque, NM 
                            87102, USA
34222  2482254  0:08:00     224 Central Ave SW, Albuquerque, NM 
                            87102, USA
34223  2482257  1:53:00     105 Stanford Dr SE, Albuquerque, NM 
                            87106, USA
34224  2482275  0:12:00   100 Broadway Blvd SE, Albuquerque, NM 
                          87102, USA
34225  2482335  1:51:00     105 Stanford Dr SE, Albuquerque, NM 
                            87106, USA
你也可以使用 sample() 从你的数据中抽取样本。样本方法允许你指定你想要抽取多少行。结果如下面的代码块所示:
df.sample(5)
     month  trip_id  ...   user_id  trip_ledger_id
4974   June  1753394  ...  35569540         1624088
18390  June  1992655  ...  42142022         1857395
3132    May  1717574  ...  37145791         1589327
1144    May  1680066  ...  36146147         1553169
21761  June  2066449  ...  42297442         1929987
注意,行的索引不是递增的,而是跳跃式的。它应该是这样的,因为它是一个样本。
你也可以切片数据。切片的格式为 [start:end],其中空白表示第一行或最后一行,具体取决于哪个位置是空的。要切片前 10 行,你可以使用以下表示法:
df[:10]  
     month  trip_id  ...   user_id  trip_ledger_id
0      May  1613335  ...   8417864         1488546
1      May  1613639  ...   8417864         1488838
2      May  1613708  ...   8417864         1488851
3      May  1613867  ...   8417864         1489064
4      May  1636714  ...  35436274         1511212
5      May  1636780  ...  34352757         1511371
6      May  1636856  ...  35466666         1511483
7      May  1636912  ...  34352757         1511390
8      May  1637035  ...  35466666         1511516
9      May  1637036  ...  34352757         1511666
同样,要获取从第 10 行到结束(34,225)的行,你可以使用以下表示法:
df[10:] 
你也可以从第三行开始切片,到第九行之前结束,如下面的代码块所示:
df[3:9]
  month  trip_id  ...   user_id  trip_ledger_id
3   May  1613867  ...   8417864         1489064
4   May  1636714  ...  35436274         1511212
5   May  1636780  ...  34352757         1511371
6   May  1636856  ...  35466666         1511483
7   May  1636912  ...  34352757         1511390
8   May  1637035  ...  35466666         1511516
有时候,你知道你想要的确切行,而不是切片,你可以使用 loc() 来选择它。loc 方法接受索引名称,在这个例子中是一个整数。下面的代码和输出显示了使用 loc() 选择的单行:
df.loc[34221]
month                                                      July
trip_id                                                 2482235
region_id                                                   202
vehicle_id                                              2893981
started_at                                      7/21/2019 23:51
ended_at                                         7/22/2019 0:05
DURATION                                                0:14:00
start_location_name  Central @ Broadway, Albuquerque, NM 87102,
                                                            USA
end_location_name    1418 4th St NW, Albuquerque, NM 87102, USA
user_id                                                42559731
trip_ledger_id                                          2340035
使用 at(),与切片示例中的位置一样,以及列名,你可以选择单个值。例如,这可以用来知道第二行的行程时长:
df.at[2,'DURATION']
'0:01:14'
切片和使用 loc() 和 at() 是基于位置拉取数据,但你也可以使用 DataFrames 根据某些条件选择行。使用 where 方法,你可以传递一个条件,如下面的代码块所示:
user=df.where(df['user_id']==8417864)
user
      month    trip_id  ...    user_id  trip_ledger_id
0       May  1613335.0  ...  8417864.0       1488546.0
1       May  1613639.0  ...  8417864.0       1488838.0
2       May  1613708.0  ...  8417864.0       1488851.0
3       May  1613867.0  ...  8417864.0       1489064.0
4       NaN        NaN  ...        NaN             NaN
...     ...        ...  ...        ...             ...
34221   NaN        NaN  ...        NaN             NaN
34222   NaN        NaN  ...        NaN             NaN
34223   NaN        NaN  ...        NaN             NaN
34224   NaN        NaN  ...        NaN             NaN
34225   NaN        NaN  ...        NaN             NaN
上述代码和结果显示了 where 方法在用户 ID 等于 8417864 的条件下的结果。结果将不符合标准的数据替换为 NaN。这将在下一节中介绍。
你可以使用与前面示例类似的结果,只是使用不同的表示法,并且这种方法将不包括 NaN 行。你可以将条件传递到 DataFrame 中,就像你传递列名一样。以下示例展示了如何操作:
df[(df['user_id']==8417864)]
上述代码的结果与 where() 示例相同,但没有 NaN 行,因此 DataFrame 将只有四行。
使用这两种表示法,你可以组合条件语句。通过使用相同的用户 ID 条件,你可以添加行程 ID 条件。以下示例展示了如何操作:
one=df['user_id']==8417864
two=df['trip_ledger_id']==1488838
df.where(one & two)
      month    trip_id  ...    user_id  trip_ledger_id
0       NaN        NaN  ...        NaN             NaN
1       May  1613639.0  ...  8417864.0       1488838.0
2       NaN        NaN  ...        NaN             NaN
3       NaN        NaN  ...        NaN             NaN
4       NaN        NaN  ...        NaN             NaN
使用第二种表示法,输出如下:
df[(one)&(two)]
  month  trip_id  ...  user_id  trip_ledger_id
1   May  1613639  ...  8417864         1488838
在前面的例子中,条件被分配给一个变量,并在 where 和二级表示法中结合,生成了预期的结果。
分析数据
现在你已经看到了数据,你可以开始分析它。通过使用 describe 方法,你可以看到一系列与你的数据相关的统计数据。在统计学中,有一组被称为五数摘要的统计数据,describe() 是其变体:
df.describe()
           trip_id  region_id    vehicle_id       user_id  trip_ledger_id
count  3.422600e+04    34226.0  3.422600e+04  3.422600e+04    3.422600e+04
mean   2.004438e+06      202.0  5.589507e+06  3.875420e+07    1.869549e+06
std    2.300476e+05        0.0  2.627164e+06  4.275441e+06    2.252639e+05
min    1.613335e+06      202.0  1.034847e+06  1.080200e+04    1.488546e+06
25%    1.813521e+06      202.0  3.260435e+06  3.665710e+07    1.683023e+06
50%    1.962520e+06      202.0  5.617097e+06  3.880750e+07    1.827796e+06
75%    2.182324e+06      202.0  8.012871e+06  4.222774e+07    2.042524e+06
max    2.482335e+06      202.0  9.984848e+06  4.258732e+07    2.342161e+06
describe 方法在没有数值数据的情况下并不很有用。例如,如果你在查看年龄,它会快速显示年龄分布,你能够快速看到错误,如负年龄或超过 100 岁的年龄过多。
在单个列上使用 describe() 有时更有帮助。让我们尝试查看 start_location_name 列。代码和结果如下所示:
df['start_location_name'].describe()
count                                               34220
unique                                               2972
top       1898 Mountain Rd NW, Albuquerque, NM 87104, USA
freq                                                 1210
数据不是数值型的,所以我们得到一组不同的统计数据,但这些提供了一些见解。在 34220 个起始位置中,实际上有 2972 个独特的位置。最热门的位置(1898 Mountain Rd NW)占 1210 个行程的起始位置。稍后,你将对这些数据进行地理编码——为地址添加坐标——知道唯一值意味着你只需要地理编码这 2,972 个,而不是全部 34,220 个。
另一种允许你查看数据细节的方法是 value_counts。value_counts 方法将为你提供所有唯一值的值和计数。我们需要将其调用到一个单列上,如下面的代码片段所示:
df['DURATION'].value_counts()
0:04:00    825
0:03:00    807
0:05:00    728
0:06:00    649
0:07:00    627
从这个方法中,你可以看到 0:04:00 是频率最高的,频率为 825 —— 你可以用 describe() 找到这个信息——但你也可以看到其他所有值的频率。要查看频率作为百分比,你可以传递 normalize 参数(默认值为 False):
df['DURATION'].value_counts(normalize=True)
0:04:00    0.025847
0:03:00    0.025284
0:05:00    0.022808
0:06:00    0.020333
0:07:00    0.019644
你会注意到没有任何单个值占用了显著的比例。
你还可以传递 dropna 参数。默认情况下,value_counts() 将其设置为 True,你将看不到它们。将其设置为 False,你可以看到 end_location_name 缺少 2070 个条目:
df['end_location_name'].value_counts(dropna=False)
NaN                                                2070
1898 Mountain Rd NW, Albuquerque, NM 87104, USA     802
Central @ Tingley, Albuquerque, NM 87104, USA       622
330 Tijeras Ave NW, Albuquerque, NM 87102, USA      529
2550 Central Ave NE, Albuquerque, NM 87106, USA     478
... 
507 Bridge Blvd SW, Albuquerque, NM 87102, USA        1
820 2nd St NW, Albuquerque, NM 87102, USA             1
909 Alcalde Pl SW, Albuquerque, NM 87104, USA         1
817 Slate Ave NW, Albuquerque, NM 87102, USA          1
要找出你列中缺失值的数量,最好的方法是使用 isnull() 方法。以下代码将 isnull() 与 sum() 结合起来以获取计数:
df.isnull().sum()
month                     0
trip_id                   0
region_id                 0
vehicle_id                0
started_at                0
ended_at                  0
DURATION               2308
start_location_name       6
end_location_name      2070
user_id                   0
trip_ledger_id            0
value_counts() 的另一个参数是 bins。滑板车数据集没有很好的列来表示这个参数,但使用数值列,你会得到以下结果:
df['trip_id'].value_counts(bins=10)
(1787135.0, 1874035.0]      5561
(1700235.0, 1787135.0]      4900
(1874035.0, 1960935.0]      4316
(1960935.0, 2047835.0]      3922
(2047835.0, 2134735.0]      3296
(2221635.0, 2308535.0]      2876
(2308535.0, 2395435.0]      2515
(2134735.0, 2221635.0]      2490
(2395435.0, 2482335.0]      2228
(1612465.999, 1700235.0]    2122
这些结果相当没有意义,但如果它用于像 age 这样的列,它将非常有用,因为你能够快速创建年龄组并了解分布情况。
现在你已经探索并分析了数据,你应该了解数据是什么以及存在的问题——例如,空值、不正确的 dtypes、组合字段等。有了这些知识,你就可以开始清理数据。下一节将指导你如何修复常见的数据问题。
使用 pandas 处理常见数据问题
你的数据可能感觉特别,它是独特的,你已经创建了世界上最好的系统来收集它,并且你已经尽了一切努力确保它是干净和准确的。恭喜!但你的数据几乎肯定会有一些问题,这些问题并不特殊或独特,可能是你的系统或数据输入的结果。电动滑板车数据集是通过 GPS 收集的,几乎没有人工输入,但仍有结束位置缺失。一辆滑板车被租用、骑行并停止,但数据却不知道它停在哪里?这似乎很奇怪,但我们就在这里。在本节中,你将学习如何使用电动滑板车数据集来处理常见的数据问题。
删除行和列
在你修改数据中的任何字段之前,你应该首先决定你是否将使用所有字段。查看电动滑板车数据,有一个名为region_id的字段。这个字段是供应商用来标记阿尔伯克基的代码。由于我们只使用阿尔伯克基的数据,我们不需要这个字段,因为它对数据没有任何贡献。
你可以使用drop方法来删除列。该方法将允许你指定是删除行还是列。默认情况下是行,所以我们将指定columns,如下面的代码块所示:
df.drop(columns=['region_id'], inplace=True)
指定要删除的列时,你还需要添加inplace以使其修改原始 DataFrame。
要删除一行,你只需要指定index而不是columns。要删除索引为34225的行,你需要使用以下代码:
df.drop(index=[34225],inplace=True)
前面的代码在你想要删除整个列或行时有效,但如果你想要根据条件删除它们怎么办?
你可能首先想要考虑的第一个条件是存在 null 值。如果你缺失数据,列和行可能没有用,或者可能扭曲数据。为了处理这种情况,你可以使用dropna()。
通过使用dropna(),你可以将axis、how、thresh、subset和inplace作为参数传递:
- 
axis指定具有索引或列的行或列(0 或 1)。默认为行。
- 
how指定如果所有值都是 null 或任何值是 null(全部或任何)时,是删除行还是列。默认为any。
- 
thresh允许你比指定必须存在的 null 值的整数数量有更多的控制。
- 
subset允许你指定要搜索的行或列的列表。
- 
inplace允许你修改现有的 DataFrame。默认为False。
观察电动滑板车数据,有六行没有起始位置名称:
df['start_location_name'][(df['start_location_name'].isnull())]
26042    NaN
26044    NaN
26046    NaN
26048    NaN
26051    NaN
26053    NaN
要删除这些行,你可以使用dropna在axis=0上,how=any,这是默认设置。然而,这将会删除存在其他 null 值的行,例如end_location_name。因此,你需要指定列名作为子集,如下面的代码块所示:
df.dropna(subset=['start_location_name'],inplace=True)
然后,当你像前面代码块中那样在start_location_name字段中选择 null 值时,你将得到一个空序列:
df['start_location_name'][(df['start_location_name'].isnull())]
Series([], Name: start_location_name, dtype: object)
基于缺失值删除整个列可能只有在一定百分比的行是空的情况下才有意义。例如,如果超过 25%的行是空的,您可能想删除它。您可以通过使用以下代码为thresh参数指定这个阈值:
thresh=int(len(df)*.25)
在展示更高级的删除行的过滤器之前,您可能不想删除空值。您可能想用值填充它们。您可以使用fillna()来填充空列或行:
df.fillna(value='00:00:00',axis='columns')
9201     00:00:00
9207     00:00:00
9213     00:00:00
9219     00:00:00
9225     00:00:00
如果您想使用fillna()但根据列使用不同的值怎么办?您可能不想每次都指定一个列并多次运行fillna()。您可以指定一个映射到 DataFrame 的对象,并将其作为value参数传递。
在下面的代码中,我们将复制起始位置和结束位置都为空的行。然后,我们将创建一个value对象,将街道名称分配给start_location_name字段,并将不同的街道地址分配给end_location_name字段。使用fillna(),我们将值传递给value参数,然后通过显示变化在 DataFrame 中打印这两个列:
startstop=df[(df['start_location_name'].isnull())&(df['end_location_name'].isnull())]
value={'start_location_name':'Start St.','end_location_name':'Stop St.'}
startstop.fillna(value=value)
startstop[['start_location_name','end_location_name']]
      start_location_name end_location_name
26042           Start St.          Stop St.
26044           Start St.          Stop St.
26046           Start St.          Stop St.
26048           Start St.          Stop St.
26051           Start St.          Stop St.
26053           Start St.          Stop St.
您可以根据更高级的过滤器删除行;例如,如果您想删除所有月份为五月的行怎么办?您可以遍历 DataFrame 并检查月份,如果是五月就删除它。或者,一个更好的方法是对行进行过滤,然后将索引传递给drop方法。您可以对 DataFrame 进行过滤并将其传递给一个新的 DataFrame,如下面的代码块所示:
may=df[(df['month']=='May')]
may
     month  trip_id  ...   user_id trip_ledger_id
0      May  1613335  ...   8417864        1488546
1      May  1613639  ...   8417864        1488838
2      May  1613708  ...   8417864        1488851
3      May  1613867  ...   8417864        1489064
4      May  1636714  ...  35436274        1511212
...    ...      ...  ...       ...            ...
4220   May  1737356  ...  35714580        1608429
4221   May  1737376  ...  37503537        1608261
4222   May  1737386  ...  37485128        1608314
4223   May  1737391  ...  37504521        1608337
4224   May  1737395  ...  37497528        1608342
然后,您可以在原始 DataFrame 上使用drop()并传递may DataFrame 中行的索引,如下所示:
df.drop(index=may.index,inplace=True)
现在,如果您查看原始 DataFrame 中的月份,您会看到五月是缺失的:
df['month'].value_counts()
June    20259
July     9742
现在您已经删除了您不需要的行和列,或者由于缺失数据而无法使用的行和列,是时候对它们进行格式化了。
创建和修改列
在前面的部分中,最引人注目的是有一个单独的列,即持续时间,它全部是大写字母。大写是一个常见问题。您经常会发现全大写的列,或者首字母大写的列——每个单词的首字母都大写——如果是一个程序员编写的,您可能会发现驼峰式命名——首字母小写,下一个单词的首字母大写,没有空格,就像camelCase。下面的代码将所有列转换为小写:
df.columns=[x.lower() for x in df.columns] print(df.columns)
Index(['month', 'trip_id', 'region_id', 'vehicle_id', 'started_at', 'ended_at','duration', 'start_location_name', 'end_location_name', 'user_id', 'trip_ledger_id'], dtype='object')
上述代码是for循环的简化版本。循环中的内容在for循环之前发生。上述代码表示对于df.columns中的每个项目,将其转换为小写,并将其赋值回df.columns。您也可以使用capitalize(),它是标题大小写,或者使用upper(),如下所示:
df.columns=[x.upper() for x in df.columns] print(df.columns)
Index(['MONTH', 'TRIP_ID', 'REGION_ID', 'VEHICLE_ID', 'STARTED_AT', 'ENDED_AT', 'DURATION', 'START_LOCATION_NAME', 'END_LOCATION_NAME', 'USER_ID', 'TRIP_LEDGER_ID'], dtype='object')
您也可以使用rename方法将DURATION字段转换为小写,如下所示:
df.rename(columns={'DURATION':'duration'},inplace=True)
你会注意到一个设置为 True 的 inplace 参数。当你使用 psycopg2 修改数据库时,你需要使用 conn.commit() 来使其永久,你也需要对 DataFrame 做同样的事情。当你修改 DataFrame 时,结果会被返回。你可以将那个新的 DataFrame(结果)存储在一个变量中,而原始 DataFrame 保持不变。如果你想修改原始 DataFrame 而不是将其分配给另一个变量,你必须使用 inplace 参数。
rename 方法适用于修复列名的大小写,但不是最佳选择。它更适合用于实际更改多个列名。你可以传递一个具有多个列名重映射的对象。例如,你可以使用 rename 来移除 region_id 中的下划线。在以下代码片段中,我们将 DURATION 列转换为小写并移除 region_id 中的下划线:
df.rename(columns={'DURATION':'duration','region_id':'region'},inplace=True)
了解完成同一任务的不同方法是有好处的,你可以决定哪种方法对你的用例最有意义。现在你已经对列名应用了更改,你也可以将这些函数应用到列的值上。你将不再使用 df.columns,而是指定要修改的列,然后决定是否将其设置为 upper()、lower() 或 capitalize()。在以下代码片段中,我们将 month 列全部转换为大写:
df['month']=df['month'].str.upper()
df['month'].head()
0    MAY
1    MAY
2    MAY
3    MAY
4    MAY
列名或值的大小写可能并不重要。然而,保持一致性是最好的。在电动滑板车数据的情况下,如果有一个列名全部大写,而其余的都是小写,可能会造成混淆。想象一下数据科学家从多个数据库或你的数据仓库查询数据,并需要记住所有他们的查询都需要考虑到 duration 字段是大写的,当他们忘记时,他们的代码就会失败。
你可以通过使用 df['new column name']=value 格式创建列来向 DataFrame 添加数据。
上述格式将创建一个新列并将值分配给每一行。你可以遍历 DataFrame 并根据条件添加值,例如:
for i,r in df.head().iterrows():
    if r['trip_id']==1613335:
        df.at[i,'new_column']='Yes'
    else:
        df.at[i,'new_column']='No'
df[['trip_id','new_column']].head()
   trip_id new_column
0  1613335        Yes
1  1613639         No
2  1613708         No
3  1613867         No
4  1636714         No
通过 DataFrame 迭代操作可行,但可能会非常慢。为了更高效地完成与前面示例相同的事情,你可以使用 loc() 并传递条件、列名和值。以下示例展示了代码和结果:
df.loc[df['trip_id']==1613335,'new_column']='1613335'
df[['trip_id','new_column']].head()
   trip_id new_column
0  1613335    1613335
1  1613639         No
2  1613708         No
3  1613867         No
4  1636714         No
创建列的另一种方法是分割数据,然后将它插入到 DataFrame 中。你可以在一个序列上使用 str.split() 来分割文本,使用任何分隔符,或者 (expand=True)。如果你不将 expand 设置为 True,你将得到一个列表在列中,这是默认的。此外,如果你没有指定分隔符,将使用空白字符。默认值如下所示:
d['started_ad=df[['trip_id','started_at']].head()
d['started_at'].str.split()
d
   trip_id          started_at
0  1613335  [5/21/2019, 18:33]
1  1613639  [5/21/2019, 19:07]
2  1613708  [5/21/2019, 19:13]
3  1613867  [5/21/2019, 19:29]
4  1636714  [5/24/2019, 13:38]
你可以扩展数据并将其传递给新变量。然后你可以将列分配到原始 DataFrame 的列中。例如,如果你想创建 date 和 time 列,你可以这样做:
new=d['started_at'].str.split(expand=True)
new
           0      1
0  5/21/2019  18:33
1  5/21/2019  19:07
2  5/21/2019  19:13
3  5/21/2019  19:29
4  5/24/2019  13:38
d['date']=new[0]
d['time']=new[1]
d
   trip_id       started_at       date   time
0  1613335  5/21/2019 18:33  5/21/2019  18:33
1  1613639  5/21/2019 19:07  5/21/2019  19:07
2  1613708  5/21/2019 19:13  5/21/2019  19:13
3  1613867  5/21/2019 19:29  5/21/2019  19:29
4  1636714  5/24/2019 13:38  5/24/2019  13:38
如果你还记得在探索数据部分,数据有几个dtypes对象。started_at列是一个对象,从外观上看,很明显它是一个datetime对象。如果你尝试使用日期对started_at字段进行过滤,它将返回所有行,如下所示:
when = '2019-05-23'
x=df[(df['started_at']>when)]
len(x)
34226
整个 DataFrame 的长度是34226,所以过滤返回了所有行。这并不是我们想要的。使用to_datetime(),你可以指定列和格式。你可以将结果分配给同一个列或指定一个新的列。在以下示例中,started_at列被替换为新的datetime数据类型:
d['started_at']=pd.to_datetime(df['started_at'],format='%m/%d/%Y %H:%M')
d.dtypes
trip_id                int64
started_at    datetime64[ns]
现在,started_at列是datetime数据类型,而不是对象。你现在可以使用日期运行查询,就像我们之前在full DataFrame 上尝试的那样,但失败了:
when = '2019-05-23'
d[(d['started_at']>when)]
   trip_id          started_at
4  1636714 2019-05-24 13:38:00
其余的行都是在 2019-05-21,所以我们得到了预期的结果。
现在你已经可以添加和删除行和列,替换空值,并创建列,在下一节中,你将学习如何通过外部来源丰富你的数据。
数据丰富
电动滑板车数据是地理数据——它包含位置——但它缺少坐标。如果你想将数据映射到地图上,或对此数据进行空间查询,你需要坐标。你可以通过地理编码位置来获取坐标。幸运的是,阿尔伯克基市有一个我们可以使用的公共地理编码器。
对于这个例子,我们将取数据的一个子集。我们将使用最频繁的前五个起始位置。然后我们将使用以下代码将它们放入一个 DataFrame 中:
new=pd.DataFrame(df['start_location_name'].value_counts().head())
new.reset_index(inplace=True)
new.columns=['address','count']
new
                            address  			count
0  1898 Mountain Rd NW, Albuquerque, NM 87104, USA   1210
1    Central @ Tingley, Albuquerque, NM 87104, USA    920
2  2550 Central Ave NE, Albuquerque, NM 87106, USA    848
3  2901 Central Ave NE, Albuquerque, NM 87106, USA    734
4   330 Tijeras Ave NW, Albuquerque, NM 87102, USA    671
address字段包含的信息比我们用于地理编码所需的多。我们只需要街道地址。你也会注意到第二个记录是一个交叉路口——Central @ Tingley。地理编码器希望街道之间有单词和。让我们清理数据并将其放入自己的列中:
n=new['address'].str.split(pat=',',n=1,expand=True)
replaced=n[0].str.replace("@","and")
new['street']=n[0]
new['street']=replaced
new
                                           address  count               street
0  1898 Mountain Rd NW, Albuquerque, NM 87104, USA   1210  1898 Mountain Rd NW
1    Central @ Tingley, Albuquerque, NM 87104, USA    920  Central and Tingley
2  2550 Central Ave NE, Albuquerque, NM 87106, USA    848  2550 Central Ave NE
3  2901 Central Ave NE, Albuquerque, NM 87106, USA    734  2901 Central Ave NE
4   330 Tijeras Ave NW, Albuquerque, NM 87102, USA    671   330 Tijeras Ave NW
现在,你可以遍历 DataFrame 并对街道字段进行地理编码。在这个部分,你将使用另一个 CSV 文件并将其与 DataFrame 连接。
你可以通过结合其他数据源来丰富数据。就像你可以在数据库中的两个表中连接数据一样,你可以在 pandas DataFrame 中做同样的事情。你可以从书籍的 GitHub 仓库中下载geocodedstreet.csv文件。使用pd.read_csv()加载数据,你将有一个包含street列以及x和y坐标列的 DataFrame。结果如下所示:
geo=pd.read_csv('geocodedstreet.csv')
geo
                street           x          y
0  1898 Mountain Rd NW -106.667146  35.098104
1  Central and Tingley -106.679271  35.091205
2  2550 Central Ave NE -106.617420  35.080646
3  2901 Central Ave NE -106.612180  35.081120
4   330 Tijeras Ave NW -106.390355  35.078958
5       nothing street -106.000000  35.000000
要用这些新数据丰富原始 DataFrame,你可以连接或合并 DataFrame。使用连接,你可以从一个 DataFrame 开始,然后添加另一个作为参数。你可以通过left、right或inner来传递如何连接,就像在 SQL 中一样。你可以添加left和right后缀,这样重叠的列就有一种确定它们来源的方法。以下示例中,我们连接了两个 DataFrame:
joined=new.join(other=geo,how='left',lsuffix='_new',rsuffix='_geo')
joined[['street_new','street_geo','x','y']]
            street_new           street_geo           x          y
0  1898 Mountain Rd NW  1898 Mountain Rd NW -106.667146  35.098104
1  Central and Tingley  Central and Tingley -106.679271  35.091205
2  2550 Central Ave NE  2550 Central Ave NE -106.617420  35.080646
3  2901 Central Ave NE  2901 Central Ave NE -106.612180  35.081120
4   330 Tijeras Ave NW   330 Tijeras Ave NW -106.390355  35.078958
street列被重复,并带有left和right后缀。这可以工作,但不是必需的,我们最终会删除一个列并将剩余的列重命名,这只是一些额外的工作。
你可以使用合并来在列上连接 DataFrame,而不需要重复。合并允许你传递要合并的 DataFrame 以及要连接的字段,如下所示:
merged=pd.merge(new,geo,on='street')
merged.columns
Index(['address', 'count', 'street', 'x', 'y'], dtype='object')
注意新的字段x和y如何被引入到新的 DataFrame 中,但只有一个street列。这要干净得多。在joined或merged的情况下,只有当你在两个 DataFrame 上都设置了索引时,你才能使用索引。
现在你已经知道了如何清理、转换和丰富数据,是时候将这些技能结合起来,利用新获得的知识构建数据管道了。接下来的两个部分将展示如何使用 Airflow 和 NiFi 构建数据管道。
使用 Airflow 清理数据
现在你可以在 Python 中清理你的数据,你可以创建执行不同任务的函数。通过组合这些函数,你可以在 Airflow 中创建数据管道。以下示例将清理数据,然后过滤它并将其写入磁盘。
从与之前示例中相同的 Airflow 代码开始,设置导入和默认参数,如下所示:
import datetime as dt
from datetime import timedelta
from airflow import DAG
from airflow.operators.bash_operator import BashOperator
from airflow.operators.python_operator import PythonOperator
import pandas as pd
default_args = {
    'owner': 'paulcrickard',
    'start_date': dt.datetime(2020, 4, 13),
    'retries': 1,
    'retry_delay': dt.timedelta(minutes=5),
}
现在,你可以编写执行清理任务的函数了。首先,你需要读取文件,然后你可以删除区域 ID,将列转换为小写,并将started_at字段更改为datetime数据类型。最后,将更改写入文件。以下是对应的代码:
def cleanScooter():
    df=pd.read_csv('scooter.csv')
    df.drop(columns=['region_id'], inplace=True)
    df.columns=[x.lower() for x in df.columns]
    df['started_at']=pd.to_datetime(df['started_at'],
                                   format='%m/%d/%Y %H:%M')
    df.to_csv('cleanscooter.csv')
接下来,管道将读取清理后的数据,并根据开始和结束日期进行过滤。代码如下:
def filterData():
    df=pd.read_csv('cleanscooter.csv')
    fromd = '2019-05-23'
    tod='2019-06-03'
    tofrom = df[(df['started_at']>fromd)&
                (df['started_at']<tod)]
    tofrom.to_csv('may23-june3.csv')	
这两个函数看起来应该很熟悉,因为代码与前面的示例完全相同,只是重新分组了。接下来,你需要定义操作符和任务。你将使用PythonOperator并将其指向你的函数。创建 DAG 和任务,如下所示:
with DAG('CleanData',
         default_args=default_args,
         schedule_interval=timedelta(minutes=5),      
         # '0 * * * *',
         ) as dag:
    cleanData = PythonOperator(task_id='clean',
                                 python_callable=cleanScooter)
    selectData = PythonOperator(task_id='filter',
                                 python_callable=filterData)
在这个例子中,我们将再次使用BashOperator添加另一个任务。如果你还记得,你在第三章**,读取和写入文件中使用过它,只是为了在终端打印一条消息。这次,你将使用它将文件从selectData任务移动到桌面。以下是代码:
copyFile = BashOperator(task_id='copy',
                                 bash_command='cp /home/paulcrickard/may23-june3.csv /home/paulcrickard/Desktop')
之前的命令只是使用了 Linux 复制命令来复制文件。当处理文件时,你需要小心确保你的任务可以访问它们。如果有多个进程尝试访问同一文件或用户尝试访问它,你可能会破坏你的管道。最后,指定任务的顺序——创建 DAG 的方向,如下所示:
cleanData >> selectData >> copyFile
现在你已经完成了一个 DAG。将此文件复制到你的$AIRFLOW_HOME/dags文件夹。然后,使用以下命令启动 Airflow:
airflow webserver
airflow scheduler
现在,您可以通过 http://localhost:8080/admin 浏览到 GUI。选择您的新 DAG,然后点击 树形视图 选项卡。您将看到您的 DAG,并可以将其开启并运行。在下面的屏幕截图中,您将看到 DAG 以及每个任务的运行情况:

图 5.1 – 运行 DAG
您会看到 DAG 有两次失败的运行。这是由于任务运行时文件不存在导致的。我在 BashOperator 中使用了 move 而不是 copy,因此出现了关于在 Airflow 中处理文件时要小心谨慎的警告。
恭喜!您已成功完成本章内容。
摘要
在本章中,您学习了如何进行基本的 EDA,目的是在您的数据中寻找错误或问题。然后您学习了如何清理您的数据并修复常见的数据问题。通过这些技能,您在 Apache Airflow 中构建了一个数据管道。
在下一章中,您将进行一个项目,构建一个在 Kibana 中的 311 数据管道和仪表板。这个项目将利用您到目前为止所掌握的所有技能,并介绍一些新技能——例如构建仪表板和进行 API 调用。
第六章:第六章:构建 311 数据管道
在前三个章节中,你学习了如何使用 Python、Airflow 和 NiFi 来构建数据管道。在本章中,你将使用这些技能来创建一个连接到 SeeClickFix 并下载一个城市所有问题的管道,然后将其加载到 Elasticsearch 中。我目前每 8 小时运行这个管道一次。我将这个管道用作开源情报的来源——用它来监控社区的生活质量问题,以及废弃车辆、涂鸦和针头的报告。而且,看到人们向他们的城市抱怨什么类型的事情也非常有趣——在 COVID-19 大流行期间,我的城市看到了几起人们在俱乐部不保持社交距离的报告。
在本章中,我们将涵盖以下主要主题:
- 
构建数据管道 
- 
构建一个 Kibana 仪表板 
构建数据管道
这个数据管道将与之前的管道略有不同,因为我们需要使用一个技巧来启动它。我们将有两个指向同一数据库的路径——其中一个在第一次运行后我们将关闭它,并且我们将有一个连接到自身的处理器来建立成功关系。以下截图显示了完成的管道:
![Figure 6.1 – The complete pipeline
![img/Figure_6.1_B15739.jpg]
图 6.1 – 完整的管道
上述截图可能看起来很复杂,但我向你保证,到本章结束时你会明白它的意义。
映射数据类型
在你构建管道之前,你需要将 Elasticsearch 中的一个字段进行映射,以便通过将它们映射为地理点数据类型来获得坐标的好处。为此,打开 Kibana,网址为 http://localhost:5601。在工具栏上,选择 Dev Tools(扳手图标)并输入以下截图左侧面板中显示的代码,然后点击运行箭头。如果成功,你将在右侧面板中看到输出,如下面的截图所示:
![Figure 6.2 – Adding geopoint mapping
![img/Figure_6.2_B15739.jpg]
图 6.2 – 添加地理点映射
现在你已经创建了具有地理点映射的 scf 索引,当你运行你的管道时,coords 字段将被转换为 Elasticsearch 中的空间坐标。
让我们开始构建。
触发管道
在上一节中,我提到你需要欺骗数据管道以启动。记住,这个管道将连接到一个 API 端点,为了做到这一点,调用 HTTP 端点的 NiFi 处理器以及你将使用的 ExecuteScript 处理器都需要一个传入的 flowfile 来启动它们。这个处理器不能是数据管道中的第一个处理器。
要启动数据管道,你将使用 GenerateFlowFile 处理器。将处理器拖放到画布上。双击它以更改配置。在 Start Flow Fake Data 中。这让我们知道这个处理器发送的是假数据,只是为了启动流程。配置将使用所有默认值,如下面的截图所示:
![图 6.3 – 配置 GenerateFlowfile 处理器
![img/Figure_6.3_B15739.jpg]
图 6.3 – 配置 GenerateFlowfile 处理器
最后,在 SCHEDULING 选项卡中,将处理器设置为按你希望的间隔运行。我使用 8,因为我不想压倒 API。
当处理器运行时,它将生成一个包含 0 字节数据的单个 flowfile。它是空的,但它确实包含由 NiFi 生成的元数据。然而,这个空 flowfile 将会起到作用并启动下一个处理器。这就是工作的开始之处。
查询 SeeClickFix
在之前的 NiFi 示例中,你没有使用任何代码,只是配置来让处理器完成你需要的工作。我们可以在这个管道中这样做。然而,现在是一个很好的时机,将使用 Python – Jython 编码引入你的管道中。
将 ExecuteScript 处理器拖放到画布上。双击它以编辑配置。从 Query SCF 开始,这样我知道它查询的是 SeeClickFix。在 Properties 选项卡中,将 Script Engine 设置为 Python。在 Script Body 参数中,你将编写处理器将执行的 Python 代码。查询步骤如下:
- 
你需要导入所需的库。以下代码是你会始终需要包含的库: import java.io from org.apache.commons.io import IOUtils from java.nio.charset import StandardCharsets from org.apache.nifi.processor.io import StreamCallback from org.python.core.util import StringUtil
- 
接下来,你将创建一个将被调用来处理工作的类。 process函数将包含执行任务的代码:class ModJSON(StreamCallback): def __init__(self): pass def process(self, inputStream, outputStream): # Task Goes Here
- 
最后,假设没有发生错误,检查是否存在 flowfile。如果存在,调用类的 flowfile。接下来,检查是否发生了错误。如果有错误,你将把 flowfile 发送到失败关系,否则,发送到成功关系: errorOccurred=False flowFile = session.get() if (flowFile != None): flowFile = session.write(flowFile, ModJSON()) #flowFile = session.putAttribute(flowFile) if(errorOccurred): session.transfer(flowFile, REL_FAILURE) else: session.transfer(flowFile, REL_SUCCESS)之前的代码是任何 Python ExecuteScript处理器的模板。你需要更改的唯一内容将是在过程函数中,我们将在接下来的步骤中完成这一操作。由于 NiFi 使用 Jython,你可以向 Jython 环境添加许多 Python 库,但这超出了本书的范围。现在,你将使用标准库。 
- 
要调用 SeeClickFix API,你需要导入 urllib库和json,如下所示:import urllib import urllib2 import json
- 
接下来,你将在 process函数中放置代码。代码将是一个tryexcept块,它向 HTTP 端点发出请求并将响应写入outputStream。如果有错误,except块将errorOccurred设置为True,这将触发其余代码将文件流发送到Failure关系。try块中不标准的 Python 代码行是outputStream.write()。这是你写入文件流的地方:try: param = {'place_url':'bernalillo-county', 'per_page':'100'} url = 'https://seeclickfix.com/api/v2/issues?' + urllib.urlencode(param) rawreply = urllib2.urlopen(url).read() reply = json.loads(rawreply) outputStream.write(bytearray(json.dumps(reply, indent=4).encode('utf-8'))) except: global errorOccurred errorOccurred=True outputStream.write(bytearray(json.dumps(reply, indent=4).encode('utf-8')))
前面的代码在成功时将输出一个 JSON 文件流。文件流的内容将包含一些元数据和问题数组。我们将感兴趣的元数据是 page 和 pages。
你已经抓取了伯纳利县的前 100 个问题,并将把这个文件流传递给两个不同的处理器——GetEveryPage 和 SplitJson。我们将遵循 SplitJson 路径,因为这个路径会将数据发送到 Elasticsearch。
将数据转换为 Elasticsearch
以下是将数据转换为 Elasticsearch 的步骤:
- 
将 SplitJson处理器拖放到画布上。双击它以修改属性。在 属性 选项卡中,将 JsonPath 表达式 属性设置为 $.issues。这个处理器现在将 100 个问题分割成它们自己的文件流。
- 
接下来,你需要添加符合 NiFi 期望格式的坐标。我们将使用一个名为 coords的x、y字符串。为此,将ExecuteScript处理器拖放到画布上。双击它并点击import json语句。
- 
process函数将输入流转换为字符串。输入流是来自前一个处理器的文件流内容。在这种情况下,它是一个单一的问题。然后它将使用json库将其加载为json。然后你添加一个名为coords的字段,并将其分配给lat和lng字段在文件流 JSON 中的连接字符串值。最后,你将 JSON 写回输出流作为新的文件流:def process(self, inputStream, outputStream): try: text = IOUtils.toString(inputStream, StandardCharsets.UTF_8) reply=json.loads(text) reply['coords']=str(reply['lat'])+', '+str(reply['lng']) d=reply['created_at'].split('T') reply['opendate']=d[0] outputStream.write(bytearray(json.dumps(reply, indent=4).encode('utf-8'))) except: global errorOccurred errorOccurred=True outputStream.write(bytearray(json.dumps(reply, indent=4).encode('utf-8')))现在你有一个单独的问题,有一个名为 coords的新字段,它是一个 Elasticsearch 识别为地理点的字符串格式。你几乎准备好在 Elasticsearch 中加载数据了,但首先你需要一个唯一的标识符。
- 
要在 Elasticsearch 中创建主键的等效项,你可以指定一个 ID。JSON 为每个问题都有一个 ID,你可以使用它。为此,将 EvaluateJsonPath处理器拖放到画布上。双击它并选择值为$.id的id。记住,$.允许你指定要提取的 JSON 字段。现在文件流包含从 JSON 中提取的唯一 ID。
- 
将 PutElasticsearchHttp处理器拖放到画布上。双击它以编辑属性。设置http://localhost:9200。在可选的标识符属性中,将值设置为id。这是您在之前的处理器中提取的属性。将索引设置为SCF(代表SeeClickFix),将类型设置为doc。最后,您将设置索引操作属性为upsert。在 Elasticsearch 中,upsert如果 ID 不存在,则会索引文档,如果 ID 存在且数据不同,则会更新。否则,不会发生任何操作,记录将被忽略,如果数据已经相同,这正是您想要的。
问题现在正在加载到 Elasticsearch 中,如果您检查,您将在scf索引中找到 100 个文档。但 Bernalillo 县 SeeClickFix 数据中记录的数量远不止 100 条;根据QuerySCF处理器的元数据,有 44 页的记录(4,336 个问题)。
以下部分将向您展示如何抓取所有数据。
获取每一页
当您查询 SeeClickFix 时,您将结果发送到两个路径。我们选择了SplitJson路径。这样做的原因是因为在初始查询中,您收到了 100 个问题和存在多少页问题(作为元数据的一部分)。您将问题发送到SplitJson路径,因为它们已经准备好处理,但现在您需要处理页数。我们将通过遵循GetEveryPage路径来完成这项工作。
将ExecuteScript处理器拖放到画布上。双击它以编辑urllib和json库。
process函数将输入流转换为 JSON,然后使用json库加载它。函数的主要逻辑表明,如果当前页小于或等于总页数,则调用 API 并请求下一页(next_page_url),然后将 JSON 作为流文件写入。否则,它将停止。代码如下:
try:
        text = IOUtils.toString(inputStream, 
                                StandardCharsets.UTF_8)
        asjson=json.loads(text)
        if asjson['metadata']['pagination']
        ['page']<=asjson['metadata']['pagination']['pages']:
          url = asjson['metadata']['pagination']
                                  ['next_page_url']
          rawreply = urllib2.urlopen(url).read()
          reply = json.loads(rawreply)
          outputStream.write(bytearray(json.dumps(reply,
                             indent=4).encode('utf-8')))
        else:
          global errorOccurred
          errorOccurred=True        
          outputStream.write(bytearray(json.dumps(asjson,
                             indent=4).encode('utf-8')))        
    except:
        global errorOccurred
        errorOccurred=True       
        outputStream.write(bytearray(json.dumps(asjson, 
                           indent=4).encode('utf-8')))
您将连接此处理器的成功关系到最后一条路径中我们使用的SplitJson处理器。流文件将在问题上分割,添加坐标,提取 ID,然后将问题发送到 Elasticsearch。然而,我们需要这样做 42 次。
为了保持处理页面,您需要将成功关系连接到自身。没错;您可以将处理器连接到自身。当您通过此处理器处理第一页时,下一页是 2。问题被发送到SplitJson,然后回到这个处理器,它说当前页是小于 44,下一页是 3。
您现在有一个包含 SeeClickFix 所有当前问题的 Elasticsearch 索引。然而,Bernalillo 县的问题数量远大于当前问题的集合——存在一个存档。现在,您有一个每 8 小时拉取新问题的管道,您将始终是最新的,但您也可以将所有存档问题回填到 Elasticsearch 中。然后您将拥有问题的完整历史记录。
回填数据
要将历史数据仅回填到SCF索引中,只需在QuerySCF处理器中的params对象中添加一个参数。为此,右键单击QuerySCF处理器并选择QuerySCFArchive。在以下代码中的params对象中:
param = {'place_url':'bernalillo-county', 'per_page': '100', 'status':'Archived'}
添加了status参数,其值为Archived。现在,将GenerateFlowfile处理器连接到这个回填处理器以启动它。然后,将处理器连接到SplitJson处理器以建立成功关系。这将把问题发送到 Elasticsearch。但您需要遍历所有页面,因此将处理器连接到GetEveryPage处理器。这将遍历存档并将所有问题发送到 Elasticsearch。一旦这个管道完成,您就可以停止QuerySCFArchive处理器。
当您有一个不断添加新记录的系统时——就像一个事务性系统——您将经常遵循这个模式。您将构建一个数据管道来提取最近的记录,并在固定的时间间隔提取新记录——每天或每小时,这取决于系统更新的频率或您需要实时数据的程度。一旦您的管道开始工作,您将添加一系列处理器来抓取所有历史数据并回填您的仓库。您可能不需要回到时间的开始,但在这个案例中,记录数量足够少,使其可行。
如果出现问题或需要填充新的仓库,您也会遵循这个模式。如果您的仓库损坏或您上线了新的仓库,您可以重新运行这个回填管道以再次引入所有数据,使新的数据库完整。但它将只包含当前状态。下一章将处理生产管道,并将通过改进您的管道来帮助您解决这个问题。现在,让我们在 Kibana 中可视化您的新 Elasticsearch 索引。
构建 Kibana 仪表板
现在您的 SeeClickFix 数据管道已将数据加载到 Elasticsearch 中,分析师也希望看到数据的结果。使用 Kibana,您可以做到这一点。在本节中,您将为您的数据管道构建一个 Kibana 仪表板。
要打开 Kibana,浏览到http://localhost:5601,您将看到主窗口。在工具栏底部(屏幕左侧;您可能需要展开它),点击底部的管理图标。您需要选择scf*,如下面的截图所示:

图 6.4 – 在 Kibana 中创建索引模式
当您点击下一步时,您将被要求选择一个created_at,如下面的截图所示:

图 6.5 – 选择时间过滤器字段
一旦在 Kibana 中创建了索引,您就可以继续进行可视化。
创建可视化
要创建可视化,请选择工具栏中的可视化图标。选择创建可视化,您将看到各种类型,如下面的截图所示:

图 6.6 – 可用的可视化类型
您将看到scf——这将适用于本章节中所有可视化。将 y 轴保留为created_at,间隔将是每月。您将看到一个如下面的截图所示的图表(您的可能不同):

图 6.7 – 按月份创建的created_at计数的条形图
保存条形图并命名为scf-bar,或任何您能与之关联的 SeeClickFix 数据。
接下来,再次选择可视化并选择度量。您只需在度量选项下添加一个自定义标签。我选择了问题。通过这样做,您移除了默认的计数,该计数被放置在度量下的数字下面。这个可视化正在给我们提供问题的计数,并且当我们在仪表板上应用过滤器时将发生变化。配置如下面的截图所示:

图 6.8 – 度量可视化配置
再次,使用任何约定保存可视化,或者像我一样以scf为前缀。
对于下一个可视化,选择饼图——默认为甜甜圈。在桶下选择分割切片。对于聚合,选择术语。对于字段,选择request_type.title.keyword。保留其余默认设置。这将给出前五个标题。结果如下面的截图所示:

图 6.9 – 前五项问题标题
虽然不是可视化,但Markdown可以通过提供一些上下文或描述来为您的仪表板增加价值。从可视化选项中选择Markdown。您可以在左侧面板中输入 Markdown,通过点击运行符号,在右侧面板中查看预览。我刚刚添加了一个 H1 标题,一些文本,以及一个项目符号列表,如下面的截图所示:

图 6.10 – 使用 Markdown 编辑器
最后一个可视化,scf。一旦进入地图屏幕,选择添加图层,源将是文档。这允许您选择一个索引。以下截图显示了您应该看到的内容:

图 6.11 – 使用文档作为源添加新层
当您选择 scf 作为索引模式时,Kibana 将识别适当的字段并将数据添加到地图中。您的地图将是空的,您可能会想知道出了什么问题。Kibana 将时间过滤器设置为最后 15 分钟,并且您没有超过最后 8 小时的新数据。将过滤器设置为更长的时间范围,如果 create_at 字段在窗口中,数据将出现。以下截图显示了结果:

图 6.12 – 来自 Elasticsearch 索引的地图可视化
现在您已经从数据中创建了可视化,您可以继续将它们组合到一个仪表板中。下一节将向您展示如何操作。
创建仪表板
要构建仪表板,请选择工具栏上的仪表板图标。然后您将选择 scf – 或您用于保存可视化的任何名称。将它们添加到仪表板后,您可以调整它们的位置和大小。确保在设置完成后保存您的仪表板。以下截图显示了我在仪表板中构建的内容:

图 6.13 – SeeClickFix 仪表板
仪表板中添加了 Markdown、饼图、地图、指标和柱状图。我通过抓住面板的顶部来移动它们,通过抓住右下角来调整大小。您还可以点击齿轮图标并为您的面板添加新名称,这样它们就不会使用您保存可视化时使用的名称。
使用您的仪表板,您可以过滤数据,并且所有可视化都会相应改变。例如,我点击了饼图中的 Graffiti 标签,以下截图显示了结果:

图 6.14 – 在涂鸦上过滤
使用过滤器时,度量可视化就派上用场了。知道记录的数量是件好事。您可以看到地图和柱状图也发生了变化。您还可以按日期范围进行过滤。我在过滤器中选择了最后 7 天,如下截图所示:

图 6.15 – 在仪表板中按时间过滤
时间过滤器允许您选择现在、相对或绝对。相对是指从现在起的一定天数、月数、年数等,而绝对允许您在日历上指定开始和结束时间。以下截图显示了七天过滤器的结果:

图 6.16 – 带有七天过滤器的仪表板
我将要展示的最后一种过滤器是地图过滤器。你可以在地图上选择一个区域或绘制一个多边形来过滤你的仪表板。通过点击地图工具图标,选项将如以下截图所示出现:

图 6.17 – 地图上的工具图标
使用绘制边界来过滤数据,我在地图上画了一个矩形,结果如下截图所示:

图 6.18 – 使用地图过滤数据
在前面的仪表板中,你可以看到完美的点矩形。地图过滤器是我最喜欢的过滤器之一。
Kibana 仪表板使你的数据管道对非数据工程师变得有用。你投入的数据移动和转换工作变成了可以由分析师和管理员用于探索和从数据中学习的实时数据。Kibana 仪表板也是你,作为数据工程师,可视化你提取、转换和加载的数据的绝佳方式,以查看你的数据管道中是否存在任何明显的问题。它们可以是一种调试工具。
摘要
在本章中,你学习了如何使用来自 REST API 的数据构建数据管道。你还向数据管道添加了一个流程,以便你可以回填数据,或者使用单个管道重新创建包含所有数据的数据库。
本章的后半部分提供了如何使用 Kibana 构建仪表板的基本概述。通常,仪表板的构建不会是数据工程师的责任。然而,在小公司中,这完全可能是你的工作。此外,能够快速构建仪表板可以帮助验证你的数据管道,并查找数据中可能存在的任何错误。
在下一章中,我们将开始本书的新章节,你将利用你学到的技能,通过使你的管道准备就绪用于生产来提高它们。你将学习关于部署、更好的验证技术以及在生产环境中运行管道时所需的其它技能。
第二部分:在生产环境中部署数据管道
第二部分基于您所学的知识,并教授您生产数据管道的特点。您将学习到与软件工程类似的技术,例如版本控制、监控和记录。具备这些技能后,您不仅能够构建,还能管理生产数据管道。最后,您将学习如何在生产环境中部署您的数据管道。
本节包含以下章节:
- 
第七章, 生产数据管道的特点 
- 
第八章, 使用 NiFi 注册表进行版本控制 
- 
第九章, 监控和记录数据管道 
- 
第十章, 部署您的数据管道 
- 
第十一章, 构建生产数据管道 
第七章:第七章:生产管道的特性
在本章中,你将学习到使数据管道为生产准备就绪的几个特性。你将了解如何构建可以多次运行而不改变结果(幂等)的数据管道。你还将了解在事务失败时应该做什么(原子性)。此外,你还将学习在预演环境中验证数据。本章将使用一个我在生产中当前运行的数据管道示例。
对于我来说,这个管道是一个额外的收获,我对错误或缺失数据并不关心。正因为如此,这个管道中缺少了一些在关键任务或生产管道中应该存在的元素。每个数据管道都会有不同的可接受错误率——缺失数据——但在生产中,你的管道应该有一些你尚未学习的额外功能。
在本章中,我们将涵盖以下主要主题:
- 
数据预演和验证 
- 
构建幂等数据管道 
- 
构建原子数据管道 
数据预演和验证
当构建生产数据管道时,数据预演和验证变得极其重要。虽然你在第五章**,数据清洗、转换和丰富中已经看到了基本的数据验证和清理,但在生产中,你需要一种更正式和自动化的方式来执行这些任务。接下来的两个部分将指导你如何在生产中完成数据预演和验证。
数据准备
在 NiFi 数据管道示例中,数据被提取,然后通过一系列连接的处理器传递。这些处理器对数据进行了一些操作,并将结果发送到下一个处理器。但如果处理器失败会发生什么?你是否需要从头开始?根据源数据,这可能是不可能的。这就是预演发挥作用的地方。我们将预演分为两种不同类型:文件或数据库转储的预演,以及将数据预演到准备加载到仓库的数据库中。
文件预演
我们将要讨论的第一种预演类型是在从源(通常是事务型数据库)提取后,在文件中的数据预演。让我们通过一个常见的场景来了解一下为什么我们需要这种类型的预演。
你是 Widget Co 公司的数据工程师——这家公司颠覆了小部件制造行业,是唯一一家在线小部件零售商。每天,来自世界各地的人们都会在公司网站上订购小部件。你的老板指示你构建一个数据管道,每小时将网站的销售数据放入数据仓库,以便分析师可以查询数据并创建报告。
由于销售是全球性的,让我们假设唯一需要的数据转换是将本地销售日期和时间转换为 GMT。这个数据管道应该是直接的,如下面的截图所示:

图 7.1 – 将小部件销售加载到仓库的数据管道
前面的数据管道查询小部件数据库。它将记录作为一个单独的 flowfile 传递给SplitText处理器,该处理器将每个记录发送到处理器,该处理器将日期和时间转换为 GMT。最后,它将结果加载到数据仓库中。
但是当你分割记录,然后日期转换失败会发生什么?你可以重新查询数据库,对吧?不,你不能,因为每分钟都在发生事务,失败的事务已被取消并且不再在数据库中,或者他们改变了顺序,现在想要一个红色的部件而不是最初订购的五个蓝色部件。你的营销团队不会高兴,因为他们不再了解这些变化,也无法计划如何转化这些销售。
示例的目的是为了说明在一个事务型数据库中,事务是持续发生的,数据正在被修改。运行一个查询会产生一组结果,如果你 5 分钟后再次运行相同的查询,结果可能会完全不同,你现在已经失去了原始数据。这就是为什么你需要分阶段提取数据的原因。
如果使用前面的管道示例进行分阶段处理,你最终会得到以下截图所示的管道:

图 7.2 – 使用分阶段将小部件销售加载到仓库的数据管道
前面的数据管道以两个图表的形式显示。第一个图表查询小部件数据库并将结果放在磁盘上的文件中。这是分阶段步骤。从这里,下一个图表将加载数据,将记录分割成 flowfiles,转换日期和时间,最后将其加载到仓库中。如果这部分管道崩溃,或者你需要出于任何原因重放你的管道,你只需重新加载 CSV 文件,方法是重新启动数据管道的第二部分。你在原始查询时有一个数据库的副本。如果 3 个月后你的仓库被损坏,你可以使用每次查询的数据重放你的数据管道,即使数据库已经完全不同。
在 CSV 文件中保留数据库提取副本的另一个好处是,它减少了重放管道的负载。如果你的查询资源密集型,可能只能在夜间运行,或者如果你查询的系统属于另一个部门、机构或公司。你不必再次使用他们的资源来修复错误,你只需使用副本即可。
在你到目前为止构建的 Airflow 数据管道中,你已经进行了查询预演。Airflow 的工作方式鼓励良好的实践。每个任务都已将结果保存到文件中,然后你在下一个任务中加载该文件。然而,在 NiFi 中,你的查询通常被发送到SplitRecords或Text处理器,到管道中的下一个处理器。这在生产中运行管道不是好的做法,并且从现在起,示例将不再是这样。
数据库中的预演
在数据管道的提取阶段,将数据存储在文件中是有帮助的。在管道的另一端,即加载阶段,最好将数据存储在数据库中,最好是仓库相同的数据库。让我们通过另一个例子来看看原因。
你已经查询了你的数据小部件数据库并进行了预演。下一个数据管道拾取数据,对其进行转换,然后将其加载到仓库中。但现在如果加载不正确会发生什么?也许记录已经进入并且一切看起来都很成功,但映射错误,日期是字符串。请注意,我没有说加载失败。你将在本章后面学习如何处理加载失败。
没有将数据实际加载到数据库中,你只能猜测可能会遇到的问题。通过预演,你将数据加载到数据仓库的副本中。然后你可以运行验证套件和查询,以查看是否得到预期的结果——例如,你可以从表中运行一个select count(*)查询,以查看是否得到正确的记录数。这将帮助你确切地知道你可能遇到的问题,或者如果没有问题,你不会遇到什么问题。
使用管道两端都进行预演的 Widget Co 数据管道应该看起来像以下截图中的管道:
![Figure 7.3 – 在管道两端使用预演的生产
![img/Figure_7.3_B15739.jpg]
Figure 7.3 – 在管道两端使用预演的生产
前一个截图中的数据管道查询了小部件数据库,并将结果存储在文件中。下一个阶段拾取该文件,并转换日期和时间。与早期示例的不同之处在于,数据管道现在将数据加载到数据仓库的副本中。数据管道的新部分随后查询此副本,执行一些验证,然后将它加载到最终的数据库或仓库中。
ETL 与 ELT
到目前为止,你已经看到了提取、转换和加载。然而,越来越多的人转向提取、加载和转换的过程。在 ELT 过程中,数据在提取后立即存储在数据库中,而不进行任何转换。你将在数据库中处理所有的转换。如果你使用基于 SQL 的转换工具,这将非常有帮助。没有正确或错误的方式,只有偏好和用例。
通过在数据管道的前端和末端进行数据阶段,您现在更适合处理错误,并在数据通过管道移动时验证数据。不要认为这些是数据阶段放置的唯一两个地方,或者数据必须放置在文件中。您可以在数据管道中的每个转换之后阶段数据。这样做将使调试错误更容易,并在错误发生后在任何数据管道点继续。随着转换变得更加耗时,这可能更有帮助。
您将小部件数据库的提取放置在文件中,但没有理由阻止您将数据提取到关系型或 noSQL 数据库中。将数据转储到文件中比将其加载到数据库中稍微简单一些 – 您不需要处理模式或构建任何额外的基础设施。
虽然阶段数据对于回放管道、处理错误和调试管道很有帮助,但它也有助于管道的验证阶段。在下一节中,您将学习如何使用 Great Expectations 在文件和数据库阶段数据上构建验证套件。
使用 Great Expectations 验证数据
当您的数据放置在文件或数据库中时,您有完美的机会来验证它。在第五章**,清理、转换和丰富数据中,您使用了 pandas 来执行探索性数据分析,了解哪些列存在,查找空值的数量,查看列内值的范围,并检查每个列的数据类型。Pandas 功能强大,通过使用 value_counts 和 describe 等方法,您可以获得大量关于数据的见解,但有一些工具可以使验证更加清晰,并使您对数据的期望更加明显。
本节中您将学习的库是 Great Expectations。以下是大纲页面的截图,您可以在其中加入并参与其中:

图 7.4 – 用于验证数据的 Great Expectations Python 库,以及其他功能
为什么选择 Great Expectations?因为使用 Great Expectations,您可以指定人类可读的期望,并让库处理实现。例如,您可以在代码中指定 age 列不应包含空值,如下所示:
expect_column_values_to_not_be_null('age')
无论您的数据是在 DataFrame 中还是在数据库中,Great Expectations 都将处理执行此操作的逻辑。相同的期望将在任何数据上下文中运行。
开始使用 Great Expectations
使用 pip3 安装 Great Expectations 可以按照以下步骤进行:
pip3 install great_expectations 
要查看 Great Expectations 生成的文档,您还需要在您的机器上安装 Jupyter Notebook。您也可以使用 pip3 安装 Notebook:
pip3 install jupyter
安装完所需的要求后,你现在可以设置一个项目了。在 $HOME/peoplepipeline 目录下创建一个目录并按 Enter 键。你可以在 Linux 上使用以下命令来完成此操作:
mkdir $HOME/peoplepipeline
cd $HOME/peoplepipeline
现在,你已经进入了项目目录,在设置 Great Expectations 之前,我们将导出我们将要处理的数据样本。使用来自 第三章**,阅读和写入文件 的代码,我们将生成 1,000 条与人物相关的记录。代码如下:
from faker import Faker
import csv
output=open('people.csv','w')
fake=Faker()
header=['name','age','street','city','state','zip','lng','lat']
mywriter=csv.writer(output)
mywriter.writerow(header)
for r in range(1000):
    mywriter.writerow([fake.name(),fake.random_int(min=18,
    max=80, step=1), fake.street_address(), fake.city(),fake.
    state(),fake.zipcode(),fake.longitude(),fake.latitude()])
output.close()
上述代码创建了一个包含人物记录的 CSV 文件。我们将把这个 CSV 文件放入项目目录中。
现在,你可以通过使用命令行界面来设置这个项目上的 Great Expectations。以下命令将初始化你的项目:
great_expectations init
你现在将经历一系列步骤来配置 Great Expectations。首先,Great Expectations 将询问你是否准备好继续。你的终端应该看起来如下截图所示:


图 7.5 – 在项目上初始化 Great Expectations
输入 Y 并按 Enter 键后,你将收到一系列问题的提示:
What data would you like Great Expectations to connect to?
What are you processing your files with?
Enter the path (relative or absolute) of a data file.
Name the new expectation suite [people.warning].
问题的答案显示在以下截图中,但应该是 Files、Pandas,即你放置文件的位置,以及你希望为其命名的任何名称:


图 7.6 – 通过回答问题初始化 Great Expectations
当 Great Expectations 运行完成后,它会告诉你已经完成,并给出它生成的文档的路径,并在你的浏览器中打开该文档。文档看起来如下截图所示:


图 7.7 – Great Expectations 生成的文档
上一张截图显示了为 Great Expectations 套件生成的文档。你可以看到有 11 个预期,并且我们已经通过了所有的预期。这些预期非常基础,指定了应该存在多少条记录以及哪些列应该以什么顺序存在。此外,在代码中我指定了一个年龄范围。因此,年龄 有一个最小值和最大值。年龄必须大于 17 且小于 81 才能通过验证。你可以通过滚动查看生成的预期样本。以下截图显示了其中的一些:


图 7.8 – 生成的预期样本
如你所见,预期非常严格——例如,年龄不能为空。让我们编辑预期。因为你已经安装了 Jupyter Notebook,所以你可以运行以下命令来一次性启动你的预期套件:
great_expectations suite edit people.validate
你的浏览器将打开一个 Jupyter notebook,应该看起来如下截图所示:

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/dt-engi-py/img/Figure_7.9_B15739.jpg)
图 7.9 – 您的期望套件在 Jupyter 笔记本中
一些项目应在代码中突出显示 – 期望套件名称,以及batch_kwargs变量中您的数据文件路径。当您滚动浏览时,您将看到带有其类型标题的期望。如果您滚动到Table_Expectation(s)标题,我将通过删除单元格或删除单元格中的代码来删除行计数期望,如下面的截图所示:

图 7.10 – 表期望
另一个需要编辑的期望是在age标题下。我将删除一个期望,具体来说是expect_quantile_values_to_be_between期望。确切的行如下面的截图所示:

图 7.11 – 要删除的量分期望的年龄期望
您可以继续删除期望,或者添加新的期望,或者甚至只是修改现有期望的值。您可以在docs.greatexpectations.io/en/latest/reference/glossary_of_expectations.html找到可用期望的词汇表。
一旦您完成所有更改并且满意,您可以通过运行整个笔记本来保存对您的期望套件的更改。以下截图显示了如何操作 – 选择单元格 | 运行所有:

图 7.12 – 通过运行笔记本保存您的期望套件中的更改
现在您有了期望套件,是时候将其添加到您的管道中。在接下来的两个部分中,您将学习如何将其添加到您的管道中以供 NiFi 使用,或者将代码嵌入到您的管道中以供 Airflow 使用。
管道外部的 Great Expectations
到目前为止,您在编辑 Jupyter 笔记本内的期望套件时验证了数据。您可以使用像 Papermill 这样的库继续这样做,但这超出了本书的范围。然而,在本节中,您将创建一个 Tap 并从 NiFi 中运行它。
Papermill
Papermill 是 Netflix 创建的一个库,允许您创建参数化的 Jupyter 笔记本,并通过命令行运行它们。您可以为结果笔记本更改参数并指定输出目录。它与另一个 Netflix 库 Scrapbook 配合良好。您可以在 github.com/nteract 找到它们,以及其他有趣的项目,包括 Hydrogen。
Tap 是 Great Expectations 创建可执行 Python 文件以运行您的期望套件的方式。您可以使用命令行界面创建一个新的 Tap,如下所示:
great_expectations tap new people.validate peoplevalidatescript.py
上述命令接受一个期望套件和一个 Python 文件名来创建。当它运行时,它会要求你提供一个数据文件。我已经将其指向你在上一节创建套件时使用的 people.csv 文件。这是数据管道在阶段数据时将覆盖的文件:

图 7.13 – 指定位置的 Python 文件的结果
如果你运行 tap,你应该会看到它成功了,如下面的截图所示:

图 7.14 – Great Expectation tap 运行
你现在可以开始在 NiFi 中构建管道并使用 Great Expectations 验证你的数据了。下一节将带你了解这个过程。
NiFi 中的 Great Expectations
结合 NiFi 和 Great Expectations 需要对上一节中创建的 tap 进行一些修改。首先,你需要将所有的出口都改为 0。如果你有一个 system.exit(1) 的出口,NiFi 处理器会因为脚本失败而崩溃。我们希望脚本能够成功关闭,即使结果不成功,因为你要更改的第二件事是 print 语句。将 print 语句改为带有结果键和通过或失败值的 JSON 字符串。现在,即使脚本成功退出,我们也会在 NiFi 中知道它实际上是否通过。以下代码块显示了 tap 的代码,其中修改的部分用粗体表示:
import sys
from great_expectations import DataContext
context = DataContext("/home/paulcrickard/peoplepipeline/great_expectations")
suite = context.get_expectation_suite("people.validate")
batch_kwargs = {
    "path": "/home/paulcrickard/peoplepipeline/people.csv",
    "datasource": "files_datasource",
    "reader_method": "read_csv",
}
batch = context.get_batch(batch_kwargs, suite)
results = context.run_validation_operator(
                               "action_list_operator", [batch])
if not results["success"]:
    print('{"result":"fail"}')
    sys.exit(0)
print('{"result":"pass"}')
sys.exit(0)
在完成对 tap 的更改后,你现在可以在 NiFi 中构建数据管道。以下截图是使用 tap 开始的数据管道:

图 7.15 – 使用 Great Expectations 的 NiFi 数据管道
上述数据管道创建了 1,000 条记录并将其保存为 CSV 文件。然后它在数据上运行 tap 并读取结果——来自脚本的通过或失败 JSON。最后,它提取结果并将 flowfile 路由到通过或失败处理器。从那里,你的数据管道可以继续,或者它可以记录错误。你将在以下步骤中走过这个管道:
- 
数据管道首先生成一个没有任何数据的假 flowfile 来触发下一个处理器。你可以用查询你的事务数据库或从你的数据湖读取文件的处理器来替换这个处理器。我已经安排这个处理器每小时运行一次。 
- 
一旦收到空流文件, ExecuteStreamCommand处理器会调用loadcsv.pyPython 脚本。此文件来自 第三章**,读取和写入文件,并使用Faker创建 1,000 条虚假人物记录。ExecuteStreamCommand处理器将读取脚本的输出。如果你有打印语句,每一行都会成为一个流文件。脚本有一个输出,那就是{"status":"Complete"}。
- 
要配置处理器以运行脚本,你可以设置 python3– 如果你可以使用完整路径运行命令,你不需要全部输入。最后,设置loadcsv.py。当处理器运行时,输出流文件将在以下屏幕截图显示:![图 7.16 – 流文件显示 JSON 字符串]() 图 7.16 – 流文件显示 JSON 字符串 
- 
下一个处理器也是一个 ExecuteStreamCommand处理器。这次,脚本将是你的 tap。配置应该与上一步相同,除了peoplevalidatescript.py。处理器完成后,流文件将包含带有通过或失败结果的 JSON。pass流文件在以下屏幕截图显示:![图 7.17 – tap 的结果,验证通过]() 图 7.17 – 拨号结果,验证通过 
- 
下一个处理器中提取了结果值 – EvaluateJsonPath。通过点击加号添加一个新属性,命名为result并将其值设置为$.result。这将提取pass或fail值并将其作为流文件属性发送。
- 
下一个过程是 RouteOnAttribute。此处理器允许你创建可以用于连接到另一个处理器的属性的属性,这意味着你可以将每个属性发送到不同的路径。创建两个新属性 –pass和fail,其值在以下代码片段中显示:${result:startsWith('pass')} ${result:startsWith('fail')}
- 
前面的命令使用 NiFi 表达式语言读取流文件中结果属性的值。 
- 
从这里,我在 PutFile处理器处终止了数据管道。但现在你可以通过将pass和fail路径连接到上一个处理器中的相应关系来继续。如果通过了,你可以读取暂存文件并将数据插入到仓库中。
在本节中,你将 Great Expectations 连接到你的数据管道。tap 是使用你的数据生成的,因此测试通过了。管道以将文件写入磁盘结束。然而,你可以继续数据管道,将成功路由到数据仓库。在现实世界中,你的测试有时会失败。在下一节中,你将学习如何处理失败的测试。
验证失败
验证总是会通过,因为我们使用的脚本生成的记录都符合验证规则。如果我们更改了脚本怎么办?如果你编辑loadcsv.py脚本并更改最小和最大年龄,我们可以使验证失败。编辑如下所示:
fake.random_int(min=1, max=100, step=1)
这将创建低于最小值和高于最大值的记录——希望如此,因为它是随机的,但 1000 条记录应该能让我们达到目标。一旦你编辑了脚本,你可以重新运行数据管道。最终的 flowfile 应该已经被路由到fail路径。Great Expectations 为你的验证创建文档。如果你记得,你最初在创建验证套件时看到了它们。现在你将有一个记录了通过和失败的运行。使用你的浏览器打开这些文档。路径位于你的项目文件夹中。例如,我的文档位于以下路径:
file:///home/paulcrickard/peoplepipeline/great_expectations/uncommitted/data_docs/local_site/validations/people/validate/20200505T145722.862661Z/6f1eb7a06079eb9cab8de404c6faa b62.html
文档应该显示所有的验证运行。文档将看起来像以下截图:

图 7.18 – 多次验证运行的结果
前面的截图显示了所有的验证运行。你可以看到表示失败的红色x。点击一个失败的运行,查看哪些期望未满足。结果应该是最小和最大年龄都没有达到。你应该看到这一点,如下面的截图所示:

图 7.19 – 年龄期望未达到
在本节中,你创建了一个 Great Expectations 套件并指定了你的数据期望。以前,你将不得不手动使用 DataFrames 和大量的代码来完成这项工作。现在你可以使用可读性强的语句,并让 Great Expectations 来做这项工作。你已经创建了一个可以在你的 NiFi 数据管道内运行或使用 Cron 或其他工具调度的 tap。
关于 Airflow 的简要说明
在前面的例子中,你在管道外部运行了验证套件 – 脚本在管道中运行,但被处理器调用。你还可以在管道内运行代码,而不需要调用它。在 Apache Airflow 中,你可以创建一个具有来自 tap 的代码的验证任务。为了处理失败,你需要抛出一个异常。为此,在你的 Airflow 代码中导入库。我在下面的代码块中包含了你需要包含在你标准样板之上的库:
import sys
from great_expectations import DataContext
from airflow.exceptions import AirflowException
from airflow import DAG
from airflow.operators.bash_operator import BashOperator
from airflow.operators.python_operator import PythonOperator
在导入所有库之后,你可以编写你的任务,如下面的代码块所示:
def validateData():
	context = DataContext("/home/paulcrickard/peoplepipeline/great_expectations")
	suite = context.get_expectation_suite("people.validate")
	batch_kwargs = {
    	"path": "/home/paulcrickard/peoplepipeline/people.csv",
    	"datasource": "files_datasource",
    	"reader_method": "read_csv",
}
	batch = context.get_batch(batch_kwargs, suite)
	results = context.run_validation_operator(
                               "action_list_operator", [batch])
	if not results["success"]:
    		raise AirflowException("Validation Failed")
上述代码将引发错误,或者如果验证成功,则结束。然而,选择处理失败取决于您。您需要做的只是检查results["success"]是否为True。您现在可以编写其他函数,使用PythonOperator创建任务,然后设置与所有其他 Airflow 示例相同的下游关系。
以下几节将讨论生产数据管道的两个其他特性——幂等性和原子性。
构建幂等数据管道
生产数据管道的一个关键特性是它必须是幂等的。幂等被定义为表示一个集合的元素,当它自身乘以或以其他方式操作时,其值不变。
在数据科学中,这意味着当您的管道失败时,这不是一个“是否”的问题,而是一个“何时”的问题,它可以重新运行,并且结果相同。或者,如果您不小心连续三次错误地点击了管道的运行按钮,则不会有重复的记录——即使您连续多次错误地点击运行按钮。
在第三章**,读取和写入文件中,您创建了一个数据管道,生成了 1,000 条人员记录,并将这些数据放入 Elasticsearch 数据库中。如果您让这个管道每 5 分钟运行一次,那么 10 分钟后您将会有 2,000 条记录。在这个例子中,记录都是随机的,您可能没问题。但如果记录是从另一个系统中查询的行呢?
每次管道运行时,它都会反复插入相同的记录。您如何创建幂等数据管道取决于您使用的是哪些系统和您希望如何存储您的数据。
在前一章的 SeeClickFix 数据管道中,您查询了 SeeClickFix API。您没有指定任何滚动时间范围,以仅获取最新的记录,并且您的回填代码抓取了所有归档的问题。如果您按照计划每 8 小时运行此数据管道一次,那么您将抓取新问题,但也会抓取您已经有的问题。
SeeClickFix 数据管道使用 Elasticsearch 中的upsert方法来使管道幂等。使用EvaluteJsonPath处理器,您提取了问题 ID,然后将其用作PutElasticsearchHttp处理器中的Identifier Attribute。您还设置了upsert。这相当于在 SQL 中使用更新。不会重复记录,并且只有在有更改的情况下才会修改记录。
使数据管道幂等的另一种方法,一些功能数据工程倡导者所推崇的方法,是在每次运行数据管道时创建一个新的索引或分区。如果您将索引命名为带有时间戳后缀的名称,那么每次管道运行时都会得到一个新的具有不同记录的索引。这不仅使数据管道幂等,还从您的数据库索引中创建了一个不可变对象。索引永远不会改变;只是会添加新的索引。
构建原子数据管道
本章我们将讨论生产数据管道的最后一个特性,即原子性。原子性意味着如果事务中的单个操作失败,那么所有操作都会失败。如果你正在将 1,000 条记录插入数据库,就像你在第三章“读取和写入文件”中所做的那样,如果有一条记录失败,那么所有 1,000 条都会失败。
在 SQL 数据库中,如果记录编号 500 失败,数据库将回滚所有更改,并且将不再尝试继续。你现在可以自由地重试事务。失败可能由许多原因引起,其中一些是你无法控制的。如果你在插入记录时电源或网络中断,你希望这些记录被保存到数据库中吗?那么你需要确定事务中哪些记录成功,哪些失败,然后只重试失败的记录。这将比重试整个事务容易得多。
在你构建的 NiFi 数据管道中,没有原子性。在 SeeClickFix 示例中,每个问题都作为一个 flowfile 发送,并在 Elasticsearch 中更新。唯一存在的原子性是文档(问题)中的每个字段都成功或失败。但可能存在这样的情况:所有问题都失败了,只有一个成功了,这会导致数据管道成功。
Elasticsearch 没有原子事务,因此任何实现 Elasticsearch 的数据管道都需要在逻辑中处理这一点。例如,你可以跟踪在 Elasticsearch 中索引的每条记录以及每个失败关系。如果在运行过程中出现失败关系,那么就会删除所有成功索引的问题。以下截图显示了示例数据管道:

图 7.20 – 将原子性构建到数据管道中
前面的数据管道创建了两个 flowfiles;一个成功,一个失败。两个的内容都放在磁盘上的文件中。从这里,你的数据管道可以列出失败目录中的文件。如果有零个或多个,那么可以读取成功文件并将它们从 Elasticsearch 中删除。
这并不优雅,但原子性很重要。当失败只是部分时,调试数据管道失败非常困难且耗时。为了引入原子性所需的额外工作是非常值得的。
SQL 数据库在事务中内置了原子性。使用如psycopg2这样的库,你可以将多个插入、更新或删除操作合并为一个事务,并保证结果要么是所有操作都成功,要么是事务失败。
创建幂等性和原子性的数据管道在创建数据管道时需要额外的工作。但是,如果没有这两个特性,你的数据管道在意外多次运行时(非幂等)或存在缺失记录时(非原子)会对结果进行更改。调试这些问题是困难的,因此,在使你的数据管道幂等和原子性上花费的时间是值得的。
摘要
在本章中,你学习了生产数据管道的三个关键特性:预演和验证、幂等性和原子性。你学习了如何使用 Great Expectations 为你的数据管道中的预演数据添加生产级验证。你还学习了如何将幂等性和原子性融入你的管道中。掌握这些技能后,你可以构建更健壮、适用于生产环境的管道。
在下一章中,你将学习如何使用 NiFi 注册表进行版本控制。
第八章:第八章:使用 NiFi 注册表进行版本控制
在前面的章节中,您构建了几个数据管道,但我们遗漏了一个非常重要的组件——版本控制。任何优秀的软件开发者几乎都会在开始编写任何代码之前为他们的项目设置版本控制。为生产构建数据管道也是如此。数据工程师使用了许多与软件工程师相同的工具和流程。使用版本控制可以让您在无需担心破坏数据管道的情况下进行更改。您始终可以回滚到之前的版本。NiFi 注册表还允许您连接新的 NiFi 实例,并完全访问您现有的所有数据管道。在本章中,我们将涵盖以下主要主题:
- 
安装和配置 NiFi 注册表 
- 
在 NiFi 中使用注册表 
- 
数据管道版本控制 
- 
使用 git-persistence 与 NiFi 注册表一起使用 
安装和配置 NiFi 注册表
当您听到版本控制时,您可能已经习惯了听到 Git。在本章的后面部分,我们将使用 Git,但 Apache NiFi 有一个子项目可以处理我们所有的版本控制需求——NiFi 注册表:

图 8.1 – NiFi 注册表主页
现在我们来安装注册表。
安装 NiFi 注册表
要安装 NiFi 注册表,请访问网站 nifi.apache.org/registry 并滚动到 发布。以下截图显示了可用的发布版本:

图 8.2 – NiFi 注册表
您将看到当前版本的源发布版和两个二进制文件,撰写本文时,版本号为 0.6.0。在 Windows 上,您可以下载 zip 版本,但由于我使用的是 Linux,我将下载 nifi-registry-0.6.0-bin.tar.gz 文件。
文件下载完成后,将其移动到您的家目录,提取内容,然后使用以下命令删除存档:
mv Downloads/nifi-r* ~/
tar -xvzf nifi-registry-0.6.0-bin.tar.gz
rm nifi-registry-0.6.0-bin.tar.gz
现在,您将有一个名为 nifi-registry-0.6.0 的文件夹。要使用默认设置(HTTP 在端口 18080 上运行)运行注册表,从目录中,使用以下命令:
sudo ./bin/nifi-registry.sh start
一旦启动了注册表,请通过 http://localhost:18080/nifi-registry 浏览到它。您应该看到以下屏幕:

图 8.3 – NiFi 注册表
如您所见,它是空的,用户是匿名的。您没有更改任何默认设置或添加任何身份验证,并且您还没有添加任何数据管道。
NiFi 注册表使用桶来存储您的数据管道。桶类似于文件夹。您可以分组相似的管道,或者为每个源创建一个文件夹,或者为目的地创建一个文件夹,或者以任何适合您需求和用例的方式创建。下一节将指导您配置 NiFi 注册表。
配置 NiFi 注册表
注册表启动并运行后,您需要创建一个文件夹来存放您的数据管道。要创建文件夹,点击屏幕右上角的扳手。屏幕上会出现一个弹出窗口。点击 新建存储桶 按钮。在下一个弹出窗口中,输入存储桶名称,如图下所示:

图 8.4 – 创建新的存储桶
一旦创建了存储桶,您将在主注册表屏幕上看到它。您的注册表现在应该看起来像以下截图:

图 8.5 – 带有新存储桶的注册表
现在您已经部署了注册表并创建了一个存储桶,您现在可以将其注册到 NiFi 中并开始对数据管道进行版本控制。下一节将引导您完成此过程。
在 NiFi 中使用注册表
注册表已启动并运行,现在您需要告诉 NiFi 关于它的信息,以便您可以使用它来对数据管道进行版本控制。NiFi 图形用户界面将处理所有配置和版本控制。在下一节中,您将向 NiFi 添加注册表。
将注册表添加到 NiFi
要将注册表添加到 NiFi,点击窗口右上角的网格菜单,然后从下拉菜单中选择 控制器设置,如图下所示:

图 8.6 – Nifi 中的控制器设置
在 控制器设置 弹出窗口中,有几个选项卡。您将选择最后一个选项卡—注册表客户端。点击窗口右上角的加号,您将添加您的注册表,如图下所示:

图 8.7 – 将 NiFi 注册表添加到 NiFi
点击 添加 按钮后,您的注册表将连接到 NiFi。关闭窗口,您将进入主 NiFi 画布。现在您已经准备好对数据管道进行版本控制了。
对数据管道进行版本控制
您可以使用 NiFi 注册表在处理器组内部对数据管道进行版本控制。我正在运行 NiFi,并将画布缩放到来自 第六章**,构建 311 数据管道 的 SeeClickFix 处理器组。要开始对此数据管道进行版本控制,右键单击处理器组标题栏并选择 版本 | 启动版本控制,如图下所示:

图 8.8 – 在处理器组上启动版本控制
您的处理器组现在正在通过版本控制进行跟踪。您将在处理器组标题框的左侧看到一个绿色勾号,如图下所示:

图 8.9 – 使用版本控制的处理器组
如果你浏览回 NiFi 注册表,你会看到 Scf-DataEngineeringPython 正在被跟踪。你还可以通过展开条形图来查看详细信息。详细信息显示了你的描述和版本说明(第一次提交),以及一些标识符。结果如下面的截图所示:

图 8.10 – 注册表中数据管道的详细信息
你没有在注册表中跟踪数据管道。在下一节中,你将进行更改并更新注册表。
编辑版本化管道
在正常的工作流程中,你会创建一个处理器组并将其添加到注册表(换句话说,开始版本化处理器)。然后你会进行更改,并将这些更改提交到注册表,并始终确保你使用的是适当的版本。
让我们对 SeeClickFix 数据管道进行更改。你的管道正在运行,一切工作得非常完美。然后你的主管说有一个新的仓库需要开始接收 SeeClickFix 数据。你不需要构建新的数据管道;你只需要将仓库添加到当前的管道中。进入处理器组,我在原始处理器旁边添加了一个 NewDataWarehouse 处理器。以下截图显示了更改:

图 8.11 – 将新的数据仓库添加到数据管道
退出处理器组回到主画布。查看处理器组的标题栏,你会看到绿色的勾号消失了,取而代之的是一个星号。将鼠标悬停在其上,会显示所做的本地更改,如下面的截图所示:

图 8.12 – 处理器组内已进行本地更改
在进行任何更改后,你需要提交这些更改,并将它们添加到注册表中。右键单击标题栏并选择版本。在提交本地更改之前,让我们查看更改。选择显示本地更改。以下截图显示了更改:

图 8.13 – 显示本地更改
如前一个截图所示,添加了一个处理器和一个关系。现在你可以选择版本 | 提交本地更改来将它们添加到注册表。你将被提示添加一个描述。一旦保存,你将在标题栏中看到一个绿色的勾号。NiFi 注册表现在将显示你有两个版本,并将显示最新版本的详细信息。
使用多个版本,你现在可以右键单击标题栏并选择版本 | 更改版本。更改为版本 1将导致一个带有向上箭头的橙色圆圈警告你,你正在使用不是最新版本。
当处理器组被版本控制跟踪时,你可以进行更改、回滚并提交新的更改。如果你犯了一个错误,你可以回滚你的工作并重新开始。但你也可以导入其他用户可能在他们的本地 NiFi 开发副本中创建的处理器。下一节将向你展示如何操作。
从 NiFi 注册表中导入处理器组
让我们假设你和另一位工作人员正在你自己的 NiFi 本地副本中构建数据管道。你们都将更改提交到 NiFi 注册表,就像软件开发者使用 Git 一样。现在你被分配去修复你的同事正在努力解决的问题。你如何使用他们的工作?你可以让他们导出一个模板,然后你可以导入它。这是在注册表之前事情通常是如何做的。但现在,你将使用注册表。
将处理器组拖动到 NiFi 画布上。注意,在为组命名文本框下方,现在有一个导入选项,如下截图所示:

Figure 8.14 – 现在处理器组有导入选项
现在 NiFi 可以访问 NiFi 注册表,它已经添加了导入处理器组的选项。通过点击导入,你将能够选择一个注册表、一个桶和一个流程。在下面的截图中,我选择了SCf流程:

Figure 8.15 – 选择流程和版本
导入流程后,你可以看到它现在已经被添加到画布上。在前一节中,我将处理器组改回版本 1,所以你可以看到我有一个版本,带有橙色圆圈和箭头,以及导入的当前版本带有绿色勾选标记。
当你引入新的数据工程师,或者设置新的 NiFi 实例时,你可以将所有生产管道导入到新的环境中。这保证了每个人都在相同的源上工作,同时也保证了所有更改在开发环境之间被跟踪和共享。
使用 NiFi 注册表进行 git-persistence
就像软件开发者一样,你也可以使用 Git 来进行你的数据处理管道的版本控制。NiFi 注册表允许你通过一些配置使用 git-persistence。要使用 Git 进行数据处理管道的版本控制,你首先需要创建一个仓库。
登录到 GitHub 并为你的数据处理管道创建一个仓库。我已经登录到我的账户并创建了如下截图所示的仓库:

Figure 8.16 – 创建 GitHub 仓库
在创建存储库后,你需要为注册表创建一个访问令牌,以便它可以使用该令牌读取和写入存储库。在 GitHub设置中,转到开发者设置,然后个人访问令牌,然后点击以下截图所示的生成个人访问令牌超链接:

图 8.17 – 创建访问令牌的设置
你可以为此令牌添加一个备注,以便你可以记住使用它的服务。然后选择作用域访问——勾选 repo 标题。以下截图显示了设置:

图 8.18 – 分配访问令牌作用域
现在,将存储库克隆到你的本地文件系统中。你可以通过点击 GitHub 存储库中的克隆或下载按钮来获取链接。然后,在你的终端中运行以下命令来克隆它:
git clone https://github.com/PaulCrickard/NifiRegistry.git
你将看到一些输出,并且现在应该将存储库作为一个文件夹放在当前目录中。命令的输出如下截图所示:

图 8.19 – 克隆 GitHub 存储库
你需要将 GitHub 信息输入到 NiFi 注册表中。你可以在conf目录下的providers.xml文件中这样做。你将在名为flowPersistenceProvider的标题下方进行编辑。配置如下截图所示:

图 8.20 – 在providers.xml中将 GitHub 信息添加到注册表中
修改providers.xml文件后,你需要重新启动注册表。你可以使用以下命令来重新启动它:
sudo ./bin/nifi-registry.sh start
当注册表重新启动时,前往你的 NiFi 画布,并将第三个数据仓库添加到SeeClickFix处理器组中。当你退出组时,你会看到有一些本地更改尚未提交——绿色的勾号消失了,出现了一个星号。在标题菜单上右键单击,然后选择版本然后提交本地版本。这次会花费更长的时间,因为文件正在发送到你的 GitHub 存储库。
查看 NiFi 注册表,你可以看到我现在有三个版本,如下截图所示:

图 8.21 – 版本 3 在注册表中
浏览到存储库,你会看到为注册表中的存储桶名称创建了一个文件夹,然后添加了流程数据。以下截图显示了文件夹的内容:

图 8.22 – 注册表存储桶和保存到 GitHub 存储库中的流程
现在您的数据管道正在通过 NiFi 注册表使用版本控制,并持久化存储在您本地的磁盘和 GitHub 在线上。
恭喜您,您现在拥有了一个功能齐全的 NiFi 注册表,并将您的数据管道保存到了 Git 仓库中。在您的职业生涯的某个时刻,您将需要运行一个类似于几个月前您所做的那样数据管道。而不再是翻阅您的文件并试图回忆起当时您所做的一切,现在您可以通过浏览到您的 NiFi 注册表并选择适当的数据管道版本。如果您的服务器崩溃并且一切丢失?现在您可以通过重新安装 NiFi 并将其连接到由 Git 仓库支持的 NiFi 注册表来恢复您所有的辛勤工作。
摘要
在本章中,您学习了生产数据管道最重要的功能之一:版本控制。软件开发者不会在没有使用版本控制的情况下编写代码,数据工程师也不应该这样做。您已经学习了如何安装和配置 Nifi 注册表,以及如何开始跟踪处理器组的版本。最后,您现在能够将版本持久化到 GitHub。对您的数据管道的任何更改都将被保存,如果您需要回滚,您也可以这样做。随着您的团队壮大,所有数据工程师都将能够管理数据管道,并确保他们拥有最新版本,同时在本地上进行开发。
在下一章中,您将学习如何记录和监控您的数据管道。如果出现问题,这是不可避免的,您将需要了解它。良好的数据管道记录和监控将允许您在发生错误时捕捉到错误,并调试它们以恢复您的数据流。
第九章:第九章:监控数据管道
您现在知道如何构建生产就绪的数据管道——它们是幂等的、使用原子事务,并且是版本控制的。它们准备好在生产环境中运行,但在您部署它们之前,您需要能够监控它们。代码、数据、网络或其他您无法控制区域可能会出现错误。您需要知道错误何时发生,以及何时一切按预期运行。
在本章中,我们将涵盖以下主要主题:
- 
在 GUI 中监控 NiFi 
- 
使用处理器监控 NiFi 
- 
使用 Python 和 REST API 监控 NiFi 
使用 GUI 监控 NiFi
NiFi GUI 提供了几种监控数据管道的方法。使用 GUI 是开始监控 NiFi 实例的最简单方法。
使用状态栏监控 NiFi
您需要的大部分信息都在状态栏上。状态栏位于组件工具栏下方,如下面的屏幕截图所示:

图 9.1 – 组件和状态工具栏
从状态栏的左侧开始,让我们看看正在监控的内容:
- 
活动线程:这可以让您知道有多少线程正在运行。您可以了解任务和负载。 
- 
总排队数据:flowfiles 的数量和磁盘上的总大小。 
- 
是否传输远程进程组和不传输远程进程组:您可以在多台机器或同一台机器上的多个实例上运行 NiFi,并允许进程组进行通信。这些图标告诉您它们是否正在通信。 
- 
运行中的组件、停止的组件、无效的组件和禁用的组件:这些显示您组件的状态。运行不一定意味着组件当前正在处理数据,但表示它已开启并计划这样做。 
- 
最新版本的进程组、本地修改的版本化进程组、过时的版本化进程组、本地修改和过时的版本化进程组以及同步失败版本化进程组:这组图标显示了您的处理器组的版本信息。从这里您可以判断是否有未提交的更改或是否使用较旧版本。 
- 
最后刷新:这可以让您知道工具栏中的数据何时有效。刷新通常每五分钟进行一次。 
状态栏为您提供所有处理器的监控信息,但每个处理器组以及每个处理器都有一个状态工具栏。您可以在以下屏幕截图中看到相同指标的监控状态:

图 9.2 – 处理器组监控
输入和输出指标显示是否有数据从另一个处理器或组流入处理组。您将在下一章中学习如何连接处理器组。版本信息不在工具栏上,而在处理器组标题的左侧。处理器组右侧的红色方块是一个公告。这提供了处理器组内错误的信息。悬停在它上面会显示错误,如下面的屏幕截图所示:

图 9.3 – 查看处理器组的公告
我目前没有运行 Elasticsearch,因此将数据发送到 Elasticsearch 的处理器由于连接超时而失败。如果您进入处理器组,您可以看到特定处理器的公告,如下面的屏幕截图所示:

图 9.4 – 特定处理器的公告
要更改公告消息,您可以在设置下的处理器配置中调整级别。公告级别下拉菜单允许您根据严重性显示更多或更少的内容,如下面的屏幕截图所示:

图 9.5 – 设置公告级别
您可以使用公告板查看所有 NiFi 处理器的公告信息,公告板可通过 NiFi 上方右角的格子菜单访问。选择公告板将显示所有消息,如下面的屏幕截图所示:

图 9.6 – 显示所有通知的公告板
在每个处理器组中,每个处理器也有状态信息,如下面的屏幕截图所示:

图 9.7 – 单个处理器的状态
处理器中的输入和输出指标显示在过去五分钟内有多少数据(流文件大小)通过了处理器。
使用计数器
与公告类似,您可以创建递增或递减计数器。计数器不会告诉您某事是否成功或失败,但它们可以给您一个关于在任何数据管道中的任何一点正在处理多少流文件的线索。
在 EvaluateJsonPath 和 ElasticSCF 处理器之间的 UpdateCounter 处理器中。这意味着在流文件被插入 Elasticsearch 之前,计数器将被更新。流程如下面的屏幕截图所示:

图 9.8 – 添加到数据管道的 UpdateCounter 处理器
如前述截图所示,162 个 flowfiles 通过处理器发送。你将在本节后面的内容中看到这些结果。但首先,为了配置处理器,你需要指定计数器名称和增量。增量是增加或减少的数值。我已经按照以下截图所示配置了处理器:

Figure 9.9 – UpdateCounter 处理器的配置
当你配置了处理器并运行了数据管道后,你将有一个计数。在我运行它之前,162 条记录通过了数据管道。要查看你的计数器,点击 NiFi 窗口右上角的蜂巢菜单,并选择计数器,如图所示:

Figure 9.10 – NiFi 计数器
上述截图显示了计数器的计数和聚合值。如果我们有其他更新相同计数器的UpdateCounter处理器,它将聚合这些值。
使用 GUI 是快速查看你的处理器组和处理器运行情况的好方法。但你也可以使用处理器来监控数据管道。
在上一节中,你学习了关于 NiFi 公告板的内容。你可以使用后台任务来监控 NiFi,并通过报告任务将数据发布到 NiFi 公告板。报告任务就像在后台运行的处理器,执行任务。结果将显示在公告板中,或者你可以将其发送到其他位置。
要创建报告任务,在蜂巢菜单中选择控制器设置,然后导航到报告任务选项卡。列表应该是空的,但你可以在窗口右上角的加号处添加一个新任务。你将看到可用任务的列表。单击其中一个以查看描述。你应该会看到一个类似于以下截图的列表:

Figure 9.11 – NiFi 中可用的报告任务
对于这个示例,双击MonitorDiskUsage任务。它将出现在可编辑的列表中。点击铅笔图标进行编辑,将阈值设置为1%,并将目录设置为您的 NiFi 目录。配置将类似于以下截图:

Figure 9.12 – 配置 MonitorDiskUsage 任务
你可以使用百分比或 20 千兆字节这样的值。我将其设置为 1%,以便将其发布到公告板。我选择 NiFi 目录,因为它包含所有日志和存储库。
运行报告任务处理器,你应该在主 NiFi 窗口中看到一个公告板。消息将是MonitorDiskUsage任务超过了 1%的阈值。以下截图显示了公告板:

图 9.13 – 报告任务公告
您可以为许多其他指标创建报告任务。使用 GUI 很有用且方便,但您很可能无法整天坐在 NiFi 前观看。这将非常低效。更好的方法是将 NiFi 发送给您消息。您可以使用处理器来实现这一点。下一节将向您展示如何操作。
使用处理器监控 NiFi
而不是依赖于监视 NiFi GUI,您可以在数据管道中插入一个处理器来报告管道正在发生的事情。例如,您可以使用PutSlack处理器在失败或成功时发送消息。
要发送 Slack 消息,您需要在您的 Slack 工作区中创建一个应用。您可以通过浏览到api.slack.com/apps来完成此操作。点击创建新应用,如下面的截图所示:

图 9.14 – 创建新应用
Slack 将要求您为应用命名,然后选择一个工作区,如下面的截图所示:

图 9.15 – 为您的应用指定名称和工作区
完成后,您将被重定向到应用页面。在功能标题下,点击入站 Webhooks并开启它,如下面的截图所示:

图 9.16 – 激活入站 Webhooks
您将被要求选择一个用于 Webhook 的通道。我选择了自己,以便通道成为直接发给我的消息。您可以为数据管道创建一个通道,以便多个人可以加入并查看消息。完成此步骤后,滚动到页面底部以查看新的 Webhook。点击复制按钮并打开 NiFi。现在是时候将PutSlack添加到您的数据管道中。
在 NiFi 中,我打开了SCF处理器组。我找到了ElasticSCF处理器——将问题发送到 Elasticsearch 的处理器。将处理器图标从控制工具栏拖放到画布上,并选择PutSlack。为关系失败创建ElasticSCF和PutSlack之间的连接,如下面的截图所示:

图 9.17 – 将 PutSlack 添加到数据管道的末尾
要配置 PutSlack 处理器,将复制的 URL 粘贴到 Webhook URL 属性。NiFi 会隐藏 URL,因为它是一个敏感属性。Username 属性是你希望 Slack 在发送消息时显示的内容。你也可以设置一个图标或表情符号。Webhook Text 属性是将会发送的消息。你可以将消息设置为纯文本,说明处理器失败,或者因为 Webhook Text 属性接受 NiFi 表达式语言,你可以使用流文件属性和文本的组合。我已将处理器配置如下所示:


图 9.18 – PutSlack 配置
我使用了 NiFi 表达式语言的附加方法。该语句如下:
${id:append(': Record failed Upsert Elasticsearch')}
前面的语句获取 id 属性,${id}, 并调用 append,:append()。在 append() 方法内部是文本。结果将是一个像以下截图所示的消息:


图 9.19 – 来自 NiFi 的 Slack 直接消息
前面的截图是我的直接消息。你可以看到我已将 NiFi 集成到工作区,然后收到了来自 NiFi 的消息。消息是 SeeClickFix 问题的 ID 以及一些说明它失败的文本。我现在可以采取行动。
你可以使用处理器发送电子邮件、写入文件或执行许多其他监控数据管道的操作。你还可以使用 Python 在 NiFi 之外编写自己的监控应用程序。下一节将介绍 NiFi REST API。
使用 Python 和 NiFi REST API
使用 Python 和 NiFi REST API,你可以编写自己的监控工具,或者搭建仪表板。NiFi REST API 文档位于 nifi.apache.org/docs/nifi-docs/rest-api/index.html。你可以通过类型查看所有不同的端点以及它们的一些信息。本节将突出显示一些你在本章中通过 GUI 覆盖的端点。
我们可以首先查看系统诊断。系统诊断将显示你的资源使用情况。你可以看到堆大小、线程、存储库使用情况以及几个其他指标。要使用 requests 调用端点,你可以使用以下代码:
r=requests.get('http://localhost:9300/nifi-api/system-diagnostics')
data=r.json()
data['systemDiagnostics']['aggregateSnapshot']['maxHeap']
#'512 MB'
data['systemDiagnostics']['aggregateSnapshot']['totalThreads']
#108
data['systemDiagnostics']['aggregateSnapshot']['heapUtilization']
#'81.0%'
其他感兴趣的端点是处理器组。使用此端点,你可以找到任何处理器组的基本信息。你需要在 NiFi 中获取组 ID。你可以在 URL 中找到它作为 processGroupId 参数。有了它,你可以使用过程组端点,如下面的代码所示:
pg=requests.get('http://localhost:9300/nifi-api/process-groups/9466c3ca-4c6d-3884-ac72-af4a27919fb0')
pgdata=pg.json()
pgdata['component']['name']
#'SCF'
pgdata['status']
status 对象包含了你在状态工具栏中找到的大部分相关信息。输出如下:
{'id': '9466c3ca-4c6d-3884-ac72-af4a27919fb0', 'name': 'SCF', 'statsLastRefreshed': '16:11:16 MDT', 'aggregateSnapshot': {'id': '9466c3ca-4c6d-3884-ac72-af4a27919fb0', 'name': 'SCF', 'versionedFlowState': 'LOCALLY_MODIFIED', 'flowFilesIn': 0, 'bytesIn': 0, 'input': '0 (0 bytes)', 'flowFilesQueued': 6481, 'bytesQueued': 18809602, 'queued': '6,481 (17.94 MB)', 'queuedCount': '6,481', 'queuedSize': '17.94 MB', 'bytesRead': 0, 'read': '0 bytes', 'bytesWritten': 0, 'written': '0 bytes', 'flowFilesOut': 0, 'bytesOut': 0, 'output': '0 (0 bytes)', 'flowFilesTransferred': 0, 'bytesTransferred': 0, 'transferred': '0 (0 bytes)', 'bytesReceived': 0, 'flowFilesReceived': 0, 'received': '0 (0 bytes)', 'bytesSent': 0, 'flowFilesSent': 0, 'sent': '0 (0 bytes)', 'activeThreadCount': 0, 'terminatedThreadCount': 0}}
使用处理器端点,您可以具体查看单个处理器。您可以使用status对象查看状态栏信息,如下面的代码所示:
p=requests.get('http://localhost:9300/nifi-api/processors/8b63e4d0-eff2-3093-f4ad-0f1581e56674')
pdata=p.json()
pdata['component']['name']
#'Query SCF - Archive'
pdata['status']
使用 NiFi API,您甚至可以查看队列并下载 flowfiles。要获取 flowfile 的内容,您需要遵循以下步骤:
- 
向队列发起列表请求: q=requests.post('http://localhost:9300/nifi-api/flowfile-queues/295fc119-0172-1000-3949-54311cdb478e/listing-requests') qdata=q.json() listid=qdata['listingRequest']['id'] # '0172100b-179f-195f-b95c-63ea96d151a3'
- 
然后,您将通过传递请求( listid)来获取列表请求的状态:url="http://localhost:9300/nifi-api/flowfile-queues/295fc119-0172-1000-3949-54311cdb478e/listing-requests/"+listid ff=requests.get(url) ffdata=ff.json() ffid=ffdata['listingRequest']['flowFileSummaries'][0]['uuid'] #'3b2dd0fa-dfbe-458b-83e9-ea5f9dbb578f'
- 
最后,您将调用 flowfiles 端点,传递 flowfile ID( ffid),然后请求内容。flowfile 是 JSON 格式,因此结果将是 JSON:ffurl="http://localhost:9300/nifi-api/flowfile-queues/295fc119-0172-1000-3949-54311cdb478e/flowfiles/"+ffid+"/content" download=requests.get(ffurl) download.json()现在,您已经拥有了整个 flowfile 的内容: {'request_type': {'related_issues_url': 'https://seeclickfix.com/api/v2/issues?lat=35.18151754051&lng=-106.689667822892&request_types=17877&sort=distance', 'title': 'Missed Trash Pick Up', 'url': 'https://seeclickfix.com/api/v2/request_types/17877', 'organization': 'City of Albuquerque', 'id': 17877}, 'shortened_url': None, 'rating': 2, 'description': 'Yard waste in bags', 'created_at': '2020-05-08T17:15:57-04:00', 'opendate': '2020-05-08', 'media': {'image_square_100x100': None, 'image_full': None, 'video_url': None, 'representative_image_url': 'https://seeclickfix.com/assets/categories/trash-f6b4bb46a308421d38fc042b1a74691fe7778de981d59493fa89297f6caa86a1.png'}, 'private_visibility': False, 'transitions': {}, 'point': {'coordinates': [-106.689667822892, 35.18151754051], 'type': 'Point'}, 'updated_at': '2020-05-10T16:31:42-04:00', 'id': 7781316, 'lat': 35.18151754051, 'coords': '35.1815175405,-106.689667823', 'summary': 'Missed Trash Pick Up', 'address': '8609 Tia Christina Dr Nw Albuquerque NM 87114, United States', 'closed_at': '2020-05-08T17:24:55-04:00', 'lng': -106.689667822892, 'comment_url': 'https://seeclickfix.com/api/v2/issues/7781316/comments', 'reporter': {'role': 'Registered User', 'civic_points': 0, 'avatar': {'square_100x100': 'https://seeclickfix.com/assets/no-avatar-100-5e06fcc664c6376bbf654cbd67df857ff81918c5f5c6a2345226093147382de9.png', 'full': 'https://seeclickfix.com/assets/no-avatar-100-5e06fcc664c6376bbf654cbd67df857ff81918c5f5c6a2345226093147382de9.png'}, 'html_url': 'https://seeclickfix.com/users/347174', 'name': 'Gmom', 'id': 347174, 'witty_title': ''}, 'flag_url': 'https://seeclickfix.com/api/v2/issues/7781316/flag', 'url': 'https://seeclickfix.com/api/v2/issues/7781316', 'html_url': 'https://seeclickfix.com/issues/7781316', 'acknowledged_at': '2020-05-08T17:15:58-04:00', 'status': 'Archived', 'reopened_at': None}
- 
您可以通过发起 drop请求来清除队列:e=requests.post('http://localhost:9300/nifi-api/flowfile-queues/295fc119-0172-1000-3949-54311cdb478e/drop-requests') edata=e.json()
- 
您可以将列表请求 ID 传递到前面的 URL 末尾以查看它是否成功。或者,您可以打开 NiFi 并浏览到队列,您将看到它是空的。 
- 
您可以通过调用公告板端点来读取 NiFi 公告: b=requests.get('http://localhost:9300/nifi-api/flow/bulletin-board') bdata=b.json() bdata结果是一条单一的消息,表示我没有运行 Elasticsearch。输出如下: {'bulletinBoard': {'bulletins': [{'id': 2520, 'groupId': '9466c3ca-4c6d-3884-ac72-af4a27919fb0', 'sourceId': 'e5fb7c4b-0171-1000-ac53-9fd365943393', 'timestamp': '17:15:44 MDT', 'canRead': True, 'bulletin': {'id': 2520, 'category': 'Log Message', 'groupId': '9466c3ca-4c6d-3884-ac72-af4a27919fb0', 'sourceId': 'e5fb7c4b-0171-1000-ac53-9fd365943393', 'sourceName': 'ElasticSCF', 'level': 'ERROR', 'message': 'PutElasticsearchHttp[id=e5fb7c4b-0171-1000-ac53-9fd365943393] Routing to failure due to exception: Failed to connect to localhost/127.0.0.1:9200: java.net.ConnectException: Failed to connect to localhost/127.0.0.1:9200', 'timestamp': '17:15:44 MDT'}}], 'generated': '17:16:20 MDT'}}
- 
您还可以读取您之前创建的计数器。以下代码将向计数器端点发送 get请求:c=requests.get('http://localhost:9300/nifi-api/counters') cdata=c.json() cdata在下面的代码块中,您将看到我添加了一个额外的计数器: {'counters': {'aggregateSnapshot': {'generated': '17:17:17 MDT', 'counters': [{'id': '6b2fdf54-a984-38aa-8c56-7aa4a544e8a3', 'context': 'UpdateCounter (01721000-179f-195f-6715-135d1d999e33)', 'name': 'SCFSplit', 'valueCount': 1173, 'value': '1,173'}, {'id': 'b9884362-c70e-3634-8e53-f0151396be0b', 'context': "All UpdateCounter's", 'name': 'SCFSplit', 'valueCount': 1173, 'value': '1,173'}, {'id': 'fb06d19f-682c-3f85-9ea2-f12b090c4abd', 'context': "All UpdateCounter's", 'name': 'SCFtoElasticsearch', 'valueCount': 162, 'value': '162'}, {'id': '72790bbc-3115-300d-947c-22d889f15a73', 'context': 'UpdateCounter (295f179f-0172-1000-ee63-c25c545f224e)', 'name': 'SCFtoElasticsearch', 'valueCount': 162, 'value': '162'}]}}}
- 
最后,您还可以获取有关您报告任务的信息。您可以在公告板上查看结果,但此端点允许您查看其状态;在这种情况下,我已经将它们停止。以下代码显示了如何操作: rp=requests.get('http://localhost:9300/nifi-api/reporting-tasks/01721003-179f-195f-9cbe-27f0f068b38e') rpdata=rp.json() rpdata报告任务的信息如下: {'revision': {'clientId': '2924cbec-0172-1000-ab26-103c63d8f745', 'version': 8}, 'id': '01721003-179f-195f-9cbe-27f0f068b38e', 'uri': 'http://localhost:9300/nifi-api/reporting-tasks/01721003-179f-195f-9cbe-27f0f068b38e', 'permissions': {'canRead': True, 'canWrite': True}, 'bulletins': [], 'component': {'id': '01721003-179f-195f-9cbe-27f0f068b38e', 'name': 'MonitorDiskUsage', 'type': 'org.apache.nifi.controller.MonitorDiskUsage', 'bundle': {'group': 'org.apache.nifi', 'artifact': 'nifi-standard-nar', 'version': '1.12.1'}, 'state': 'STOPPED', 'comments': '', 'persistsState': False, 'restricted': False, 'deprecated': False, 'multipleVersionsAvailable': False, 'schedulingPeriod': '5 mins', 'schedulingStrategy': 'TIMER_DRIVEN', 'defaultSchedulingPeriod': {'TIMER_DRIVEN': '0 sec', 'CRON_DRIVEN': '* * * * * ?'}, 'properties': {'Threshold': '1%', 'Directory Location': '/home/paulcrickard/nifi-1.12.1', 'Directory Display Name': 'MyDrive'}, 'descriptors': {'Threshold': {'name': 'Threshold', 'displayName': 'Threshold', 'description': 'The threshold at which a bulletin will be generated to indicate that the disk usage of the partition on which the directory found is of concern', 'defaultValue': '80%', 'required': True, 'sensitive': False, 'dynamic': False, 'supportsEl': False, 'expressionLanguageScope': 'Not Supported'}, 'Directory Location': {'name': 'Directory Location', 'displayName': 'Directory Location', 'description': 'The directory path of the partition to be monitored.', 'required': True, 'sensitive': False, 'dynamic': False, 'supportsEl': False, 'expressionLanguageScope': 'Not Supported'}, 'Directory Display Name': {'name': 'Directory Display Name', 'displayName': 'Directory Display Name', 'description': 'The name to display for the directory in alerts.', 'defaultValue': 'Un-Named', 'required': False, 'sensitive': False, 'dynamic': False, 'supportsEl': False, 'expressionLanguageScope': 'Not Supported'}}, 'validationStatus': 'VALID', 'activeThreadCount': 0, 'extensionMissing': False}, 'operatePermissions': {'canRead': True, 'canWrite': True}, 'status': {'runStatus': 'STOPPED', 'validationStatus': 'VALID', 'activeThreadCount': 0}}
使用这些 NiFi 端点,您可以收集有关系统、流程组、处理器和队列的信息。您可以使用这些信息构建自己的监控系统或创建仪表板。API 具有很大的潜力——您甚至可以使用 NiFi 本身调用 API。
摘要
在本章中,您学习了如何使用 NiFi GUI 通过状态栏、公告板和计数器来监控您的数据管道。您还学习了如何添加可以向您数据管道内部发送信息的处理器。使用PutSlack处理器,您能够在出现故障时发送给自己直接消息,并且您使用 NiFi 表达式语言将 flowfile 中的数据传递到消息中。最后,您学习了如何使用 API 编写自己的监控工具并抓取与 NiFi GUI 中相同的数据——甚至可以读取单个 flowfile 的内容。
在下一章中,您将学习如何部署您的生产管道。您将学习如何使用处理器组、模板、版本和变量来允许您以最小配置将数据管道导入生产 NiFi 实例。
第十章:第十章:部署数据管道
在软件工程中,你通常会有开发、测试和生产环境。测试环境可能被称为质量控制、预发布或某些其他名称,但理念是相同的。你在某个环境中开发,然后将其推送到另一个环境,该环境将是生产环境的克隆,如果一切顺利,它将被推送到生产环境。在数据工程中,也使用相同的方法。到目前为止,你已经构建了数据管道并在单个机器上运行它们。在本章中,你将学习构建可以部署到生产环境的数据管道的方法。
在本章中,我们将涵盖以下主要主题:
- 
为生产环境最终确定你的数据管道 
- 
使用 NiFi 变量注册表 
- 
部署你的数据管道 
为生产环境最终确定你的数据管道
在最后几章中,你已经学习了创建生产数据管道的功能和方法。在你能够部署你的数据管道之前,还需要一些其他功能——背压、具有输入和输出端口的处理器组,以及漏斗。本节将带你了解这些功能的每一个。
背压
在你的数据管道中,每个处理器或任务完成所需的时间不同。例如,一个数据库查询可能返回数十万个结果,这些结果在几秒钟内被分割成单个流文件,但评估和修改流文件内属性的处理器可能需要更长的时间。将所有数据以比下游处理器实际处理速度更快的速度放入队列中是没有意义的。Apache NiFi 允许你控制发送到队列的流文件数量或数据大小。这被称为背压。
为了理解背压是如何工作的,让我们创建一个生成数据并将其写入文件的管道。数据管道如下截图所示:

图 10.1 – 生成数据并将流文件写入文件的管道
之前的数据管道 a 创建了GenerateFlowFile处理器和PutFile处理器之间的连接,用于成功关系。我已经配置了PutFile处理器将文件写入/home/paulcrickard/output。GenerateFlowFile处理器使用默认配置。
如果你只通过启动GenerateFlowFile处理器来运行数据管道,你会看到队列中有 10,000 个流文件,并且是红色的,如下面的截图所示:

图 10.2 – 填满 10,000 个流文件的完整队列
如果你刷新 NiFi,队列中流文件的数量不会增加。它有 10,000 个流文件,不能再容纳更多。但 10,000 是最大数量吗?
队列的配置方式与为其提供数据的处理器类似。右键点击队列并选择配置。选择设置选项卡,你将看到以下选项:

图 10.3 – 队列配置设置
你会注意到有10000个流文件和1 GB。GenerateFlowFile处理器将每个流文件的大小设置为 0 字节,因此对象阈值在大小阈值之前被触发。你可以通过更改GenerateFlowFile处理器来测试触发大小阈值。我已经将其更改为 50 MB。当我启动处理器时,队列现在停止在 21 个流文件,因为它已经超过了 1 GB 的数据。以下截图显示了完整的队列:

图 10.4 – 具有大小阈值的队列
通过调整对象阈值或大小阈值,你可以控制发送到队列的数据量,并创建反向压力以减缓上游处理器。虽然加载队列不会破坏你的数据管道,但如果数据流动更加均匀,它将运行得更加顺畅。
下一节将放大你的数据管道,并展示其他改进处理器组使用的技术。
改进处理器组
到目前为止,你已经使用了处理器组来保存单个数据管道。如果你要将所有这些数据管道推送到生产环境,你很快就会意识到每个处理器组中有很多处理器正在执行完全相同的任务。例如,你可能有多几个使用SplitJson的处理器后面跟着一个EvaluateJsonPath处理器,用于从流文件中提取 ID。或者,你可能有多几个将流文件插入到 Elasticsearch 的处理器。
你不会在代码中有几个执行相同操作但针对不同变量的函数;你会有一个接受参数的函数。同样的规则适用于数据处理管道,你通过使用具有输入和输出端口的处理器组来实现这一点。
为了说明如何将数据处理管道分解成逻辑部分,让我们通过一个例子来演示:
- 
在 NiFi 中创建一个处理器组,并将其命名为 Generate Data。
- 
在处理器组内部,将 GenerateFlowFile处理器拖到画布上。我已经设置了{"ID":123}。
- 
接下来,将一个输出端口拖到画布上。你将被提示选择 FromGeneratedData,并将发送到设置为本地连接。
- 
最后,将 GenerateFlowfile处理器连接到输出端口。你将在输出端口上收到一个警告,表明它无效,因为它没有出向连接。我们将在下一步中修复这个问题。
- 
退出处理器组。 
- 
创建一个新的处理器组,并将其命名为 Write Data。
- 
进入处理器组,将 EvaluateJsonPath处理器拖到画布上。通过创建一个值为$.{ID}的属性 ID 来配置它,并将目标属性设置为flowfile-attribute。
- 
接下来,将“更新属性”处理器拖动到画布上,并创建一个新的属性文件名,将其值设置为 ${ID}。
- 
现在,将 PutFile处理器拖动到画布上。设置/home/paulcrickard/output。
- 
最后,将一个输入端口拖动到画布上,并使其成为数据管道中的第一个处理器。完成的管道应如下截图所示: ![图 10.5 – 以输入端口开始的数据管道]() 图 10.5 – 以输入端口开始的数据管道 
- 
退出处理器组。现在,您应该在画布上看到两个处理器组——“生成数据”和“写入数据”。您可以像连接单个处理器一样连接这些处理器组。当您通过从“生成数据”拖动箭头到“写入数据”来连接它们时,您将提示选择要连接的端口,如下面的截图所示: ![图 10.6 – 连接两个处理器组]() 图 10.6 – 连接两个处理器组 
- 
默认值将适用,因为您只有一个输出端口和一个输入端口。如果您有更多,可以使用下拉菜单选择正确的端口。这就是将它们命名为除输入和输出之外的其他名称变得重要的地方。使名称具有描述性。 
- 
连接处理器组后,仅启动“生成数据”组。您将看到队列中充满了流文件。要了解端口的工作方式,进入“写入数据”处理器组。 
- 
仅启动传入数据输入端口。一旦开始运行,下游队列将充满流文件。 
- 
右键单击队列并选择“生成数据”处理器组。现在您可以启动其他处理器。 
- 
随着数据管道的运行,您将在输出目录中创建一个名为 123的文件。
您已成功使用输入和输出端口连接了两个处理器组。在生产环境中,现在您可以有一个单独的处理器组将数据写入文件,并且它可以接收任何需要写入数据的处理器组的数据,如下面的截图所示:

图 10.7 – 两个处理器组使用“写入数据”处理器组
在前面的数据管道中,我复制了“生成数据”,并配置了{"ID":456},并将运行计划设置为每小时一次,这样我就可以从每个处理器——生成数据和生成数据 2——只获取一个流文件。运行所有处理器组,您会列出队列并确认每个处理器组都来自一个流文件,并且您的输出目录现在有两个文件——123和456。
使用 NiFi 变量注册表
当您构建数据管道时,您正在硬编码变量——除了某些表达式语言,您可以从 flowfile 中提取数据。当您将数据管道移动到生产环境时,您需要更改数据管道中的变量,这可能既耗时又容易出错。例如,您将有一个与生产不同的测试数据库。当您将数据管道部署到生产环境时,您需要指向生产并更改处理器。或者,您可以使用变量注册表。
使用来自第四章**,与数据库一起工作*的postgresToelasticsearch处理器组,我将修改数据管道以使用 NiFi 变量注册表。作为提醒,数据管道如下所示:
![Figure 10.8 – A data pipeline to query PostgreSQL and save the results to Elasticsearch
![img/Figure_10.8_B15739.jpg]
图 10.8 – 一个查询 PostgreSQL 并将结果保存到 Elasticsearch 的数据管道
从处理器组外部,右键单击它并选择变量。要添加新变量,您可以点击加号并提供名称和值。这些变量现在与处理器组相关联。
就像编程中的函数一样,变量有作用域。处理器组中的变量是局部变量。您可以在 NiFi 画布上右键单击并创建一个变量,您可以考虑它在作用域上是全局的。我创建了两个局部变量elastic和index,以及一个全局变量elastic。当我打开组中的变量时,它看起来如下所示:
![Figure 10.9 – NiFi variable registry
![img/Figure_10.9_B15739.jpg]
图 10.9 – NiFi 变量注册表
在前面的屏幕截图中,您可以看到作用域。elastic的作用域,局部变量具有优先级。
您现在可以使用表达式语言引用这些变量。在PutElasticsearchHttp过程中,我设置了${elastic}和${index}。这些将填充为局部变量——http://localhost:9200和nifivariable。
运行数据管道,您可以在 Elasticsearch 中看到结果。现在有一个名为nifivariable的新索引和 1,001 条记录。以下屏幕截图显示了结果:
![Figure 10.10 – The new index, nifivariable, is the second row
![img/Figure_10.10_B15739.jpg]
图 10.10 – 新索引,nifivariable,是第二行
您现在已经完成了生产管道的收尾工作,并完成了部署所需的全部步骤。下一节将向您介绍不同的部署数据管道的方法。
部署您的数据管道
处理不同环境——开发、测试、生产——有许多方法,您如何选择取决于最适合您的业务实践。话虽如此,您采取的任何策略都应该涉及使用 NiFi 注册表。
使用最简单的策略
最简单的策略是在网络上运行 NiFi 并将画布分割成多个环境。当你提升了一个流程组时,你会将其移动到下一个环境。当你需要重建数据管道时,你会将其添加回开发环境并修改它,然后更新生产数据管道到最新版本。你的 NiFi 实例将如下截图所示:

图 10.11 – 单个 NiFi 实例作为 DEV、TEST 和 PROD 运行
注意在前面的截图中,只有PROD有一个绿色的勾选标记。DEV环境创建了处理器组,然后提交了更改,并将它们带入TEST。如果进行了任何更改,它们将被提交,并将最新版本带入PROD。为了以后改进数据管道,你会将最新版本带入DEV并重新开始,直到PROD也有最新版本。
虽然这样做也可以,但如果你有资源构建一个独立的 NiFi 实例,你应该这样做。
使用中间策略
中间策略利用 NiFi 注册表,但还添加了一个生产 NiFi 实例。我在另一台机器上安装了 NiFi,这台机器与本书中使用的机器不同,也在运行 NiFi 注册表——这也可以位于另一台机器上。
在启动我的新 NiFi 实例后,我添加了 NiFi 注册表,如下截图所示:

图 10.12 – 将 NiFi 注册表添加到另一个 NiFi 实例
在开发机器上,注册表使用 localhost 创建。然而,其他机器可以通过指定主机机的 IP 地址进行连接。读取后,NiFi 实例可以访问所有版本化的数据管道。
将处理器组拖到画布上并选择导入。你现在可以选中已提升到生产的处理器组,如下截图所示:

图 10.13 – 导入处理器组
一旦你导入了处理器,它将带有在开发环境中定义的变量。你可以覆盖变量的值。一旦你更改了变量,你就不需要再次这样做。你可以在开发环境中进行更改并更新生产环境,新变量将保留。更新的变量如下截图所示:

图 10.14 – 更新用于生产的本地变量
在开发环境中,你可以更改处理器并提交本地更改。现在,生产环境将显示有新版本可用,如下截图所示:

图 10.15 – 生产不再使用当前版本
你可以右键单击处理器组并选择新版本。以下截图显示了版本 2:

图 10.16 – 新版本
选择新版本后,生产环境现在是最新的。以下截图显示了生产环境。你可以右键单击处理器组以查看变量仍然指向生产值:

图 10.17 – 生产状态是最新的
这种策略应该适用于大多数用户的需求。在这个例子中,我使用了开发和生产环境,但你也可以添加 TEST 并在这里使用相同的策略,只需更改本地变量以指向你的测试数据库。
之前使用的策略使用了单个 NiFi 注册表,但你可以为每个环境使用一个注册表。
使用多个注册表
管理开发、测试和生产的一个更高级策略是使用多个 NiFi 注册表。在这个策略中,你会设置两个 NiFi 注册表——一个用于开发,一个用于测试和生产。你会将开发环境连接到开发注册表,将测试和生产环境连接到第二个注册表。
当你将数据管道提升到测试阶段时,管理员会使用 NiFi CLI 工具导出数据管道并将其导入到第二个 NiFi 注册表中。从那里,你可以对其进行测试并将其提升到开发阶段。你会将第二个注册表中的版本导入到生产环境中,就像在中端策略中所做的那样。这种策略使得错误处理变得更加困难,因为你不能在没有手动操作的情况下将数据管道提交到测试和生产环境。这是一个非常好的策略,但需要更多的资源。
摘要
在本章中,你学习了如何将数据管道最终化以部署到生产环境。通过使用特定任务的处理器组,就像代码中的函数一样,你可以减少处理器的重复。使用输入和输出端口,你将多个处理器组连接在一起。为了部署数据管道,你学习了如何使用 NiFi 变量来声明全局和局部作用域的变量。
在下一章中,你将使用在本节中学到的所有技能来创建和部署生产数据管道。
第十一章:第十一章: 构建生产数据管道
在本章中,您将使用本书本节中学到的功能和技巧来构建生产数据管道。数据管道将被分成执行单个任务的处理器组。这些组将进行版本控制,并且它们将使用 NiFi 变量注册表,以便它们可以在生产环境中部署。
在本章中,我们将介绍以下主要内容:
- 
创建测试和生产环境 
- 
构建生产数据管道 
- 
在生产环境中部署数据管道 
创建测试和生产环境
在本章中,我们将回到使用 PostgreSQL 进行数据的提取和加载。数据管道需要一个测试和生产环境,每个环境都将有一个临时表和仓库表。要创建数据库和表,您将使用PgAdmin4。
创建数据库
要使用 PgAdmin4,请执行以下步骤:
- 
浏览到 http://localhostw/pgadmin4/l,输入您的用户名和密码,然后点击登录按钮。登录后,展开左侧面板中的服务器图标。
- 
要创建数据库,右键点击数据库图标并选择 test。
- 
接下来,您需要添加表格。要创建临时表,右键点击 staging。然后,选择Columns选项卡。使用加号创建以下截图所示的字段:![图 11.1 – 临时表中使用的列![图 11.1 – Figure_11.1_B15739.jpg]() 图 11.1 – 临时表中使用的列 
- 
完成后保存表格。您需要为测试数据库创建此表一次,为生产数据库创建两次。为了节省时间,您可以使用CREATE Script来自动完成此操作。右键点击临时表,然后选择Scripts | CREATE Script,如图下所示:![图 11.2 – 生成 CREATE 脚本 ![图 11.2 – Figure_11.2_B15739.jpg]() 图 11.2 – 生成 CREATE 脚本 
- 
主屏幕将打开一个窗口,显示生成表的 SQL 语句。通过将名称从 staging更改为warehouse,您可以在测试环境中创建仓库表,这将与临时表相同。一旦您进行了更改,点击工具栏中的播放按钮。
- 
最后,右键点击 production。使用脚本创建这两个表。
现在您已经为测试和生产环境创建了表,您将需要一个数据湖。
填充数据湖
数据湖通常是一个磁盘上的存储文件的地方。通常,您会使用 Hadoop 的Hadoop 分布式文件系统(HDFS)和其他构建在 Hadoop 生态系统之上的工具来找到数据湖。在本章中,我们将在一个文件夹中放置文件来模拟从数据湖中读取的工作方式。
要创建数据湖,你可以使用 Python 和 Faker 库。在编写代码之前,创建一个文件夹作为数据湖。我在我的主目录中创建了一个名为 datalake 的文件夹。
要填充数据湖,你需要编写包含个人信息的 JSON 文件。这与你在本书第一部分编写的 JSON 和 CSV 代码类似。步骤如下:
- 
导入库,设置数据湖目录,并将 userid设置为1。userid变量将成为主键,因此我们需要它具有唯一性 – 增量将为我们完成这项工作:from faker import Faker import json import os os.chdir("/home/paulcrickard/datalake") fake=Faker() userid=1
- 
接下来,创建一个循环,生成包含用户 ID、姓名、年龄、街道、城市、州和邮编的虚构个人的数据对象。 fname变量包含一个人的首名和姓氏,中间没有空格。如果你有一个空格,Linux 将用引号包裹文件:for i in range(1000): name=fake.name() fname=name.replace(" ","-")+'.json' data={ "userid":userid, "name":name, "age":fake.random_int(min=18, max=101, step=1), "street":fake.street_address(), "city":fake.city(), "state":fake.state(), "zip":fake.zipcode() }
- 
最后,将 JSON 对象导出,然后将其写入以人为名的文件。关闭文件并让循环继续: datajson=json.dumps(data) output=open(fname,'w') userid+=1 output.write(datajson) output.close()
运行前面的代码,你将在数据湖中拥有 1,000 个 JSON 文件。现在你可以开始构建数据管道。
构建生产数据管道
你构建的数据管道将执行以下操作:
- 
从数据湖中读取文件。 
- 
将文件插入到临时存储中。 
- 
验证临时存储数据。 
- 
将临时存储移动到仓库。 
最终的数据管道将如下截图所示:

Figure 11.3 – 数据管道的最终版本
我们将按处理器组构建数据管道处理器组。第一个处理器组将读取数据湖。
读取数据湖
在本书的第一部分,你从 NiFi 读取文件,这里也将做同样的事情。这个处理器组将包括三个处理器 – GetFile、EvaluateJsonPath 和 UpdateCounter – 以及一个输出端口。将处理器和端口拖到画布上。在接下来的章节中,你将配置它们。
GetFile
GetFile 处理器从文件夹中读取文件,在这种情况下,我们的数据湖。如果你正在读取 Hadoop 中的数据湖,你需要将此处理器替换为 GetHDFS 处理器。要配置处理器,指定输入目录;在我的情况下,它是 /home/paulcrickard/datalake。确保 ^.*\.([jJ][sS][oO][nN]??)$。如果你保留默认值,它将正常工作,但如果文件夹中有其他文件,NiFi 将尝试抓取它们并失败。
EvaluateJsonPath
EvaluateJsonPath 处理器将从 JSON 中提取字段并将它们放入 flowfile 属性中。为此,将 Destination 属性设置为 flowfile-attribute。保留其余属性为默认值。使用加号,为 JSON 中的每个字段创建一个属性。配置如下截图所示:

Figure 11.4 – EvaluateJsonPath 处理器的配置
这就足够完成从数据湖读取的任务了,但我们将添加一个处理器用于监控。
更新计数器
此处理器允许您创建一个增量计数器。随着流文件通过,我们可以统计正在处理的文件数量。此处理器不会操纵或更改我们的数据,但将允许我们监控处理器组的进度。我们将能够看到通过处理器的 FlowFiles 数量。这比使用 GUI 显示更准确,但它只显示过去 5 分钟内的记录数量。要配置处理器,保留1并设置datalakerecordsprocessed。
要完成这一节的数据管道,将一个输出端口拖到画布上并命名为OutputDataLake。退出处理器组,右键单击,选择ReadDataLake,编写简短描述和版本注释,然后保存。
NiFi-Registry
我已创建了一个名为DataLake的新存储桶。要创建存储桶,您可以浏览到http://localhost:18080/nifi-registry/上的注册表。点击右下角的扳手,然后点击新建存储桶按钮。命名并保存存储桶。
第一个处理器组已完成。您可以在需要从数据湖读取数据时使用此处理器组。处理器组将向您提供每个字段提取的每个文件。如果数据湖发生变化,您只需修复这个处理器组即可更新所有数据管道。
在继续向下进行数据管道之前,下一节将稍微偏离一下,展示如何附加其他处理器组。
扫描数据湖
数据管道的目标是从数据湖读取数据并将数据放入数据仓库。但让我们假设我们公司里还有另一个部门需要监控某些人的数据湖——可能是 VIP 客户。你不必构建一个新的数据管道,只需将他们的任务添加到ReadDataLake处理器组中即可。
ScanLake处理器组有一个输入端口,该端口连接到ReadDataLake处理器的输出。它使用连接到EvaluateJsonPath处理器的ScanContent处理器,该处理器终止于PutSlack处理器,同时将数据发送到输出端口。流程如下面的截图所示:

图 11.5 – ScanLake 处理器组
上一章使用了PutSlack处理器,您已经熟悉了EvaluateJsonPath处理器。然而,ScanContent是一个新的处理器。ScanContent处理器允许您查看 flowfile 内容中的字段,并将它们与一个字典文件进行比较——一个包含您要查找的每行内容的文件。我已经在/home/paulcrickard/data.txt文件中放入了一个单独的名称。我通过将路径设置为字典文件属性的值来配置处理器。现在,当包含该名称的文件通过时,我将在 Slack 上收到一条消息。
将数据插入临时表
我们读取的数据来自数据湖,并且不会被删除,因此我们不需要采取任何中间步骤,例如将数据写入文件,就像数据来自事务数据库时那样。但我们将要做的是将数据放入一个临时表中,以确保在将其放入数据仓库之前一切按预期工作。仅要将数据插入临时表就需要一个处理器,PutSQL。
PutSQL
PutSQL处理器将允许您在数据库表中执行INSERT或UPDATE操作。处理器允许您在 flowfile 的内容中指定查询,或者您可以将查询硬编码为处理器中的一个属性。在这个例子中,我已经在SQL 语句属性中硬编码了查询,如下所示:
INSERT INTO ${table} VALUES ('${userid}', '${name}',${age},'${street}','${city}','${state}','${zip}');
上述查询从 flowfile 中获取属性并将它们传递到查询中,因此虽然它是硬编码的,但它将根据它接收到的 flowfiles 而变化。您可能已经注意到,您在所有的EvaluateJsonPath处理器中都没有使用${table}。我使用 NiFi 注册声明了一个变量,并将其添加到处理器组作用域中。对于这个测试环境,表的值将是staging,但当我们将数据管道部署到生产环境时,它将稍后更改。
您还需要添加一个Java 数据库连接(JDBC)池,这在本书的前几章中已经完成。您可以指定批量大小、要检索的记录数以及是否希望在失败时回滚。将失败时回滚设置为True是您在事务中创建原子性的方法。如果批处理中的单个 flowfile 失败,处理器将停止,其他任何操作都无法继续。
我已经将处理器连接到另一个UpdateCounter处理器。此处理器创建并更新InsertedStaging。当一切完成后,计数器应与datalakerecordsprocessor匹配。UpdateCounter处理器连接到名为OutputStaging的输出端口。
查询临时数据库
下一个处理器组用于查询阶段数据库。现在数据已经加载,我们可以查询数据库以确保所有记录实际上都已进入。您可以执行其他验证步骤或查询,以查看结果是否符合您的预期 – 如果您有数据分析师,他们将是定义这些查询的良好信息来源。在以下章节中,您将查询阶段数据库并根据是否符合您的标准来路由结果。
ExecuteSQLRecord
在上一个处理器组中,您使用了PutSQL处理器将数据插入到数据库中,但在这个处理器组中,您想要执行一个select查询。select查询如下所示:
select count(*) from ${table}
前面的查询被设置为可选 SQL select查询属性的值。${table}是分配给处理器组的 NiFi 变量注册表变量,其值为staging。您需要在处理器属性中定义一个 JDBC 连接和一个记录写入器。记录写入器是一个 JSON 记录集写入器。处理器的返回值将是一个包含一个字段 – count的 JSON 对象。这个处理器被发送到EvaluateJsonPath处理器以提取recordcount。然后,该处理器被发送到下一个处理器。
RouteOnAttribute
RouteOnAttribute处理器允许您使用表达式或值来定义一个流文件的去向。为了配置处理器,我已经设置了allrecords并将值设置为 NiFi 表达式,如下所示:
${recordcount:ge( 1000 )}
前面的表达式评估recordcount属性以查看它是否大于或等于 1,000。如果是,它将基于这个关系进行路由。我已经将输出附加到名为OutputQueryStaging的输出端口。
验证阶段数据
前一个处理器组进行了一些验证,您可以在那里停止。然而,Great Expectations 是一个处理验证的出色库。您在第七章**,生产管道功能中学习了关于 Great Expectations 的内容,但我会在这里快速再次介绍它。
要使用 Great Expectations,您需要创建一个项目文件夹。我在下面的代码片段中已经创建了它,并初始化了 Great Expectations:
mkdir staging
great_expectations init
您将被提示创建您的验证套件。选择关系数据库(SQL),然后Postgres,并提供所需信息。提示将类似于以下截图:

图 11.6 – 配置 Great Expectations 以与 PostgreSQL 协同工作
当它完成后,Great Expectations 将尝试连接到数据库。如果成功,它将提供您文档的 URL。由于表是空的,它不会创建一个非常详细的验证套件。您可以使用以下命令编辑套件:
great_expectations suite edit staging.validation
这将启动一个包含套件代码的 Jupyter 笔记本。我已经删除了一行,该行设置了行数在 0 到 0 之间,如下面的截图所示:

图 11.7 – 编辑 Great Expectations 套件
删除高亮显示的行后,运行笔记本中的所有单元格。现在您可以刷新您的文档,您将看到行数期望不再作为套件的一部分,如下面的截图所示:

图 11.8 – 套件的 Great Expectations 文档
现在套件已完成,你需要生成一个可以运行以启动验证的文件。使用以下命令使用 staging.validation 套件创建一个 tap 并输出 sv.py 文件:
great_expectations tap new staging.validation sv.py
现在您可以运行此文件以验证测试数据库的阶段表。
第一个处理器从连接到 QueryStaging 处理器组输出端口的输入端口接收流文件。它连接到一个 ExecuteStreamCommand 处理器。
ExecuteStreamCommand
ExecuteStreamCommand 将执行一个命令并监听输出,流式传输结果。由于 sv.py 文件只打印一行并退出,因此没有流,但如果你的命令有多个输出,处理器会捕获它们的所有输出。
要配置处理器,设置 sv.py 文件。
处理器连接到一个 EvaluateJsonPath 处理器,该处理器提取 $.result 并将其发送到 RouteOnAttribute 处理器。我已经配置了一个属性并赋予它值 pass:
${result:startsWith('pass')}
上述表达式检查结果属性以查看它是否与 pass 匹配。如果是这样,处理器将流文件发送到输出端口。
Insert Warehouse
你已经到达了最后一个处理器组 - ExecuteSQLRecord 和一个 PutSQL 处理器。
ExecuteSQLRecord
ExecuteSQLProcessor 在阶段表中执行选择操作。它有一个在 NiFi 变量注册表中定义的变量表,指向阶段。查询是一个 select * 查询,如下所示:
select * from ${table}
此查询是 SQL select 查询属性的值。您需要设置一个 Database Pooling Connection 服务和一个 Record Writer 服务。Record Writer 将是一个 JsonRecordSetWriter,您需要确保设置了 SplitText 处理器,该处理器连接到 EvalueJsonPath 处理器,它是来自 ReadDataLake 处理器组的直接副本,连接到最终的 PutSQL 处理器。
PutSQL
PutSQL处理器将staging表中的所有数据放入最终数据warehouse表。您可以配置批量大小和失败回滚属性。我已经将 SQL 语句属性设置为与它被插入到staging时相同,除了表变量已被更改为warehouse,我们在 NiFi 变量注册表中将其设置为warehouse。查询如下:
INSERT INTO ${warehouse} VALUES ('${userid}', '${name}',${age},'${street}','${city}','${state}','${zip}');
我已终止了所有关系的处理器,因为这标志着数据管道的结束。如果您启动所有处理器组,您将在staging和warehouse表中获得数据。您可以检查计数器以查看处理的记录数是否与插入的记录数相同。如果一切正常,您现在可以将数据管道部署到生产环境。
在生产中部署数据管道
在上一章中,您学习了如何将数据部署到生产环境,所以在这里我不会深入探讨,只是提供一个回顾。要将新的数据管道投入生产,请执行以下步骤:
- 
浏览到您的生产 NiFi 实例。我在本地主机上运行了另一个 NiFi 实例,端口号为 8080。
- 
将处理器组拖放到画布上并选择导入。选择您刚刚构建的处理器组的最新版本。 
- 
修改处理器组上的变量,使其指向数据库生产。表名可以保持不变。 
然后,您可以运行数据管道,您将看到数据已填充到生产数据库的staging和warehouse表中。
您刚刚构建的数据管道从数据湖读取文件,将它们放入数据库表,运行查询以验证表,然后将它们插入到仓库。您可以用几个处理器构建这个数据管道并完成,但当你为生产构建时,您需要提供错误检查和监控。在前期花时间正确构建您的数据管道将节省您在生产中遇到变化或故障时的大量时间。您将处于调试和修改数据管道的有利位置。
摘要
在本章中,你学习了如何构建和部署生产数据管道。你学习了如何创建 TEST 和 PRODUCTION 环境,并在 TEST 中构建了数据管道。你使用文件系统作为示例数据湖,并学习了如何从湖中读取文件以及如何监控它们在处理过程中的状态。本章不是教你将数据加载到数据仓库中,而是教你如何使用临时数据库来存储数据,以便在将其加载到数据仓库之前进行验证。使用 Great Expectations,你能够构建一个验证处理器组,该组将扫描临时数据库以确定数据是否已准备好加载到数据仓库中。最后,你学习了如何将数据管道部署到 PRODUCTION。掌握了这些技能,你现在可以完全构建、测试和部署生产批量数据管道。
在下一章中,你将学习如何构建 Apache Kafka 集群。使用 Kafka,你将开始学习如何处理数据流。这些数据通常是接近实时,与您目前一直在处理的批量处理相比。你将安装和配置集群,使其在单个机器上运行,或者如果你有多个设备,也可以在多个设备上运行。
第三部分:超越批处理——构建实时数据管道
在本节中,你将了解批处理(即你目前所做的工作)与流处理之间的区别。你将学习一套新的工具,这些工具允许你实时流式传输和处理数据。首先,你将学习如何构建一个 Apache Kafka 集群以流式传输实时数据。为了处理这些数据,你将使用一个你将构建和部署的 Apache Spark 集群。最后,你将学习两个更高级的 NiFi 主题——如何使用 MiNiFi 从物联网设备将数据流式传输到 NiFi,以及如何为获得更多处理能力而对 NiFi 进行集群化。
本节包含以下章节:
- 
第十二章,构建 Apache Kafka 集群 
- 
第十三章,使用 Kafka 进行流式数据处理 
- 
第十四章,使用 Apache Spark 进行数据处理 
- 
第十五章,实时边缘数据——Kafka、Spark 和 MiNiFi 
第十二章:第十二章:构建 Kafka 集群
在本章中,您将超越批量处理 – 在完整数据集上运行查询 – 并了解流处理中使用的工具。在流处理中,数据在查询时可能是无限的和不完整的。处理流数据的主要工具之一是 Apache Kafka。Kafka 是一个允许您将数据实时发送到主题的工具。这些主题可以被消费者读取并处理数据。本章将教您如何构建一个三节点 Apache Kafka 集群。您还将学习如何创建和发送消息(生产)以及从主题中读取数据(消费)。
在本章中,我们将涵盖以下主要主题:
- 
创建 ZooKeeper 和 Kafka 集群 
- 
测试 Kafka 集群 
创建 ZooKeeper 和 Kafka 集群
大多数关于运行可分布式应用程序的教程通常只展示如何运行单个节点,然后您会想知道如何在生产环境中运行。在本节中,您将构建一个三节点 ZooKeeper 和 Kafka 集群。它将在一台机器上运行。然而,我将每个实例分割到其自己的文件夹中,每个文件夹模拟一个服务器。在运行在不同服务器上的唯一修改就是将 localhost 更改为服务器 IP。
下一章将详细介绍 Apache Kafka 的主题,但到目前为止,了解 Kafka 是一个用于构建实时数据流的工具就足够了。Kafka 是在 LinkedIn 开发的,现在是一个 Apache 项目。您可以在 kafka.apache.org 上找到 Kafka。网站截图如下:

图 12.1 – Apache Kafka 网站
Kafka 需要另一个应用程序 ZooKeeper 来管理有关集群的信息,处理发现并选举领导者。您可以自己安装和构建 ZooKeeper 集群,但在此示例中,您将使用 Kafka 提供的 ZooKeeper 脚本。要了解更多关于 ZooKeeper 的信息,您可以在 zookeeper.apache.org 上找到它。网站截图如下:

图 12.2 – Apache ZooKeeper 网站
以下部分将指导您构建集群。
下载 Kafka 和设置环境
您可以从网站上的 wget 下载 Apache Kafka,从命令行运行以下命令:
Wget https://downloads.apache.org/kafka/2.5.0/kafka_2.12-2.5.0.tgz
tar -xvzf kafka_2.12-2.5.0.tgz 
之前的命令下载了当前的 Kafka 版本并将其提取到当前目录中。由于您将运行三个节点,您需要为 Kafka 创建三个单独的文件夹。使用以下命令创建目录:
cp kafka_2.12-2.5.0 kafka_1
cp kafka_2.12-2.5.0 kafka_2
cp kafka_2.12-2.5.0 kafka_3
现在,您将拥有三个 Kafka 文件夹。您还需要为 Kafka 的每个实例指定一个日志目录。您可以使用 mkdir 命令创建三个文件夹,如下所示:
mkdir logs_1
mkdir logs_2
mkdir logs_2
接下来,您需要为 ZooKeeper 创建一个 data 文件夹。创建该目录,然后使用 cd 进入,如下所示:
mkdir data
cd data
您将运行三个 ZooKeeper 实例,因此您需要为每个实例创建一个文件夹。您可以使用 mkdir 命令来完成,如下所示:
mkdir zookeeper_1
mkdir zookeeper_2
mkdir zookeeper_3
每个 ZooKeeper 实例需要一个 ID。它将寻找一个名为 myid 的文件,其中包含一个整数值。在每个文件夹中,创建相应的 myid 文件并设置正确的值。以下命令将创建该文件:
echo 1 > zookeeper_1/myid
echo 2 > zookeeper_2/myid
echo 3 > zookeeper_3/myid
您已完成配置 ZooKeeper 和 Kafka 的先决任务。现在您可以编辑两者的配置文件。下一节将指导您完成这个过程。
配置 ZooKeeper 和 Kafka
ZooKeeper 和 Kafka 的配置文件都位于 Kafka 目录下的 conf 文件夹中。由于您有三个 Kafka 目录,我将通过 Kafka_1 进行说明,步骤需要应用到其他每个目录。
从 ~/kafka_1/conf 目录,您需要编辑 zookeeper.properties 文件。您将编辑数据目录和服务器,以及添加属性。配置文件如下所示,其中修改的内容用粗体标出(完整文件请参考 GitHub 仓库):
# the directory where the snapshot is stored.
dataDir=/home/paulcrickard/data/zookeeper_1
# the port at which the clients will connect
clientPort=2181
# disable the per-ip limit on the number of connections since this is a non-production config
maxClientCnxns=0
# Disable the adminserver by default to avoid port conflicts.
# Set the port to something non-conflicting if choosing to enable this
admin.enableServer=false
# admin.serverPort=8080
tickTime=2000
initLimit=5
syncLimit=2
server.1=localhost:2666:3666
server.2=localhost:2667:3667
server.3=localhost:2668:3668
修改完成后,您可以保存文件。现在您需要修改 kafka_2 和 kafka_3 目录中的此文件。请注意,dataDir 设置将分别以 zookeeper_2 和 zookeeper_3 结尾。此外,端口号应递增到 2182 和 2183。其他所有内容都将保持不变。再次强调,您更改目录和端口号的唯一原因是可以在单台机器上运行三个服务器。在三个不同的服务器上,您将保持设置不变,只需将 localhost 更改为服务器的 IP 地址。
现在,ZooKeeper 已配置完毕,您可以配置 Kafka。在相同的 conf 目录中,打开 server.properties 文件。文件中用粗体标出了编辑内容(完整文件请参考 GitHub 仓库):
############################# Server Basics #############################
# The id of the broker. This must be set to a unique integer for each broker.
broker.id=1
############################# Socket Server Settings #############################
# The address the socket server listens on. It will get the value returned from 
# java.net.InetAddress.getCanonicalHostName() if not configured.
#   FORMAT:
#     listeners = listener_name://host_name:port
#   EXAMPLE:
#     listeners = PLAINTEXT://your.host.name:9092
listeners=PLAINTEXT://localhost:9092
############################# Log Basics #############################
# A comma separated list of directories under which to store log files
log.dirs=/home/paulcrickard/logs_1
############################# Zookeeper #############################
# Zookeeper connection string (see zookeeper docs for details).
# This is a comma separated host:port pairs, each corresponding to a zk
# server. e.g. "127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002".
# You can also append an optional chroot string to the urls to specify the
# root directory for all kafka znodes.
zookeeper.connect=localhost:2181,localhost:2182,localhost:2183
对于每个 Kafka 目录,您需要修改 server.properties 文件,使其具有 1、2 和 3 的代理 ID。您可以使用任何整数,但我会保持它们与文件夹名称相同。此外,您需要将监听器设置为 localhost:9092、localhost:9093 和 localhost:9094。log.dirs 属性将被设置为 log_1、log_2 和 log_3 文件夹中的每一个。所有三个配置的 zookeeper.connect 属性都将具有相同的值。
您已创建了模拟三个服务器所需的所有必要目录,并已配置了 ZooKeeper 和 Kafka。现在您可以开始启动集群。
启动 ZooKeeper 和 Kafka 集群
要运行服务器,您需要打开六个终端 – 您不能在后台运行它们。
Docker
您可以使用 Docker Compose 来运行多个容器,并通过单个文件启动所有内容。容器是一个出色的工具,但超出了本书的范围。
在前三个终端中,您将启动 ZooKeeper 集群。在每个终端中,输入每个实例的 Kafka 文件夹。运行以下命令:
bin/zookeeper-server-start.sh config/zookeeper.properties 
当您启动所有服务器时,服务器将寻找彼此并举行选举,因此会有大量文本滚动。一旦它们连接,文本将停止,集群将准备就绪。
要启动 Kafka 集群,在每个剩余的三个终端中输入 kafka 目录的一个实例。然后,您可以在每个终端中运行以下命令:
bin/kafka-server-start.sh config/server.properties
当您完成时,每个终端都应该有一行看起来像以下这样的行:
INFO [ZookeeperClient Kafka server] Connected. (kafka.zookeeper.zookeeperClient)
现在,您有两个由三个节点组成的集群正在运行,分别用于 ZooKeeper 和 Kafka。为了测试集群并确保一切正常工作,下一节将创建一个主题、一个消费者和一个生产者,并发送一些消息。
测试 Kafka 集群
Kafka 附带了一些脚本,允许您从命令行执行一些基本功能。要测试集群,您可以创建一个主题,创建一个生产者,发送一些消息,然后创建一个消费者来读取它们。如果消费者可以读取它们,则您的集群正在运行。
要创建一个主题,从您的 kafka_1 目录运行以下命令:
bin/kafka-topics.sh --create --zookeeper localhost:2181,localhost:2182,localhost:2183 --replication-factor 2 --partitions 1 --topic dataengineering
上述命令使用 create 标志运行 kafka-topics 脚本。然后指定 ZooKeeper 集群的 IP 地址和主题。如果主题已创建,终端将打印以下行:
created topic dataengineering
您可以通过使用相同的脚本并带有 list 标志来列出 Kafka 集群中的所有主题来验证这一点:
bin/kafka-topics.sh –list --zookeeper localhost:2181,localhost:2182,localhost:2183
结果应该是一行:dataengineering。现在您已经有一个主题,您可以在其上发送和接收消息。下一节将向您展示如何操作。
使用消息测试集群
在接下来的章节中,您将使用 Apache NiFi 和 Python 来发送和接收消息,但为了快速测试集群,您也可以使用提供的脚本来完成这项工作。要创建一个生产者,请使用以下命令:
bin/kafka-console-producer.sh --broker-list localhost:9092,localhost:9093,localhost:9094 --topic dataengineering
上述命令使用带有 broker-list 标志的 kafka-console-producer 脚本,该标志传递 kafka 集群服务器。最后,它接受一个主题,因为我们只有一个,所以它是 dataengineering。当它准备好时,您将有一个 > 提示符,可以输入消息。
要读取消息,您需要使用 kafka-console-consumer 脚本。命令如下所示:
bin/kafka-console-consumer.sh --zookeeper localhost:2181,localhost:2182,localhost:2183 --topic dataengineering –from-beginning
消费者传递带有服务器列表的 zookeeper 标志。它还指定了主题和 from-beginning 标志。如果您已经读取了消息,您可以指定带有最后一条消息索引的 offset 标志,这样您就可以从您的最后位置开始。
将生产者和消费者终端并排放置,您应该会有以下截图类似的东西:

图 12.3 – 生产者和消费者
在前面的屏幕截图中,你会注意到我两次输入了“第一条消息”和“第二条消息”。当消费者启动时,它会读取该主题上的所有消息。一旦它读取完毕,它将等待新的消息。如果你在生产者那里输入一条消息,它将在短暂的延迟后出现在消费者窗口中。
现在,你已经拥有了一个完全功能化的 Kafka 集群,并准备好在下一章中继续使用 NiFi 和 Python 进行流处理。
摘要
在本章中,你学习了如何创建一个 Kafka 集群,这需要创建一个 ZooKeeper 集群。虽然你是在一台机器上运行所有实例,但你采取的步骤也适用于不同的服务器。Kafka 允许创建实时数据流,并且需要与之前你进行的批量处理不同的思维方式。
下一章将深入解释流中涉及的概念。你还将学习如何在 NiFi 和 Python 中处理流。
第十三章:第十三章:使用 Apache Kafka 进行流数据
Apache Kafka 打开了实时数据流的世界。虽然流处理和批处理之间存在基本差异,但你构建数据管道的方式将非常相似。理解流数据与批处理之间的差异将使你能够构建考虑这些差异的数据管道。
在本章中,我们将涵盖以下主要主题:
- 
理解日志 
- 
理解 Kafka 如何使用日志 
- 
使用 Kafka 和 NiFi 构建数据管道 
- 
区分流处理和批处理 
- 
使用 Python 进行生产和消费 
理解日志
如果你编写过代码,你可能熟悉软件日志。软件开发者使用日志将应用程序的输出写入文本文件以存储软件内部发生的事件。然后,他们使用这些日志来帮助调试出现的任何问题。在 Python 中,你可能实现了类似于以下代码的代码:
import logging
logging.basicConfig(level=0,filename='python-log.log', filemode='w', format='%(levelname)s - %(message)s')
logging.debug('Attempted to divide by zero')
logging.warning('User left field blank in the form')
logging.error('Couldn't find specified file')
前面的代码是一个基本的日志记录示例,它将不同的级别——debug、warning和error——记录到名为python-log.log的文件中。该代码将产生以下输出:
DEBUG - Attempted to divide by zero
WARNING - User left field blank in the form
ERROR - Couldn't find specified file
消息按照它们发生的顺序记录到文件中。然而,你并不知道事件发生的确切时间。你可以通过添加时间戳来改进这个日志,如下面的代码所示:
logging.basicConfig(level=0,filename='python-log.log', filemode='w', format='%(asctime)s - %(levelname)s - %(message)s')
logging.info('Something happened')
logging.info('Something else happened, and it was bad')
前面代码的结果显示在下面的代码块中。注意现在有一个时间戳。日志是有序的,就像之前的日志一样。然而,在这个日志中,确切的时间是已知的:
2020-06-21 10:55:40,278 - INFO - Something happened
2020-06-21 10:55:40,278 - INFO - Something else happened, and it was bad
在前面的日志中,你应该注意到它们遵循一个非常特定的格式,或者说是模式,这个模式在basicConfig中定义。另一个你可能熟悉的常见日志是web日志。
Web 服务器日志类似于软件日志;它们按时间顺序报告事件——通常是请求——包括时间戳和事件。这些日志遵循一个非常特定的格式,并且有许多工具可用于解析它们。数据库也使用日志来帮助复制并记录事务中的修改。
如果应用程序、Web 服务器和数据库都使用日志,并且它们都有所不同,那么日志究竟是什么?
日志是有序事件或记录的集合,是只追加的。
这就是全部内容。简单。直接。然而,在软件开发和数据处理中却是一个极其强大的工具。以下图表显示了一个示例日志:

图 13.1 – 日志的一个示例
上述图显示了单个记录作为块。第一个记录在左侧。时间由记录在日志中的位置表示。另一个记录右侧的记录较新。因此,记录3比记录2新。记录不会被从日志中删除,而是追加到末尾。记录9被添加到日志的右侧,因为它是最新的记录——直到记录 10 出现。
理解 Kafka 如何使用日志
Kafka 维护由生产者写入并由消费者读取的日志。以下章节将解释主题、消费者和生产者。
主题
Apache Kafka 使用日志来存储数据——记录。Kafka 中的日志被称为dataengineering。主题被保存为日志文件存储在磁盘上。主题可以是一个单独的日志,但通常它们会水平扩展到分区。每个分区是一个日志文件,可以存储在另一台服务器上。在具有分区的主题中,消息顺序保证不再适用于主题,而只适用于每个分区。以下图显示了将主题分割成三个分区的情况:

图 13.2 – 具有三个分区的 Kafka 主题
上述主题——事务——有三个标记为P1、P2和P3的分区。在每个分区内部,记录是有序的,左侧的记录比右侧的记录旧——框中的数字越大,记录越新。你会注意到记录在P1中有K:A,在P2和P3中分别有K:B和K:C。那些是与记录关联的键。通过分配键,你可以保证包含相同键的记录将进入同一个分区。虽然主题中记录的顺序可能是不规则的,但每个分区是有序的。
Kafka 生产者和消费者
Kafka 生产者将数据发送到主题和分区。记录可以轮询发送到分区,或者你可以使用键将数据发送到特定的分区。当你使用生产者发送消息时,你可以以三种方式之一进行:
- 
发送后即忘:你发送一条消息并继续操作。你不需要等待 Kafka 的确认。在此方法中,记录可能会丢失。 
- 
同步:发送一条消息,并在继续之前等待响应。 
- 
异步:发送一条消息和一个回调。消息发送后,你可以继续操作,但会在某个时候收到一个你可以处理的响应。 
生产者相当直接——它们向主题和分区发送消息,可能请求确认,如果消息失败则重试——或者不重试——然后继续。然而,消费者可能更复杂一些。
消费者从主题中读取消息。消费者在一个无限循环的轮询中运行,等待消息。消费者可以从开始处读取——它们将从主题中的第一条消息开始读取整个历史记录。一旦追上,消费者将等待新消息。
如果消费者读取五条消息,偏移量是五。偏移量是消费者在主题中的位置。它是消费者停止读取的位置的标记。消费者可以从偏移量或指定的偏移量开始读取主题,该偏移量由 Zookeeper 存储。
当你在 dataengineering 主题上有消费者,但主题有三个分区且写入记录的速度比你读取的速度快时,会发生什么?以下图显示了一个消费者试图消费三个分区:

图 13.3 – 单个消费者读取多个分区
使用消费者组,您可以扩展 Kafka 主题的读取。在前面的图中,消费者 C1 在一个消费者组中,但它是唯一的消费者。通过添加额外的消费者,可以将主题进行分配。以下图显示了这种情况:

图 13.4 – 消费者组中的两个消费者消费三个分区
前面的图显示,组中的分区数仍然多于消费者数,这意味着一个消费者将处理多个分区。您可以根据以下图添加更多消费者:

图 13.5 – 消费者数量多于分区数导致一个空闲
在前面的图中,消费者组中的消费者数量多于分区数。当消费者数量多于分区数时,消费者将处于空闲状态。因此,没有必要创建多于分区数的消费者。
然而,您可以创建多个消费者组。以下图显示了这种情况:

图 13.6 – 多个消费者组从单个主题读取
多个消费者组可以读取同一个分区。为每个需要访问主题的应用程序创建一个消费者组是良好的实践。
现在你已经了解了与 Kafka 一起工作的基础知识,下一节将向你展示如何使用 NiFi 和 Kafka 构建数据管道。
使用 Kafka 和 NiFi 构建数据管道
要使用 Apache Kafka 构建数据管道,您需要创建一个生产者,因为我们没有可连接的生产 Kafka 集群。在生产者运行时,您可以像读取其他文件或数据库一样读取数据。
Kafka 生产者
Kafka 生产者将利用来自第十一章**,项目 — 构建生产数据管道的生产数据管道。生产者数据管道所做的只是将数据发送到 Kafka 主题。以下截图显示了完成的生产者数据管道:

Figure 13.7 – NiFi 数据管道
要创建数据管道,请执行以下步骤:
- 
打开终端。在 NiFi 中向其发送消息之前,您需要先创建主题。输入以下命令: 3. This will allow you to test using Consumer Groups in the next section.
- 
按照以下截图所示,将输入端口拖动并连接到 ReadDataLake处理器组的输出:![Figure 13.8 – 连接输入端口到输出端口![Figure 13.8_B15739.jpg]() Figure 13.8 – 连接输入端口到输出端口 
- 
接下来,将 ControlRate处理器拖放到画布上。ControlRate处理器将允许我们比仅使用队列中的背压有更多控制地减慢数据流。这将使数据看起来是流式传输到 Kafka 主题,而不是一次性全部存在。如果您一次性写入所有内容,一旦读取完毕,管道将停止,直到您添加更多数据。
- 
要配置 ControlRate处理器,设置flowfile count。设置为1。这两个属性允许您指定通过的数据量。由于您使用了 flowfile count,最大速率将是一个整数,表示允许通过的 flowfile 数量。如果您使用了默认选项,则应设置为1 MB。最后,在 Time Duration 属性中指定允许最大速率通过的频率。我将其设置为 1 分钟。每分钟,将有一个 flowfile 发送到用户的 Kafka 主题。
- 
要将数据发送到 Kafka,将 PublishKafka_2_0处理器拖放到画布上。有多个 Kafka 处理器用于不同版本的 Kafka。要配置处理器,您将设置localhost:9092、localhost:9093和localhost:9094。Kafka broker 是 Kafka 服务器。由于您正在运行一个集群,您将输入所有 IP 地址作为逗号分隔的字符串——就像您在前一章的命令行示例中所做的那样。输入 Topic Name 作为用户和 Delivery Guarantee 属性为 Guarantee Replication Delivery。
您现在已在 NiFi 中配置了 Kafka 生成器,它将从 ReadDataLake 获取输出,并以一分钟间隔将每条记录发送到 Kafka。要读取主题,您将创建一个 NiFi 数据管道。
Kafka 消费者
作为数据工程师,您可能需要或不需设置 Kafka 集群和生成器。然而,正如您在本书的开始所学的,数据工程师的角色差异很大,构建 Kafka 基础设施可能是您工作的一部分。Kafka 在主题上接收消息后,是时候读取这些消息了。
完成后的数据管道如图所示:


Figure 13.9 – Consuming a Kafka topic in NiFi
要创建数据管道,请执行以下步骤:
- 
将 ConsumeKafka_2_0处理器拖放到画布上。要配置处理器,将 Kafka 代理设置为您的集群——localhost:9092、localhost:9093和localhost:9094。设置users和Earliest。最后,设置NiFi Consumer。Group ID属性定义了消费者(处理器)将成为其一部分的消费者组。
- 
接下来,我已经将 ControlRate处理器添加到画布上。ControlRate处理器将减慢对主题上已有记录的读取速度。如果主题不是太大,您可以使用队列上的背压,这样一旦您处理了历史数据,新的记录将实时移动。
- 
要配置 ControlRate处理器,设置flowfile count为1,1分钟。
- 
在画布上添加一个输出端口并命名它。我将其命名为 OutputKafkaConsumer。这将允许您将此处理器组连接到其他处理器组以完成数据处理管道。
- 
启动处理器组,您将每分钟看到处理记录。您正在使用单个消费者在一个消费者组中读取 Kafka 主题。如果您还记得,当您创建主题时,您将分区数设置为三个。因为有多个分区,您可以向组中添加更多消费者。为此,您只需添加另一个 ConsumeKafka_2_0处理器并对其进行配置。
- 
将另一个 ConsumeKafka_2_0处理器拖放到画布上。使用相同的 Kafka 代理进行配置——localhost:9092、localhost:9093和localhost:9094——以及相同的主题——users。设置NiFi Consumer。
- 
现在两个处理器具有相同的组 ID,它们将成为同一个消费者组的成员。在两个消费者和三个分区的情况下,一个消费者将读取两个分区,另一个将读取一个。如果主题正在流式传输大量数据,您可以添加另一个 ConsumeKafka_2_0处理器,但超过三个将处于空闲状态。
新的数据管道在以下屏幕截图中显示:
![图 13.10 – 在消费者组中使用多个消费者消费 Kafka
![img/Figure_13.10_B15739.jpg]
图 13.10 – 在消费者组中使用多个消费者消费 Kafka
运行处理器组,您将开始看到记录通过两个ConsumerKafka_2_0处理器流动。生产者的配置将决定哪些分区记录被发送以及有多少将通过您的消费者。由于默认设置和消费者数量,您可能会看到每个由另一个消费者处理的 flowfile,只有一个消费者处理两个 flowfile。
正如您可以向消费者组添加更多消费者一样,您可以让多个消费者组读取一个主题——消费者组的数量与分区数量没有任何关系。
要将另一个消费者组添加到数据管道中,拖放另一个 ConsumeKafka_2_0 处理器。设置 localhost:9092、localhost:9093 和 localhost:9094,并设置 users。组 ID 是消费者组的名称。将其设置为除 NiFi Consumer 之外的其他任何名称 – 因为这个消费者组已经存在。我将其设置为 NiFi Consumer2 – 几乎没有创意或原创性,但它完成了工作。现在数据管道将看起来像以下屏幕截图:

图 13.11 – NiFi 中的两个消费者组
在前面的屏幕截图中,您会注意到第二个消费者组上没有 ControlRate 处理器。一旦启动,处理器将消费主题的全部历史记录并将记录发送到下游。主题中有 17 条记录。其他队列要小得多,因为主题正在被节流。
您现在可以将处理器组连接到任何其他处理器组,以创建一个从 Apache Kafka 读取的数据管道。在下面的屏幕截图中,我已经将 ReadKafka 处理器组连接到来自 第十一章 的生产数据管道 – 项目 – 构建生产数据管道:

图 13.12 – 完成的数据管道
与从数据湖读取数据不同,新的数据管道从 Kafka 主题 users 读取数据。记录被发送到临时处理器组以继续数据管道。最终结果是 PostgreSQL 生产表将包含 Kafka 主题的所有记录。现在读取数据湖的操作变成了 Kafka 生产者。
在 NiFi 中创建生产者和消费者只需要使用单个处理器 – PublishKafka 或 ConsumeKafka。配置取决于您将发布到或从中读取的 Kafka 集群。在 NiFi 中,Kafka 只是一个数据输入。一旦接收数据,您处理数据的方式将与运行数据库查询时没有区别。您必须考虑数据性质的一些差异,下一节将讨论它们。
区分流处理和批处理
虽然处理工具不会改变您是处理流还是批处理,但在处理流时,您应该注意两点 – 无限 和 时间。
数据可以是有限或无限的。有限数据有一个终点,而无限数据是不断创建的,可能是无限的。有限数据是去年的小部件销售额。无限数据是高速公路上交通传感器的汽车计数和记录它们的速度。
为什么这在构建数据管道时很重要?因为对于有界数据,你会知道关于数据的一切。你可以一次性看到所有内容。你可以查询它,将其放入临时环境,然后运行 Great Expectations 来了解范围、值或其他指标,这些指标可以在处理数据时用于验证。
对于无界数据,它是流式进入的,你不知道下一份数据将是什么样子。这并不意味着你不能验证它 – 你知道一辆车的速度必须在某个范围内,并且不能有值 h – 它将是一个介于 0 和 200 左右的整数。
在有界数据上,你可以查询字段的平均值或最大值。在无界数据上,你需要随着数据流通过数据管道时不断重新计算这些值。在下一章中,你将学习 Apache Spark 以及它是如何帮助处理无界或流数据的。
你可能会想,是的,去年的销售额是有界的,但今年的不是。一年还没有结束,数据仍在流式传输。这引出了你在处理流时应注意的第二件事,那就是时间。有界数据是在一个时间段或窗口内完成的。而窗口化是将无界数据有界化的方法。
常见的窗口有三种 – 固定、滑动和会话:
- 固定 – 有时称为滚动窗口,这些是覆盖固定时间且记录不重叠的窗口。如果你指定一个一分钟窗口,记录将落在每个间隔内,如下面的图表所示:

图 13.13 – 固定窗口中的数据
- 滑动 – 这是一个窗口,其中窗口被定义,例如 1 分钟,但下一个窗口在窗口长度内开始 – 比如,每 30 秒开始一次。这种窗口会有重复,适合滚动平均值。以下图表显示了滑动窗口:

图 13.14 – 滑动窗口中的数据
图表显示了两个窗口,一个从 0 开始,持续 1 分钟。第二个窗口在 0:30 处重叠,持续 1 分钟直到 1:30。
- 会话 – 会话不会有一个相同的时间窗口,但它们是事件。例如,一个用户登录购物,他们的数据会在这个登录会话中流式传输,会话由记录中的某些数据定义,称为会话令牌。
当讨论窗口和时间时,你也必须考虑使用什么时间 – 事件、摄取或处理。三个不同的时间可能具有不同的值,而你选择哪一个取决于你的用例:
- 
事件时间是事件发生的时间。这可能在发送到 Kafka 之前被记录在记录中。例如,在 1:05,一辆车被记录以 55 英里/小时的速度行驶。 
- 
摄入时间是数据记录在 Kafka 主题中的时间。事件与记录之间的延迟可能会根据网络延迟而波动。 
- 
处理时间是你从 Kafka 主题中读取数据并对其进行操作(例如,通过你的数据管道处理并将其放入仓库)的时间。 
通过认识到你可能正在处理无界数据,你将通过一次不尝试分析所有数据,而是通过选择适当的窗口以及使用适合你用例的正确时间来避免你的数据管道中的问题。
使用 Python 进行生产和消费
你可以使用 Python 为 Kafka 创建生产者和消费者。有多个 Kafka Python 库 – Kafka-Python、PyKafka 和 Confluent Python Kafka。在本节中,我将使用 Confluent Python Kafka,但如果你想要使用基于开源和社区的库,你可以使用 Kafka-Python。无论你选择哪个库,Python 程序的原理和结构都将相同。
要安装库,你可以使用pip。以下命令将安装它:
pip3 install confluent-kafka
一旦库安装完成,你就可以通过将其导入到你的应用程序中来使用它。接下来的部分将介绍如何编写生产者和消费者。
在 Python 中编写 Kafka 生产者
要在 Python 中编写生产者,你需要创建一个生产者、发送数据并监听确认。在之前的示例中,你使用了Faker来创建关于人的假数据。你将再次使用它来生成本例中的数据。要编写生产者,执行以下步骤:
- 
导入所需的库并创建一个 faker: from confluent_kafka import Producer from faker import Faker import json import time fake=Faker()
- 
接下来,通过指定你的 Kafka 集群的 IP 地址来创建生产者: p=Producer({'bootstrap.servers':'localhost:9092,localhost:9093,localhost:9094'})
- 
你可以列出可用于发布的主题如下: p.list_topics().topics
- 
对于确认及其处理有不同的设置,但就目前而言,创建一个将接收错误( err)和确认(msg)的回调函数。在每次调用中,只有其中一个将是真实的并且有数据。使用if语句检查是否存在错误,否则可以打印消息:0, 1, 2) and then the value of the message. The messages come back as bytes so you can decode them to utf-8.
- 
接下来,创建生产者循环。代码通过一个范围创建一个假数据对象。该对象与第三章**,与文件一起工作相同。然后它将字典导出,以便可以将其发送到 Kafka: for i in range(10): data={'name':fake.name(),'age':fake.random_ int(min=18, max=80, step=1),'street':fake. street_address(),'city':fake.city(), 'state':fake.state(),'zip':fake.zipcode()} m=json.dumps(data)
- 
在将数据发送到 Kafka 之前,调用 poll()以获取之前消息的任何确认。这些确认将被发送到回调函数(receipt)。现在你可以调用produce()并传递主题名称、数据和发送确认的函数:p.poll(0) p.produce('users',m.encode('utf-8'),callback=receipt)
- 
要完成,刷新生产者。这将获取任何现有的确认并将它们发送到 receipt():p.flush()
上述代码的结果将是发送到 Kafka 集群中user主题的消息,终端将打印确认,如下所示:
2020-06-22 15:29:30 : Message on topic users on partition 1 with value of {'name': 'Willie Chambers', 'age': 66, 'street': '13647 Davis Neck Suite 480', 'city': 'Richardside', 'state': 'Nebraska', 'zip': '87109'}
现在你已经可以向 Kafka 主题发送数据,下一节将展示如何消费它。
在 Python 中编写 Kafka 消费者
要在 Python 中创建消费者,你需要创建指向 Kafka 集群的消费者,选择一个要监听的主题,然后进入一个循环,监听新消息。下面的代码将指导你如何编写 Python 消费者:
- 
首先,导入 Consumer库并创建消费者。你需要传递 Kafka 集群的 IP 地址、消费者组名称——这可以是任何你想要的名字,但如果你要将多个消费者添加到该组,它们需要相同的名称,并且 Kafka 会记住这个消费者组停止读取主题的位置——最后,你需要传递偏移量重置,或者你想从哪里开始读取:from confluent_kafka import Consumer c=Consumer({'bootstrap.servers': 'localhost:9092,localhost:9093,localhost9093','group.id':'python-consumer','auto.offset.reset':'earliest'})
- 
你可以获取可以订阅的主题列表以及特定主题的分区数: c.list_topics().topics t.topics['users'].partitions
- 
一旦你知道你想要消费哪个主题,你就可以订阅它: c.subscribe(['users'])
- 
要接收消息,创建一个无限循环——如果你想要永远监听。你可以始终使用偏移量来开始和停止,以便从上次离开的地方继续。调用 poll()来获取消息。结果将是以下三种情况之一——还没有,错误,或消息。使用if语句检查没有,错误,或解码消息并对数据进行处理,在这种情况下是打印它。当你完成时,关闭连接:while True: msg=c.poll(1.0) #timeout if msg is None: continue if msg.error(): print('Error: {}'.format(msg.error())) continue data=msg.value().decode('utf-8') print(data) c.close()
结果将在终端中滚动显示几个 JSON 对象,如下所示:
{'name': 'Joseph Vaughn', 'age': 39, 'street': '978 Jordan Extensions Suite 684', 'city': 'Zunigamouth', 'state': 'Michigan', 'zip': '38090'}
这是一个使用 Python 消费主题的基本示例,但应该能给你一个架构和如何开始构建更复杂消费者的想法。
摘要
在本章中,你学习了 Apache Kafka 的基础知识——从日志是什么以及 Kafka 如何使用它,到分区、生产者和消费者。你学习了 Apache NiFi 如何使用单个处理器创建生产者和消费者。本章简要介绍了流数据的无界性以及时间和窗口如何与流一起工作。这些是在处理流数据时的重要考虑因素,如果你假设你一次就有所有数据,可能会导致错误。最后,你学习了如何使用 Confluent Python Kafka 在 Python 中编写基本的生产者和消费者。
拥有这些技能,下一章将展示如何构建实时数据管道。
第十四章:第十四章:使用 Apache Spark 进行数据处理
在上一章中,您学习了如何将流数据添加到您的数据管道中。使用 Python 或 Apache NiFi,您可以提取、转换和加载数据。然而,为了对大量流数据进行转换,数据工程师转向 Apache Spark 等工具。Apache Spark 比大多数其他方法(如非平凡转换上的 MapReduce)都要快,并且它允许分布式数据处理。
在本章中,我们将涵盖以下主要主题:
- 
安装和运行 Spark 
- 
安装和配置 PySpark 
- 
使用 PySpark 处理数据 
安装和运行 Spark
Apache Spark 是一个可以处理流和批量数据,甚至图数据的分布式数据处理引擎。它有一组核心组件和其他用于添加功能的库。以下图表显示了 Spark 生态系统的常见表示:

图 14.1 – Apache Spark 生态系统
要以集群模式运行 Spark,您有几个选项。Spark 可以在独立模式下运行,这使用 Spark 提供的简单集群管理器。它也可以在 Amazon EC2 实例上运行,使用 YARN、Mesos 或 Kubernetes。在生产环境中,如果您的工作负载很大,您可能不想在独立模式下运行;然而,在本章中,我们将这样建立我们的集群。原则将是相同的,但独立集群提供了最快的启动和运行方式,无需深入研究更复杂的基础设施。
要安装 Apache Spark,请按照以下步骤操作:
- 
访问 spark.apache.org网站浏览。从这里,您可以了解 Apache Spark 的新版本,阅读文档,了解库,并查找代码示例:![图 14.2 – Apache Spark 网站]() 图 14.2 – Apache Spark 网站 
- 
从网站中选择下载菜单选项。选择您想要使用的 Spark 版本 – 在撰写本文时,最新版本是 3.0.0。您将被要求选择一个包类型。我们不会使用 Hadoop,但必须选择一个版本或提供自己的。我选择了为 Apache Hadoop 2.7 预构建。在 Windows 上,您可能需要通过设置环境变量来欺骗操作系统认为已安装了 Hadoop,但在 Linux 和 macOS 上,这应该不会是问题。下载选项如下所示: ![图 14.3 – 为 Hadoop 2.7 下载 Apache Spark]() 图 14.3 – 为 Hadoop 2.7 下载 Apache Spark 
- 
下载文件后,您将提取它,然后将其移动到名为 spark3的目录中的主目录。您可以使用以下命令完成此操作:tar -xvzf spark-3.0.0-bin-hadoop2.7.tgz mv spark-3.0.0-bin-hadoop2.7 ~/spark3
- 
接下来,您需要创建一个集群。就像您对 Kafka 所做的那样,您将在同一台机器上复制 Spark 目录,并使其作为另一个节点运行。如果您还有另一台机器,您也可以在该服务器上放置 Spark 的另一个副本。复制目录并将其重命名为 spark-node,如下所示:cp -r spark3/ spark-node
- 
要运行 Spark 集群,您可以使用提供的脚本。运行集群的脚本使用过时的术语 – master和slave。这种语言在技术空间中很常见;然而,长期以来,很多人反对使用这些术语。最终,似乎有一些进展正在被做出,GitHub 将从分支名称中移除master。我也已将脚本重命名,使用术语head和node。要这样做,请使用以下命令:cd ~/spark3/sbin cp start-master.sh start-head.sh cd ~/spark-node/sbin cp start-slave.sh start-node.sh
- 
要启动集群,您现在可以像下面这样运行脚本: ./start-head.sh ./start-node.sh spark://pop-os.localdomain:7077 -p 9911
- 
您可以向脚本传递参数,在先前的命令中,您通过传递端口标志( -p)来告诉脚本您希望节点在哪个端口上运行。您还可以传递以下参数:a) -h, --host: 要运行的计算机名。i, -ip标志已被弃用。b) -p, --port: 要监听的端口。c) --webui-port: Web GUI 的端口,默认为8080。d) -c, --cores: 要使用的核心数。e) -m, --memory: 要使用的内存量。默认情况下,它比您的全部内存少 1 吉字节。f) -d, --work-dir: 工作节点的临时空间目录。g) --properties-file: 您可以在spark.conf文件中指定这些标志中的几个。
集群将花费一分钟来加载,加载完成后,您可以浏览到 Web UI,网址为localhost:8080/。您将看到集群的详细信息,它将如下截图所示:

图 14.4 – Spark 集群 Web UI
当集群启动并运行时,您需要设置 Python 环境,以便您可以针对它进行编码。下一节将指导您完成这些步骤。
安装和配置 PySpark
PySpark 与 Spark 一起安装。您可以在~/spark3/bin目录中看到它,以及其他库和工具。要配置 PySpark 以运行,您需要导出环境变量。变量如下所示:
export SPARK_HOME=/home/paulcrickard/spark3
export PATH=$SPARK_HOME/bin:$PATH
export PYSPARK_PYTHON=python3 
先前的命令设置了SPARK_HOME变量。这将是指定 Spark 安装的位置。我已经将变量指向 Spark 集群的头部,因为节点实际上将位于另一台机器上。然后,它将SPARK_HOME添加到您的路径中。这意味着当您输入命令时,操作系统将在您路径中指定的目录中查找它,因此现在它将搜索~/spark3/bin,这是 PySpark 所在的位置。
在终端中运行前面的命令将允许 Spark 在终端打开时运行。您每次都必须重新运行这些命令。要使它们永久,可以将命令添加到您的 ~/.bashrc 文件中。保存 .bashrc 文件后,您需要重新加载它。您可以通过运行以下命令来完成此操作:
source ~/.bashrc
您现在应该能够打开一个终端并运行 PySpark,结果将是 PySpark 交互式 shell,如下所示:

图 14.5 – 交互式 Spark shell
如果您看到了前面的截图,恭喜您,您已经配置好了 PySpark。在本章中,示例将使用 Jupyter notebooks 中的 PySpark。有两种方法可以配置 PySpark 以与 Jupyter 一起工作:
- 
使用以下命令将 PYSPARK_DRIVER_PYTHON环境变量和_OPTS变量添加到 Jupyter Notebook 中(如果您想使其永久,请将其添加到~/.bashrc):export PYSPARK_DRIVER_PYTHON=jupyter export PYSPARK_DRIVER_PYTHON_OPTS='notebook'
- 
使用 findspark库并将代码添加到您的 Jupyter notebook 中,以在运行时获取 Spark 信息。本章中的示例将使用此方法。您可以使用pip安装findspark,如下所示:findspark method, add the following two lines to your notebook and run it:导入 findspark findspark.init() If the preceding lines ran without error, then the code was able to find Spark.
您现在可以在您的 Spark 集群上运行 PySpark 代码。下一节将带您了解一些基本的 PySpark 示例。
使用 PySpark 处理数据
在使用 PySpark 处理数据之前,让我们运行一个示例来展示 Spark 的工作原理。然后,在后续示例中我们将跳过模板代码,专注于数据处理。以下截图显示了 Spark 网站上 spark.apache.org/examples.html 的 Pi 估计 示例的 Jupyter notebook:

图 14.6 – Jupyter notebook 中的 Pi 估计示例
网站上的示例未经修改将无法运行。在以下要点中,我将带您浏览单元格:
- 
第一个单元格导入 findspark并运行init()方法。这在前一节中已解释为在 Jupyter notebooks 中包含 PySpark 的首选方法。代码如下:import findspark findspark.init()
- 
下一个单元格导入 pyspark库和SparkSession。然后通过传递 Spark 集群的头部节点来创建会话。您可以从 Spark web UI 获取 URL – 您也用它来启动工作节点:import pyspark from pyspark.sql import SparkSession spark=SparkSession.builder.master('spark://pop-os.localdomain:7077').appName('Pi-Estimation').getOrCreate()
- 
运行前两个单元格,您可以浏览到 Spark GUI 并看到有一个任务正在运行。运行的任务如下截图所示 – 注意工作节点的名称是 Pi-Estimation,这是前面代码中的appName参数:![图 14.7 – 带有运行会话和两个完成会话的 Spark web UI]() 图 14.7 – 带有运行会话和两个完成会话的 Spark web UI 前面的代码将在您的所有 Spark 代码中使用。这是模板代码。 
- 
下一个单元包含工作内容。下面的代码将估算 π 的值。代码的细节并不重要,但请注意, count变量使用sparkContext并在集群上并行化一个任务。在样板代码之后,您的 Spark 代码将执行一个任务并获取结果:import random NUM_SAMPLES=1 def inside(p): x, y = random.random(), random.random() return x*x + y*y < 1 count = spark.sparkContext.parallelize(range(0, NUM_SAMPLES)).filter(inside).count() print('Pi is roughly {}'.format(4.0 * count / NUM_SAMPLES))
- 
最后,停止会话: spark.stop()
一旦会话停止,它将在 web UI 中显示为一个已完成的应用程序。下一节将使用 Spark 和 DataFrame 将数据发送到 Kafka。
数据工程中的 Spark
上一节展示了 Spark 应用的结构:我们使用 findspark 获取路径,导入库,创建会话,执行一些操作,然后停止会话。当您执行某些操作时,它很可能涉及到 Spark DataFrame。本节将简要概述 Spark DataFrame 的工作原理——它与 pandas 略有不同。
您必须做的第一件事是使用 findspark 设置环境。然后,您可以导入所需的库。然后,创建会话。以下代码显示了设置所需的样板代码:
import findspark
findspark.init()
import pyspark
from pyspark.sql import SparkSession
import os
os.chdir('/home/paulcrickard')
spark=SparkSession.builder.master('spark://pop-os.localdomain:7077').appName('DataFrame-Kafka').getOrCreate()
现在,您已连接到 Spark 集群上的一个会话。您可以像在 第三章**,读取和写入文件* 和 第四章**,与数据库交互* 中使用 DataFrame 一样读取 CSV 和 JSON 数据,有一些细微的修改。当读取数据时,您可以使用 read.csv 而不是 pandas 中的 read_csv。Spark 和 pandas 之间的另一个区别是 Spark 中使用 .show() 来查看 DataFrame。在 pandas 中,您可以查看 DataFrame 的 dtypes,而在 Spark 中,您可以使用 printSchema() 做到同样的事情。以下代码读取 data.csv 文件并打印前五行和模式:
df = spark.read.csv('data.csv')
df.show(5)
df.printSchema()
输出将类似于以下截图所示的 DataFrame:

图 14.8 – 带有模式和 CSV 的 DataFrame
您会注意到标题是第一行,并且有默认的 _c0 列名。输出还显示所有列都是字符串类型。您可以指定一个模式并将其作为参数传递;然而,您也可以告诉 Spark 推断模式。以下代码传递存在标题并告诉 Spark 推断模式:
df = spark.read.csv('data.csv',header=True,inferSchema=True)
df.show(5)
结果正如您所预期的:一个具有正确类型的 DataFrame。以下截图显示了结果:

图 14.9 – 带有标题和正确类型的 DataFrame
您可以通过使用 select() 函数并传递列名作为参数来选择一列。别忘了添加 .show(),否则它将返回一个 DataFrame 而不会显示:
df.select('name').show()
你会注意到在 pandas 中,你会使用 [] 和列名,并且不需要 select 方法。在 pandas 中,你也可以使用 df[(df['field']< value)] 格式来过滤 DataFrame。在 Spark 中,你可以使用 select 和 filter 来做同样的事情,区别在于 select 方法对于一个条件返回 True 和 False,而 filter 将返回该条件的 DataFrame。使用 filter,你还可以添加一个 select 方法并传递一个列名的数组来返回。代码如下所示:
df.select(df['age']<40).show()
df.filter(df['age']<40).show()
df.filter('age<40').select(['name','age','state']).show()
注意,在最后一行中,你没有使用 df['age'],而是直接传递了列名。当你想要遍历 DataFrame 时,你可以在 pandas 中使用 iterrows。在 Spark 中,你使用 collect() 创建一个行数组。以下代码将使用 filter 方法获取所有 40 岁以下的人并打印数组:
u40=df.filter('age<40').collect()
u40
要获取单行,你只需传递索引。你可以将行转换为不同的格式,在这个例子中,我将它转换成了字典。作为字典,你可以通过指定键来选择任何值。代码如下所示:
u40[0]
u40[0].asDict()
u40[0].asDict()['name']
前面代码的输出是一个 Row 对象、一个字典和键名的值字符串,如下所示:
Row(name='Patrick Hendrix', age=23, street='5755 Jonathan Ranch', city='New Sheriland', state='Wisconsin', zip=60519, lng=103.914462, lat=-59.0094375)
{'name': 'Patrick Hendrix', 'age': 23, 'street': '5755 Jonathan Ranch', 'city': 'New Sheriland', 'state': 'Wisconsin', 'zip': 60519, 'lng': 103.914462, 'lat': -59.0094375}
'Patrick Hendrix'
要在 Spark 中遍历 DataFrame,你调用 collect(),然后使用 for 循环遍历数组。然后你可以将每一行转换为字典,并对其进行处理。以下代码片段打印了字典:
for x in u40:
    print(x.asDict())
如果你更熟悉 SQL,你可以使用 spark.sql 来过滤 DataFrame。要使用 SQL,你必须首先创建一个视图,然后你可以用 SQL 查询它,如下面的代码所示:
df.createOrReplaceTempView('people')
df_over40=spark.sql('select * from people where age > 40')
df_over40.show()
结果将与 filter 方法相同,但只是达到相同结果的不同方法。
有几个函数可以对 DataFrame 中的列或数据进行修改或分析。在 Spark 中,你可以使用 describe() 来获取列中数据的简要概述。以下代码在 age 列上使用它:
df_over40.describe('age').show()
输出是常见的描述性统计,包括 count、mean、standard deviation、min 和 max。这五个统计量为你提供了数据的良好概述。
你也可以像在 pandas 中一样对数据进行分组和聚合。要按州分组计数,你可以使用 groupBy(),如下所示:
df.groupBy('state').count().show()
聚合允许你传入一个字段和方法的字典。要计算 age 列的平均值,你会使用以下代码:
df.agg({'age':'mean'}).show()
对于 groupBy 和 agg,您可以使用 mean、max、min、sum 以及您可以在文档中阅读到的其他方法。您可以使用大量其他函数,这些函数需要您导入 pyspark.sql.functions 模块。以下代码将其导入为 f 并演示了一些有用的函数。同样,有关所有函数的更多信息,您可以在 spark.apache.org/docs/latest/api/python/pyspark.sql.html 阅读 Python API 文档:
import pyspark.sql.functions as f
df.select(f.collect_set(df['state'])).collect()
# Returns a Row of unique states which will be all 50.
df.select(f.countDistinct('state').alias('states')).show()
#returns a single column named states with a single value of 50.
df.select(f.md5('street').alias('hash')).collect()
#Returns an md5 hash of the street value for each row
# Row(hash='81576976c4903b063c46ed9fdd140d62'),
df.select(f.reverse(df.state).alias('state-reverse')).collect()
# returns each rows street value reversed
# Row(state-reverse='nisnocsiW')
select(f.soundex(df.name).alias('soundex')).collect()
# returns a soundex of the name field for each row
# Row(soundex='P362')
当您完成数据处理后,使用 stop() 停止会话,如下所示:
spark.stop()
恭喜!您已成功使用 PySpark 处理数据。
摘要
在本章中,您学习了使用 Apache Spark 的基础知识。首先,您下载并安装了 Spark,并配置 PySpark 在 Jupyter 笔记本中运行。您还学习了如何通过添加节点来水平扩展 Spark。Spark 使用类似于 pandas 中使用的 DataFrame。最后一节教您了在 Spark 中操作数据的基本方法。
在下一章中,您将使用 Spark 与 Apache MiNiFi 一起在边缘或物联网设备上移动数据。
第十五章:第十五章:使用 MiNiFi、Kafka 和 Spark 的实时边缘数据
在本章中,您将学习如何使用 Apache NiFi 将 物联网(IoT)设备、小型计算机和传感器发送数据到数据管道。对于处理能力较小的计算机或设备,MiNiFi 允许它们成为 NiFi 数据管道的一部分。MiNiFi 是 NiFi 的轻量级版本,具有精简的处理器集和没有图形用户界面。它是构建用于通过内置在 NiFi 中的数据管道发送数据,并部署到设备上的。
在本章中,我们将涵盖以下主要主题:
- 
在设备上设置 MiNiFi 
- 
在 NiFi 中构建和部署 MiNiFi 任务 
设置 MiNiFi
Apache MiNiFi 是 NiFi 的轻量级版本,用于在数据源处进行数据收集。来源越来越小,包括物联网设备、传感器和低功耗计算机,如 Raspberry Pi。要将这些设备纳入您的数据管道,您需要一种方法从设备中获取数据。MiNiFi 允许您将数据作为标准数据管道的一部分流式传输到 NiFi。
要获取 MiNiFi 二进制文件,请浏览到 nifi.apache.org/minifi/。以下截图是 MiNiFi 主页,并将为您提供有关项目和文档的信息:

图 15.1 – Apache MiNiFi 主页
从主导航栏进入 下载 并选择 下载 MiNiFi 组件 选项。您需要决定您是想运行 MiNiFi Java 还是 MiNiFi C++ 版本。哪个版本适合将取决于 MiNiFi 将驻留的设备的规格。如果您需要最小的占用空间和内存使用,那么 C++ 版本适合您。如果您有更多资源并且需要更广泛的可用处理器选择,那么 Java 版本将是您的最佳选择。您可以通过类别查找处理器列表,其中包含描述。nifi.apache.org/docs/nifi-docs/html/getting-started.html#what-processors-are-available。
您始终可以复制 NiFi 中任何处理器的 NAR 文件并将其放入 MiNiFi 的 lib 目录。某些处理器可能还需要您复制并发送控制器服务的 NAR 文件。本章将使用 MiNiFi Java 版本。
下载当前版本的 MiNiFi(Java),目前为 0.5.0。选择 minifi-0.5.0-bin.tar.gz 链接并下载它。您还需要向下滚动页面并选择 MiNiFi 工具包二进制文件的相应版本。C++ 和 Java 版本使用相同的工具包,因此您只需选择正确的发布版本 – 0.5.0。下载 minifi-toolkit-0.5.0-bin.tar.gz 文件。
使用以下命令提取并复制 MiNiFi 和 MiNiFi 工具包到您的家目录:
tar -xvzf minifi-0.5.0-bin.tar.gz
tar -xvzf minifi-toolkit-0.5.0-bin.tar.gz
mv minifi-0.5.0 ~/minifi
mv minifi-toolkit-0.5.0 ~/minifi-toolkit
当我将minifi和minifi-toolkit移动到我的主目录时,我删除了-0.5.0。在本章中,我将在与 NiFi 相同的机器上运行 MiNiFi – 就像我已经与 Kafka 和 Spark 做的那样 – 但如果你想在另一个设备上运行 MiNiFi,就像在生产环境中一样,将minifi-0.5.0目录复制到该机器上。MiNiFi 工具包保持在 NiFi 机器上。
最后一步是将$MINIFI_HOME变量设置为 MiNiFi 的位置。你可以导出变量并将其添加到你的路径中,或者更好的方法是编辑你的.bashrc文件,如下所示:
export MINIFI_HOME=/home/paulcrickard/minifi
export PATH=$MINIFI_HOME/bin:$PATH
你的.bashrc文件将看起来如下面的屏幕截图所示。注意,我在 Apache Spark 的编辑上方有 MiNiFi 的编辑:

图 15.2 – 包含 Spark 和 MiNiFi 导出设置的.bashrc 文件
现在你已经配置了 MiNiFi 并且 MiNiFi 工具包准备就绪,是时候在 Apache NiFi 中创建你的第一个数据管道了。下一节将指导你创建一个。
在 NiFi 中构建 MiNiFi 任务
在本节中,你将构建一个数据管道并将其部署到 MiNiFi。该数据管道将生成流文件并将它们发送到 NiFi。下一节将进一步介绍并使用 MiNiFi 未包含的处理程序。
要使用 MiNiFi,你需要一个较旧的 NiFi 版本。当前的工具 – 0.5.0 – 由于nifi模板输出的属性更改而损坏。它将在 0.6.0 中修复,但在此之前,你需要至少使用 NiFi 的 1.9.0 版本。你可以在archive.apache.org/dist/nifi/1.9.0/获取较旧的 NiFi 版本。使用带有-xvzf标志的tar命令解压 NiFi。使用mv或你的文件资源管理器工具将文件夹放置在你的主目录中。
你还需要一个较旧的 Java 版本。要安装正确的 Java 版本,请使用以下命令:
sudo apt-get install openjdk-8-jre
最后,你还需要确保 NiFi 已配置为允许站点到站点连接。在终端中,转到$NIFI_HOME/conf并打开nifi.properties文件。滚动到文件大约一半的位置,你会看到站点到站点属性部分。在我的文件中,nifi.remote.input.socket.port是空的。如果没有指定端口,编辑文件以便端口为1026,如下面的屏幕截图所示:

图 15.3 – 当 input.socket.port 设置为 1026 时的站点到站点属性
接下来,启动 NiFi 并创建一个输入端口以连接 MiNiFi 和 NiFi。将输入端口拖放到画布上,并将其命名为minifi。来自 MiNiFi 的数据将通过此端口进入 NiFi。
将输入端口连接到数据管道。管道在以下屏幕截图中显示:

图 15.4 – 将 MiNiFi 数据消费并写入 NiFi 主机上的文件的数据管道
要构建数据管道,请按照以下步骤操作:
- 
将 EvaluteJsonPath处理器拖放到画布上。配置flowfile-attribute。创建一个名为fname的新属性,并将其值设置为$.fname。这将来自 MiNiFi 的 JSON 接收到的信息。
- 
将 UpdateAttribute处理器拖放到画布上。创建一个名为filename的新属性,并将其值设置为${fname}。
- 
将 PutFile处理器拖放到画布上。设置/home/paulcrickard/output。将其他属性保留为默认值。
上述步骤创建了从 MiNiFi 到 NiFi 的连接,但到目前为止,我们还没有为 MiNiFi 创建数据管道。要创建 MiNiFi 数据管道,将处理器组拖放到画布上,并将其命名为 minifitask。
在处理器组内部,将 GenerateFlowfile 处理器拖放到画布上。在 30 sec 上设置 {"fname":"minifi.txt","body":"Some text"}。
接下来,您将添加 http://localhost:9300 和 HTTP。其余部分保留为默认值或留空。设置应如图下截图所示:

图 15.5 – 远程处理器组配置
将 GenerateFlowFile 处理器连接到 远程处理器组。创建连接 弹出窗口将允许您选择输入端口为 To Input。它应该正确猜测并选择 MiNiFi。如果不是,请使用下拉菜单选择您在上一步骤中创建的 MiNiFi 端口。一旦处理器连接,右键单击 远程处理器组 并选择 启用传输。图标现在应该是一个蓝色圆圈,如图下截图所示:
![Figure 15.6 – MiNiFi data pipeline to a remote processor group]
![img/Figure_15.6_B15739.jpg]
图 15.6 – 将 MiNiFi 数据管道连接到远程处理器组
MiNiFi 数据管道已完成。为了确保它可以在 MiNiFi 上运行,您需要对其进行转换。要转换它,您需要将其导出为模板。要创建模板,退出处理器组。右键单击处理器组,然后通过点击表格右侧的下载图标选择 minifitask 模板。这将下载数据管道的 XML 版本。
要转换模板,您需要在 MiNiFi 工具包中运行 config.sh。我在我的主目录中创建了一个 minifi-templates 文件夹。切换到 $MINIFI_HOME 目录,运行以下命令:
./bin/config.sh transform /home/paulcrickard/Downloads/minifitask.xml /home/paulcrickard/minifi-templates/config.yml
如果一切正常工作,你应该会收到如下截图所示的类似消息:
![Figure 15.7 – minifi-toolkit transforming the XML template into a YML file]
![img/Figure_15.7_B15739.jpg]
图 15.7 – minifi-toolkit 将 XML 模板转换为 YML 文件
现在,你将在minifi-templates目录中有一个config.yml文件。将此文件复制到$MINIFI_HOME/conf目录。你可以覆盖 MiNiFi 附带的原有config.yml文件。
从$MINIFI_HOME/bin目录,你可以启动minifi,当它启动时将读取你的config.yml文件。使用以下命令来启动 MiNiFi:
./minifi.sh start
你的 MiNiFi 数据管道现在正在运行。你可以在$MINIFI_HOME/logs/minifi-app.log中查看日志。但你现在也可以打开 NiFi,通过FromMinifi输入端口查看从 MiNiFi 流进来的数据。你的 NiFi 数据管道应该看起来如下截图所示:

图 15.8 – 从 MiNiFi 输入端口接收数据的管道
你会注意到你用来创建模板的处理器组已经停止。数据正从 MiNiFi 流入 NiFi 实例,并被处理并保存到 NiFi 机器的磁盘上。MiNiFi 机器只发送数据,这允许它不会因为尝试在本地运行 NiFi 的版本或需要定期与其他机器建立远程连接来写入文件而超负荷。可以从 MiNiFi 机器向 NiFi 发送流数据。
一旦 MiNiFi 数据流进入 NiFi,你将拥有处理这些数据的全部工具。你可以将其发送到如第十三章中所示的主题,即使用 Kafka 的流式数据,并使其对监听该主题的许多其他工具可用。MiNiFi 开启了从小型设备捕获数据的可能性。
摘要
在本章中,你学习了 MiNiFi 提供了一种方法,通过它可以流式传输数据到 NiFi 实例。使用 MiNiFi,你可以从传感器、如树莓派等较小的设备,或数据所在的传统服务器捕获数据,而无需安装完整的 NiFi。你学习了如何设置和配置远程处理器组,允许你与远程 NiFi 实例通信。
在附录中,你将学习如何对 NiFi 进行集群以在不同的机器上运行你的数据管道,以便你可以进一步分散负载。这将允许你为特定任务保留服务器,或将大量数据水平地跨集群分散。通过将 NiFi、Kafka 和 Spark 组合成集群,你将能够处理比任何单个机器更多的数据。
第十六章:附录
构建 NiFi 集群
在这本书中,你已经构建了 Kafka 集群、ZooKeeper 集群和 Spark 集群。通过集群,你能够增加更多机器来提高数据管道的处理能力,而不是增加单个服务器的功率。在本章中,你将学习如何集群化 NiFi,以便你的数据管道可以在多台机器上运行。
在本附录中,我们将涵盖以下主要主题:
- 
NiFi 集群的基本知识 
- 
构建 NiFi 集群 
- 
构建分布式数据处理管道 
- 
管理分布式数据处理管道 
NiFi 集群的基本知识
Apache NiFi 中的集群遵循零主集群架构。在这种类型的集群中,没有预定义的主节点。每个节点都可以执行相同的任务,数据在他们之间分配。当作为集群部署时,NiFi 使用 Zookeeper。
Zookeeper 将选举一个集群协调器。集群协调器负责决定新节点是否可以加入——节点将连接到协调器——并提供更新后的流程给新节点。
虽然听起来集群协调器是主节点,但实际上并非如此。你可以在任何节点上更改数据管道,并且这些更改将被复制到所有其他节点,这意味着非集群协调器或非主节点可以提交更改。
ExecuteSQL处理器可以在主节点上运行,然后将数据分发到下游的其他节点进行处理。你将在本章后面看到这是如何操作的。
集群允许你构建能够处理比单台机器更多数据量的数据管道。此外,它允许单个点构建和监控数据管道。如果你运行了多个单节点 NiFi 实例,你需要管理所有这些实例。一个数据管道的更改需要复制到其他实例,或者至少检查以确保它不是重复的。数据仓库管道又是在哪台机器上运行的?我忘了。从任何节点管理集群,会使它更加容易和高效。
构建 NiFi 集群
在本节中,你将在不同的机器上构建一个双节点集群。然而,与 MiNiFi 一样,NiFi 和 Zookeeper 的最新版本存在一些兼容性问题。为了解决这些问题并展示概念,本章将使用较旧的 NiFi 版本和预捆绑的 Zookeeper。要构建 NiFi 集群,请执行以下步骤:
- 
以 root 用户或使用 sudo,打开你的 /etc/hosts文件。你需要为你的集群中使用的机器分配名称。最佳实践是使用主机名而不是 IP 地址。你的 hosts 文件应该看起来像以下示例:127.0.0.1 localhost ::1 localhost 127.0.1.1 pop-os.localdomain pop-os 10.0.0.63 nifi-node-2 10.0.0.148 nifi-node-1
- 
在前面的 hosts 文件中,我已经添加了最后两行。节点是 nifi-node-1和nifi-node-2,您可以看到它们有不同的 IP 地址。请在每台机器的 hosts 文件中做出这些更改。完成之后,您可以通过使用ping来测试它是否工作。从每台机器,尝试通过主机名使用ping来击中另一台机器。以下是从nifi-node-1机器击中nifi-node-2的命令:paulcrickard@pop-os:~$ ping nifi-node-2 PING nifi-node-2 (10.0.0.63) 56(84) bytes of data. 64 bytes from nifi-node-2 (10.0.0.63): icmp_seq=1 ttl=64 time=55.1 ms 64 bytes from nifi-node-2 (10.0.0.63): icmp_seq=2 ttl=64 time=77.1 ms 64 bytes from nifi-node-2 (10.0.0.63): icmp_seq=3 ttl=64 time=101 ms 64 bytes from nifi-node-2 (10.0.0.63): icmp_seq=4 ttl=64 time=32.8 ms
- 
如果您对另一个节点, nifi-node-2,做相反的操作,您应该得到相同的结果——nifi-node-1将返回数据。
- 
接下来,下载 Apache NiFi 的旧版本,1.0.0,请访问 archive.apache.org/dist/nifi/1.0.0/。选择-bin.tar.gz文件,因为它包含二进制文件。一旦文件下载完成,使用您的文件管理器或以下命令提取文件:tar -xvzf nifi-1.0.0-bin.tar.gz一旦您提取了文件,您将编辑配置文件。 
- 
要编辑 Zookeeper 配置文件,请在 $NIFI_HOME/conf目录中打开zookeeper.properties文件。在文件底部,按照以下示例添加您的服务器:server.1=nifi-node-1:2888:3888 server.2=nifi-node-2:2888:3888
- 
在文件顶部,您将看到 clientPort和dataDir。它应该看起来像以下示例:clientPort=2181 initLimit=10 autopurge.purgeInterval=24 syncLimit=5 tickTime=2000 dataDir=./state/zookeeper autopurge.snapRetainCount=30
- 
在 dataDir中,您需要添加一个名为myfile的文件,其内容为服务器的编号。在server.1(nifi-node-1) 上,您将创建一个myidID,内容为1。为此,从$NIFI_HOME目录,使用以下命令:mkdir state mkdir state/zookeeper echo 1 >> myid
- 
在 nifi-node-2上,重复前面的步骤,除了将echo改为以下行:nifi.properties file.
- 
要编辑 nifi.properties,您需要更改几个属性。第一个属性是nifi.state.management.embedded.zookeeper.start,需要将其设置为true。文件的部分内容如下所示:#################### # State Management # #################### nifi.state.management.configuration.file=./conf/state-management.xml # The ID of the local state provider nifi.state.management.provider.local=local-provider # The ID of the cluster-wide state provider. This will be ignored if NiFi is not clustered but must be populated if running in a cluster. nifi.state.management.provider.cluster=zk-provider # Specifies whether or not this instance of NiFi should run an embedded ZooKeeper server nifi.state.management.embedded.zookeeper.start=true # Properties file that provides the ZooKeeper properties to use if <nifi.state.management.embedded.zookeeper.start> is set to true nifi.state.management.embedded.zookeeper.properties=./conf/zookeeper.properties上述命令告诉 NiFi 使用内嵌版本的 Zookeeper。 
- 
您现在需要告诉 NiFi 如何在 nifi.zookeeper.connect.string中连接到 Zookeeper。该字符串是逗号分隔的 Zookeeper 服务器列表,格式为<hostname>:<port>,端口是zookeeper.config文件中的clientPort,它是2181。文件的部分内容如下所示:# zookeeper properties, used for cluster management # nifi.zookeeper.connect.string=nifi.zookeeper.connect.string=nifi-node-1:2181,nifi-node-2:2181 nifi.zookeeper.connect.timeout=3 secs nifi.zookeeper.session.timeout=3 secs nifi.zookeeper.root.node=/nifi
- 
接下来,您将配置 NiFi 的 cluster属性。具体来说,您将设置nifi.cluster.node为true。您将添加节点的主机名到nifi.cluster.node.address,以及添加端口到nifi.cluster.node.protocol.port。您可以将其设置为任何可用且足够高的端口,这样您就不需要 root 权限来访问它(超过1024)。最后,您可以将nifi.cluster.flow.election.max.wait.time更改为小于 5 分钟,并为nifi.cluster.flow.election.max.candidates添加一个值。我已经将等待时间更改为1分钟,并留空候选人。文件的部分内容如下所示:# cluster node properties (only configure for cluster nodes) # nifi.cluster.is.node=true nifi.cluster.node.address=nifi-node-1 nifi.cluster.node.protocol.port=8881 nifi.cluster.node.protocol.threads=10 nifi.cluster.node.protocol.max.threads=50 nifi.cluster.node.event.history.size=25 nifi.cluster.node.connection.timeout=5 sec nifi.cluster.node.read.timeout=5 sec nifi.cluster.node.max.concurrent.requests=100 nifi.cluster.firewall.file= nifi.cluster.flow.election.max.wait.time=1 mins nifi.cluster.flow.election.max.candidates=
- 
网络属性需要机器的主机名以及端口号。默认情况下, nifi.web.http.port是8080,但如果该端口上已经运行了其他东西,您可以更改它。我已经将其更改为8888。主机名是nifi-node-1或nifi-mode-2。网络属性在以下代码块中显示:# web properties # nifi.web.war.directory=./lib nifi.web.http.host=nifi-node-1 <---------------------- nifi.web.http.port=8888
- 
最后,NiFi 使用站点到站点进行通信。您需要将 nifi.remote.input.host属性配置为机器的主机名,并将nifi.remote.input.socket.port配置为可用端口。属性文件在以下代码块中显示:nifi.properties file, with the exception of changing the hostname to the appropriate number, nifi-node-#.
您的集群现在已配置完成,您准备好启动两个节点。从每台机器上,使用以下命令以正常方式启动 NiFi:
./nifi.sh start
您现在应该能够浏览到 http://nifi-node-1:8888/nifi 上的任何节点。您将看到与往常一样的 NiFi,如下面的屏幕截图所示:

图 16.1 – 以集群方式运行的 NiFi
一切看起来都完全一样,只是在状态栏的左上角。现在您应该有一个带有 2/2 的云图标。这表示 NiFi 作为一个集群运行,有 2 个节点中的 2 个可用并连接。您可以通过在状态栏右侧的消息上悬停来查看事件。以下屏幕截图显示了节点的选举和连接:

图 16.2 – 显示集群中事件的消息
最后,您可以通过在 NiFi 窗口右上角的 waffle 菜单中选择集群来打开集群窗口。集群在以下屏幕截图中显示:

图 16.3 – 集群详细信息
之前的屏幕截图显示了哪个节点是主节点,以及控制器节点和一个常规节点。从这里你还可以查看有关队列的详细信息,或者断开或重新连接节点。集群正在运行,你现在可以构建一个分布式数据管道。
构建分布式数据管道
构建分布式数据管道几乎与构建在单台机器上运行的数据管道完全相同。NiFi 将处理传递和重新组合数据的物流。以下屏幕截图显示了基本数据管道:

图 16.4 – 一个基本的数据管道,用于生成数据、提取属性到 JSON 并写入磁盘
之前的数据管道使用 GenerateFlowFile 处理器来创建唯一的流文件。这些文件随后传递到 AttributesToJSON 处理器,该处理器提取属性并将它们写入流文件内容。最后,文件被写入到 /home/paulcrickard/output 目录下的磁盘。
在运行数据管道之前,你需要确保每个节点上都有PutFile处理器的输出目录。之前我说过,在分布式情况下数据管道没有不同,但是有一些事情你必须记住,其中之一就是PutFile默认情况下会在每个节点上写入磁盘。你需要配置你的处理器以便在任何节点上运行。我们将在本节稍后解决这个问题。
在运行数据管道之前还有一件事。打开浏览器到你的另一个节点。你将看到那个节点上完全相同的数据管道。甚至处理器的布局也是一样的。对任何节点的更改都将分布到所有其他节点。你可以在任何节点上工作。
当你运行数据管道时,你将看到两个节点的输出目录中都有文件被写入。数据管道正在运行并将负载分配到各个节点。以下截图显示了数据管道的输出:
![Figure 16.5 – Data pipeline writing flowfiles to a node
![img/Figure_16.5_B15739.jpg]
图 16.5 – 数据管道将流文件写入节点
如果你得到的结果与前面的截图相同,恭喜你,你刚刚构建了一个分布式数据管道。接下来,你将学习 NiFi 集群的一些更多功能。
管理分布式数据管道
前面的数据管道在每个节点上运行。为了补偿这一点,你必须为PutFile处理器在两个节点上创建相同的路径才能工作。之前你了解到有几个处理器会导致竞争条件——尝试同时读取相同的文件,这将会引起问题。为了解决这些问题,你可以指定一个处理器应该只在主节点上运行——作为一个独立的过程。
在PutFile处理器的配置中,选择调度选项卡。在调度策略下拉菜单中,选择主节点,如下截图所示:
![Figure 16.6 – Running a processor on the Primary Node only
![img/Figure_16.6_B15739.jpg]
图 16.6 – 仅在主节点上运行处理器
现在,当你运行数据管道时,文件将只放置在主节点上。你可以调度像GetFile或ExecuteSQL这样的处理器来完成相同的事情。
要查看每个节点上数据管道的负载,你可以从 waffle 菜单查看集群详情。当数据通过数据管道移动时,你可以看到有多少流文件坐在每个节点的队列中。以下截图显示了在我集群上运行的管道:
![Figure 16.7 – Viewing the queues of each node. Each node has four flowfiles
![img/Figure_16.7_B15739.jpg]
图 16.7 – 查看每个节点的队列。每个节点有四个流文件
数据管道正在将 flowfiles 均匀地分配到各个节点。在零主集群中,数据不会被复制或复制。它只存在于正在处理它的节点上。如果一个节点宕机,数据需要重新分配。这只能在节点仍然连接到网络的情况下发生,否则,它将不会发生,直到节点重新加入。
你可以通过点击节点行右侧的电源图标手动断开节点连接。以下截图显示了节点正在断开连接的情况:

图 16.8 – nifi-node-1 已从集群断开连接
在前面的截图中,你可以看到nifi-node-1的状态为断开连接。但你也应该注意到它有八个需要重新分配的 flowfiles。由于你断开了节点,但没有将其从网络中删除,NiFi 将重新分配 flowfiles。你可以通过刷新屏幕看到结果,如下面的截图所示:

图 16.9 – 从断开连接的节点重新分配的 flowfiles
你还可以重新连接任何断开的节点。你通过点击插头图标来完成此操作。当你这样做时,节点将重新加入集群,flowfiles 将被重新分配。以下截图显示了节点重新加入集群的情况:

图 16.10 – 重新连接节点和 flowfile 重新分配
在前面的截图中,由于节点断开连接,flowfiles 在节点之间均匀累积。
摘要
在本附录中,你学习了 NiFi 集群的基本知识,以及如何使用嵌入式 Zookeeper 构建集群以及如何构建分布式数据管道。NiFi 处理大部分数据分布;你只需要记住一些注意事项——例如竞争条件和处理器需要配置在任意节点上运行。使用 NiFi 集群允许你从单个实例管理多个机器上的 NiFi。它还允许你处理大量数据,并在实例崩溃的情况下提供一些冗余。

 
                     
                    
                 
                    
                


















 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号