数据清理指南-全-

数据清理指南(全)

原文:annas-archive.org/md5/9291c4c1871fca078a620d5e67720b6e

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

“巴贝奇先生,如果你把错误的数字输入到机器里,正确的答案会出来吗?”
--查尔斯·巴贝奇(1864)
“垃圾进,垃圾出”
--美国国税局(1963)
“没有干净的数据集。”
--乔什·沙利文,Booz Allen 副总裁,《财富》杂志(2015)

在他 1864 年的随笔集中,计算机发明者查尔斯·巴贝奇回忆起当时对“思想混乱”的困惑:有人竟然认为,尽管输入了错误的数据,计算机仍然能够算出正确的答案。再快进 100 年,税务局开始耐心解释“垃圾进,垃圾出”,以表达即使是强大的税务机构,计算机处理仍然依赖于输入数据的质量。再过 50 年,进入 2015 年:看似神奇的机器学习、自动校正、预测性界面和推荐系统的时代,它们比我更了解我自己。然而,这些有用的算法仍然需要高质量的数据才能正确地学习,而我们却感叹“没有干净的数据集”。

本书适合任何经常处理数据的人,无论是数据科学家、数据记者、软件开发者,还是其他相关职业。目标是教授实用策略,帮助快速且轻松地弥合我们希望获得的数据与实际拥有的数据之间的差距。我们希望拥有高质量、完美的数据,但现实是,我们的大多数数据远远不足够。无论是数据缺失、格式错误、位置不对,还是数据中的异常,结果往往正如说唱歌手 Notorious B.I.G.所说,“数据越多,问题越多”。

在本书中,我们将把数据清洗视为数据科学过程中的一个重要且值得做的步骤:它是可以轻松改进的,绝不被忽视。我们的目标是重新构建数据清洗的概念,使其不再是一个令人畏惧且繁琐的任务,而是必须完成的准备工作,才能进入真正的工作。相反,凭借一些经过验证的流程和工具,我们将学会像在厨房里一样,先洗好蔬菜,你的食物会看起来更好,味道更好,对身体也更有益。如果你掌握一些正确的刀工技巧,肉类会更嫩,蔬菜也会更均匀地煮熟。就像一个优秀的厨师会有他最喜欢的刀具和烹饪传统一样,一个优秀的数据科学家也会希望在最佳的数据和最佳的条件下工作。

本书内容

第一章,为什么需要干净的数据? 通过展示数据清理在整个数据科学过程中所起的核心作用,激发了我们对干净数据的追求。接下来,我们通过一个简单的例子展示了来自真实数据集的脏数据。我们权衡了每个潜在清理过程的利弊,然后描述了如何将我们的清理更改传达给他人。

第二章,基础知识 – 格式、类型和编码,介绍了关于文件格式、压缩、数据类型的基础知识,包括缺失数据、空数据和字符编码。每个小节都有来自真实数据集的示例。此章很重要,因为接下来的内容我们将依赖这些基本概念的知识。

第三章,数据清理的主力军 – 电子表格和文本编辑器,描述了如何最大化利用两种常用工具:文本编辑器和电子表格来进行数据清理。我们将涵盖解决常见问题的简单方法,包括如何使用函数、查找和替换、正则表达式来修正和转换数据。在本章的最后,我们将通过使用这两种工具来清理一些关于大学的真实数据,来检验我们的技能。

第四章,使用通用语言 – 数据转换,重点讲解了如何将数据从一种格式转换成另一种格式。这是数据清理中最重要的任务之一,拥有各种工具可以轻松完成此任务非常有用。我们首先一步步讲解不同的转换,包括常见格式之间的相互转换,如逗号分隔值(CSV)、JSON 和 SQL。为了将新的数据转换技能付诸实践,我们进行一个项目,下载一个 Facebook 好友网络,并将其转换为几种不同的格式,以便我们可以可视化其结构。

第五章,从网络收集和清理数据,介绍了三种不同的清理 HTML 页面内数据的方法。本章展示了三种流行的工具,用于从标记化文本中提取数据元素,同时也为理解除了本章展示的特定工具之外的其他方法提供了概念基础。作为本章的项目,我们构建了一套清理程序来从基于网络的讨论论坛中提取数据。

第六章, PDF 文件中的数据清理,介绍了几种方法来应对数据清理者常遇到的最顽固和最常见的挑战:提取存储在 Adobe 的便携文档格式(PDF)文件中的数据。我们首先检查一些低成本工具来完成这项任务,然后尝试一些低门槛工具,最后,我们还尝试了 Adobe 的非免费软件。和往常一样,我们使用真实世界的数据进行实验,这为我们提供了丰富的经验,帮助我们在遇到问题时找到解决办法。

第七章, RDBMS 清理技术,使用一个公开可用的推文数据集,演示如何清理存储在关系型数据库中的数据。展示的数据库是 MySQL,但许多概念,包括基于正则表达式的文本提取和异常检测,也可以很容易地应用到其他存储系统。

第八章, 分享你的干净数据的最佳实践,描述了一些策略,帮助你使自己的辛勤工作尽可能地容易为他人使用。即使你从未计划与他人分享数据,本章中的策略也将帮助你在自己的工作中保持有序,从而节省未来的时间。本章讲解了如何创建理想的数据包并提供多种格式,如何文档化你的数据,如何选择并附加数据许可证,以及如何公开你的数据,以便你选择后它能够继续存在。

第九章, Stack Overflow 项目,带领你通过一个完整的项目,使用真实世界的数据集。我们从提出一系列可以回答的数据集相关问题开始。在回答这些问题时,我们将完成在第一章,为什么需要干净的数据?中介绍的整个数据科学流程,并且将实践我们在之前章节中学习到的许多清理过程。此外,由于这个数据集非常庞大,我们还将介绍一些新技巧,用来处理测试数据集的创建。

第十章,Twitter 项目,是一个完整的项目,展示了如何执行当前最热门、变化最快的数据收集和清理任务之一:挖掘 Twitter。我们将展示如何查找并收集一个关于真实世界当前事件的公开推文存档,同时遵守 Twitter 服务的法律使用限制。我们将通过这个数据集回答一个简单的问题,同时学习如何清理和提取 JSON 格式的数据,这也是目前最流行的 API 可访问的网络数据格式。最后,我们将设计一个简单的数据模型,用于长期存储提取和清理后的数据,并展示如何生成一些简单的可视化。

本书所需的工具

要完成本书中的项目,你将需要以下工具:

  • 一个网页浏览器、互联网连接和一个现代操作系统。浏览器和操作系统通常没有太大关系,但最好能够访问命令行终端窗口(例如 OS X 中的终端应用)。在第五章,收集与清理来自网络的数据,其中的三项活动之一依赖于一个基于浏览器的工具,该工具在 Chrome 浏览器中运行,所以如果你想完成此活动,请记得这一点。

  • 一个文本编辑器,比如 Mac OSX 上的 Text Wrangler 或 Windows 上的 Notepad++。一些集成开发环境(IDEs,例如 Eclipse)也可以作为文本编辑器使用,但它们通常有很多你不需要的功能。

  • 一个电子表格应用程序,如 Microsoft Excel 或 Google 表格。尽可能地,我们会提供通用的示例,这些示例适用于这两种工具,但在某些情况下,可能需要使用其中一种。

  • 一个 Python 开发环境和安装 Python 库的能力。我推荐使用 Enthought Canopy Python 环境,它可以在这里找到:www.enthought.com/products/canopy/

  • 一个安装并运行的 MySQL 5.5+服务器。

  • 一个网页服务器(运行任何服务器软件)和安装了 PHP 5+。

  • 一个 MySQL 客户端接口,可以是命令行接口、MySQL Workbench,或 phpMyAdmin(如果你已安装 PHP)。

本书适用的读者

如果你正在阅读本书,我猜你可能属于以下两类之一。第一类是数据科学家群体,他们已经花费了大量时间清理数据,但你希望在这方面做得更好。你可能对数据清理的单调感到沮丧,并且你正在寻找加速清理过程、提高效率或只是使用不同工具来完成工作的方法。在我们的厨房隐喻中,你是那位只需要稍微提升一些刀工技能的厨师。

另一组人是做数据科学工作的人,但他们之前并不关心数据清理。现在,您开始认为,如果有清理过程,也许您的结果会更好。也许那句古老的格言“垃圾进,垃圾出”开始变得更现实了。也许您想与他人共享您的数据,但您对自己生成的数据集的质量没有信心。通过本书,您将获得足够的信心来“在公共场合烹饪”,通过学习一些技巧和培养新习惯,确保数据科学环境的整洁和清洁。

无论如何,本书将帮助您重新审视数据清理,将其从一项沉重的工作转变为您质量、品味、风格和效率的象征。您应该有一定的编程基础,但不必非常精通。和大多数数据科学项目一样,愿意学习和实验,以及拥有强烈的好奇心和细致入微的关注力,都非常重要且值得重视。

约定

本书中,您会看到多种文本样式,用以区分不同类型的信息。以下是一些样式示例及其含义解释。

文本中的代码词、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 用户名显示如下:“问题在于open()无法处理 UTF-8 字符。”

代码块设置如下:

for tweet in stream:
    encoded_tweet = tweet['text'].encode('ascii','ignore')
    print counter, "-", encoded_tweet[0:10]
    f.write(encoded_tweet)

当我们希望引起您注意某个代码块的特定部分时,相关的行或项目会以粗体显示:

First name,birth date,favorite color,salary
"Sally","1971-09-16","light blue",129000
"Manu","1984-11-03","",159960
"Martin","1978-12-10","",76888

任何命令行输入或输出都如下所示:

tar cvf fileArchive.tar reallyBigFile.csv anotherBigFile.csv
gzip fileArchive.tar

新术语重要词汇以粗体显示。屏幕上看到的词语,例如菜单或对话框中的文字,出现在文本中是这样的:“按住Option键并选择列中的文本。”

注意

警告或重要提示会以框的形式呈现,如下所示。

提示

小贴士和技巧像这样呈现。

读者反馈

我们始终欢迎读者的反馈。告诉我们您对本书的看法——喜欢或不喜欢的部分。读者反馈对我们来说很重要,因为它帮助我们开发您真正能从中受益的书籍。

要向我们发送一般反馈,只需通过电子邮件发送至 <[email protected]>,并在邮件主题中提及书名。

如果您在某个领域有专业知识,并且有兴趣撰写或贡献一本书,请查看我们的作者指南:www.packtpub.com/authors

客户支持

现在,作为 Packt 书籍的骄傲拥有者,我们为您提供了一些帮助,让您最大化地利用您的购买。

下载本书的彩色图片

我们还为您提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图片。彩色图片将帮助您更好地理解输出中的变化。您可以从 www.packtpub.com/sites/default/files/downloads/Clean_Data_Graphics.zip 下载该文件。

勘误

尽管我们已尽力确保内容的准确性,但错误难免发生。如果您在我们的书籍中发现错误——可能是文本或代码中的错误——我们将非常感激您能向我们报告。这样,您不仅能帮助其他读者避免困扰,还能帮助我们改进书籍的后续版本。如果您发现任何勘误,请访问 www.packtpub.com/submit-errata 报告,选择您的书籍,点击勘误提交表单链接,并输入错误的详细信息。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该书标题下的勘误列表中。

若要查看之前提交的勘误,请访问 www.packtpub.com/books/content/support 并在搜索框中输入书名。所需信息将出现在勘误部分。

盗版

互联网盗版问题在所有媒体中都普遍存在。在 Packt,我们非常重视保护我们的版权和许可证。如果您在互联网上发现任何非法复制的我们作品,请立即提供该材料的具体位置或网站名称,以便我们采取相应措施。

请通过电子邮件 <[email protected]> 与我们联系,并提供涉嫌盗版材料的链接。

感谢您的帮助,保护我们的作者和我们为您提供有价值内容的能力。

问题

如果您对本书的任何部分有疑问,可以通过电子邮件 <[email protected]> 联系我们,我们将尽力解决问题。

第一章 为什么需要清洁数据?

大数据、数据挖掘、机器学习和可视化——看起来数据是最近计算领域中一切伟大事件的中心。从统计学家到软件开发人员再到图形设计师,每个人都突然对数据科学感兴趣。便宜的硬件、更好的处理和可视化工具以及大量免费可用的数据的结合意味着我们现在可以比以往更准确更轻松地发现趋势和进行预测。

但你可能没有听说过的是,所有这些数据科学的希望和梦想都基于数据是混乱的事实。通常情况下,数据需要被移动、压缩、清洗、切割、切片、切块,并经历任何其他变换,然后才能准备好用于我们认为是数据科学核心的算法或可视化。

在本章中,我们将涵盖:

  • 一个简单的包括数据清洁在内的数据科学六步骤流程

  • 有助于传达如何清洁数据的有用指南

  • 一些你可能会发现对数据清洁有帮助的工具

  • 一个简单的例子,展示数据清洁如何融入整体数据科学过程中

一种新鲜的视角

我们最近读到《纽约时报》称数据清洁为清洁工作,并称数据科学家 80%的时间将花在这种清洁工作上。正如我们在下图中看到的那样,尽管其重要性,数据清洁并没有像大数据、数据挖掘或机器学习那样真正吸引公众的想象力:

一种新鲜的视角

谁能责怪我们不想成群结队地讨论清洁工作有多有趣和超级酷呢?不幸的是——这对于实际的家务活也是如此——如果我们只是完成工作而不是忽视它、抱怨它并给它各种贬低的名字,我们所有人都会更好。

还不信?考虑一个不同的隐喻,你不是数据清洁工;你是数据大厨。想象一下,你被交给了一篮子盛满了你曾经见过的最美味的传统蔬菜,每一种都在新鲜和有机农场上经过精心挑选并且可持续生产。番茄是完美多汁的,生菜是脆的,辣椒是明亮而坚固的。你兴奋地开始烹饪,但四周都是脏乱的厨房,锅碗瓢盆上堆积着不知道是什么的脏污,而且,对于工具,你只有一把生锈的刀和一块潮湿的毛巾。水槽坏了,你刚刚看到一只甲虫从那些曾经美丽的生菜下爬出来。

即便是初学者厨师也知道,你不应该在这样的地方做饭。至少,你会毁掉那一篮子原本美味的食材。而最糟糕的是,你会让人们生病。而且像这样做饭根本不有趣,用一把生锈的刀切菜整天都切不完。

就像在厨房里一样,提前花时间清理和准备你的数据科学工作空间、工具和原材料是绝对值得的。1960 年代的老计算机编程格言“垃圾进,垃圾出”在数据科学中同样适用。

数据科学流程

清理数据如何融入数据科学的其余工作中呢?简而言之,它是一个关键环节,并且直接影响到前后的各个过程。

更长的答案依赖于通过六个步骤来描述数据科学过程,如下所示。数据清理正处于中间阶段,位于步骤 3。与其将这些步骤视为一个线性的、从开始到结束的框架,我们将根据需要在项目过程中多次回顾这些步骤,采用更迭代的方式进行处理。还值得指出的是,并非每个项目都会包含所有步骤;例如,有时我们没有收集步骤或可视化步骤。这完全取决于项目的具体需求:

  1. 第一步是提出问题陈述。明确你要解决的问题是什么。

  2. 下一步是数据收集与存储。帮助你回答这个问题的数据来自哪里?你将其存储在哪里,是什么格式?

  3. 接下来是数据清理。你是否对数据进行了任何更改?删除了什么吗?你是如何为接下来的分析和挖掘步骤做准备的?

  4. 下一步是数据分析和机器学习。你对数据做了什么样的处理?进行了哪些转换?使用了什么算法?应用了什么公式?用了哪些机器学习算法?顺序是什么?

  5. 表示与可视化是下一步。你如何展示你的工作结果?这可以是一个或多个表格、图纸、图表、图形、网络图、词云、地图等等。这是最适合表示数据的可视化方式吗?你考虑过哪些替代方案?

  6. 最后一步是问题解决。你在第一步提出的问题或问题的答案是什么?你的结果有什么局限性?是否有些问题你无法通过此方法回答?你本可以做些什么不同的事情?下一步该做什么?

数据清理在进行分析/挖掘/机器学习或可视化步骤之前是有道理的。不过,记住,由于这是一个迭代过程,在项目进行过程中,我们可能需要多次回顾清理工作。此外,我们将要进行的挖掘或分析类型通常会影响我们清理数据的方式。我们认为清理包括一系列任务,这些任务可能会根据所选的分析方法而有所不同,例如,转换文件格式、改变字符编码,或者解析数据中的某些部分进行处理。

数据清理还将与数据收集和存储步骤(步骤 2)紧密相关。这意味着你可能需要先收集原始数据,存储它,清理它,然后再存储已清理的数据,接着收集一些更多的数据,清理这些数据,将其与之前的数据合并,再次清理,存储,依此类推。因此,记住你做了什么,并能够在需要时重复该过程或告诉别人你做了什么,变得非常重要。

数据清理的沟通

由于六步过程是围绕一个故事情节组织的,从一个问题开始,最后解决该问题,因此它非常适合作为报告框架。如果你决定将六步框架作为报告数据科学过程的一种方式,你会发现,在第三步,你将能够写出关于数据清理的内容。

但即使你没有以正式报告的形式记录你的数据科学过程,你会发现,仔细记录你对数据所做的事情及其顺序是非常有帮助的。

记住,即使是最小、风险最低的项目,你也总是为至少两个人服务:现在的你和六个月后的你。相信我,当我告诉你六个月后的你不会记得今天的你是如何清理数据的,更不用说为什么这么做或如何再次进行清理了!

最简单的解决方案就是记录一个日志,记录你所做的事情。日志应包括链接、截图或你运行的具体命令的复制粘贴,并附上简短的解释说明为什么这么做。以下示例展示了一个非常小的文本挖掘项目的日志,其中嵌入了每个阶段输出文件的链接,以及清理脚本的链接。如果你对日志中提到的一些技术不熟悉,请不要担心。这个例子会告诉你日志可能是什么样子的:

  1. 我们写了一个 SQL 查询来检索每个项目及其描述的列表。

  2. 为了在 Python 中进行词频分析,我们需要特定格式的 JSON 数据。我们构建了一个 PHP 脚本,遍历查询的结果,将其结果存放在一个 JSON 文件中(版本 1)。

  3. 这个文件存在一些格式错误,比如未转义的引号和嵌入的 HTML 标签。这些错误通过第二个 PHP 脚本得到了修正,运行该脚本后,会打印出这个已清理的 JSON 文件(版本 2)。

请注意,我们的日志尝试解释我们做了什么以及为什么这么做。它简短且可以在可能的情况下包含链接。

如果你愿意使用更复杂的解决方案来进行数据清理的管理,有很多选择,例如,如果你熟悉版本控制系统,如Git或 Subversion,这些通常用于管理软件项目,那么你可能能想到如何扩展它们来跟踪数据清理的过程。无论你选择什么系统,哪怕是一个简单的日志,最重要的是实际上去使用它。因此,选择一个能鼓励你使用它而不是妨碍你进展的工具。

我们的数据清理环境

本书中使用的数据清理方法是一种通用且广泛适用的方法。它不要求或假设你拥有任何高端的单一供应商数据库或数据分析产品(事实上,这些供应商和产品可能有自己的清理程序或方法)。我设计了本书中的清理教程,围绕你在使用现实世界数据集时可能遇到的常见问题。我将本书设计为围绕任何人都可以访问的真实数据。我将向你展示如何使用开源的通用软件和技术来清理数据,这些工具易于获得且在职场中被广泛使用。

以下是你需要准备使用的一些工具和技术:

  • 对于几乎每一章,我们将使用一个终端窗口及其命令行界面,例如 Mac OSX 上的 Terminal 程序或 Linux 系统中的 bash。在 Windows 上,某些命令可以通过 Windows 命令提示符运行,但其他命令可能需要使用更全功能的 Windows 命令行程序,如 CygWin。

  • 对于几乎每一章,我们将使用文本编辑器或程序员编辑器,例如 Mac 上的 Text Wrangler,Linux 中的 vi 或 emacs,或 Windows 中的 Notepad++或 Sublime Editor。

  • 对于大多数章节,我们将需要一个 Python 2.7 客户端,例如 Enthought Canopy,并且我们需要足够的权限来安装软件包。许多示例将适用于 Python 3,但有些则不适用,因此,如果你已经安装了 Python 3,可能需要创建一个备用的 2.7 安装环境。

  • 对于第三章,清洁数据的主力工具——电子表格和文本编辑器,我们将需要一个电子表格程序(我们将专注于 Microsoft Excel 和 Google Spreadsheets)。

  • 对于第七章,RDBMS 清理技术,我们将需要一个可用的 MySQL 安装和客户端软件来访问它。

一个入门示例

为了开始,让我们用一个小例子来锐化我们的厨刀,该例子结合了六步框架,并说明如何处理一些简单的数据清理问题。这个例子使用了公开的 Enron 电子邮件数据集。这个数据集非常著名,包含了已故的 Enron 公司员工之间发送和接收的电子邮件。作为美国政府对 Enron 会计欺诈的调查的一部分,这些电子邮件成为了公共记录,现在任何人都可以下载。来自多个领域的研究人员发现这些电子邮件对于研究职场沟通、社交网络等非常有帮助。

注意

你可以在en.wikipedia.org/wiki/Enron上阅读更多关于 Enron 及其导致公司倒闭的财务丑闻的信息,此外,你还可以在en.wikipedia.org/wiki/Enron_Corpus上了解更多关于 Enron 电子邮件数据集的信息。

在这个例子中,我们将在一个简单的数据科学问题上实现六步框架。假设我们想揭示 Enron 公司内电子邮件使用的趋势和模式。我们将从按日期统计发送/接收的电子邮件开始。然后,我们将在图表上按时间展示这些计数。

首先,我们需要按照www.ahschulz.de/enron-email-data/上的说明下载 MySQL Enron 数据集。另一个(备用)下载源是www.cs.purdue.edu/homes/jpfeiff/enron.html。按照这些说明,我们需要将数据导入到 MySQL 服务器上的一个新的数据库架构中,名为Enron。数据现在已经可以通过 MySQL 命令行界面或通过 PHPMyAdmin 等基于 Web 的工具进行查询。

我们的第一个计数查询如下所示:

SELECT date(date) AS dateSent, count(mid) AS numMsg
FROM message
GROUP BY dateSent
ORDER BY dateSent;

立刻,我们注意到有许多电子邮件的日期不正确,例如,有一些日期似乎早于或晚于公司存在的时间(例如,1979 年),或者是一些年份显得不合逻辑(例如,0001 年或 2044 年)。电子邮件很旧,但可不是那么旧!

以下表格显示了部分异常行的摘录(完整的结果集大约有 1300 行)。所有这些日期格式都正确;然而,一些日期显然是错误的:

dateSent numMsg
0002-03-05 1
0002-03-07 3
0002-03-08 2
0002-03-12 1
1979-12-31 6
1997-01-01 1
1998-01-04 1
1998-01-05 1
1998-10-30 3

这些错误的日期很可能是由于邮件客户端配置不当造成的。此时,我们有三种选择来处理:

  • 什么都不做:也许我们可以忽略错误数据,照样制作折线图。但是,最低的错误日期来自公元 0001 年,最高的来自 2044 年,我们可以想象我们的折线图上有 1300 个时间轴刻度,每个刻度显示 1 或 2。这个图看起来既不吸引人,也不具备信息价值,因此什么都不做是不可行的。

  • 修复数据:我们可以尝试找出每条错误消息的正确日期,并生成一个修正后的数据集,然后用来构建我们的图表。

  • 丢弃受影响的电子邮件:我们可以做出一个明智的决定,丢弃任何日期超出预定范围的电子邮件。

为了在选项 2 和选项 3 之间做出决定,我们需要使用 1999-2002 年的窗口来统计受影响的消息数量。我们可以使用以下 SQL:

SELECT count(*) FROM message
WHERE year(date) < 1998 or year(date) > 2002;
Result: 325

325 条带有错误日期的消息最初看起来很多,但实际上它们仅占整个数据集的 1%左右。根据我们的目标,我们可能决定手动修正这些日期,但这里假设我们不介意丢失 1%的消息。我们可以谨慎地选择第 3 个选项,丢弃受影响的电子邮件。以下是修改后的查询:

SELECT date(date) AS dateSent, count(mid) AS numMsg
FROM message
WHERE year(date) BETWEEN 1998 AND 2002
GROUP BY dateSent
ORDER BY dateSent;

清理后的数据现在包含 1211 行,每行都有一个计数。以下是新数据集的一个片段:

dateSent numMsg
1998-01-04 1
1998-01-05 1
1998-10-30 3
1998-11-02 1
1998-11-03 1
1998-11-04 4
1998-11-05 1
1998-11-13 2

在这个例子中,看起来 1998 年 1 月有两个可疑的日期,直到 10 月才开始有规律地收到消息。这有些奇怪,同时也引出了另一个问题,我们是否需要在 x 轴上显示每一个日期,即使那一天没有发送电子邮件?

如果我们回答“是”,即使某些日期的计数为零,也需要显示每个日期;这可能意味着需要再进行一轮数据清理,以便生成显示零值日期的行。

但也许我们可以更有策略性地考虑这个问题。是否需要在原始数据中保留零值,实际上取决于我们使用的工具和图表的类型。例如,Google 表格会构建一个折线图或条形图,自动检测 x 轴上缺失的日期,并且即使初始数据集中没有给出,仍会填充零值。在我们的数据中,这些零值将代表 1998 年大部分时间里缺失的日期。

接下来的三张图展示了这些工具及它们如何处理日期轴上的零值。请注意,Google 表格中数据的表示方式在开始和结束处有较长的零尾:

示例图

Google 表格会自动填充任何缺失的日期为零。

D3 JavaScript 可视化库也会执行相同的操作,默认情况下填充缺失日期的零值,如下图所示。

提示

对于一个简单的 D3 折线图示例,看看这个教程:bl.ocks.org/mbostock/3883245

入门示例

D3 会自动用零填充任何缺失的日期。

Excel 在其默认折线图中也有相同的日期填充行为,如下所示:

入门示例

Excel 会自动用零填充任何缺失的日期。

接下来,我们需要考虑的是,允许日期的零值是否会使我们的 x 轴变得更长(我的计数查询返回了 1211 行,但指定的日期范围总共有 1822 天,范围是 1998-2002)。也许显示零计数的日期并不适用;如果图表太拥挤,我们反正也看不见间隙。

为了比较,我们可以快速将相同的数据导入到 Google 表格(你也可以在 Excel 或 D3 中做这件事),但这次我们只选择计数列来构建图表,从而强制 Google 表格 显示 x 轴上的日期。结果是只显示来自数据库计数查询的真正数据形状,未填充零计数日期。长尾消失了,但图表的关键部分(中间部分)的整体形状保持不变:

入门示例

现在,图表仅显示有一条或多条消息的日期。

幸运的是,数据的形状相似,除了图表的头部和尾部较短。基于这种比较,并且基于我们计划对数据进行的操作(记住,我们的目标只是创建一个简单的折线图),我们可以放心继续推进,而不需要专门创建一个包含零计数日期的数据集。

完成所有操作后,折线图显示了安然公司在电子邮件流量方面的几次显著峰值。最大的一些峰值和最重的流量出现在 2001 年 10 月和 11 月,当时丑闻爆发。两个较小的峰值发生在 2001 年 6 月 26-27 日和 2000 年 12 月 12-13 日,那时安然发生了类似的引人注目的事件(一次是加利福尼亚能源危机,另一次是公司领导层变动)。

如果你对数据分析感到兴奋,你可能会对如何进一步处理这些数据有各种酷点子。现在,既然你已经清理了数据,分析工作应该会更轻松,希望如此!

总结

经过这么多的工作,看起来《纽约时报》说得对。从这个简单的例子中可以看出,数据清理确实占据了解答一个即使是微小数据相关问题的 80%的工作量(在这个案例中,关于数据清理的 rationale 和选择的讨论就占用了 900 字案例研究中的 700 字)。数据清理真的是数据科学过程中的关键部分,它不仅涉及理解技术问题,还需要我们做出一些价值判断。作为数据清理的一部分,我们甚至必须考虑到分析和可视化步骤的预期结果,尽管我们还没有真正完成这些步骤。

在考虑本章所呈现的数据清理角色后,我们会更加明显地意识到,提升数据清理效率能够迅速转化为大量的时间节省。

下一章将介绍一些基本知识,任何想要进入更大、更好的“厨房”的“数据大厨”都需要掌握这些,包括文件格式、数据类型和字符编码等内容。

第二章。基础知识 - 格式、类型和编码

几年前,在我家每年一度的节日礼物交换活动中,我收到了一件非常有趣的礼物。那是一个garde manger厨房工具套装,包括各种不同的刀具以及削皮器、勺子和去皮器,用于准备蔬菜和水果。我学会了使用每一个工具,随着时间的推移,我对槽刀和番茄刀特别喜爱。本章就像是你的入门数据清理工具集。我们将审视:

  • 文件格式,包括压缩标准

  • 数据类型的基础知识(包括不同类型的缺失数据)

  • 字符编码

随着我们进入后面的章节,我们将需要所有这些基础知识。我们将覆盖一些概念,它们如此基础,以至于你几乎每天都会遇到,例如压缩和文件格式。这些是如此常见,就像厨师的刀一样。但是一些概念,如字符编码,更具特殊用途和异国情调,就像番茄刀一样!

文件格式

本节描述了数据科学家在处理野外数据时可能遇到的不同文件格式,换句话说,那些精心构建的数据集中不会出现的数据类型。在这里,我们遇到了与最常见的文件格式交互的一些策略和限制,然后我们回顾了各种压缩和存档格式。

文本文件与二进制文件的区别

当从在线来源收集数据时,你可能会以以下一种方式遇到数据:

  • 数据将以文件形式可供下载

  • 数据将通过交互式前端提供给存储系统,例如通过带有查询接口的数据库系统

  • 数据将通过连续流的形式提供

  • 数据将通过应用程序编程接口API)提供。

无论如何,你可能会发现自己需要稍后创建数据文件以便与他人分享。因此,对各种数据格式及其优缺点有坚实的基础是很重要的。

首先,我们可以将计算机文件视为属于两大类,通常称为文本文件二进制文件。严格来说,所有文件都由一系列字节组成,依次排列,因此严格地说,所有文件都是二进制的。但是如果文件中的字节全部都是文本字符(例如字母、数字以及一些控制字符,如换行符、回车符或制表符),那么我们称该文件为文本文件。相反,二进制文件则是那些包含大多数非人类可读字符的字节的文件。

打开和读取文件

文本文件可以通过叫做文本编辑器的程序进行读取和写入。如果你尝试在文本编辑器中打开一个文件并成功读取(即使你不理解它),那么它可能是一个文本文件。然而,如果你在文本编辑器中打开文件,看到的只是一些杂乱无章的字符和难以辨认的符号,那么它很可能是一个二进制文件。

二进制文件是为了通过特定的应用程序打开或编辑,而不是通过文本编辑器。例如,Microsoft Excel 表格是为了在 Microsoft Excel 中打开和读取的,数字相机拍摄的照片可以通过图形程序读取,如 Photoshop 或 Preview。有时,二进制文件可以通过多个兼容的软件包读取,例如,许多不同的图形程序可以读取和编辑照片,甚至为专有格式设计的二进制文件,例如 Microsoft Excel 或 Microsoft Word 文件,也可以通过兼容的软件读取和编辑,如 Apache OpenOffice。还有一些程序叫做二进制编辑器,可以让你查看二进制文件内部并进行编辑。

有时,文本文件也旨在被应用程序读取,但你仍然可以在文本编辑器中读取它们。例如,网页和计算机源代码仅由文本字符组成,可以轻松在文本编辑器中编辑,但没有一定的格式或布局训练,很难理解。

通常,即使不在编辑器中打开文件,也可以知道你拥有的是什么类型的文件。例如,大多数人会根据文件名寻找线索。三位数和四位数的文件扩展名是指示文件类型的常见方式。许多人都知道的常见扩展名包括:

  • .xlsx 用于 Excel 文件,.docx 用于 Word 文件,.pptx 用于 PowerPoint 文件

  • .png.jpg.gif 用于图像文件

  • .mp3.ogg.wmv.mp4 用于音乐和视频文件

  • .txt 用于文本文件

还有一些网站列出了文件扩展名以及与这些特定扩展名关联的程序。一个受欢迎的网站是 fileinfo.com,维基百科也提供了一个按字母顺序排列的文件扩展名列表,链接为 en.wikipedia.org/wiki/List_of_filename_extensions_(alphabetical)

查看文件内容

如果你必须打开一个未知文件以查看其内容,有几种命令行选项可以让你查看文件的前几个字节。

在 OSX 或 Linux 系统上

在 OSX Mac 或 Linux 中,打开终端窗口(在 Mac 中,你可以通过导航到 应用程序 | 实用工具 | 终端 来找到标准的终端应用程序),使用打印当前工作目录(pwd)和更改目录(cd)命令导航到文件所在位置,然后使用 less 命令逐页查看文件。以下是我执行此操作时的命令:

flossmole2:~ megan$ pwd
/Users/megan
flossmole2:~ megan$ cd Downloads
flossmole2:Downloads megan$ less OlympicAthletes_0.xlsx
"OlympicAthletes_0.xlsx" may be a binary file. See it anyway?

如果系统提示你“仍然查看”,那么该文件是二进制文件,你应该做好看到乱码的准备。你可以输入 y 来查看文件,或者输入 n 来退出。下图展示了使用 less 命令查看名为 OlympicAthletes_0.xlsx 的文件的结果,如对话框所示。真是一团乱麻!

在 OSX 或 Linux 上

查看完后,你可以输入 q 来退出 less 程序。

在 Windows 上

Windows 的命令提示符中也有 more 程序。它的功能与前面描述的 less 命令类似(毕竟,less is more)。你可以通过 开始 菜单中的 cmd 访问 Windows 命令提示符。在 Windows 8 中,导航到 应用程序 | Windows 系统 | 命令提示符。你可以像前面的示例中那样使用 cdpwd 来导航到你的文件。

常见的文本文件格式

本书中,我们主要关注的是文本文件,而不是二进制文件。(一些特殊例外包括压缩文件和归档文件,我们将在下一节讨论;Excel 和 PDF 文件,它们各自有独立的章节讨论。)

本书中我们关注的三种主要文本文件类型是:

  • 分隔符格式(结构化数据)

  • JSON 格式(半结构化数据)

  • HTML 格式(非结构化数据)

这些文件在布局上有所不同(它们在我们阅读时看起来不同),在结构的可预测性方面也有所差异。换句话说,文件中有多少部分是有组织且结构化的数据?文件中的数据有多少是无规律或非结构化的?在这里,我们将逐一讨论这些文本文件格式。

分隔符格式

分隔符文件是非常常见的一种数据共享和传输方式。分隔符格式文件就是每个数据属性(每个 )和每个数据实例(每个 )通过一个一致的字符符号进行分隔的文本文件。我们称这些分隔符为 分隔符。最常见的两个分隔符是制表符和逗号。这两种常见选择分别反映在 制表符分隔值TSV)和 逗号分隔值CSV)的文件扩展名中。有时,分隔符文件也被称为 面向记录 文件,因为假设每一行代表一个记录。

以下是三组数据实例(三行描述人名),数据值由逗号分隔。第一行列出了列名,这一行也被称为表头行,它在数据中被突出显示以便更清晰地查看:

First name,birth date,favorite color
Sally,1970-09-09,blue
Manu,1984-11-03,red
Martin,1978-12-10,yellow

注意,在这个分隔数据的例子中,没有非数据的信息。每一项内容都是一行或一个数据值,而且数据高度结构化。然而,仍然有一些选项可以区分不同的分隔格式。第一个区分点是数据的每一实例(每一行)是如何分隔的。通常,在行末使用换行符、回车符,或者两者结合,这取决于在创建文件时使用的操作环境。

查看不可见字符

在前面的例子中,换行符或回车符是不可见的。如何查看这些不可见字符呢?我们将在 Mac 上的 Text Wrangler 中读取相同的文件(类似的功能强大的编辑器如 Notepad++也适用于 Windows),在这里我们可以使用显示不可见字符选项(通过导航到查看 | 文本显示来启用)。

查看不可见字符

查看不可见字符的另一种方法是在 Linux 系统或 Mac 的终端窗口中使用vi(在 Windows 机器上默认不可用)。使用vi查看文件中不可见字符的过程如下:

  1. 首先,使用以下命令:

    vi <filename>

  2. 接着,输入:进入vi编辑模式。

  3. 然后,输入set list并按回车查看不可见字符。

以下截图显示了vi中通过set list揭示的行结束符,显示了行终止符号$。

查看不可见字符

用符号括起来的值以防止字符错误

分隔文件的另一个重要选项是使用什么字符来括住每个被分隔的值。例如,逗号分隔值非常好,但如果你的数字值中包含逗号作为千位分隔符呢?考虑以下例子,其中薪资值的千位分隔符用了逗号,而逗号又是分隔符:

First name,birth date,favorite color,salary
Sally,1971-09-16,light blue,129,000
Manu,1984-11-03,red,159,960
Martin,1978-12-10,yellow,76,888

如何在文件创建时修复这个问题?好吧,我们有两个选项:

  • 选项 1:创建分隔文件的人需要在创建表格之前移除最后一列中的逗号(换句话说,薪资金额中不能有逗号)。

  • 选项 2:创建这个文件的人需要使用额外的符号来括起数据值。

如果选择了选项 2,通常会添加双引号分隔符来括住数据值。所以,第一行的129,000会变成"129,000"

转义字符

如果数据本身包含引号呢?比如 Sally 最喜欢的颜色是light "Carolina" blue?看看这个例子:

First name,birth date,favorite color,salary
"Sally","1971-09-16","light "Carolina" blue","129,000"
"Manu","1984-11-03","red","159,960"
"Martin","1978-12-10","yellow","76,888"

内部引号必须通过使用另一个特殊字符——反斜杠 \ 来转义:

First name,birth date,favorite color,salary
"Sally","1971-09-16","light \"Carolina\" blue","129,000"
"Manu","1984-11-03","red","159,960"
"Martin","1978-12-10","yellow","76,888"

小贴士

或者,我们可以尝试用单引号来替代双引号进行封装,但那样我们可能会遇到拥有所有格的情况,比如“it's”或者像 O'Malley 这样的名字。总是会有些问题!

分隔文件非常方便,因为它们容易理解,并且可以在简单的文本编辑器中轻松访问。然而,正如我们所见,它们也需要提前进行一些规划,以确保数据值能够正确地分隔,并且一切格式都按照预期进行。

如果你不幸收到一个包含分隔错误的文件,就像前面那些错误一样,我们将在第三章中为你提供一些技巧和窍门,清洁数据的工作马——电子表格和文本编辑器,帮助你清理这些问题。

JSON 格式

JavaScript 对象表示法JSON),发音为 JAY-sahn,是用于所谓的半结构化数据的流行格式之一。与其名字中的含义相反,JSON 并不依赖于 JavaScript 才能使用。这个名字指的是它最初是为了序列化 JavaScript 对象而设计的。

当一组数据的值被标记,但其顺序不重要,甚至某些数据值可能缺失时,这组数据可以称为半结构化数据。一个 JSON 文件就是一组属性-值对,像这样:

{
  "firstName": "Sally",
  "birthDate": "1971-09-16",
  "faveColor": "light\"Carolina\" blue",
  "salary":129000
}

属性位于冒号的左边,值位于右边。每个属性之间用逗号分隔。整个实体被大括号包围。

有一些关于 JSON 的特点与分隔文件相似,也有一些不同。首先,字符串值必须用双引号括起来,因此字符串内部的任何双引号必须用反斜杠转义。(当反斜杠作为普通字符使用时,也必须进行转义!)

在 JSON 中,数字值不能包含逗号,除非这些值被当作字符串处理并正确地用引号括起来。(不过,在将数字转换为字符串时要小心——你确定要这么做吗?那么,请参考本章的 数据类型 部分,它可以帮助你解决这些问题。)

{
  "firstName": "Sally",
  "birthDate": "1971-09-16",
  "faveColor": "light\"Carolina\" blue",
  "salary": "129,000"
}

多值属性是可以接受的,层次结构值也可以使用(这些在分隔文件中很难做到)。这里有一个 JSON 文件的示例,包含一个 pet 的多值属性和一个 jobTitle 的层次数据。请注意,我们已将 salary 数据移入这个新的 job 层次结构中:

{
  "firstName": "Sally",
  "birthDate": "1971-09-16",
  "faveColor": "light\"Carolina\" blue",
  "pet":
  [
    {
      "type": "dog",
      "name": "Fido"
    },
    {
      "type": "dog",
      "name": "Lucky"
    }
  ],
  "job": {
    "jobTitle": "Data Scientist",
    "company": "Data Wizards, Inc.",
    "salary":129000
  }
}

JSON 实验

JSON 是一种非常流行的数据交换格式,因其可扩展性、简洁性以及对多值属性、缺失属性和嵌套属性/层次结构属性的支持而受到青睐。API 在分发数据集方面的日益流行也推动了 JSON 的实用性。

要查看 API 如何使用搜索词生成以 JSON 编码的数据集示例,我们可以尝试使用 iTunes API。iTunes 是由 Apple 运营的音乐服务。任何人都可以查询 iTunes 服务,获取有关歌曲、艺术家和专辑的详细信息。搜索词可以附加到 iTunes API 的 URL 上,如下所示:

itunes.apple.com/search?term=the+growlers

在这个 URL 中,=符号后面的部分是搜索词。在这种情况下,我搜索了一个我喜欢的乐队,叫做 The Growlers。请注意,+符号代表空格字符,因为 URL 不允许使用空格。

iTunes API 根据我的搜索关键词从其音乐数据库返回 50 个结果。整个结果集被格式化为一个 JSON 对象。与所有 JSON 对象一样,它被格式化为一组名称-值对。由于返回了 50 个结果,因此此示例中的 JSON 看起来非常长,但每个结果实际上非常简单——iTunes 数据中没有多值属性或任何层次结构的数据,显示在此 URL 中的数据也是如此。

注意

要了解更多关于如何使用 iTunes API 的信息,请访问 Apple iTunes 开发者文档:www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html

HTML 格式

HTML 文件,或者说网页文件,是另一种文本文件,通常包含很多有用的数据。你是否曾在网站上看到过有趣的表格或信息列表,想要保存这些数据?有时,复制和粘贴可以用来尝试从网页创建分隔文件,但大多数时候,复制和粘贴并不能有效工作。HTML 文件可能非常混乱,因此,它们可能是提取数据的痛苦方式。出于这个原因,有时我们称网页文件为非结构化数据。尽管网页可能有一些 HTML 标签,可以尝试基于模式的分隔数据组织,但它们并不总是这么做。而且这些 HTML 标签的应用也有很多出错的空间,无论是在不同的网站之间,还是在同一个网站内。

下图显示了weather.com网站的一小部分。即使截图中有图片、颜色和其他非文本元素,但在其基本层面,这个网页是用 HTML 编写的,如果我们想从这个页面提取文本数据,我们是可以做到的。

HTML 格式

如果我们查看网页的 HTML 源代码,我们可以找到包含约 1,000 行 HTML 代码的几行,这些代码包含了数据和布局指令,供浏览器显示该特定天气表格:

HTML 格式

确实是无结构的!当然,从技术上讲,可以从这个页面提取数据值 43(例如华氏温度),但这将不是一个愉快的过程,而且我们无法保证明天或后天我们的方法仍然有效,因为 weather.com 随时可能更改该站点的源代码。尽管如此,网上有大量的数据,因此在第五章,收集和清理来自网络的数据,我们讨论了几种从基于 Web 的非结构化文件中提取数据的策略。

归档和压缩

什么情况下文本文件也会是二进制文件?当然是在它被压缩或归档之后。什么是归档和压缩?在这一节中,我们将学习什么是文件归档和压缩文件,归档和压缩是如何工作的,以及它们各自的不同标准。

这一部分非常重要,因为在实际应用中,许多真实世界的数据(尤其是分隔符数据)都会在您找到它时被压缩。作为数据科学家,您最常遇到的压缩格式是什么?我们一定会找到这个问题的答案。您可能还想在与他人共享数据时对其进行压缩。如何确定哪种压缩方法是最适合的选择?

归档文件

归档文件只是一个包含多个文件的单一文件。文件内部可以是文本文件、二进制文件,或者两者的混合。归档文件是由一个特殊程序创建的,该程序会将文件列表转化为一个单一的文件。当然,归档文件的创建方式使得它们可以被展开回多个文件。

tar

在进行数据科学工作时,您最常遇到的归档文件是所谓的磁带归档TAR)文件,它们是通过 tar 程序创建的,通常以 .tar 为扩展名。它们的最初用途是创建磁带的归档。

tar 程序在类 Unix 操作系统上可用,我们也可以在 Mac OSX 终端中访问它。

要创建一个 tar 文件,您只需指示 tar 程序包含哪些文件以及输出文件的名称是什么。(程序选项c用于指示我们正在创建一个新的归档文件,选项v会在提取文件时打印文件名。f选项让我们指定输出文件的名称。)

tar cvf fileArchive.tar reallyBigFile.csv anotherBigFile.csv

要“解开”一个文件(或将其扩展为所有文件的完整列表),您只需将 tar 程序指向您想要展开的文件。x 字母在 xvf 中代表 eXtract:

tar xvf fileArchive.tar

所以,.tar 归档文件包含多个文件,但有多少个文件?这些文件是什么?在开始提取文件之前,您需要确保有足够的磁盘空间,并且这些文件确实是您一开始想要的。tar 命令中的 t 选项会显示 tar 文件中包含的文件列表:

tar –tf fileArchive.tar

除了 tar,还有许多其他归档程序,但一些有趣的程序(例如,OSX 上内置的 ZIP 压缩工具,以及 Windows 上的各种 ZIP 和 RAR 工具)不仅可以归档文件,还能对文件进行压缩,所以我们接下来应该讨论这个概念。

压缩文件

压缩文件是指那些已经被压缩以减少空间占用的文件。较小的文件意味着更少的磁盘存储空间,并且如果文件需要通过网络共享,传输时间也会更快。对于数据文件而言,像我们关注的文件,假设压缩文件可以很容易地解压回原始状态。

如何压缩文件

创建压缩文件的方法有很多,选择哪种方法取决于你使用的操作系统以及你安装了什么压缩软件。例如,在 OSX 上,任何文件或文件夹(或组)都可以通过在 Finder 中选择它,右键单击并从菜单中选择 压缩 来轻松压缩。此操作将在与原始文件相同的目录中创建一个压缩(.zip 扩展名)文件。下图展示了这一过程:

如何压缩文件

如何解压文件

数据科学过程中的收集步骤通常会包括下载压缩文件。这些文件可能是带有分隔符的文本文件,就像我们在本章前面描述的那样,或者可能是包含多种数据文件的文件,例如电子表格或用于构建数据库的 SQL 命令。

无论如何,解压文件会将数据恢复到我们可以用来实现目标的状态。我们如何知道使用哪个程序来解压文件呢?最大的线索是文件的扩展名。这是识别压缩文件所用程序的重要提示。知道如何解压文件取决于知道它是如何被压缩的。

在 Windows 中,你可以通过右键单击文件并选择 属性 来查看与文件扩展名关联的已安装程序。然后,查找 打开方式 选项,看看 Windows 认为哪个程序可以解压该文件。

本节的其余部分将概述如何在 OSX 或 Linux 系统上使用命令行程序。

使用 zip、gzip 和 bzip2 进行压缩

zip、gzip 和 bzip2 是最常见的压缩程序。它们的解压程序分别称为 Unzip、Gunzip 和 Bunzip2。

下表展示了在这些程序中压缩和解压的示例命令行。

压缩 解压
Zip zip filename.csv filename.zip unzip filename.zip
gzip gzip filename.csv filename.gz gunzip filename.gz
bzip2 bzip2 filename.csv filename.bz2 bunzip2 filename.bz2

有时,你会看到一个文件同时包含.tar.gz扩展名,或者是.bz2扩展名,比如:somefile.tar.gz。其他常见的组合包括:.tgz.tbz2,例如somefile.tgz。这些是首先使用 tar 进行归档(打包)后,再用 gzip 或 bzip2 进行压缩的文件。之所以这样,是因为 gzip 和 bzip2 并不是归档程序,它们只是压缩程序。因此,它们只能压缩单个文件(或文件归档)。而 tar 的作用是将多个文件合并成一个文件,所以这两种程序通常会一起使用。

tar 程序甚至有一个内置选项,可以在归档后立即 gzip 或 bzip2 文件。要对新创建的.tar文件进行 gzip 压缩,我们只需在之前的 tar 命令中添加z选项,并修改文件名:

tar cvzf fileArchive.tar.gz reallyBigFile.csv anotherBigFile.csv

或者,你可以分两步进行操作:

tar cvf fileArchive.tar reallyBigFile.csv anotherBigFile.csv
gzip fileArchive.tar

以下命令序列将创建fileArchive.tar.gz文件:

要解压 tar.gz 文件,可以使用两步:

gunzip fileArchive.tar.gz
tar xvf fileArchive.tar

这些步骤同样适用于 bzip2 文件:

tar cvjf fileArchive.tar.bz2 reallyBigFile.csv anotherBigFile.csv

要解压 tar.bz2 文件,可以使用两步:

bunzip2 fileArchive.tar.bz
tar xvf fileArchive.tar

压缩选项

在压缩和解压缩时,有许多其他选项你应该考虑,以便让你的数据清理工作更轻松:

  • 你想要压缩文件并保留原文件吗?默认情况下,大多数压缩和归档程序会删除原文件。如果你想保留原文件并同时创建压缩版本,通常可以指定这一点。

  • 你想要向现有的压缩文件中添加新文件吗?大多数归档和压缩程序都提供了这个选项。有时,这个操作被称为更新替换

  • 你想加密压缩文件并要求打开时输入密码吗?许多压缩程序提供了这个选项。

  • 在解压时,你想要覆盖目录中同名的文件吗?可以寻找强制选项。

根据你使用的压缩软件以及其选项,你可以使用这些选项中的许多来简化文件处理的工作。尤其是在处理大文件时——无论是文件本身很大,还是文件数量很多!

我应该使用哪个压缩程序?

本节关于归档和压缩的概念在任何操作系统和任何类型的压缩数据文件中都有广泛的适用性。大多数情况下,我们将从某处下载压缩文件,我们的主要任务是高效地解压这些文件。

然而,如果你自己创建压缩文件呢?如果你需要解压一个数据文件,清理它,然后重新压缩并发送给同事呢?或者如果你需要从多个压缩格式的文件中选择下载:zip、bz2 或者 gz 格式?你该选择哪个格式?

假设我们处在一个允许使用多种压缩类型的操作环境中,那么对于不同压缩类型的优缺点,有一些通用的经验法则。

在做压缩决策时,我们使用的一些因素有:

  • 压缩和解压缩的速度

  • 压缩比率(文件缩小了多少?)

  • 压缩解决方案的互操作性(我的受众能轻松解压这个文件吗?)

经验法则

Gzip 在压缩和解压缩速度上更快,并且在每台 OSX 和 Linux 机器上都可以轻松使用。然而,一些 Windows 用户可能没有准备好使用 gunzip 程序。

Bzip2 压缩出的文件比 gzip 和 zip 小,但需要更长时间。它在 OSX 和 Linux 上使用广泛。如果 Windows 用户没有安装特殊软件,可能会遇到处理 bzip2 文件的困难。

Zip 在 Linux、OSX 和 Windows 上都很常见,压缩和解压缩的速度也不差。然而,它的压缩比率并不理想(其他压缩工具能压缩出更小的文件)。不过,Zip 的普及度和相对较快的速度(与 bzip2 相比)是它的优势所在。

RAR 是一种广泛可用的 Windows 压缩和归档解决方案;然而,在 OSX 和 Linux 上使用时需要特殊软件,并且其压缩速度不如其他一些解决方案。

最终,你将不得不根据你正在从事的特定项目和受众或用户的需求来决定采用哪种压缩标准,无论这个用户是你自己、客户还是委托人。

数据类型、空值和编码

本节概述了数据科学家日常需要处理的最常见的数据类型,以及这些类型之间的一些差异。我们还讨论了数据类型之间的转换,以及如何安全地进行转换而不丢失信息(或至少提前了解风险)。

本节还涵盖了空值、空对象和空白的神秘世界。我们探讨了缺失数据的各种类型,并描述了缺失数据如何负面影响数据分析结果。我们将比较处理缺失数据的不同选择和权衡,以及每种方法的优缺点。

由于我们的大部分数据将以字符串形式存储,我们将学习如何识别不同的字符编码及一些你在实际数据中常遇到的格式。我们将学习如何识别字符编码问题,以及如何确定适合特定数据集的字符编码类型。我们还将编写一些 Python 代码,将一种编码方案转换为另一种编码方案。我们还将讨论这种策略的局限性。

数据类型

无论我们是在清理存储在文本文件、数据库系统还是其他格式中的数据,我们将开始识别出反复出现的相同类型的数据:各种各样的数字、日期、时间、字符、字符字符串等等。接下来的章节将描述一些最常见的数据类型,并给出每种类型的示例。

数值数据

在本节中,我们将发现存储数字的方式有很多种,其中一些比其他方式更容易清理和管理。不过,相较于字符串和日期,数字的处理相对简单,因此我们将在处理更复杂的数据类型之前,先从数字开始。

整数

整数,或称为自然数,可以是正数或负数,正如名称所示,它们没有小数点或分数。根据整数存储的系统不同,例如在数据库管理系统DBMS)中,我们还可能获得有关整数可以存储的最大大小,以及是否允许存储有符号(正负值)或仅无符号(全正值)的额外信息。

带小数的数字

在我们的数据清理工作中,带有小数部分的数字——例如价格、平均值、测量值等——通常使用小数点表示(而不是分子/分母)。有时,所使用的存储系统也会规定小数点后允许的位数(小数位数)以及数字总位数的限制(精度)。例如,我们说数字 34.984 的精度为 3,小数位数为 5。

不同的数据存储系统也允许不同类型的小数。例如,数据库管理系统(DBMS)可能允许我们在设置数据库时声明是否存储浮动点数字、小数数字以及货币/金额数字。每种类型的处理方式可能略有不同——例如在数学问题中。我们需要阅读 DBMS 为每种数据类型提供的指导,并密切关注变化。许多时候,DBMS 提供商会因为内存问题或其他原因而改变特定数据类型的规格。

另一方面,与 DBMS 应用程序不同,电子表格应用程序除了存储数据外,还设计用于显示数据。因此,我们可能会在一种格式下存储数字并以另一种格式显示它。如果在电子表格中的数据单元格应用了格式,这可能会引起一些困惑。以下图例显示了为某个单元格设置小数显示属性的示例。公式栏显示了完整的数字 34.984,但单元格显示该数字似乎已被四舍五入。

带小数的数字

提示

在世界许多地方,逗号字符用于将数字的小数部分与非小数部分分开,而不是使用点或句点字符。这提醒我们,始终检查你所在系统的本地化设置,并确保它们符合你正在处理的数据的预期。例如,在 OSX 中,有一个语言与地区对话框,位于系统偏好设置菜单中。从这里,你可以根据需要更改本地化设置。

与 DBMS 不同,原始文本文件没有选项来指定数字字段的大小或期望,且与电子表格不同,文本文件对给定数据值也没有显示选项。如果一个文本文件显示了值 34.98,那么这可能是我们对该数字的全部了解。

当数字不是数字时

数字数据首先由 0-9 的数字序列以及有时的十进制点组成。但关于真正的数字数据的一个关键点是,它主要是为了执行计算而设计的。当我们期望能够对数据值进行数学运算,期望对一个值与另一个值进行数值比较,或者当我们希望按数字顺序排序项目时,我们应选择数字存储方式。在这些情况下,数据值需要存储为数字。考虑以下按数值大小排序的数字列表,从小到大:

  • 1

  • 10

  • 11

  • 123

  • 245

  • 1016

现在,考虑将相同的列表按文本值排序,就像它们在地址字段中的表现一样:

  • 1 Elm Lane

  • 10 Pine Cir.

  • 1016 Pine Cir.

  • 11 Front St.

  • 123 Main St.

  • 245 Oak Ave.

电话号码和邮政编码(以及街道地址中的房号,如前面的例子所示)通常由数字组成,但当我们将它们视为数据值时,它们更像是文本数据还是数字数据?我们计划对它们进行加法、减法,还是计算它们的平均数或标准差?如果没有,它们可能更适合存储为文本值。

日期和时间

你可能熟悉许多不同的日期书写方式,并且可能有一些你偏好的方式。例如,以下是几种常见的写法:

  • 11-23-14

  • 11-23-2014

  • 23-11-2014

  • 2014-11-23

  • 23-Nov-14

  • 2014 年 11 月 23 日

  • 2014 年 11 月 23 日

  • 2014 年 11 月 23 日

无论我们如何偏好书写日期,完整的日期由三部分组成:月份、日期和年份。任何日期都应该能够解析为这些组成部分。日期中的困惑通常集中在两个方面:关于月份符号的模糊性,和对于 12 以下日期数字的日子符号不明确,以及关于年份指定的困惑。例如,如果我们只看到“11-23”,我们可以假设这是 11 月 23 日,因为没有“23”这个月份的缩写,但它是哪一年呢?如果我们看到一个“11-12”的日期,那是 11 月 12 日还是 12 月 11 日?而是哪一年呢?38 年是指 1938 年还是 2038 年?

大多数数据库管理系统(DBMS)都有特定的方式来导入日期数据,如果你将其指定为日期格式,当你导出数据时,也会以这种日期格式返回数据。然而,这些系统还提供了许多可以用来重新格式化日期或提取你需要的部分的功能。例如,MySQL 有许多有趣的日期函数,可以让我们仅提取月份或日期,以及更复杂的函数,帮助我们找出某个特定日期是年份中的第几周,或者它是星期几。例如,以下 SQL 语句统计了在第一章,为什么你需要清洁数据? 中, 每年 5 月 12 日发送的 Enron 数据集中的消息,并且还打印了该日期的星期几:

SELECT YEAR(date) AS yr, DAYOFWEEK(date) AS day, COUNT(mid) FROM message WHERE MONTHNAME(date) = "May" AND DAY(date) = 12
GROUP BY yr, day
ORDER BY yr ASC;

一些电子表格程序,如 Excel,内部将日期存储为数字,但允许用户使用内置格式或自定义格式将其显示为任何喜欢的形式。Excel 将给定日期的值存储为自 1899 年 12 月 31 日以来的日期的分数。你可以通过输入日期并选择常规格式,查看 Excel 中日期的内部表示,如下图所示。Excel 将 1986 年 5 月 21 日存储为31553

日期和时间

所以,当你在不同格式之间来回转换时,切换斜杠为破折号,或者交换月份和日期的顺序,Excel 实际上只是改变了你看到的日期“外观”,但在其内部,日期值的表示方式并没有改变。

为什么 Excel 需要使用分数形式来存储自 1899 年以来的日期?难道日期的天数不应该是整数吗?原来,小数部分是 Excel 存储时间的方式。

日期和时间

在前面的图中,我们可以看到一个内部日期值 31553 是如何映射到午夜的,但 31553.5(一天的中间)是中午,31553.75 是晚上 6 点。我们在小数点后添加的精度越高,内部表示的时间细节就越具体。

但并不是所有的数据存储系统都将日期和时间存储为小数形式,而且它们的起始时间也不相同。有些系统将日期和时间存储为自 Unix 纪元(1970 年 1 月 1 日 00:00:00,协调世界时)以来的秒数,而负数则用于存储纪元之前的时间。

数据库管理系统和电子表格应用程序都允许进行类似于数字的日期计算。在这两种系统中,都有函数可以让日期相减以找到差异,或进行其他计算,比如将若干周添加到某个日期上并得到一个新的日期值。

字符串

字符串表示任何字符数据的序列,包括字母、数字、空格和标点符号、数百种语言的字符以及各种特殊符号。字符串非常灵活,这使得它们成为最常见的数据存储方式。此外,字符串可以存储几乎所有其他类型的数据(不一定高效),因此它们成为传输数据或将数据从一个系统移到另一个系统的最低公分母。

与数字数据一样,我们目前使用的存储机制可能有一些我们需要遵循的字符串使用指南。例如,一个数据库管理系统(DBMS)或电子表格可能要求我们提前声明字符串的预期大小或我们预期的字符类型。字符编码是一个非常有趣且重要的领域,因此我们在本章稍后有一节专门讨论它。

或者,可能会有关于我们在特定环境中可以处理的数据大小的指南。在数据库的世界里,有定长和变长字符列,这些列设计用于存储较短的字符串,一些数据库管理系统(DBMS)厂商还设计了一种文本类型,用于存储更长的字符串。

注意

通常,许多数据科学家会扩展这一术语。当字符串数据变得更大、更难以处理时,它通常被称为文本数据,其分析就成为了文本分析或文本挖掘。

字符串(或文本)数据可以出现在我们在本章前面讨论的任何文件格式中(如分隔符文件、JSON 或网页),并且可以存储在或通过我们讨论的许多存储解决方案访问(如 API、DBMS、电子表格和文本文件)。但无论存储和传输机制如何,似乎字符串总是与“大型、杂乱、无结构数据”一起讨论。一个 Excel 专家在被要求解析几百个街道地址或将几千本书名分类时毫不犹豫。如果被要求计算一个单词列表中的字符频率,统计学家或程序员也不会感到惊讶。但当字符串操作变成“提取嵌入在 9000 万封用俄语写的电子邮件消息中的源代码”或“计算整个 Stack Overflow 网站内容的词汇多样性”时,事情就变得更加有趣了。

其他数据类型

数字、日期/时间和字符串是三大数据类型,但根据我们所处的环境,还有许多其他类型的特殊数据。在这里,我们简单列出一些更有趣的类型:

  • 集合/枚举:如果你的数据似乎只有几个可能的值,那么你可能正面对一个集合或枚举(enum)类型。枚举数据的一个例子可能是大学课程的可能最终成绩集合:{A, A-, B+, B, B-, C+, C, C-, D+, D, D-, F, W}。

  • 布尔值:如果你的数据仅限于两个选项之一,并且它们的值为 0/1 或真/假,那么你可能正在处理布尔数据。一个名为package_shipped的数据库列可能只有“是”或“否”的值,用来表示包裹是否已经发出。

  • BLOBs(二进制大对象):如果你的数据是二进制数据,例如,你存储的是图片文件的实际字节(而不仅仅是文件的链接)或者是 MP3 音乐文件的实际字节,那么你可能正在处理 BLOB 数据。

数据类型转换

数据类型之间的转换是数据清理工作中不可避免的一部分。你可能会得到字符串数据,但你知道你需要对其进行数学运算,因此希望将其存储为数字。你可能会得到一种日期字符串,格式为某种形式,但你希望它以不同的日期格式显示。不过,在继续之前,我们需要讨论转换过程中的潜在问题。

数据丢失

在从一种数据类型转换到另一种数据类型时,可能会丢失数据。通常,这发生在目标数据类型无法存储与原始数据类型一样多的详细信息时。有时,我们并不会因为数据丢失而感到不安(事实上,有时清理过程中可能故意丢失一些数据),但如果我们没有预料到数据丢失,它可能会带来灾难性的后果。风险因素包括:

  • 同一数据类型之间的不同大小转换:假设你有一个 200 个字符的字符串列,你决定将数据移到一个只有 100 个字符长的列中。那么,超过 100 个字符的任何数据可能会被截断或削减。这种情况也可能发生在数字列大小转换时,比如从大整数转换为普通整数,或者从普通整数转换为小整数时。

  • 在不同精度级别之间的转换:假设你有一个精确到四位数字的十进制数字列表,然后你将它们转换为精度为两位的数字,或者更糟的是,转换为整数。每个数字都会被四舍五入或截断,从而失去原有的精度。

转换策略

处理数据类型转换有许多策略,具体使用哪种方法取决于数据当前存储的位置。我们将讨论两种在数据科学清理过程中最常见的数据类型转换方法。

第一种策略,基于 SQL 的操作,适用于当我们有数据库中的数据时。我们可以使用几乎所有数据库系统中的数据库函数,将数据切割并转换成不同的格式,以便导出为查询结果或存储在另一列中。

第二种策略是基于文件的操作,适用于我们已经获得了一个平面数据文件——例如,一个电子表格或 JSON 文件——并且在从文件中读取数据后,需要以某种方式操作数据类型。

SQL 层级的数据类型转换

在这里,我们将介绍几个常见的案例,展示如何使用 SQL 来操作数据类型。

示例一 - 解析 MySQL 日期并转换为格式化字符串

在这个例子中,我们将回到我们在第一章中使用的 Enron 电子邮件数据集,为什么需要清理数据?。和之前的例子一样,我们将查看消息表,其中包含我们之前使用过的日期列,这些日期列存储为datetime MySQL 数据类型。假设我们想打印一个完整的日期,包括拼写出来的月份(而非数字),甚至是星期几和一天中的时间。我们应该如何做到这一点?

对于消息 ID(mid)为 52 的记录,我们得到:

2000-01-21 04:51:00

我们想要的是这样:

4:51am, Friday, January 21, 2000
  • 选项 1:使用concat()和单独的日期与时间函数,如下所示的代码示例。这个选项的弱点是无法轻松打印出上午/下午(a.m./p.m.):

    SELECT concat(hour(date),
    ':',
    minute(date),
    ', ',
    dayname(date),
    ', ',
    monthname(date),
    ' ',
    day(date),
    ', ',
    year(date))
    FROM message WHERE mid=52;
    
    

    结果:

    4:51, Friday, January 21, 2000
    
    

    如果我们决定真的需要上午/下午(a.m./p.m.),我们可以使用一个 if 语句,测试小时数,并在小时小于 12 时打印"a.m.",否则打印"pm":

    SELECT concat(
     concat(hour(date),
     ':',
     minute(date)),
     if(hour(date)<12,'am','pm'),
     concat(
     ', ',
     dayname(date),
     ', ',
     monthname(date),
     ' ',
     day(date),
     ', ',
     year(date)
     )
    )
    FROM message
    WHERE mid=52;
    
    

    结果:

    4:51am Friday, January 21, 2000
    
    

    小贴士

    MySQL 的日期和时间函数,如day()year(),在它们的文档中有详细描述:dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html,它们的字符串函数,如concat(),可以在这里找到:dev.mysql.com/doc/refman/5.7/en/string-functions.html。其他数据库管理系统也会有类似的函数版本。

  • 选项 2:使用更复杂的date_format() MySQL 函数。该函数接受一系列字符串说明符,用于指定日期格式化方式。在 MySQL 文档中有一个非常长的说明符列表。为了将日期转换为我们需要的格式,下面是一个完成的示例代码:

    SELECT date_format(date, '%l:%i%p, %W, %M %e, %Y')
    FROM message
    WHERE mid=52;
    
    

    结果:

    4:51AM, Friday, January 21, 2000
    
    

    这已经很接近我们想要的结果,而且比选项 1 更简短。唯一的区别是上午/下午(a.m./p.m.)是大写的。如果我们真的希望它是小写的,可以这样做:

    SELECT concat(
     date_format(date, '%l:%i'),
     lower(date_format(date,'%p ')),
     date_format(date,'%W, %M %e, %Y')
    )
    FROM message
    WHERE mid=52;
    
    

    结果:

    4:51am, Friday, January 21, 2000
    
    

示例二 - 将字符串转换为 MySQL 的日期类型

在这个例子中,让我们看看 Enron 模式中的一个新表:名为referenceinfo的表。这个表展示了其他消息所引用的消息。例如,表中的第一个条目,rfid2,包含了消息 79 所引用的电子邮件的文本。该列是一个字符串,数据部分如下所示(部分内容):

> From: Le Vine, Debi> Sent: Thursday, August 17, 2000 6:29 PM> To: ISO Market Participants> Subject: Request for Bids - Contract for Generation Under Gas> Curtailment Conditions>> Attached is a Request for Bids to supply the California ISO with> Generation

这是一串非常混乱的字符串!让我们来处理一下,提取出第一行显示的日期,并将其转换为 MySQL 日期类型,适合插入到另一个表中,或者进行日期计算。

为此,我们将使用内置的 str_to_date() MySQL 函数。这个函数有点像我们之前看到的 date_format(),不过它是反向的。下面是一个工作示例,它会查找单词 Sent: 并提取紧跟其后的字符,直到 > 符号,然后将这些字符转换为真实的 MySQL 日期时间数据类型:

SELECT
str_to_date(
 substring_index(
 substring_index(reference,'>',3),
 'Sent: ',
 -1
 ),
 '%W,%M %e, %Y %h:%i %p'
)
FROM referenceinfo
WHERE mid=79;

结果:

2000-08-17 18:29:00

现在我们有一个准备好插入到新的 MySQL 列中或进行更多日期函数或计算的日期时间值。

示例三 – 将 MySQL 字符串数据转换为十进制数字

在这个示例中,我们将讨论如何将隐藏在文本列中的数字转换为适合计算的格式。

假设我们有兴趣从 Enron 接收到的某些邮件中提取油桶的价格(简称 bbl)。我们可以编写一个查询,每当我们在某个发件人的邮件中看到 /bbl 字符串时,查找前面的美元符号并提取后面的数字值,将其转换为十进制数字。

以下是来自 Enron 消息表中一条电子邮件消息的示例片段,消息 ID(mid)为 270516,展示了数字在字符串中的样子:

March had slipped by 51 cts at the same time to trade at $18.47/bbl.

执行此字符串提取并转换为十进制的 MySQL 命令如下:

SELECT convert(
 substring_index(
 substring(
 body,
 locate('$',body)+1
 ),
 '/bbl',
 1
 ),
 decimal(4,2)
 ) as price
FROM message
WHERE body LIKE "%$%" AND body LIKE "%/bbl%" AND sender = '[email protected]';

添加了 WHERE 子句的限制,以确保我们只获取包含 bbl 油价的消息。

convert() 函数类似于 MySQL 中的 cast()。大多数现代数据库系统都有类似的方式将数据类型转换为数字。

文件级别的类型转换

在本节中,我们将展示一些常见的情况,当数据类型需要在文件级别进行处理时。

提示

这些内容实际上只适用于具有隐式类型结构的文件类型,例如电子表格和类似 JSON 的半结构化数据。我们这里没有使用分隔符(仅文本)平面文件的示例,因为在文本文件中,所有数据都是文本数据!

示例一 – Excel 中的类型检测和转换

你可能熟悉在 Excel 和类似的电子表格应用程序中通过单元格格式化菜单选项进行类型转换。典型的操作流程包括选择你想要更改的单元格,并使用位于功能区的下拉菜单。

示例一 – Excel 中的类型检测和转换

或者,如果这些选项不够,可以使用 格式单元格 对话框,该对话框位于格式菜单中,提供了对转换过程输出的更细粒度的控制。

示例一 – Excel 中的类型检测和转换

较不为人知的 istext()isnumber() 函数在 Excel 中也可能在格式化数据时非常有用。

示例一 – Excel 中的类型检测和转换

这些函数可以应用于任何单元格,返回 TRUEFALSE,取决于数据是否为文本,或者在 isnumber() 的情况下,数字是否真的为数字。结合条件格式等功能,这两个公式可以帮助你在少量数据中定位错误值或输入错误的值。

Excel 也有一些简单的函数可以将字符串转换为其他数据类型,除了使用菜单。下图展示了 TEXT() 函数用于将数字日期转换为 yyyy-mm-dd 格式的字符串版本。在公式栏中,我们输入 =TEXT(A4,"yyyy-mm-dd")36528 这个数字被转换为 2000-01-03。日期字符串现在以我们指定的格式显示。

示例一——Excel 中的类型检测和转换

示例二——JSON 中的类型转换

JSON 作为一种半结构化的文本格式,并未提供很多关于格式化和数据类型的选项。回顾本章前面对 JSON 的描述,JSON 对象是由名称-值对构建的。构成名称-值对的值部分格式化选项仅限于文本字符串、数字或列表。虽然可以手动构建 JSON 对象——例如,通过在文本编辑器中输入——通常我们是通过编程的方式来构建 JSON 对象,无论是从数据库导出 JSON,还是通过编写一个小程序将一个平面文本文件转换为 JSON。

如果我们设计用于生成 JSON 的程序出现缺陷,会发生什么呢?假设程序给我们的是字符串而不是数字。这种情况偶尔会发生,并且可能对任何设计来消费 JSON 的程序产生意想不到的后果。以下是一个例子,展示了一些简单的 PHP 代码,旨在生成 JSON,之后将其读取到 D3 中构建图表。

用于从数据库生成数据集的 JSON 表示的 PHP 代码是直接的。如下所示的示例代码连接到 Enron 数据库,构建查询,执行查询,将查询结果的每个部分放入数组中,然后将数组值编码为 JSON 名称-值对。以下是构建日期和计数列表的代码,和我们在 第一章 中使用的日期和计数一样,为什么你需要干净的数据?

<?php
// connect to db
$dbc = mysqli_connect('localhost','username','password','enron')
       or die('Error connecting to database!' . mysqli_error());

// the same sample count-by-date query from chapter 1
$select_query = "SELECT date(date) AS dateSent, count(mid) AS numMsg FROM message GROUP BY 1 ORDER BY 1";
$select_result = mysqli_query($dbc, $select_query);
// die if the query failed
if (!$select_result)
       die ("SELECT failed! [$select_query]" .  mysqli_error());
// build a new array, suitable for json printing
$counts = array();
while($row = mysqli_fetch_array($select_result))
{
     array_push($counts, array('dateSent' => $row['dateSent'], 'numMsg'   => $row['numMsg']));
}
echo json_encode($counts);
?>

提示

请注意,json_encode() 函数要求 PHP 版本为 5.3 或更高,本示例依赖于我们在 第一章 中构建的相同的 Enron 数据库,为什么你需要干净的数据?

这里的问题是,结果被转换成了字符串——PHP 已将 numMsg 的数字值放在了引号中,这在 JSON 中表示字符串值:

[
  {"dateSent":"0001-05-30","numMsg":"2"},
  {"dateSent":"0001-06-18","numMsg":"1"}
]

为了让 PHP 函数更加小心地处理数字值,而不是单纯地假设所有内容都是字符串,我们需要在将它们打印到屏幕之前,专门将这些值转换为数字。只需要改变我们调用json_encode()的方式,让它看起来像这样:

echo json_encode($counts, JSON_NUMERIC_CHECK);

现在,JSON 结果中已经包含了numMsg的实际数字:

[
  {"dateSent":"0001-05-30","numMsg":2},
  {"dateSent":"0001-06-18","numMsg":1}
]

PHP 还包括类似的函数,用于将大整数转换为字符串。当你有极其大的数字,但出于某种原因需要将其存储为字符串数据时,这个功能很有用,例如,在存储会话值或 Facebook、Twitter 用户 ID 值时。

如果我们无法控制生成 JSON 输出的 PHP 代码——例如,如果我们通过 API 访问 JSON 而无法控制它的生成——那么我们就需要在 JSON 已经构建完成后进行转换。在这种情况下,我们需要请求我们的 D3 JavaScript 代码使用+操作符将字符串强制转换为数字。 在这个例子中,我们已经读取了 JSON 输出,并准备构建图表。numMsg值已经从字符串强制转换为数字:

d3.json("counts.json", function(d) {
  return {
    dateSent: d.dateSent,
    numMsg: +d.numMsg
  };
}, function(error, rows) {
  console.log(rows);
});

如果一个 null 掉进了森林……

在这一部分,我们将把厨师的勺子伸进充满零、空值和 null 的神秘炖菜中。

这些有什么区别呢?好吧,既然我提到了炖菜,先看这个例子。假设你有一台顶级炉灶,位于你豪华的厨师厨房里,在那台炉子上,你要准备一锅浓厚、丰富的炖菜。你的副厨师问你:“锅里有多少炖菜?”看看这些选项:

  1. 一天开始时,你发现炉子上没有锅。问题“锅里有多少炖菜?”是无法回答的。没有锅!答案既不是正值,也不是零值,甚至不为空。值是NULL

  2. 几个小时后,在经过大规模的清洗和切割之后,你看着锅里,发现有三升美味的炖菜。太棒了;你现在有了问题的答案。在这种情况下,我们的答案是数据值为 3

  3. 午餐高峰过后,你再次查看锅,发现炖菜已经没有了。每一滴都已经卖完了。你看了,看了量,发现“锅里有多少炖菜?”的答案是。你把锅送去水槽清洗。

  4. 在晚餐前,你从水槽拿起干净的锅。走过厨房时,副厨师问:“锅里有什么?”目前,锅是空的。锅里什么都没有。注意,这个答案不是零,因为他问的问题并不是数字问题。答案也不是 NULL,因为我们确实有锅,我们确实看过锅,但只是没有找到答案。

不同的数据和编程环境(如数据库管理系统、存储系统和编程语言)对这些零、空值和 NULL 的处理略有不同。并非所有系统都会清晰地区分这四种情况。因此,本章内容写得比较通用,给出示例时,我们尽量指出这些示例应用于哪些环境。了解在每个环境中当你说某样东西是 NULL、空值或零时的具体含义非常重要。当你在实际数据集中看到这些值时,明确你可以(或不能)对每个值做出什么假设也非常重要。

小贴士

处理 Oracle 数据库时有一个特别的注意事项:空值、空白和 NULL 在 Oracle 中与许多其他系统不同。在此部分处理时请小心,并查阅 Oracle 的数据库文档,以了解特定的细节。

首先,最重要的一点。在零、空值和 NULL 值中,最容易处理的是零值。零是一个可测量的量,并且在数字系统中有意义。我们可以对零进行排序(它排在 1, 2, 3…之前),也可以使用方便的数轴(-2, -1, 0, 1, 2, 3…)来与其他数字进行比较。我们还可以对零进行数学运算(除零以外,除零运算总是很麻烦)。

作为一个合法的值,零作为数字数据最为合适。零的字符串值意义不大,因为它最终只会被解读为 0,或者是字符 0,这可能并不是我们最初的意图。

空值

空值比零稍微难处理一些,但在某些情况下它们很有意义,特别是处理字符串时。例如,假设我们有一个名为“中间名”的属性。嗯,我没有中间名,因此我总是希望将此字段留空。(有趣的事实:我母亲至今还讲述我幼儿园毕业证书上有一个临时编造的中间名的故事,因为我太害羞,不敢告诉老师我没有中间名。)对一个真正为空的值填充空格或连字符(或编造一些内容)是没有意义的。空格不同于空值。在空字符串的情况下,正确的值可能实际上是“空”。

在一个 CSV 或分隔符文件中,空值可能看起来像这样——在这里,我们已经将第二和第三条记录的最爱颜色值清空:

First name,birth date,favorite color,salary
"Sally","1971-09-16","light blue",129000
"Manu","1984-11-03","",159960
"Martin","1978-12-10","",76888

例如,在INSERT数据库中,要将Manu记录插入 MySQL 系统,我们可以使用如下代码:

INSERT INTO people (firstName, birthdate, faveoriteColor, salary) VALUES ("Manu","1984-11-03","",159960);

有时,半结构化数据格式(如 JSON)会允许空对象和空字符串。请看这个例子:

{
  "firstName": "Sally",
  "birthDate": "1971-09-16",
  "faveColor": "",
  "pet": [],
  "job": {
    "jobTitle": "Data Scientist",
    "company": "Data Wizards, Inc.",
    "salary":129000
  }
}

在这里,我们已经去掉了 Sally 的宠物,并将她的最爱颜色设置为空字符串。

空白

请注意," "(两个双引号之间有一个空格,有时称为空白,但更恰当地称为空格)不一定等同于""(两个紧挨在一起的双引号,有时也称为空白,但更恰当地称为空)。考虑这两个 MySQL INSERT 语句之间的区别:

-- this SQL has an empty for Sally's favoriteColor and a space for Frank's
INSERT INTO people (firstName, birthdate, faveoriteColor, salary)
VALUES ("Sally","1971-09-16","",129000),
 ("Frank","1975-10-23"," ",76000);

除了空格外,有时还会误将其他不可见字符(如制表符和换行符)误解为空或空白。要小心这些情况,当有疑问时,可以使用本章前面介绍的一些技巧来查找不可见字符。

空值

我知道,如果你在字典中查找null,可能会说它的意思是零。但不要被愚弄了。在计算机中,我们对 NULL 有一整套特殊的定义。对我们来说,NULL 不是什么都没有;事实上,它甚至不是空的缺席。

这与空有什么不同呢?首先,空可以等于空,因为空字符串的长度为 0。因此,我们可以想象一个存在的值,可以进行比较。然而,NULL 不能等于 NULL,NOT NULL 也不会等于 NOT NULL。我听说过有人建议我们应该把“NULL 不等于任何东西,甚至不等于自己”作为口头禅。

当我们确实希望某个数据值不要有任何输入时,我们使用 NULL。我们甚至不想把锅放在火上!

为什么中间名示例是“empty”,而不是 NULL?

好问题。回想一下从炖锅类比中,如果我们问了问题而答案是空的,这与永远得不到答案(NULL)是不同的。如果你问我我的中间名是什么,而我告诉你我没有中间名,那么数据值就是空的。但是如果你只是不知道我是否有中间名,那么数据值就是 NULL。

使用零而不是空或 null 来清理数据有时是有用的吗?

或许吧。还记得在第一章中的电子邮件示例中,为什么需要清理数据?,我们讨论了如何在我们的折线图中添加缺失的日期,并且各种电子表格程序在创建具有缺失日期的折线图时自动填充了零的计数吗?即使我们没有在这些日期计算电子邮件,我们的图表也会插值出缺失值,就好像它们是零一样。我们对此满意吗?嗯,这取决于情况。如果我们可以确认电子邮件系统在这些日期上是活动的,并且我们确信我们收集了所有发送的电子邮件,那么这些日期的计数确实可以推断为零。然而,在我们在第一章中使用的安然电子邮件中,我们相当确定这种情况是对的。

另一个情况可能有用的地方是在存储零类型数据而不是空数据时,例如,如果您知道月份和年份,但不知道日期,而且必须将数据插入到完整的日期列中。在这种情况下,填入 2014-11-00 可能是一个不错的选择。但当然,您应该记录此操作(参见 第一章 中关于 数据清理沟通 的部分,为什么需要清洁数据?),因为您六个月后再查看这些数据时,您所做的和原因可能并不明显!

字符编码

在计算机的早期,每个字符串值都必须由仅有的 128 个不同符号构成。这种早期的编码系统被称为美国信息交换标准代码ASCII),主要基于英文字母,并且一成不变。这 128 个字符包括 a-z、A-Z、0-9、一些标点符号和空格,以及现在无用的电传代码。在我们的数据科学厨房里,今天使用这种编码系统就像是做冷冻晚餐。是的,它便宜,但也缺乏多样性和营养,你真的不能指望招待客人。

20 世纪 90 年代初,提出并标准化了一种可变长度编码系统,现在称为UTF-8。这种可变长度方案允许更多的自然语言符号以及所有数学符号得到适当编码,并提供了丰富的未来扩展空间。(所有这些符号的列表称为Unicode。Unicode 符号的编码称为 UTF-8。)现在还有 UTF-16 编码,其中每个字符至少需要两个字节来编码。在撰写本文时,UTF-8 是 Web 的主要编码方式。

对于本书的目的,在大多数情况下,我们主要关注的是如何处理存在一种编码的数据,必须通过转换为另一种编码来进行清洁。一些可能相关的示例情景包括:

  • 您有一个 MySQL 数据库,使用简单的编码(如 MySQL 默认的一种 256 位 Latin-1 字符集)创建,并且将 UTF-8 数据存储为 Latin-1,但现在您希望将整个表转换为 UTF-8。

  • 您有一个使用 ASCII 设计的 Python 2.7 程序,但现在必须处理 UTF-8 文件或字符串。

在本节中,我们将根据这些场景的几个基本示例进行工作。有许多同样可能的情况会导致您遇到字符编码问题,但这将是一个起点。

示例一 - 在 MySQL 数据中查找多字节字符

假设我们有一个数据列,并且我们想知道该列中有多少值实际上使用了多字节编码。通过将字符的字节长度(使用 length() 函数)与字符的字符长度(使用 char_length() 函数)进行比较,我们可以发现那些看似只有一个字符但实际上需要多个字节来编码的字符。

提示

以下示例使用的是 MyISAM 版本的 MySQL 世界数据库,该数据库作为 MySQL 文档的一部分可以在dev.mysql.com/doc/index-other.html找到。

默认情况下,MyISAM 版本的 MySQL 世界测试数据库使用 latin1_swedish_ci 字符编码。因此,如果我们查询一个国家名称中包含特殊字符的记录,我们可能会看到类似科特迪瓦(Côte d'Ivoire)这样的编码问题:

SELECT Name, length(Name)
FROM Country
WHERE Code='CIV';

这个示例显示了国家名称的长度为 15,而科特迪瓦(Côte d'Ivoire)的名称被编码为 CÙte díIvoire。还有一些其他列中的条目也被奇怪地编码。为了修复这个问题,我们可以使用以下 SQL 命令将名称列的默认排序规则更改为 utf8

ALTER TABLE  Country CHANGE Name `Name` CHAR(52) CHARACTER SET utf8 COLLATE utf8_general_ci
 NOT NULL DEFAULT  '';

现在我们可以清空表格并再次插入 239 行国家数据:

TRUNCATE Country;

现在我们有了使用 UTF-8 编码的 Country 名称。我们可以通过运行以下 SQL 来测试是否有任何国家名称使用了多字节字符表示:

SELECT *  FROM Country WHERE length(Name) != char_length(Name);

它显示科特迪瓦(Côte d'Ivoire)和法国岛屿留尼汪(Réunion)都有多字节字符表示。

这里有另一个示例,假设你没有世界数据集,或者实际上没有任何数据集。你可以运行一个 MySQL 查询命令来比较多字节字符:

SELECT length('私は、データを愛し'), char_length('私は、データを愛し');

在这个示例中,日语字符的长度为 27,但字符长度为 9。

这个技巧用于测试我们数据的字符集—也许你有太多的行无法逐一查看,你只想要一个 SQL 语句来一次性显示所有多字节条目,以便你能够规划如何清理它们。这个命令会显示当前具有多字节格式的数据。

示例二——查找 MySQL 中存储的 Unicode 字符的 UTF-8 和 Latin-1 等效字符

以下代码将使用 convert() 函数和 RLIKE 运算符,打印出使用 Latin-1 保存到 MySQL 中的 Unicode 字符的 UTF-8 等效字符。这对于你有多字节数据且该数据已存储在 MySQL 中使用 Latin-1 编码的文本列中时非常有用,这是一个不幸的常见情况,因为 Latin-1 仍然是默认字符集(截至 MySQL 5)。

这段代码使用了公开且广泛使用的 Movielens 数据库,包括电影及其评论。整个 Movielens 数据集在许多网站上都有广泛提供,包括原始项目网站:grouplens.org/datasets/movielens/。另一个 SQL 友好的链接是:github.com/ankane/movielens.sql。为了方便您进行示例操作,作者在本书的 GitHub 网站上提供了只包含相关行子集的 CREATEINSERT 语句:github.com/megansquire/datacleaning/blob/master/ch2movies.sql。这样,如果您愿意,您可以直接使用该代码创建这一张表并进行后续的示例操作。

SELECT convert(
 convert(title USING BINARY) USING latin1 ) AS 'latin1 version', 
convert(
 convert(title USING BINARY) USING utf8
) AS 'utf8 version'
FROM movies WHERE convert(title USING BINARY)
 RLIKE concat(
 '[', unhex('80'), '-', unhex('FF'), ']'
 );

以下截图显示了在 Movielens 数据库的电影表中的 Latin-1 编码标题列上运行此命令后,前 3 部电影的结果:

示例二 - 查找 MySQL 中存储的 Unicode 字符的 UTF-8 和 Latin-1 等价字符

注意

有关将现有数据库转换为 UTF-8 的建议吗?

由于 UTF-8 在网页上的普及以及它在准确传达全球各地自然语言书写的信息中的重要性,我们强烈建议您使用 UTF-8 编码方案创建新的数据库。从一开始就使用 UTF-8 编码要比以后再做转换容易得多。

然而,如果您已经创建了非 UTF-8 编码的表格,但它们尚未填充数据,您需要将表格更改为 UTF-8 编码,并将每一列的字符集更改为 UTF-8 编码。然后,您就可以准备插入 UTF-8 数据了。

最困难的情况是,当您已有大量数据采用非 UTF-8 编码,并且希望在数据库中就地转换时。在这种情况下,您需要进行一些规划。根据您是否能够仅在少量表格和/或列上运行命令,或者是否需要调整非常长的列和表格列表,您将采取不同的方法。在规划此转换时,您应参考与您的数据库系统相关的文档。例如,在执行 MySQL 转换时,有些解决方案使用 mysqldump 工具,或者结合使用 SELECTconvert()INSERT。您需要确定这些方法中的哪一种最适合您的数据库系统。

示例三 – 处理文件级别的 UTF-8 编码

有时,你需要调整代码以便在文件层面处理 UTF-8 数据。假设有一个简单的程序用于收集并打印网页内容。如果大部分网页内容现在是 UTF-8 编码的,那么我们的程序内部就需要准备好处理这些内容。不幸的是,许多编程语言仍然需要一些调整才能干净地处理 UTF-8 编码的数据。考虑以下这个 Python 2.7 程序的示例,它旨在通过 API 连接 Twitter 并将 10 条推文写入文件:

import twitter
import sys

####################
def oauth_login():
    CONSUMER_KEY = ''
    CONSUMER_SECRET = ''
    OAUTH_TOKEN = ''
    OAUTH_TOKEN_SECRET = ''
    auth = twitter.oauth.OAuth(OAUTH_TOKEN, OAUTH_TOKEN_SECRET,
                               CONSUMER_KEY, CONSUMER_SECRET)
    twitter_api = twitter.Twitter(auth=auth)
    return twitter_api
###################

twitter_api = oauth_login()
codeword = 'DataCleaning'
twitter_stream = twitter.TwitterStream(auth=twitter_api.auth)
stream = twitter_stream.statuses.filter(track=codeword) 

f = open('outfile.txt','wb')
counter = 0
max_tweets = 10
for tweet in stream:
    print counter, "-", tweet['text'][0:10]
    f.write(tweet['text'])
    f.write('\n')
    counter += 1
    if counter >= max_tweets:
        f.close()
        sys.exit()

提示

如果你担心设置 Twitter 认证以获取脚本中使用的密钥和令牌,请不要担心。你可以通过dev.twitter.com/apps/new上的简单设置流程,或者我们在第十章中提供了一个更长、更深入的 Twitter 数据挖掘示例,Twitter 项目。在那一章中,我们详细介绍了 Twitter 开发者账户的整个设置过程,并且更深入地讲解了推文收集的过程。

这个小程序查找使用DataCleaning关键词的 10 条最近推文。(我选择这个关键词是因为我最近发布了几条包含表情符号和 UTF-8 字符的推文,使用了这个标签,我确信它会在前 10 条推文中快速生成一些不错的结果字符。)然而,当请求 Python 将这些推文保存到文件时,代码会抛出如下错误信息:

UnicodeEncodeError: 'ascii' codec can't encode character u'\u00c9' in position 72: ordinal not in range(128)

问题在于open()函数没有准备好处理 UTF-8 字符。我们有两种解决方法:去除 UTF-8 字符或改变写入文件的方式。

选项一 – 去除 UTF-8 字符

如果我们选择这种方法,我们需要理解,通过去除字符,我们失去了可能有意义的数据。正如我们在本章早些时候讨论的那样,数据丢失通常是不希望发生的事情。然而,如果我们确实希望去除这些字符,我们可以对原始的for循环进行如下更改:

for tweet in stream:
    encoded_tweet = tweet['text'].encode('ascii','ignore')
    print counter, "-", encoded_tweet[0:10]
    f.write(encoded_tweet)

这段新代码将一条原本是冰岛语的推文更改为:

Ég elska gögn

对此:

g elska ggn

对于文本分析而言,这句话已经没有任何意义,因为 g 和 ggn 不是词汇。这可能不是清理这些推文字符编码的最佳选择。

选项二 – 以能够处理 UTF-8 字符的方式写入文件

另一种选择是使用codecsio库,这些库允许在打开文件时指定 UTF-8 编码。在文件顶部添加import codec行,然后像这样更改打开文件的行:

f = codecs.open('outfile.txt', 'a+', 'utf-8')

a+参数表示如果文件已经存在,我们希望将数据附加到文件中。

另一种选择是在程序顶部包含io库,然后使用它的open()版本,可以传递特定的编码,如下所示:

f= io.open('outfile.txt', 'a+', encoding='utf8')

我们能直接使用 Python 3 吗?为什么你还在使用 2.7?

确实,Python 3 比 Python 2.7 更容易处理 UTF-8 编码。如果你的开发环境能够支持,尽管使用 Python 3。在我的工作中,我更倾向于使用 Enthought Canopy 进行数据分析和数据挖掘,并且用它来教学。许多 Python 的发行版——包括 Enthought——仍然是为 2.7 版本编写的,并且在相当长的一段时间内不会迁移到 Python 3。原因在于,Python 3 对语言的内部机制做了一些重大改动(例如,如我们刚刚讨论的那样,自然支持 UTF-8 编码),这意味着很多重要且有用的包仍然需要重写才能兼容。这一重写过程需要很长时间。想了解更多关于此问题的信息,请访问wiki.python.org/moin/Python2orPython3

概述

本章涵盖了许多我们在本书后续章节中清理数据时需要用到的基本主题。我们在这里学到的技术中,有些简单,有些则比较复杂。我们学习了文件格式、压缩、数据类型和字符编码,这些都涉及到文件层级和数据库层级。在下一章,我们将讨论清理数据的另两个重要工具:电子表格和文本编辑器。

第三章 清理数据的工作马 – 电子表格和文本编辑器

在设计家庭厨房时,典型的布局通常会涉及经典的工作三角形,其中的三点是冰箱、水槽和炉子。在我们的清理数据厨房中,也有一些必不可少的工具,其中两个就是朴素的电子表格文本编辑器。尽管这些简洁的工具往往被忽视,但充分了解它们的功能可以使许多清理任务变得更加快速和轻松。在第二章,基础 – 格式、类型和编码,我们简要介绍了这两种工具,重点讨论了数据类型和文件类型,但在本章中,我们将深入探讨:

  • Excel 和 Google 电子表格中有用的函数,这些函数可以帮助我们处理数据,包括文本转列、分割和合并字符串、搜索和格式化以查找异常值、排序、将电子表格数据导入 MySQL,甚至使用电子表格生成 SQL 语句。

  • 文本编辑器的典型功能,包括使用正则表达式进行搜索和替换、修改行首和行尾以及基于列的编辑,以便自动提取和处理数据,使其变得更有用。

  • 一个小项目,使用这两种工具的功能清理一些实际数据

电子表格数据清理

电子表格在数据清理中的实用性源于两点:它能够将数据组织成列和行,并且具有一整套内置函数。在本节中,我们将学习如何充分利用电子表格,以帮助我们实现清理数据的目标。

Excel 中的文本转列

由于电子表格是设计用来存储数据在列和行中的,因此我们可能需要做的第一项清理任务就是根据需要整理数据。例如,如果你将大量数据粘贴到 Excel 或 Google 电子表格中,软件会首先尝试寻找分隔符(如逗号或制表符),并通过这种方式将数据分割成列。(有关分隔数据的更多信息,请参见第二章,基础 – 格式、类型和编码。)有时,电子表格软件可能无法找到分隔符,这时我们需要为它提供更多指导,告诉它如何将数据划分为列。考虑以下来自 Freenode 上的数千个 Internet Relay Chat 频道主题列表的文本片段:

[2014-09-19 14:10:47] === #eurovision   4   Congratulations to Austria, winner of the Eurovision Song Contest 2014!
[2014-09-19 14:10:47] === #tinkerforge   3
[2014-09-19 14:10:47] === ##new   3   Welcome to ##NEW

注意

要在 IRC 聊天服务器中生成频道及其主题的列表,可以使用alis命令,该命令可以通过/query/msg发送,具体取决于服务器的设置。在 Freenode 上,/msg alis *命令将生成一个频道列表。关于 IRC 聊天的更多信息,可以参考:freenode.net/services.shtml

我们通过肉眼可以看到,数据的第一部分是时间戳,接着是===#和频道名称、一个数字(即构建列表时频道上的用户数量)以及频道描述。然而,如果将这些行粘贴到 Excel 或 Google 电子表格中,它无法自动识别哪些应该是列。电子表格能够正确检测行,但由于分隔不一致,无法自动检测出列。当将数据粘贴到 Google 电子表格中时,数据的显示如下图所示。通过高亮显示单元格 A1,我们可以看到整个行都显示在公式栏中,表示整个行已经粘贴到了 A1 单元格中:

Excel 中的文本到列

我们如何在电子表格中轻松地从这些数据创建列?我们希望每个单独的数据项都位于自己的列中。通过这种方式,我们可以例如对频道的用户数量进行平均值计算,或根据频道名称对数据进行排序。目前,由于所有数据都在一个巨大的文本字符串中,我们无法轻松地对其进行排序或使用公式。

一种策略是使用 Excel 的文本到列向导将数据拆分成可识别的部分;然后,我们可以重新组合它们,并在需要时去除多余的字符。以下是相关步骤:

  1. 高亮显示列A,并启动文本到列向导(位于数据菜单中)。在第一步中,选择固定宽度,在第二步中,双击所有绘制的线条以分隔描述字段。下图展示了拆分前几列并移除所有多余行后数据的样子:Excel 中的文本到列

    Excel 中的固定宽度拆分。

    结果数据如下图所示。前三列没有问题,但固定宽度拆分未能成功分开列 D 中的用户计数和频道名称。这是因为频道名称的长度不像前几列那样具有可预测性。

    Excel 中的文本到列

    这是第一次文本到列拆分后的结果。

  2. 我们需要再次运行文本到列向导,但这次只针对列 D,并且使用分隔符方式而非固定宽度方式。首先,注意频道名称#eurovision与用户数量(4)之间有两个空格,4与频道描述之间也有两个空格。尽管文本到列向导不允许我们使用两个空格作为分隔符(它只允许单个字符),我们可以使用查找-替换对话框将所有两个空格的情况替换为一个在文本中未使用的符号。(首先执行查找操作以确保。)我选择了^符号。

    这一特定步骤看起来有点草率,所以如果你有兴趣尝试另一种方法,我一点也不怪你。与其他工具相比,Excel 在查找和替换坏文本方面的功能较为有限。我们将在本章后面的文本编辑器数据清理部分学习如何使用正则表达式。

    Excel 中的文本到列

    添加一个不常见的分隔符可以让我们将剩余的列分开。

  3. 现在,使用查找替换从 A 列和 B 列中移除[]字符,并将它们替换为空。 (在开始查找替换之前,高亮显示这些列,以免不小心在整个工作表中删除符号。)

    不幸的是,Excel 为了帮助我们,将这些转换为我们可能不喜欢的日期格式:9/19/2014。如果你想将它们恢复到原来的格式(2014-09-19),请选择整列,并使用自定义格式对话框指定日期格式为 yyyy-mm-dd。

  4. F列中的字符串仍然在前面有多余的空格。我们可以使用trim()函数去除每个字符串值前面的额外空格。插入一个新列到F列的左侧,并应用trim()函数,如下所示:Excel 中的文本到列

    这是应用trim()函数以去除前导或尾随空格后的结果。

  5. 我们还可以对修剪后的文本应用clean()函数。这个函数将删除前 32 个 ASCII 字符:所有可能进入这些频道描述中的不可打印控制字符。你可以在trim()函数外部应用clean(),如下所示:clean(trim(g1))

  6. 将 F1 框的角拖动到底部,以将clean(trim())应用到F列的其余单元格。

  7. 选择F列,复制它,并在F列中使用仅粘贴值,这样我们就可以删除G列。

  8. 删除G列。瞧,现在你有了完美清理的数据。

拆分字符串

Google 表格中有一个轻量级版本的“文本到列”功能,但 Excel 中没有,那就是split()函数。这个函数只是接受一个字符串值,并将其拆分成组成部分。请注意,你需要提供足够的新的列来容纳拆分后的数据。在下面的例子中,我们使用了与之前示例相同的数据,但创建了三个新列来容纳D列的拆分值。

拆分字符串

连接字符串

concatenate()函数接受多个字符串,无论是作为单元格引用还是作为带引号的字符串,并将它们连接到一个新单元格中。在下面的例子中,我们使用concatenate()函数将日期和时间字符串连接为一个。这个函数在 Excel 和 Google 表格中都有,如下图所示:

连接字符串

条件格式化以查找异常值

Excel 和 Google 电子表格都有条件格式功能。条件格式使用一组规则,根据是否满足某些条件(条件)来更改单元格或单元格的外观(格式)。我们可以使用此功能来查找过高、过低、缺失或其他异常的数据显示。一旦我们识别出这些数据,就可以清理它们。

这是一个如何在 Google 电子表格中使用条件格式查找样本数据中不包含#的频道名称且聊天室参与者人数为空的行的示例:

使用条件格式查找异常值

这是背景单元格颜色已更改后的结果,定位了D列中不以#开头的单元格,以及E列中为空的单元格。现在,这些问题值可以通过视觉检查轻松找到。

使用条件格式查找异常值

排序以查找异常值

如果数据量太大,无法进行视觉检查,那么我们可以尝试使用排序来查找问题数据。在 Google 电子表格或 Excel 中,选择你想要排序的列,这可以是整个表格,然后使用数据菜单中的排序选项。这对于大多数列来说非常简单,尤其是当你在查找像 D4 单元格这样的数据时。

那么,如果你尝试按E列排序以查找丢失的值会发生什么呢?也许我们希望将所有缺失的数据放在一起,以便删除这些数据行。E4 的值是空的。记住在第二章中提到的,基础知识 – 格式、类型和编码,NULL(在 Google 电子表格中为空)不能与任何其他值进行比较,因此无论你是按从低到高还是从高到低排序E列的值,它都会保持在排序列表的底部。

将电子表格数据导入 MySQL

现在你已经有了一个满是清洁数据的漂亮电子表格,你可能希望将其存储到数据库中以便长期使用。

从电子表格创建 CSV

许多数据库系统会通过围绕 CSV 文件构建的导入程序来接收数据。如果你使用的是 MySQL,可以使用LOAD DATA IN FILE命令将数据直接从分隔文件导入数据库,甚至可以设置自己的分隔符。首先,让我们看看这个命令的示例,然后我们可以根据需要的参数在 Excel 中创建文件。

从 MySQL 命令行,我们可以运行:

load data local infile 'myFile.csv' 
 into table freenode_topics
 fields terminated by ','
 (dateOfTopic, channel, numUsers, message);

当然,这假设已经创建了一个表格。在这个例子中,它叫做freenode_topics,并且它有四列,这些列出现在此 SQL 查询的最后一行。

因此,这个查询中引用的 CSV 文件myFile.csv需要按照此顺序排列列,并用逗号分隔。

在 Excel 中,可以通过导航到 文件 | 另存为,然后从格式选项列表中选择 CSV (MS-DOS) 来从当前工作簿的工作表创建 CSV 文件。在 Google 电子表格中,您可以通过导航到 文件 | 下载 | CSV 来实现相同的操作。在这两种情况下,保存文件到本地系统,然后启动 MySQL 客户端,并按照之前显示的命令行操作。

提示

如果您不喜欢使用 MySQL 命令行客户端,还可以通过 MySQL 自带的 Workbench 图形客户端或类似 PhpMyAdmin 的工具将 CSV 文件上传到服务器。PhpMyAdmin 对上传的文件大小有一个限制(目前为 2MB)。

使用电子表格生成 SQL

另一种将数据导入数据库的方法一开始看起来很奇怪,但如果您由于某些原因(可能是权限问题或文件大小限制)无法通过前面讨论的 CSV 文件加载数据,这个方法可能会节省很多时间。在这种方法中,我们将在电子表格中构建 INSERT 语句,然后在数据库中运行这些命令。

如果电子表格中的每一列都代表数据库中的一列,那么我们可以简单地将 SQL INSERT 命令的结构组件(带引号的字符串、括号、命令和行结束分号)添加到电子表格中的各列周围,并将结果拼接成一个巨大的 INSERT 命令字符串。

使用电子表格生成 SQL

使用 concatenate(A1:I1) 函数将 A:I 列中的所有字符串连接后,我们得到的 INSERT 语句如下所示:

INSERT INTO freenode_topics (dateOfTopic, channel, num_users, message) VALUES('2014-09-19 14:10:47', '#eurovision',4, 'Congratulations to Austria, winner of the Eurovision Song Contest 2014!');

这些可以粘贴到用户友好的前端界面中,例如 PhpMyAdmin 或 MySQL Workbench。或者,您可以将其保存为文本文件(使用文本编辑器),每个 INSERT 语句之间用换行分隔。我把我的文件命名为 inserts.sql。现在,可以通过命令行和 MySQL 客户端将此文件导入数据库,方法如下:

$mysql -uusername -p -hhostname databasename < inserts.sql

或者,它也可以通过在 MySQL 命令行客户端中使用 source 命令来导入,命令如下:

$mysql -uusername -p -hhostname
[enter your password]
> use databasename;
> source inserts.sql

任何一种方法都可以将数据导入 MySQL。如果脚本足够小,您还可以使用图形客户端,如 MySQL Workbench。不过要小心,将非常大的脚本加载到图形客户端中,因为客户端机器的内存可能不足以加载数百 GB 的 SQL。我更喜欢第二种方法(source),因为它会在每个成功插入后打印出成功消息,这样我就知道我的命令是正确的。

如果您不太清楚如何创建名为 inserts.sql 的文本文件,那么下一节将为您详细讲解。我们将介绍比您想象的更多文本编辑器相关的内容!

文本编辑器数据清理

我们在第二章,基础 – 格式、类型和编码中学习到,文本编辑器是读取和创建文本文件的首选方式。这听起来很合理,也完全能理解。我们当时没有真正解释的是,文本编辑器有时也被称为程序员的编辑器,因为它具有许多有用的功能,帮助像程序员以及数据清理人员这样的工作者,他们整天都在处理文本文件。我们现在将参观一些最有用的功能。

提示

每个操作系统都有数十款文本编辑器可供选择。有些需要付费,但许多都是免费的。在本章中,我将使用 Text Wrangler,它是一个免费的编辑器,适用于 OSX(可以在此获取:www.barebones.com/products/textwrangler)。本章展示的功能在大多数其他编辑器中也广泛可用,如 Sublime Editor,但如果某个特定功能或工具的位置不明显,你应该查看你所选择的编辑器的文档。

文本调整

我们选择的文本编辑器内置了许多用于文本处理的有用功能。这里列出的功能是数据清理任务中最常用的一些。请记住,在清理过程中,你可能会对单个文本文件执行几十个清理程序,所以我们在第一章,为什么需要干净的数据?中提供的关于如何清晰地传达你所做更改的提示在这里会非常有用。

更改大小写是数据清理中非常常见的请求。很多时候,我们会继承完全小写或完全大写的数据。下图展示了 Text Wrangler 中一个用于对选中文本进行大小写更改的对话框。每个选项旁边显示了键盘快捷键。

文本调整

大小写更改的选项包括大写、小写以及将每个单词、每行的第一个字母或句子中的第一个单词大写。

在选择中的每一行添加或删除前缀或后缀是另一个常见的任务。前几天,当我在构建文本分类器时,我需要处理大量的文本行。需要在每一行的末尾加上一个逗号,并标明该行所代表的类别(正面或负面)。以下是 Text Wrangler 中的前缀和后缀对话框。注意,你可以选择插入或删除,但不能在同一操作中同时进行。如果你需要执行这两个任务,先执行一个,然后再执行另一个。

文本调整

删除小怪物是另一个非常适合您使用文本编辑器的任务。TextWrangler 和适用于 Windows 的 Sublime 编辑器都有这个功能。在删除小怪物时,编辑器可以查找任何不在您希望的字符集中的字符,例如控制字符、NULL 字符和非 ASCII 字符。它可以删除它们,或者将它们替换为它们的字符代码。它还可以将这些小怪物替换为您指定的任何字符。这样可以在之后更容易找到它们。

文本调整

列模式

当文本编辑器处于列模式时,意味着您可以按列选择文本,而不仅仅是按行选择。这里是正常(非列)模式下的选择示例:

列模式

这是列模式下选择的示例。按住Option键并选择列中的文本。一旦文本被选中,您可以像处理任何已选文本一样处理它:您可以删除它、剪切它或将其复制到剪贴板,或者使用我们在前一部分中讨论的任何文本调整,只要它们适用于选中的文本。

列模式

这个功能的一些限制包括:

  • 每个字符占用一列,因此字符应以不带比例的打字机风格字体显示。

  • 在 Text Wrangler 中,列模式仅在关闭换行时有效。关闭软换行意味着您的行将延伸到右边,不会被换行。

  • 在 Text Wrangler 中,您绘制的列的垂直高度必须能够手动绘制,因此这是一个用于小规模数据(几百行或几千行,但可能不是几百万行)的技巧。

强力查找与替换

文本编辑器在处理文本方面非常出色。使用文本编辑器在列模式下工作可能会让人觉得奇怪,因为这看起来更像是电子表格中的任务。类似地,在您看到文本编辑器能做的事情后,使用电子表格进行查找-替换可能会显得有些笨拙。

Text Wrangler 中查找对话框的主要部分如下所示。提供的功能包括大小写敏感的搜索、循环搜索、在文本的子选择中进行搜索以及在单词或单词的一部分中查找给定的文本模式。您还可以将特殊字符、空白(包括制表符和行终止符)、表情符号等粘贴到文本框中。查找框右侧的小下拉框提供了额外的功能。带有时钟图标的顶部框包含最近的搜索和替换列表。带有字母g的底部框包含一组可能有用的内置搜索模式,在此菜单的底部,还有一个选项可以将您自己的模式添加到列表中。

强力查找与替换

其中一个最强大的查找替换功能是通过选中 Grep 复选框来启用的。选中此框后,我们可以使用正则表达式模式进行搜索。简而言之,正则表达式regex)是一种用特殊语言编写的模式,由符号组成,旨在与某些字符串文本匹配。正则表达式的详细解释超出了本书的范围,但可以说它们非常有用,我们会在需要清理数据时定期使用它们。

提示

Text Wrangler 界面中的复选框标示为 Grep——而不是 RegEx 或 Match——是因为正则表达式模式匹配语言存在一些微小的差异。Text Wrangler 在提醒我们,它使用的是 Grep 版本,Grep 是最常见的变种,最初是为 Unix 系统编写的程序。

在这里,我们将概述一些我们在清理数据时会反复使用的必备正则表达式符号。对于更复杂的模式,值得参考一本专门的书籍或多个展示各种正则表达式语法的网页:

符号 功能
` 符号
--- ---
匹配行尾
^ 匹配行首
+ 匹配一个或多个指定字符
* 匹配零个或多个指定字符
\w 匹配任意单词字符(0-9,A-z)。要匹配非单词字符,请使用 \W
\s 匹配任意空白字符(制表符、换行符或回车符)。要匹配非空白字符,请使用 \S
\t 匹配制表符
\r 匹配回车符。使用 \n 来匹配换行符。
\ 这是转义字符。它匹配紧随其后的确切字符,而不是正则表达式模式字符。

以下是一些查找替换组合的示例,帮助我们学习如何在 Text Wrangler 中使用正则表达式。确保选中 Grep 框。如果替换列为空,则意味着替换字段应该保持为空。

查找 替换 功能
\r 查找换行符(行终止符),并将其替换为空。换句话说,就是“将多行合并为一行”。
^\w+ - 匹配行首,后跟至少一个单词字符,并在行前加上一个 - 字符。
`\r 查找 替换
--- --- ---
\r 查找换行符(行终止符),并将其替换为空。换句话说,就是“将多行合并为一行”。
^\w+ - 匹配行首,后跟至少一个单词字符,并在行前加上一个 - 字符。
\[end\] 查找所有以实际的 \r 字符(反斜杠后跟 r)结尾的行,并将其替换为实际字符 [end]。请注意,[] 也是正则表达式中的特殊字符,因此在将其作为普通字符使用时需要进行转义。

如果正则表达式让你感到畏惧,请放心。首先,回想一下本章早些时候提到的 文本调整 部分,大多数文本编辑器,包括 Text Wrangler,都有许多内置的搜索和替换功能,这些功能是基于最常用的正则表达式构建的。因此,你可能会发现自己并不常常需要编写很多正则表达式。其次,正则表达式如此强大和实用,因此有许多在线资源可以供你参考,帮助你学习如何构建复杂的正则表达式,如果你需要的话。

我的两个最喜欢的正则表达式资源是 Stack Overflow(stackoverflow.com)和 regular-expressions.info(regular-expressions.info)。此外,通过快速的网页搜索,你还可以找到许多正则表达式测试网站。这些网站让你编写并测试正则表达式,应用于样本文本。

提醒一下

然而,在使用在线正则表达式测试工具时要小心,因为它们通常是为教授特定类型的正则表达式而设计的,比如 JavaScript、PHP 或 Python 中的正则表达式解析器。你想做的一些操作,可能在你的文本编辑器中的正则表达式语法与这些语言中的相同,也可能不同。根据你尝试做的事情的复杂程度,最好的方法可能是创建一个文本数据的备份副本(或者将问题文本的一个小样本提取到一个新文件中),然后使用你自己的文本编辑器并根据其正则表达式语法进行实验。

文本排序与重复处理

一旦我们稍微实验了一下正则表达式,我们会注意到我们的文本编辑器偶尔会在其他菜单选项中提供这些模式匹配技术,例如在排序和重复处理中。考虑以下排序对话框,看看如何将正则表达式应用于排序多行:

文本排序与重复处理

在这种情况下,我们可以使用 按模式排序 复选框来输入一个正则表达式模式进行排序。重复处理对话框类似。你可以告诉编辑器是否保留原始行或删除它。有趣的是,如果你需要将它们用于其他用途,比如保持已删除行的日志,你还可以将重复项移到另一个文件或剪贴板。

小贴士

在数据清理中,至少考虑将移除的行保存到单独的文件中,以备将来需要时使用,这是一个好主意。

文本排序与重复处理

包含处理行

Text Wrangler 中有一个很实用的功能,叫做 包含处理行。它将搜索(包括使用正则表达式的可能性)与逐行处理结合起来,例如将受影响的行移到另一个文件或剪贴板,或者删除匹配的行。

包含处理行

一个示例项目

在这个示例项目中,我们将下载一个电子表格,使用 Excel 或文本编辑器清理它,然后对其进行一些简单的分析。

第一步 – 陈述问题

本项目的灵感来源于《高等教育纪事》提供的一些数据,这是一份关于大学和高等院校新闻及事件的出版物。在 2012 年,他们创建了一个名为“你的大学认为它的同行是谁?”的互动功能。在这个功能中,用户可以输入任何美国大学的名称,并看到一个互动可视化,显示其他哪些大学称目标大学为同行。(同行是指在某些方面相似的大学。)原始数据来自美国政府报告,但《高等教育纪事》已将这些数据免费提供给任何人使用。

在这个示例中,我们关心的是一个补充性问题:当大学 X 出现在一个列表中时,哪些其他大学也在这些列表中?为了解答这个问题,我们需要找到所有与大学 X 一起列出的其他大学,然后统计它们的名字出现了多少次。

第二步 – 数据收集

在这一步中,我们将看到收集数据的过程,并逐步清理它。接下来的部分将讨论我们需要采取的行动,以便为我们的项目收集适当的数据。

下载数据

本项目的数据可以通过原文中的链接下载,或者通过直接链接到电子表格下载。

文件已被压缩,请使用你喜欢的解压软件解压文件。

熟悉数据

ZIP 文件中的文件具有 .csv 扩展名,确实是一个以逗号分隔的 CSV 文件,包含 1686 行,其中包括一行标题。逗号分隔了两列:第一列是相关大学的名称,第二列是该大学列出的所有同行大学。同行大学之间由管道符号 (|) 分隔。以下是一个示例行:

Harvard University,Yale University|Princeton University|Stanford University

在这种情况下,第一列表示哈佛大学是目标大学,第二列显示哈佛将耶鲁、普林斯顿和斯坦福列为其同行。

第三步 – 数据清理

由于本示例的目标是查看某所大学以及它在其他大学的同行名单中的所有出现情况,我们的第一步是清除任何包含目标大学的行。然后,我们将把文件转化为一个包含所有大学的长列表。到那时,我们的数据将已经清理干净,准备好进入分析和可视化步骤。

提取相关行

我们来比较两种提取相关行的方法:一种是使用电子表格,另一种是使用文本编辑器,并应用本章中概述的技术。

使用电子表格

打开电子表格程序中的文件,使用条件格式突出显示包含哈佛大学(或你选择的目标大学)的任何行,然后删除其余的行。

使用文本编辑器

打开文件并使用包含的行处理来查找所有包含哈佛大学短语(或你选择的大学)的行,然后将它们复制到一个新文件中。

使用文本编辑器

无论使用哪种选项,结果是一个新文件,包含 26 行,每行都包含哈佛大学

转换行

现在我们有一个包含 26 行的文件,每行包含多个大学(宽数据)。我们预计将来某个时候会将文件读入 Python,以进行简单的大学频率统计。因此,我们决定将其转换为长数据,每行一个大学。

为了将文件转换成每行一个大学,我们将使用文本编辑器和三次查找与替换操作。首先,我们将查找逗号并将其替换为/r(回车符)。

转换行

接下来,查找管道字符(|)并将其替换为\r。此时,文本文件有 749 行。

最后,我们需要删除所有哈佛大学的实例。(记住,我们只关心与哈佛大学一起提到的同行,所以我们不需要计入哈佛大学本身。)在 Text Wrangler 中,我们可以在查找框中输入Harvard University\r,将替换框留空。这样会删除 26 行,文件中的总行数为 723 行。

第四步 - 数据分析

由于我们的主要关注点是数据清理,因此我们不会花太多时间在分析或可视化上,但这里有一个简短的 Python 脚本,用于统计每个同行与哈佛大学一同被提到的次数:

from collections import Counter
with open('universities.txt') as infile:
    counted = Counter(filter(None,[line.strip() for line in infile]))
    sorted = counted.most_common()
    for key,value in sorted:
      print key, ",", value

结果显示 232 所独特的大学,以及每所大学被提到的次数。以下是前几项结果。我们可以这样解读这些结果:

当哈佛大学作为同行被提到时,耶鲁大学也被提到 26 次

Yale University , 26
Princeton University , 25
Cornell University , 24
Stanford University , 23
Columbia University in the City of New York , 22
Brown University , 21
University of Pennsylvania , 21

到此为止,你可以将这个列表(或较长列表的一部分)输入到条形图、词云或任何你认为有说服力或有趣的可视化工具中。由于数据是以逗号分隔的,你甚至可以轻松地将其粘贴到电子表格程序中进行进一步分析。但到目前为止,我们已经回答了初步问题:给定一个目标大学,哪些同行被提到最多。

摘要

在本章中,我们学习了一些非常实用的数据清理技巧,使用了两个易于获取的工具:文本编辑器和电子表格。我们概述了电子表格中可用的函数,用于拆分数据、移动数据、查找和替换部分内容、格式化数据以及将数据重新组合。接着,我们学习了如何最大限度地发挥简单文本编辑器的作用,包括一些内置功能,以及如何最有效地使用查找和替换及正则表达式。

在下一章中,我们将把目前为止学到的各种技巧结合起来,进行一些重要的文件转换。我们将使用的许多技巧将基于我们在过去两章中学到的文本编辑、正则表达式、数据类型和文件格式的知识,因此,准备好解决一些真实世界的数据清理问题吧。

第四章:说通用语——数据转换

去年夏天,我在当地的烹饪学校参加了一门奶酪制作课程。我们做的第一件事就是制作里科塔奶酪。我兴奋地得知,里科塔奶酪可以在大约一个小时内,只用牛奶和酪乳就能做成,而且酪乳本身可以通过牛奶和柠檬汁制作。在厨房中,食材不断地转化为其他食材,进而变成美味的菜肴。在我们的数据科学厨房中,我们也将定期进行数据格式之间的转换。我们可能需要这样做来执行各种分析,当我们想要合并数据集,或者如果我们需要以新的方式存储数据集时。

通用语 是一种在不同语言使用者之间进行交流时被采纳为共同标准的语言。在数据转换中,有几种数据格式可以作为共同标准。我们在第二章,基础 – 格式、类型和编码中讨论过其中的一些。JSON 和 CSV 是最常见的两种格式。在本章中,我们将花一些时间学习:

  • 如何从软件工具和语言(Excel、Google Spreadsheets 和 phpMyAdmin)中快速转换为 JSON 和 CSV 格式。

  • 如何编写 Python 和 PHP 程序生成不同的文本格式并在它们之间进行转换。

  • 如何实现数据转换以完成一个实际的-世界任务。在这个项目中,我们将使用 netvizz 软件从 Facebook 下载朋友网络数据,然后清理数据并将其转换为构建社交网络可视化所需的 JSON 格式。接着,我们将以不同的方式清理数据,并将其转换为 networkx 社交网络包所需的 Pajek 格式。

快速的工具基础转换

转换少量到中等量数据的最快、最简单方法之一就是直接让你使用的任何软件工具为你执行转换。有时,你使用的应用程序已经具备将数据转换为你需要的格式的选项。就像在第三章,清洁数据的得力工具 – 电子表格和文本编辑器中提到的技巧一样,我们希望尽可能地利用工具中的这些隐藏功能。如果你要处理的数据量太大,无法通过应用程序转换,或者你需要的特定转换功能不可用,我们将在接下来的部分中介绍编程解决方案,即使用 PHP 转换使用 Python 转换

电子表格转 CSV

将电子表格保存为分隔符文件非常简单。Excel 和 Google 电子表格的 文件 菜单中都有 另存为 选项;在该选项中,选择 CSV (MS DOS)。此外,Google 电子表格还提供将文件保存为 Excel 文件和制表符分隔文件的选项。将文件保存为 CSV 也有一些限制:

  • 在 Excel 和 Google 电子表格中,当你使用 另存为 功能时,只会保存当前工作表。这是因为 CSV 文件本身仅描述一组数据,因此它不能包含多个工作表。如果你有一个多工作表的电子表格,你需要将每个工作表保存为单独的 CSV 文件。

  • 在这两个工具中,定制 CSV 文件的选项相对较少,例如,Excel 保存数据时使用逗号作为分隔符(因为它是一个 CSV 文件,这样做很合理),并且没有选项将数据值用引号括起来或设置不同的行结束符。

电子表格转 JSON

与 CSV 相比,JSON 要稍微难处理一些。虽然 Excel 没有简单的 JSON 转换器,但有一些在线转换工具声称能够将 CSV 文件转换为 JSON。

然而,Google 电子表格提供了一个通过 URL 访问的 JSON 转换器。使用这种方法有一些缺点,首先是你必须将文档发布到 Web 上(至少是暂时的),才能访问它的 JSON 版本。你还需要用一些非常长的数字来定制 URL,这些数字用来标识你的电子表格。它还会在 JSON 输出中生成大量信息——可能比你需要的还要多。尽管如此,以下是将 Google 电子表格转换为 JSON 表示的逐步说明。

第一步 – 将 Google 电子表格发布到 Web 上

在你的 Google 电子表格创建并保存后,从 文件 菜单中选择 发布到 Web。点击接下来的对话框(我选择了所有默认选项)。此时,你就可以通过 URL 访问该文件的 JSON 版本。

第二步 – 创建正确的 URL

从发布的 Google 电子表格中创建 JSON 的 URL 模式如下:

spreadsheets.google.com/feeds/list/key/sheet/public/basic?alt=json

这个 URL 中有三个部分需要修改,以匹配你的特定电子表格文件:

  • list: (可选)你可以将 list 更改为比如 cells,如果你希望在 JSON 文件中查看每个单元格的参考(如 A1, A2 等),而每个单元格单独列出。如果你希望每一行作为一个实体,保持 list 在 URL 中即可。

  • 密钥:将此 URL 中的key更改为与 Google 内部用于表示您的文件的长且唯一的编号匹配。在电子表格的 URL 中,您在浏览器中查看时,密钥显示为两个斜杠之间的长标识符,位于/spreadsheets/d部分之后,如下所示:第二步 - 创建正确的 URL

  • 表格:将示例网址中的“sheet”更改为od6,以表示您希望转换第一个工作表。

    注意

    od6是什么意思?Google 使用代码来表示每个工作表。然而,这些代码并非严格按数字顺序排列。在 Stack Overflow 上的一个问题及其回答中有关于编号方案的详细讨论:stackoverflow.com/questions/11290337/

为了测试这个过程,我们可以创建一个 Google 电子表格,包含我们从示例项目第三章最后的练习中生成的大学名称和计数数据,数据清理的工作马——电子表格和文本编辑器。这个电子表格的前三行如下所示:

耶鲁大学 26
普林斯顿大学 25
康奈尔大学 24

我用来通过 JSON 访问此文件的 URL 如下所示:

spreadsheets.google.com/feeds/list/1mWIAk_5KNoQHr4vFgPHdm7GX8Vh22WjgAUYYHUyXSNM/od6/public/basic?alt=json

将这个 URL 粘贴到浏览器中会返回数据的 JSON 表示。它包含 231 个条目,每个条目如下所示。我已经为更易阅读对该条目进行了格式化,添加了换行符:

{
  "id":{
    "$t":"https://spreadsheets.google.com/feeds/list/1mWIAk_5KN oQHr4vFgPHdm7GX8Vh22WjgAUYYHUyXSNM/od6/public/basic/cokwr"
  },
  "updated":{"$t":"2014-12-17T20:02:57.196Z"},
  "category":[{
    "scheme":"http://schemas.google.com/spreadsheets/2006",
    "term"  :"http://schemas.google.com/spreadsheets/2006#list"
  }],
  "title":{
    "type":"text",
    "$t"  :"Yale University "
  },
  "content":{
    "type":"text",
    "$t"  :"_cokwr: 26"
  },
  "link": [{
    "rel" :"self",
    "type":"application/atom+xml",
    "href":"https://spreadsheets.google.com/feeds/list/1mWIAk_5KN oQHr4vFgPHdm7GX8Vh22WjgAUYYHUyXSNM/od6/public/basic/cokwr"
  }]
}

即使我进行了重新格式化,这个 JSON 看起来也不太美观,而且其中许多名称-值对对我们来说并不有趣。尽管如此,我们已经成功生成了一个可用的 JSON。如果我们使用程序来处理这个 JSON,我们会忽略关于电子表格本身的所有冗余信息,只关注标题和内容实体以及$t值(在此案例中是Yale University_cokwr: 26)。这些值在前面示例中的 JSON 中已被突出显示。如果你在想是否有办法从电子表格到 CSV 再到 JSON,答案是肯定的。我们将在本章后面的使用 PHP 转换使用 Python 转换部分详细介绍如何做到这一点。

使用 phpMyAdmin 将 SQL 导出为 CSV 或 JSON

在本节中,我们将讨论两种直接从数据库(我们这里使用的是 MySQL)写入 JSON 和 CSV 的选项,而无需使用任何编程。

首先,phpMyAdmin 是一个非常常见的基于 Web 的 MySQL 数据库前端。如果你使用的是该工具的现代版本,你将能够将整个表格或查询结果导出为 CSV 或 JSON 文件。使用我们在第一章中首次访问的相同 enron 数据库,为什么你需要干净的数据?,请看以下的 导出 标签截图,其中 JSON 被选为整个 employeelist 表的目标格式(此选择框中也可以选择 CSV):

使用 phpMyAdmin 将 SQL 转换为 CSV 或 JSON

PhpMyAdmin 导出整个表格的 JSON

导出查询结果的过程非常类似,唯一的不同是,在页面顶部使用 导出 标签,而是运行 SQL 查询后,使用页面底部 查询结果操作 下的 导出 选项,如下所示:

使用 phpMyAdmin 将 SQL 转换为 CSV 或 JSON

PhpMyAdmin 也可以导出查询结果

这里有一个简单的查询,我们可以在 employeelist 表上运行来测试这个过程:

SELECT concat(firstName,  " ", lastName) as name, email_id
FROM employeelist
ORDER BY lastName;

当我们将结果导出为 JSON 时,phpMyAdmin 会显示我们 151 个按以下格式整理的值:

{
  "name": "Lysa Akin",
  "email_id": "[email protected]"
}

phpMyAdmin 工具是一个不错的工具,对于将中等量的数据从 MySQL 转换出来非常有效,尤其是作为查询结果。如果你使用的是不同的关系数据库管理系统(RDBMS),你的 SQL 界面可能也有一些你应该探索的格式化选项。

另一种策略是完全绕过 phpMyAdmin,直接使用 MySQL 命令行来写出你想要格式化的 CSV 文件:

SELECT concat(firstName,  " ", lastName) as name, email_id
INTO OUTFILE 'enronEmployees.csv'
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '\n'
FROM employeelist;

这将写入一个以逗号分隔的文件,文件名为指定的 (employees.csv)。它将被写入当前目录。

那么 JSON 呢?使用这种策略没有非常简洁的方式来输出 JSON,因此你应该使用之前展示的 phpMyAdmin 解决方案,或者使用 PHP 或 Python 编写的更强大的解决方案。这些编程解决方案将在后面的章节中详细介绍,因此请继续阅读。

使用 PHP 转换

在我们的第二章中,基础知识 – 格式、类型和编码,我们在讨论 JSON 数值格式化时,简要展示了如何使用 PHP 连接到数据库、运行查询、从结果中构建 PHP 数组,然后将 JSON 结果打印到屏幕上。在这里,我们将首先扩展这个示例,将结果写入文件而不是打印到屏幕,并且还会写入 CSV 文件。接下来,我们将展示如何使用 PHP 读取 JSON 文件并转换为 CSV 文件,反之亦然。

使用 PHP 将 SQL 转换为 JSON

在本节中,我们将编写一个 PHP 脚本,连接到 enron 数据库,运行 SQL 查询,并将其导出为 JSON 格式的文件。为什么要写 PHP 脚本而不是使用 phpMyAdmin 呢?这个方法在我们需要在导出数据之前对其进行额外处理,或者怀疑数据量大于基于 Web 的应用(如 phpMyAdmin)能够处理的情况下会很有用:

<?php
// connect to db, set up query, run the query
$dbc = mysqli_connect('localhost','username','password','enron')
or die('Error connecting to database!' . mysqli_error());

$select_query = "SELECT concat(firstName,  \" \", lastName) as name, email_id FROM  employeelist ORDER BY lastName";

$select_result = mysqli_query($dbc, $select_query);

if (!$select_result)
    die ("SELECT failed! [$select_query]" .  mysqli_error());

// ----JSON output----
// build a new array, suitable for json
$counts = array();
while($row = mysqli_fetch_array($select_result))
{
// add onto the json array
    array_push($counts, array('name'     => $row['name'],
    'email_id' => $row['email_id']));
}
// encode query results array as json
$json_formatted = json_encode($counts);

// write out the json file
file_put_contents("enronEmail.json", $json_formatted);
?>

这段代码将 JSON 格式的输出文件写入你在 file_put_contents() 行中指定的位置。

使用 PHP 将 SQL 转换为 CSV

以下代码片段展示了如何使用 PHP 文件输出流创建一个包含 SQL 查询结果的 CSV 格式文件。将此代码保存为 .php 文件到你网站服务器的脚本目录中,然后在浏览器中请求该文件。它将自动下载一个包含正确值的 CSV 文件:

<?php
// connect to db, set up query, run the query
  $dbc = mysqli_connect('localhost','username','password','enron')
  or die('Error connecting to database!' . mysqli_error());

$select_query = "SELECT concat(firstName,  \" \", lastName) as name, email_id FROM  employeelist ORDER BY lastName";

$select_result = mysqli_query($dbc, $select_query);

if (!$select_result)
    die ("SELECT failed! [$select_query]" .  mysqli_error());

// ----CSV output----
// set up a file stream
$file = fopen('php://output', 'w');
if ($file && $select_result)
{
    header('Content-Type: text/csv');
    header('Content-Disposition: attachment;
    filename="enronEmail.csv"');
    // write each result row to the file in csv format
    while($row = mysqli_fetch_assoc($select_result))
    {
      fputcsv($file, array_values($row));
    }
}
?>

结果的格式如下(仅展示前三行):

"Lysa Akin",[email protected]
"Phillip Allen",[email protected]
"Harry Arora",[email protected]

如果你在想是否 Phillip 的电子邮件真的是应该有两个点,我们可以运行一个快速查询来查找有多少 Enron 的电子邮件是这样格式的:

SELECT CONCAT(firstName,  " ", lastName) AS name, email_id
FROM employeelist
WHERE email_id LIKE "%..%"
ORDER BY name ASC;

结果显示有 24 个电子邮件地址含有双点。

使用 PHP 将 JSON 转换为 CSV

在这里,我们将使用 PHP 读取 JSON 文件并将其转换为 CSV,并输出一个文件:

<?php
// read in the file
$json = file_get_contents("outfile.json");
// convert JSON to an associative array
$array = json_decode ($json, true);
// open the file stream
$file = fopen('php://output', 'w');
header('Content-Type: text/csv');
header('Content-Disposition: attachment;
filename="enronEmail.csv"');
// loop through the array and write each item to the file
foreach ($array as $line)
{
    fputcsv($file, $line);
}
?>

这段代码将创建一个包含每行数据的 CSV 文件,类似于前面的示例。我们应该注意,file_get_contents() 函数将文件读取到内存中作为字符串,因此对于极大的文件,你可能需要使用 fread()fgets()fclose() 等 PHP 函数的组合来处理。

使用 PHP 将 CSV 转换为 JSON

另一个常见任务是读取 CSV 文件并将其写入为 JSON 文件。通常情况下,我们有一个 CSV 文件,其中第一行是表头行。表头行列出了文件中每一列的列名,我们希望将表头行中的每一项作为 JSON 格式文件的键:

<?php
$file = fopen('enronEmail.csv', 'r');
$headers = fgetcsv($file, 0, ',');
$complete = array();

while ($row = fgetcsv($file, 0, ','))
{
    $complete[] = array_combine($headers, $row);
}
fclose($file);
$json_formatted = json_encode($complete);
file_put_contents('enronEmail.json',$json_formatted);
?>

这段代码对之前创建的 enronEmail.csv 文件(包含一个表头行)的结果如下:

[{"name":"Lysa Akin","email_id":"[email protected]"},
{"name":"Phillip Allen","email_id":"[email protected]"},
{"name":"Harry Arora","email_id":"[email protected]"}…]

在这个示例中,实际 CSV 文件中的 151 条结果中,只显示了前三行。

使用 Python 转换

在本节中,我们描述了多种使用 Python 操作 CSV 转换为 JSON,以及反向操作的方法。在这些示例中,我们将探索实现这一目标的不同方式,既包括使用特别安装的库,也包括使用更基础的 Python 代码。

使用 Python 将 CSV 转换为 JSON

我们已经找到几种使用 Python 将 CSV 文件转换为 JSON 的方法。其中一种方法使用了内置的 csvjson 库。假设我们有一个 CSV 文件,其中的行如下(仅展示前三行):

name,email_id
"Lysa Akin",[email protected]
"Phillip Allen",[email protected]
"Harry Arora",[email protected]

我们可以编写一个 Python 程序来读取这些行并将其转换为 JSON:

import json
import csv

# read in the CSV file
with open('enronEmail.csv') as file:
    file_csv = csv.DictReader(file)
    output = '['
    # process each dictionary row
    for row in file_csv:
      # put a comma between the entities
      output += json.dumps(row) + ','
    output = output.rstrip(',') + ']'
# write out a new file to disk
f = open('enronEmailPy.json','w')
f.write(output)
f.close()

结果的 JSON 将如下所示(仅展示前两行):

[{"email_id": "[email protected]", "name": "Lysa Akin"},
{"email_id": "[email protected]", "name": "Phillip Allen"},…]

使用这种方法的一个优点是,它不需要任何特殊的库安装或命令行访问,除了获取和放置你正在读取(CSV)和写入(JSON)文件之外。

使用 csvkit 将 CSV 转换为 JSON

改变 CSV 为 JSON 的第二种方法依赖于一个非常有趣的 Python 工具包 csvkit。要通过 Canopy 安装 csvkit,只需启动 Canopy 终端窗口(你可以在 Canopy 中通过导航至 工具 | Canopy 终端 找到它),然后运行 pip install csvkit 命令。所有使用 csvkit 所需的依赖将自动为你安装。此时,你可以选择通过 Python 程序作为库使用 import csvkit 来访问 csvkit,或通过命令行访问,正如我们将在接下来的代码段中所做的那样:

csvjson enronEmail.csv > enronEmail.json

该命令将 enronEmail.csv 文件转换为 JSON 格式的 enronEmail.csvkit.json 文件,过程快速而简便。

csvkit 包含多个其他非常有用的命令行程序,其中包括 csvcut,它可以从 CSV 文件中提取任意列列表,以及 csvformat,它可以执行 CSV 文件的分隔符交换,或修改行结束符等清理操作。如果你只想提取少数几列进行处理,csvcut 程序特别有用。对于这些命令行工具中的任何一个,你都可以将其输出重定向到一个新文件。以下命令行处理一个名为 bigFile.csv 的文件,剪切出第一列和第三列,并将结果保存为一个新的 CSV 文件:

csvcut bigFile.csv –c 1,3 > firstThirdCols.csv

提示

关于 csvkit 的更多信息,包括完整的文档、下载和示例,见 csvkit.rtfd.org/

Python JSON 转 CSV

使用 Python 读取 JSON 文件并将其转换为 CSV 进行处理是非常简单的:

import json
import csv

with open('enronEmailPy.json', 'r') as f:
    dicts = json.load(f)
out = open('enronEmailPy.csv', 'w')
writer = csv.DictWriter(out, dicts[0].keys())
writer.writeheader()
writer.writerows(dicts)
out.close()

该程序接收一个名为 enronEmailPy.json 的 JSON 文件,并使用 JSON 的键作为头行,将该文件导出为 CSV 格式的新文件,名为 enronEmailPy.csv

示例项目

在本章中,我们重点介绍了将数据从一种格式转换为另一种格式,这是一个常见的数据清理任务,在数据分析项目的其余部分完成之前需要多次执行。我们聚焦于一些非常常见的文本格式(CSV 和 JSON)和常见的数据存储位置(文件和 SQL 数据库)。现在,我们准备通过一个示例项目扩展我们对数据转换的基本知识,该项目将要求我们在一些不那么标准化但仍然是文本格式的数据格式之间进行转换。

在这个项目中,我们将研究我们的 Facebook 社交网络。我们将:

  1. 使用 netvizz 下载我们的 Facebook 社交网络(朋友及其之间的关系),并保存为一种名为 图描述格式GDF)的文本格式。

  2. 生成 Facebook 社交网络的图形表示,将我们网络中的人物显示为节点,并将他们的友谊作为连接线(称为)连接到这些节点之间。为此,我们将使用 D3 JavaScript 图形库。此库期望以 JSON 格式呈现网络数据。

  3. 计算社交网络的一些指标,如网络的大小(称为)和网络中两个人之间的最短路径。为此,我们将使用 Python 中的networkx包。此包期望以文本格式呈现数据,称为Pajek格式。

该项目的主要目标将是展示如何调和所有这些不同的期望格式(GDF、Pajek 和 JSON),并执行从一种格式到另一种格式的转换。我们的次要目标将是确实提供足够的示例代码和指导,以执行对我们社交网络的小规模分析。

步骤一 - 下载 Facebook 数据为 GDF

在此步骤中,您需要登录您的 Facebook 账户。使用 Facebook 的搜索框找到 netvizz 应用程序,或使用此 URL 直接链接到 netvizz 应用程序:apps.facebook.com/netvizz/

一旦进入 netvizz 页面,请点击个人网络。随后的页面将解释,点击开始按钮将提供一个可下载的文件,其中包含两个项目:列出所有您的朋友及其之间连接的 GDF 格式文件,以及一个制表符分隔值(TSV)统计文件。对于本项目,我们主要感兴趣的是 GDF 文件。点击开始按钮,并在随后的页面上右键单击 GDF 文件以将其保存到您的本地磁盘,如下截图所示:

步骤一 - 下载 Facebook 数据为 GDF

使用 netvizz Facebook 应用程序可以将我们的社交网络下载为 GDF 文件。

此时给文件起一个更短的名字可能会有帮助。(我称我的文件为personal.gdf,并将其保存在专门为此项目创建的目录中。)

步骤二 - 查看文本编辑器中的 GDF 文件格式

在你的文本编辑器中打开文件(我在这里使用 Text Wrangler),并注意该文件的格式的几个要点:

  1. 文件分为两部分:节点和边。

  2. 节点位于文件的第一部分,以nodedef一词为前缀。节点列表是关于我所有朋友及其一些基本信息的列表(它们的性别和其内部 Facebook 标识号)。节点按照人们加入 Facebook 的日期顺序列出。

  3. 文件的第二部分显示了我的朋友之间的边或连接。有时,这些也称为链接。文件的这一部分以edgedef一词为前缀。边描述了哪些我的朋友与其他朋友相连。

这是节点部分的摘录示例:

nodedef>name VARCHAR,label VARCHAR,sex VARCHAR,locale VARCHAR,agerank INT
  1234,Bugs Bunny,male,en_US,296
  2345,Daffy Duck,male,en_US,295
  3456,Minnie Mouse,female,en_US,294

以下是边部分的摘录。它显示了 Bugs1234)和 Daffy2345)是朋友,而 Bugs 也与 Minnie3456)是朋友:

edgedef>node1 VARCHAR,node2 VARCHAR 
1234,2345
1234,3456
3456,9876

第三步 - 将 GDF 文件转换为 JSON

我们要执行的任务是使用 D3 构建该数据的社交网络表示。首先,我们需要查看 D3 中用于构建社交网络的几十个现有示例,例如 D3 示例库中的示例:github.com/mbostock/d3/wiki/Gallerychristopheviau.com/d3list/

这些社交网络图的示例依赖于 JSON 文件。每个 JSON 文件展示了节点以及它们之间的边。以下是这些 JSON 文件的一个示例:

{"nodes": [
  {"name":"Bugs Bunny"},
  {"name":"Daffy Duck"},
  {"name":"Minnie Mouse"}],
  "edges": [
  {"source": 0,"target": 2},
  {"source": 1,"target": 3},
  {"source": 2,"target": 3}]}

这段 JSON 代码最重要的一点是,它具有与 GDF 文件相同的两个主要部分:节点和边。节点仅仅是人的名字。边是一个数字对列表,表示友谊关系。不过,这些对并没有使用 Facebook 的标识号,而是使用了节点列表中每个项的索引,从 0 开始。

此时我们还没有 JSON 文件,只有一个 GDF 文件。那么我们如何构建这个 JSON 文件呢?当我们仔细查看 GDF 文件时,会发现它看起来像是两个 CSV 文件叠在一起。正如本章早些时候提到的,我们有几种不同的策略可以将 CSV 转换为 JSON。

因此,我们决定将 GDF 转换为 CSV,再将 CSV 转换为 JSON。

注意

等等,如果那个 JSON 示例看起来和我在网上找到的用于在 D3 中绘制社交网络图的 JSON 文件不一样怎么办?

你在网上找到的 D3 社交网络可视化示例可能会展示每个节点或连接的许多附加值,例如,它们可能包括额外的属性,用于表示大小差异、悬停功能或颜色变化,如这个示例所示:bl.ocks.org/christophermanning/1625629。该可视化展示了芝加哥的付费政治游说者之间的关系。在这个示例中,代码会根据 JSON 文件中的信息来决定节点的圆圈大小以及当你将鼠标悬停在节点上时显示的文本。它制作了一个非常漂亮的图表,但它比较复杂。由于我们的主要目标是学习如何清理数据,我们将在这里处理一个简化的示例,不包含许多这些额外功能。不过不用担心,我们的示例仍然会构建出一个漂亮的 D3 图表!

要将 GDF 文件转换为我们需要的 JSON 格式,可以按照以下步骤进行:

  1. 使用文本编辑器将 personal.gdf 文件拆分为两个文件,nodes.gdflinks.gdf

  2. 修改每个文件中的标题行,以匹配最终希望在 JSON 文件中出现的列名:

    id,name,gender,lang,num
    1234,Bugs Bunny,male,en_US,296
    2345,Daffy Duck,male,en_US,295
    9876,Minnie Mouse,female,en_US,294
    
    source,target
    1234,2345
    1234,9876
    2345,9876
    
  3. 使用csvcut工具(前面讨论的 csvkit 的一部分)从nodes.gdf文件中提取第一列和第二列,并将输出重定向到一个名为nodesCut.gdf的新文件:

    csvcut -c 1,2 nodes.gdf > nodesCut.gdf
    
    
  4. 现在,我们需要给每个边对分配一个索引值,而不是使用完整的 Facebook ID 值。这个索引仅通过节点在节点列表中的位置来标识该节点。我们需要执行这个转换,以便数据能够尽可能少地重构地输入到我们的 D3 力导向网络代码示例中。我们需要将其转换为:

    source,target
    1234,2345
    1234,9876
    2345,9876
    

    变成这样:

    source,target
    0,1
    0,2
    1,2
    

    这是一个小的 Python 脚本,它将自动创建这些索引值:

    import csv
    
    # read in the nodes
    with open('nodesCut.gdf', 'r') as nodefile:
        nodereader = csv.reader(nodefile)
        nodeid, name = zip(*nodereader)
    
    # read in the source and target of the edges
    with open('edges.gdf', 'r') as edgefile:
        edgereader = csv.reader(edgefile)
        sourcearray, targetarray = zip(*edgereader)
    slist = list(sourcearray)
    tlist = list(targetarray)
    
    # find the node index value for each source and target
    for n,i in enumerate(nodeid):
        for j,s in enumerate(slist):
            if s == i:
                slist[j]=n-1
        for k,t in enumerate(tlist):
            if t == i: 
                tlist[k]=n-1
    # write out the new edge list with index values
    with open('edgelistIndex.csv', 'wb') as indexfile:
        iwriter = csv.writer(indexfile)
        for c in range(len(slist)):
            iwriter.writerow([ slist[c], tlist[c]])
    
  5. 现在,返回到nodesCut.csv文件,并删除id列:

    csvcut -c 2 nodesCut.gdf > nodesCutName.gdf
    
    
  6. 构建一个小的 Python 脚本,将这些文件逐一处理,并将它们写入一个完整的 JSON 文件,准备好进行 D3 处理:

    import csv
    import json
    
    # read in the nodes file
    with open('nodesCutName.gdf') as nodefile:
        nodefile_csv = csv.DictReader(nodefile)
        noutput = '['
        ncounter = 0;
    
        # process each dictionary row
        for nrow in nodefile_csv:
            # look for ' in node names, like O'Connor
            nrow["name"] = \
            str(nrow["name"]).replace("'","")
            # put a comma between the entities
            if ncounter > 0:
                noutput += ','
            noutput += json.dumps(nrow)
            ncounter += 1
        noutput += ']'
        # write out a new file to disk
        f = open('complete.json','w')
        f.write('{')
        f.write('\"nodes\":' )
        f.write(noutput)
    
    # read in the edge file
    with open('edgelistIndex.csv') as edgefile:
        edgefile_csv = csv.DictReader(edgefile)
        eoutput = '['
        ecounter = 0;
        # process each dictionary row
        for erow in edgefile_csv:
            # make sure numeric data is coded as number not # string
            for ekey in erow:
                try:
                    erow[ekey] = int(erow[ekey])
                except ValueError:
                    # not an int
                    pass
            # put a comma between the entities
            if ecounter > 0:
                eoutput += ','
            eoutput += json.dumps(erow)
            ecounter += 1
        eoutput += ']'
    
        # write out a new file to disk
        f.write(',')
        f.write('\"links\":')
        f.write(eoutput)
        f.write('}')
        f.close()
    

第四步 – 构建 D3 图表

这一部分展示了如何将我们的节点和链接的 JSON 文件输入到一个构建力导向图的 D3 基础示例中。这个代码示例来自 D3 网站,使用提供的 JSON 文件构建一个简单的图表。每个节点都显示为一个圆圈,当你将鼠标悬停在节点上时,节点的名称会以工具提示的形式显示出来:

<!DOCTYPE html>
<!-- this code is based on the force-directed graph D3 example given at : https://gist.github.com/mbostock/4062045 -->

<meta charset="utf-8">
<style>

.node {
  stroke: #fff;
  stroke-width: 1.5px;
}

.link {
  stroke: #999;
  stroke-opacity: .6;
}

</style>
<body>
<!-- make sure you have downloaded the D3 libraries and stored them locally -->
<script src="img/d3.min.js"></script>
<script>

var width = 960, height = 500;
var color = d3.scale.category20();
var force = d3.layout.force()
    .charge(-25)
    .linkDistance(30)
    .size([width, height]);

var svg = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height);

d3.json("complete.json", function(error, graph) {
  force
      .nodes(graph.nodes)
      .links(graph.links)
      .start();

  var link = svg.selectAll(".link")
      .data(graph.links)
    .enter().append("line")
      .attr("class", "link")
      .style("stroke-width", function(d) { return Math.sqrt(d.value); });

  var node = svg.selectAll(".node")
      .data(graph.nodes)
    .enter().append("circle")
      .attr("class", "node")
      .attr("r", 5)
      .style("fill", function(d) { return color(d.group); })
      .call(force.drag);

  node.append("title")
      .text(function(d) { return d.name; });

  force.on("tick", function() {
    link.attr("x1", function(d) { return d.source.x; })
      .attr("y1", function(d) { return d.source.y; })
      .attr("x2", function(d) { return d.target.x; })
      .attr("y2", function(d) { return d.target.y; });

    node.attr("cx", function(d) { return d.x; })
      .attr("cy", function(d) { return d.y; });
  });
});
</script>

以下截图显示了这个社交网络的示例。一个节点已经被悬停,显示了该节点的工具提示(名称)。

第四步 – 构建 D3 图表

使用 D3 构建的社交网络

第五步 – 将数据转换为 Pajek 文件格式

到目前为止,我们已经将 GDF 文件转换为 CSV 文件,然后转换为 JSON 文件,并构建了一个 D3 图表。在接下来的两步中,我们将继续实现目标,把数据转换成可以用来计算社交网络指标的格式。

在这一步中,我们将原始的 GDF 文件稍作调整,使其成为一个有效的Pajek文件,这是社交网络工具 networkx 所需要的格式。

注意

单词pajek在斯洛文尼亚语中意味着蜘蛛。社交网络可以被看作是由节点及其之间的链接组成的网络。

我们的 Facebook GDF 文件转换为 Pajek 文件后的格式如下:

*vertices 296
1234 Bugs_Bunny male en_US 296
2456 Daffy_Duck male en_US 295
9876 Minnie_Mouse female en_US 294
*edges
1234 2456
2456 9876
2456 3456

下面是关于这个 Pajek 文件格式需要注意的几件重要事情:

  • 它是以空格分隔的,而不是以逗号分隔的。

  • 就像在 GDF 文件中一样,数据分为两个主要部分,并且这些部分有标签,标签以星号*开头。这两个部分分别是顶点(另一个单词是节点)和边。

  • 文件中有一个计数,显示总共有多少个顶点(节点),这个计数会显示在顶部一行的“vertices”旁边。

  • 每个人的姓名去掉了空格并用下划线替换。

  • 在节点部分,其他列是可选的。

为了将我们的 GDF 文件转换为 Pajek 格式,让我们使用文本编辑器,因为这些更改相当简单,而且我们的文件也不大。我们将执行以下数据清理任务:

  1. 将你的 GDF 文件保存为一个新的文件,并将其命名为类似 fbPajek.net.net 扩展名通常用于 Pajek 网络文件)。

  2. 替换文件中的顶部行。当前它看起来像这样:

    nodedef>name VARCHAR,label VARCHAR,sex VARCHAR,locale VARCHAR,agerank INT
    

    你需要将其更改为类似以下的形式:

    *vertices 296
    

    确保顶点的数量与实际文件中对应的数量相匹配。这是节点的数量。每行应当有一个节点,出现在你的 GDF 文件中。

  3. 替换文件中的边缘行。当前它看起来像这样:

    edgedef>node1 VARCHAR,node2 VARCHAR
    

    你需要将其更改为如下形式:

    *edges
    
  4. 从第二行开始,替换文件中每个空格为下划线。这是可行的,因为此文件中唯一的空格出现在名字中。看一下这个:

    1234,Bugs Bunny,male,en_US,296
    2456,Daffy Duck,male,en_US,295
    3456,Minnie Mouse,female,en_US,294
    

    这个操作将把前面的内容变成这样:

    1234,Bugs_Bunny,male,en_US,296
    2456,Daffy_Duck,male,en_US,295
    3456,Minnie_Mouse,female,en_US,294
    
  5. 现在,使用查找和替换将所有逗号替换为空格。节点部分的结果将是:

    *vertices 296
    1234 Bugs_Bunny male en_US 296
    2456 Daffy_Duck male en_US 295
    3456 Minnie_Mouse female en_US 294
    

    边缘部分的结果将是:

    *edges
    1234 2456
    2456 9876
    2456 3456
    
  6. 最后一步;使用文本编辑器的查找功能,定位所有在名字中有撇号的 Facebook 朋友。将这些撇号替换为空字符。因此,Cap'n_Crunch 变为:

    1998988 Capn_Crunch male en_US 137
    

    现在这是一个完全清理过的、Pajek 格式化的文件。

第六步——计算简单的网络指标

到此为止,我们已经准备好使用 Python 包如 networkx 来运行一些简单的社交网络指标。尽管社交网络分析SNA)超出了本书的范围,但我们仍然可以轻松地进行一些计算,而无需深入探索 SNA 的奥秘。

首先,我们应该确保已安装 networkx 包。我正在使用 Canopy 作为我的 Python 编辑器,所以我将使用包管理器搜索并安装 networkx。

然后,一旦安装了 networkx,我们可以编写一些简单的 Python 代码来读取我们的 Pajek 文件,并输出一些关于我的 Facebook 网络结构的有趣信息:

import networkx as net

# read in the file
g = net.read_pajek('fb_pajek.net')

# how many nodes are in the graph?
# print len(g)

# create a degree map: a set of name-value pairs linking nodes
# to the number of edges in my network
deg = net.degree(g)
# sort the degree map and print the top ten nodes with the
# highest degree (highest number of edges in the network)
print sorted(deg.iteritems(), key=lambda(k,v): (-v,k))[0:9]

我的网络的结果如下所示。列出了前十个节点,以及这些节点分别连接到我其他节点的数量:

[(u'Bambi', 134), (u'Cinderella', 56), (u'Capn_Crunch', 50), (u'Bugs_Bunny', 47), (u'Minnie_Mouse', 47), (u'Cruella_Deville', 46), (u'Alice_Wonderland', 44), (u'Prince_Charming', 42), (u'Daffy_Duck', 42)]

这表明 Bambi 与我的 134 个其他朋友连接,而 Prince_Charming 只与我的 42 个其他朋友连接。

提示

如果你遇到任何关于缺少引号的 Python 错误,请仔细检查你的 Pajek 格式文件,确保所有节点标签中没有空格或其他特殊字符。在前面示例中的清理过程中,我们去除了空格和引号字符,但你的朋友可能在名字中使用了更多特殊字符!

当然,你可以使用 networkx 和 D3 可视化做更多有趣的事情,但这个示例项目的目的是让我们理解数据清洗过程对任何大规模分析成功结果的重要性。

总结

在本章中,我们学习了许多不同的数据格式转换方法。其中一些技巧很简单,比如直接将文件保存为你想要的格式,或者寻找菜单选项来输出正确的格式。其他时候,我们需要编写自己的程序化解决方案。

很多项目,比如我们在本章实现的示例项目,都需要进行多个不同的清理步骤,我们必须仔细规划清理步骤并记录下我们所做的事情。networkx 和 D3 都是非常棒的工具,但在使用它们之前,数据必须是特定格式的。同样,通过 netvizz 可以轻松获取 Facebook 数据,但它也有自己的数据格式。在不同文件格式之间找到简单的转换方法是数据科学中的一项关键技能。

在本章中,我们进行了很多结构化数据和半结构化数据之间的转换。但清理混乱数据呢,比如非结构化文本?

在第五章,从网页收集和清理数据中,我们将继续充实我们的数据科学清理工具箱,学习一些清理我们在网页上找到的页面的方法。

第五章:从网页收集和清理数据

最常见和有用的厨房工具之一是滤网,也叫筛子、漏网或中式过滤器,其目的是在烹饪过程中将固体与液体分开。在本章中,我们将为我们在网页上找到的数据构建滤网。我们将学习如何创建几种类型的程序,帮助我们找到并保留我们想要的数据,同时丢弃不需要的部分。

在本章中,我们将:

  • 理解两种想象 HTML 页面结构的方式,第一种是(a)作为我们可以查找模式的行集合,第二种是(b)作为包含节点的树形结构,我们可以识别并收集这些节点的值。

  • 尝试三种解析网页的方法,一种是使用逐行方法(基于正则表达式的 HTML 解析),另外两种使用树形结构方法(Python 的 BeautifulSoup 库和名为 Scraper 的 Chrome 浏览器工具)。

  • 在一些现实世界的数据上实现这三种技术;我们将练习从网页论坛中抓取日期和时间。

理解 HTML 页面结构

网页只是一个包含一些特殊标记元素(有时称为 HTML标签)的文本文件,目的是告诉网页浏览器页面应该如何在用户端显示。例如,如果我们想让某个特定的词语以强调的方式显示,我们可以将其用<em>标签包围,如下所示:

<em>非常重要</em>的是你必须遵循这些指令。

所有网页都有这些相同的特征;它们由文本组成,文本中可能包含标签。我们可以使用两种主要的思维模型从网页中提取数据。两种模型各有其有用的方面。在这一节中,我们将描述这两种结构模型,然后在下一节中,我们将使用三种不同的工具来提取数据。

逐行分隔符模型

在思考网页的最简单方式时,我们集中于这样一个事实:有很多 HTML 元素/标签被用来组织和显示网页内容。如果我们想从网页中提取有趣的数据,在这个简单的模型中,我们可以使用页面文本和嵌入的 HTML 元素本身作为分隔符。例如,在前面的例子中,我们可能决定收集所有<em>标签中的内容,或者我们可能想收集在<em>标签之前或</em>标签之后的所有内容。

在这个模型中,我们将网页视为一个大致无结构的文本集合,而 HTML 标签(或文本中的其他特征,如重复的词语)帮助提供结构,我们可以利用这些结构来界定我们想要的部分。一旦我们有了分隔符,就能从杂乱无章的内容中筛选出有趣的数据。

例如,下面是来自现实世界 HTML 页面的一段摘录,来自 Django IRC 频道的聊天日志。让我们考虑如何使用其 HTML 元素作为分隔符,提取有用的数据:

<div id="content">
<h2>Sep 13, 2014</h2>

<a href="/https/www.cnblogs.com/2014/sep/14/">← next day</a> Sep 13, 2014  <a href="/https/www.cnblogs.com/2014/sep/12/">previous day →</a>

<ul id="ll">
<li class="le" rel="petisnnake"><a href="#1574618" name="1574618">#</a> <span style="color:#b78a0f;8" class="username" rel="petisnnake">&lt;petisnnake&gt;</span> i didnt know that </li>
...
</ul>
...
</div>

以这个示例文本为例,我们可以使用<h2></h2>标签作为分隔符,提取出这个聊天记录的日期。我们可以使用<li></li>标签作为分隔符提取文本行,在该行内,我们可以看到rel=""可用于提取聊天者的用户名。最后,似乎从</span>结束到</li>开始的所有文本就是用户发送到聊天频道的实际消息行。

注意

这些聊天日志可以在线访问,网址是 Django IRC 日志网站,django-irc-logs.com。该网站还提供了一个关键词搜索接口,供用户查询日志。前述代码中的省略号()表示示例中已删除的部分文本。

从这段杂乱的文本中,我们可以使用分隔符的概念提取出三部分干净的数据(日志日期、用户和消息行)。

树形结构模型

另一种想象网页文本的方式是将其视为由 HTML 元素/标签组成的树形结构,每个元素与页面上的其他标签相互关联。每个标签都显示为一个节点,树形结构由页面中所有不同的节点组成。出现在另一个标签内的标签被视为子节点,而包围它的标签则是父节点。在之前的 IRC 聊天示例中,HTML 代码可以通过树形图表示,如下所示:

树形结构模型

如果我们能够将 HTML 文本想象成树形结构,我们可以使用编程语言为我们构建树形结构。这使我们能够根据元素名称或元素在列表中的位置提取所需的文本值。例如:

  • 我们可能需要按名称获取标签的值(给我<h2>节点中的文本)

  • 我们可能需要某种特定类型的所有节点(给我所有在<div>中的<ul>里的<li>节点)

  • 我们可能需要某个元素的所有属性(给我所有<li>元素中的rel属性列表)

在本章的其余部分,我们将结合这两种思维模型——逐行处理和树形结构——并通过一些示例进行实践。我们将演示三种不同的方法,来提取和清理 HTML 页面中的数据。

方法一 – Python 和正则表达式

在本节中,我们将使用一种简单的方法,从 HTML 页面中提取我们想要的数据。这种方法基于识别页面中的分隔符,并通过正则表达式进行模式匹配来提取数据。

你可能还记得我们在第三章,清理数据的得力助手——电子表格和文本编辑器中,曾经尝试过一些正则表达式(regex),当时我们学习如何使用文本编辑器。在这一章中,某些概念将类似,不过这次我们将编写一个 Python 程序来找到匹配的文本并提取出来,而不是像在那一章中那样使用文本编辑器进行替换。

在我们开始示例之前,最后需要注意的是,虽然这种正则表达式(regex)方法相对容易理解,但根据你特定的项目,它也有一些限制,可能会影响效果。我们将在本节的最后详细描述这种方法的局限性。

第一步 —— 找到并保存一个用于实验的网页文件

在这个示例中,我们将从之前提到的 Django 项目中获取一个 IRC 聊天记录。这些文件是公开的,具有相当规则的结构,因此非常适合用于这个项目。前往 Django IRC 日志档案,django-irc-logs.com/,找到一个你感兴趣的日期。进入目标日期的页面并将其保存到你的工作目录。当你完成时,应该会得到一个 .html 文件。

第二步 —— 查看文件并决定值得提取的内容

由于我们在第二章,基础知识——格式、类型和编码中学到过,.html 文件实际上就是文本,而第三章,清理数据的得力助手——电子表格和文本编辑器,让我们非常熟悉如何在文本编辑器中查看文本文件,因此这一步应该很容易。只需在文本编辑器中打开 HTML 文件,查看它。有什么内容看起来适合提取?

当我查看文件时,我看到有几个我想要提取的内容。立刻我就看到,对于每条聊天评论,有行号、用户名和评论本身。我们计划从每一行聊天记录中提取这三个项目。

下图显示了我的文本编辑器中打开的 HTML 文件。由于某些行非常长,我已经启用了软换行(在 TextWrangler 中此选项位于菜单下的 查看 | 文本显示 | 软换行文本)。大约在第 29 行,我们看到了聊天记录列表的开始,每一行都包含我们感兴趣的三个项目:

第二步 —— 查看文件并决定值得提取的内容

我们的任务是找出每一行看起来相同的特征,以便我们可以预测性地从每一行聊天记录中提取出相同的三个项目。查看文本后,以下是我们可以遵循的一些可能规则,以准确地提取每个数据项并尽量减少调整:

  • 看起来我们要找的三个项目都位于<li>标签中,而这些<li>标签本身位于<ul id="ll">标签内。每个<li>表示一条聊天消息。

  • 在该消息中,行号出现在两个位置:它跟在字符串<a href="#后面,并且出现在name属性后的引号内。在示例文本中,第一个行号是1574618

  • username属性出现在三个位置,首先是li class="le"rel属性值。在span标签内,username属性再次作为rel属性的值出现,且它也出现在&lt;和&gt;符号之间。在示例文本中,第一个usernamepetisnnake

  • 行消息出现在</span>标签后和</li>标签前。在示例中,第一个行消息是i didnt know that

现在我们已经知道了如何找到数据项的规则,可以开始编写我们的程序了。

第三步——编写一个 Python 程序,提取有用的信息并将其保存到 CSV 文件中

下面是一个简短的代码,用于打开先前显示格式的 IRC 日志文件,解析出我们感兴趣的三个部分,并将它们打印到一个新的 CSV 文件中:

import re
import io

row = []

infile  = io.open('django13-sept-2014.html', 'r', encoding='utf8')
outfile = io.open('django13-sept-2014.csv', 'a+', encoding='utf8')
for line in infile:
    pattern = re.compile(ur'<li class=\"le\" rel=\"(.+?)\"><a href=\"#(.+?)\" name=\"(.+?)<\/span> (.+?)</li>', re.UNICODE)
    if pattern.search(line):
        username = pattern.search(line).group(1)
        linenum = pattern.search(line).group(2)
        message = pattern.search(line).group(4)
        row.append(linenum)
        row.append(username)
        row.append(message)
        outfile.write(', '.join(row))
        outfile.write(u'\n')
        row = []
infile.close()

这段代码中最棘手的部分是pattern这一行。该行构建了模式匹配,文件中的每一行都将与之进行比较。

提示

保持警惕。每当网站开发者更改页面中的 HTML 时,我们就有可能面临构造的正则表达式模式失效的风险。事实上,在编写这本书的几个月里,页面的 HTML 至少更改过一次!

每个匹配的目标组看起来像这样:.+?。总共有五个匹配组。其中三个是我们感兴趣的项目(usernamelinenummessage),而其他两个组只是无关的内容,我们可以丢弃。我们还会丢弃网页中其他部分的内容,因为它们根本不符合我们的模式。我们的程序就像一个筛子,只有三个有效的孔。好东西会通过这些孔流出,而无关的内容则被留下。

第四步——查看文件并确保其内容干净

当我们在文本编辑器中打开新的 CSV 文件时,可以看到前几行现在像这样:

1574618, petisnnake, i didnt know that 
1574619, dshap, FunkyBob: ahh, hmm, i wonder if there's a way to do it in my QuerySet subclass so i'm not creating a new manager subclass *only* for get_queryset to do the intiial filtering 
1574620, petisnnake, haven used Django since 1.5

这看起来是一个很好的结果。你可能注意到,第三列的文本没有被包围在任何分隔符中。这可能会成为一个问题,因为我们已经使用逗号作为分隔符。如果第三列中有逗号怎么办?如果你担心这个问题,可以为第三列添加引号,或者使用制表符(tab)作为列的分隔符。为此,将第一行outfile.write()中的连接字符从逗号改为\t(制表符)。你也可以通过ltrim()函数修剪消息中的空格,去除任何多余的字符。

使用正则表达式解析 HTML 的局限性

这个正则表达式方法一开始看起来很简单,但它也有一些局限性。首先,对于新的数据清理者来说,设计和完善正则表达式可能会非常麻烦。你肯定需要计划花费大量时间进行调试,并且写下大量文档。为了帮助生成正则表达式,我强烈建议使用正则表达式测试工具,如 Pythex.org,或者直接使用你喜欢的搜索引擎找到一个。如果你使用的是 Python 语言,请确保你选择的是 Python 正则表达式测试工具。

接下来,你应该提前知道,正则表达式完全依赖于网页结构在未来保持不变。因此,如果你计划定期从一个网站收集数据,你今天写的正则表达式可能明天就不管用了。只有在网页布局没有变化的情况下,它们才会有效。即使在两个标签之间添加一个空格,也会导致整个正则表达式失败,而且调试起来非常困难。还要记住,大多数时候你无法控制网站的变化,因为你通常不是自己的网站在收集数据!

最后,有许多情况几乎不可能准确地编写正则表达式来匹配给定的 HTML 结构。正则表达式很强大,但并不完美或无懈可击。对于这个问题的幽默解读,我推荐你去看那个在 Stack Overflow 上被点赞超过 4000 次的著名回答:stackoverflow.com/questions/1732348/。在这个回答中,作者幽默地表达了许多程序员的挫败感,他们一遍又一遍地尝试解释为什么正则表达式并不是解析不规则且不断变化的 HTML 的完美解决方案。

方法二 - Python 和 BeautifulSoup

由于正则表达式存在一些局限性,我们在数据清理工具包中肯定还需要更多工具。这里,我们介绍如何使用基于解析树的 Python 库 BeautifulSoup 从 HTML 页面中提取数据。

第一步 - 找到并保存一个用于实验的文件

对于这一步,我们将使用与方法 1 相同的文件:来自 Django IRC 频道的文件。我们将搜索相同的三个项目。这样做将使得这两种方法之间的比较更加容易。

第二步 - 安装 BeautifulSoup

BeautifulSoup 目前是 4 版本。这个版本可以在 Python 2.7 和 Python 3 中使用。

注意

如果你使用的是 Enthought Canopy Python 环境,只需在 Canopy 终端中运行 pip install beautifulsoup4

第三步 - 编写一个 Python 程序来提取数据

我们感兴趣的三个项位于一组 li 标签中,具体来说是那些 class="le" 的标签。在这个特定文件中没有其他 li 标签,但为了以防万一,我们还是要具体说明。以下是我们需要的项及其在解析树中的位置:

  • 我们可以从 li 标签下的 rel 属性中提取用户名。

  • 我们可以从 a 标签的 name 属性中获取 linenum 值。a 标签也是 li 标签内容中的第一个项。

    注意

    请记住,数组是从零开始的,所以我们需要请求项 0。

    在 BeautifulSoup 中,标签的内容是该标签在解析树中的下级项。一些其他包会将这些称为 子项

  • 我们可以从 li 标签的第四个内容项中提取消息(引用为数组项 [3])。我们还注意到每条消息前面都有一个空格,因此我们需要在保存数据之前去除它。

这里是与我们在解析树中想要的内容相对应的 Python 代码:

from bs4 import BeautifulSoup
import io

infile  = io.open('django13-sept-2014.html', 'r', encoding='utf8')
outfile = io.open('django13-sept-2014.csv', 'a+', encoding='utf8')
soup = BeautifulSoup(infile)

row = []
allLines = soup.findAll("li","le")
for line in allLines:
    username = line['rel']
    linenum = line.contents[0]['name']
    message = line.contents[3].lstrip()
    row.append(linenum)
    row.append(username)
    row.append(message)
    outfile.write(', '.join(row))
    outfile.write(u'\n')
    row = []
infile.close()

第四步 – 查看文件并确保其清洁

当我们在文本编辑器中打开新的 CSV 文件时,可以看到前几行现在与方法 1 中的内容完全相同:

1574618, petisnnake, i didnt know that 
1574619, dshap, FunkyBob: ahh, hmm, i wonder if there's a way to do it in my QuerySet subclass so i'm not creating a new manager subclass *only* for get_queryset to do the intiial filtering 
1574620, petisnnake, haven used Django since 1.5

就像使用正则表达式方法一样,如果你担心最后一列中嵌入的逗号,你可以将该列的文本用引号括起来,或者直接使用制表符来分隔列。

方法三 – Chrome Scraper

如果你真的不想编写程序来解析数据,也有几个基于浏览器的工具使用树形结构来帮助你识别和提取感兴趣的数据。我认为使用起来最简单、工作量最少的工具是一个名为 Scraper 的 Chrome 扩展,由名为 mnmldave(真名:Dave Heaton)的开发者创建。

第一步 – 安装 Scraper Chrome 扩展

如果你还没有安装 Chrome 浏览器,请下载并安装。确保你获得正确的 Scraper 扩展;有几个扩展的名字非常相似。我建议使用开发者自己提供的 GitHub 网站来下载该产品,网址是 mnmldave.github.io/scraper/。这样你可以确保使用正确的抓取工具,而不是通过 Chrome 商店搜索。从 mmldave.github.io/scraper 网站,点击链接从 Google 商店安装扩展并重启浏览器。

第二步 – 从网站收集数据

将浏览器指向我们之前用来获取其他两个网页数据提取实验数据的相同网页 URL,其中一个是 Django IRC 日志。我在这里使用的是 2014 年 9 月 13 日的日志作为示例和截图,所以我将访问 django-irc-logs.com/2014/sep/13/

在我编写本文时,浏览器中的该页面显示如下:

第二步 – 从网站收集数据

我们对这份 IRC 日志中的三个项目感兴趣:

  • 行号(我们从前两个实验中知道,这部分链接位于#符号下方)

  • 用户名(位于<>符号之间)

  • 实际的行信息

Scraper 允许我们依次高亮显示这三项内容,并将其值导出到 Google 电子表格中,之后我们可以将它们重新组合成一个单一的表格,并导出为 CSV 格式(或根据需要进行其他操作)。以下是操作步骤:

  1. 使用鼠标高亮你想要抓取的项目。

  2. 右键点击并从菜单中选择抓取相似项…。在以下示例中,我选择了用户名petisnnake作为我要让 Scraper 使用的目标:步骤二 – 从网站收集数据

  3. 在选择抓取相似项后,工具会显示一个新窗口,列出页面上所有相似项目。以下截图显示了 Scraper 找到的所有用户名列表:步骤二 – 从网站收集数据

    Scraper 根据一个示例用户名找到所有相似的项目。

  4. 窗口底部有一个标有导出到 Google 文档…的按钮。请注意,根据您的设置,您可能需要点击同意以允许 Scraper 访问 Google 文档。

步骤三 – 数据列的最终清理

一旦我们从页面上提取了所有数据元素并将它们存储在独立的 Google 文档中,我们将需要将它们合并为一个文件,并进行最后的清理。以下是提取后的行号示例,但在清理之前的样子:

步骤三 – 数据列的最终清理

我们对列A完全不感兴趣,也不关心前导的#符号。用户名和行信息数据类似——我们需要大部分内容,但我们想要去除一些符号,并将所有内容合并到一个 Google 电子表格中。

使用我们在第三章中介绍的查找和替换技巧,清理数据的得力工具 – 电子表格和文本编辑器(即去除#<>符号,并将行粘贴到单一表格中),我们最终得到一个干净的单一数据集,如下所示:

步骤三 – 数据列的最终清理

Scraper 是一个从网页中提取少量数据的好工具。它有一个方便的 Google 电子表格界面,如果你不想写程序来完成这个工作,它可以是一个快捷的解决方案。在下一部分,我们将处理一个更大的项目。这个项目可能足够复杂,需要我们将本章的一些概念融入到一个综合解决方案中。

示例项目 – 从电子邮件和网络论坛中提取数据

Django IRC 日志项目相当简单。它的设计目的是向你展示三种常用于从 HTML 页面中提取干净数据的技术之间的差异。我们提取的数据包括行号、用户名和 IRC 聊天消息,所有这些都很容易找到,几乎不需要额外的清理。在这个新的示例项目中,我们将考虑一个概念上类似的案例,但这将要求我们将数据提取的概念从 HTML 扩展到 Web 上的另外两种半结构化文本:托管在 Web 上的电子邮件消息和基于 Web 的讨论论坛。

项目的背景

最近,我在进行一项关于社交媒体如何用于提供软件技术支持的研究。具体来说,我在尝试发现,某些开发 API 和框架的软件开发组织是否应该将开发者的技术支持转移到 Stack Overflow,还是应该继续使用旧的媒体,如电子邮件和网络论坛。为了完成这项研究,我比较了(其中之一)开发者通过 Stack Overflow 获得 API 问题答案的时间与通过旧社交媒体(如网络论坛和电子邮件组)获得答案的时间。

在这个项目中,我们将处理这个问题的一个小部分。我们将下载两种表示旧社交媒体的原始数据:来自网络论坛的 HTML 文件和来自 Google Groups 的电子邮件消息。我们将编写 Python 代码来提取这些两个支持论坛中发送消息的日期和时间。然后我们将找出哪些消息是对其他消息的回复,并计算一些关于每条消息收到回复所花时间的基本统计数据。

提示

如果你想知道为什么我们在这个示例项目中没有提取 Stack Overflow 部分的数据,稍等一下,直到第九章,Stack Overflow 项目。整章内容将致力于创建和清理一个 Stack Overflow 数据库。

这个项目将分为两部分。在第一部分,我们将从 Google Groups 上托管的项目的电子邮件存档中提取数据;在第二部分,我们将从另一个项目的 HTML 文件中提取数据。

第一部分 – 清理 Google Groups 电子邮件中的数据

许多软件公司传统上使用电子邮件邮件列表或混合的电子邮件-网络论坛来为其产品提供技术支持。Google Groups 是这种服务的一个流行选择。用户可以通过电子邮件发送消息到小组,或者在 Web 浏览器中阅读和搜索消息。然而,一些公司已经不再通过 Google Groups 为开发者提供技术支持(包括 Google 自己的产品),而是转而使用 Stack Overflow。Google BigQuery 这样的数据库产品就是现在使用 Stack Overflow 的一个例子。

第一步 – 收集 Google Groups 消息

为了研究 BigQuery Google Group 中问题的响应时间,我首先创建了该组中所有帖子 URL 的列表。你可以在我的 GitHub 网站上找到我的完整 URL 列表:github.com/megansquire/stackpaper2015/blob/master/BigQueryGGurls.txt

一旦我们有了目标 URL 列表,就可以编写一个 Python 程序下载所有存储在这些 URL 中的电子邮件,并将它们保存到磁盘。在下面的程序中,我的 URL 列表已保存为名为 GGurls.txt 的文件。time 库已经包含,所以我们可以在请求 Google Groups 服务器之间使用短暂的 sleep() 方法:

import urllib2
import time

with open('GGurls.txt', 'r') as f:
    urls = []
    for url in f:
        urls.append(url.strip())

currentFileNum = 1
for url in urls:
    print("Downloading: {0} Number: {1}".format(url, currentFileNum))
    time.sleep(2)
    htmlFile = urllib2.urlopen(url)
    urlFile = open("msg%d.txt" %currentFileNum,'wb')
    urlFile.write(htmlFile.read())
    urlFile.close()
    currentFileNum = currentFileNum +1

该程序最终将 667 个文件写入磁盘。

第二步 - 从 Google Groups 消息中提取数据

现在我们有 667 封电子邮件消息存储在不同的文件中。我们的任务是编写一个程序,一次读取这些文件并使用本章中的某种技术提取我们需要的信息。如果我们查看其中一封电子邮件消息,我们会看到许多 头部,它们存储关于电子邮件的信息,或者说是其 元数据。我们可以迅速看到标识我们需要的元数据元素的三个头部:

In-Reply-To: <[email protected]>
Date: Mon, 30 Apr 2012 10:33:18 -0700
Message-ID: <CA+qSDkQ4JB+Cn7HNjmtLOqqkbJnyBu=Z1Ocs5-dTe5cN9UEPyA@mail.gmail.com>

所有消息都有 Message-IDDate,但是 In-Reply-To 头部只有在消息是对另一条消息的回复时才会出现。In-Reply-To 的值必须是另一条消息的 Message-ID 值。

以下代码展示了基于正则表达式的解决方案,用于提取 DateMessage-IDIn-Reply-To(如果有)值,并创建一些原始消息和回复消息的列表。然后,代码尝试计算消息与其回复之间的时间差:

import os
import re
import email.utils
import time
import datetime
import numpy

originals = {}
replies = {}
timelist = []

for filename in os.listdir(os.getcwd()):
    if filename.endswith(".txt"):
        f=open(filename, 'r')
        i=''
        m=''
        d=''
        for line in f:
            irt = re.search('(In\-Reply\-To: <)(.+?)@', line)    
            mid = re.search('(Message\-ID: <)(.+?)@', line)
            dt = re.search('(Date: )(.+?)\r', line)
            if irt: 
                i= irt.group(2) 
            if mid:
                m= mid.group(2)
            if dt:
                d= dt.group(2)
        f.close()
        if i and d:
            replies[i] = d
        if m and d:
            originals[m] = d

for (messageid, origdate) in originals.items():
    try:
        if replies[messageid]:
            replydate = replies[messageid]                
            try:
                parseddate = email.utils.parsedate(origdate)
                parsedreply = email.utils.parsedate(replydate)
            except:
                pass
            try:
                # this still creates some malformed (error) times
                timeddate = time.mktime(parseddate)
                timedreply = time.mktime(parsedreply)
            except:
                pass
            try:
                dtdate = datetime.datetime.fromtimestamp(timeddate)
                dtreply = datetime.datetime.fromtimestamp(timedreply)
            except:
                pass
            try:
                difference = dtreply - dtdate
                totalseconds = difference.total_seconds()
                timeinhours =  (difference.days*86400+difference.seconds)/3600
                # this is a hack to take care of negative times
                # I should probably handle this with timezones but alas
                if timeinhours > 1:
                    #print timeinhours
                    timelist.append(timeinhours)
            except:
                pass
    except:
        pass

print numpy.mean(timelist)
print numpy.std(timelist)
print numpy.median(timelist)

在这段代码中,初始的 for 循环遍历每一条消息并提取我们感兴趣的三项数据。(该程序不会将这些数据存储到单独的文件或磁盘上,但如果你需要,可以添加这个功能。)这部分代码还创建了两个重要的列表:

  • originals[] 是原始消息的列表。我们假设这些主要是成员提问的问题。

  • replies[] 是回复消息的列表。我们假设这些主要是回答其他消息中问题的回复。

第二个 for 循环处理原始消息列表中的每一条消息,执行以下操作:如果原始消息有回复,尝试计算该回复发送所花费的时间。然后我们会记录下回复时间的列表。

提取代码

对于本章,我们主要关注代码中的清理和提取部分,因此让我们仔细看看这些代码行。在这里,我们处理每一行电子邮件文件,寻找三个电子邮件头部:In-Reply-ToMessage-IDDate。我们使用正则表达式搜索和分组,就像我们在本章第一部分的方法中做的那样,来限定这些头部并轻松提取其后的值:

for line in f:
    irt = re.search('(In\-Reply\-To: <)(.+?)@', line) 
    mid = re.search('(Message\-ID: <)(.+?)@', line)
    dt = re.search('(Date: )(.+?)\r', line)
    if irt: 
        i = irt.group(2) 
    if mid:
        m = mid.group(2)
    if dt:
        d = dt.group(2)

为什么我们决定在这里使用正则表达式而不是基于树的解析器?主要有两个原因:

  1. 由于我们下载的电子邮件不是 HTML 格式,因此它们不能轻松地描述为具有父节点和子节点的树状结构。因此,基于解析树的解决方案(如 BeautifulSoup)不是最好的选择。

  2. 由于电子邮件头部是结构化且非常可预测的(尤其是我们在这里寻找的三个头部),因此使用正则表达式解决方案是可以接受的。

程序输出

该程序的输出是打印出三个数字,估算该 Google Group 上消息的回复时间的均值、标准差和中位数(单位:小时)。当我运行此代码时,我得到的结果如下:

178.911877395
876.102630872
18.0

这意味着,发布到 BigQuery Google Group 上的消息的中位响应时间大约为 18 小时。现在,让我们考虑如何从另一种来源提取类似的数据:网络论坛。你认为在网络论坛上回答问题的速度会更快、更慢,还是与 Google Group 相当?

第二部分 – 清理来自网络论坛的数据

我们将在这个项目中研究的网络论坛来自一家名为DocuSign的公司。他们也将开发者支持转移到了 Stack Overflow,但他们仍然保留着一个旧版的基于网页的开发者论坛存档。我在他们的网站上四处寻找,直到发现了如何下载那些旧论坛中的一些消息。这里展示的过程比 Google Groups 示例要复杂一些,但你将学到很多关于如何自动收集数据的知识。

步骤一 – 收集一些指向 HTML 文件的 RSS

DocuSign 开发者论坛上有成千上万的消息。我们希望能够获得所有这些消息或讨论帖子的 URL 列表,以便我们编写代码自动下载它们,并高效提取回复时间。

为此,首先我们需要获取所有讨论的 URL 列表。我发现 DocuSign 旧版 Dev-Zone 开发者网站的存档位于community.docusign.com/t5/DevZone-Archives-Read-Only/ct-p/dev_zone

该网站在浏览器中的显示如下:

步骤一 – 收集一些指向 HTML 文件的 RSS

我们绝对不希望点击进入每一个论坛,再进入每条消息并手动保存。这会花费很长时间,而且极其无聊。有没有更好的方法?

DocuSign网站的帮助页面表明,可以下载一个真正简单的聚合RSS)文件,显示每个论坛中最新的讨论和消息。我们可以使用这些 RSS 文件自动收集网站上许多讨论的 URL。我们感兴趣的 RSS 文件仅与开发者支持论坛相关(而不是公告或销售论坛)。这些 RSS 文件可以通过以下网址获得:

在您的浏览器中访问列表中的每个 URL(如果时间紧迫,可以只访问一个)。该文件是 RSS 格式,类似于带标签的半结构化文本,类似 HTML。将 RSS 保存为本地系统中的文件,并给每个文件加上.rss扩展名。完成此过程后,您最多应拥有七个 RSS 文件,每个文件对应前面展示的一个 URL。

每个 RSS 文件中都有描述论坛中所有讨论主题的元数据,其中包括我们在此阶段真正需要的数据:每个特定讨论主题的 URL。用文本编辑器打开其中一个 RSS 文件,您将能够看到我们感兴趣的 URL 示例。它看起来像这样,并且在文件中,您会看到每个讨论主题都有一个这样的 URL:

<guid>http://community.docusign.com/t5/Misc-Dev-Archive-READ-ONLY/Re-Custom-CheckBox-Tabs-not-marked-when-setting-value-to-quot-X/m-p/28884#M1674</guid>

现在,我们可以编写一个程序,循环遍历每个 RSS 文件,查找这些 URL,访问它们,然后提取我们感兴趣的回复时间。接下来的部分将这些步骤拆解成一系列更小的步骤,并展示一个完成整个任务的程序。

第二步 – 从 RSS 中提取 URL;收集并解析 HTML

在这一步,我们将编写一个程序,执行以下操作:

  1. 打开我们在第 1 步中保存的每个 RSS 文件。

  2. 每次看到<guid></guid>标签对时,提取其中的 URL 并将其添加到列表中。

  3. 对列表中的每个 URL,下载该位置上的 HTML 文件。

  4. 阅读该 HTML 文件,提取每条消息的原始发帖时间和回复时间。

  5. 计算发送回复所需的时间,并计算平均值、中位数和标准差,就像我们在第一部分做的那样。

以下是一些 Python 代码,用于处理所有这些步骤。我们将在代码列出的末尾详细介绍提取部分:

import os
import re
import urllib2
import datetime
import numpy

alllinks = []
timelist = []
for filename in os.listdir(os.getcwd()):
    if filename.endswith('.rss'):
        f = open(filename, 'r')
        linktext = ''
        linkurl = ''
        for line in f:
            # find the URLs for discussion threads
            linktext = re.search('(<guid>)(.+?)(<\/guid>)', line)    

            if linktext:
                linkurl= linktext.group(2)
                alllinks.append(linkurl)
        f.close()

mainmessage = ''
reply = ''
maindateobj = datetime.datetime.today()
replydateobj = datetime.datetime.today()
for item in alllinks:
    print "==="
    print "working on thread\n" + item
    response = urllib2.urlopen(item)
    html = response.read() 
    # this is the regex needed to match the timestamp 
    tuples = re.findall('lia-message-posted-on\">\s+<span class=\"local-date\">\\xe2\\x80\\x8e(.*?)<\/span>\s+<span class=\"local-time\">([\w:\sAM|PM]+)<\/span>', html)	
    mainmessage = tuples[0]
    if len(tuples) > 1:
        reply = tuples[1]
    if mainmessage:
        print "main: "
        maindateasstr = mainmessage[0] + " " + mainmessage[1]
        print maindateasstr
        maindateobj = datetime.datetime.strptime(maindateasstr, '%m-%d-%Y %I:%M %p')
    if reply:
        print "reply: "
        replydateasstr = reply[0] + " " + reply[1]
        print replydateasstr
        replydateobj = datetime.datetime.strptime(replydateasstr, '%m-%d-%Y %I:%M %p')

        # only calculate difference if there was a reply 
        difference = replydateobj - maindateobj
        totalseconds = difference.total_seconds()
        timeinhours =  (difference.days*86400+difference.seconds)/3600
        if timeinhours > 1:
            print timeinhours
            timelist.append(timeinhours)

print "when all is said and done, in hours:"
print numpy.mean(timelist)
print numpy.std(timelist)
print numpy.median(timelist)

程序状态

程序运行时,会打印出状态消息,以便我们知道它正在处理什么。每找到一个 RSS 源中的 URL,就会有一个这样的状态消息,内容如下:

===
working on thread
http://community.docusign.com/t5/Misc-Dev-Archive-READ-ONLY/Can-you-disable-the-Echosign-notification-in-Adobe-Reader/m-p/21473#M1156
main: 
06-21-2013 08:09 AM
reply: 
06-24-2013 10:34 AM
74

在此展示中,74 表示从线程中第一条消息的发布时间到第一条回复之间的小时数(大约三天,再加上两小时)。

程序输出

在程序结束时,它会打印出平均值、标准差和中位数回复时间(以小时为单位),就像第一部分程序为 Google Groups 所做的那样:

when all is said and done, in hours:
695.009009009
2506.66701108
20.0

看起来 DocuSign 论坛的回复时间比 Google Groups 略慢。它报告了 20 小时,而 Google Groups 是 18 小时,但至少两个数字在同一大致范围内。你的结果可能会有所不同,因为新消息一直在添加。

提取代码

由于我们主要关注数据提取,让我们仔细看看代码中发生这一过程的部分。以下是最相关的一行代码:

tuples = re.findall('lia-message-posted-on\">\s+<span class=\"local-date\">\\xe2\\x80\\x8e(.*?)<\/span>\s+<span class=\"local-time\">([\w:\sAM|PM]+)<\/span>', html)

就像我们之前的一些示例一样,这段代码也依赖于正则表达式来完成工作。然而,这个正则表达式相当混乱。也许我们应该用 BeautifulSoup 来写?让我们看一下我们试图匹配的原始 HTML,以便更好地理解这段代码的目的,以及是否应该采取不同的方式。以下是页面在浏览器中的截图,感兴趣的时间已在截图上做了标注:

提取代码

那么底层 HTML 是什么样的呢?这正是我们程序需要能够解析的部分。原始消息的日期在 HTML 页面中打印了多个地方,但日期和时间组合只打印了一次,原始消息和回复各一次。以下是 HTML 的展示,显示了这些内容是如何呈现的(HTML 已被压缩并去除换行,以便更易查看):

<p class="lia-message-dates lia-message-post-date lia-component-post-date-last-edited" class="lia-message-dates lia-message-post-date">
<span class="DateTime lia-message-posted-on lia-component-common-widget-date" class="DateTime lia-message-posted-on">
<span class="local-date">‎06-18-2013</span>
<span class="local-time">08:21 AM</span>

<p class="lia-message-dates lia-message-post-date lia-component-post-date-last-edited" class="lia-message-dates lia-message-post-date">
<span class="DateTime lia-message-posted-on lia-component-common-widget-date" class="DateTime lia-message-posted-on">
<span class="local-date">‎06-25-2013</span>
<span class="local-time">12:11 AM</span>

这实际上是一个正则表达式可以轻松解决的问题,因为我们可以编写一个正则表达式,找到两种类型的消息中的所有实例。在代码中,我们声明第一个找到的实例是原始消息,下一个实例是回复,代码如下:

mainmessage = tuples[0]
if len(tuples) > 1:
    reply = tuples[1]

我们本可以使用基于解析树的解决方案,如 BeautifulSoup,但我们需要处理这样一个事实:span类的值在两组日期中是相同的,甚至父元素(<p>标签)也有相同的类。因此,这个解析树比章节中第二种方法所展示的要复杂得多。

如果你真的想尝试使用 BeautifulSoup 进行这种提取,我的建议是首先使用浏览器的开发者工具查看页面结构。例如,在 Chrome 浏览器中,你可以选择你感兴趣的元素——在这个案例中是日期和时间——右键点击它,然后选择检查元素。这将打开一个开发者工具面板,显示该数据在整个文档树中的位置。在每个 HTML 元素左侧的小箭头指示是否存在子节点。然后,你可以决定如何通过程序化地定位目标元素在解析树中的位置,并制定区分它与其他节点的计划。由于这个任务超出了本书的范围,我将留给读者作为练习。

总结

在本章中,我们发现了几种经过验证的技术,用于将有趣的数据与不需要的数据分离。当我们在厨师的厨房里做高汤时,我们使用筛子来过滤掉我们不需要的骨头和蔬菜残渣,同时让我们想要的美味液体通过筛网的孔流入容器中。当我们在数据科学的厨房里从网页中提取数据时,采用的也是同样的思路。我们需要设计一个清理计划,使我们能够提取所需的数据,同时把剩下的 HTML 丢弃。

在过程中,我们回顾了提取 HTML 数据时使用的两种主要思维模型,即逐行分隔符方法和解析树/节点模型。接着,我们探讨了三种可靠且经过验证的 HTML 解析方法,用于提取我们想要的数据:正则表达式、BeautifulSoup 和基于 Chrome 的点击式网页抓取工具。最后,我们完成了一个项目,收集并提取了来自现实世界电子邮件和 HTML 页面的有用数据。

诸如电子邮件和 HTML 之类的文本数据证明并不难清理,但那二进制文件呢?在下一章中,我们将探讨如何从一个更加复杂的目标——PDF 文件中提取干净的数据。

第六章:清理 PDF 文件中的数据

在上一章中,我们发现了从我们想要的数据中分离不需要的数据的不同方法。我们将数据清理过程想象成做鸡汤,我们的目标是保留汤底,去掉骨头。但是,如果我们想要的数据和不需要的数据难以区分,应该怎么办呢?

想象一瓶陈年好酒,酒中有大量沉淀物。乍一看,我们可能看不见悬浮在液体中的沉淀物。但当酒液在醒酒器中放置一段时间后,沉淀物会沉到底部,我们就能倒出更加清澈、更具香气的酒液。在这种情况下,普通的过滤器无法将酒与沉淀物分开——需要一个专用工具。

在本章中,我们将尝试使用几种数据醒酒器来提取隐藏在难以理解的 PDF 文件中的所有有用内容。我们将探索以下主题:

  • PDF 文件的用途是什么,为什么从中提取数据很困难

  • 如何从 PDF 文件中复制和粘贴内容,以及当这不起作用时该怎么办

  • 如何通过只保存我们需要的页面来缩小 PDF 文件的大小

  • 如何使用名为pdfMiner的 Python 包中的工具从 PDF 文件中提取文本和数字

  • 如何使用一种名为Tabula的基于浏览器的 Java 应用程序从 PDF 文件中提取表格数据

  • 如何使用完整的、付费版的 Adobe Acrobat 提取数据表格

为什么清理 PDF 文件很困难?

便携式文档格式PDF)文件比我们之前在本书中查看的文本文件要复杂一些。PDF 是一种二进制格式,由 Adobe Systems 发明,后来发展成开放标准,以便多个应用程序能够创建文档的 PDF 版本。PDF 文件的目的是提供一种查看文档中文本和图形的方式,而不依赖于进行原始排版的软件。

在 1990 年代初期,桌面出版的全盛时期,每个图形设计软件包都有不同的专有格式,并且这些软件包非常昂贵。在那个时候,为了查看一个用 Word、Pagemaker 或 Quark 创建的文档,你必须使用创建该文档的相同软件打开它。这在 Web 初期尤为成问题,因为 HTML 中并没有很多技术可以创建复杂的布局,但人们还是想要相互分享文件。PDF 的设计目的是作为一种供应商中立的布局格式。Adobe 免费提供其 Acrobat Reader 软件,供任何人下载,随后 PDF 格式得到了广泛应用。

注意

这是关于 Acrobat Reader 早期的一些有趣事实。当你在 Google 搜索引擎中输入“click here”时,仍然会将Adobe 的 Acrobat PDF Reader 下载网站作为第一个结果,并且已经维持了多年。这是因为许多网站分发 PDF 文件时会附带类似“要查看此文件,您必须安装 Acrobat Reader。点击此处下载。”的信息。由于 Google 的搜索算法会利用链接文本来判断哪些网站与哪些关键词相关联,因此click here这一关键词现在与 Adobe Acrobat 的下载站点紧密相关。

PDF 仍然被用来制作供应商和应用程序中立的文件版本,这些文件的布局比纯文本能够实现的更为复杂。例如,在不同版本的 Microsoft Word 中查看同一文档时,包含大量嵌入表格、样式、图像、表单和字体的文档仍然可能会表现出不同的效果。这可能是由多个因素引起的,例如操作系统的差异或安装的 Word 软件版本不同。即使是旨在跨软件包或版本之间兼容的应用程序,细微的差异也可能导致不兼容问题。PDF 就是为了解决其中的一些问题而创建的。

我们可以立即看出,处理 PDF 文件要比文本文件更困难,因为它是二进制格式,而且还嵌入了字体、图像等。因此,我们可靠的数据清理工具箱中的大部分工具,比如文本编辑器和命令行工具(less),在处理 PDF 文件时基本无用。幸运的是,仍然有一些技巧可以帮助我们从 PDF 文件中提取数据。

首先尝试简单的解决方案——复制

假设在你准备倒出瓶中的美味红酒时,不小心把瓶子打翻了。你可能会认为这是一次彻底的灾难,认为需要更换整个地毯。但在你开始撕掉整个地板之前,或许值得尝试一个老酒吧招数:苏打水和湿布。在这一部分,我们概述了一些可以首先尝试的步骤,而不是直接投入昂贵的文件翻新项目。它们可能不起作用,但值得一试。

我们的实验文件

让我们通过使用一个实际的 PDF 文件来练习清理 PDF 数据。我们也不希望这个实验太简单,因此我们选择了一个非常复杂的文件。假设我们有兴趣从一个在 Pew Research Center 网站上找到的文件中提取数据,文件名为《Is College Worth It?》。这份 2011 年发布的 PDF 文件共有 159 页,包含了大量数据表格,展示了衡量美国大学教育是否值得投资的各种方法。我们希望找到一种方法,能够快速提取这些表格中的数据,以便进行进一步的统计分析。例如,下面是报告中某个表格的样子:

我们的实验文件

这个表格相当复杂。它只有六列和八行,但有几行占用了两行,并且标题行的文本仅在五列中显示。

提示

完整报告可以在 PewResearch 网站找到:www.pewsocialtrends.org/2011/05/15/is-college-worth-it/,我们使用的特定文件标注为《完整报告》:www.pewsocialtrends.org/files/2011/05/higher-ed-report.pdf

第一步——尝试复制我们需要的数据

我们将在这个示例中使用的数据位于 PDF 文件的第 149 页(在他们的文档中标记为第 143 页)。如果我们在 PDF 查看器中打开文件,例如 Mac OSX 上的预览,并尝试仅选择表格中的数据,我们已经看到一些奇怪的事情发生了。例如,即使我们不打算选择页面编号(143),它仍然被选中了。这对我们的实验来说不是个好兆头,但我们还是继续吧。使用 Command-C 复制数据,或者选择编辑 | 复制

第一步——尝试复制我们需要的数据

在预览中选择这个 PDF 中的文本时的显示效果

第二步——尝试将复制的数据粘贴到文本编辑器中

以下截图展示了复制的文本粘贴到我们的文本编辑器 Text Wrangler 中后的样子:

第二步——尝试将复制的数据粘贴到文本编辑器中

很明显,在复制并粘贴数据后,这些数据的顺序完全混乱。页面编号被包含在内,数字是横向排列的而非纵向,列标题也排列错乱。甚至有些数字被合并了;例如,最后一行包含了 4、4、3、2 这四个数字,但在粘贴后的版本中,这些数字变成了一个单一的数字4432。此时,手动清理这些数据可能需要比重新输入原始表格更长的时间。我们可以得出结论,在处理这个特定的 PDF 文件时,我们需要采取更强的措施来清理它。

提示

我们应当注意,此时 PDF 文件的其他部分清理得很好。例如,位于文件第 3 页的前言(纯文本部分),使用前述技巧复制出来也很顺利。在这个文件中,唯一的问题是实际的表格数据。你应该在决定提取技巧之前,先对 PDF 文件的所有部分——包括文本和表格数据——进行实验。

第三步——制作文件的简化版本

我们的复制粘贴操作没有成功,因此我们已经接受了需要采取更具侵入性措施的事实。也许如果我们不打算提取这个 PDF 文件的所有 159 页数据,我们可以仅识别出我们想要操作的 PDF 区域,并将该部分保存为一个单独的文件。

在 MacOSX 的 预览 中执行此操作,启动 文件 | 打印… 对话框。在 页面 区域,我们将输入实际想要复制的页面范围。为了进行这个实验,我们只对第 149 页感兴趣;因此,在 从:到: 框中都输入 149,如以下截图所示。

然后在底部的 PDF 下拉框中,选择 在预览中打开 PDF。您将看到您的单页 PDF 出现在新窗口中。从这里,我们可以将其保存为新文件并给它一个新名称,如 report149.pdf 或类似名称。

第三步 – 制作文件的较小版本

另一种可尝试的技术 – pdfMiner

现在我们有了一个较小的文件来进行实验,让我们尝试一些编程解决方案来提取文本,看看是否能有所改善。pdfMiner 是一个 Python 包,包含两个嵌入式工具,用于处理 PDF 文件。我们特别感兴趣的是尝试其中一个工具,一个名为 pdf2txt 的命令行程序,旨在从 PDF 文档中提取文本。也许它能够帮助我们正确提取文件中的数字表格。

第一步 – 安装 pdfMiner

启动 Canopy Python 环境。从 Canopy 终端窗口中,运行以下命令:

pip install pdfminer

这将安装整个 pdfMiner 包及其所有相关的命令行工具。

提示

pdfMiner 及其随附的两个工具 pdf2txtdumpPDF 的文档可以在 www.unixuser.org/~euske/python/pdfminer/ 查阅。

第二步 – 从 PDF 文件中提取文本

我们可以使用名为 pdf2txt.py 的命令行工具从 PDF 文件中提取所有文本。为此,请使用 Canopy 终端并导航到文件所在目录。命令的基本格式是 pdf2txt.py <filename>。如果您的文件较大并包含多页(或者如果您尚未将 PDF 拆分成较小的文件),您还可以运行 pdf2txt.py –p149 <filename> 来指定只提取第 149 页。

与前面的复制粘贴实验一样,我们不仅会尝试在第 149 页的表格上使用此技术,还会尝试在第 3 页的序言上。为了仅提取第 3 页的文本,我们运行以下命令:

pdf2txt.py –p3 pewReport.pdf

运行此命令后,Pew Research 报告的提取序言将出现在我们的命令行窗口中:

第二步 – 从 PDF 文件中提取文本

要将此文本保存为名为 pewPreface.txt 的文件,我们只需在命令行中添加重定向,如下所示:

pdf2txt.py –p3 pewReport.pdf > pewPreface.txt

那么位于第 149 页的那些棘手的数据表格呢?当我们对它们使用pdf2txt时会发生什么呢?我们可以运行以下命令:

pdf2txt.py pewReport149.pdf

结果比复制粘贴稍微好一些,但差别不大。实际的数据输出部分如下截图所示。列头和数据混在一起,来自不同列的数据顺序错乱。

步骤二——从 PDF 文件中提取文本

我们必须宣告此次表格数据提取实验失败,尽管 pdfMiner 在逐行文本提取方面表现得相当不错。

注意

请记住,使用这些工具的成功率可能因人而异,很多时候取决于原始 PDF 文件的特定特性。

看起来我们为这个示例选择了一个非常棘手的 PDF 文件,但我们不必灰心丧气。相反,我们将转向另一个工具,看看它的表现如何。

第三种选择——Tabula

Tabula是一个基于 Java 的程序,用于提取 PDF 文件中表格内的数据。我们将下载 Tabula 软件,并让它在我们第 149 页的棘手表格上发挥作用。

步骤一——下载 Tabula

Tabula 可以从其官方网站下载:tabula.technology/。该网站提供了一些简单的下载说明。

提示

在 Mac OSX 10.10.1 版本上,我必须先下载旧版的 Java 6 应用程序,才能运行 Tabula。整个过程非常简单,只需按照屏幕上的说明操作即可。

步骤二——运行 Tabula

从下载的.zip压缩文件中启动 Tabula。在 Mac 系统上,Tabula 应用程序文件简单地被称为Tabula.app。如果你愿意,可以将其复制到Applications文件夹中。

当 Tabula 启动时,它会在默认的网页浏览器中打开一个标签页或窗口,地址是http://127.0.0.1:8080/。屏幕上的初始操作部分如下所示:

步骤二——运行 Tabula

自动检测表格需要较长时间,这个警告是对的。对于包含三个表格的单页perResearch149.pdf文件,表格自动检测花费了整整两分钟,并且出现了一个关于 PDF 文件格式不正确的错误信息。

步骤三——指示 Tabula 提取数据

一旦 Tabula 读取了文件,就该指示它所在的位置在哪里了。使用鼠标光标,选择你感兴趣的表格。我在第一个表格的四周画了一个框。

Tabula 花了大约 30 秒钟读取表格,结果如下所示:

步骤三——指示 Tabula 提取数据

与通过复制粘贴和pdf2txt读取数据的方式相比,这些数据看起来非常不错。但是,如果你对 Tabula 读取表格的方式不满意,你可以通过清除选择并重新绘制矩形来重复此过程。

步骤四——提取数据

我们可以使用 Tabula 中的下载数据按钮将数据保存为更友好的文件格式,如 CSV 或 TSV。正如我们在前几章所学,这些格式如果需要,可以在电子表格或文本编辑器中清理。恰到好处,我们准备进行下一步了。

第五步——更多清理

在 Excel 或文本编辑器中打开 CSV 文件并查看它。在这个阶段,我们在提取 PDF 数据时已经遇到了很多失败,所以现在很容易就想放弃。然而,如果你已经读到数据清理的书籍中的这一部分,你可能会猜到这些数据还能进一步清理。这里有一些我们从前几章学到的简单数据清理任务:

  1. 我们可以将所有两行文本单元格合并成一个单元格。例如,在B列,许多短语占据了不止一行。让学生具备高效工作能力成为劳动力成员应该放在一个单元格中,作为一个完整的短语。1行和2行的表头也是如此(4-yearPrivate应该放在一个单元格中)。要在 Excel 中清理这些内容,可以在B列和C列之间创建一个新列。使用concatenate()函数将 B3:B4、B5:B6 等进行合并。使用粘贴特殊将新合并的值添加到新列中。然后删除不再需要的两列。对1行和2行也执行相同的操作。

  2. 删除行之间的空白行。

    当这些操作完成后,数据看起来是这样的:

    第五步——更多清理

提示

与直接剪切粘贴数据或运行简单命令行工具相比,Tabula 可能看起来要花费更多精力。这是真的,除非你的 PDF 文件像这个一样挑剔。记住,专业工具之所以存在是有原因的——但除非真的需要,否则不要使用它们。首先使用简单的解决方案,只有在真正需要时才使用更复杂的工具。

当一切都失败时——第四种技术

Adobe Systems 销售的付费商业版 Acrobat 软件比起仅仅允许你阅读 PDF 文件,还具有一些附加功能。使用完整版的 Acrobat,你可以创建复杂的 PDF 文件,并以各种方式操作现有文件。与此相关的一个功能是 Acrobat 中的导出选定内容为...选项。

要开始使用此功能,请启动 Acrobat,并使用文件打开对话框打开 PDF 文件。在文件内,导航到包含要导出的数据的表格。以下截图展示了如何选择我们操作过的 149 页 PDF 中的数据。使用鼠标选择数据,然后右键点击并选择导出选定内容为...

当一切都失败时——第四种技术

到此为止,Acrobat 会询问你想如何导出数据。CSV 是其中的一个选择。如果你确定不想在文本编辑器中编辑文件,Excel 工作簿(.xlsx)也是一个不错的选择。由于我知道 Excel 也能打开 CSV 文件,我决定将文件保存为这种格式,以便在 Excel 和文本编辑器之间具有更大的灵活性。

当一切都失败时——第四种方法

在选择文件格式后,我们会被提示输入文件名和保存位置。当我们在文本编辑器或 Excel 中打开生成的文件时,会发现它看起来和我们在前一节看到的 Tabula 版本很像。以下是 CSV 文件在 Excel 中打开后的样子:

当一切都失败时——第四种方法

到这个阶段,我们可以使用与 Tabula 数据清洗时相同的清理程序,合并 B2:B3 单元格到一个单元格中,然后删除空行。

摘要

本章的目标是学习如何从 PDF 文件中导出数据。就像美酒中的沉淀物,PDF 文件中的数据起初看起来非常难以分离。然而,与倒酒这一非常被动的过程不同,分离 PDF 数据需要大量的反复试探。我们学会了四种处理 PDF 文件清洗数据的方法:复制粘贴、pdfMiner、Tabula 和 Acrobat 导出。每种工具都有其优缺点:

  • 复制粘贴不需要任何成本,且几乎不需要工作,但对于复杂的表格来说,效果并不理想。

  • pdfMiner/Pdf2txt 也是免费的,作为一个命令行工具,它可以被自动化处理,且能处理大量数据。但和复制粘贴一样,它也容易被某些类型的表格所混淆。

  • Tabula 的设置工作量较大,而且由于它是一个正在开发中的产品,有时会出现奇怪的警告。它的处理速度也比其他选项慢一些。然而,它的输出非常干净,即便是复杂的表格。

  • Acrobat 的输出与 Tabula 类似,但几乎不需要设置,也几乎不费力气。它是一个付费产品。

到最后,我们得到了一个干净的数据集,准备进行分析或长期存储。

在下一章,我们将重点讨论已存储在关系型数据库管理系统RDBMS)中的数据。我们将学习如何清洗这种方式存储的数据,了解一些常见的数据异常以及如何修复它们。

第七章:RDBMS 清理技术

家用冰箱通常配备架子,大多数还配有一两个蔬菜抽屉。但是,如果你曾经参观过家居整理商店或与专业的整理师交流过,你会发现还有许多额外的储存选项,包括蛋托、奶酪盒、饮料罐分配器、酒瓶架、剩菜标签系统以及各种尺寸的堆叠式、彩色编码的收纳盒。但我们真的需要这些额外的东西吗?要回答这个问题,你可以问自己以下几个问题:我常用的食物是否容易找到?食物是否占用了不应有的空间?剩菜是否清楚标注了内容和制作时间?如果我们的答案是,整理专家表示,容器和标签可以帮助我们优化存储、减少浪费,并让生活更轻松。

这与我们的关系型数据库管理系统RDBMS)是一样的。作为经典的长期数据存储解决方案,RDBMS 是现代数据科学工具包的标准部分。然而,我们常常犯的一个错误是,仅仅将数据存入数据库,却很少考虑细节。在本章中,我们将学习如何设计一个超越两层架子和一个抽屉的 RDBMS。我们将学习一些技术,确保我们的 RDBMS 能够优化存储、减少浪费,并使我们的生活更轻松。具体来说,我们将:

  • 学习如何发现我们 RDBMS 数据中的异常

  • 学习几种策略来清理不同类型的问题数据

  • 学习何时以及如何为清理过的数据创建新表,包括创建子表和查找表

  • 学习如何记录你所做更改的规则

准备工作

在本章的示例中,我们将使用一个流行的数据集——Sentiment140。该数据集的创建旨在帮助学习 Twitter 消息中的正面和负面情绪。我们在本书中并不专注于情感分析,但我们将使用这个数据集来练习在数据已导入关系型数据库后进行数据清理。

要开始使用 Sentiment140 数据集,你需要设置好 MySQL 服务器,和之前的 Enron 示例一样。

第一步——下载并检查 Sentiment140 数据集

我们想使用的 Sentiment140 数据版本是来自 Sentiment140 项目的原始文件集,直接可以从help.sentiment140.com/for-students获取。这份包含推文及其积极与消极情感(或情绪,评分为 0、2 或 4)的 ZIP 文件由斯坦福大学的研究生创建。自从这份文件公开发布后,其他网站也将原始的 Sentiment140 文件添加到其平台,并将其作为更大推文集合的一部分公开。对于本章内容,我们将使用原始的 Sentiment140 文本文件,可以通过前面提到的链接或直接访问cs.stanford.edu/people/alecmgo/trainingandtestdata.zip来获取。

下载 ZIP 文件,解压缩并使用文本编辑器查看其中的两个 CSV 文件。你会立刻注意到,一个文件比另一个文件的行数多得多,但这两个文件的列数是相同的。数据是逗号分隔的,并且每一列都被双引号括起来。每一列的描述可以在前一部分链接的for-students页面中找到。

第二步 – 清理以便数据库导入

对于我们的目的——学习如何清理数据——将这些文件中较小的一个加载到单个 MySQL 数据库表中就足够了。我们所需要做的所有学习,都可以通过较小的文件来完成,这个文件叫做testdata.manual.2009.06.14.csv

在查看数据时,我们可能会注意到一些地方,如果我们直接将此文件导入 MySQL,可能会出现问题。其中一个问题出现在文件的第 28 行:

"4","46","Thu May 14 02:58:07 UTC 2009","""booz allen""", 

你看到在booz关键字前和allen一词后有三重引号"""吗?同样的问题出现在第 41 行,在歌曲标题P.Y.T周围有双引号:

"4","131","Sun May 17 15:05:03 UTC 2009","Danny Gokey","VickyTigger","I'm listening to ""P.Y.T"" by Danny Gokey…"

这些额外的引号问题在于 MySQL 导入程序会使用引号来分隔列文本。这将导致错误,因为 MySQL 会认为这一行的列数比实际的多。

为了解决这个问题,在文本编辑器中,我们可以使用查找和替换功能,将所有的"""替换为"(双引号),并将所有的""替换为'(单引号)。

提示

这些""可能也可以完全移除,对这个清理工作几乎没有负面影响。为此,我们只需要搜索""并将其替换为空。但如果你希望尽量接近推文的原始意图,使用单引号(甚至像这样转义的双引号\")作为替代字符是一个安全的选择。

将这个清理过的文件保存为新文件名,比如cleanedTestData.csv。现在我们准备将它导入到 MySQL 中。

第三步 – 将数据导入到 MySQL 的单一表中

为了将我们稍微清理过的数据文件加载到 MySQL 中,我们需要回顾一下第三章中导入电子表格数据到 MySQL部分的 CSV 到 SQL 技术:

  1. 从命令行,导航到保存你在第二步中创建的文件的目录。这就是我们将要导入到 MySQL 中的文件。

  2. 然后,启动你的 MySQL 客户端,并连接到你的数据库服务器:

    user@machine:~/sentiment140$ mysql -hlocalhost -umsquire -p
    Enter password:
    
  3. 输入你的密码,登录后,在 MySQL 中创建一个数据库来存储表格,方法如下:

    mysql> CREATE DATABASE sentiment140;
    mysql> USE sentiment140;
    
  4. 接下来,我们需要创建一个表格来存储数据。每一列的数据类型和长度应该尽可能匹配我们所拥有的数据。某些列将是 varchar 类型的,每列都需要指定长度。由于我们可能不知道这些长度应该是多少,我们可以使用清理工具来确定一个合适的范围。

  5. 如果我们在 Excel 中打开 CSV 文件(Google 电子表格同样可以很好地完成这项工作),我们可以运行一些简单的函数来找到某些文本字段的最大长度。例如,len()函数可以给出文本字符串的字符长度,max()函数则能告诉我们某个范围中的最大值。打开 CSV 文件后,我们可以应用这些函数来查看 MySQL 中 varchar 列的长度应是多少。

    以下截图展示了一种使用函数来解决这个问题的方法。它展示了length()函数应用于列G,并且max()函数应用于列H,但作用于列G

    步骤三——将数据导入 MySQL 中的单一表格

    GH展示了如何在 Excel 中获取文本列的长度,然后获取最大值。

  6. 为了更快速地计算这些最大长度,我们还可以使用 Excel 的快捷方式。以下数组公式可以快速将文本列的最大值和长度合并到一个单元格中——只需确保在输入此嵌套函数后按Ctrl + Shift + Enter,而不是仅按Enter

    =max(len(f1:f498))
    

    这个嵌套函数可以应用于任何文本列,以获取该列中文本的最大长度,它只使用一个单元格来完成这一操作,而不需要任何中间的长度计算。

在我们运行这些函数之后,结果显示我们任何一个推文的最大长度是 144 个字符。

检测和清理异常

你可能会好奇,为什么这个数据集中一条推文的长度会是 144 个字符,而 Twitter 限制所有推文的最大长度为 140 个字符。结果发现,在 sentiment140 数据集中,&字符有时被翻译成 HTML 等效代码&amp,但并不是每次都这样。有时也使用了其他 HTML 代码,例如,<字符变成了&lt;>变成了&gt;。所以,对于一些非常长的推文,增加的几个字符很容易使这条推文超过 140 个字符的长度限制。我们知道,这些 HTML 编码的字符并不是原始用户推文的内容,而且我们发现这些情况并不是每次都会发生,因此我们称之为数据异常

要清理这些数据,我们有两种选择。我们可以选择直接将脏数据导入数据库并尝试在那里清理,或者先在 Excel 或文本编辑器中清理。为了展示这两种方法的不同,我们在这里会同时演示这两种做法。首先,我们将在电子表格或文本编辑器中使用查找和替换功能,尝试将下表中显示的字符转换。我们可以将 CSV 文件导入 Excel,看看能在 Excel 中清理多少:

HTML 代码 替换为 实例计数 用来查找计数的 Excel 函数
&lt; < 6 =COUNTIF(F1:F498,"*&lt*")
&gt; > 5 =COUNTIF(F1:F498,"*&gt*")
&amp; & 24 =COUNTIF(F1:F498,"*&amp*")

前两个字符替换在 Excel 中的查找和替换功能中运行正常。&lt;&gt;这些 HTML 编码字符已被替换。看看文本像这样:

I'm listening to 'P.Y.T' by Danny Gokey &lt;3 &lt;3 &lt;3 Aww, he's so amazing. I &lt;3 him so much :)

上述内容会变成如下文本:

I'm listening to 'P.Y.T' by Danny Gokey <3 <3 <3 Aww, he's so amazing. I <3 him so much :)

然而,当我们尝试在 Excel 中查找&amp;并将其替换为&时,可能会遇到一个错误,如下所示:

检测和清理异常

一些操作系统和 Excel 版本存在我们选择&字符作为替代符时的一个问题。如果遇到这个错误,我们可以采取几种不同的方法:

  • 我们可以使用自己喜欢的搜索引擎,尝试找到一个 Excel 解决方案来修复这个错误。

  • 我们可以将 CSV 文本数据移到文本编辑器中,并在那里执行查找和替换功能。

  • 我们也可以继续将数据导入数据库,即使其中包含了奇怪的&amp;字符,然后再尝试在数据库中清理这些数据。

通常,我会倾向于不将脏数据导入数据库,除非可以在数据库外清理干净。然而,既然这章是关于在数据库内清理数据的,那我们就将半清理的数据导入数据库,等数据进入表格后,再清理&amp;问题。

创建我们的表格

为了将我们半清理的数据导入数据库,我们首先需要编写CREATE语句,然后在 MySQL 数据库中运行它。CREATE语句如下所示:

mysql> CREATE TABLE sentiment140 (
    ->   polarity enum('0','2','4') DEFAULT NULL,
    ->   id int(11) PRIMARY KEY,
    ->   date_of_tweet varchar(28) DEFAULT NULL,
    ->   query_phrase varchar(10) DEFAULT NULL,
    ->   user varchar(10) DEFAULT NULL,
    ->   tweet_text varchar(144) DEFAULT NULL
    -> ) ENGINE=MyISAM DEFAULT CHARSET=utf8;

注意

该语句使用简单快速的 MyISAM 引擎,因为我们预计不会需要任何 InnoDB 功能,例如行级锁定或事务处理。关于 MyISAM 与 InnoDB 的区别,这里有一个关于何时使用每种存储引擎的讨论:stackoverflow.com/questions/20148/myisam-versus-innodb

你可能会注意到,代码仍然要求tweet_text列的长度为 144。这是因为我们无法清理包含&amp;代码的这些列。然而,这对我影响不大,因为我知道 varchar 列不会使用额外的空间,除非它们需要。毕竟,这就是它们被称为 varchar(可变字符)列的原因。但是,如果这个额外的长度真的让你困扰,你可以稍后修改表格,只保留该列的 140 个字符。

接下来,我们将使用 MySQL 命令行从以下位置运行导入语句:

mysql> LOAD DATA LOCAL INFILE 'cleanedTestData.csv'
    ->   INTO TABLE sentiment140
    ->   FIELDS TERMINATED BY ',' ENCLOSED BY '"' ESCAPED BY '\'
    ->   (polarity, id, date_of_tweet, query_phrase, user, tweet_text);

该命令将从我们清理过的 CSV 文件中加载数据到我们创建的新表中。成功消息将显示如下,表明所有 498 行数据已经成功加载到表中:

Query OK, 498 rows affected (0.00 sec)
Records: 498  Deleted: 0  Skipped: 0  Warnings: 0

提示

如果你可以访问浏览器界面的工具,如 phpMyAdmin(或者桌面应用程序如 MySQL Workbench 或 Toad for MySQL),所有这些 SQL 命令都可以在这些工具中轻松完成,而无需在命令行中输入。例如,在 phpMyAdmin 中,你可以使用导入标签并在那里上传 CSV 文件。只要确保数据文件按照第二步 – 为数据库导入清理中的步骤进行清理,否则你可能会遇到文件中列数过多的错误。这个错误是由于引号问题导致的。

第四步 – 清理&amp;字符

在最后一步,我们决定暂时不清理&amp;字符,因为 Excel 在处理时给出了一个奇怪的错误。现在我们已经完成了第三步 – 将数据导入到 MySQL 的单一表格,并且数据已经导入到 MySQL 中,我们可以非常轻松地使用UPDATE语句和replace()字符串函数来清理数据。以下是需要的 SQL 查询,用来将所有出现的&amp;替换为&

UPDATE sentiment140 SET tweet_text = replace(tweet_text,'&amp;', '&');

replace()函数的工作方式就像在 Excel 或文本编辑器中的查找和替换一样。我们可以看到,推文 ID 594 曾经显示为#at&amp;t is complete fail,现在变成了#at&t is complete fail

第五步 – 清理其他神秘字符

当我们浏览tweet_text列时,可能会注意到一些奇怪的推文,例如推文 ID 613 和 2086:

613, Talk is Cheap: Bing that, I?ll stick with Google
2086, Stanford University?s Facebook Profile

?字符是我们应该关注的重点。和我们之前看到的 HTML 编码字符一样,这个字符问题也很可能是字符集转换中的一个副作用。在这种情况下,原始推文中可能有某种高 ASCII 或 Unicode 的撇号(有时称为智能引号),但当数据转换为低级字符集,如纯 ASCII 时,那个特定的撇号就被简单地更改为?

根据我们打算如何处理这些数据,我们可能不希望省略?字符。例如,如果我们进行词频统计或文本挖掘,可能非常重要的是将I?ll转换为I'll,并将University?s转换为University's。如果我们决定这很重要,那么我们的任务就是检测到发生错误的推文,然后制定策略将问号转换回单引号。当然,诀窍在于,我们不能仅仅把tweet_text列中的每个问号都替换成单引号,因为有些推文中的问号是应该保留的。

为了定位问题字符,我们可以运行一些 SQL 查询,尝试使用正则表达式来查找问题。我们关注的是出现在奇怪位置的问号,例如紧跟其后的是字母字符。以下是使用 MySQL REGEXP 功能的初步正则表达式。运行此查询将大致告诉我们问题问号可能所在的位置:

SELECT id, tweet_text
FROM sentiment140
WHERE tweet_text
REGEXP '\\?[[:alpha:]]+';

这个 SQL 正则表达式查找紧跟一个或多个字母字符的问号字符。SQL 查询返回了六行结果,其中四行结果的问号是异常的,另外两行是假阳性。假阳性是指匹配了我们模式但实际上不应更改的推文。两个假阳性是推文 ID 为2342204的推文,它们包含的问号是合法的 URL 的一部分。推文1392246132086真阳性,也就是说,这些推文被正确地检测为异常,需要进行修改。所有结果如下图所示,来自 phpMyAdmin 的截图:

步骤五 – 清理其他神秘字符

不过,139号推文有点奇怪。它在Obama这个词前面有一个问号,就像是在引用某篇新闻文章的标题,但在字符串末尾没有匹配的引号(或者是丢失的引号)。这应该是某个其他字符吗?这实际上也可能是一个假阳性,或者至少它的阳性不足以让我们真正去修复它。在仔细检查推文时,224号推文也在一个看起来不该出现问号的地方多了一个奇怪的问号。

如果我们要编写一个replace()函数,将问题问号替换为单引号,我们将需要编写一个正则表达式,仅匹配真正的问题,并且不匹配任何误报。然而,由于这个数据集很小,且只有四个真正的问题——如果我们认为139不需要清理的话,就是三个——那么我们完全可以手动清理这些问题。特别是因为我们对于其他可能存在的问题(例如推文224中的额外问号)还有一些疑问。

在这种情况下,由于我们只有三行问题数据,直接对数据运行三个小的UPDATE命令会比尝试构建完美的正则表达式更快捷。以下是处理推文224(仅第一个问题)、6132086的 SQL 查询:

UPDATE sentiment140 SET tweet_text = 'Life''s a bitch? and so is Dick Cheney. #p2 #bipart #tlot #tcot #hhrs #GOP #DNC http://is.gd/DjyQ' WHERE id = 224;

UPDATE sentiment140 SET tweet_text = 'Talk is Cheap: Bing that, I''ll stick with Google. http://bit.ly/XC3C8' WHERE id = 613;

UPDATE sentiment140 SET tweet_text = 'Stanford University''s Facebook Profile is One of the Most Popular Official University Pages - http://tinyurl.com/p5b3fl' WHERE id = 2086;

注意

请注意,在这些更新语句中,我们必须对单引号进行转义。在 MySQL 中,转义字符可以是反斜杠或单引号本身。这些示例中使用了单引号作为转义字符。

第六步——清理日期

如果我们查看date_of_tweet列,会发现我们将其创建为一个简单的可变字符字段,varchar(30)。那有什么问题呢?好吧,假设我们想按时间顺序排列这些推文。现在,我们不能使用简单的 SQL ORDER BY语句来获取正确的日期顺序,因为我们得到的将是字母顺序。所有星期五都会排在任何星期一之前,五月总是在六月之后。我们可以用以下 SQL 查询来测试这一点:

SELECT id, date_of_tweet
FROM sentiment140
ORDER BY date_of_tweet;

最初的几行是按顺序排列的,但在第 28 行附近,我们开始看到问题:

2018  Fri May 15 06:45:54 UTC 2009
2544  Mon Jun 08 00:01:27 UTC 2009
…
3  Mon May 11 03:17:40 UTC 2009

5 月 11 日并不在5 月 15 日6 月 8 日之后。为了解决这个问题,我们需要创建一个新列,清理这些日期字符串,并将它们转换为合适的 MySQL 日期时间数据类型。我们在第二章的数据类型转换部分中学到,MySQL 在日期和时间作为原生datetimedatetime类型存储时效果最好。插入 datetime 类型的格式如下:YYYY-MM-DD HH:MM:SS。但我们在date_of_tweet列中的数据并不是这种格式。

MySQL 有许多内置函数可以帮助我们将杂乱的日期字符串格式化为首选格式。通过这样做,我们可以利用 MySQL 在日期和时间上的数学运算能力,例如,找出两个日期或时间之间的差异,或者按日期或时间正确地排序项目。

为了将我们的字符串转换为 MySQL 友好的日期时间类型,我们将执行以下操作:

  1. 修改表格,增加一个新列,用于存储新的日期时间信息。我们可以将这个新列命名为date_of_tweet_newdate_clean,或者其他一个清晰区分于原date_of_tweet列的名称。执行此任务的 SQL 查询如下:

    ALTER TABLE sentiment140
    ADD date_clean DATETIME NULL
    AFTER date_of_tweet;
    
  2. 对每一行执行更新操作,将旧的日期字符串格式化为正确格式的日期时间类型,而不是字符串,并将新值添加到新创建的date_clean列中。执行此任务的 SQL 语句如下:

    UPDATE sentiment140
    SET date_clean = str_to_date(date_of_tweet, '%a %b %d %H:%i:%s UTC %Y');
    

此时,我们已经有了一个新列,里面填充了清理后的日期时间。回想一下,原来的date_of_tweet列有问题,因为它没有正确地对日期进行排序。为了测试日期是否现在已经正确排序,我们可以按新列的顺序选择数据:

SELECT id, date_of_tweet 
FROM sentiment140
ORDER BY date_clean;

我们看到现在的行已经完美排序,5 月 11 日的日期排在最前面,且没有日期错乱。

我们是否应该删除旧的date列?这由你决定。如果你担心可能犯了错误,或者因为某些原因你可能需要原始数据,那么就保留它。但如果你觉得可以删除,直接删除该列,如下所示:

ALTER TABLE sentiment140 
DROP date_of_tweet;

你也可以创建一个 Sentiment140 表的副本,里面包含原始列作为备份。

第七步 – 分离用户提及、话题标签和 URL

目前这个数据的另一个问题是,tweet_text列中隐藏了很多有趣的信息,例如,考虑一个人使用@符号将推文指向另一个人的情况。这叫做 Twitter 上的提及。统计一个人被提及的次数,或者统计他们与特定关键词一起被提及的次数,可能会很有趣。另一个隐藏在部分推文中的有趣数据是话题标签;例如,ID 为 2165 的推文使用了#jobs#sittercity话题标签讨论工作和保姆的概念。

这条推文还包含了一个外部的非 TwitterURL。我们可以提取每个提及、话题标签和 URL,并将它们单独保存到数据库中。

这个任务与我们清理日期时的操作类似,但有一个重要的区别。在日期的情况下,我们只有一个可能的修正版本,因此只需添加一个新列来存储清理后的日期版本。然而,对于提及、话题标签和 URL,我们在单个tweet_text值中可能会有零个或多个,例如我们之前查看的推文(ID 2165)包含了两个话题标签,这条推文(ID 2223)也是如此:

HTML 5 Demos! Lots of great stuff to come! Yes, I'm excited. :) http://htmlfive.appspot.com #io2009 #googleio

这条推文没有提及、一个 URL 和两个话题标签。推文 ID 为 13078 的推文包含了三个提及,但没有话题标签或 URL:

Monday already. Iran may implode. Kitchen is a disaster. @annagoss seems happy. @sebulous had a nice weekend and @goldpanda is great. whoop.

我们需要更改数据库结构,以便存储这些新的信息——话题标签、URLs 和用户提及——同时要记住,一条推文中可能包含许多这样的内容。

创建一些新表

根据关系数据库理论,我们应避免创建用于存储多值属性的列。例如,如果一条推文有三个话题标签,我们不应该将这三个话题标签都存入同一列。对我们来说,这条规则意味着我们不能直接复制用于日期清理问题的ALTER过程。

相反,我们需要创建三个新表:sentiment140_mentionssentiment140_urlssentiment140_hashtags。每个新表的主键将是一个合成 ID 列,每个表将包括另外两个列:tweet_id,它将该新表与原始sentiment140表联系起来,以及实际提取的标签、提及或 URL 文本。以下是创建这些表的三个CREATE语句:

CREATE TABLE IF NOT EXISTS sentiment140_mentions (
  id int(11) NOT NULL AUTO_INCREMENT,
  tweet_id int(11) NOT NULL,
  mention varchar(144) NOT NULL,
  PRIMARY KEY (id)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS sentiment140_hashtags (
  id int(11) NOT NULL AUTO_INCREMENT,
  tweet_id int(11) NOT NULL,
  hashtag varchar(144) NOT NULL,
  PRIMARY KEY (id)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS sentiment140_urls (
  id int(11) NOT NULL AUTO_INCREMENT,
  tweet_id int(11) NOT NULL,
  url varchar(144) NOT NULL,
  PRIMARY KEY (id)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

注意

这些表不使用外键回到原始的sentiment140推文表。如果您想添加这些外键,这是完全可能的。但为了学习如何清理这个数据集,我们在此并不需要外键。

现在我们已经创建了表格,是时候将我们从tweet_text column中仔细提取的数据填充到这些表格中了。我们将分别处理每个提取的案例,从用户提及开始。

提取用户提及

为了设计一个能处理用户提及提取的程序,我们首先回顾一下我们已知的推文中关于提及的内容:

  • 用户提及总是以@符号开始

  • 用户提及是紧跟在@符号后的单词

  • 如果@后面有空格,则不是用户提及

  • 用户提及本身内部没有空格

  • 由于电子邮件地址也使用@符号,我们应该注意这一点

使用这些规则,我们可以构造一些有效的用户提及:

  • @foo

  • @foobar1

  • @1foobar

我们可以构造一些无效的用户提及示例:

  • @ foo(@后面的空格使其无效)

  • [email protected](bar.com 未被识别)

  • @foo bar(只会识别@foo)

  • @foo.bar(只会识别@foo)

注意

在这个例子中,我们假设我们不关心常规的@mention.@mention(有时称为点提及)之间的区别。这些是推文中在@`符号前有一个句点的推文,目的是将推文推送到所有用户的粉丝。

由于这个规则集比我们能在 SQL 中高效执行的要复杂,因此更倾向于编写一个简单的小脚本,利用正则表达式来清理这些推文。我们可以用任何能连接到数据库的语言来编写这种类型的脚本,比如 Python 或 PHP。由于我们在第二章中使用了 PHP 连接数据库,基础知识 - 格式、类型和编码,我们在这里也使用一个简单的 PHP 脚本。这个脚本连接到数据库,搜索 tweet_text 列中的用户提及,并将找到的提及移动到新的 sentiment140_mentions 表中:

<?php
// connect to db
$dbc = mysqli_connect('localhost', 'username', 'password', 'sentiment140')
    or die('Error connecting to database!' . mysqli_error());
$dbc->set_charset("utf8");

// pull out the tweets
$select_query = "SELECT id, tweet_text FROM sentiment140";
$select_result = mysqli_query($dbc, $select_query);

// die if the query failed
if (!$select_result)
    die ("SELECT failed! [$select_query]" .  mysqli_error());

// pull out the mentions, if any
$mentions = array();
while($row = mysqli_fetch_array($select_result))
{
    if (preg_match_all(
        "/(?<!\pL)@(\pL+)/iu",
        $row["tweet_text"],
        $mentions
    ))
    { 
        foreach ($mentions[0] as $name)
        {
            $insert_query = "INSERT into sentiment140_mentions (id, tweet_id, mention) VALUES (NULL," . $row["id"] . ",'$name')";
            echo "<br />$insert_query";
            $insert_result = mysqli_query($dbc, $insert_query);
            // die if the query failed
            if (!$insert_result)
                die ("INSERT failed! [$insert_query]" .  mysqli_error());
        }
    }
}
?>

在对 sentiment140 表运行这个小脚本之后,我们发现从原始的 498 条推文中提取了 124 个独特的用户提及。这个脚本的几个有趣之处包括,它可以处理用户名中的 Unicode 字符,即使这个数据集中没有这些字符。我们可以通过快速插入一行测试数据到 sentiment140 表的末尾来进行测试,例如:

INSERT INTO sentiment140 (id, tweet_text) VALUES(99999, "This is a @тест");

然后,再次运行脚本;你会看到在 sentiment140_mentions 表中添加了一行,并成功提取了 @тест 的 Unicode 用户提及。在下一节中,我们将构建一个类似的脚本来提取标签。

提取标签

标签有其自身的规则,这些规则与用户提及略有不同。以下是一些我们可以用来判断是否为标签的规则:

  • 标签以 # 符号开头

  • 标签是紧跟在 # 符号后面的单词

  • 标签可以包含下划线,但不能有空格和其他标点符号

用于提取标签的 PHP 代码与用户提及的代码几乎完全相同,唯一不同的是代码中间的正则表达式。我们只需将 $mentions 变量改为 $hashtags,然后调整正则表达式如下:

if (preg_match_all(
        "/(#\pL+)/iu",
        $row["tweet_text"],
        $hashtags
    ))

这个正则表达式表示我们对匹配大小写不敏感的 Unicode 字母字符感兴趣。然后,我们需要将 INSERT 行改为使用正确的表和列名,如下所示:

$insert_query = "INSERT INTO sentiment140_hashtags (id, tweet_id, hashtag) VALUES (NULL," . $row["id"] . ",'$name')";

当我们成功运行这个脚本时,我们看到 54 个标签已被添加到 sentiment140_hashtags 表中。更多的推文中包含了多个标签,甚至比包含多个用户提及的推文还多。例如,我们可以立即看到推文 174 和 224 都包含了多个嵌入的标签。

接下来,我们将使用这个相同的骨架脚本,并再次修改它来提取 URLs。

提取 URLs

从文本中提取 URL 可以像寻找任何以http://https://开头的字符串一样简单,或者根据文本中包含的 URL 类型的不同,可能会变得更为复杂。例如,有些字符串可能包括file:// URL 或者磁力链接(如磁力链接),或者其他类型的特殊链接。在我们的 Twitter 数据中,情况相对简单,因为数据集中包含的所有 URL 都以 HTTP 开头。所以,我们可以偷懒,设计一个简单的正则表达式来提取任何以 http:// 或 https:// 开头的字符串。这个正则表达式看起来就是这样:

if (preg_match_all(
        "!https?://\S+!",
        $row["tweet_text"],
        $urls
    ))

然而,如果我们在喜欢的搜索引擎上稍作搜索,实际上我们可以轻松找到一些相当印象深刻且实用的通用 URL 匹配模式,这些模式可以处理更复杂的链接格式。这样做的好处在于,如果我们编写的 URL 提取程序能够处理这些更复杂的情况,那么即使未来我们的数据发生变化,它依然能够正常工作。

一个非常详细的 URL 匹配模式文档给出了daringfireball.net/2010/07/improved_regex_for_matching_urls网站。以下代码展示了如何修改我们的 PHP 代码,以便在 Sentiment140 数据集中使用该模式进行 URL 提取:

<?php
// connect to db
$dbc = mysqli_connect('localhost', 'username', 'password', 'sentiment140')
    or die('Error connecting to database!' . mysqli_error());
$dbc->set_charset("utf8");

// pull out the tweets
$select_query = "SELECT id, tweet_text FROM sentiment140";
$select_result = mysqli_query($dbc, $select_query);

// die if the query failed
if (!$select_result)
    die ("SELECT failed! [$select_query]" .  mysqli_error());

// pull out the URLS, if any
$urls = array();
$pattern  = '#\b(([\w-]+://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))#';

while($row = mysqli_fetch_array($select_result))
{
    echo "<br/>working on tweet id: " . $row["id"];
    if (preg_match_all(
        $pattern,
        $row["tweet_text"],
        $urls
    ))
    {
        foreach ($urls[0] as $name)
        {
            echo "<br/>----url: ".$name;
            $insert_query = "INSERT into sentiment140_urls (id, tweet_id, url)
                VALUES (NULL," . $row["id"] . ",'$name')";
            echo "<br />$insert_query";
            $insert_result = mysqli_query($dbc, $insert_query);
            // die if the query failed
            if (!$insert_result)
                die ("INSERT failed! [$insert_query]" .mysqli_error());
        }
    }
}
?>

这个程序几乎与我们之前编写的提及提取程序相同,只有两个不同点。首先,我们将正则表达式模式存储在一个名为 $pattern 的变量中,因为它较长且复杂。其次,我们对数据库的 INSERT 命令做了小的修改,就像我们在话题标签提取时做的那样。

正则表达式模式的逐行解释可以在原始网站上找到,但简短的解释是,所示的模式将匹配任何 URL 协议,如 http:// 或 file://,它还尝试匹配有效的域名模式以及几级深度的目录/文件模式。如果你想查看它匹配的多种模式以及一些肯定不会匹配的已知模式,源网站也提供了自己的测试数据集。

第八步——清理查找表

第七步——分离用户提及、话题标签和 URL部分,我们创建了新的表格来存储提取的标签、用户提及和 URL,然后提供了一种方法,通过id列将每一行与原始表格关联起来。我们按照数据库规范化的规则,通过创建新的表格来表示推文与用户提及、推文与话题标签、推文与 URL 之间的多对一关系。在这一步中,我们将继续优化此表格的性能和效率。

我们现在关心的列是query_phrase列。查看该列数据,我们可以看到同样的短语反复出现。这些显然是最初用于定位和选择现在存在于数据集中的推文的搜索短语。在sentiment140表中的 498 条推文中,查询短语有多少次被反复使用?我们可以通过以下 SQL 来检测这一点:

SELECT count(DISTINCT query_phrase)
FROM sentiment140;

查询结果显示,只有 80 个不同的查询短语,但它们在 498 行数据中反复出现。

这在 498 行数据的表中可能看起来不算问题,但如果我们有一个非常大的表,比如包含数亿行的表,我们就需要关注这个列的两个问题。首先,重复这些字符串占用了数据库中不必要的空间;其次,查找不同的字符串值会非常慢。

为了解决这个问题,我们将创建一个查找表来存储查询值。每个查询字符串只会在这个新表中出现一次,我们还会为每个查询字符串创建一个 ID 号。接下来,我们将修改原始表,使用这些新的数字值,而不是目前使用的字符串值。我们的操作流程如下:

  1. 创建一个新的查找表:

    CREATE TABLE sentiment140_queries (
      query_id int(11) NOT NULL AUTO_INCREMENT,
      query_phrase varchar(25) NOT NULL,
      PRIMARY KEY (query_id)
    ) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;
    
  2. 用不同的查询短语填充查找表,并自动为每个短语分配一个query_id编号:

    INSERT INTO sentiment140_queries (query_phrase)
    SELECT DISTINCT query_phrase FROM sentiment140;
    
  3. 在原始表中创建一个新列,用于存储查询短语编号:

    ALTER TABLE sentiment140
    ADD query_id INT NOT NULL AFTER query_phrase;
    
  4. 在下一步操作出错的情况下,备份sentiment140表。每次对表执行UPDATE操作时,最好先进行备份。我们可以使用像 phpMyAdmin 这样的工具轻松复制表(使用操作标签)。或者,我们可以重新创建一份表的副本,并将原始表中的行导入到副本中,如下所示的 SQL:

    CREATE TABLE sentiment140_backup(
      polarity int(1) DEFAULT NULL,
      id int(5)NOT NULL,
      date_of_tweet varchar(30) CHARACTER SET utf8 DEFAULT NULL ,
      date_clean datetime DEFAULT NULL COMMENT 'holds clean, formatted date_of_tweet',
      query_id int(11) NOT  NULL,
      user varchar(25) CHARACTER SET utf8 DEFAULT NULL,
      tweet_text varchar(144) CHARACTER SET utf8 DEFAULT NULL ,
      PRIMARY KEY (id)) ENGINE=MyISAM DEFAULT CHARSET=utf8;
    
    SET SQL_MODE='NO_AUTO_VALUE_ON_ZERO';
    INSERT INTO sentiment140_backup SELECT * FROM sentiment140;
    
  5. 用正确的数字填充新列。为此,我们通过它们的文本列将两个表连接起来,然后从查找表中查找正确的数字值,并将其插入到sentiment140表中。在以下查询中,每个表都被赋予了别名ssq

    UPDATE sentiment140 s
    INNER JOIN sentiment140_queries sq
    ON s.query_phrase = sq.query_phrase
    SET s.query_id = sq.query_id;
    
  6. 删除sentiment140表中的旧query_phrase列:

    ALTER TABLE sentiment140
    DROP query_phrase;
    

到目前为止,我们已经找到了一种有效的方法来创建短语列表,具体如下。这些短语按字母顺序排列:

SELECT query_phrase
FROM sentiment140_queries
ORDER BY 1;

我们还可以通过对这两个表进行连接,查找包含给定短语(如baseball)的推文:

SELECT s.id, s.tweet_text, sq.query_phrase
FROM sentiment140 s
INNER JOIN sentiment140_queries sq
  ON s.query_id = sq.query_id
WHERE sq.query_phrase = 'baseball';

到此为止,我们已经清理了sentiment140表,并创建了四个新表来存储各种提取和清理后的值,包括话题标签、用户提及、网址和查询短语。我们的tweet_textdate_clean列已经清理干净,并且我们已经有了一个查询短语的查找表。

第九步 – 记录你所做的工作

由于有九个清理步骤,并且使用了多种语言和工具,毫无疑问我们在某个环节会犯错并需要重复某个步骤。如果我们需要向别人描述我们做了什么,我们几乎肯定会记不清楚确切的步骤以及每个步骤背后的原因。

为了避免过程中出现错误,我们必须保持清理步骤的日志记录。至少,日志应按照执行顺序包含以下内容:

  • 每个 SQL 语句

  • 每个 Excel 函数或文本编辑器的操作流程,包括必要时的截图

  • 每个脚本

  • 每一步的备注和评论,解释为什么做每一件事

另一个优秀的建议是,在每个阶段都创建表的备份,例如,我们在对sentiment140表执行UPDATE操作之前就创建了备份,我们还讨论了在创建新的date_clean列之后进行备份。备份操作很简单,如果以后决定不需要备份表,可以随时删除它。

总结

在这一章中,我们使用了一个示例数据集——一个名为 Sentiment140 的推文集合,学习如何在关系数据库管理系统中清理和处理数据。我们在 Excel 中进行了几项基础清理操作,然后我们回顾了如何将数据从 CSV 文件导入到数据库中。在此之后,剩余的清理操作是在 RDBMS 内部进行的。我们学习了如何将字符串转换为正确的日期格式,然后我们提取了推文文本中的三种数据,并最终将这些提取的值移至新的干净表格中。接下来,我们学习了如何创建一个查找表,存储当前效率低下的数值,这样我们就能用高效的数字查找值更新原表。最后,由于我们执行了很多步骤,并且总有可能出现错误或关于我们所做操作的沟通不清晰,我们回顾了一些记录清理过程的策略。

在下一章中,我们将改变视角,专注于为他人准备已清理的数据,而不是清理我们已经得到的数据。我们将学习一些最佳实践,创建需要他人最少清理的 数据集。

第八章 最佳实践:共享你的清洗数据

到目前为止,在本书中,我们已经学习了许多不同的方法来清洗和组织数据集。也许现在是时候考虑让别人使用我们清洗过的数据了。本章的目标是提供一些最佳实践,以便邀请朋友进入你的数据科学厨房。共享数据可能意味着将其提供给其他人、其他团队,甚至是你未来的某个版本。那么,如何将数据包装好,以供他人使用?你应该如何告诉别人你已经清洗好的数据?如何确保你所有的辛勤工作都归功于你?

在本章中,我们将学习:

  • 如何展示和包装你的清洗数据

  • 如何为你的数据提供清晰的文档说明

  • 如何通过授权你的清洗数据来保护并扩展你的辛勤工作

  • 如何找到并评估公开清洗数据的选项

在开始本章之前,我们应该明确指出,我们只能清洗并共享我们有权共享的数据。也许这听起来很显而易见,但值得重复一遍。本章假设你所清洗并随后共享的数据,实际上是你有权以这种方式处理的数据。如果你对此有疑问,请阅读本章中的设置数据的条款和许可部分,确保你遵循你希望你的用户遵循的相同准则。

准备一个清晰的数据包

在本节中,我们深入探讨了在发布数据包供大众使用之前需要回答的许多重要问题。

你希望人们如何访问你的数据?如果数据在数据库中,你希望用户能够登录并执行 SQL 命令吗?还是希望创建可以下载的纯文本文件供他们使用?你是否需要为数据创建 API?反正你到底有多少数据,是否希望对数据的不同部分设置不同的访问权限?

如何分享清洗后的数据的技术方面非常重要。一般来说,最好从简单的方式开始,必要时再采用更复杂的分发计划。以下是一些数据分发选项,按从最简单到最复杂的顺序排列。当然,随着复杂度的增加,也会带来更大的好处:

  • 压缩纯文本 – 这是一种非常低风险的分发方式。正如我们在第二章中学到的,基础知识 - 格式、类型和编码,纯文本可以被压缩成非常小的文件大小。一个简单的 CSV 或 JSON 文件是通用的,可以轻松转换为许多其他格式。需要考虑的事项包括:

    • 你将如何让用户下载文件?在网页上提供开放链接非常简单且方便,但它不允许你要求凭证(如用户名和密码)来访问文件。如果这对你很重要,那么你需要考虑其他分发文件的方法;例如,通过使用带有用户名和密码的 FTP 服务器,或通过使用你 web 服务器的访问控制。

    • 你的文件有多大?你预期的流量有多少?你的托管服务商允许多少流量,超出部分会开始收费?

  • 压缩 SQL 文件 — 分发 SQL 文件可以让你的用户在自己的系统上重建你的数据库结构和数据。需要考虑的一些因素包括:

    • 你的用户可能使用的数据库系统与你不同,因此他们无论如何都需要清理数据。直接给他们纯文本可能更高效。

    • 你的数据库系统可能与他们的服务器设置不同,因此你需要提前澄清这些自定义设置。

    • 你还需要提前规划是否你的数据集设计为随着时间增长,例如,决定你是否只提供 UPDATE 语句,还是始终提供足够的 CREATEINSERT 语句来重新创建整个数据库。

  • 实时数据库访问 — 提供实时访问你的数据库是一种很好的方式,可以让用户在较低层次上与数据互动。需要考虑的一些因素包括:

    • 提供实时访问要求你为每个用户设置单独的用户名和密码,这意味着需要跟踪用户。

    • 因为你有可识别的用户,你需要提供一种方式来与他们沟通支持问题,包括丢失凭证以及如何使用系统。

    • 除非你已经为数据库构建了一个安全的前端,并采取了基本的预防措施,比如限制用户执行查询的次数和查询执行的时间,否则允许通用用户名和密码可能不是一个好主意。一个不当的 OUTER JOIN 查询,尤其是对一个有几千 TB 数据的表,可能会使你的数据库崩溃,影响其他用户。

    • 你是否希望用户能够构建访问你数据的程序,例如,通过 ODBC 或 JDBC 中间件层?如果是这样,你需要在规划访问权限和配置服务器时考虑到这一点。

  • API — 设计一个应用程序编程接口API)来访问你的数据,将允许你的最终用户编写自己的程序,能够以可预测的方式访问数据并获取结果集。API 的优点是,它将在互联网上提供以已知的、受限的方式访问数据的途径,并且用户无需解析数据文件或处理将数据从一种格式转换到另一种格式的任务。需要考虑的一些因素包括:

    • 构建一个良好的 API 在前期投入上比其他选项更昂贵;然而,如果你有很多需要支持的用户且支持人员有限,那么从长远来看,构建 API 可能会帮你节省资金。

    • 使用 API 要求用户具备比其他方法更高的技术知识。你应该准备好详细的文档,并附带示例。

    • 你需要有一个认证和安全计划来追踪谁被允许访问你的数据以及他们可以对数据进行哪些操作。如果你计划设立多个访问层级,例如,将数据的不同层次进行货币化,那么诸如用户从一个层次转到另一个层次等问题需要提前进行清晰的规划。

    • 就像常规的数据库访问一样,用户滥用或误用 API 是始终可能发生的情况。你需要提前规划并采取预防措施,发现并移除那些可能通过故意或无意的误用,使得服务无法访问的恶意或不小心的用户。

选择分发方法会受到你的预算(包括资金和时间)以及用户期望的影响。我能给出的最好建议是:我在遵循开源软件座右铭“尽早发布,经常发布”时,取得了不错的成果。这对我有用,因为我有一个小的用户群,有限的预算,并且没有太多闲暇时间来应对那些可能有效也可能无效的复杂打包计划。

提醒一句——使用 GitHub 分发数据

GitHub 是一个基于云的文件存储库,旨在帮助软件开发人员协作开发软件并托管他们的代码以供他人下载。它的受欢迎程度急剧上升,目前托管了超过 1600 万个项目仓库。正因为如此,我与许多数据科学家交流时,他们都建议将数据存储在 GitHub 上。

不幸的是,GitHub 在存储非代码数据方面存在一些局限性,尽管它在技术人员中非常流行且易于使用,但你仍应注意其一些政策,这些政策可能会影响你的数据。相关政策已在帮助指南中列出,链接为help.github.com/articles/what-is-my-disk-quota,但我们已在这里总结了其中的重要内容:

  • 首先,GitHub 是对源代码控制系统 Git 的封装,而该系统并不适合存储 SQL。帮助指南中写道,“大型 SQL 文件与 Git 等版本控制系统兼容性差。”我不确定“兼容性差”具体指什么,但我确信在用户体验至关重要的情况下,我一定要避免遇到这种问题。

  • 其次,GitHub 对文件大小有一些严格的限制。每个项目(仓库)限制为 1GB,每个文件限制为 100MB。我发布的大多数数据文件作为个人文件时都小于这个限制,但由于我每年多次发布许多时间序列文件,我不得不为它们创建多个仓库。在这种方案下,每次我发布新文件时,都需要评估是否会超出文件大小限制。这很快就变成了一个大麻烦。

简而言之,GitHub 本身推荐使用一种网络托管解决方案来分发文件,特别是当文件较大或面向数据库时。如果你决定在 GitHub 上托管,务必小心发布包含用户凭据的文件。这包括发布你数据库系统的用户名和密码、Twitter 的认证密钥和秘密,或其他任何个人信息。由于 GitHub 本质上是一个 Git 仓库,这些错误将永远存在,除非整个仓库被删除。如果你发现自己犯了错误,将个人信息发布到了 GitHub,必须立即取消当前账户的认证,并重新创建所有密钥和密码。

文档化你的数据

一旦人们获得了数据,理想情况下,即使是在之前,他们也需要了解自己所得到的是什么。文档化数据可能对你来说只是事后想起来的事情,但对于你的用户来说极其重要,因为他们对数据和你做的所有处理并不熟悉。在本节中,我们将回顾一些可以添加到数据包中的内容,以帮助理解数据。

README 文件

简单的 README 文件在计算机领域有着悠久的历史。它只是一个与软件包一起分发的文本文件,或者位于包含其他文件的目录中,目的是让用户在开始使用其余的软件包或文件之前,先阅读 README 文件。README 文件会告诉用户有关软件包的重要信息,如作者和创作原因、安装说明、已知的错误和使用文件的其他基本说明。

如果你正在构建数据包,例如,包含文本或 SQL 文件的压缩文件,快速且简单的方法是在压缩前为文件包添加一个 README 文件。如果你正在为文件创建一个网站或在线目录,在显眼的位置添加一个 README 文件会非常有帮助。以下截图展示了我用来分发我参与的项目 FLOSSmole 文件的一个网站目录。我添加了一个 README 目录,以便将所有需要用户首先阅读的文件放入其中。我将该目录名以一个下划线开头,这样它将始终按字母顺序排在列表的顶部:

README 文件

网站上显示 README 文件的文件目录,README 文件位于顶部。

README.txt文件中,我给用户提供了关于文件的通用和具体说明。以下是我为这个目录中的数据提供的README文件示例:

README for http://flossdata.syr.edu/data directory 

What is this place?
This is a repository of flat files or data "dumps", from the FLOSSmole project.

What is FLOSSmole? 
Since 2004, FLOSSmole aims to:
    --freely provide data about free, libre, and open source software (FLOSS) projects in multiple formats for anyone to download;
    --integrate donated data & scripts from other research teams;
    --provide a community for researchers to discuss public data about FLOSS development.

FLOSSmole contains: Several terabytes (TB) of data covering the period 2004-now, and growing with data sets from nearly 10,000 web-based collection operations, and growing each month. This includes data for millions of open source projects and their developers.

If you use FLOSSmole data, please cite it accordingly:
Howison, J., Conklin, M., & Crowston, K. (2006). FLOSSmole: A collaborative repository for FLOSS research data and analyses. International Journal of Information Technology and Web Engineering, 1(3), 17–26.

What is included on this site? 
Flat files, date and time-stamped, from various software forges & projects. We have a lot of other data in our database that is not available here in flat files. For example, IRC logs and email from various projects. For those, see the following:

1\. Direct database access. Please use this link for direct access to our MySQL database: http://flossmole.org/content/direct-db-access-flossmole-collection-available

2\. FLOSSmole web site. Includes updates, visualizations, and examples. http://flossmole.org/

这个示例README文件适用于整个目录的文件,但你也可以为每个文件或不同的目录准备一个README文件。这完全取决于你。

文件头部

另一种有效的向用户传达信息的方式,特别是当你创建的是平面文本文件或 SQL 命令时,是在每个文件的顶部放置一个头部,解释其格式和使用方式。一种常见做法是为每一行头部加上某种类似注释的字符,如#//

文件头部中常包含的一些内容有:

  • 文件名和文件所在的包的名称

  • 参与创建此文件的人员姓名、他们的组织和所在地

  • 文件发布的日期

  • 文件的版本号,或者在哪里可以找到该文件的早期版本

  • 文件的目的

  • 数据的来源地,以及数据自那时以来所做的任何更改

  • 文件的格式及其组织方式,例如,列出字段及其含义

  • 文件的使用条款或许可

以下示例显示了我某个数据项目中分发的 TSV 文件的头部示例。在其中,我解释了数据的内容以及如何解释文件中的每一列。我还解释了如何引用数据以及如何分享数据的政策。我们将在本章后面讨论授权和分享的选项:

# Author: Squire, M. & Gazda, R.
# License: Open Database License 1.0
# This data 2012LTinsultsLKML.tsv.txt is made available under the 
# Open Database License: http://opendatacommons.org/licenses/ 
# odbl/1.0/. 
#
# filename: 2012LTinsultsLKML.tsv.txt
# explanation: This data set is part of a larger group of data 
# sets described in the paper below, and hosted on the 
# FLOSSmole.org site. Contains insults gleaned from messages sent 
# to the LKML mailing list by Linus Torvalds during the year 2012
#
# explanation of fields:
# date: this is the date the original email was sent
# markmail permalink: this is a permalink to the email on markmail 
# (for easy reading)
# type: this is our code for what type of insult this is
# mail excerpt: this is the fragment of the email containing the 
# insult(s). Ellipses (...) have been added where necessary.
#
# Please cite the paper and FLOSSmole as follows:
#
# Squire, M. & Gazda, R. (2015). FLOSS as a source for profanity 
# and insults: Collecting the data. In Proceedings of 48th 
# Hawai'i International Conference on System Sciences (HICSS-48).
# IEEE. Hawaii, USA. 5290-5298
#
# Howison, J., Conklin, M., & Crowston, K. (2006). FLOSSmole: A 
# collaborative repository for FLOSS research data and analyses. 
# International Journal of Information Technology and Web 
# Engineering, 1(3), 17–26.

如果你预期用户将定期收集你的数据文件,你应该在文件头部使用一致的注释字符。在前面的示例中,我使用了#字符。这样做的原因是,用户可能会编写程序来自动下载和解析你的数据,或许将其加载到数据库中,或在程序中使用。你一致使用注释字符将使用户能够跳过头部内容,不进行处理。

数据模型和图表

如果你分发的是用于构建数据库的 SQL 文件,或者你提供了用于查询的数据库的实时访问,可能会发现一个可视化图表,如实体-关系图ERD),将极大地帮助用户。

在我的一些项目中,我喜欢同时提供表格的文字描述,例如前面描述的头部和README文件,同时也提供表格及其之间关系的可视化图表。因为我分发的数据库非常庞大,我还会对我的图表进行着色,并注释图表的每个部分,以指示该部分数据库中的内容。

以下截图展示了我的一个大型图表的高层次概览。它被缩小,以显示实体-关系图(ERD)的大小:

数据模型和图表

由于这个 ERD(实体关系图)有点复杂且难以阅读,即使是在大屏幕上,我已将数据库的每个独立部分进行了着色,并在需要的地方提供了注释。下面是从大图的左上角放大后的橙色部分截图:

数据模型和图表

数据库某一部分的特写图,包括描述表格目的的注释。

通过查看这个图表,用户能够清晰地了解数据库不同部分是如何组合在一起的。重要的是,高层次的注释直接显示在图表上,当用户需要关于某个特定字段的详细信息时,他们可以参考 README 文件或该文件中的头部部分。

要创建 ERD,你可以使用任何数据库管理系统(RDBMS)工具,包括 MySQL Workbench(这是我用来创建你所看到的着色版本的工具)。其他流行的工具包括 Microsoft Visio、Sparx Enterprise Architect 和 draw.io。许多这些工具允许你连接到 RDBMS 并从现有数据库逆向生成图表,或者从图纸中正向生成 SQL 语句。在任何一种情况下,ERD 都能帮助你的用户更好地理解数据模型。

文档维基或内容管理系统(CMS)

另一种组织项目文档的方法是将其发布到维基或内容管理系统CMS)中。有数百种 CMS 和维基软件可供选择,常见的选项包括 MediaWiki、WordPress、Joomla!和 Drupal。GitHub 也为托管在其上的项目提供了维基服务,其他一些软件托管服务,如 Sourceforge 和 Bitbucket,也提供类似的服务。

你可以使用 CMS 或维基提供文件的下载链接,并可以通过 CMS 发布文档和解释。我在自己的工作中也使用了 CMS 来托管更新日志、展示示例图表的可视化内容、使用数据构建的图表,以及一个供用户使用的脚本库。

以下是大多数面向数据的项目在文档 CMS 或维基中包括的一些常见部分:

  • 关于项目 — 这一部分告诉用户数据项目的目的以及如何联系项目负责人。此部分还可能包括如何参与、如何加入邮件列表或讨论区,或如何联系项目负责人等信息。

  • 获取数据 — 这一部分解释了访问数据的不同方式。常见的选择包括直接数据库访问、文件下载或 API。这一部分还解释了任何特殊的注册或登录程序。

  • 使用数据 — 这包括启动查询、使用示例、基于数据构建的图形、图表和实体关系图(ERD)。它提供了其他人使用这些数据所做的事情的链接。本节还会再次说明,如果有必要的话,您对数据引用的期望和任何许可政策。

在本节中,我们讨论了记录数据的各种方式,包括 README 文件、文件头、实体关系图(ERD)和基于 Web 的解决方案。在这个讨论中,我们提到了许可数据的概念,并说明了您对数据引用和共享的期望。在下一节中,我们将深入探讨许可数据集的具体内容。

设置数据的条款和许可

数据分发计划的重要组成部分是确定您希望用户如何引用、共享或重混您的数据的期望。您希望用户使用数据的期望清单被称为数据的使用条款ToU)。使用条款还可能赋予用户某些特定权利,例如修改或再分发数据的能力。您授予用户的这些权利集合被称为数据许可。用户可以根据是否同意遵守使用条款来选择是否使用您的数据。用户也可以根据他们想要对数据进行的操作是否被许可来决定是否使用数据。

在本节中,我们将概述一些您可以做出的选择,以设定用户与您的数据互动的期望。我们还将回顾一些您可能希望在使用条款中包含的常见事项,以及一些可以应用于您的数据集的常见预设许可。

常见的使用条款

不是每个人在共享数据时都有相同的目标。例如,我参与的一个项目的具体目标是为科学界收集、清理并重新分发数据。因为我是大学教授,我的部分工作责任是发表对他人有用的学术研究论文、软件和数据集。因此,对我来说,确保人们在使用我的论文和发布的数据集时引用它们非常重要。然而,我的另一位朋友并非学术界人士,他经常匿名发布数据集,并且不要求在使用这些数据时进行引用或通知。

下面是设置数据使用期望时的一些常见考虑事项:

  • 引用 — 您是否希望基于您的数据发布内容的人明确说明他们获得数据的来源?如果是,应该使用什么网址或引用?

  • 隐私——你是否有关于保护用户或其信息隐私的规则?你是否希望用户遵守任何特定的隐私指南或研究指南?例如,有些人要求用户遵循类似于他们自己所遵循的机构研究委员会IRB)或其他研究伦理团体(例如,互联网研究者协会AOIR))的程序。

  • 数据的适当使用——你是否怀疑你的数据集可能以某种方式被滥用?数据是否可能被断章取义?它的内容是否可能与其他数据集结合,从而造成伤害?对于某些项目,设定用户如何使用你提供的数据的期望将是一个非常好的主意。

  • 联系方式——你是否有特定的方式希望数据用户在使用数据时通知你?他们是否需要通知你?如果你预期用户可能对数据有疑问或关切,提供关于如何以及为什么联系你(作为数据集提供者)的指南会很有帮助。

正如我们在本章的文档化数据部分讨论的那样,数据集的使用条款(ToU)可以通过 README 文件、文件头或网站提供给潜在用户。如果你提供实时数据库访问,你也可以通知潜在用户,在接受数据库系统的用户名和密码时,他们即表示同意遵守你的条款。对于 API 访问,也可以使用类似的结构,用户通过主动使用身份验证令牌或访问凭证来表示同意你的 ToU。

当然,所有这些最佳实践都受到各个国际国家和组织的法律和政策的约束。如果没有一点帮助,尝试做到这一点可能非常复杂。为了帮助数据提供者设定用户期望,随着时间的推移,已经出现了一些通用的许可方案。我们现在将讨论其中的两种:

创意共享

创意共享CC)许可证是预先打包的通用规则集,版权或可版权材料的提供者可以将其应用于他们的作品。这些许可证规定了作品使用者可以做什么。通过提前声明许可证,作品所有者可以避免需要单独授予每个希望修改或重新分发特定作品的人的许可证。

关于 CC 许可证的问题——这可能对你来说不是问题,取决于你打算如何使用它——是 CC 许可证旨在应用于具有版权的作品。你的数据库或数据集可否享有版权?你是否打算授权数据库的内容,还是整个数据库?为了帮助你回答这个问题,我们将引导你访问 Creative Commons 的 Wiki,这里比我们在此能做的更详细地解答了这个问题。该页面甚至有一个专门关于数据和数据库的常见问题部分:wiki.creativecommons.org/Data

ODbL 和开放数据共同体

另一个不错的数据授权选择是开放数据库许可证ODbL)。这是专为数据库设计的许可证。开放知识基金会OKF)创建了一个两分钟的指南,帮助你决定如何开放数据,你可以在这里找到:OpenDataCommons.org/guide/

如果你需要更多的选择,OpenDefinition.org 网站,作为 OKF 的一部分,提供了更多预打包许可证,你可以将它们应用于你的数据集。这些许可证涵盖从非常开放的公共领域式许可证,到要求署名和共享衍生作品的许可证。此外,他们还提供了一本《开放数据手册》,这本手册在帮助你理清数据库或数据集中的知识产权问题,以及你希望如何处理这些数据方面非常有帮助。你可以在这里下载或在线浏览《开放数据手册》:OpenDataHandbook.org

宣传你的数据

一旦你拥有了完整的数据包,就该向全世界展示它了。宣传你的数据将确保尽可能多的人使用它。如果你已经有了特定的用户群体,宣传它可能只需要在邮件列表或特定的研究小组中发送一个 URL。但有时候,我们会创建一个数据集,认为它可能对更大、更不明确的群体感兴趣。

数据集列表

网络上有许多数据集列表,大多数是围绕某种主题组织的。这些元集合(集合的集合)的发布者通常非常乐意列出适合其细分领域的新数据来源。元集合的主题可以包括:

  • 与相同主题相关的数据集,例如,音乐数据、生物数据或关于新闻报道的文章集合

  • 解决同一类问题相关的数据集,例如,能够用于开发推荐系统或训练机器学习分类器的数据集

  • 与特定技术问题相关的数据集,例如,旨在基准测试或测试特定软件或硬件设计的数据集

  • 针对特定系统使用而设计的数据集,例如,针对学习编程语言(如 R)、数据可视化服务(如 Tableau)或基于云的平台(如 Amazon Web Services)优化的数据集。

  • 所有数据集都拥有相同类型的许可证,例如,仅列出公共领域数据集或仅列出已批准用于学术研究的数据集。

如果你发现你的数据集在这些列表中没有得到很好的展示,或者不符合现有的元集合要求,另一个选择是创建你自己的数据仓库。

Stack Exchange 上的开放数据

Stack Exchange 上的开放数据区,网址是opendata.stackexchange.com,是一个与开放数据集相关的问答集合。我曾在这里找到过许多有趣的数据集,有时候我也能向其他人展示如何使用我自己的数据集回答问题。这个问答网站也是一个很好的方式,帮助你了解人们有什么样的问题,以及他们希望使用的数据格式。

在 Stack Exchange 上将你的数据宣传为解决某个问题的方案之前,请确保你的访问方法、文档和许可证符合标准,参考我们在本章之前讨论的指南。这在 Stack Exchange 上尤其重要,因为无论是问题还是答案都可能被用户投下反对票。你最不想做的事情,就是用一堆断开的链接和混乱的文档来宣传你的数据。

黑客马拉松

让人们参与到你的数据中来,另一个有趣的方式是将其作为一个可用的数据集来宣传,适用于黑客马拉松。数据黑客马拉松通常是一天或多天的活动,程序员和数据科学家汇聚一堂,实践不同的技术,或者利用数据解决某类特定的问题。

一个简单的搜索引擎查询“数据黑客马拉松”可以帮助你了解当前黑客马拉松的关注重点。部分黑客马拉松由公司赞助,另一些则是为回应社会问题而举办的。大多数黑客马拉松都有维基或其他方法,允许你将你的网址和数据集的简要描述添加到可以在活动当天使用的数据集列表中。我不太推荐某一个具体的黑客马拉松,因为黑客马拉松本质上是一次性举办的活动,之后它们会发生变化,转变成其他形式。它们通常在不规则的时间举办,并由临时组织的团队进行安排。

如果你的数据集是为学术目的设计的,例如,如果它是一个研究数据集,你可以考虑在学术会议的工作坊或海报展示环节举办自己的黑客马拉松。这是让人们参与到数据操作中的一个绝佳方式,至少,你可以从会议上的人们那里获得一些有价值的反馈,了解如何改进你的数据,或者他们认为你应该下一个构建什么样的数据集。

总结

在本章中,我们探讨了多种分享我们清洗后数据的可能性。我们讨论了不同数据包装和分发方式的各种解决方案和权衡。我们还回顾了提供文档的基础知识,包括用户需要知道的最重要内容以及如何在文档文件中传达这些内容。我们注意到,许可证和使用条款几乎总是在文档中出现,但它们是什么意思,您应该如何为您的数据选择一个合适的许可证?我们回顾了一些数据项目的常见使用条款,以及最常见的许可方案:创作共用(Creative Commons)和开放数据库许可证(ODbL)。最后,我们集思广益,提出了几种宣传您数据的方式,包括数据元集合、开放数据堆栈交换网站和以数据为中心的黑客马拉松。

在本书的这一部分,您已经看到了数据清洗的完整的从头到尾的概述。接下来的两章将包含更长、更详细的项目,带给您更多关于数据清洗任务的实际体验,使用我们在本书前面部分学到的技能。

第九章 Stack Overflow 项目

这是两个完整的、章节级的项目中的第一个,我们将在其中实践所有关于数据清理的知识。我们可以把每个项目看作是一场晚宴,在这场晚宴上,我们展示自己在数据科学厨房中的最佳技能。要举办一场成功的晚宴,当然需要提前规划好菜单和嘉宾名单。然而,真正的专家标志是我们如何应对那些事情不完全按照计划进行的时刻。我们每个人都曾经历过这样的时刻:尽管我们精心准备了食谱和购物清单,但还是忘记购买一个重要的食材。我们能否调整计划,面对途中遇到的新挑战?

在本章中,我们将使用公开发布的 Stack Overflow 数据库转储进行一些数据清理。Stack Overflow 是 Stack Exchange 问答网站家族的一部分。在这些网站上,编写优秀的问题和答案可以为用户赢得积分和徽章,这些积分和徽章会随着时间积累。为了练习我们的数据清理技能,我们将使用我们在第一章中介绍的相同的六步方法,为什么你需要清洁数据?

  • 决定我们要解决的是什么问题——我们为什么要看这些数据?

  • 收集和存储我们的数据,包括下载并提取由 Stack Overflow 提供的数据转储,创建一个 MySQL 数据库来存储数据,并编写脚本将数据导入 MySQL 数据库。由于 Stack Overflow 数据集庞大,我们还将创建一些较小的测试表,填充随机选择的行。

  • 在尝试清理整个数据集之前,先对测试表执行一些试验性的清理任务。

  • 分析数据。我们是否需要进行计算?我们是否应该编写一些聚合函数来计数或求和数据?我们是否需要以某种方式转换数据?

  • 如果可能的话,提供数据的可视化。

  • 解决我们最初要调查的问题。我们的过程是否有效?我们成功了吗?

这需要大量的工作,但我们提前准备得越充分,开始得越早,我们就越有可能将我们的数据科学晚宴称为成功。

第一步——提出关于 Stack Overflow 的问题

为了开始我们的项目,我们需要提出一个合理有趣的问题,需要一些简单的数据分析来回答。我们该从哪里开始?首先,让我们回顾一下我们对 Stack Overflow 的了解。我们知道它是一个程序员的问答网站,我们可以假设程序员在提问和回答时可能会使用大量的源代码、错误日志和配置文件。此外,我们知道,有时在像 Stack Overflow 这样的基于 Web 的平台上发布这些长文本转储会因为行长、格式和其他可读性问题而显得很尴尬。

看到那么多包含大量文本的提问和回答让我不禁想知道,Stack Overflow 上的程序员是否会通过外部粘贴站点(例如 www.Pastebin.com)链接到他们的代码或日志文件。Pastebin 是一个可以粘贴大量文本的网站,如源代码或日志文件,网站会返回一个短链接,供你与他人分享。大多数粘贴站点也支持源代码语法高亮,而 Stack Overflow 默认不支持这一功能。

粘贴站点在 IRC 和电子邮件中非常常见,但在 Stack Overflow 上呢?一方面,就像在 IRC 或电子邮件中一样,提供一个链接可以使问题或回答更简洁,从而使其余的内容更容易阅读。但另一方面,根据使用的粘贴站点,URL 不一定能永远有效。这意味着,随着时间的推移,问题或回答可能会因为链接腐烂而失去价值。

像 JSFiddle 这样的工具在某些方面使这个问题变得更加复杂。在一个互动粘贴站点(如 JSFiddle)上,你不仅可以粘贴源代码并获得一个 URL,还可以允许他人在浏览器中编辑并运行代码。这在 Stack Overflow 的问答场景中非常有帮助,尤其是在像 JavaScript 这样的基于浏览器的语言中。然而,链接腐烂的问题依然存在。此外,对于初学者来说,JSFiddle 比像 Pastebin 这样简单的代码粘贴站点要稍微复杂一些。

步骤一 – 提出关于 Stack Overflow 的问题

JSFiddle 有四个窗口,分别用于 HTML、CSS、JavaScript 和结果展示。

注意

在 Stack Overflow 的社区讨论区里,关于是否应该使用粘贴站点,尤其是对于只包含粘贴站点链接而没有实际代码的提问或回答,展开了相当多的争论。总的来说,尽管人们普遍认为粘贴站点很有用,但他们也认识到保护 Stack Overflow 自身的长期性和实用性至关重要。社区决定避免发布仅包含链接而没有代码的提问或回答。如果你想回顾这场讨论,好的起点是这个链接:meta.stackexchange.com/questions/149890/

对于我们在这里的讨论,我们不需要站队。在这个辩论中,我们可以提出一些简单的基于数据的问题,例如:

  1. 人们在 Stack Overflow 上使用像 Pastebin 和 JSFiddle(以及其他类似的粘贴站点)的频率有多高?

  2. 他们在提问中还是回答中更常使用粘贴站点?

  3. 引用粘贴站点 URL 的帖子通常会包含源代码吗?如果包含,通常是多少?

我们可以将这些问题作为动机,收集、存储和清理我们的 Stack Overflow 数据。即使结果是其中的一些问题太难或无法回答,记住我们的总体目标将有助于指导我们需要进行的清理类型。将问题牢记在心可以防止我们偏离轨道,避免执行最终会变得毫无意义或浪费时间的任务。

第二步——收集和存储 Stack Overflow 数据

写作时,Stack Exchange 提供了他们所有网站(包括 Stack Overflow)的数据——以 XML 文件的形式,任何人都可以免费下载。在本节中,我们将下载 Stack Overflow 文件,并将数据导入到我们 MySQL 服务器的数据库中。最后,我们将创建这些表的几个小版本进行测试。

下载 Stack Overflow 数据转储

Stack Exchange 上的所有数据可以从互联网档案馆下载。2014 年 9 月的转储是写作时最新的版本。每个 Stack Exchange 网站都有一个或多个与之相关的文件,每个文件都链接到该详细信息页面:archive.org/details/stackexchange

我们只关心按字母顺序排列的八个 Stack Overflow 文件,具体如下所示:

下载 Stack Overflow 数据转储

Archive.org 列出显示我们感兴趣的八个 Stack Overflow 文件。

对于列表中的每个文件,右键点击链接,并指示你的浏览器将文件保存到磁盘。

解压文件

请注意,每个文件都具有 .7z 扩展名。这是一种压缩归档格式。可以使用匹配的 7-Zip 软件或其他兼容的软件包进行解压缩和解档。7-Zip 不是我们在第二章中讨论的最常见的文件归档工具,基础 - 格式、类型与编码,而且你可能电脑上还没有安装兼容的解压软件,因此我们可以把它当作第一个小问题,需要绕过。尝试双击文件以打开它,但如果你没有安装与 .7z 扩展名相关联的软件,你将需要安装一个适当的 7-Zip 解压工具。

  • 对于 Windows,你可以从他们的网站下载 7-Zip 软件:www.7-zip.org

  • 对于 Mac OS X,你可以下载并安装 The Unarchiver,这是一款免费的实用工具,下载地址:unarchiver.c3.cx

一旦安装了软件,逐个解压每个文件。解压后的文件相当大,所以请确保你有足够的磁盘空间来存放它们。

提示

在我目前的系统上,比较压缩和未压缩文件的大小显示,未压缩版本大约是压缩版本的十倍。这些文件解压时也需要几分钟时间,具体取决于你正在使用的系统规格,因此请为此步骤预留时间。

创建 MySQL 表并加载数据

我们现在有八个 .xml 文件,每个文件将映射到我们即将构建的数据库中的一个表。为了创建数据库和表,我们可以使用 phpMyAdmin 或其他图形工具通过点击的方式完成,或者我们可以运行 Georgios Gousios 编写并可在 gist.github.com/gousiosg/7600626 获得的以下简单 SQL 代码。此代码包含前六个表的 CREATELOAD INFILE 语句,但自从编写此脚本以来,数据库转储已经添加了两个额外的表。

为了构建新的表结构,我们可以在终端窗口或 shell 中运行 head 命令,以检查文件的前几行。在终端中,运行该命令在最小的 XML 文件 PostLinks.xml 上,方法如下:

head PostLinks.xml

结果中的前四行如下所示:

<?xml version="1.0" encoding="utf-8"?>
<postlinks>
 <row Id="19" CreationDate="2010-04-26T02:59:48.130" PostId="109" RelatedPostId="32412" LinkTypeId="1" />
 <row Id="37" CreationDate="2010-04-26T02:59:48.600" PostId="1970" RelatedPostId="617600" LinkTypeId="1" />

我们新数据库表中的每一行应对应 XML <row> 行中的一行,行中显示的每个属性表示数据库表中的一个列。我们可以对 Tags.xml 文件执行相同的 head 命令,查看它的列应该是什么。以下 SQL 代码将处理两个额外表的 CREATE 语句和 LOAD 语句:

CREATE TABLE post_links (
  Id INT NOT NULL PRIMARY KEY,
  CreationDate DATETIME DEFAULT NULL,
  PostId INT NOT NULL,
  RelatedPostId INT NOT NULL,
  LinkTypeId INT DEFAULT NULL
);

CREATE TABLE tags (
  Id INT NOT NULL PRIMARY KEY,
  TagName VARCHAR(50) DEFAULT NULL,
  Count INT DEFAULT NULL,
  ExcerptPostId INT DEFAULT NULL,
  WikiPostId INT DEFAULT NULL
);

LOAD XML LOCAL INFILE 'PostLinks.xml'
INTO TABLE post_links
ROWS IDENTIFIED BY '<row>';

LOAD XML LOCAL INFILE 'Tags.xml'
INTO TABLE tags
ROWS IDENTIFIED BY '<row>';

注意

注意,LOAD XML 语法略有变化,因此我们可以将文件保存在本地。如果你的.xml文件存储在本地计算机上而不是数据库服务器上,只需在LOAD XML语句中添加LOCAL字样,如前面的代码所示,然后可以引用文件的完整路径。

关于 MySQL LOAD XML 语法的更多信息,请参阅 MySQL 文档:dev.mysql.com/doc/refman/5.5/en/load-xml.html

到此为止,我们已经拥有一个功能完整的 MySQL 数据库,包含八个表,每个表都填充了数据。然而,这些表非常大,只有八个表就有超过一亿九千万行。当我们开始清理数据并为分析做准备时,我们会注意到,如果在像postscommentsvotespost_history这样的超大表上犯错,重建该表将需要很长时间。在下一步中,我们将学习如何创建测试表,以便如果程序或查询出现问题,我们能将损失控制在最小范围。

构建测试表

在本节中,我们将构建八个较小版本的原始表,每个表都随机填充来自原始表的数据。

我们的第一步是重新运行CREATE语句,但这次在每个表名之前加上test_前缀,如下所示:

DROP TABLE IF EXISTS test_post_links;
CREATE TABLE test_post_links (
  Id INT NOT NULL PRIMARY KEY,
  CreationDate INT,
  PostId INT,
  RelatedPostId INT,
  LinkTypeId INT
);

除了在表名之前添加test_外,这八个测试表将与我们之前创建的其他表完全相同。

接下来,我们需要向新的测试表中填充数据。我们可以简单地从每个表中选择前 1,000 行并加载到测试表中。然而,做这样做的缺点是这些行是根据它们插入 Stack Overflow 数据库的顺序排列的,因此如果我们仅请求前 1,000 行,我们的子集就不会有来自不同日期和时间的良好样本。我们希望选择的行有较为随机的分布。我们如何随机选择一组行?在本书中,我们之前并没有处理过这个问题,因此这是另一个需要我们准备尝试新方法的情况,以确保我们的数据科学晚宴顺利进行。

有几种选择随机行的方式,其中一些比其他方式更高效。效率对于我们这个项目来说非常重要,因为我们正在处理的表非常大。让我们随机选择行的一个小难点是,虽然我们的表有一个数字类型的主键作为Id列,但这些Id号码并不是连续的。例如,在post_links表中,Id列的前几个值分别是 19、37、42 和 48。

数据中的空缺是一个问题,因为简单的随机生成器是这样操作的:

  1. 构建一个 PHP 脚本,要求提供表中最低和最高的Id值,如下所示:

    SELECT min(Id) FROM post_links;
    SELECT max(Id) FROM post_links;
    
  2. 然后,仍然在脚本中,生成一个介于minmax值之间的随机数,并请求该随机值对应的行:

    SELECT * FROM post_links WHERE Id = [random value];
    
  3. 根据需要重复步骤 2,直到获取所需的行数。

不幸的是,举个例子,在 Stack Overflow 数据库表中执行此操作,例如在我们的post_links表上,将导致许多查询失败,因为我们的数据在Id列中有很多空缺。例如,如果前面的示例中的步骤 2 生成了数字 38,怎么办?我们的post_links表中没有Id为 38 的记录。这意味着我们需要检测到这个错误,并尝试用一个新的随机值重新执行。

注意

到这一步,一个懂一些 SQL 的人 — 但不多 — 通常会建议我们只需让 MySQL 在包含Id的列上执行ORDER BY rand(),然后执行LIMIT命令来挑选我们想要的记录数。这个想法的问题是,即使我们排序的列是索引列,ORDER BY rand()仍然必须读取每一行来分配一个新的随机数。因此,在像 Stack Overflow 数据库中那样的大表上,这种方法完全不适用。我们将不得不等待ORDER BY rand()查询完成,等待时间会太长。ORDER BY rand()对于小表是可以接受的解决方案,但对于我们在这里处理的大表大小则不适用。

以下 PHP 脚本展示了我们的最终随机行选择过程是如何工作的,它将构建八个测试表,每个表恰好包含 1,000 行。每个表将通过尽可能随机地选择行值,并尽量减少努力,且不对这个简单问题进行过度设计来填充:

<?php //randomizer.php
// how many rows should be in each of the test tables?
$table_target_size = 1000;

// connect to db, set up query, run the query
$dbc = mysqli_connect('localhost','username','password','stackoverflow')
       or die('Error connecting to database!' . mysqli_error());
$dbc->set_charset("utf8");

$tables = array("badges",
    "comments",
    "posts",
    "post_history",
    "post_links",
    "tags",
    "users",
    "votes");

foreach ($tables as $table)
{
  echo "\n=== Now working on $table ===\n";
    $select_table_info = "SELECT count(Id) as c, min(Id) as mn, max(Id) as mx FROM $table";
    $table_info = mysqli_query($dbc, $select_table_info);
    $table_stuff = mysqli_fetch_object($table_info);
    $table_count = $table_stuff->c;
    $table_min = $table_stuff->mn;
    $table_max = $table_stuff->mx;

    // set up loop to grab a random row and insert into new table
    $i=0;
    while($i < $table_target_size)
    {
        $r = rand($table_min, $table_max);
        echo "\nIteration $i: $r";
        $insert_rowx = "INSERT IGNORE INTO test_$table (SELECT * FROM $table WHERE Id = $r)";
        $current_row = mysqli_query($dbc, $insert_rowx);

        $select_current_count = "SELECT count(*) as rc FROM test_$table";
        $current_count= mysqli_query($dbc, $select_current_count);
        $row_count = mysqli_fetch_object($current_count)->rc;
        $i = $row_count;
    }
}
?>

运行该代码后,我们可以看到如果需要的话,我们有一组八个测试表可以使用。使用这些较小的表进行测试能确保我们的清理工作顺利进行,且错误能够被控制。如果我们发现需要更多的行数在我们的随机表中,我们可以简单地提高$table_target_size命令并重新运行。

构建测试表是一个很好的习惯,一旦你知道如何以简单和有用的方式创建它们。

第三步 – 清理数据

记住我们的目标是开始分析在问题、回答和评论中某些 URL 被引用的频率,因此从 Stack Overflow 的postscomments表中的文本开始是合乎逻辑的。然而,由于这些表非常大,我们将使用我们刚刚创建的test_poststest_comments表来代替。然后,一旦我们确信查询完美无缺,我们可以在更大的表上重新运行它们。

这个清理任务与我们在第七章中提取推文中的 URL 存储方式非常相似,RDBMS 清理技术。然而,这个项目有自己的一套具体规则:

  • 由于帖子和评论本身就是不同的实体,我们应该为来自帖子(包括问题和回答)和来自评论的 URL 分别创建不同的表。

  • 每个问题、回答或评论可以包含多个 URL。我们应该存储所有的 URL,同时也应该追踪这些 URL 来自于哪个帖子或评论的唯一标识符。

  • 每个问题和回答也可以包含格式化的源代码。<code>标签用于在 Stack Overflow 的帖子中界定源代码。将代码与帖子分开将帮助我们回答有关粘贴站点 URL 和源代码共存的问题。通常有多少代码会与这样的链接一起出现,如果有的话?

    注意

    从技术上讲,帖子可以在没有<code>标签的情况下创建,但通常会有人很快编辑这些不规范的帖子,加入这些有用的标签,并因此获得 Stack Overflow 的积分。为了简洁起见,在本项目中,我们假设代码会被包含在<code>标签中。

  • 根据 Stack Overflow 数据库转储文档(可以在meta.stackexchange.com/questions/2677/查看),实际上有八种帖子类型,其中问题和答案只是两种类型。因此,我们需要将查询限制为postTypeId=1表示问题,postTypeId=2表示答案。

  • 为了确保我们只从评论中提取指向问题或答案的 URL,而不是其他类型的帖子,我们需要将查询连接回帖子表,并将结果限制为postTypeId=1postTypeId=2

创建新表

创建我们需要的数据库表来存储这些 URL 的 SQL 查询如下:

CREATE TABLE clean_comments_urls (
  id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
  commentId INT NOT NULL,
  url VARCHAR(255) NOT NULL
) ENGINE=MyISAM  DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS clean_posts_urls (
  id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
  postId INT NOT NULL,
  url VARCHAR(255) NOT NULL
) ENGINE=MyISAM  DEFAULT CHARSET=utf8;

我们还需要创建一个表来存储我们从帖子中剥离出的代码:

CREATE TABLE clean_posts_code (
  id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
  postId INT NOT NULL,
  code TEXT NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

到目前为止,我们有了三个新表,这些表将存储我们的清理后的 URL 和清理后的源代码。在接下来的部分中,我们将提取 URL 和代码,并填充这些新表。

提取 URL 并填充新表

我们可以修改我们之前在第七章中编写的脚本,RDBMS 清理技术,以在这个新的 Stack Overflow 环境中提取 URL,具体如下:

<?php // urlExtractor.php
// connect to db
$dbc = mysqli_connect('localhost', 'username', 'password', 'stackoverflow')
    or die('Error connecting to database!' . mysqli_error());
$dbc->set_charset("utf8");

// pull out the text for posts with
// postTypeId=1 (questions)
// or postTypeId=2 (answers)
$post_query = "SELECT Id, Body
    FROM test_posts
    WHERE postTypeId=1 OR postTypeId=2";

$comment_query = "SELECT tc.Id, tc.Text
    FROM test_comments tc
    INNER JOIN posts p ON tc.postId = p.Id
    WHERE p.postTypeId=1 OR p.postTypeId=2";

$post_result = mysqli_query($dbc, $post_query);
// die if the query failed
if (!$post_result)
    die ("post SELECT failed! [$post_query]" .  mysqli_error());

// pull out the URLS, if any
$urls = array();
$pattern  = '#\b(([\w]+://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))#';

while($row = mysqli_fetch_array($post_result))
{
    echo "\nworking on post: " . $row["id"];
    if (preg_match_all(
        $pattern,
        $row["Body"],
        $urls
    ))
    {
        foreach ($urls[0] as $url)
        {
          $url = mysqli_escape_string($dbc, $url);
            echo "\n----url: ".$url;
            $post_insert = "INSERT INTO clean_posts_urls (id, postid, url)
                VALUES (NULL," . $row["Id"] . ",'$url')";
            echo "\n$post_insert";
            $post_insert_result = mysqli_query($dbc, $post_insert);
        }
    }
}

$comment_result = mysqli_query($dbc, $comment_query);
// die if the query failed
if (!$comment_result)
    die ("comment SELECT failed! [$comment_query]" .  mysqli_error());

while($row = mysqli_fetch_array($comment_result))
{
    echo "\nworking on comment: " . $row["id"];
    if (preg_match_all(
        $pattern,
        $row["Text"],
        $urls
    ))
    {
        foreach ($urls[0] as $url)
        {
            echo "\n----url: ".$url;
            $comment_insert = "INSERT INTO clean_comments_urls (id, commentid, url)
                VALUES (NULL," . $row["Id"] . ",'$url')";
            echo "\n$comment_insert";
            $comment_insert_result = mysqli_query($dbc, $comment_insert);
        }
    }
}
?>

我们现在已经完全填充了clean_post_urlsclean_comment_urls表。对于我随机填充的测试表,运行这个脚本只会得到大约 100 个评论 URL 和 700 个帖子 URL。不过,这些数据足以在我们对整个数据集运行脚本之前,先测试一下我们的思路。

提取代码并填充新表

为了提取<code>标签中嵌入的文本并填充我们的新clean_posts_code表,我们可以运行以下脚本。这个过程类似于 URL 提取器,唯一不同的是它不需要搜索评论,因为评论中没有用<code>标签界定的代码。

在我随机选择的测试表中,初始的SELECT查询从test_post表中提取了约 800 行,表中总共有 1000 行。然而,每个帖子可能包含多个代码片段,因此最终的表格有超过 2000 行。以下 PHP 代码提取了<code>标签中嵌入的文本:

<?php // codeExtractor.php
// connect to db
$dbc = mysqli_connect('localhost', 'username, 'password', 'stackoverflow')
    or die('Error connecting to database!' . mysqli_error());
$dbc->set_charset("utf8");

// pull out the text for posts with
// postTypeId=1 (questions)
// or postTypeId=2 (answers)
$code_query = "SELECT Id, Body
    FROM test_posts
    WHERE postTypeId=1 OR postTypeId=2
    AND Body LIKE '%<code>%'";

$code_result = mysqli_query($dbc, $code_query);
// die if the query failed
if (!$code_result)
    die ("SELECT failed! [$code_query]" .  mysqli_error());

// pull out the code snippets from each post
$codesnippets = array();
$pattern  = '/<code>(.*?)<\/code>/';

while($row = mysqli_fetch_array($code_result))
{
    echo "\nworking on post: " . $row["Id"];
    if (preg_match_all(
        $pattern,
        $row["Body"],
        $codesnippets
    ))
    {
      $i=0;
        foreach ($codesnippets[0] as $code)
        {
          $code = mysqli_escape_string($dbc, $code);
            $code_insert = "INSERT INTO clean_posts_code (id, postid, code)
                VALUES (NULL," . $row["Id"] . ",'$code')";
            $code_insert_result = mysqli_query($dbc, $code_insert);
            if (!$code_insert_result)
                die ("INSERT failed! [$code_insert]" .  mysqli_error());
            $i++;
        }
        if($i>0)
        {
          echo "\n   Found $i snippets";
        }
    }
}
?>

我们现在有了一个包含每个帖子中打印出的所有代码的列表,并已将其存储在clean_post_code表中。

第四步 – 分析数据

在这一部分,我们编写一些代码来回答本章开始时的三个问题。我们感兴趣的是寻找:

  • 帖子和评论中提到的不同粘贴网站的 URL 数量

  • 比较问题和答案中粘贴网站 URL 的数量

  • 统计带有粘贴网站 URL 的帖子中<code>标签的普及率

哪些粘贴网站最受欢迎?

为了回答这个问题,我们将生成一个 JSON 表示,包含 paste 站点 URL 和计数,使用clean_posts_urlsclean_comments_urls表格。这项简单的分析将帮助我们找出哪些 pastebin 网站在这个 Stack Overflow 数据集中特别受欢迎。以下 PHP 查询从数据库中查询我们预先列出的$pastebins数组中的 paste 站点,并执行从帖子和评论中匹配 URL 的计数。它使用的是测试表格,因此这些数字要比实际表格中的数字小得多:

<?php // q1.php
// connect to db
$dbc = mysqli_connect('localhost', 'username', 'password', 'stackoverflow')
    or die('Error connecting to database!' . mysqli_error());
$dbc->set_charset("utf8");

// these are the web urls we want to look for and count
$pastebins = array("pastebin",
    "jsfiddle",
    "gists",
    "jsbin",
    "dpaste",
    "pastie");
$pastebin_counts = array();

foreach ($pastebins as $pastebin)
{
    $url_query = "SELECT count(id) AS cp,
          (SELECT count(id)
          FROM clean_comments_urls
          WHERE url LIKE '%$pastebin%') AS cc
        FROM clean_posts_urls
        WHERE url LIKE '%$pastebin%'";
    $query = mysqli_query($dbc, $url_query);
    if (!$query)
        die ("SELECT failed! [$url_query]" .  mysqli_error());
    $result = mysqli_fetch_object($query);
    $countp = $result->cp;
    $countc = $result->cc;
    $sum = $countp + $countc;

    array_push($pastebin_counts, array('bin' => $pastebin,
                                        'count' => $sum));
}
// sort the final list before json encoding it
// put them in order by count, high to low
foreach ($pastebin_counts as $key => $row)
{
    $first[$key]  = $row['bin'];
    $second[$key] = $row['count'];
}

array_multisort($second, SORT_DESC, $pastebin_counts);
echo json_encode($pastebin_counts);
?>

我们可以查看运行该脚本时从测试表格中得到的 JSON 输出,通过查看脚本的输出。我的随机行产生了以下计数:

[{"bin":"jsfiddle","count":44},{"bin":"jsbin","count":4},{"bin":"pastebin","count":3},{"bin":"dpaste","count":0},{"bin":"gists","count":0},{"bin":"pastie","count":0}]

注意

记住,由于你选择的随机 URL 集合不同,你的值可能会有所不同。

当我们进入本章的步骤 5 – 数据可视化部分时,我们将使用这个 JSON 代码来构建一个条形图。但首先,让我们先回答之前提出的另外两个问题。

哪些 paste 站点在问题中流行,哪些在回答中流行?

我们的第二个问题是,pastebin URL 在问题帖子中更常见,还是在回答帖子中更常见。为了开始解决这个问题,我们将运行一系列 SQL 查询。第一个查询仅仅是询问clean_posts_urls表中每种类型的帖子数,问题和回答:

SELECT tp.postTypeId, COUNT(cpu.id)
FROM test_posts tp
INNER JOIN clean_posts_urls cpu ON tp.Id = cpu.postid
GROUP BY 1;

结果显示,在我随机选择的测试集中,我有 237 个问题和 440 个回答:

哪些 paste 站点在问题中流行,哪些在回答中流行?

phpMyAdmin 显示问题 URL 和回答 URL 的计数。

现在,我们想要知道这个问题的答案:在这 677 个 URL 中,按问题和回答分类,有多少个专门引用了六个 pastebin 网站中的某一个?我们可以运行以下 SQL 代码来找出答案:

SELECT  tp.postTypeId, count(cpu.id)
FROM test_posts tp
INNER JOIN clean_posts_urls cpu ON tp.Id = cpu.postId
WHERE cpu.url LIKE '%jsfiddle%'
OR cpu.url LIKE '%jsbin%'
OR cpu.url LIKE '%pastebin%'
OR cpu.url LIKE '%dpaste%'
OR cpu.url LIKE '%gist%'
OR cpu.url LIKE '%pastie%'
GROUP BY 1;

结果如下表所示。共有 18 个问题引用了某个 paste 站点,而 24 个回答引用了某个 paste 站点。

哪些 paste 站点在问题中流行,哪些在回答中流行?

phpMyAdmin 显示了引用 pastebin 的问答 URL 的计数。

需要注意的一点是,这些查询统计的是每个 URL 的出现次数。所以,如果某个postId引用了五个 URL,那么它们会被计数五次。如果我关心的是有多少帖子使用了某个 paste 站点 URL 一次或更多次,我需要修改两个查询的第一行,如下所示。这个查询统计了 URLs 表格中不同的帖子:

SELECT tp.postTypeId, COUNT(DISTINCT cpu.postId)
FROM test_posts tp
INNER JOIN clean_posts_urls cpu ON tp.Id = cpu.postId
GROUP BY 1;

以下截图显示了有多少问题和回答包含了一个 URL:

哪些 paste 站点在问题中流行,哪些在回答中流行?

phpMyAdmin 显示了有多少问题和回答包含了任何 URL。

这个查询统计了在 URLs 表格中提到 paste 站点的特定帖子:

SELECT  tp.postTypeId, count(DISTINCT cpu.postId)
FROM test_posts tp
INNER JOIN clean_posts_urls cpu ON tp.Id = cpu.postId
WHERE cpu.url LIKE '%jsfiddle%'
OR cpu.url LIKE '%jsbin%'
OR cpu.url LIKE '%pastebin%'
OR cpu.url LIKE '%dpaste%'
OR cpu.url LIKE '%gist%'
OR cpu.url LIKE '%pastie%'
GROUP BY 1;

这个粘贴网站查询的结果如下,正如预期的那样,数字较小。在我们的测试集里,11 个问题使用了至少一个粘贴站 URL,16 个答案也如此。合计,37 个帖子至少引用了一个粘贴站 URL。

哪些粘贴网站在问题中流行,哪些在答案中流行?

PhpMyAdmin 显示了包含任何粘贴网站 URL 的问题和答案的数量。

尽管这些结果似乎显示人们在答案中引用粘贴网站 URL 的频率高于问题中,但我们需要从问题和答案的总体数量来进行比较。我们应该将结果值报告为该帖子类型(问题或答案)的总数的百分比。考虑到总数,我们现在可以说类似这样的话:“只考虑那些至少一次使用了某种 URL 的问题和答案,81 个问题中有 11 个使用了至少一个粘贴网站 URL(13.6%),222 个答案中有 16 个使用了至少一个粘贴网站 URL(7.2%)。” 综上所述,实际上问题在引用粘贴网站方面超过了答案,几乎是两倍。

在任何数据分析项目的这个阶段,你一定会有一大堆问题,比如:

  • 粘贴网站 URL 在问题和答案中的使用随时间发生了什么变化?

  • 带有粘贴网站 URL 的问题在投票和收藏中表现如何?

  • 发布带有粘贴网站 URL 的问题的用户有什么特点?

但由于这是一本关于数据清洗的书,而且我们仍然没有可视化这些数据,我会克制自己,暂时不回答这些问题。我们还有一个原始的三个问题没有回答,然后我们将继续可视化我们的一些结果。

帖子中是否同时包含粘贴站的 URL 和源代码?

回答我们第三个问题需要将 Stack Overflow 问题中的代码量与答案中的代码量进行比较,特别关注那些包含某种源代码(由 <code> 标签分隔)的帖子。在 第三步 - 清洗数据 部分中,我们从测试表中的帖子中提取了所有代码,并创建了一个新表来存放这些代码片段。现在,一个简单的查询来找出包含代码的帖子数量如下:

SELECT count(DISTINCT postid)
FROM clean_posts_code;

在我的样本集中,这产生了 664 个包含代码的帖子,来自 1,000 个测试帖子。换句话说:1,000 个帖子中有 664 个包含至少一个 <code> 标签。

要找出这些包含代码的帖子中有多少也包含了任何 URL,我们可以运行以下 SQL 查询:

SELECT count(DISTINCT cpc.postid)
FROM clean_posts_code cpc
INNER JOIN clean_posts_urls cpu
ON cpu.postId = cpc.postId;

我的样本集返回了 175 行数据。我们可以这样解释:原始测试集 1,000 个帖子中,17.5% 包含了代码和 URL。

现在,为了找出有多少包含代码的帖子也包含了粘贴站 URL,我们将进一步缩小 SQL 查询的范围:

SELECT count(DISTINCT cpc.postid)
FROM clean_posts_code cpc
INNER JOIN clean_posts_urls cpu
ON cpu.postId = cpc.postId
WHERE cpu.url LIKE '%jsfiddle%'
OR cpu.url LIKE '%jsbin%'
OR cpu.url LIKE '%pastebin%'
OR cpu.url LIKE '%dpaste%'
OR cpu.url LIKE '%gist%'
OR cpu.url LIKE '%pastie%';

从这些结果中,我们可以看到,只有 25 篇帖子同时包含源代码和粘贴站点 URL。从第二个问题中,我们知道 37 篇不同的帖子(包括问题和答案)至少使用过一次某种粘贴站点 URL。因此,25 比 37 大约是 68%。在更大的数据集上运行这些查询,看看这些值如何变化,将会很有趣。

与此同时,我们将对至少一个问题进行简单的可视化,以便完成数据科学六步法的一个完整回合。

第五步 – 可视化数据

可视化步骤有点像我们晚宴中的甜点环节。每个人都喜欢丰富的图形,它们看起来非常漂亮。然而,由于本书的重点在于数据清理而非分析与可视化,我们这里的图形将非常简单。在接下来的代码中,我们将使用 JavaScript D3 可视化库,以图形方式展示第一个问题的结果。这次可视化比我们在第四章中做的 D3 可视化要简单得多。你会记得,在那一章中,我们构建了一个相当复杂的网络图,但在这里,简单的条形图就足够了,因为我们只需要展示一些标签和计数。

以下是 HTML 和 JavaScript/D3 代码。该代码扩展了 Mike Bostock 的让我们制作一个条形图教程,教程地址为bl.ocks.org/mbostock/3885304。我扩展这段代码的方式之一是让它读取我们之前在q1.php脚本中生成的 JSON 文件。我们的 JSON 文件格式很漂亮,并且已经按从高到低排序,因此从中构建一个小条形图非常容易:

<!DOCTYPE html>
<meta charset="utf-8">
<!--
this code is modeled on mbostock's
"Let's Make a Bar Chart" D3 tutorial
available at http://bl.ocks.org/mbostock/3885304
My modifications:
* formatting for space
* colors
* y axis labels
* changed variable names to match our data
* loads data via JSON rather than .tsv file
-->

<style>
.bar {fill: lightgrey;}
.bar:hover {fill: lightblue;}
.axis {font: 10px sans-serif;}
.axis path, .axis line {
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;
}
.x.axis path {display: none;}
</style>
<body>
<script src="img/d3.min.js"></script>
<script>

var margin = {top: 20, right: 20, bottom: 30, left: 40},
    width = 960 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;

var x = d3.scale.ordinal()
    .rangeRoundBands([0, width], .1);

var y = d3.scale.linear()
    .range([height, 0]);

var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom");

var yAxis = d3.svg.axis()
    .scale(y)
    .orient("left");

var svg = d3.select("body").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

d3.json("bincounter.php", function(error, json)
{
    data = json;
    draw(data);
});

function draw(data)
{
  x.domain(data.map(function(d) { return d.bin; }));
  y.domain([0, d3.max(data, function(d) { return d.count; })]);

  svg.append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + height + ")")
      .call(xAxis);

  svg.append("g")
      .attr("class", "y axis")
      .call(yAxis)
    .append("text")
      .attr("transform", "rotate(-90)")
      .attr("y", 6)
      .attr("dy", ".71em")
      .style("text-anchor", "end")
      .text("Frequency");

  svg.selectAll(".bar")
      .data(data)
    .enter().append("rect")
      .attr("class", "bar")
      .attr("x", function(d) { return x(d.bin) ; })
      .attr("width", x.rangeBand())
      .attr("y", function(d) { return y(d.count); })
      .attr("height", function(d) { return height - y(d.count); });
}

</script>
</body>
</html>

我们可以将其保存为q1chart.html,并在浏览器中查看。该代码调用了我们的q1.php脚本,后者生成 JSON 文件,D3 则用它来构建这个图表,左侧部分如下所示:

第五步 – 可视化数据

D3 可视化展示从三个 URL 计数生成的 JSON 数据。

条形图显示指向 JSFiddle 的 URL 似乎是最常见的,至少在我版本的随机选择测试数据集中是这样。我们仅通过查看q1.php的 JSON 输出就知道了这一点,但看到图形化的展示仍然让人感觉很直观。接下来的部分,我们将总结结果和过程,并讨论下一步该如何推进这个项目。

第六步 – 问题解决

从我们在第四步 – 分析数据第五步 – 可视化数据部分开发的查询和可视化中,我们现在可以尝试回答当初促使这个项目的三个问题。

在第一个问题中,我们希望查找在帖子和评论中按 URL 提到的不同粘贴站点的数量。我们创建的q1.php脚本和条形图可视化数据显示,至少在测试数据中,JSFiddle 是我们查看的六个粘贴站点 URL 中最常被提及的。

第二个问题是关于粘贴站点 URL 在问题和答案中是否更为普遍。我们的查询显示,粘贴站点 URL 出现在问题中的几率是出现在答案中的大约两倍,但无论是问题还是答案中的数量都很少,至少在我们的测试集中是这样。

对于第三个问题,我们希望查看人们是否真的听从了 Stack Overflow 的建议,在发布粘贴站点 URL 的同时也附上了源代码。在我们的测试集中,查询结果显示,37 条记录中有 25 条同时包含了粘贴站点 URL 和推荐的源代码。这意味着大约有 68%的合规率。

此时,我们可以提出并回答许多其他问题,也有很多激动人心的方法,可以将这个简单的研究扩展成更有趣的内容。但现在,我们将专注于存储和清理程序,以便将这个项目扩展到使用完整的数据集。

从测试表迁移到完整表

在这个项目的开始阶段,我们创建了一组测试表,以便在一个无压力的环境中开发项目,每个表只有 1,000 行数据。使用行数可控的小型表格非常重要,尤其是在我们不确定查询是否按预期工作的情况下,或者当我们需要尝试一些复杂的连接、子查询、奇怪的正则表达式等。此时,如果我们对已经编写的查询和脚本感到满意,就可以开始重写过程,使用完整大小的表格。

以下是我们将采取的步骤,将项目迁移到完整表:

  1. DROP测试表:

    DROP TABLE IF EXISTS test_badges;
    DROP TABLE IF EXISTS test_comments;
    DROP TABLE IF EXISTS test_posts;
    DROP TABLE IF EXISTS test_post_history;
    DROP TABLE IF EXISTS test_post_links;
    DROP TABLE IF EXISTS test_tags;
    DROP TABLE IF EXISTS test_users;
    DROP TABLE IF EXISTS test_votes;
    
  2. 如下所示,清空cleaned_posts_codecleaned_posts_urlscleaned_comments_urls表:

    TRUNCATE TABLE cleaned_posts_code;
    TRUNCATE TABLE cleaned_posts_urls;
    TRUNCATE TABLE cleaned_comments_urls;
    
  3. 编辑urlExtractor.phpcodeExtractor.php脚本,使其从posts表中SELECT而不是从test_posts表中选择。可以按如下方式编辑这些查询:

    SELECT Id, Body FROM posts
    
  4. 重新运行urlExtractor.phpcodeExtractor.php脚本,以便它们重新填充之前清空(截断)的干净代码和 URL 表。

此时,我们已经准备好清理后的代码和 URL 表进行分析和可视化。在执行这些步骤时请耐心,了解许多查询和脚本可能需要很长时间才能完成。posts表非常大,且我们编写的许多查询都是针对使用通配符的文本列进行选择的。

摘要

在这个项目中,我们提出了几个关于 Stack Overflow 上 URL 普及的问题,特别是那些与粘贴网站相关的链接,如www.Pastebin.comwww.JSFiddle.net。为了开始回答这些问题,我们从 Stack Exchange 的公开文件发布中下载了 Stack Overflow 帖子(以及其他 Stack Overflow 数据)。我们建立了一个 MySQL 数据库,并创建了八个表来存储这些数据。然后,我们为测试目的创建了每个表的 1,000 行小版本,这些版本填充了随机选择的数据样本。通过这些测试表,我们提取了每个问题、答案和评论中提到的 URL,并将它们保存到一个新的干净表格中。我们还提取了问题和答案中的源代码,并将这些代码片段保存到一个新的表格中。最后,我们能够构建一些简单的查询和可视化工具,帮助我们回答最初提出的问题。

尽管结果相对简单,从数据清理的角度来看,我们的“晚宴”还是成功的。我们能够制定一个连贯的计划,并采取系统的步骤来执行计划,并在必要时调整。现在我们已经准备好迎接我们的最终项目,以及一个完全不同的晚宴菜单。

在下一章,我们将收集并清理我们自己版本的著名 Twitter 数据集。

第十章. Twitter 项目

与我们在第九章完成的Stack Overflow 项目一样,我们下一个完整章节的晚宴派对旨在特别展示我们的数据清理技能,同时仍完成整个数据科学过程的每个阶段。我们之前的项目使用了 Stack Overflow 数据,结合 MySQL 和 PHP 进行清理,并使用 JavaScript D3 进行可视化。在本章中,我们将使用 Twitter 数据,MySQL 和 Python 收集和清理数据,并使用 JavaScript 和 D3 进行可视化。与我们之前的项目一样,本项目中的数据科学过程仍然是相同的:

  1. 确定我们要解决的问题类型——我们为什么需要检查这些数据?

  2. 收集和存储数据,包括下载和提取一组公开的推文识别号码,然后使用程序重新下载原始推文。此步骤还包括为测试目的创建较小的数据集。

  3. 清理数据,通过提取和存储我们需要的部分来完成。在这一步中,我们编写一个快速的 Python 程序来加载数据,提取我们想要的字段,并将它们写入一小组数据库表中。

  4. 分析数据。我们是否需要对数据进行任何计算?我们是否需要编写一些聚合函数来计数或求和数据?我们是否需要以某种方式转换数据?

  5. 如果可能,提供数据的可视化。

  6. 解决我们要调查的问题。我们的过程是否有效?我们是否成功了?

与我们之前的项目一样,工作的大部分内容将集中在数据的收集、存储和清理任务上。在本项目中,我们会注意到我们将数据存储为不同的版本几次,首先是文本文件,然后是已填充的 JSON 文件,接着是 MySQL 数据库。这些不同的格式是通过不同的收集或清理过程产生的,并将数据传递到下一个步骤。通过这种方式,我们开始看到收集、存储和清理步骤是如何迭代的——彼此反馈,而不仅仅是线性的。

第一步——提出一个关于推文档案的问题

Twitter 是一个受欢迎的微博平台,全球数百万人使用它分享想法或讨论时事。由于 Twitter 在发布和阅读方面相对易于访问,尤其是通过移动设备,它已成为在公共事件(如政治危机和抗议)期间分享信息的重要平台,或用来追踪一个问题在公众意识中的出现。保存的推文成为一种时间胶囊,为我们提供了关于事件发生时公共情绪的丰富见解。定格在时间中的推文不会受到记忆丧失或公众舆论后续反转的影响。学者和媒体专家可以收集并研究这些专题推文档案,以试图了解当时的公众舆论,或者了解信息是如何传播的,甚至了解某个事件发生时发生了什么,何时发生,为什么发生。

现在许多人已经开始公开他们的推文集合。一些现在可以公开下载的推文档案包括:

在接下来的步骤中,我们需要从这些数据集中选择一个进行下载并开始使用。由于费格森推文是这三个示例数据集中最新且最完整的,因此我设计了本章的其余内容围绕它进行。不过,无论你使用哪个数据集,这里的概念和基本流程都适用。

我们在本章中要提出的基本问题非常简单:当人们关于费格森发表推文时,他们最常提到的是哪些互联网域名?与我们在第九章中的问题集相比,Stack Overflow 项目,这是一个非常简单的问题,但数据收集过程在这里略有不同,因此一个简单的问题足以激励我们的数据科学晚宴。

第二步 – 收集数据

与我们在第七章中研究的小型情感推文档案不同,RDBMS 清理技术,更新的推文档案,如上文提到的,已不再包含推文的实际文本。Twitter 的服务条款ToS)自 2014 年起发生了变化,分发他人的推文现在违反了这些条款。相反,在较新的推文档案中,你将找到的仅仅是推文的标识符(ID)号码。使用这些号码,我们必须单独收集实际的推文。到那时,我们可以自行存储和分析这些推文。请注意,在此过程的任何时刻或之后,我们不能重新分发推文的文本或其元数据,只能分发推文的标识符。

注意

尽管对于想要收集推文的研究人员来说,这样做不太方便,但 Twitter 更改其服务条款(ToS)的原因是为了尊重最初发布推文的人的版权观念。尤其是在删除推文时非常重要。如果一条推文已经被复制并在网络上广泛传播,并且被第三方存储在数据文件中,那么推文实际上是无法被删除的。通过要求研究人员仅复制推文 ID,Twitter 试图保护用户删除自己内容的能力。如果请求已删除的推文 ID,将不会返回任何结果。

下载并提取费格森文件

与费格森相关的推文可以在互联网档案馆找到,作为一组归档的压缩文本文件。请将浏览器指向archive.org/details/ferguson-tweet-ids并下载 147 MB 的 ZIP 文件。

我们可以按以下方式提取文件:

unzip ferguson-tweet-ids.zip

ls命令显示创建了一个名为ferguson-tweet-ids的目录,并且该目录内也有两个压缩文件:ferguson-indictment-tweet-ids.zipferguson-tweet-ids.zip。我们实际上只需要解压其中一个文件来执行这个项目,因此我选择了这个:

unzip ferguson-tweet-ids.zip

解压此文件后,可以看到多个清单文本文件以及一个数据文件夹。data文件夹中包含一个 gzipped 文件。按以下方式解压:

gunzip ids.txt.gz

这会产生一个名为ids.txt的文件。这个文件就是我们实际需要的文件。让我们来探索一下这个文件。

要查看文件的大小,我们可以运行wc命令。从命令提示符运行时,如这里所示,wc命令会显示文件中的行数、单词数和字符数,顺序如下:

megan$ wc ids.txt
 13238863 13238863 251538397 ids.txt

第一个数字表示ids.txt文件中的行数,总共有 1300 多万行。接下来,我们可以使用head命令查看文件内容:

megan$ head ids.txt
501064188211765249
501064196642340864
501064197632167936
501064196931330049
501064198005481472
501064198009655296
501064198059597824
501064198513000450
501064180468682752
501064199142117378

head命令显示文件的前十行,我们可以看到每行由一个 18 位的推文 ID 组成。

创建文件的测试版本

在这一阶段,我们将创建一个小的测试文件,供本项目其余部分使用。我们想这么做的原因与我们在第九章中处理测试表格的原因相同,Stack Overflow 项目。因为原始文件非常大,我们希望在出错时能够处理数据的一个子集。我们还希望在测试代码时,每一步不会花费太长时间。

与之前的练习不同,在这种情况下,我们选择测试数据行的顺序可能并不重要。仅通过查看我们之前运行的 head 命令的结果,我们可以看到这些行并不完全是按低到高的顺序排列。事实上,我们对原始数据行的顺序一无所知。因此,我们只需抓取前 1,000 个推文 ID 并将其保存到一个文件中。这个文件将成为我们的测试集:

head -1000 ids.txt > ids_1000.txt

填充推文 ID

我们现在将使用这组 1,000 个推文 ID 来测试我们基于推文 ID 收集原始推文的流程。这个过程称为填充推文。为此,我们将使用一个非常方便的 Python 工具,叫做twarc,它是由 Ed Summers 编写的,Ed 同时也是存档了所有 Ferguson 推文的人。它的工作原理是通过获取推文 ID 列表,逐个从 Twitter API 获取原始推文。要使用 Twitter API,必须先设置好 Twitter 开发者帐户。我们先创建一个 Twitter 账户,然后再安装 twarc 并使用它。

设置 Twitter 开发者账户

要设置 Twitter 开发者账户,请访问 apps.twitter.com,并使用你的 Twitter 账户登录。如果你还没有 Twitter 账户,你需要先创建一个,才能完成后续步骤。

一旦你用 Twitter 账户登录,从 apps.twitter.com 页面,点击创建新应用。填写必要的详细信息来创建你的应用(为它命名,可以是类似 My Tweet Test 的名字;提供简短的描述;以及 URL,URL 不必是永久性的)。我的应用创建表单已填写如下,供你参考:

设置 Twitter 开发者账户

填写示例数据的 Twitter 创建新应用表单

点击表示同意开发者协议的复选框,然后你将返回到显示所有 Twitter 应用的屏幕,新的应用会显示在列表的最上方。

接下来,要使用这个应用程序,我们需要获取一些必要的关键信息。点击你刚创建的应用,你将看到下一屏幕顶部有四个标签。我们感兴趣的是标有密钥和访问令牌的那个。它看起来是这样的:

设置 Twitter 开发者账户

你新创建的 Twitter 应用中的标签界面。

在这个页面上有很多数字和密钥,但有四个项目我们需要特别关注:

  • CONSUMER_KEY

  • CONSUMER_SECRET

  • ACCESS_TOKEN

  • ACCESS_TOKEN_SECRET

无论你在进行哪种类型的 Twitter API 编程,至少目前为止,你总是需要这四项凭证。无论你使用的是 twarc 还是其他工具,这些凭证都是 Twitter 用来验证你并确保你有权限查看你请求的内容的方式。

注意

这些 API 凭证也是 Twitter 限制你可以发送多少请求以及请求速度的依据。twarc 工具会代我们处理这些问题,所以我们不需要过多担心超过请求频率限制。欲了解更多关于 Twitter 限制的信息,请查看他们的开发者文档,dev.twitter.com/rest/public/rate-limiting

安装 twarc

现在我们有了 Twitter 凭证,就可以安装 twarc 了。

twarc 下载页面可以在 GitHub 上找到,github.com/edsu/twarc。在该页面上,有关于如何使用这个工具以及可用选项的文档。

要在你的 Canopy Python 环境中安装 twarc,启动 Canopy 后从工具菜单中选择Canopy 命令提示符

在命令提示符中输入:

pip install twarc

这条命令将安装 twarc 并使其可以作为命令行程序调用,或者在你的 Python 程序中使用。

运行 twarc

现在我们可以使用命令行通过 twarc 工具来获取我们之前创建的ids_1000.txt文件。由于我们需要输入之前在 Twitter 网站上创建的四个长的密钥令牌,命令行非常长。为了避免出错,我先使用文本编辑器 Text Wrangler 创建了命令行,然后将其粘贴到命令提示符中。你最终的命令行会像下面这样,但每当出现abcd时,你应该用你对应的密钥令牌或密钥来替代:

twarc.py --consumer_key abcd --consumer_secret abcd --access_token abcd --access_token_secret abcd --hydrate ids_1000.txt > tweets_1000.json

请注意,这条命令行将把输出重定向到一个名为tweets_1000.json的 JSON 文件。该文件中包含了我们之前只有 ID 的每条推文的 JSON 表示。让我们检查一下新文件的长度:

wc tweets_1000.json

wc 工具显示我的文件长达 894 行,这表示一些推文无法找到(因为我原本的数据集中有 1,000 条推文)。如果这些推文在我编写本文后被删除,你的文件可能会更小。

接下来,我们还可以打开文件查看它的内容:

less tweets_1000.json

我们也可以在文本编辑器中打开它查看。

JSON 文件中的每一行代表一条推文,每条推文的结构类似于以下示例。然而,这个示例推文并不来自 Ferguson 数据集,因为我没有权利分发这些推文。相反,我使用了我在第二章中创建的推文,基础知识 – 格式、类型和编码,用于讨论 UTF-8 编码。由于这条推文是专门为本书创建的,而且我拥有内容的所有权,因此我可以在不违反 Twitter 使用条款的情况下展示其 JSON 格式。以下是我的推文在 Twitter 网页界面上的显示方式:

运行 twarc

一个推文的示例,如 Twitter 网页界面所示。

以下是推文在经过 twarc 水合后的 JSON 表示形式。我在每个 JSON 元素之间添加了换行符,这样我们可以更容易地看到每个推文中可用的属性:

{"contributors": null, 
"truncated": false, 
"text": "Another test. \u00c9g elska g\u00f6gn. #datacleaning", 
"in_reply_to_status_id": null, 
"id": 542486101047275520, 
"favorite_count": 0, 
"source": "<a href=\"http://twitter.com\" rel=\"nofollow\">Twitter Web Client</a>", 
"retweeted": false, 
"coordinates": null, 
"entities": 
{"symbols": [], 
"user_mentions": [], 
"hashtags": 
[{"indices": [29, 42], 
"text": "datacleaning"}], 
"urls": []}, 
"in_reply_to_screen_name": null, 
"id_str": "542486101047275520", 
"retweet_count": 0, 
"in_reply_to_user_id": null, 
"favorited": false, 
"user": 
{"follow_request_sent": false, 
"profile_use_background_image": false, 
"profile_text_color": "333333", 
"default_profile_image": false, 
"id": 986601, 
"profile_background_image_url_https": "https://pbs.twimg.com/profile_background_images/772436819/b7f7b083e42c9150529fb13971a52528.png", 
"verified": false, 
"profile_location": null, 
"profile_image_url_https": "https://pbs.twimg.com/profile_images/3677035734/d8853be8c304729610991194846c49ba_normal.jpeg", 
"profile_sidebar_fill_color": "F6F6F6", 
"entities": 
{"url": 
{"urls": 
[{"url": "http://t.co/dBQNKhR6jY", 
"indices": [0, 22], 
"expanded_url": "http://about.me/megansquire", 
"display_url": "about.me/megansquire"}]},
"description": {"urls": []}}, 
"followers_count": 138, 
"profile_sidebar_border_color": "FFFFFF", 
"id_str": "986601", 
"profile_background_color": "000000", 
"listed_count": 6, 
"is_translation_enabled": false, 
"utc_offset": -14400, 
"statuses_count": 376, 
"description": "Open source data hound. Leader of the FLOSSmole project. Professor of Computing Sciences at Elon University.", 
"friends_count": 82, 
"location": "Elon, NC", 
"profile_link_color": "038543", 
"profile_image_url": "http://pbs.twimg.com/profile_images/3677035734/d8853be8c304729610991194846c49ba_normal.jpeg", 
"following": false, 
"geo_enabled": false, 
"profile_banner_url": "https://pbs.twimg.com/profile_banners/986601/1368894408", 
"profile_background_image_url": "http://pbs.twimg.com/profile_background_images/772436819/b7f7b083e42c9150529fb13971a52528.png", 
"name": "megan squire", 
"lang": "en", 
"profile_background_tile": false, 
"favourites_count": 64, 
"screen_name": "MeganSquire0", 
"notifications": false, 
"url": "http://t.co/dBQNKhR6jY", 
"created_at": "Mon Mar 12 05:01:55 +0000 2007", 
"contributors_enabled": false, 
"time_zone": "Eastern Time (US & Canada)", 
"protected": false, 
"default_profile": false, 
"is_translator": false}, 
"geo": null, 
"in_reply_to_user_id_str": null, 
"lang": "is", 
"created_at": "Wed Dec 10 01:09:00 +0000 2014", 
"in_reply_to_status_id_str": null, 
"place": null}

每个 JSON 对象不仅包括关于推文本身的事实,例如文本、日期和发送时间,它还包括关于发布推文的人的大量信息。

小贴士

水合过程会生成关于单条推文的大量信息——而且这是一个包含 1300 万条推文的数据集。在你准备在本章末尾水合整个 Ferguson 数据集时,请记住这一点。

第三步 – 数据清洗

目前,我们已经准备好开始清理 JSON 文件,提取我们希望保存在长期存储中的每条推文的详细信息。

创建数据库表

由于我们的问题只关心 URL,因此我们实际上只需要提取这些 URL 和推文 ID。然而,为了练习数据清洗,并且为了能够将此练习与我们在第七章中使用 sentiment140 数据集时做的工作进行对比,RDBMS 清洗技术,我们设计了一组小型数据库表,如下所示:

  • 一个 tweet 表,专门存储推文的信息

  • 一个 hashtag 表,存储哪些推文引用了哪些话题标签的信息

  • 一个 URL 表,存储哪些推文引用了哪些 URL 的信息

  • 一个 mentions 表,包含有关哪些推文提到了哪些用户的信息

这与我们在第七章中设计的结构类似,RDBMS 清洗技术,不同的是,在那个例子中我们必须从推文文本中解析出自己的话题标签、URL 和用户提及。使用 twarc 工具无疑为我们完成本章的工作节省了很多精力。

创建这四个表的 CREATE 语句如下:

CREATE TABLE IF NOT EXISTS ferguson_tweets (
  tid bigint(20) NOT NULL,
  ttext varchar(200) DEFAULT NULL,
  tcreated_at varchar(50) DEFAULT NULL,
  tuser bigint(20) DEFAULT NULL,
  PRIMARY KEY (tid)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;

CREATE TABLE IF NOT EXISTS ferguson_tweets_hashtags (
  tid bigint(20) NOT NULL,
  ttag varchar(200) NOT NULL,
  PRIMARY KEY (tid, ttag)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS ferguson_tweets_mentions (
  tid bigint(20) NOT NULL,
  tuserid bigint(20) NOT NULL,
  tscreen varchar(100) DEFAULT NULL,
  tname varchar(100) DEFAULT NULL,
  PRIMARY KEY (tid,tuserid)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS ferguson_tweets_urls (
  tid bigint(20) NOT NULL,
  turl varchar(200) NOT NULL,
  texpanded varchar(255) DEFAULT NULL,
  tdisplay varchar(200) DEFAULT NULL,
  PRIMARY KEY (tid,turl)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

有一点需要注意,推文表是使用utf8mb4字符集创建的。这是因为推文中可能包含 UTF-8 范围内非常高的字符。实际上,这些推文中的某些字符将需要比 MySQL 原生 UTF-8 字符集所能容纳的 3 字节限制更多的空间。因此,我们设计了主要的推文表,使其能够使用 MySQL 的 utf8mb4 字符集,该字符集在 MySQL 5.5 或更高版本中可用。如果你正在使用比这个版本更旧的 MySQL,或者由于其他原因无法访问 utf8mb4 字符集,你可以使用 MySQL 较旧的 UTF-8-general 字符集,但需要注意,某些表情符号可能会导致编码错误。如果确实遇到此错误,MySQL 可能会显示关于错误 1366 和不正确的字符串值的消息,当你尝试INSERT记录时。

现在每个表格都已创建,我们可以开始选择并加载数据了。

在 Python 中填充新表格

以下 Python 脚本将加载 JSON 文件,提取我们感兴趣的字段的值,并填充之前描述的四个表格。关于此脚本,还有一些额外的重要事项,我将一一讲解。

这个脚本确实需要安装MySQLdb Python 模块。作为 Canopy Python 用户,这些模块可以通过包管理器轻松安装。只需在包管理器中搜索MySQLdb,然后点击安装:

#jsonTweetCleaner.py
import json
import MySQLdb

# Open database connection
db = MySQLdb.connect(host="localhost",\
    user="username", \
    passwd="password", \
    db="ferguson", \
    use_unicode=True, \
    charset="utf8")
cursor = db.cursor()
cursor.execute('SET NAMES utf8mb4')
cursor.execute('SET CHARACTER SET utf8mb4')
cursor.execute('SET character_set_connection=utf8mb4')

# open the file full of json-encoded tweets
with open('tweets_1000.json') as f:
    for line in f:
        # read each tweet into a dictionary
        tweetdict = json.loads(line)

        # access each tweet and write it to our db table
        tid    = int(tweetdict['id'])
        ttext  = tweetdict['text']
        uttext = ttext.encode('utf8')
        tcreated_at = tweetdict['created_at']
        tuser  = int(tweetdict['user']['id'])

        try:
            cursor.execute(u"INSERT INTO ferguson_tweets(tid, 
ttext, tcreated_at, tuser) VALUES (%s, %s, %s, %s)", \
(tid,uttext,tcreated_at,tuser))
            db.commit() # with MySQLdb you must commit each change
        except MySQLdb.Error as error:
            print(error)
            db.rollback()

        # access each hashtag mentioned in tweet
        hashdict = tweetdict['entities']['hashtags']
        for hash in hashdict:
            ttag = hash['text']
            try:
                cursor.execute(u"INSERT IGNORE INTO 
ferguson_tweets_hashtags(tid, ttag) VALUES (%s, %s)",(tid,ttag))
                db.commit()
            except MySQLdb.Error as error:
                print(error)
                db.rollback()

        # access each URL mentioned in tweet
        urldict = tweetdict['entities']['urls']
        for url in urldict:
            turl      = url['url']
            texpanded = url['expanded_url']
            tdisplay  = url['display_url']

            try:
                cursor.execute(u"INSERT IGNORE INTO  ferguson_tweets_urls(tid, turl, texpanded, tdisplay) 
VALUES (%s, %s, %s, %s)", (tid,turl,texpanded,tdisplay))
                db.commit()
            except MySQLdb.Error as error:
                print(error)
                db.rollback()

        # access each user mentioned in tweet
        userdict = tweetdict['entities']['user_mentions']
        for mention in userdict:
            tuserid = mention['id']
            tscreen = mention['screen_name']
            tname   = mention['name']

            try:
                cursor.execute(u"INSERT IGNORE INTO 
ferguson_tweets_mentions(tid, tuserid, tscreen, tname) 
VALUES (%s, %s, %s, %s)", (tid,tuserid,tscreen,tname))
                db.commit()
            except MySQLdb.Error as error:
                print(error)
# disconnect from server
db.close()

提示

关于 JSON 表示的推文中每个字段的更多信息,Twitter API 文档非常有帮助。有关用户、实体以及推文中的实体部分的内容,尤其是在规划从 JSON 推文中提取哪些字段时,文档中的这些部分特别有指导意义。你可以通过dev.twitter.com/overview/api/开始查阅文档。

一旦运行此脚本,四个表格将填充数据。在我的 MySQL 实例中,运行上述脚本并对ids_1000.txt文件进行操作后,我在推文表中得到了 893 行数据;在标签表中得到了 1,048 行数据;在用户提及表中得到了 896 行数据;在 URL 表中得到了 371 行数据。如果你的某些表格行数较少,检查是否是因为某些推文被删除了。

第四步 —— 简单的数据分析

假设我们想要了解在 Ferguson 数据集中,哪些网页域名被链接得最多。我们可以通过提取存储在ferguson_tweets_urls表中tdisplay列的 URL 域名部分来回答这个问题。对于我们的目的,我们将 URL 中第一个斜杠(/)之前的部分视为感兴趣的部分。

以下 SQL 查询为我们提供了域名和引用该域名的帖子数:

SELECT left(tdisplay,locate('/',tdisplay)-1) as url, 
  count(tid) as num
FROM ferguson_tweets_urls
GROUP BY 1 ORDER BY 2 DESC;

这个查询的结果是一个数据集,看起来大致如下(在 1,000 行样本数据上运行):

url num
bit.ly 47
wp.me 32
dlvr.it 18
huff.to 13
usat.ly 9
ijreview.com 8
latimes.com 7
gu.com 7
ift.tt 7

这段数据集片段仅展示了前几行,但我们已经可以看到一些更受欢迎的结果是 URL 缩短服务,比如bit.ly。我们还可以看到,我们可以通过使用显示列而不是主 URL 列,轻松去除所有由 Twitter 自有的短链接服务t.co创建的缩短 URL。

在接下来的章节中,我们可以使用这些计数来构建一个条形图,方式与我们在第九章中构建简单图表的方式类似,Stack Overflow 项目

第五步 - 可视化数据

要构建一个小的 D3 启用图表,我们可以遵循与第九章中Stack Overflow 项目类似的流程,在那个章节里我们制作了一个 PHP 脚本来查询数据库,然后我们的 JavaScript 使用结果作为条形图的实时输入。或者,我们可以使用 Python 生成 CSV 文件,让 D3 从这些结果中生成图表。由于我们在上一章已经使用过 PHP 方法,这里就用 CSV 文件方法,换个口味。这也是一个继续使用 Python 的好理由,因为我们已经在本章中使用了这个语言。

以下脚本连接到数据库,选择出使用最多的前 15 个 URL 及其计数,并将整个内容写入 CSV 文件:

import csv
import MySQLdb

# Open database connection
db = MySQLdb.connect(host="localhost",
    user="username", 
    passwd="password", 
    db="ferguson", 
    use_unicode=True, 
    charset="utf8")
cursor = db.cursor()

cursor.execute('SELECT left(tdisplay, LOCATE(\'/\', 
    tdisplay)-1) as url, COUNT(tid) as num 
    FROM ferguson_tweets_urls 
    GROUP BY 1 ORDER BY 2 DESC LIMIT 15')

with open('fergusonURLcounts.tsv', 'wb') as fout:
    writer = csv.writer(fout)
    writer.writerow([ i[0] for i in cursor.description ])
    writer.writerows(cursor.fetchall())

一旦我们拥有了这个 CSV 文件,我们就可以将它输入到一个标准的 D3 条形图模板中,看看它的效果。以下内容可以命名为buildBarGraph.html之类的:

注意

确保你的本地文件夹中有 D3 库,就像在前几章中一样,另外还要有你刚刚创建的 CSV 文件。

<!DOCTYPE html>
<meta charset="utf-8">
<!-- 
this code is modeled on mbostock's 
"Let's Make a Bar Chart" D3 tutorial 
available at http://bl.ocks.org/mbostock/3885304
My modifications:
* formatting for space
* colors
* y axis label moved
* changed variable names to match our data
* loads data via CSV rather than TSV file
-->

<style>
.bar {fill: lightgrey;}
.bar:hover {fill: lightblue;}
.axis {font: 10px sans-serif;}
.axis path, .axis line {
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;
}
.x.axis path {display: none;}
</style>
<body>
<script src="img/d3.min.js"></script>
<script>

var margin = {top: 20, right: 20, bottom: 30, left: 40},
    width = 960 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;

var x = d3.scale.ordinal()
    .rangeRoundBands([0, width], .1);

var y = d3.scale.linear()
    .range([height, 0]);

var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom");

var yAxis = d3.svg.axis()
    .scale(y)
    .orient("left");

var svg = d3.select("body").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

d3.csv("fergusonURLcounts.csv", type, function(error, data) {
  x.domain(data.map(function(d) { return d.url; }));
  y.domain([0, d3.max(data, function(d) { return d.num; })]);

  svg.append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + height + ")")
      .call(xAxis);

  svg.append("g")
      .attr("class", "y axis")
      .call(yAxis)
    .append("text")
      .attr("transform", "rotate(-90)")
      .attr("y", 6)
      .attr("dy", "-3em")
      .style("text-anchor", "end")
      .text("Frequency");

  svg.selectAll(".bar")
      .data(data)
    .enter().append("rect")
      .attr("class", "bar")
      .attr("x", function(d) { return x(d.url) ; })
      .attr("width", x.rangeBand())
      .attr("y", function(d) { return y(d.num); })
      .attr("height", function(d) { return height - y(d.num); });
});

function type(d) {
  d.num = +d.num;
  return d;
}

</script>
</body>
</html>

结果的条形图看起来就像这里展示的那样。再强调一次,记得我们使用的是测试数据集,因此这些数字非常小:

第五步 - 可视化数据

使用我们的 CSV 文件在 D3 中绘制的简单条形图。

第六步 - 问题解决

由于数据可视化不是本书的主要目的,我们并不太关注这一部分的图表有多复杂,简单来说,费格森数据集里还有很多有趣的模式等着我们去发现,不仅仅是哪些 URL 被点击得最多。既然你已经知道如何轻松地下载和清理这个庞大的数据集,也许你可以发挥想象力,去揭示这些模式。当你向公众发布你的发现时,记得不要发布推文本身或其元数据。但如果你的问题需要,你可以发布推文的 ID,或者它们的一个子集。

将此过程转移到完整(非测试)表格中

就像在第九章中提到的堆栈溢出项目一样,我们制作了测试表,以便在一个无压力的环境中开发我们的项目,并收集可管理数量的推文。当你准备好收集完整的推文列表时,做好花费一些时间的准备。Twitter 的速率限制将会生效,twarc 也会运行很长时间。Ed Summers 在这篇博客文章中指出,运行 Ferguson 推文大约需要一周的时间:inkdroid.org/journal/2014/11/18/on-forgetting/。当然,如果你小心操作,你只需要运行一次。

你可以做的另一件事来加速推文 ID 的水合作用时间是与其他人合作。你可以将推文 ID 文件分成两半,各自处理自己部分的推文。在数据清理过程中,确保你将两者都INSERT到同一个最终数据库表中。

以下是我们将遵循的步骤,以便将我们的项目从收集 1,000 条推文样本更改为收集完整的推文集合:

  1. 按照以下方式清空ferguson_tweetsferguson_tweets_hashtagsferguson_tweets_mentionsferguson_tweets_urls表:

    TRUNCATE TABLE ferguson_tweets;
    TRUNCATE TABLE ferguson_tweets_hashtags;
    TRUNCATE TABLE ferguson_tweets_mentions;
    TRUNCATE TABLE ferguson_tweets_urls;
    
  2. 按照以下方式在完整的ids.txt文件上运行 twarc,而不是在ids_1000.txt文件上运行:

    twarc.py --consumer_key abcd --consumer_secret abcd --access_token abcd --access_token_secret abcd --hydrate ids.txt > tweets.json
    
    
  3. 重新运行jsonTweetCleaner.py Python 脚本。

到这时,你将拥有一个已清理的数据库,里面充满了推文、标签、提及和网址,准备进行分析和可视化。由于现在每个表中有更多的行,请注意,每个可视化步骤的运行时间可能会更长,这取决于你执行的查询类型。

总结

在这个项目中,我们学会了如何根据推文的 ID 重建推文列表。首先,我们找到符合 Twitter 最新服务条款的高质量存档推文数据。我们学会了如何将其拆分成一个足够小的集合,适用于测试目的。接着,我们学会了如何使用 Twitter API 和 twarc 命令行工具将每条推文水合成其完整的 JSON 表示。然后,我们学会了如何在 Python 中提取 JSON 实体的部分内容,并将字段保存到 MySQL 数据库中的新表中。最后,我们运行了一些简单的查询来计算最常见的网址,并使用 D3 绘制了一个条形图。

在本书中,我们已经学习了如何执行各种数据清理任务,包括简单的和复杂的。我们使用了多种语言、工具和技术来完成任务,在这个过程中,我希望你能够完善现有技能,并学到许多新技能。

此时,我们的最终晚宴已圆满结束,你现在准备好在你充实而又非常干净的数据科学厨房中开始自己的清理项目了。你应该从哪里开始?

  • 你喜欢比赛和奖品吗?Kaggle 在他们的网站上举办频繁的数据分析比赛,kaggle.com。你可以单独工作,也可以作为团队的一部分。许多团队需要干净的数据,这也是你贡献力量的绝佳方式。

  • 如果你是一个更偏向于公共服务的人,我建议你去看看数据学院(School of Data)?他们的网址是 schoolofdata.org,他们提供课程和数据探险活动,来自世界各地的专家和业余爱好者共同解决使用数据的现实问题。

  • 为了进一步扩展你的数据清洗实践,我强烈推荐你亲自去摸索一些公共可用的数据集。KDnuggets 提供了一个很好的数据集列表,包含一些列表中的列表,网址是:www.kdnuggets.com/datasets/

  • 你喜欢第九章,Stack Overflow 项目中的 Stack Overflow 示例吗?Meta Stack Exchange 是一个专门讨论 StackExchange 网站工作方式的网站,网址是 meta.stackexchange.com。用户们讨论了数百个关于如何查询 Stack Overflow 数据以及如何处理这些数据的精彩想法。或者,你也可以贡献自己的一份力量,参与 Stack Overflow 上与数据清洗相关的大量问题讨论。最后,还有一些与数据清洗相关的其他 Stack Exchange 网站,其中一个有用的网站是 Open Data Stack Exchange,网址是 opendata.stackexchange.com

  • Twitter 数据目前非常受欢迎。如果你喜欢处理 Twitter 数据,可以考虑将我们的第十章,Twitter 项目,提升到一个新的层次,通过提出和回答你自己的问题,分析那些公开可用的推文集合。或者,你也可以收集和整理一组属于你自己的推文 ID。如果你在某个感兴趣的话题上建立干净的推文 ID 集合,你可以将其分发给研究人员和其他数据科学家,他们会非常感激。

祝你在数据清洗的冒险中好运,祝好胃口!

posted @ 2025-07-20 11:32  绝不原创的飞龙  阅读(3)  评论(0)    收藏  举报