14  字符串

14.1 引言

到目前为止,你已经使用了很多字符串,但对细节知之甚少。现在是时候深入了解它们,学习字符串的运作原理,并掌握一些你可以使用的强大字符串处理工具。

我们将从创建字符串和字符向量的细节开始。然后,你将深入学习如何从数据创建字符串,以及反过来:如何从数据中提取字符串。接着,我们将讨论处理单个字母的工具。本章最后会介绍处理单个字母的函数,并简要讨论在处理其他语言时,你从英语中获得的期望可能会如何误导你。

我们将在下一章继续学习字符串,届时你将学到更多关于正则表达式的强大功能。

14.1.1 先决条件

在本章中,我们将使用 stringr 包中的函数,它是核心 tidyverse 的一部分。我们还将使用 babynames 数据,因为它提供了一些有趣的字符串供我们操作。

你可以很快分辨出你正在使用的是一个 stringr 函数,因为所有 stringr 函数都以 str_ 开头。如果你使用 RStudio,这一点特别有用,因为输入 str_ 会触发自动补全,让你回想起可用的函数。

在 RStudio 控制台中输入 str_c,上方显示了自动补全的提示框, 其中列出了以 str_c 开头的函数。自动补全列表中高亮显示的函数 的函数签名和帮助文档的开头部分显示在其右侧的面板中。

14.2 创建字符串

我们在本书的前面部分已经顺便创建过字符串,但没有讨论细节。首先,你可以使用单引号 (') 或双引号 (") 来创建字符串。这两者在行为上没有区别,因此为了保持一致性,tidyverse 风格指南 建议使用 ",除非字符串中包含多个 "

string1 <- "This is a string"
string2 <- 'If I want to include a "quote" inside a string, I use single quotes'

如果你忘记关闭引号,你会看到 +,即续行提示符:

> "This is a string without a closing quote
+ 
+ 
+ HELP I'M STUCK IN A STRING

如果发生这种情况,而你又不知道该关闭哪个引号,可以按 Escape 键取消并重试。

14.2.1 转义

要在字符串中包含字面意义上的单引号或双引号,你可以使用 \ 来“转义”它:

double_quote <- "\"" # 或者 '"'
single_quote <- '\'' # 或者 "'"

所以,如果你想在字符串中包含一个字面意义上的反斜杠,你需要对它进行转义:"\\"

backslash <- "\\"

请注意,字符串的打印表示形式与字符串本身不同,因为打印表示形式会显示转义符(换句话说,当你打印一个字符串时,你可以复制并粘贴输出来重新创建那个字符串)。要查看字符串的原始内容,请使用 str_view()1

x <- c(single_quote, double_quote, backslash)
x
#> [1] "'"  "\"" "\\"
str_view(x)
#> [1] │ '
#> [2] │ "
#> [3] │ \

14.2.2 原始字符串

创建一个包含多个引号或反斜杠的字符串很快就会变得混乱。为了说明这个问题,让我们创建一个字符串,它包含我们定义 double_quotesingle_quote 变量的代码块的内容:

tricky <- "double_quote <- \"\\\"\" # or '\"'
single_quote <- '\\'' # or \"'\""
str_view(tricky)
#> [1] │ double_quote <- "\"" # or '"'
#>     │ single_quote <- '\'' # or "'"

这可真是一大堆反斜杠!(这有时被称为倾斜的牙签综合症 (leaning toothpick syndrome)。)为了消除转义,你可以改用原始字符串 (raw string)2

tricky <- r"(double_quote <- "\"" # or '"'
single_quote <- '\'' # or "'")"
str_view(tricky)
#> [1] │ double_quote <- "\"" # or '"'
#>     │ single_quote <- '\'' # or "'"

原始字符串通常以 r"( 开始,以 )" 结束。但是,如果你的字符串包含 )",你可以改用 r"[]"r"{}",如果这还不够,你可以插入任意数量的破折号来使开始和结束对唯一,例如 r"--()--"r"---()---" 等。原始字符串足够灵活,可以处理任何文本。

