3.3 Parsing a vector
在详细介绍 readr
如何从磁盘读取文件之前,我们需要先讨论一下 parse_*()
函数族。这些函数接受一个字符向量(因为文件中的数据全部是以字符串的形式进入 R 的),并返回一个特定向量,如逻辑、整数或日期向量:
## 用str()函数返回类别
str(parse_logical(c("True", "False", "True")))
#> logi [1:3] TRUE FALSE TRUE
str(parse_integer(c("1", "2", "3")))
#> int [1:3] 1 2 3
str(parse_date(c("2010-01-01", "2019-08-15")))
#> Date[1:2], format: "2010-01-01" "2019-08-15"
这些函数各司其职,且都是 readr
的重要组成部分。一旦掌握了本节中这些单个解析函数的用法,我们就可以继续讨论如何综合使用它们来解析整个文件了。
和 tidyverse
中出现的函数族一样,parse_*()
函数族有着相同的参数结构。第一个参数是需要解析的字符向量,na
参数设定了哪些字符串应该当做缺失值处理:
如果解析失败,你会收到一条警告:
解析失败的值在输出中以缺失值的形式存在:
x
#> [1] 123 345 NA NA
#> attr(,"problems")
#> # A tibble: 2 x 4
#> row col expected actual
#> <int> <int> <chr> <chr>
#> 1 3 NA an integer abc
#> 2 4 NA no trailing characters .45
如果解析失败的值很多,那么就应该使用 problems()
函数来获取完整的失败信息集合。这个函数会返回一个 tibble,可以使用 dplyr
来处理它:
problems(x)
#> # A tibble: 2 x 4
#> row col expected actual
#> <int> <int> <chr> <chr>
#> 1 3 NA an integer abc
#> 2 4 NA no trailing characters .45
在解析函数的使用方面,最重要的是知道有哪些解析函数,以及每种解析函数用来处理哪种类型的输入。具体来说,重要的解析函数有8中:
parse_logical()
和parse_integer()
函数分别解析逻辑值和整数,因为这两个解析函数基本不会出现问题,所以我们不再进行更多介绍
parse_double()
是严格的数值型解析函数,而parse_number()
则是灵活的数值型解析函数,这两个函数背后很复杂,因为世界各地书写数值的方式不尽相同
parse_character()
函数似乎太过简单,甚至没必要存在,因为R读取的文件本身就是字符串形式。但一个棘手的问题使得这个函数非常重要:字符编码
parse_factor()
函数可以创建因子,R使用这种数据结构表示分类变量,该变量具有固定数目的已知值
parse_datetime()
、parse_date()
和parse_time()
函数可以解析不同类型的日期和时间,它们是最复杂的,因为有太多不同的日期书写形式
3.3.1 Numeric
解析数值似乎是直截了当的,但以下 3 个问题增加了数値解析的复杂性:
- 世界各地的人们书写数值的方式不尽相同。例如有些国家使用
.
当做小数点,而有些国家使用,
- 数值周围可能会有其他字符,例如
$1000
、15℃
和10%
- 数值经常包含某种形式的分组(grouping),以便更易读,如
1 000 000
,而且世界各地用来分组的字符也不统一
为了解决第一个问题,readr
使用了地区的概念(locale),使得可以按照不同地区设置解析选项。在解析数值时,最重要的选项就是用来表示小数点的字符,通过设置一个新的地区对象并设定decimal_mark
参数,可以覆盖.
的默认值:
parse_double("1.23")
#> [1] 1.23
parse_double("1,23", locale = locale(decimal_mark = ","))
#> [1] 1.23
locale()
函数可以通过decimal_mark,grouping_mark,date_format,time_format,tz,encoding
等参数创建一个地区对象,设定该地区内的一些表示习惯,这里我们告诉R哪个符号被用来当做小数点。
readr
的默认地区是US-centric。获取默认地区的另一种方法是利用操作系统,但这可能让你的代码只能在你的电脑上运行,通过电子邮件共享给另一个国家的同事时,就可能失效。
parse_number()
解决了第二个问题:它可以忽略数值前后的非数值型字符。这个函数特别适合处理货币和百分比,你可以提取嵌在文本中的数值:
parse_number("$100")
#> [1] 100
parse_number("20%")
#> [1] 20
parse_number("37℃")
#> [1] 37
parse_number("It cost $123.45")
#> [1] 123
组合使用 parse_number()
和 locale()
可以解决最后一个问题,因为 parse_number()
可以忽略“分组符号” :
3.3.2 Character
To better illustrate the challenges of parsing a character vector, we need to have a reasonable understanding of the following questions.
- 什么是字符?
字符是各种文字和符号的总称,包括各个国家文字、标点符号、图形符号、数字等,甚至还包括表情符号。
- 什么是字符集?
字符集是多个字符的集合,字符集种类较多,每个字符集包含的字符个数不同,常见字符集有:ASCII 字符集、ISO 8859字符集、GB2312 字符集、BIG5字符集、GB18030 字符集、Unicode 字符集等。ASCII 可以非常好地表示英文字符,因为它就是美国信息交换标准代码(American Standard Code for Information Interchange)的缩写。Unicode是国际组织制定的可以容纳世界上所有文字和符号的字符编码方案。
- 什么是字符编码?
计算机要准确的处理各种字符集文字,需要进行字符编码,以便计算机能够识别和存储各种文字。 字符编码(encoding)和字符集不同。字符集只是字符的集合,不一定适合作网络传送、处理,有时须经编码(encode)后才能应用。如Unicode可依不同需要以UTF-8、UTF-16、UTF-32等方式编码。 字符编码就是以二进制的数字来对应字符集的字符。因此,对字符进行编码,是信息交流的技术基础。
- 概括
使用哪些字符。也就是说哪些汉字,字母和符号会被收入标准中。所包含“字符”的集合就叫做“字符集”。 规定每个“字符”分别用一个字节还是多个字节存储,用哪些字节来存储,这个规定就叫做“编码”。 各个国家和地区在制定编码标准的时候,“字符的集合”和“编码”一般都是同时制定的。因此,平常我们所说的“字符集”,比如:GB2312, GBK, JIS等,除了有“字符的集合”这层含义外,同时也包含了“编码”的含义。 注意:Unicode字符集有多种编码方式,如UTF-8、UTF-16等;ASCII只有一种;大多数MBCS(包括GB2312,GBK)也只有一种。
在 R 中,我们可以使用charToRaw()
函数获得一个字符串的底层表示(underlying representation):
charToRaw()
返回的尚不是编码结果(二进制),而是十六进制的表示。每个十六进制数表示字符串的一个字节:4d
是M,61
是a等。charToRaw()
返回的对象在R中被称为raw type
,想要得到真正的二进制编码,要对raw type
再使用rawToBits()
函数:
rawToBits(char_raw)
#> [1] 01 00 01 01 00 00 01 00 01 00 00 00 00 01 01 00 00 00 00 01 01 01 01 00 01
#> [26] 00 00 01 00 01 01 00 00 01 01 01 00 01 01 00 01 00 01 00 00 01 01 00
readr
默认使用UTF-8编码。其中的含义在于,每当接受到一个字符串,R收到的不是字符串本身,而是它背后的二进制表示,于是R便尝试按照UTF-8的规则解读这些二进制码,把它们还原为人类所能理解的字符。问题是,如果你的文件不是用UTF-8编码,这就像用英文字典来解释汉语拼音,可能小张计算机存储字母”A”是1100001,而小王存储字母”A”是11000010,这样双方交换信息时就会误解。比如小张把1100001发送给小王,小王并不认为1100001是字母”A”,可能认为这是字母”X”,于是小王在用记事本访问存储在硬盘上的1100001时,在屏幕上显示的就是字母”X”。
要解决这个问题,需要在parse_character()
函数中通过locale(encoding = )
参数设定编码方式:
如何才能找到正确的编码方式呢?有可能数据来源会注明,但如果没有到话,readr
包提供了guess_encoding()
函数来帮助你找出编码方式。这个函数并非万无一失,如果有大量文本效果就会更好,它的第一个参数可以是直接的文件路径,也可以是一个raw type
:
guess_encoding(charToRaw("中国"))
#> # A tibble: 1 x 2
#> encoding confidence
#> <chr> <dbl>
#> 1 ASCII 1
parse_character("中国",locale = locale(encoding = "UTF-8"))
#> [1] "<U+4E2D><U+56FD>"
parse_character("中国",locale = locale(encoding = "windows-1252"))
#> [1] "<U+4E2D><U+56FD>"
guess_encoding("data\\hotdogs.txt")
#> # A tibble: 1 x 2
#> encoding confidence
#> <chr> <dbl>
#> 1 ASCII 1
编码问题博大精深,这里只是蜻蜓点水式地介绍一下。如果想要学习更多相关知识,可以阅读http://kunststube.net/encoding/
3.3.3 Factor
因子对应的解析函数是 parse_factor()
,其中 levels
参数被赋予一个包含所有因子可能水平的向量,如果要解析的列存在 levels
中没有的值,就会生成一条警告。
fruit <- c("apple", "banana")
f <- parse_factor(c("apple", "banana", "bananana"), levels = fruit)
problems(f)
#> # A tibble: 1 x 4
#> row col expected actual
#> <int> <int> <chr> <chr>
#> 1 3 NA value in level set bananana
如果有很多问题条目的话,最简单的是它们当做字符串来解析,然后用forcats
包进行后续处理。
3.3.4 Date and time
根据需要的是日期型数据(从 1970-01-01 开始的天数)、日期时间型数据(从 1970-01-01 开始的秒数)还是时间型数据(从午夜开始的描述),我们可以在3中解析函数之间进行选择。在没有使用任何附加参数时调用,具体情况如下:
parse_datetime()
期待的是符合 ISO 8601 标准的日期时间。ISO 8601是一种国际标准,其中日期的各个部分按从大到小的顺序排列,即年、月、日、小时、分钟、秒:
parse_datetime("2010-10-01 201002")
#> [1] "2010-10-01 20:10:02 UTC"
## 如果时间被忽略了,就会被设置为午夜
parse_datetime("20101001")
#> [1] "2010-10-01 UTC"
这是最重要的日期/时间标准,如果经常使用日期和时间,可以阅读以下维基百科上的ISO 8601标准
parse_date()
期待的是四位数的年份、一个-
或者/
作为分隔符,月,一个-
或者/
作为分隔符,然后是日:
parse_time()
期待的是小时,:
作为分隔符,分钟,可选的:
和后面的秒,以及一个可选的 am/pm 标识符:
如果默认数据不适合实际数据,那么可以为 parse_date()
的第二个参数 format
传入一个字符串指定自己的日期时间格式(这个参数是解析日期时间的函数在parse_*()
函数族中特有的),格式由以下各部分组成:
成分 | 符号 |
---|---|
年 | %Y (四位数)%y (两位数;00-69被解释为2000-2069、70-99被解释为1970-1999) |
月 | %m (两位数)%b (简写名称,如“Jan”)%B (完整名称,如January) |
日 | %d (一位数或两位数) %e(两位数) |
时间 | %H (0-23小时)%I (0-12小时,必须和%p 一起使用) %p (表示am/pm) %M (分钟) S (整数秒) %OS (实数秒) %Z (时区) |
非数值字符 | %. (跳过一个非数值字符) %* 跳过所有非数值字符 |
找出正确格式的最好方法是创建几个解析字符向量的示例,并使用某种解析函数进行测试(如果数据中有分隔符,则):
3.3.5 Exercises
locale()
函数中把decimal_mark
和grouping_mark
设为同一个字符,会发生什么情况?如果将decimal_mark
设为逗号,grouping_mark
的默认值会发生什么变化?如果将grouping_mark
设置为句点,decimal_mark
的默认值会发生什么变化?
不能将decimal_mark
和 group_mark
设为同一个字符:
locale(decimal_mark = ".",grouping_mark = ".")
#> Error: `decimal_mark` and `grouping_mark` must be different
将decimal_mark
设为逗号时,grouping_mark
的默认值将变为.
(见第一行):
locale(decimal_mark = ",")
#> <locale>
#> Numbers: 123.456,78
#> Formats: %AD / %AT
#> Timezone: UTC
#> Encoding: UTF-8
#> <date_names>
#> Days: Sunday (Sun), Monday (Mon), Tuesday (Tue), Wednesday (Wed), Thursday
#> (Thu), Friday (Fri), Saturday (Sat)
#> Months: January (Jan), February (Feb), March (Mar), April (Apr), May (May),
#> June (Jun), July (Jul), August (Aug), September (Sep), October
#> (Oct), November (Nov), December (Dec)
#> AM/PM: AM/PM
将grouping_mark
设为句点时,decimal_mark
的默认值将变为,
:
locale(grouping_mark = ".")
#> <locale>
#> Numbers: 123.456,78
#> Formats: %AD / %AT
#> Timezone: UTC
#> Encoding: UTF-8
#> <date_names>
#> Days: Sunday (Sun), Monday (Mon), Tuesday (Tue), Wednesday (Wed), Thursday
#> (Thu), Friday (Fri), Saturday (Sat)
#> Months: January (Jan), February (Feb), March (Mar), April (Apr), May (May),
#> June (Jun), July (Jul), August (Aug), September (Sep), October
#> (Oct), November (Nov), December (Dec)
#> AM/PM: AM/PM
locale()
函数中的date_format()
和time_format()
参数有什么用?
date_format()
和 time_format()
和 format
参数的功能一样,用以指定日期时间数据的格式,如果在 locale()
中设定了以上两个参数,就不需要再设定 format
;反之亦然:
- 生成正确行使的字符串来解析以下日期和时间。
d1 <- "January 1,2010"
parse_date(d1,"%B %d,%Y")
#> [1] "2010-01-01"
d2 <- "2015-Mar-07"
parse_date(d2,"%Y-%b-%e")
#> [1] "2015-03-07"
d3 <- c("August 19 (2015)","July 1 (2015)")
parse_date(d3,"%B %d (%Y)")
#> [1] "2015-08-19" "2015-07-01"
t1 <- "1705"
parse_time("1705","%H%M")
#> 17:05:00
t2 <- "11:15:10.12 PM"
parse_time(t2,"%I:%M:%OS %p")
#> 23:15:10.12