上一篇:R语言-分析与绘图(1):使用tidyverse工具包处理数据

并不是所有网站都会提供可下载的数据文件,因此在某些场景下需要我们使用rvest工具包去爬取网页上展示出来的数据并整理成R或者说tidyverse可以处理的数据格式。可以理解成实现简单的爬虫。

rvest工具包

官方文档对于rvest的介绍非常简洁:

  • rvest帮助你从网页中抓取信息。它被设计成与magrittr一起工作,使其更容易表达常见的web抓取任务,灵感来自于像beautiful soup这样的库。

虽然没听说过magrittr这个东西,但是beautiful soup我是听说过的。所以在我的理解下,rvest大抵就是一种html解析器。它独立于上一次的tidyverse,所以需要单独安装(也需要安装其依赖的xml2)然后导入:

1
2
3
install.packages(c("rvest", "xml2"))
library(tidyverse)
library(rvest)

网站的页面一般使用html(超文本标记语言)书写,为了浏览器能成功解析并展示内容,html的各个版本有着非常乱七八糟规范统一的格式要求,不同的内容在代码中使用标签进行分块。这种特性为各种工具解析html提供了非常大的便利。

为了抓取网页文件及其中某些区域(如页面上显示的表格)的信息,除了复制粘贴、手动输入这种非常低效的办法,我们也可以选择使用rvest等工具定位到html的固定代码块并按照规则进行解析。

爬取网页文件及其中的表格

我们以维基百科-奥斯卡学院奖条目为例,尝试获取历年获奖电影的表格。该表格在网页上显示如下:

为了完成这个目标,我们需要用到rvest的三个比较关键的方法:read_html()html_nodes()html_table()

read_html()

read_html()的作用是把页面源代码html文件读取成一个列表,这个列表中存储着页面中不同的对象。我们使用以下示例代码爬取上述网页html中的对象列表。

1
2
3
4
5
6
# 页面url
url <- "https://en.wikipedia.org/wiki/List_of_Academy_Award-winning_films"
# 读取页面源码
wikipage <- read_html(url)
# 用字符串形式展示读取内容的格式
str(wikipage)

需要注意的是,访问维基百科需要使用代理以绕开GFW,请自行百度“在终端中使用代理的方法”或者在代理软件中直接设置系统代理+全局模式。否则,终端或者jupyter会报错:

  • Error in open.connection(x, “rb”): LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to en.wikipedia.org:443.

备用的解决方案是下载我上传的压缩包并把其中的所有文件放到代码相同目录下,里面有本文需要的所有html文件;然后将上文的第二行代码修改为:

1
2
3
4
5
6
# 本地页面源码(相对)路径
url <- "Award.html"
# 读取页面源码
wikipage <- read_html(url)
# 用字符串形式展示读取内容的格式
str(wikipage)

如果成功读取,输出的字符串应该是这样子:

html_nodes()

上文说过,read_html()的作用是把页面源代码html文件读取成一个列表。这个列表中存储的其实是不同的源代码块所表示的对象。这个网页上可能有多个“table”对象,而我们只需要爬取页面上的第一个表格。因此,需要再次解析上文存储的对象列表wikipage,从中搜索关键字为“table”的对象列表,并取出列表当中的第一个对象。

html_nodes()函数的作用是在read_html()读出来的对象列表中通过关键字取出我们需要的对象的列表(比如我们需要找出所有“table”关键字的对象),而我们可以在这样的一个列表中通过下标取出其中的一个DataFrame对象(比如按照我们上文的需求取出第一个)。示例代码如下。

1
2
# 取出对象列表中关键字为“table”的第一个对象
movies_alt <- html_nodes(wikipage, "table")[[1]]

需要注意的是,在R语言中对于list对象而言,单层的中括号形如object[1]并不是其他语言当中按照下标索引取对象的意思,而是指向自己本身。如果我们需要按照下标索引取list中的对象,必须使用双层中括号形如object[[1]](尽管匪夷所思,但是语法和规则本就是应该无条件遵守的东西,否则大家口味喜好不同,全世界可能每个人都在讲不同的语言了)。

html_table()

在上一步,我们使用html_nodes(wikipage, "table")[[1]]读出来的movies_alt是啥?是个对象。但我们做数据分析需要的是DataFrame,所以html_table()函数就是用来把DataFrame的对象转为真正的DataFrame格式。如下所示执行函数,并使用head()展示一下我们历经千辛万苦扒出来的表格。