14.2.3 其他特殊字符

除了 \"\'\\,还有一些其他特殊字符可能会派上用场。最常见的是 \n(换行符)和 \t(制表符)。你有时还会看到包含以 \u\U 开头的 Unicode 转义的字符串。这是一种书写非英文字符的方法,能在所有系统上工作。你可以在 ?Quotes 中看到其他特殊字符的完整列表。

x <- c("one\ntwo", "one\ttwo", "\u00b5", "\U0001f604")
x
#> [1] "one\ntwo" "one\ttwo" "µ"        "😄"
str_view(x)
#> [1] │ one
#>     │ two
#> [2] │ one{\t}two
#> [3] │ µ
#> [4] │ 😄

请注意,str_view() 对制表符使用花括号,以便更容易发现它们3。处理文本的挑战之一是,空白字符有多种方式进入文本,所以这个背景能帮助你识别出有异常情况正在发生。

14.2.4 练习

  1. 创建包含以下值的字符串:

    1. He said "That's amazing!"

    2. \a\b\c\d

    3. \\\\\\

  2. 在你的 R 会话中创建以下字符串并打印它。特殊的 \u00a0 发生了什么?str_view() 如何显示它?你能用 Google 搜索一下,找出这个特殊字符是什么吗?

    x <- "This\u00a0is\u00a0tricky"

14.3 从数据创建多个字符串

现在你已经学会了“手动”创建一个或两个字符串的基础知识,我们将深入探讨从其他字符串创建字符串的细节。这将帮助你解决一个常见问题:你有一些自己写的文本,想把它与数据框中的字符串结合起来。例如,你可能会将 “Hello” 与一个 name 变量结合起来,创建一个问候语。我们将向你展示如何使用 str_c()str_glue() 来做到这一点,以及如何将它们与 mutate() 一起使用。这自然会引出一个问题:你可能会在 summarize() 中使用哪些 stringr 函数?因此,我们将在本节最后讨论 str_flatten(),这是一个用于字符串的汇总函数。

14.3.1 str_c()

str_c() 接受任意数量的向量作为参数,并返回一个字符向量:

str_c("x", "y")
#> [1] "xy"
str_c("x", "y", "z")
#> [1] "xyz"
str_c("Hello ", c("John", "Susan"))
#> [1] "Hello John"  "Hello Susan"

str_c() 与 R base 的 paste0() 非常相似,但它被设计为与 mutate() 一起使用,遵循了 tidyverse 的常规规则,即循环补齐和传播缺失值:

df <- tibble(name = c("Flora", "David", "Terra", NA))
df |> mutate(greeting = str_c("Hi ", name, "!"))
#> # A tibble: 4 × 2
#>   name  greeting 
#>   <chr> <chr>    
#> 1 Flora Hi Flora!
#> 2 David Hi David!
#> 3 Terra Hi Terra!
#> 4 <NA>  <NA>

如果你希望缺失值以另一种方式显示,可以使用 coalesce() 来替换它们。根据你的需求,你可以在 str_c() 内部或外部使用它:

df |> 
  mutate(
    greeting1 = str_c("Hi ", coalesce(name, "you"), "!"),
    greeting2 = coalesce(str_c("Hi ", name, "!"), "Hi!")
  )
#> # A tibble: 4 × 3
#>   name  greeting1 greeting2
#>   <chr> <chr>     <chr>    
#> 1 Flora Hi Flora! Hi Flora!
#> 2 David Hi David! Hi David!
#> 3 Terra Hi Terra! Hi Terra!
#> 4 <NA>  Hi you!   Hi!

14.3.2 str_glue()

如果你用 str_c() 混合许多固定的和可变的字符串,你会注意到你输入了大量的 ",这使得代码的整体目标难以看清。另一种方法是由 glue 包 通过 str_glue()4 提供的。你给它一个具有特殊功能的单一字符串:{} 里的任何东西都会像在引号外面一样被求值:

