作者 | Joel Grus
译者 | cloverErna
第九章 获取数据
9.1 stdin 和 stdout
9.2 读取文件
9.2.1 文本文件基础
9.2.2 分割的(delimited)文件
9.3 网络抓取
9.3.1 HTML和解析方法
9.3.2 例:密切关注国会
9.4 使用API
9.4.1 JSON(和XML)
9.4.2 使用无验证的API
9.4.3 寻找API
9.5 案例:使用 Twitter API
9.6 延伸学习
写作本书我用了三个月的时间;构思只用了三分钟;而收集书中的数据,则用了我的一生。
——F. 斯科特 · 菲兹杰拉德
为了成为一名数据科学家,你需要数据。事实上,作为数据科学家,你会花很大一部分时间来获取、清理和转换数据。必要时,你总可以自己输入数据(或者可以让你的助手来做),但通常这样做比较浪费时间。本章我们来看看利用 Python 获取数据并得到正确格式的不同方法。
9.1 stdin和stdout
如果在命令行运行 Python 脚本,你可以用 sys.stdin 和 sys.stdout 以管道(pipe)方式传递数据。例如,以下脚本按行读入文本,然后输出与一个正则表达式匹配的行:
然后对收到的行计数并输出计数结果:
你可以用这种方法来计数文件中有多少行包含数字。在 Windows 中,你可以用:
而在 Unix 系统中,你可以用:
“|”运算符是个管道字符,它的意思是“使用左边命令的输出作为右边命令的输入”。可以使用这种方法精心设计数据处理的管道。
注意
如果你使用的是 Windows,你可以在这行命令中去掉所包含的 python 部分:
如果你在Unix系统上,那么这样做需要更多的步骤。首先添加一个“Shebang”作为脚本#!/usr/bin/env python的第一行。然后,在命令行中,使用chmod x egrep.py使文件可执行。
类似地,下面的这个脚本计算了单词的数量并给出了最常用的单词:
之后你可以做这样的事情:
(如果你正在使用Windows,那么使用type而不是cat。)
注意
如果你是个 Unix 编程老手,你可能会很熟悉系统里许多内置的命令行工具 (比如 egrep),但你自己从零开始创建这些工具或许更好一些。毕竟,用到的时候刚好知道,总是好的。
9.2 读取文件
可以显式地用代码来读写文件。用 Python 处理文件非常简便。
9.2.1 文本文件基础
处理文本文件的第一步是通过 open 命令来获取一个文件对象:
因为非常容易忘记关闭文件,所以你应该在 with 程序块里操作文件,这样在结尾处文件会被自动关闭:
如果需要读取一个完整的文本文件,可以使用 for 语句对文件的行进行迭代:
按这种方法得到的每一行会用换行符来结尾,所以在对读入的行操作之前会经常需要用strip() 来进行处理。
例如,假设你有一个写满电子邮件地址的文件,每个地址一行,你想利用这个文件生成域名的直方图。正确地提取域名的规则有些微妙--如公共后缀列表,但一个好的近似方案是只取出电子邮件地址中 @ 后面的部分。(对于像 joel@mail.
datasciencester.com 这样的邮件地址,会给出错误的答案,但为了本例,我们愿意接受):
9.2.2 分割的(delimited)文件
我们刚刚处理的假想电子邮件地址文件每行只有一个地址。更常见的情况是你会处理每一行包含许多数据的文件。这种文件通常是用逗号分割或tab分割的:每一行有许多字段,用逗号或tab来表示一个字段的结束和另一个字段的开始。
这开始变得复杂了,各字段中带有逗号、tab 和换行符(这是你不可避免地要处理的)。因为这个原因,几乎总是会犯的一个错误是你自己尝试去解析它们。相反,你应该使用Python的csv模块(或者pandas库,或其他一些设计用来读取逗号分隔或制表符分隔的文件的库)。
警告
不要自己解析逗号分隔的文件。 你会把一些特殊情况搞砸的!
如果文件没有头部(意味着你可能想把每一行作为一个列表,这带来的麻烦是你需要知道每一列是什么),你可以使用 csv.reader 对行进行迭代,每一行都会被处理成恰当划分的列表。
例如,如果有这样一个用 tab 划分的股票价格文件:
我们可以用下面的程序块来处理:
如果文件存在头部:
你既可以利用对 read.next() 的初始调用跳过头部的行,也可以利用 csv.DictReader 把每一行读成字典(把头部作为关键字):
即使你的文件缺少头部,你仍可以通过把关键字作为文件名参数传输来使用 DictReader。
同样,你可以用 csv.writer 来写限制的文件:
如果行中的各字段本身包含逗号,csv.writer 可以正确处理。但你自己手动写成的则很可能不会正确处理。比如,如果你尝试这样做:
你最终会得到像下面这样一个csv文件,它看起来像这样:
没人能看懂它的意思。
9.3 网络抓取
另一种获取数据的方法是从网页抓取数据。事实证明,获取网页很容易;但从网页上抓取有意义的结构化信息就不那么容易了。
9.3.1 HTML和解析方法
网络上的页面是由 HTML 写成的,其中文本被(理想化地)标记为元素和它们的属性:
在理想的情况下,所有的网页按语义标记,这对我们很方便,我们可以使用类似这样的规则来提取数据:找到 id 是 subject 的 <p> 元素并返回它所包含的文本。但在真实的世界中,HTML 并不总是具有很好的格式的,更不用说注解了。这意味着如果我们想搞清其含义,需要一些帮助。
为 了 从 HTML 里 得 到 数 据, 我 们 需 要 使 用 BeatifulSoup 库,它对来自网页的多种元素建立了树结构,并提供了简单的接口来获取它们。本书写作时,最新的版本是 Beatiful Soup 4.6.0,我们即将用到的就是这个版本。我们也会用到 requests 库,它与内置在 Python 中的其他方法相比,是一种发起HTTP 请求的更好的方式。
Python 内置的 HTML 解析器是有点严格的,这意味着它并不总是能处理那些没有很好地格式化的 HTML。因此,我们需要使用另外一种解析器,它需要先安装:
确保你处于正确的虚拟环境中,安装库:
为了使用 Beatiful Soup,我们要把一些 HTML 传递给 BeautifulSoup() 函数。在我们的例子中,这些 HTML 是对 requests.get 进行调用的结果:
完成这个步骤之后,我们可以用一些简单的方法得到完美的解析。
通常我们会处理一些 Tag 对象,它们对应于 HTML 页面结构的标签表示。
比如,找到你能用的第一个 <p> 标签(及其内容):
可以对 Tag 使用它的 text 属性来得到文本内容:
另外可以把标签当作字典来提取其属性:
可以一次得到多个标签:
通常你会想通过一个类(class)来找到标签:
此外,可以把这些方法组合起来运用更复杂的逻辑。比如,如果想找出包含在一个 <div>元素中的每一个 <span> 元素,可以这么做:
仅仅上述几个特性就可以帮助我们做很多事。如果你需要做更复杂的事情(或仅仅是出于好奇),那就去查看文档吧。
当然,无论多重要的数据,通常也不会标记成。你需要仔细检查源HTML,通过你选择的逻辑进行推理,并多考虑边界情况来确保数据的正确性。接下来我们看一个例子。
9.3.2 例:密切关注国会
数据科学公司的政策副总裁担心对数据科学行业的潜在监管,并要求你量化国会对这个话题的看法。他特别希望你能找到所有有发布“数据”新闻稿的代表。
在发布时,有一个页面有所有代表网站的链接
如果你“查看来源”,所有网站的链接看起来都像:
让我们从收集从该页面链接到的所有URL开始:
这将返回太多的URL。如果你看它们,我们从http://或者https://开始,中间是某种名字,并以 .house.gov 或者 .house.gov/. 结束。
这是一个使用正则表达式的好地方:
这仍然太多,因为只有435名代表。如果你看一下清单,有很多重复。我们可以用set来克服这些问题:
总是有几个众议院的座位空着,或者可能有一些没有网站的代表。无论如何,这已经足够好了。当我们查看这些网站时,大多数网站都有新闻稿的链接。例如:
请注意,这是一个相对链接,这意味着我们需要记住原始站点。让我们来抓取一下:
注意
通常情况下,像这样随意地爬一个网站是不礼貌的。 大多数网站都会有一个robots.txt文件,该文件表明你可以频繁地抓取站点(以及你不应该抓取哪些路径),但由于这是国会,我们不需要特别礼貌。
如果你通过滚动来查看它们,你将会看到大量/媒体/新闻稿和媒体中心/新闻稿,以及各种其他地址。其中一个URL是https://jayapal.house.gov/media/press-releases.
记住,我们的目标是找出哪些国会议员提到“数据”。“我们会写一个稍微更通用的功能,检查一页新闻稿中是否提到任何给定的术语。
如果你访问该网站并查看源代码,似乎在<p>标签中有来自每个新闻稿的片段,因此我们将使用它作为我们的第一次尝试:
让我们为它写一个快速测试:
最后我们准备找到相关国会议员,并告知他们的姓名给政策副总裁:
当我运行这个时,我得到了大约20名代表的名单。你的结果可能会不同。
注意
如果你查看不同的“新闻稿”页面,它们中的大多数都是分页的,每页只有5或10个新闻稿。 这意味着我们只检索了每位国会议员最近的几份新闻稿。 一个更彻底的解决方案将在页面上迭代并检索每个新闻稿的全文。
9.4 使用API
许多网站和网络服务提供相应的应用程序接口(Application Programming Interface,APIS),允许你明确地请求结构化格式的数据。这省去了你不得不抓取数据的麻烦!
9.4.1 JSON(和XML)
因为 HTTP 是一种转换文本的协议,你通过网络 API 请求的数据需要序列化(serialized)地 转 换 为 字 符 串 格 式。通 常 这 种 串 行 化 使 用JavaScript 对 象 符 号(JavaScript Object Notation,JSON)。JavaScript 对象看起来和 Python 的字典很像,使得字符串表达非常容易解释:
我们可以使用 Python 的 json 模块来解析 JSON。尤其是,我们会用到它的 loads 函数,这个函数可以把一个代表JSON对象的字符串反序列化(deserialize)为 Python 对象:
有时候 API 的提供者可能会不那么友好,只给你提供 XML 格式的响应:
我们也可以仿照从 HTML 获取数据的方式,用 BeautifulSoup 从 XML 中获取数据;更多细节可查阅文档。
9.4.2 使用无验证的API
现在大多数的 API 要求你在使用之前先验证身份。而若我们不愿勉强自己屈就这种政策,API 会给出许多其他的陈词滥调来阻止我们的浏览。因此,先来看一下 GitHub 的API,利用它我们可以做一些简单的无需验证的事情:
此处 repos 是一个 Python 字典的列表,其中每一个字典表示我的 GitHub 账户的一个代码仓库。(可以随意替换成你的用户名,以获取你的代码仓库的数据。你有 GitHub 账号,对吧?)
我们可以使用它来找出一周中最有可能创建哪些月份和天数的存储库。唯一的问题是,响应中的日期是字符串:
Python 本身没有很强大的日期解析器,所以我们需要安装一个:
其中你需要的可能只是 dateutil.parser.parse 函数:
类似地,你可以获取我最后五个代码仓库所用的语言:
通常我们无需在“做出请求而且自己解析响应”这种低层次上使用 API。使用 Python 的好处之一是已经有人建好了库,方便你访问你感兴趣的几乎所有 API。这些库可以把事情做好,为你省下查找 API 访问的诸多冗长细节的麻烦。(如果这些库不能很好地完成任务,或者它们依赖的是对应的 API 已失效的版本,那就会给你带来巨大的麻烦。)
尽管如此,偶尔你还是需要操作你自己的 API 访问库(或者,更常见的,去调试别人不能顺利操作的库),所以了解一些细节是很有好处的。
9.4.3 寻找API
如果你需要一个特定网站的数据,可以查看它的开发者部分或 API 部分的细节,然后以关键词“python <站点名> api”在网络上搜索相应的库。
有Yelp API、Instagram API、Spotify API等库。
如果你想查看有 Python 封装的 API 列表,那么在GitHub上有一个来自Real Python的不错的API列表(https://github.com/realpython/list-of-python-api-wrappers)。
如果最终还是找不到你需要的 API,还是可以通过抓取获得的。这是数据科学家最后的绝招。
9.5 案例:使用Twitter API
Twitter 是一个非常好的数据源。你可以从它得到实时的新闻,可以用它来判断对当前事件的反应,可以利用它找到与特定主题有关的链接。使用 Twitter 可以做几乎任何你能想到的事,只要你能获得它的数据。可以通过它的 API 来获得数据。
为了和 Twitter API 互动,我们需要使用 Twython 库(python -m pip install twython)。实际上有很多 Python Twitter 的库,但这一个是我用过的库中最好用的一个。你也可以尝试一下其他的库。
获取凭据
为了使用 Twitter 的 API,需要先获取一些证明文件(为此你无论如何都要有一个 Twitter的账户,这样你就能成为一个活跃友好的 Twitter #datascience 社区的一部分)。
注意
就像那些所有我不能控制的网站的指令一样,它们会在某个时刻过时,但是现在还是能发挥一段时间的作用的。(尽管在我写作本书的这段时间里,它们至少已经变更过一次了,所以祝你好运!)
以下是步骤:
1. 找到链接 https://apps.twitter.com/。
2. 如果你还没有注册,点击“注册”,并输入你的 Twitter 用户名和密码。
3.单击Apply申请开发人员帐户。
4.请求访问以供你自己使用。
5.填写申请书。它需要填写300字(真的)说明清楚你为什么需要访问数据,所以为了通过审核,你可以告诉他们这本书以及你有多喜欢它。
6.等一段不确定的时间。
7.如果你认识在Twitter上工作的人,给他们发邮件,问他们是否可以加快你的申请。否则,请继续等待。
8.获得批准后,请返回到developer.twitter.com,找到“应用程序”部分,然后单击“创建应用程序。”
9.填写所有必需的字段(同样,如果描述需要额外的字符,你可以讨论这本书以及如何找到它)。
10.单击“创建”。
现在你的应用程序应该有一个“键和令牌”选项卡,其中包含“消费者API公钥”部分,其中列出了“API公钥”和“API密钥”。“注意这些键;你需要它们。(而且,对他们保密!它们就像是密码一样。)
小心
不要分享它们,不要把它们印在书里,也不要把它们记录在 GitHub 公共代码库里。一种简单的方法是把它们存储在不会被签入(checked in)的 credentials.json文件里,而且可以使用 json.loads 取回它们。另一个解决方案是将它们存储在环境变量中,并使用os.environ检索它们。
使用Twython
使用Twitter API最棘手的部分是认证。(事实上,这是使用大量API最棘手的部分。) API提供者希望确保你被授权访问他们的数据,并且你不会超过他们的使用限制。他们还想知道谁在访问他们的数据。
身份验证有点令人痛苦。有一个简单的方法,OAuth 2,当你只想做简单的搜索时就足够了。还有一种复杂的方式,OAuth 1,当你想要执行操作(例如推特)或(特别是对我们)连接到推特流时,这是必需的。
所以我们坚持了更复杂的方式,我们将尽可能多地实现自动化。
首先,你需要API公钥和API密钥(有时分别称为消费公钥和消费密钥)。我可以从环境变量中获得,如果你愿意的话,你可以随时替换它们:
现在我们可以实例化客户端:
提示
在这一点上,你可能想考虑把ACCESS_TOKEN和ACCESS_TOKEN_SECRET保存在安全的地方,这样下一次你就不用经历这严格的过程了。
一旦我们有了一个经过验证的Twython实例,我们就可以开始执行搜索:
如果你运行上面这个,你应该得到一些推文,比如:
这并不那么有趣,主要是因为Twitter搜索API只是向你显示了一些最近的结果。当你在做数据科学时,你经常想要很多推文。这就是流媒体API有用的地方。它允许你连接到一个伟大的Twitter“消防水管”。若要使用它,你需要使用访问令牌进行身份验证。
为了使用Twython访问流API,我们需要定义一个从TwythonStreamer继承并覆盖它的on_success方法,也可能是它的on_error方法:
MyStreamer 会连接到 Twitter 流并等待 Twitter 给它发送数据。它每收到一些数据(在这里,一条推文表示为一个 Python 对象)就传递给 on_success 方法,如果推文是英文的,这个方法会把推文附加到 tweets 列表中,在收集到 1000 条推文后会断开和流的连接。
剩下的工作就是初始化和启动运行了:
它会一直运行下去直到收集 1000条推文为止(或直到遇到一个错误为止),此时就可以着手分析这些推文了。比如,你可以用下面的方法寻找最常见的标签:
每条推文都包含许多数据。你可以自己尝试一下各种方法,或仔细查阅 Twitter API 的文档。
注意
在一个正式的项目中,你可能并不想依赖内存中的列表来存储推文。相反,你可能想把推文保存在文件或者数据库中,这样就可以永久地拥有它们。