14 Chuỗi ký tự
14.1 Giới thiệu
Cho đến nay, bạn đã sử dụng khá nhiều string (string) mà chưa tìm hiểu nhiều về chi tiết. Bây giờ là lúc đi sâu vào chúng, tìm hiểu cách string hoạt động, và nắm vững một số công cụ xử lý string mạnh mẽ mà bạn có trong tay.
Chúng ta sẽ bắt đầu với chi tiết về cách tạo string và vector ký tự (character vector). Sau đó bạn sẽ tìm hiểu cách tạo string từ dữ liệu, rồi ngược lại: trích xuất string từ dữ liệu. Tiếp theo chúng ta sẽ thảo luận về các công cụ làm việc với từng ký tự riêng lẻ. Chương này kết thúc với các function làm việc với từng ký tự riêng lẻ và một cuộc thảo luận ngắn về những chỗ mà kỳ vọng từ tiếng Anh có thể khiến bạn đi sai hướng khi làm việc với các ngôn ngữ khác.
Chúng ta sẽ tiếp tục làm việc với string trong chương tiếp theo, nơi bạn sẽ tìm hiểu thêm về sức mạnh của regular expression.
14.1.1 Điều kiện tiên quyết
Trong chương này, chúng ta sẽ sử dụng các function từ package stringr, là một phần của tidyverse cốt lõi. Chúng ta cũng sẽ sử dụng dữ liệu babynames vì nó cung cấp một số string thú vị để thao tác.
Bạn có thể nhanh chóng nhận ra khi nào mình đang sử dụng function stringr vì tất cả các function stringr đều bắt đầu bằng str_. Điều này đặc biệt hữu ích nếu bạn dùng RStudio vì gõ str_ sẽ kích hoạt tự động hoàn thành, giúp bạn nhớ lại các function có sẵn.