df |> mutate(greeting = str_glue("Hi {name}!"))
#> # A tibble: 4 × 2
#>   name  greeting 
#>   <chr> <glue>   
#> 1 Flora Hi Flora!
#> 2 David Hi David!
#> 3 Terra Hi Terra!
#> 4 <NA>  Hi NA!

如你所见,str_glue() 目前将缺失值转换为字符串 "NA",不幸的是,这与 str_c() 不一致。

你可能还会想,如果需要在字符串中包含一个常规的 {},会发生什么。如果你猜到需要以某种方式转义它,那你的思路是正确的。诀窍在于 glue 使用了一种稍微不同的转义技术:不是用像 \ 这样的特殊字符作为前缀,而是将特殊字符加倍:

df |> mutate(greeting = str_glue("{{Hi {name}!}}"))
#> # A tibble: 4 × 2
#>   name  greeting   
#>   <chr> <glue>     
#> 1 Flora {Hi Flora!}
#> 2 David {Hi David!}
#> 3 Terra {Hi Terra!}
#> 4 <NA>  {Hi NA!}

14.3.3 str_flatten()

str_c()str_glue()mutate() 配合得很好,因为它们的输出与输入长度相同。如果你想要一个能与 summarize() 很好配合的函数,即一个总是返回单个字符串的东西呢?这就是 str_flatten()5 的工作:它接受一个字符向量,并将向量的每个元素组合成一个单一的字符串:

str_flatten(c("x", "y", "z"))
#> [1] "xyz"
str_flatten(c("x", "y", "z"), ", ")
#> [1] "x, y, z"
str_flatten(c("x", "y", "z"), ", ", last = ", and ")
#> [1] "x, y, and z"

这使得它能与 summarize() 很好地配合工作:

df <- tribble(
  ~ name, ~ fruit,
  "Carmen", "banana",
  "Carmen", "apple",
  "Marvin", "nectarine",
  "Terence", "cantaloupe",
  "Terence", "papaya",
  "Terence", "mandarin"
)
df |>
  group_by(name) |> 
  summarize(fruits = str_flatten(fruit, ", "))
#> # A tibble: 3 × 2
#>   name    fruits                      
#>   <chr>   <chr>                       
#> 1 Carmen  banana, apple               
#> 2 Marvin  nectarine                   
#> 3 Terence cantaloupe, papaya, mandarin

14.3.4 练习

  1. 对于以下输入,比较并对比 paste0()str_c() 的结果:

    str_c("hi ", NA)
    str_c(letters[1:2], letters[1:3])
  2. paste()paste0() 有什么区别?你如何用 str_c() 重现与 paste() 等价的功能?

  3. 将以下表达式从 str_c() 转换为 str_glue(),或反之:

    1. str_c("The price of ", food, " is ", price)

    2. str_glue("I'm {age} years old and live in {country}")

    3. str_c("\\section{", title, "}")

14.4 从字符串中提取数据

将多个变量塞进一个字符串中是很常见的。在本节中,你将学习如何使用四个 tidyr 函数来提取它们:

  • df |> separate_longer_delim(col, delim)
  • df |> separate_longer_position(col, width)
  • df |> separate_wider_delim(col, delim, names)
  • df |> separate_wider_position(col, widths)

如果你仔细观察,你会发现这里有一个共同的模式:separate_,然后是 longerwider,然后是 _,然后是 delimposition。这是因为这四个函数是由两个更简单的原语组成的:

  • 就像 pivot_longer()pivot_wider() 一样,_longer 函数通过创建新行来使输入的数据框变长,而 _wider 函数通过生成新列来使输入的数据框变宽。
  • delim 用像 ", "" " 这样的分隔符来分割字符串;position 在指定的宽度处分割,比如 c(3, 5, 2)

