如何用R和API免费获取Web数据?
API是获得Web数据的重要途径之一。想不想了解如何用R调用API,提取和整理你需要的免费Web数据呢?本文一步步为你详尽展示操作流程。
权衡
俗话说“巧妇难为无米之炊”。即便你已经掌握了数据分析的十八般武艺,没有数据也是苦恼的事情。“拔剑四顾心茫然”说的大概就是这种情境吧。
数据的来源有很多。Web数据是其中数量庞大,且相对容易获得的类型。更妙的是,许多的Web数据,都是免费的。
在这个号称大数据的时代,你是如何获得Web数据的呢?
许多人会使用那些别人整理好并且发布的数据集。
他们很幸运,工作可以建立在别人的基础上。这样效率最高。
但是不见得每个人都有这样的幸运。如果你需要用到的数据,偏巧没有人整理和发布过,怎么办?
其实,这样的数据数量更为庞大。我们难道对它们视而不见吗?
如果你想到了爬虫,那么你的思考方向是对的。爬虫几乎可以把一切看得见的(甚至是看不见的) Web数据,都统统帮你弄下来。然而编写和使用爬虫是有很高的成本的。包括时间资源、技术能力等。如果面对任何Web数据获取问题,你都不假思索“上大锤”,有时候很可能是“杀鸡用了牛刀”。
在“别人准备好的数据”和“需要自己爬取的数据”之间,还有很宽广的一片地带,这里就是API的天地。
API是什么?
它是Application Programming Interface的缩写。具体而言,就是某个网站,有不断积累和变化的数据。这些数据如果整理出来,不仅耗时,而且占地方,况且刚刚整理好就有过期的危险。大部分人需要的数据,其实都只是其中的一小部分,时效性的要求却可能很强。因此整理储存,并且提供给大众下载,是并不经济划算的。
可是如果不能以某种方式把数据开放出来,又会面对无数爬虫的骚扰。这会给网站的正常运行带来很多烦恼。折中的办法,就是网站主动提供一个通道。当你需要某一部分数据的时候,虽然没有现成的数据集,却只需要利用这个通道,描述你自己想要的数据,然后网站审核(一般是自动化的,瞬间完成)之后,认为可以给你,就立刻把你明确索要的数据发送过来。双方皆大欢喜。
今后你找数据的时候,也不妨先看看目标网站是否提供了API,以避免做无用功。
这个github项目里,有一份非常详尽的列表,涵盖了目前常见的主流网站API资源状况。作者还在不断整理修订,你可以把它收藏起来,慢慢看。
如果我们得知某个网站提供API,并且通过看说明文档,知道了我们需要的数据就在其中,那问题就变成了——该如何通过API来获得数据呢?
下面我们用一个实际的例子,为你全程展示操作步骤。
来源
我们找的样例,是维基百科。
维基百科的API总览,请参考这个页面。
假设我们关心的,是某一个时间段内,指定维基百科文章页面的访问量。
维基百科专门为我们提供了一类数据,叫做度量数据(metrics),其中就涵盖了页面访问次数这个关键值。对应API的介绍页面,在这里。
页面里有一个样例。假设你需要获得2015年10月,爱因斯坦这个词条页面的访问数量,就可以这样调用:
GET http://wikimedia.org/api/rest_v1/metrics/pageviews/per-article/en.wikipedia/all-access/all-agents/Albert_Einstein/daily/2015100100/2015103100
我们可以把GET后面这一长串的网址,输入到浏览器的地址栏,然后回车,看看会得到什么结果。
我们在浏览器里,看到上图中那一长串文字。你可能感觉很奇怪——这是什么玩意儿?
恭喜你,这就是我们需要获得的数据了。只不过,它使用了一种特殊的数据格式,叫做JSON。
JSON是目前互联网上数据交互的主流格式之一。如果你想搞清楚JSON的含义和用法,可以参考这个教程。
我们在浏览器里,初始只能看到数据最开头的一部分。但是里面已经包含了很有价值的内容:
{"items":[{"project":"en.wikipedia","article":"Albert_Einstein","granularity":"daily","timestamp":"2015100100","access":"all-access","agent":"all-agents","views":18860}
这一段里,我们看到项目名称(en.wikipedia),文章标题(Albert Einstein),统计粒度(天),时间戳(2015年10月1日),访问类型(全部),终端类型(全部),以及访问数量(18860)。
我们用滑动条拖拽返回的文本到最后,会看到如下的信息:
{"project":"en.wikipedia","article":"Albert_Einstein","granularity":"daily","timestamp":"2015103100","access":"all-access","agent":"all-agents","views":16380}]}
与10月1日的数据对比,只有时间戳(2015年10月31日)和访问数量(16380)发生了变化。
中间我们跳过的,是10月2日到10月30日之间的数据。存储格式都是一样的,也只是日期和访问量两项数据值在变化。
需要的数据都在这里,你只需要提取出相应的信息,就可以了。但是如果让你手动来做(例如拷贝需要的项,粘贴到Excel中),显然效率很低,而且很容易出错。下面我们来展示一下,如何用R编程环境来自动化完成这一过程。
准备
在正式用R调用API前,我们需要进行一些必要的准备工作。
首先是安装R。
请先到这个网址下载R基础安装包。
R的下载位置有很多。建议你选择清华大学的镜像,可以获得比较高的下载速度。
请根据你的操作系统平台,选择其中对应的版本下载。我用的是macOS版本。
下载得到pkg文件。双击就可以安装。
安装了基础包之后,我们继续安装集成开发环境RStudio。它可以帮助你轻松地以交互方式和R沟通。RStudio的下载地址在这里。
依据你的操作系统情况,选择对应的安装包。macOS安装包为dmg文件。双击打开后,把其中的RStudio.app图标拖动到Applications文件夹中,安装就完成了。
下面我们从应用目录中,双击运行RStudio。
我们先在RStudio的Console中,运行如下语句,安装一些需要用到的软件包:
install.packages("tidyverse")
install.packages("rlist")
安装完毕后,选择菜单里的File->New
,从以下界面中选择 R Notebook。
R Notebook默认提供给我们一个模板,附带一些基础使用说明。
我们尝试点击编辑区域(左侧)代码部分(灰色)的运行按钮。
立即就可以看到绘图的结果了。
我们点击菜单栏上的Preview按钮,来看整个儿代码的运行结果。运行结果会以图文并茂的HTML文件方式展示出来。
熟悉了环境后,我们该实际操作运行自己的代码了。我们把左侧编辑区的开头说明区保留,把其余部分删除,并且把文件名改成有意义的web-data-api-with-R
。
至此,准备工作就绪。下面我们就要开始实际操作了。
操作
实际操作过程中,我们从维基百科上换另外一篇维基文章作为样例,以证明本操作方法的通用性。选择的文章是我们在介绍词云制作时使用过的,叫做“Yes, Minisiter”。这是一部1980年代的英国喜剧。
我们首先在浏览器里尝试一下,能否修改API样例里的参数,来获得“Yes, Minister”文章访问统计数据。作为测试,我们暂时只收集2017年10月1日到2017年10月3日 ,共3天的数据。
相对样例,我们需要替换的内容包括起止时间和文章标题。
我们在浏览器的地址栏输入:
https://wikimedia.org/api/rest_v1/metrics/pageviews/per-article/en.wikipedia/all-access/all-agents/Yes_Minister/daily/2017100100/2017100300
返回结果如下:
数据能够正常返回,下面我们在RStudio中采用语句方式来调用。
注意下面的代码中,程序输出部分的开头会有##
标记,以便和执行代码本身相区别。
一上来,我们就需要设置一下时区。不然后面处理时间数据的时候,会遇到错误。
Sys.setenv(TZ="Asia/Shanghai")
然后,我们调用tidyverse软件包,它是个合集,一次性加载许多我们后面要用到的功能。
library(tidyverse)
## Loading tidyverse: ggplot2
## Loading tidyverse: tibble
## Loading tidyverse: tidyr
## Loading tidyverse: readr
## Loading tidyverse: purrr
## Loading tidyverse: dplyr
## Conflicts with tidy packages ----------------------------------------------
## filter(): dplyr, stats
## lag(): dplyr, stats
这里可能会遇到一些警告内容,不要理会就可以。对咱们的操作毫不影响。
根据前面的例子,我们定义需要查询的时间跨度,并且指定要查找的维基文章名称。
注意与Python不同,R语言中,赋值采用<-
标记,而不是=
。不过R语言其实挺随和,你要是非得坚持用=
,它也能认得,并不会报错。
starting <- "20171001"
ending <- "20171003"
article_title <- "Yes Minister"
根据已经设定的参数,我们就可以生成调用的API地址了。
url <- paste("https://wikimedia.org/api/rest_v1/metrics/pageviews/per-article/en.wikipedia/all-access/all-agents",
article_title,
"daily",
starting,
ending,
sep = "/")
这里我们使用的是paste函数,它帮助我们把几个部分串接起来,最后的sep
指的是链接几个字符串部分时,需要使用的连接符。因为我们要形成的是类似于目录格式的网址数据,所以这里用的是分隔目录时常见的斜线。
我们检查一下生成的url地址是不是正确:
url
## [1] "https://wikimedia.org/api/rest_v1/metrics/pageviews/per-article/en.wikipedia/all-access/all-agents/Yes Minister/daily/20171001/20171003"
检查完毕,结果正确。下面我们需要实际执行GET
函数,来调用API,获得维基百科的反馈数据。
要执行这一功能,我们需要加载另外一个软件包,httr
。它类似于Python中的request软件包,类似于Web浏览器,可以完成和远端服务器的沟通。
library(httr)
然后我们开始调用。
response <-GET(url, user_agent="my@email.com this is a test")
我们看看调用API的结果:
response
## Response [https://wikimedia.org/api/rest_v1/metrics/pageviews/per-article/en.wikipedia/all-access/all-agents/Yes Minister/daily/20171001/20171003]
## Date: 2017-10-13 03:10
## Status: 200
## Content-Type: application/json; charset=utf-8
## Size: 473 B
注意其中的status
一项。我们看到它的返回值为200。以2开头的状态编码是最好的结果,意味着一切顺利;如果状态值的开头是数字4或者5,那就有问题了,你需要排查错误。
既然我们很幸运地没有遇到问题,下面就打开返回内容看看里面都有什么吧。因为我们知道返回的内容是JSON格式,所以我们加载jsonlite
软件包,以便用清晰的格式把内容打印出来。
library(jsonlite)
##
## Attaching package: 'jsonlite'
## The following object is masked from 'package:purrr':
##
## flatten
然后我们打印返回JSON文本的内容。
toJSON(fromJSON(content(response, as="text")), pretty = TRUE)
## {
## "items": [
## {
## "project": "en.wikipedia",
## "article": "Yes_Minister",
## "granularity": "daily",
## "timestamp": "2017100100",
## "access": "all-access",
## "agent": "all-agents",
## "views": 654
## },
## {
## "project": "en.wikipedia",
## "article": "Yes_Minister",
## "granularity": "daily",
## "timestamp": "2017100200",
## "access": "all-access",
## "agent": "all-agents",
## "views": 644
## },
## {
## "project": "en.wikipedia",
## "article": "Yes_Minister",
## "granularity": "daily",
## "timestamp": "2017100300",
## "access": "all-access",
## "agent": "all-agents",
## "views": 578
## }
## ]
## }
可以看到,3天的访问数量统计信息,以及包含的其他元数据,都正确地从服务器用API反馈给了我们。
我们把这个JSON内容存储起来。
result <- fromJSON(content(response, as="text"))
检查一下存储的内容:
result
## $items
## project article granularity timestamp access agent
## 1 en.wikipedia Yes_Minister daily 2017100100 all-access all-agents
## 2 en.wikipedia Yes_Minister daily 2017100200 all-access all-agents
## 3 en.wikipedia Yes_Minister daily 2017100300 all-access all-agents
## views
## 1 654
## 2 644
## 3 578
我们看看解析之后,存储的类型是什么:
typeof(result)
## [1] "list"
存储的类型是列表(list)。可是为了后续的分析,我们希望把其中需要的信息提取出来,组成数据框(dataframe)。方法很简单,使用rlist
这个R包,就可以轻松办到。
library(rlist)
我们需要使用其中的两个方法,一个是list.select
,用来把指定的信息抽取出来;一个是list.stack
,用来把列表生成数据框。
df <- list.stack(list.select(result, timestamp, views))
我们看看结果:
df
## timestamp views
## 1 2017100100 654
## 2 2017100200 644
## 3 2017100300 578
数据抽取是正确的,包括了日期和浏览数量。但是这个日期格式不是标准格式,后面分析会有问题。我们需要做转化。
处理时间日期格式,最好的办法是用lubridate
软件包。我们先调用它。
library(lubridate)
##
## Attaching package: 'lubridate'
## The following object is masked from 'package:base':
##
## date
由于日期字符串后面还有表示时区的两位(这里都是0),我们需要调用stringr
软件包,将其截取掉。然后才能正确转换。
library(stringr)
然后我们开始转换,先用str_sub
函数(来自于stringr
软件包)把日期字符串的后两位抹掉,然后用lubridate
软件包里面的ymd
函数,将原先的字符串转换为标准日期格式。修改后的数据,我们存储回df$timestamp
。
df$timestamp <- ymd(str_sub(df$timestamp, 1, -3))
我们再来看看此时的df
内容:
df
## timestamp views
## 1 2017-10-01 654
## 2 2017-10-02 644
## 3 2017-10-03 578
至此,我们需要的数据都格式正确地保留下来了。
不过,如果为了处理每一篇文章的阅读数量,我们都这样一条条跑语句,效率很低,而且难免会出错。我们把刚才的输入语句整理成函数,后面使用起来会更加方便。
整理函数的时候,我们顺便采用dplyr
包的“管道”(即你会看到的%>%
符号)格式改写一下前面的内容,这样可以省却中间变量,而且看起来更为清晰明确。
get_pv <- function(article_title, starting, ending){
url <- paste("https://wikimedia.org/api/rest_v1/metrics/pageviews/per-article/en.wikipedia/all-access/all-agents",
article_title,
"daily",
starting,
ending,
sep = "/")
df <- url %>%
GET(user_agent="my@email.com this is a test") %>%
content(as="text") %>%
fromJSON() %>%
list.select(timestamp, views) %>%
list.stack() %>%
mutate(timestamp = timestamp %>%
str_sub(1,-3) %>%
ymd())
df
}
我们用新定义的函数,重新尝试一下刚才的API数据获取:
starting <- "20171001"
ending <- "20171003"
article_title <- "Yes Minister"
get_pv(article_title, starting, ending)
## timestamp views
## 1 2017-10-01 654
## 2 2017-10-02 644
## 3 2017-10-03 578
结果正确。
不过如果只是抓取3天的数据,我们这么大费周章就没有意思了。下面我们扩展时间范围,尝试抓取自2014年初至2017年10月10日的数据。
starting <- "20141001"
ending <- "20171010"
article_title <- "Yes Minister"
df <- get_pv(article_title, starting, ending)
我们看看运行结果:
head(df)
## timestamp views
## 1 2015-07-01 538
## 2 2015-07-02 588
## 3 2015-07-03 577
## 4 2015-07-04 473
## 5 2015-07-05 531
## 6 2015-07-06 500
有意思的是,数据的统计并不是从2014年开始,而是2015年7月。这究竟是由于"Yes, Minister"维基文章是2015年7月才发布?还是因为我们调用的API对检索时间范围有限制?抑或是其他原因?这个问题留作思考题,欢迎把你的答案和分析过程分享给大家。
下面,我们把获得的数据用ggplot2
软件包绘制图形。用一行语句,看看几年之内,"Yes,
Minister"维基文章访问数量的变化趋势。
ggplot(data=df, aes(timestamp, views)) + geom_line()
作为一部30多年前的剧集,今天还不断有人访问其维基页面,可见它的魅力。从图中可以非常明显看到几个峰值,你能解释它们出现的原因吗?这将作为今天的另外一道习题,供你思考。
小结
简单回顾一下,本文我们接触到了以下重要知识点:
- 获取Web数据的三种常见方式及其应用场景;
- 常见API的目录资源获取地址和使用方法;
- 如何用R来调用API,并且从服务器反馈结果中抽取关心的数据。
希望读过本文,你能初步掌握上述内容,并且根据文中提供的链接和教程资源拓展学习相关知识。
讨论
你之前利用API获取过Web数据吗?除了R以外,你还使用过哪些API的调用工具?与本文的介绍比起来,这些工具有什么特点?欢迎留言,把你的心得经验分享给大家,我们一起交流讨论。
如果你对我的文章感兴趣,欢迎点赞,并且微信关注和置顶我的公众号“玉树芝兰”(nkwangshuyi)。
如果本文可能对你身边的亲友有帮助,也欢迎你把本文通过微博或朋友圈分享给他们。让他们一起参与到我们的讨论中来。