1
2
3
4
# 把对象转为真正的DataFrame格式
movies <- html_table(movies_alt)
# 展示表格前几行
head(movies)

当然,如果想避免使用那么多中间变量倒来倒去,也可以使用之前学过的管道%>%进行一段前后连贯的操作。

1
2
3
4
5
6
# 取出对象列表中关键字为“table”的第一个对象
movies <- html_nodes(wikipage, "table")[[1]] %>%
# 把对象转为真正的DataFrame格式
html_table()
# 展示表格前几行
head(movies)

这两段代码的输出相同,长这个样子:

整理读取出来的表格

细心的你可能已经发现了,上面读出来的表格有一些小问题:Year,Awards,Nominations三列都是<chr>格式。数据分析的基础是数值型数据,因此我们需要把这几列数字转换为数值格式的数据。之前学过类型转换函数as_numeric()和修改表格列的函数mutate(),这时候就起作用了:我们只需要把对应的列修改成这一列类型转换后的结果即可。

1
2
3
4
5
6
7
8
# 1. 使用管道的写法(推荐用这个!)
movies <- movies %>%
mutate(Awards = as.numeric(Awards))
head(movies)

# 2. 不使用管道的写法(不太好)
movies <- mutate(movies, Awards = as.numeric(Awards))
head(movies)

除了使用mutate函数之外,使用$美元符号也可以直接指定DataFrame中的数据列并进行修改(新知识哦):

1
2
3
# 新方法:$符号直接把某一列拿出来改
movies$Awards <- as.numeric(movies$Awards)
head(movies)

输出长这样:

哈,上面报了一个Warning,字面意思是有的列的值无法转为数值,可能因为它根本不是一个数字。我们看到的这几行里好像没有出现这种情况,但是既然报了警告就说明确实存在这样的数据。这样的数据会被表示为NA,也就是Not Available的意思。为了展示一下,可以尝试输出2017年的最后几行数据:

1
2
3
# 用filter筛选一下2017年的数据
mov_2017 = filter(movies, Year == 2017)
tail(mov_2017)

看看输出,你就知道上面为啥报错了。2017年的电影Flesh and Sand的Awards这一列是一个无法转换为数值的变量。

回去看一下网页上这一行长什么样子:

Fine,好像确实不是一个规范的数字,又带括号又带下标的。我们大可以一通操作手动修改这个数据为0或者1,像下面这样:

1
2
3
4
5
6
7
8
# 找到这个电影的索引
idx <- summarize(movies, which(Film == "Flesh and Sand (Carne y arena)"))
# 索引转为数值,把这一行的Awards修改为1
movies$Awards[[as.numeric(idx)]] <- 1
# 重新筛选一下2017年的电影
mov_2017 = filter(movies, Year == 2017)
# 看一下尾部几行
tail(mov_2017)

结果是ok的,但是那样的操作既费事又没有意义,所以我们干脆用一种叫做na.omit()的方法把所有有NA数值的行都去掉。

1
2
table <- na.omit(mov_2017)
tail(table)

最后让我们来尝试使用管道把上面这些代码封装一下,不要用那么多的中间变量倒来倒去。尝试一行一行理解这些代码,尝试对照着写一遍,并且尝试自己独立写一遍。

1
2
3
4
5
6
7
movies <- html_nodes(wikipage, "table")[[1]]  %>%
html_table() %>%
mutate(Awards = as.numeric(Awards),
Nominations = as.numeric(Nominations)) %>%
na.omit()

head(movies)

变通地使用之前学过的方法

还记不记得之前学过的summarize方法summarize()函数的输出是一个DataFrame,这一点要切记。里可以引用函数which.max()which.min(),当时我们还不会$直接引用DataFrame里的列。今天我们学了这个方法后,这两个方法可以作为返回整数的函数使用了——也就是说,输入一列,返回一个整数。比如下面这行运行的结果直接就是输出47.

1
which.max(movies$Nominations)

再结合我们之前学的操作DataFrame(或者DataFrame的高级版——tibble,二者差别不大)的方法,我们可以用表[行, 列]的方式输出我们需要的信息啦。

1
2
# 输出movies表里Nominations这一列值最大的这一行的所有列(记得留逗号)
movies[which.max(movies$Nominations),]

当然也可以用filter()筛选我们想要的子集,在这里只写管道的写法了,以后都尽量用管道来完成多步操作,这样会比较符合人类思考的逻辑(拿来一个东西,对它先进行某操作,再进行某操作)。我们试试在原始数据集中筛选2018年的子集。