我们将在 Chapter 15 中回到这个家族的最后一个成员,separate_wider_regex()。它是 wider 函数中最灵活的一个,但在使用它之前,你需要对正则表达式有所了解。

接下来的两节将为你介绍这些 separate 函数背后的基本思想,首先是分成行(这稍微简单一些),然后是分成列。最后,我们将讨论 wider 函数为你提供的诊断问题的工具。

14.4.1 分成行

当不同行中组件的数量不同时,将字符串分成行通常最有用。最常见的情况是需要 separate_longer_delim() 根据分隔符进行分割:

df1 <- tibble(x = c("a,b,c", "d,e", "f"))
df1 |> 
  separate_longer_delim(x, delim = ",")
#> # A tibble: 6 × 1
#>   x    
#>   <chr>
#> 1 a    
#> 2 b    
#> 3 c    
#> 4 d    
#> 5 e    
#> 6 f

separate_longer_position() 在实践中比较少见,但一些较老的数据集确实使用一种非常紧凑的格式,其中每个字符都用来记录一个值:

df2 <- tibble(x = c("1211", "131", "21"))
df2 |> 
  separate_longer_position(x, width = 1)
#> # A tibble: 9 × 1
#>   x    
#>   <chr>
#> 1 1    
#> 2 2    
#> 3 1    
#> 4 1    
#> 5 1    
#> 6 3    
#> # ℹ 3 more rows

14.4.2 分成列

当每个字符串中都有固定数量的组件,并且你希望将它们分散到列中时,将字符串分成列通常最有用。它们比它们的 longer 等价物稍微复杂一些,因为你需要命名这些列。例如,在下面的数据集中,x 由一个代码、一个版本号和一个年份组成,用 . 分隔。要使用 separate_wider_delim(),我们需要在两个参数中提供分隔符和名称:

df3 <- tibble(x = c("a10.1.2022", "b10.2.2011", "e15.1.2015"))
df3 |> 
  separate_wider_delim(
    x,
    delim = ".",
    names = c("code", "edition", "year")
  )
#> # A tibble: 3 × 3
#>   code  edition year 
#>   <chr> <chr>   <chr>
#> 1 a10   1       2022 
#> 2 b10   2       2011 
#> 3 e15   1       2015

如果某个特定的部分没有用,你可以使用 NA 名称来从结果中省略它:

df3 |> 
  separate_wider_delim(
    x,
    delim = ".",
    names = c("code", NA, "year")
  )
#> # A tibble: 3 × 2
#>   code  year 
#>   <chr> <chr>
#> 1 a10   2022 
#> 2 b10   2011 
#> 3 e15   2015

separate_wider_position() 的工作方式略有不同,因为你通常需要指定每列的宽度。所以你给它一个命名的整数向量,其中名称给出新列的名称,值是它占用的字符数。你可以通过不命名来从输出中省略值:

df4 <- tibble(x = c("202215TX", "202122LA", "202325CA")) 
df4 |> 
  separate_wider_position(
    x,
    widths = c(year = 4, age = 2, state = 2)
  )
#> # A tibble: 3 × 3
#>   year  age   state
#>   <chr> <chr> <chr>
#> 1 2022  15    TX   
#> 2 2021  22    LA   
#> 3 2023  25    CA

14.4.3 诊断变宽问题

separate_wider_delim()6 需要一个固定的、已知的列集合。如果某些行没有预期的片段数量,会发生什么?可能存在两种问题:片段太少或太多,所以 separate_wider_delim() 提供了两个参数来帮助解决:too_fewtoo_many。我们首先用下面的示例数据集来看一下 too_few 的情况:

df <- tibble(x = c("1-1-1", "1-1-2", "1-3", "1-3-2", "1"))

df |> 
  separate_wider_delim(
    x,
    delim = "-",
    names = c("x", "y", "z")
  )
