第二章 数据操作
前面章节已涵盖了R 语言基本语法,特别是让读者训练了向量化编程思维(同时操作一堆数据)、函数式编程思维(自定义函数解决问题+ 泛函式循环迭代)。
R 语言更多的是与数据打交道,本章正式进入tidyverse 系列,将全面讲解‘‘管道流、整洁流’’操作数据的基本语法,包括:数据读写、数据连接、数据重塑,以及各种数据操作。
本章最核心的目的是训练读者的数据思维,那么什么是数据思维?
我的理解最关键的三点是:
(1) 更进一步,将向量化编程思维和函数式编程思维,纳入到数据框或更高级的数据结构中来
比如,向量化编程同时操作一个向量的数据,变成在数据框中操作一列的数据,或者同时操作数据框的多列,甚至分别操作数据框每个分组的多列;函数式编程变成为想做的操作自定义函数(或现成函数),再依次应用到数据框的多个列上,以修改列或做汇总。
(2) 将复杂数据操作分解为若干基本数据操作的能力
复杂数据操作都可以分解为若干简单的基本数据操作:数据连接、数据重塑(长宽变换/拆分合并列)、筛选行、排序行、选择列、修改列、分组汇总。一旦完成问题的梳理和分解,又熟悉每个基本数据操作,用‘‘管道’’ 流依次对数据做操作即可。
(3) 接受数据分解的操作思维
比如,想对数据框进行分组,分别对每组数据做操作,整体来想这是不容易想透的复杂事情,实际上只需做group_by() 分组,然后把你要对一组数据做的操作实现;再比如,across() 同时操作多列,实际上只需把对一列要做的操作实现。这就是数据分解的操作思维,这些函数会帮你分解+ 分别操作+ 合并结果,你只需要关心分别操作的分,它就是一件简单的事情。
很多从C 语言等过来的编程新手,有着根深蒂固地逐个元素for 循环操作、每个计算都得‘‘眼见为实’’ 的习惯,这都是训练数据思维的大忌,是最应该首先摒弃的恶习。
2.1 tidyverse 简介与管道
2.1.1 tidyverse 包简介
tidyverse 包是Hadley Wickham 及团队的集大成之作,是专为数据科学而开发的一系列包的合集,基于整洁数据,提供了一致的底层设计哲学、语法、数据结构。
tidyverse 用” 现代的’’、“优雅的’’ 方式,以管道式、泛函式编程技术实现了数据科学的整个流程:数据导入、数据清洗、数据操作、数据可视化、数据建模、可重现与交互报告。
tidyverse 操作数据的优雅,就体现在:
- 每一步要‘‘做什么’’,就写‘‘做什么’’,用管道依次做下去,得到最终结果
- 代码读起来,就像是在读文字叙述一样,顺畅自然,毫无滞涩
在tidyverse 包的引领下,近年来涌现出一系列具体研究领域的tidy* 版本的包:tidymodels(统计与机器学习)、mlr3verse(机器学习)、rstatix(应用统计)、tidybayes(贝叶斯模型)、tidyquant(金融)、fpp3(时间序列)、tidytext(文本挖掘)、tidygraph(网络图)、sf(空间数据分析)、tidybulk(生信)、sparklyr(大数据)等。
tidyverse 与data.table
tidyverse 操作数据语法优雅、容易上手,但效率与主打高效的data.table 包不可同日而语,处理几G 甚至十几G 的数据,需要用data.table.
但data.table 的语法高度抽象、不容易上手。本书不对data.table 做过多展开,只讲一下基本使用。另一种不错的方案是使用专门的转化包:有不少包尝试底层用data.table,上层用tidyverse语法包装(转化),如dtplyr, tidyfst 等。
2.1.2 管道操作
- 什么是管道操作?
magrittr 包引入了管道操作,能够通过管道将数据从一个函数传给另一个函数,从而用若干函数构成的管道依次变换你的数据。
例如,对数据集mtcars,先按分类变量cyl 分组,再对连续变量mpg 做分组汇总计算均值:
library(tidyverse)
mtcars %>%
group_by(cyl) %>%
summarise(mpg_avg = mean(mpg))
## # A tibble: 3 x 2
## cyl mpg_avg
## <dbl> <dbl>
## 1 4 26.7
## 2 6 19.7
## 3 8 15.1
管道运算符%>% (Windows 快捷键:Shift+Ctrl+M) 的意思是:将左边的运算结果,以输入的方式传给右边函数。若干个函数通过管道链接起来,叫作管道(pipeline)。
x %>% f() %>% g() # 等同于g(f(x))
对该管道示例应该这样理解:依次对数据进行若干操作:先对x 进行f 操作, 接着对结果数据进行g 操作。
管道,也支持base R 函数:
month.abb %>% # 内置月份名缩写字符向量
sample(6) %>%
tolower() %>%
str_c(collapse = "|")
## [1] "jul|apr|jun|aug|jan|mar"
注:R 4.1 增加了同样功能的管道运算符:|>.
使用管道的好处是:
避免使用过多的中间变量;
程序可读性大大增强:
管道操作的过程,读起来就是对原数据集依次进行一系列操作的过程。而非管道操作,读起来与操作的过程是相反的,比如同样实现上例:
str_c(tolower(sample(month.abb, 6)), collapse="|")
- 常用管道操作
- 管道默认将数据传给下一个函数的第1 个参数,且它可以省略
c(1, 3, 4, 5, NA) %>%
mean(., na.rm = TRUE) # "." 可以省略
c(1, 3, 4, 5, NA) %>%
mean(na.rm = TRUE) # 建议写法
这种机制使得管道代码看起来就是:从数据开始,依次用函数对数据施加一系列的操作(变换数据),各个函数都直接从非数据参数开始写即可,而不用再额外操心数据的事情,数据会自己沿管道向前‘‘流动’’。
所以,tidyverse 中的函数都设计为数据作为第1 个参数,自定义的函数也建议这样做。
- 数据可以在下一个函数中使用多次
数据经过管道默认传递给函数的第1 个参数(通常直接省略);若在非第1 个参数处使用该数据,必须用“.” 代替(绝对不能省略),这使得管道作用更加强大和灵活。下面看一些具体实例:
#数据传递给plot 第一个参数作为绘图数据(. 省略),
# 同时用于拼接成字符串给main 参数用于图形标题
c(1, 3, 4, 5) %>%
plot(main = str_c(., collapse=","))
# 数据传递给第二个参数data
mtcars %>% plot(mpg ~ disp, data = .)
# 选择列
iris %>% .$Species # 选择Species 列内容
iris %>% .[1:3] # 选择1-3 列子集
再来看一个更复杂的例子:分组批量建模
mtcars %>%
group_split(cyl) %>% # . 相当于mtcars
map(~ lm(mpg ~ wt, data = .x))
split() 是将数据框mtcars 根据其cyl 列(包含3 个水平的分类变量)分组,得到包含3 个成分的列表;列表接着传递给map(.x, .f) 的第一个参数(直接省略),~ lm(mpg ~ wt, data = .x) 是第二参数.f,为purrr 风格公式写法。
整体来看,实现的是分组建模:将数据框根据分类变量分组,再用map 循环机制依次对每组数据建立线性回归模型。
建议进行区分:. 用于管道操作中代替数据;.x 用于purrr 风格公式(匿名函数)。
2.2 数据读写
2.2.1 数据读写的包与函数
先来罗列一下读写常见数据文件的包和函数,具体使用可查阅其帮助文档。
- readr 包
读写带分隔符的文本文件,如csv 和tsv; 也能读写序列化的R 对象rds,若想保存数据集后续再加载回来,rds 将保存元数据和该对象的状态,如分组和数据类型。
readr 2.0 版本发布,read_csv() 采用vroom 引擎读取性能大大提升,同时支持批量读取文件。
- 读入数据到数据框:read_csv() 和read_tsv()
- 读入欧式格式数据1:read_csv2() 和read_tsv2()
- 读写rds 数据:read_rds() 和write_rds()
- 写出数据到文件:write_csv(), write_tsv(), write_csv2(), write_tsv2()
- 转化数据类型:parse_number(), parse_logical(), parse_factor() 等
- readxl 包
专门读取Excel 文件,包括同一个工作簿中的不同工作表:
- read_excel(): 自动检测xls或xlsx文件
- read_xls(): 读取xls 文件
- read_xlsx(): 读取xlsx 文件
读写Excel 文件好用的包,还有openxlsx
- haven 包
读写SPSS, Stata, SAS 数据:
- 读:read_spss(), read_dta(), read_sas()
- 写:write_spss(), write_stata(), write_sas()
- jsonlite 包
读写JSON 数据,与R 数据结构相互转换:
- 读:read_json(), fromJSON()
- 写:write_json(), toJSON()
- readtext 包
读取全部文本文件的内容到数据框,每个文件变成一行,常用于文本挖掘或数据收集;readtext包还支持读取csv, tab, json, xml, html, pdf, doc, docx, rtf, xls, xlsx 等。
readtext(): 返回数据框,doc_id 列为文档标识,text 列为读取的全部文本内容(1 个字符串)。
library(readtext)
document = readtext("datas/十年一觉.txt")
document
## readtext object consisting of 1 document and 0 docvars.
## # Description: df [1 x 2]
## doc_id text
## <chr> <chr>
## 1 十年一觉.txt "\" “这位公子爷\"..."
2.2.2 数据读写实例
以读取csv 和Excel 文件为例演示,读取其他类型的数据文件,换成其他读取函数即可。
read_csv(file, col_names, col_types, locale, skip, na, n_max, ...)
- file: 数据文件所在相对或绝对路径、网址、压缩包、批量文件路径等
- col_names: 第一行是否作为列名
- skip: 开头跳过的行数
- na: 设置什么值解读为缺失值
- n_max: 读取的最大行数
- col_select: 支持dplyr 选择列语法选择要读取的列
- col_types: 设置列类型3,默认NULL(全部猜测),可为每列单独设置,例如设置3 列的列类型(缩写):coltypes="cnd"
- locale: 设置区域语言环境(时区, 编码方式, 小数标记、日期格式),主要是用来设置所读取数据文件的编码方式,如从默认"UTF-8" 编码改为"GBK" 编码:locale = locale(encoding = "GBK")
- 还有参数comment(忽略的注释标记),skip_empty_rows 等。
read_xlsx(path, sheet, range, col_names, col_types, skip, na, n_max, ...)
- path: 数据文件所在相对或绝对路径
- sheet: 要读取的工作表
- range: 要读取的单元格范围
- col_names: 第一行是否作为列名
- col_types: 设置列类型4,可总体设置一种类型(循环使用)或为每列单独设置,默认NULL (全部猜测)
- 也有参数:skip, na, n_max.
readr 包读取数据的函数,默认会保守猜测各列的列类型。若在读取数据时部分列有丢失信息,则建议先将数据以文本(字符)型读取进来,再用dplyr 修改列类型。
- 读取csv 文件
df = read_csv("datas/六1班学生成绩.csv")
df
## # A tibble: 4 x 6
## 班级 姓名 性别 语文 数学 英语
## <chr> <chr> <chr> <dbl> <dbl> <dbl>
## 1 六1班 何娜 女 87 92 79
## 2 六1班 黄才菊 女 95 77 75
## 3 六1班 陈芳妹 女 79 87 66
## 4 六1班 陈学勤 男 82 79 66
- 批量读取Excel 文件
批量读取的数据文件往往具有相同的列结构(列名、列类型),读入后紧接着需要按行合并为一个数据框。批量读取并合并,道理很简单,总共分三步:
- 获取批量数据文件的路径
- 循环机制批量读取
- 合并成一个数据文件
强大的purrr 包,使得后两步可以同时做,即借助
map_dfr(.x, .f, .id)
将函数.f 依次应用到序列.x 的每个元素返回数据框,再bind_rows 按行合并为一个数据框,.id 可用来增加新列描述来源。
比如,在read_datas 文件夹下有5 个xlsx 文件,每个文件的列名都是相同的:
首先要得到要导入的全部Excel 文件的完整路径,可以任意嵌套,只需将参数recurse 设为TRUE:
files = fs::dir_ls("datas/read_datas", recurse = TRUE, glob = "*.xlsx")
files
## datas/read_datas/六1班学生成绩.xlsx
## datas/read_datas/六3班学生成绩.xlsx
## datas/read_datas/六4班学生成绩.xlsx
## datas/read_datas/六5班学生成绩.xlsx
## datas/read_datas/新建文件夹/六2班学生成绩.xlsx
接着,用map_dfr() 在该路径向量上做迭代,应用read_xlsx() 到每个文件路径,再按行合并。
另外,再多做一步:用set_names() 将文件路径字符向量创建为命名向量,再结合参数.id 将路径值作为数据来源列。
library(readxl)
df = map_dfr(files, read_xlsx)
head(df)
## # A tibble: 6 x 6
## 班级 姓名 性别 语文 数学 英语
## <chr> <chr> <chr> <dbl> <dbl> <dbl>
## 1 六1班 何娜 女 87 92 79
## 2 六1班 黄才菊 女 95 77 75
## 3 六1班 陈芳妹 女 79 87 66
## 4 六1班 陈学勤 男 82 79 66
## 5 六3班 江佳欣 女 80 69 75
## 6 六3班 何诗婷 女 76 53 72
# 若想增加一列表明数据来自哪个文件
df = map_dfr(set_names(files), read_xlsx, .id = " 来源")
head(df)
## # A tibble: 6 x 7
## 来源 班级 姓名 性别 语文 数学 英语
## <chr> <chr> <chr> <chr> <dbl> <dbl> <dbl>
## 1 datas/read_datas/六1班学生成绩~ 六1班 何娜 女 87 92 79
## 2 datas/read_datas/六1班学生成绩~ 六1班 黄才~ 女 95 77 75
## 3 datas/read_datas/六1班学生成绩~ 六1班 陈芳~ 女 79 87 66
## 4 datas/read_datas/六1班学生成绩~ 六1班 陈学~ 男 82 79 66
## 5 datas/read_datas/六3班学生成绩~ 六3班 江佳~ 女 80 69 75
## 6 datas/read_datas/六3班学生成绩~ 六3班 何诗~ 女 76 53 72
files 是文件路径构成的字符向量(未命名,只有索引访问),set_names(files) 是将该字符向量,变成命名字符向量,名字就用元素值;参数.id 定义的新列’’ 来源’’,将使用这些名字。
函数read_xlsx() 的其他控制读取的参数,可直接‘‘作为” map_dfr 参数在后面添加,或改用purrr风格公式形式:
map_dfr(set_names(files), read_xlsx, sheet = 1, .id = " 来源") # 或者
map_dfr(set_names(files), ~ read_xlsx(.x, sheet = 1), .id = " 来源")
若批量Excel 数据是来自同一xlsx 的多个sheet,比如还是上述数据,只是在‘‘学生成绩.xlsx” 的5个sheet 中:
path = "datas/学生成绩.xlsx" # Excel 文件路径
df = map_dfr(set_names(excel_sheets(path)),
~ read_xlsx(path, sheet = .x), .id = "sheet")
head(df)
## # A tibble: 6 x 7
## sheet 班级 姓名 性别 语文 数学 英语
## <chr> <chr> <chr> <chr> <dbl> <dbl> <dbl>
## 1 六1班 六1班 何娜 女 87 92 79
## 2 六1班 六1班 黄才菊 女 95 77 75
## 3 六1班 六1班 陈芳妹 女 79 87 66
## 4 六1班 六1班 陈学勤 男 82 79 66
## 5 六2班 六2班 黄祖娜 女 94 88 75
## 6 六2班 六2班 徐雅琦 女 92 86 72
excel_sheets() 函数作用在该Excel 文件上,提取各个sheet 名字,得到字符向量;然后同样是实现批量读取,只是这次是在sheet 名字的字符向量上循环而已。
另外,readr 2.0 提供了非常简单方法实现批量读取+ 合并csv 文件(列名/列类型相同):
files = fs::dir_ls("datas/read_datas", recurse = TRUE, glob = "*.csv")
df = read_csv(files)
- 写出到一个Excel 文件
用readr 包中的write_csv() 和write_rds(),或writexl 包中的write_xlsx() 可以保存数据到文件。
以写出到Excel 文件为例:
library(writexl)
write_xlsx(df, "datas/output_file.xlsx")
- 批量写出到Excel 文件
比如有多个数据框,存在一个列表中,依次将它们写入文件,需要准备好文件名;在该数据框列表和文件名上,依次应用写出函数write_xlsx(),又不需要返回值,故适合用purrr 包中的walk2()函数:
df = iris %>%
group_split(Species) # 鸢尾花按组分割, 得到数据框列表
files = str_c("datas/", levels(iris$Species), ".xlsx") # 准备文件名
walk2(df, files, write_xlsx)
若要多个数据框分别写入一个Excel 文件的多个sheet,先将多个数据框创建为命名列表(名字将作为sheet 名) ,再用write_xlsx() 写出即可:
df = df %>%
set_names(levels(iris$Species))
write_xlsx(df, "datas/iris.xlsx")
- 保存与载入rds 数据
除了save() 和load() 函数外,下面以导出数据到.rds 文件为例,因为它能保存数据框及其元数据,如数据类型和分组等。
write_rds(iris, "my_iris.rds")
dat = read_rds("my_iris.rds") # 导入.rds 数据
2.2.3 连接数据库
R 操作数据是先将数据载入内存,当数据超过内存限制时,可能会让您束手无策。一种解决办法是,将大数据存放在远程数据库(远程服务器或本地硬盘),然后建立与R 的连接,再从R 中执行查询、探索、建模等。
注意,内存可以应付的数据集,是没有必要这样操作的。
dplyr 是tidyverse 操作数据的最核心包,而dbplyr 包是用于数据库的dplyr 后端,让您能够操作远程数据库中的数据表,就像它们是内存中的数据框一样。安装dbplyr 包时,还会自动安装DBI包,它提供了通用的接口,使得能够使用相同的代码与许多不同的数据库连用。
常见的主流数据库软件:SQL Server, MySQL, Oracle 等都能支持,但还需要为其安装特定的驱动,比如
- RMariaDB 包:连接到MySQL 和MariaDB
- RPostgres 包:连接到Postgres 和Redshift
- RSQLite 包:嵌入SQLite 数据库
- odbc 包:通过开放数据库连接协议连接到许多商业数据库
- bigrquery 包:连接到谷歌的BigQuery
下面以R 连接MySQL 数据库为例,用小数据集演示基本操作。连接其他数据库也是类似的。
(1) 配置MySQL 开发环境
我这里是用的MySQL zip 版+ Navicat(数据库管理工具), 具体操作可参阅(知乎)八咫镜:mysql安装及配置。
(2) 新建MySQL 连接和数据库
在Navicat 新建MySQL 连接,输入连接名(随便起名)和配置MySQL 时设好的用户名及相应密码:
打开该连接,右键新建数据库,数据库名为mydb(随便起名),选择字符编码和排序规则:
(3) 建立R 与MySQL 的连接
先加载RMariaDB 包,再用dbConnect() 函数来建立连接,需要提供数据库后端、用户名、密码、数据库名、主机:
library(RMariaDB)
con = dbConnect(MariaDB(), user = "root", password = "123456",
dbname = "mydb", host = "localhost")
dbListTables(con) # 查看con 连接下的数据表
## [1] "exam"
这表明该连接下还没有数据表。
(4) 创建数据表
在该连接下,若已有MySQL 数据表则直接进入下一步,否则有两种方法创建数据表:
- 在MySQL 端,从Navicat 创建表,可从外部数据文件导入到数据表
- 在R 端,读取数据,再通过函数dbWriteTable() 写入到数据表;若是大数据,可以借助循环逐块地读取和追加写入。
datas = read_xlsx("datas/ExamDatas.xlsx")
dbWriteTable(con, name = "exam", value = datas, overwrite = TRUE)
dbListTables(con)
## [1] "exam"
(5) 数据表引用
用函数tbl() 获取数据表的引用,引用是一种浅拷贝机制,能够不做物理拷贝而使用数据,一般处理大数据都采用该策略。
df = tbl(con, "exam")
df
## # Source: table<exam> [?? x 8]
## # Database: mysql [root@localhost:NA/mydb]
## class name sex chinese math english moral science
## <chr> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 六1班 何娜 女 87 92 79 9 10
## 2 六1班 黄才菊 女 95 77 75 8 9
## 3 六1班 陈芳妹 女 79 87 66 9 10
## 4 六1班 陈学勤 男 82 79 66 9 10
## 5 六1班 陈祝贞 女 76 79 67 8 10
## 6 六1班 何小薇 女 83 73 65 8 9
## # ... with more rows
输出数据表引用,看起来和tibble 几乎一样,主要区别就是它是来自远程MySQL 数据库。
(6) 数据表查询
与数据库交互,通常是用SQL(结构化查询语言),几乎所有的数据库都在使用SQL.
dbplyr 包让R 用户用dplyr 语法就能执行SQL 查询,就像用在R 中操作数据框一样:
df %>%
group_by(sex) %>%
summarise(avg = mean(math, na.rm = TRUE))
## # Source: lazy query [?? x 2]
## # Database: mysql [root@localhost:NA/mydb]
## sex avg
## <chr> <dbl>
## 1 女69.1
## 2 男65.2
普通数据框与远程数据库查询之间最重要的区别是,您的R 代码被翻译成SQL 并在远程服务器上的数据库中执行,而不是在本地机器上的R 中执行。当与数据库一起工作时,dplyr 试图尽可能地懒惰:
- 除非明确要求(接collect()),否则它不会把数据拉到R 中;
- 它把任何工作都尽可能地推迟到最后一刻:把您想做的所有事情合在一起,然后一步送到数据库中。
dbplyr 包还提供了将dplyr 代码翻译成SQL 查询代码的函数show_query(). 可以进一步用于MySQL, 或dbSendQuery(), dbGetQuery():
df %>%
group_by(sex) %>%
summarise(avg = mean(math, na.rm = TRUE)) %>%
show_query()
## <SQL>
## SELECT `sex`, AVG(`math`) AS `avg`
## FROM `exam`
## GROUP BY `sex`
dbGetQuery(con, "SELECT `sex`, AVG(`math`) AS `avg`
FROM `exam`
GROUP BY `sex`")
## sex avg
## 1 女 69.11538
## 2 男 65.20833
最后,关闭R 与MySQL 的连接:
dbDisconnect(con)
2.2.4 关于中文编码
中文乱码是让很多编程者头痛的问题。
- 什么是编码?
文字符号在计算机中是用0 和1 的字节序列表示的,编码就是将字节序列与所要表示的文字符号建立起映射。
要把各个国家不同的所有文字符号(字符集)正常显示和使用,需要做两件事情:
- 各个国家不同的所有文字符号一一对应地建立数字编码
- 数字编码按一定编码规则用0-1 表示出来
第一件事情已有一种Unicode 编码(万国码)来解决:它给全世界所有语言的所有文字符号规定了独一无二的数字编码,字符间分隔的方式是用固定长度字节数。
这样各个国家只需要做第二件事情:为自己国家的所有文字符号设计一种编码规则来表示对应的Unicode 编码。
从Unicode 到各国具体编码,称为编码过程;从各国具体编码到Unicode,称为解码过程。
再来说中国的第二件事情:汉字符号(中文)编码。历史原因产生了多种中文编码,从图来看更直观:
所谓兼容性,可以理解为子集,同时存在也不冲突。由图2.8 可见,ASCII(128 个字母和符号,英文够用)被所有编码兼容,而最常见的UTF-8 与GBK 之间除了ASCII 部分之外没有交集。
文件采用什么编码方式,就用什么编码方式打开。只要是用不兼容的编码方式打开文件,就会出现乱码,日常最容易导致乱码场景就是:
用UTF-8(GBK)编码方式去读取GBK(UTF-8)编码的文字,就会出现各种乱码
GBK(国标扩展)系列,根据包含汉字符号从少到多,依次是
GB2312: 只包含6763 个汉字
GBK: 包含20902 个汉字,基本足够用
GB18030: 又分GB1830-2000 和GB1830-2005,包含七万多个汉字
GBK 编码的汉字基本是2 字节,节省空间,但只适合国内中文环境。
UTF-8 编码(Unicode 转换格式),是Unicode 的再表示,支持各个国家的文字符号,兼容性非常好。所以,目前UTF-8 有一统天下的趋势。
UTF-8 是一种变长编码,解决字符间分隔的方式是通过对二进制中最高位连续1 的个数来决定该字是几字节编码。所有常用汉字的Unicode 值均可用3 字节的UTF-8 表示出来。
- 中文乱码的解决办法
首先,查看并确认你的windows 系统的默认编码方式:
Sys.getlocale("LC_CTYPE") # 查看系统默认字符集类型
## [1] "Chinese (Simplified)_China.936"
代码936 就表明是‘‘中国- 简体中文(GB2312)’’。
注意:不建议修改系统的默认编码方式,因为可能会导致一些软件、文件乱码。
大多数中文乱码都是GBK 与UTF-8 不兼容导致的,常见的有两种情形。
R 文件中的中文乱码
在你的电脑不中文乱码的R 脚本、Rmarkdown 等,拷贝到另一台电脑上时出现中文乱码。
解决办法:前文在配置Rstudio 时已讲到,设置code – saving 的Default text encoding 为兼容性更好的UTF-8。
读写数据文件中文乱码
数据文件采用什么编码方式,就用什么编码方式打开或读取。采用了不兼容的另一种编码打开或读取,肯定出现中文乱码。
R 自带函数读取GBK 或UTF-8
- 与所用操作系统默认编码相同的数据文件,即GBK,R 自带的函数read.csv()、read.table()、readLines() 都可以正常读取但不能直接读取UTF-8
- 但在read.csv() 和read.table() 中设置参数fileEncoding = "UTF-8",可以读取UTF-8,但无论如何不能读取BOM UTF-8
- 在readLines() 中设置参数encoding = "UTF-8",可以读取UTF-8 和BOM UTF-8
read.csv("datas/bp-gbk.csv") # GBK, 直接读取
read.csv("datas/bp-utf8nobom.csv", # UTF-8, 设置参数读取
fileEncoding = "UTF-8")
readLines("datas/bp-gbk.csv") # GBK, 直接读取
# UTF-8 和BOM UTF-8, 设置参数读取
readLines("datas/bp-utf8nobom.csv", encoding = "UTF-8")
readLines("datas/bp-utf8bom.csv", encoding = "UTF-8")
readr 包读取GBK 或UTF-8
- readr 包中的read_csv()、read_table2()、read_lines() 默认读取UTF-8 和BOM UTF-8;
- 但不能直接读取GBK, 需要设置参数locale = locale(encoding="GBK")
read_csv("datas/bp-utf8nobom.csv") # UTF-8, 直接读取
read_csv("datas/bp-utf8bom.csv") # BOM UTF-8, 直接读取
read_csv("datas/bp-gbk.csv",
locale = locale(encoding="GBK")) # GBK, 设置参数读取
写入GBK 或UTF-8 文件
- R 自带的write.csv(), writeLines() 仍是跟随操作系统默认编码,即默认写出为GBK 文件;设置参数fileEncoding = "UTF-8" 可写为UTF-8
- readr 包中的write_csv(), write_lines() 默认写为UTF-8, 但不能被Excel 软件正确打开
- readr::write_excel_csv() 可以写为BOM UTF-8, Excel 软件能正确打开
write.csv(df, "file-GBK.csv") # 写出为GBK 文件
write.csv(df, "file-UTF8.csv",
fileEncoding = "UTF-8") # 写出为UTF-8 文件
write_csv(df, "file-UTF8.csv") # 写出为UTF-8 文件
write_excel_csv(df, "file-BOM-UTF8.csv") # 写出为BOM UTF-8 文件
不局限于上述编码,一个数据文件只要知道了其编码方式,就可以通过在读写时指定该编码而避免乱码。那么关键的问题就是:怎么确定一个数据文件的编码?
AkelPad 是一款优秀开源小巧的文本编辑器,用它打开数据文件,自动在窗口下方显示文件的编码:
若要转换编码,只需要点文件另存为,在代码页下拉框选择想要的编码方式,保存即可。
另外,readr 包和rvest 包(爬虫)都提供了函数guess_encoding(),可检测文本和网页的编码方式;python 有一个chardet 库在检测文件编码方面更强大。