1
2
3
4
5
6
7
movies2018 <- movies %>%
# 先把年份转换为数值
mutate(Year = as.numeric(Year)) %>%
# 再过滤一下,留2018年的
filter(Year == 2018)

head(movies2018)

爬取更复杂的表格

这个表格只提供了本地的html源码,先老样子读取表格。

1
2
3
4
5
6
7
8
9
# 设置本地html路径
url="MovieBudgets.html"
# 读取html文件
m_numbers <- read_html(url)
# 把第一个表格关键字的对象转为DataFrame
table <- html_nodes(m_numbers,"table")[[1]] %>%
# 下面这句里加了fill参数为TRUE,意思是自动用NA填充不完整的行
html_table(fill=TRUE)
head(table)

看看爬出来的表格结构是啥。可以用之前的summary(table),也可以用str(table)用字符串形式展示结构。后者输出结果长这样:

不管你用了哪种方法,应该都能看到第一列数值是没有名字的。我们可以用names()或者colnames()函数为第一列分配一个名字“Name”。

1
2
3
4
5
# option 1
names(table)[1] <- "Number"
# option 2
colnames(table)[1] <- "Number"
head(table)

在字符串中搜索数字

接下来的任务是想办法把那几列美元单位的字符串转换为数值。tidyverse也贴心地提供了一个parse_number()方法,可以对它输入一个字符串,它能帮你找出这段字符串里第一段数字(比如输入@#abc1234$%^def5678ghi,会输出1234)。这刚好能满足我们的需求。

1
2
3
4
5
6
# 为这个管道输入table
table_1 <- table %>%
# 修改表格:DomesticGross和ProductionBudget两列转为数字
mutate(DomesticGross = parse_number(DomesticGross),
ProductionBudget = parse_number(ProductionBudget))
head(table_1)

再重申一下,为啥一定要转为数字呢,因为我们要做数据的分析运算啥的,字符串做不了这个。比如我想求一下电影票房收入和国内总收入的比率,这时候才可以计算:

1
2
3
# 在最后插一列呗
table_1 <- mutate(table_1, "DomesticRatio" = ProductionBudget / DomesticGross)
head(table_1)

创建一个频率表(frequency table)

n()是计算频率的函数,使用形如group_by(xxx) %>% summarize(freq = n())的管道函数可以很快捷地生成一列名为freq的xxx频率的表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 本地html路径
url="BigFiveAcademyAwardwinners.html"
# 读取html
m_numbers <- read_html(url)
# 转换为Data Frame数据表(这次我们需要的表是第二个)
Table <- html_nodes(m_numbers,"table")[[2]] %>%
html_table(fill=TRUE)
head(Table)
# 对Table按照Best Actress进行分组,建立频率表
BestActress_df <- Table %>%
group_by(`Best Actress`) %>%
mutate(summarise(n = n())

head(BestActress_df)

按照某一列进行升/降序排序

终于来到了“我用excel一下就做出来了”的环节,问题是excel没法爬网页上的table啊 🐶

使用arrange()函数可以快捷地对按照某一列进行排序。这个地方注意有个坑:group_by()函数里面填写的列名前后要加重音记号 ` (键盘左上角那个符号),我们在下面代码中尝试按照上面计算的频率进行升序排序。

1
2
3
4
5
6
7
8
9
10
BestActress_df <- Table %>%
# 记得加``
group_by(`Best Actress`) %>%
# 插入一列频率
mutate(Freq = n()) %>%
# 按照频率进行升序排序,降序排序是arrange(desc(Freq))
arrange(Freq)

# 这次改成看屁股
tail(BestActress_df)

最后一列可以看出来,是升序排序了。

画个图吧?

上一篇里好像提了一嘴ggplot2,这是出自tidyverse作者之手的绘图工具包。我们先不展开讲,先欣赏一下代码和结果(我会写一点简单的注释,但是这个工具在后面的notes中会展开聊一下)

1
2
3
4
5
6
7
8
9
# 第一行是使用reorder函数对原来的表依照Freq进行重新排序,确定x、y轴的含义
ggplot(BestActress,aes(x = reorder(`Best Actress`, Freq), y=Freq)) +
# 设置外观参数
geom_bar(stat='identity',fill='darkgreen') +
# 坐标轴上的文字标记
coord_flip() +
labs(x = "Best Actress", y = "Nominations")

ggsave('result.pdf')

我觉得比Excel画的图显得Professional!

下一篇:R语言-分析与绘图(3):可视化入门