#> Error in `separate_wider_delim()`:
#> ! Expected 3 pieces in each element of `x`.
#> ! 2 values were too short.
#> ℹ Use `too_few = "debug"` to diagnose the problem.
#> ℹ Use `too_few = "align_start"/"align_end"` to silence this message.

你会注意到我们得到了一个错误,但错误信息给了我们一些关于如何继续的建议。让我们从调试问题开始:

debug <- df |> 
  separate_wider_delim(
    x,
    delim = "-",
    names = c("x", "y", "z"),
    too_few = "debug"
  )
#> Warning: Debug mode activated: adding variables `x_ok`, `x_pieces`, and
#> `x_remainder`.
debug
#> # A tibble: 5 × 6
#>   x     y     z     x_ok  x_pieces x_remainder
#>   <chr> <chr> <chr> <lgl>    <int> <chr>      
#> 1 1-1-1 1     1     TRUE         3 ""         
#> 2 1-1-2 1     2     TRUE         3 ""         
#> 3 1-3   3     <NA>  FALSE        2 ""         
#> 4 1-3-2 3     2     TRUE         3 ""         
#> 5 1     <NA>  <NA>  FALSE        1 ""

当你使用调试模式时,输出中会增加三列:x_okx_piecesx_remainder(如果你分离一个不同名称的变量,你会得到一个不同的前缀)。在这里,x_ok 让你能够快速找到失败的输入:

debug |> filter(!x_ok)
#> # A tibble: 2 × 6
#>   x     y     z     x_ok  x_pieces x_remainder
#>   <chr> <chr> <chr> <lgl>    <int> <chr>      
#> 1 1-3   3     <NA>  FALSE        2 ""         
#> 2 1     <NA>  <NA>  FALSE        1 ""

x_pieces 告诉我们找到了多少个片段,而预期是 3 个(names 的长度)。当片段太少时,x_remainder 没有用,但我们很快会再次看到它。

有时,查看这些调试信息会揭示你的分隔符策略存在问题,或者表明你需要在分离前进行更多的预处理。在这种情况下,修复上游的问题,并确保移除 too_few = "debug",以确保新问题会变成错误。

在其他情况下,你可能希望用 NA 填充缺失的片段然后继续。这就是 too_few = "align_start"too_few = "align_end" 的工作,它们允许你控制 NA 应该放在哪里:

df |> 
  separate_wider_delim(
    x,
    delim = "-",
    names = c("x", "y", "z"),
    too_few = "align_start"
  )
#> # A tibble: 5 × 3
#>   x     y     z    
#>   <chr> <chr> <chr>
#> 1 1     1     1    
#> 2 1     1     2    
#> 3 1     3     <NA> 
#> 4 1     3     2    
#> 5 1     <NA>  <NA>

如果你有太多的片段,同样的原则也适用:

df <- tibble(x = c("1-1-1", "1-1-2", "1-3-5-6", "1-3-2", "1-3-5-7-9"))

df |> 
  separate_wider_delim(
    x,
    delim = "-",
    names = c("x", "y", "z")
  )
#> Error in `separate_wider_delim()`:
#> ! Expected 3 pieces in each element of `x`.
#> ! 2 values were too long.
#> ℹ Use `too_many = "debug"` to diagnose the problem.
#> ℹ Use `too_many = "drop"/"merge"` to silence this message.

但是现在,当我们调试结果时,你可以看到 x_remainder 的用途:

debug <- df |> 
  separate_wider_delim(
    x,
    delim = "-",
    names = c("x", "y", "z"),
    too_many = "debug"
  )
#> Warning: Debug mode activated: adding variables `x_ok`, `x_pieces`, and
#> `x_remainder`.
debug |> filter(!x_ok)
#> # A tibble: 2 × 6
#>   x         y     z     x_ok  x_pieces x_remainder
#>   <chr>     <chr> <chr> <lgl>    <int> <chr>      
#> 1 1-3-5-6   3     5     FALSE        4 -6         
#> 2 1-3-5-7-9 3     5     FALSE        5 -7-9

