3.3 Parsing a vector

在详细介绍 readr 如何从磁盘读取文件之前,我们需要先讨论一下 parse_*()函数族。这些函数接受一个字符向量(因为文件中的数据全部是以字符串的形式进入 R 的),并返回一个特定向量,如逻辑、整数或日期向量:

这些函数各司其职,且都是 readr 的重要组成部分。一旦掌握了本节中这些单个解析函数的用法,我们就可以继续讨论如何综合使用它们来解析整个文件了。

tidyverse 中出现的函数族一样,parse_*() 函数族有着相同的参数结构。第一个参数是需要解析的字符向量,na参数设定了哪些字符串应该当做缺失值处理:

如果解析失败,你会收到一条警告:

解析失败的值在输出中以缺失值的形式存在:

如果解析失败的值很多,那么就应该使用 problems() 函数来获取完整的失败信息集合。这个函数会返回一个 tibble,可以使用 dplyr 来处理它:

在解析函数的使用方面,最重要的是知道有哪些解析函数,以及每种解析函数用来处理哪种类型的输入。具体来说,重要的解析函数有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 个问题增加了数値解析的复杂性:

  • 世界各地的人们书写数值的方式不尽相同。例如有些国家使用.当做小数点,而有些国家使用,
  • 数值周围可能会有其他字符,例如 $100015℃10%
  • 数值经常包含某种形式的分组(grouping),以便更易读,如1 000 000,而且世界各地用来分组的字符也不统一

为了解决第一个问题,readr使用了地区的概念(locale),使得可以按照不同地区设置解析选项。在解析数值时,最重要的选项就是用来表示小数点的字符,通过设置一个新的地区对象并设定decimal_mark参数,可以覆盖.的默认值:

locale()函数可以通过decimal_mark,grouping_mark,date_format,time_format,tz,encoding等参数创建一个地区对象,设定该地区内的一些表示习惯,这里我们告诉R哪个符号被用来当做小数点。

readr的默认地区是US-centric。获取默认地区的另一种方法是利用操作系统,但这可能让你的代码只能在你的电脑上运行,通过电子邮件共享给另一个国家的同事时,就可能失效。

parse_number()解决了第二个问题:它可以忽略数值前后的非数值型字符。这个函数特别适合处理货币和百分比,你可以提取嵌在文本中的数值:

组合使用 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.

  1. 什么是字符?

字符是各种文字和符号的总称,包括各个国家文字、标点符号、图形符号、数字等,甚至还包括表情符号。

  1. 什么是字符集?

字符集是多个字符的集合,字符集种类较多,每个字符集包含的字符个数不同,常见字符集有:ASCII 字符集、ISO 8859字符集、GB2312 字符集、BIG5字符集、GB18030 字符集、Unicode 字符集等。ASCII 可以非常好地表示英文字符,因为它就是美国信息交换标准代码(American Standard Code for Information Interchange)的缩写。Unicode是国际组织制定的可以容纳世界上所有文字和符号的字符编码方案。

  1. 什么是字符编码?

计算机要准确的处理各种字符集文字,需要进行字符编码,以便计算机能够识别和存储各种文字。 字符编码(encoding)和字符集不同。字符集只是字符的集合,不一定适合作网络传送、处理,有时须经编码(encode)后才能应用。如Unicode可依不同需要以UTF-8、UTF-16、UTF-32等方式编码。 字符编码就是以二进制的数字来对应字符集的字符。因此,对字符进行编码,是信息交流的技术基础。

  1. 概括

使用哪些字符。也就是说哪些汉字,字母和符号会被收入标准中。所包含“字符”的集合就叫做“字符集”。 规定每个“字符”分别用一个字节还是多个字节存储,用哪些字节来存储,这个规定就叫做“编码”。 各个国家和地区在制定编码标准的时候,“字符的集合”和“编码”一般都是同时制定的。因此,平常我们所说的“字符集”,比如: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()函数:

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:

编码问题博大精深,这里只是蜻蜓点水式地介绍一下。如果想要学习更多相关知识,可以阅读http://kunststube.net/encoding/

3.3.3 Factor

因子对应的解析函数是 parse_factor() ,其中 levels 参数被赋予一个包含所有因子可能水平的向量,如果要解析的列存在 levels 中没有的值,就会生成一条警告。

如果有很多问题条目的话,最简单的是它们当做字符串来解析,然后用forcats包进行后续处理。

3.3.4 Date and time

根据需要的是日期型数据(从 1970-01-01 开始的天数)、日期时间型数据(从 1970-01-01 开始的秒数)还是时间型数据(从午夜开始的描述),我们可以在3中解析函数之间进行选择。在没有使用任何附加参数时调用,具体情况如下:

  • parse_datetime()期待的是符合 ISO 8601 标准的日期时间。ISO 8601是一种国际标准,其中日期的各个部分按从大到小的顺序排列,即年、月、日、小时、分钟、秒:

这是最重要的日期/时间标准,如果经常使用日期和时间,可以阅读以下维基百科上的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

Exercise 1.3 如果在locale()函数中把decimal_markgrouping_mark设为同一个字符,会发生什么情况?如果将decimal_mark设为逗号,grouping_mark的默认值会发生什么变化?如果将grouping_mark设置为句点,decimal_mark的默认值会发生什么变化?

不能将decimal_markgroup_mark 设为同一个字符:

decimal_mark设为逗号时,grouping_mark的默认值将变为.(见第一行):

grouping_mark设为句点时,decimal_mark的默认值将变为,:

Exercise 3.3 locale()函数中的date_format()time_format()参数有什么用?

date_format()time_format()format 参数的功能一样,用以指定日期时间数据的格式,如果在 locale() 中设定了以上两个参数,就不需要再设定 format;反之亦然:

  1. 生成正确行使的字符串来解析以下日期和时间。