14.2 Tạo string
Chúng ta đã tạo string một cách sơ lược ở phần trước của cuốn sách nhưng chưa thảo luận chi tiết. Trước tiên, bạn có thể tạo một string bằng dấu nháy đơn (') hoặc dấu nháy kép ("). Không có sự khác biệt về hành vi giữa hai loại, vì vậy để nhất quán, hướng dẫn phong cách tidyverse khuyến nghị sử dụng ", trừ khi string chứa nhiều ".
string1 <- "This is a string"
string2 <- 'If I want to include a "quote" inside a string, I use single quotes'Nếu bạn quên đóng dấu nháy, bạn sẽ thấy +, dấu nhắc tiếp tục:
> "This is a string without a closing quote
+
+
+ HELP I'M STUCK IN A STRING
Nếu điều này xảy ra với bạn và bạn không thể tìm ra dấu nháy nào cần đóng, hãy nhấn Escape để hủy và thử lại.
14.2.1 Ký tự thoát
Để chèn dấu nháy đơn hoặc nháy kép theo nghĩa đen vào string, bạn có thể sử dụng \ để “thoát” (escape) nó:
double_quote <- "\"" # or '"'
single_quote <- '\'' # or "'"Vì vậy nếu bạn muốn chèn dấu gạch chéo ngược theo nghĩa đen vào string, bạn sẽ cần thoát nó: "\\":
backslash <- "\\"Lưu ý rằng biểu diễn in ra của string không giống với bản thân string vì biểu diễn in ra hiển thị các ký tự thoát (nói cách khác, khi bạn in một string, bạn có thể sao chép và dán kết quả để tái tạo string đó). Để xem nội dung thực của string, hãy dùng str_view()1:
14.2.2 Chuỗi thô
Tạo string với nhiều dấu nháy hoặc dấu gạch chéo ngược sẽ nhanh chóng trở nên rối rắm. Để minh họa vấn đề, hãy tạo một string chứa nội dung của khối mã nơi chúng ta định nghĩa biến double_quote và single_quote:
tricky <- "double_quote <- \"\\\"\" # or '\"'
single_quote <- '\\'' # or \"'\""
str_view(tricky)
#> [1] │ double_quote <- "\"" # or '"'
#> │ single_quote <- '\'' # or "'"Quá nhiều dấu gạch chéo ngược! (Hiện tượng này đôi khi được gọi là hội chứng tăm nghiêng.) Để loại bỏ việc thoát ký tự, bạn có thể sử dụng string thô (raw string)2:
tricky <- r"(double_quote <- "\"" # or '"'
single_quote <- '\'' # or "'")"
str_view(tricky)
#> [1] │ double_quote <- "\"" # or '"'
#> │ single_quote <- '\'' # or "'"Chuỗi thô thường bắt đầu bằng r"( và kết thúc bằng )". Nhưng nếu string của bạn chứa )" thì bạn có thể dùng r"[]" hoặc r"{}", và nếu vẫn chưa đủ, bạn có thể chèn bất kỳ số lượng dấu gạch ngang nào để tạo cặp mở và đóng duy nhất, ví dụ r"--()--", r"---()---", v.v. Chuỗi thô đủ linh hoạt để xử lý bất kỳ văn bản nào.
14.2.3 Các ký tự đặc biệt khác
Ngoài ", ', và \, còn một số ký tự đặc biệt khác có thể hữu ích. Phổ biến nhất là \n, xuống dòng mới, và \t, tab. Đôi khi bạn cũng sẽ thấy các string chứa ký tự Unicode thoát bắt đầu bằng \u hoặc \U. Đây là cách viết các ký tự không phải tiếng Anh hoạt động trên mọi hệ thống. Bạn có thể xem list đầy đủ các ký tự đặc biệt khác trong ?Quotes.
Lưu ý rằng str_view() sử dụng dấu ngoặc nhọn cho tab để dễ nhận biết hơn3. Một trong những thách thức khi làm việc với văn bản là có nhiều cách khác nhau mà khoảng trắng có thể xuất hiện trong văn bản, vì vậy nền này giúp bạn nhận ra khi có điều gì đó bất thường.
14.2.4 Bài tập
-
Tạo các string chứa các giá trị sau:
He said "That's amazing!"\a\b\c\d\\\\\\
-
Tạo string sau trong phiên R của bạn và in nó ra. Điều gì xảy ra với ký tự đặc biệt “\u00a0”?
str_view()hiển thị nó như thế nào? Bạn có thể tìm kiếm trên Google để biết ký tự đặc biệt này là gì không?x <- "This\u00a0is\u00a0tricky"
14.3 Tạo nhiều string từ dữ liệu
Bây giờ bạn đã học những kiến thức cơ bản về cách tạo một hoặc hai string “bằng tay”, chúng ta sẽ đi vào chi tiết cách tạo string từ các string khác. Điều này sẽ giúp bạn giải quyết vấn đề phổ biến khi bạn có một đoạn văn bản muốn kết hợp với các string từ data frame. Ví dụ, bạn có thể kết hợp “Hello” với biến name để tạo lời chào. Chúng tôi sẽ chỉ bạn cách thực hiện điều này với str_c() và str_glue() cũng như cách sử dụng chúng với mutate(). Điều đó tự nhiên dẫn đến câu hỏi bạn có thể dùng function stringr nào với summarize(), vì vậy chúng ta sẽ kết thúc phần này bằng thảo luận về str_flatten(), một function tóm tắt cho string.
14.3.1 str_c()
str_c() nhận bất kỳ số lượng vector nào làm argument (argument) và trả về một vector ký tự:
str_c() rất giống với function cơ bản paste0(), nhưng được thiết kế để dùng với mutate() bằng cách tuân theo các quy tắc tidyverse thông thường về tái chế (recycling) và lan truyền missing value:
Nếu bạn muốn missing value hiển thị theo cách khác, hãy dùng coalesce() để thay thế chúng. Tùy thuộc vào mục đích, bạn có thể dùng nó bên trong hoặc bên ngoài 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()
Nếu bạn trộn nhiều string cố định và biến với str_c(), bạn sẽ nhận thấy phải gõ rất nhiều ", khiến khó nhìn thấy mục tiêu tổng thể của đoạn mã. Một cách tiếp cận thay thế được cung cấp bởi package glue thông qua str_glue()4. Bạn đưa cho nó một string duy nhất có tính năng đặc biệt: bất kỳ thứ gì bên trong {} sẽ được đánh giá như thể nằm bên ngoài dấu nháy:
Như bạn thấy, str_glue() hiện chuyển missing value thành string "NA", đáng tiếc là không nhất quán với str_c().
Bạn cũng có thể thắc mắc điều gì xảy ra nếu cần chèn { hoặc } thông thường vào string. Bạn đang đi đúng hướng nếu đoán rằng cần thoát chúng bằng cách nào đó. Mẹo là glue sử dụng kỹ thuật thoát hơi khác: thay vì thêm tiền tố bằng ký tự đặc biệt như \, bạn viết gấp đôi các ký tự đặc biệt:
14.3.3 str_flatten()
str_c() và str_glue() hoạt động tốt với mutate() vì đầu ra của chúng có cùng độ dài với đầu vào. Nếu bạn muốn một function hoạt động tốt với summarize(), tức là function luôn trả về một string duy nhất thì sao? Đó là công việc của str_flatten()5: nó nhận một vector ký tự và kết hợp từng phần tử của vector thành một string duy nhất:
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"Điều này giúp nó hoạt động tốt với 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, mandarin14.3.4 Bài tập
-
So sánh và đối chiếu kết quả của
paste0()vớistr_c()cho các đầu vào sau: Sự khác biệt giữa
paste()vàpaste0()là gì? Làm thế nào bạn có thể tái tạo tương đươngpaste()bằngstr_c()?-
Chuyển đổi các biểu thức sau từ
str_c()sangstr_glue()hoặc ngược lại:str_c("The price of ", food, " is ", price)str_glue("I'm {age} years old and live in {country}")str_c("\\section{", title, "}")
14.4 Trích xuất dữ liệu từ string
Rất phổ biến khi nhiều biến được nhồi nhét vào một string duy nhất. Trong phần này, bạn sẽ học cách sử dụng bốn function tidyr để trích xuất chúng:
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)
Nếu nhìn kỹ, bạn có thể thấy có một mẫu chung ở đây: separate_, sau đó longer hoặc wider, rồi _, tiếp theo là delim hoặc position. Đó là vì bốn function này được tạo thành từ hai nguyên thủy đơn giản hơn:
- Giống như với
pivot_longer()vàpivot_wider(), các function_longerlàm data frame đầu vào dài hơn bằng cách tạo row mới và các function_widerlàm data frame đầu vào rộng hơn bằng cách tạo column mới. -
delimtách string bằng ký tự phân cách (delimiter) như", "hoặc" ";positiontách tại các độ rộng được chỉ định, nhưc(3, 5, 2).
Chúng ta sẽ quay lại thành viên cuối cùng của họ function này, separate_wider_regex(), trong Chương 15. Đây là function linh hoạt nhất trong các function wider, nhưng bạn cần biết về regular expression trước khi có thể sử dụng nó.
Hai phần tiếp theo sẽ cho bạn ý tưởng cơ bản về các function tách này, đầu tiên là tách thành row (đơn giản hơn một chút) và sau đó tách thành column. Chúng ta sẽ kết thúc bằng thảo luận về các công cụ mà các function wider cung cấp để chẩn đoán vấn đề.
14.4.1 Tách thành hàng
Tách string thành row thường hữu ích nhất khi số lượng thành phần thay đổi giữa các row. Trường hợp phổ biến nhất là sử dụng separate_longer_delim() để tách dựa trên ký tự phân cách:
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 fHiếm hơn khi thấy separate_longer_position() trong thực tế, nhưng một số tập dữ liệu cũ sử dụng định dạng rất gọn trong đó mỗi ký tự được dùng để ghi lại một giá trị:
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 rows14.4.2 Tách thành column
Tách string thành column thường hữu ích nhất khi có số lượng thành phần cố định trong mỗi string và bạn muốn trải chúng thành các column. Chúng phức tạp hơn một chút so với các function longer tương ứng vì bạn cần đặt tên cho các column. Ví dụ, trong tập dữ liệu sau, x được tạo thành từ mã, số phiên bản, và năm, phân cách bằng ".". Để sử dụng separate_wider_delim(), chúng ta cung cấp ký tự phân cách và tên trong hai argument:
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 2015Nếu một phần cụ thể không hữu ích, bạn có thể dùng tên NA để bỏ nó khỏi kết quả:
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 2015separate_wider_position() hoạt động hơi khác vì thông thường bạn muốn chỉ định độ rộng của mỗi column. Vì vậy bạn đưa cho nó một vector số nguyên có tên, trong đó tên đặt tên cho column mới, và giá trị là số ký tự mà nó chiếm. Bạn có thể bỏ giá trị khỏi đầu ra bằng cách không đặt tên cho chúng:
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 CA14.4.3 Chẩn đoán vấn đề khi mở rộng
separate_wider_delim()6 yêu cầu một tập column cố định và đã biết. Điều gì xảy ra nếu một số row không có số lượng phần như mong đợi? Có hai vấn đề có thể xảy ra, quá ít hoặc quá nhiều phần, vì vậy separate_wider_delim() cung cấp hai argument để giúp: too_few và too_many. Hãy xem trường hợp too_few trước với tập dữ liệu mẫu sau:
df <- tibble(a = c("1-1-1", "1-1-2", "1-3", "1-3-2", "1"))
df |>
separate_wider_delim(
a,
delim = "-",
names = c("x", "y", "z")
)
#> Error in `separate_wider_delim()`:
#> ! Expected 3 pieces in each element of `a`.
#> ! 2 values were too short.
#> ℹ Use `too_few = "debug"` to diagnose the problem.
#> ℹ Use `too_few = "align_start"/"align_end"` to silence this message.Bạn sẽ thấy rằng chúng ta nhận được lỗi, nhưng thông báo lỗi đưa ra một số gợi ý về cách bạn có thể tiến hành. Hãy bắt đầu bằng cách gỡ lỗi vấn đề:
debug <- df |>
separate_wider_delim(
a,
delim = "-",
names = c("x", "y", "z"),
too_few = "debug"
)
#> Warning: Debug mode activated: adding variables `a_ok`, `a_pieces`, and
#> `a_remainder`.
debug
#> # A tibble: 5 × 7
#> x y z a a_ok a_pieces a_remainder
#> <chr> <chr> <chr> <chr> <lgl> <int> <chr>
#> 1 1 1 1 1-1-1 TRUE 3 ""
#> 2 1 1 2 1-1-2 TRUE 3 ""
#> 3 1 3 <NA> 1-3 FALSE 2 ""
#> 4 1 3 2 1-3-2 TRUE 3 ""
#> 5 1 <NA> <NA> 1 FALSE 1 ""Khi bạn sử dụng chế độ gỡ lỗi (debug), ba column phụ được thêm vào đầu ra: a_ok, a_pieces, và a_remainder (nếu bạn tách biến có tên khác, bạn sẽ nhận được tiền tố khác). Ở đây, a_ok cho phép bạn nhanh chóng tìm các đầu vào bị lỗi:
debug |> filter(!a_ok)
#> # A tibble: 2 × 7
#> x y z a a_ok a_pieces a_remainder
#> <chr> <chr> <chr> <chr> <lgl> <int> <chr>
#> 1 1 3 <NA> 1-3 FALSE 2 ""
#> 2 1 <NA> <NA> 1 FALSE 1 ""a_pieces cho biết có bao nhiêu phần được tìm thấy, so với 3 phần mong đợi (độ dài của names). a_remainder không hữu ích khi có quá ít phần, nhưng chúng ta sẽ thấy nó lại ngay.
Đôi khi xem thông tin gỡ lỗi này sẽ tiết lộ vấn đề với chiến lược phân cách hoặc gợi ý rằng bạn cần tiền xử lý thêm trước khi tách. Trong trường hợp đó, hãy sửa vấn đề từ đầu và nhớ xóa too_few = "debug" để đảm bảo các vấn đề mới trở thành lỗi.
Trong các trường hợp khác, bạn có thể muốn điền các phần thiếu bằng NA và tiếp tục. Đó là công việc của too_few = "align_start" và too_few = "align_end" cho phép bạn kiểm soát vị trí đặt NA:
df |>
separate_wider_delim(
a,
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>Các nguyên tắc tương tự áp dụng nếu bạn có quá nhiều phần:
df <- tibble(a = c("1-1-1", "1-1-2", "1-3-5-6", "1-3-2", "1-3-5-7-9"))
df |>
separate_wider_delim(
a,
delim = "-",
names = c("x", "y", "z")
)
#> Error in `separate_wider_delim()`:
#> ! Expected 3 pieces in each element of `a`.
#> ! 2 values were too long.
#> ℹ Use `too_many = "debug"` to diagnose the problem.
#> ℹ Use `too_many = "drop"/"merge"` to silence this message.Nhưng bây giờ, khi chúng ta gỡ lỗi kết quả, bạn có thể thấy mục đích của a_remainder:
debug <- df |>
separate_wider_delim(
a,
delim = "-",
names = c("x", "y", "z"),
too_many = "debug"
)
#> Warning: Debug mode activated: adding variables `a_ok`, `a_pieces`, and
#> `a_remainder`.
debug |> filter(!a_ok)
#> # A tibble: 2 × 7
#> x y z a a_ok a_pieces a_remainder
#> <chr> <chr> <chr> <chr> <lgl> <int> <chr>
#> 1 1 3 5 1-3-5-6 FALSE 4 -6
#> 2 1 3 5 1-3-5-7-9 FALSE 5 -7-9Bạn có một tập tùy chọn hơi khác để xử lý quá nhiều phần: bạn có thể âm thầm “bỏ” các phần thừa hoặc “gộp” tất cả vào column cuối cùng:
df |>
separate_wider_delim(
a,
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(
a,
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-914.5 Ký tự
Trong phần này, chúng tôi sẽ giới thiệu cho bạn các function cho phép làm việc với từng ký tự riêng lẻ trong string. Bạn sẽ học cách tìm độ dài của string, trích xuất string con, và xử lý string dài trong biểu đồ và bảng.
14.5.1 Độ dài
str_length() cho bạn biết số lượng ký tự trong string:
str_length(c("a", "R for data science", NA))
#> [1] 1 18 NABạn có thể sử dụng function này với count() để tìm phân phối độ dài tên trẻ em Mỹ và sau đó dùng filter() để xem các tên dài nhất, có 15 ký tự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 rows14.5.2 Trích xuất string con
Bạn có thể trích xuất các phần của string bằng str_sub(string, start, end), trong đó start và end là vị trí bắt đầu và kết thúc của string con. Các argument start và end bao gồm cả hai đầu, vì vậy độ dài string trả về sẽ là end - start + 1:
Bạn có thể dùng giá trị âm để đếm ngược từ cuối string: -1 là ký tự cuối cùng, -2 là ký tự kế cuối, v.v.
str_sub(x, -3, -1)
#> [1] "ple" "ana" "ear"Lưu ý rằng str_sub() sẽ không báo lỗi nếu string quá ngắn: nó sẽ trả về nhiều nhất có thể:
str_sub("a", 1, 5)
#> [1] "a"Chúng ta có thể dùng str_sub() với mutate() để tìm chữ cái đầu tiên và cuối cùng của mỗi tên:
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 rows14.5.3 Bài tập
- Khi tính phân phối độ dài của babynames, tại sao chúng ta dùng
wt = n? - Sử dụng
str_length()vàstr_sub()để trích xuất chữ cái ở giữa mỗi tên trẻ em. Bạn sẽ làm gì nếu string có số ký tự chẵn? - Có xu hướng lớn nào trong độ dài tên trẻ em theo thời gian không? Còn mức độ phổ biến của chữ cái đầu và cuối thì sao?
14.6 Văn bản không phải tiếng Anh
Cho đến nay, chúng ta đã tập trung vào văn bản tiếng Anh, đặc biệt dễ làm việc vì hai lý do. Thứ nhất, bảng chữ cái tiếng Anh tương đối đơn giản: chỉ có 26 chữ cái. Thứ hai (và có lẽ quan trọng hơn), hạ tầng máy tính mà chúng ta sử dụng ngày nay chủ yếu được thiết kế bởi người nói tiếng Anh. Đáng tiếc, chúng tôi không có đủ chỗ để trình bày đầy đủ về các ngôn ngữ không phải tiếng Anh. Tuy nhiên, chúng tôi muốn lưu ý bạn về một số thách thức lớn nhất mà bạn có thể gặp phải: mã hóa (encoding), biến thể chữ cái, và các function phụ thuộc vào ngôn ngữ địa phương (locale).
14.6.1 Mã hóa
Khi làm việc với văn bản không phải tiếng Anh, thách thức đầu tiên thường là mã hóa (encoding). Để hiểu chuyện gì đang xảy ra, chúng ta cần tìm hiểu cách máy tính biểu diễn string. Trong R, chúng ta có thể xem biểu diễn bên dưới của string bằng charToRaw():
charToRaw("Hadley")
#> [1] 48 61 64 6c 65 79Mỗi trong sáu số thập lục phân này đại diện cho một chữ cái: 48 là H, 61 là a, và tiếp tục. Ánh xạ từ số thập lục phân sang ký tự được gọi là mã hóa, và trong trường hợp này, mã hóa được gọi là ASCII. ASCII biểu diễn tốt các ký tự tiếng Anh vì nó là Mã Chuẩn Mỹ cho Trao đổi Thông tin.
Mọi thứ không dễ dàng như vậy đối với các ngôn ngữ không phải tiếng Anh. Trong những ngày đầu của điện toán, có nhiều tiêu chuẩn cạnh tranh để mã hóa các ký tự không phải tiếng Anh. Ví dụ, có hai mã hóa khác nhau cho châu Âu: Latin1 (hay ISO-8859-1) được dùng cho các ngôn ngữ Tây Âu, và Latin2 (hay ISO-8859-2) được dùng cho các ngôn ngữ Trung Âu. Trong Latin1, byte b1 là “±”, nhưng trong Latin2, nó là “ą”! May mắn thay, ngày nay có một tiêu chuẩn được hỗ trợ gần như ở mọi nơi: UTF-8. UTF-8 có thể mã hóa hầu hết mọi ký tự được con người sử dụng ngày nay và nhiều ký hiệu bổ sung như emoji.
readr sử dụng UTF-8 ở mọi nơi. Đây là mặc định tốt nhưng sẽ thất bại với dữ liệu được tạo bởi các hệ thống cũ không sử dụng UTF-8. Nếu điều này xảy ra, string của bạn sẽ trông kỳ lạ khi in ra. Đôi khi chỉ một hoặc hai ký tự bị sai; lúc khác, bạn sẽ nhận được toàn bộ ký tự vô nghĩa. Ví dụ đây là hai CSV inline với mã hóa bất thường8:
Để đọc chúng chính xác, bạn chỉ định mã hóa qua argument locale:
Làm thế nào để tìm mã hóa đúng? Nếu may mắn, nó sẽ được ghi trong tài liệu dữ liệu. Đáng tiếc, điều đó hiếm khi xảy ra, vì vậy readr cung cấp guess_encoding() để giúp bạn tìm ra. Nó không hoàn hảo và hoạt động tốt hơn khi bạn có nhiều văn bản (không như ở đây), nhưng đó là điểm khởi đầu hợp lý. Hãy chuẩn bị thử vài mã hóa khác nhau trước khi tìm được đúng.
Mã hóa là theme phong phú và phức tạp; chúng ta mới chỉ chạm vào bề mặt ở đây. Nếu bạn muốn tìm hiểu thêm, chúng tôi khuyến nghị đọc giải thích chi tiết tại http://kunststube.net/encoding/.
14.6.2 Biến thể chữ cái
Làm việc với các ngôn ngữ có dấu đặt ra thách thức đáng kể khi xác định vị trí ký tự (ví dụ, với str_length() và str_sub()) vì các chữ cái có dấu có thể được mã hóa thành một ký tự đơn lẻ (ví dụ, ü) hoặc thành hai ký tự bằng cách kết hợp chữ cái không dấu (ví dụ, u) với dấu phụ (ví dụ, ¨). Ví dụ, đoạn mã này cho thấy hai cách biểu diễn ü trông giống hệt nhau:
Nhưng cả hai string có độ dài khác nhau, và ký tự đầu tiên của chúng cũng khác:
str_length(u)
#> [1] 1 2
str_sub(u, 1, 1)
#> [1] "ü" "u"Cuối cùng, lưu ý rằng phép so sánh các string này bằng == coi chúng là khác nhau, trong khi function str_equal() tiện lợi trong stringr nhận ra rằng cả hai có cùng hình dạng:
u[[1]] == u[[2]]
#> [1] FALSE
str_equal(u[[1]], u[[2]])
#> [1] TRUE14.6.3 Các function phụ thuộc ngôn ngữ địa phương
Cuối cùng, có một số function stringr mà hành vi phụ thuộc vào ngôn ngữ địa phương (locale) của bạn. Ngôn ngữ địa phương tương tự như ngôn ngữ nhưng bao gồm một chỉ định vùng tùy chọn để xử lý biến thể trong cùng một ngôn ngữ. Ngôn ngữ địa phương được chỉ định bằng chữ viết tắt ngôn ngữ viết thường, tùy chọn theo sau bởi _ và mã vùng viết hoa. Ví dụ, “en” là tiếng Anh, “en_GB” là tiếng Anh Anh, và “en_US” là tiếng Anh Mỹ. Nếu bạn chưa biết mã cho ngôn ngữ của mình, Wikipedia có list tốt, và bạn có thể xem ngôn ngữ nào được hỗ trợ trong stringr bằng cách xem stringi::stri_locale_list().
Các function string base R tự động sử dụng ngôn ngữ địa phương do hệ điều hành thiết lập. Điều này có nghĩa là các function string base R hoạt động đúng như bạn mong đợi cho ngôn ngữ của mình, nhưng mã của bạn có thể hoạt động khác nếu chia sẻ với ai đó sống ở quốc gia khác. Để tránh vấn đề này, stringr mặc định sử dụng quy tắc tiếng Anh bằng cách dùng locale “en” và yêu cầu bạn chỉ định argument locale để ghi đè. May mắn thay, chỉ có hai nhóm function mà ngôn ngữ địa phương thực sự quan trọng: đổi chữ hoa chữ thường và sắp xếp.
Quy tắc đổi chữ hoa chữ thường khác nhau giữa các ngôn ngữ. Ví dụ, tiếng Thổ Nhĩ Kỳ có hai chữ i: có và không có dấu chấm. Vì chúng là hai chữ cái riêng biệt, chúng được viết hoa khác nhau:
str_to_upper(c("i", "ı"))
#> [1] "I" "I"
str_to_upper(c("i", "ı"), locale = "tr")
#> [1] "İ" "I"Sắp xếp string phụ thuộc vào thứ tự bảng chữ cái, và thứ tự bảng chữ cái không giống nhau trong mọi ngôn ngữ9! Đây là một ví dụ: trong tiếng Séc, “ch” là một chữ cái ghép xuất hiện sau h trong bảng chữ cái.
Điều này cũng xuất hiện khi sắp xếp string với dplyr::arrange(), đó là lý do tại sao nó cũng có argument locale.
14.7 Tóm tắt
Trong chương này, bạn đã tìm hiểu về một số sức mạnh của package stringr: cách tạo, kết hợp, và trích xuất string, cũng như một số thách thức bạn có thể gặp với string không phải tiếng Anh. Bây giờ là lúc tìm hiểu một trong những công cụ quan trọng và mạnh mẽ nhất để làm việc với string: regular expression. Biểu thức chính quy là ngôn ngữ rất ngắn gọn nhưng rất biểu cảm để mô tả các mẫu trong string và là theme của chương tiếp theo.
Hoặc dùng function base R
writeLines().↩︎Có sẵn trong R 4.0.0 trở lên.↩︎
str_view()cũng sử dụng màu sắc để làm nổi bật tab, khoảng trắng, kết quả khớp, v.v. Màu sắc hiện không hiển thị trong sách, nhưng bạn sẽ thấy chúng khi chạy mã tương tác.↩︎Nếu bạn không dùng stringr, bạn cũng có thể truy cập trực tiếp bằng
glue::glue().↩︎Function base R tương đương là
paste()được dùng với argumentcollapse.↩︎Các nguyên tắc tương tự áp dụng cho
separate_wider_position()vàseparate_wider_regex().↩︎Nhìn vào các mục này, chúng tôi đoán rằng dữ liệu babynames bỏ khoảng trắng hoặc dấu gạch nối và cắt ngắn sau 15 ký tự.↩︎
Ở đây tôi sử dụng
\xđặc biệt để mã hóa dữ liệu nhị phân trực tiếp vào string.↩︎Sắp xếp trong các ngôn ngữ không có bảng chữ cái, như tiếng Trung, còn phức tạp hơn nữa.↩︎