对于处理过多的片段,你有一套略有不同的选项:你可以静默地“drop”(丢弃)任何额外的片段,或者将它们全部“merge”(合并)到最后一列:

df |> 
  separate_wider_delim(
    x,
    delim = "-",
    names = c("x", "y", "z"),
    too_many = "drop"
  )
#> # A tibble: 5 × 3
#>   x     y     z    
#>   <chr> <chr> <chr>
#> 1 1     1     1    
#> 2 1     1     2    
#> 3 1     3     5    
#> 4 1     3     2    
#> 5 1     3     5


df |> 
  separate_wider_delim(
    x,
    delim = "-",
    names = c("x", "y", "z"),
    too_many = "merge"
  )
#> # A tibble: 5 × 3
#>   x     y     z    
#>   <chr> <chr> <chr>
#> 1 1     1     1    
#> 2 1     1     2    
#> 3 1     3     5-6  
#> 4 1     3     2    
#> 5 1     3     5-7-9

14.5 字母

在本节中,我们将向你介绍一些函数,让你能够处理字符串中的单个字母。你将学习如何找到字符串的长度,提取子字符串,以及在图表和表格中处理长字符串。

14.5.1 长度

str_length() 告诉你字符串中有多少个字母:

str_length(c("a", "R for data science", NA))
#> [1]  1 18 NA

你可以将此与 count() 一起使用,以找出美国婴儿姓名长度的分布,然后用 filter() 查看最长的名字,这些名字恰好有 15 个字母7

babynames |>
  count(length = str_length(name), wt = n)
#> # A tibble: 14 × 2
#>   length        n
#>    <int>    <int>
#> 1      2   338150
#> 2      3  8589596
#> 3      4 48506739
#> 4      5 87011607
#> 5      6 90749404
#> 6      7 72120767
#> # ℹ 8 more rows

babynames |> 
  filter(str_length(name) == 15) |> 
  count(name, wt = n, sort = TRUE)
#> # A tibble: 34 × 2
#>   name                n
#>   <chr>           <int>
#> 1 Franciscojavier   123
#> 2 Christopherjohn   118
#> 3 Johnchristopher   118
#> 4 Christopherjame   108
#> 5 Christophermich    52
#> 6 Ryanchristopher    45
#> # ℹ 28 more rows

14.5.2 子集

你可以使用 str_sub(string, start, end) 来提取字符串的一部分,其中 startend 是子字符串应该开始和结束的位置。startend 参数是包含性的,所以返回的字符串长度将是 end - start + 1

x <- c("Apple", "Banana", "Pear")
str_sub(x, 1, 3)
#> [1] "App" "Ban" "Pea"

你可以使用负值从字符串的末尾向前计数:-1 是最后一个字符,-2 是倒数第二个字符,依此类推。

str_sub(x, -3, -1)
#> [1] "ple" "ana" "ear"

请注意,如果字符串太短,str_sub() 不会失败:它只会返回尽可能多的内容:

str_sub("a", 1, 5)
#> [1] "a"

我们可以使用 str_sub()mutate() 来找出每个名字的首字母和末尾字母:

babynames |> 
  mutate(
    first = str_sub(name, 1, 1),
    last = str_sub(name, -1, -1)
  )
#> # A tibble: 1,924,665 × 7
#>    year sex   name          n   prop first last 
#>   <dbl> <chr> <chr>     <int>  <dbl> <chr> <chr>
#> 1  1880 F     Mary       7065 0.0724 M     y    
#> 2  1880 F     Anna       2604 0.0267 A     a    
#> 3  1880 F     Emma       2003 0.0205 E     a    
#> 4  1880 F     Elizabeth  1939 0.0199 E     h    
#> 5  1880 F     Minnie     1746 0.0179 M     e    
#> 6  1880 F     Margaret   1578 0.0162 M     t    
#> # ℹ 1,924,659 more rows

14.5.3 练习

  1. 在计算婴儿姓名长度的分布时,我们为什么使用 wt = n
  2. 使用 str_length()str_sub() 从每个婴儿名字中提取中间的字母。如果字符串有偶数个字符,你该怎么办?
  3. 婴儿姓名的长度随时间变化有任何主要趋势吗?首字母和末尾字母的流行度呢?

14.6 非英文文本

到目前为止,我们主要关注英文文本,处理英文文本特别容易,原因有二。首先,英文字母相对简单:只有 26 个字母。其次(也许更重要),我们今天使用的计算基础设施主要是由说英语的人设计的。不幸的是,我们没有足够的篇幅来全面处理非英文语言。尽管如此,我们还是想提醒你注意一些你可能会遇到的最大挑战:编码、字母变体和依赖于区域设置的函数。

14.6.1 编码

在处理非英文文本时,第一个挑战通常是编码 (encoding)。要理解发生了什么,我们需要深入了解计算机是如何表示字符串的。在 R 中,我们可以使用 charToRaw() 来获取字符串的底层表示:

charToRaw("Hadley")
#> [1] 48 61 64 6c 65 79

这六个十六进制数中的每一个都代表一个字母:48 是 H,61 是 a,依此类推。从十六进制数到字符的映射称为编码,在这种情况下,编码被称为 ASCII。ASCII 在表示英文字符方面做得很好,因为它是美国信息交换标准代码。

对于非英语语言来说,事情就没那么简单了。在计算的早期,有许多竞争性的标准用于编码非英文字符。例如,欧洲有两种不同的编码:Latin1(又名 ISO-8859-1)用于西欧语言,而 Latin2(又名 ISO-8859-2)用于中欧语言。在 Latin1 中,字节 b1 是 “±”,但在 Latin2 中,它是 “ą”!幸运的是,今天有一个几乎在任何地方都支持的标准:UTF-8。UTF-8 几乎可以编码当今人类使用的所有字符,以及许多额外的符号,如表情符号。

readr 处处使用 UTF-8。这是一个很好的默认设置,但对于由不使用 UTF-8 的旧系统产生的数据来说,这会失败。如果发生这种情况,你的字符串在打印时会看起来很奇怪。有时可能只有一两个字符出错;其他时候,你会得到完全的乱码。例如,这里是两个带有不寻常编码的内联 CSV8

x1 <- "text\nEl Ni\xf1o was particularly bad this year"
read_csv(x1)$text
#> [1] "El Ni\xf1o was particularly bad this year"

x2 <- "text\n\x82\xb1\x82\xf1\x82\xc9\x82\xbf\x82\xcd"
read_csv(x2)$text
#> [1] "\x82\xb1\x82\xf1\x82ɂ\xbf\x82\xcd"

要正确读取这些内容,你需要通过 locale 参数指定编码:

read_csv(x1, locale = locale(encoding = "Latin1"))$text
#> [1] "El Niño was particularly bad this year"

read_csv(x2, locale = locale(encoding = "Shift-JIS"))$text
#> [1] "こんにちは"

你如何找到正确的编码?如果幸运的话,它会包含在数据文档的某个地方。不幸的是,这种情况很少见,所以 readr 提供了 guess_encoding() 来帮助你找出它。它不是万无一失的,并且在你有很多文本时效果更好(不像这里),但它是一个合理的起点。预计你需要尝试几种不同的编码才能找到正确的那个。

编码是一个丰富而复杂的主题;我们在这里只触及了皮毛。如果你想了解更多,我们建议阅读 http://kunststube.net/encoding/ 上的详细解释。

14.6.2 字母变体

在处理带重音的语言时,确定字母的位置(例如,使用 str_length()str_sub())会带来一个重大挑战,因为带重音的字母可能被编码为单个独立的字符(例如,ü),或者通过组合一个不带重音的字母(例如,u)和一个变音符号(例如,¨)来编码为两个字符。例如,这段代码显示了两种看起来相同的 ü 的表示方法:

u <- c("\u00fc", "u\u0308")
str_view(u)
#> [1] │ ü
#> [2] │ ü

但这两个字符串的长度不同,并且它们的第一个字符也不同:

str_length(u)
#> [1] 1 2
str_sub(u, 1, 1)
#> [1] "ü" "u"

最后,请注意,使用 == 比较这些字符串会将它们解释为不同的,而 stringr 中方便的 str_equal() 函数则能识别出它们俩具有相同的外观:

u[[1]] == u[[2]]
#> [1] FALSE

str_equal(u[[1]], u[[2]])
#> [1] TRUE

14.6.3 依赖于区域设置的函数

最后,还有一些 stringr 函数的行为取决于你的区域设置 (locale)。区域设置类似于一种语言,但包含一个可选的地区说明符,以处理一种语言内部的地区差异。区域设置由一个小写的语言缩写指定,后面可以选择性地跟一个 _ 和一个大写的地区标识符。例如,“en” 是英语,“en_GB” 是英式英语,“en_US” 是美式英语。如果你还不知道你语言的代码,维基百科 有一个很好的列表,你可以通过查看 stringi::stri_locale_list() 来看看 stringr 支持哪些。

R base 的字符串函数会自动使用你的操作系统设置的区域设置。这意味着 R base 的字符串函数会按照你期望的方式对你的语言进行操作,但如果你的代码与生活在不同国家的人共享,它可能会有不同的表现。为了避免这个问题,stringr 默认使用 “en” 区域设置的英语规则,并要求你指定 locale 参数来覆盖它。幸运的是,只有两组函数的区域设置真正重要:改变大小写和排序。

改变大小写的规则在不同语言中有所不同。例如,土耳其语中有两个 i:带点和不带点的。由于它们是两个不同的字母,它们的大小写转换也不同:

str_to_upper(c("i", "ı"))
#> [1] "I" "I"
str_to_upper(c("i", "ı"), locale = "tr")
#> [1] "İ" "I"

对字符串进行排序取决于字母表的顺序,而字母表的顺序在每种语言中都不尽相同9!这里有一个例子:在捷克语中,“ch” 是一个复合字母,它在字母表中出现在 h 之后。

str_sort(c("a", "c", "ch", "h", "z"))
#> [1] "a"  "c"  "ch" "h"  "z"
str_sort(c("a", "c", "ch", "h", "z"), locale = "cs")
#> [1] "a"  "c"  "h"  "ch" "z"

这在使用 dplyr::arrange() 对字符串进行排序时也会出现,这就是为什么它也有一个 locale 参数。

14.7 总结

在本章中,你已经了解了 stringr 包的一些强大功能:如何创建、组合和提取字符串,以及在处理非英语字符串时可能面临的一些挑战。现在是时候学习一个处理字符串最重要、最强大的工具之一了:正则表达式。正则表达式是一种非常简洁但表达能力极强的语言,用于描述字符串内的模式,它是下一章的主题。


  1. 或者使用 R base 函数 writeLines()↩︎

  2. R 4.0.0 及以上版本可用。↩︎

  3. str_view() 还使用颜色来提请你注意制表符、空格、匹配项等。这些颜色目前在书中不显示,但你在交互式运行代码时会注意到它们。↩︎

  4. 如果你没有使用 stringr,你也可以直接用 glue::glue() 来访问它。↩︎

  5. R base 中与之等价的是使用 collapse 参数的 paste()↩︎

  6. 同样的原则也适用于 separate_wider_position()separate_wider_regex()↩︎

  7. 查看这些条目,我们猜测 babynames 数据丢弃了空格或连字符,并在 15 个字母后截断。↩︎

  8. 在这里,我使用特殊的 \x 将二进制数据直接编码到字符串中。↩︎

  9. 在没有字母表的语言(如中文)中排序则更为复杂。↩︎