關於文字

Tidy datasets are all alike, but every messy dataset is messy in its own way.

Hadley Wickham

學習程式語言的第一個章節通常是認識變數型別,這個名詞聽起來陌生,簡單的想法是將它視為一種純量(scalar)的資料樣式,如果匯集了多個純量就能夠組合成為我們先前在認識常見的資料結構中所介紹的各種資料結構;不論是寫作 Python、R 語言或其他程式語言,或多或少都必須要暸解三個大類:

  • 數值:可再細分為整數、浮點數或複數等的值
  • 文字:以單引號或雙引號包括起來的值
  • 布林:僅包含真、假判斷的二元值

對多數資料科學團隊來說面對以及處理文字是工作中非常重要的一環,因為不論是清理從網路上擷取而得的資料(從 html 擷取的資料都為文字)、合併從資料庫查詢所得的表格或者整備要進行探勘的文本,我們處理許多包含文字的資料結構。

建立

使用單引號或雙引號將值包括起來,不論是在引號中放置數值、文字或者布林,都會以文字型別儲存。

Python

在 Python 文字的型別稱為 str ,是 string 的簡寫。

asset_tony_stark = "12.4 billion"
print(type(asset_tony_stark))
asset_tony_stark = "12400000000"
print(type(asset_tony_stark))
tony_stark_is_rich = "True"
print(type(tony_stark_is_rich))
1
2
3
4
5
6
## <class 'str'>
## <class 'str'>
## <class 'str'>
1
2
3

R 語言

在 R 語言文字的型別稱為 character。

asset_tony_stark <- "12.4 billion"
class(asset_tony_stark)
asset_tony_stark <- "12400000000"
class(asset_tony_stark)
tony_stark_is_rich <- "True"
class(tony_stark_is_rich)
1
2
3
4
5
6
## [1] "character"
## [1] "character"
## [1] "character"
1
2
3

在所有狀況下單引號與雙引號都可以任意使用嗎?其實不是的,在文字內容中有出現雙引號或單引號的時候,就要特別留意,像是 NBA 球星俠客歐尼爾 Shaquille O’Neal 的姓氏 O’Neal 有一個單引號,如果貿然使用單引號包括他的姓名,就會產生錯誤。

Python

print('Shaquille O'Neal')
1
## SyntaxError: invalid syntax
1

R 語言

'Shaquille O'Neal'
1
## Error: unexpected symbol in "'Shaquille O'Neal"
1

Python

這個時候有兩種解法,一種是在 O’Neal 的單引號前面加上跳脫符號 \ 另一種則是改以雙引號包括姓名:

print("Shaquille O'Neal")
print('Shaquille O\'Neal')
1
2
## Shaquille O'Neal
## Shaquille O'Neal
1
2

R 語言

"Shaquille O'Neal"
'Shaquille O\'Neal'
1
2
## [1] "Shaquille O'Neal"
## [1] "Shaquille O'Neal"
1
2

Python

同樣道理也可以套用至文字中有雙引號或者兩種引號都有的文字段落中:

print("Okay. Let's put aside the fact that you \"accidentally\" picked up my grandmother's ring and you \"accidentally\" proposed to Rachel.")
1
## Okay. Let's put aside the fact that you "accidentally" picked up my grandmother's ring and you "accidentally" proposed to Rachel.
1

R 語言

writeLines("Okay. Let's put aside the fact that you \"accidentally\" picked up my grandmother's ring and you \"accidentally\" proposed to Rachel.")
1
## Okay. Let's put aside the fact that you "accidentally" picked up my grandmother's ring and you "accidentally" proposed to Rachel.
1

量測長度

Python

在 Python 中 len() 函數除了可以量測陣列的長度也能夠量測單一文字的長度。

shaq = "Shaquille O'Neal"
print(len(shaq))
1
2
## 16
1

R 語言

在 R 語言中 length() 函數僅能用來量測陣列長度,若要用來量測單一文字的長度,則應該使用 nchar() 函數,即 number of characters 的縮寫。

shaq <- "Shaquille O'Neal"
nchar(shaq)
1
2
## [1] 16
1

值得注意的是,即便是空格或者單引號,也都佔有長度 1,因此雖然 shaq 中只有 14 個英文字母,但長度計算為 16。

調整大小寫

Python

在 Python 中可以使用幾個方法調整英文字母的大小寫:

  • .upper():全數變成大寫
  • .lower():全數變成小寫
  • .title():單字字首大寫
  • .capitalize():字首變成大寫
  • .swapcase():大小寫轉換
shaq = "Shaquille O'Neal"
print(shaq.upper())
print(shaq.lower())
print(shaq.lower().title())
print(shaq.capitalize())
print(shaq.swapcase())
1
2
3
4
5
6
## SHAQUILLE O'NEAL
## shaquille o'neal
## Shaquille O'Neal
## Shaquille o'neal
## sHAQUILLE o'nEAL
1
2
3
4
5

R 語言

R 語言可以運用 toupper()tolower() 函數來調整大小寫。

shaq <- "Shaquille O'Neal"
toupper(shaq)
tolower(shaq)
1
2
3
## [1] "SHAQUILLE O'NEAL"
## [1] "shaquille o'neal"
1
2

去除多餘空格

從網頁或者資料庫擷取下來的文字資料,常會發生左邊或者右邊有多餘的空格。

Python

在 Python 中可以使用下列幾個方法將它們去除:

  • .lstrip():去除文字左邊的空格
  • .rstrip():去除文字右邊的空格
  • .strip():去除文字左邊與右邊的空格
shaq = "     Shaquille O'Neal     "
print(shaq)
print(shaq.lstrip())
print(shaq.rstrip())
print(shaq.strip())
1
2
3
4
5
##      Shaquille O'Neal     
## Shaquille O'Neal     
##      Shaquille O'Neal
## Shaquille O'Neal
1
2
3
4

R 語言

在 R 語言使用 trimws() 函數清除文字中多餘的空格,搭配 which 參數調整:

  • trimws(x, which = "left"):去除文字左邊的空格
  • trimws(x, which = "right"):去除文字右邊的空格
  • trimws(x, which = "both"):去除文字左邊與右邊的空格
shaq <- "     Shaquille O'Neal     "
trimws(shaq, which = "left")
trimws(shaq, which = "right")
trimws(shaq, which = "both")
1
2
3
4
## [1] "Shaquille O'Neal     "
## [1] "     Shaquille O'Neal"
## [1] "Shaquille O'Neal"
1
2
3

格式化輸出

完成一段程式撰寫後,常有需求將生成的變數輸出檢視,這時會利用格式化輸出(print with format)將結果以文字呈現。

Python

在 Python 中以大括號 {} 搭配 .format() 方法做格式化輸出:

asset_tony_stark = 12400000000
print("The net worth of Stark Industries is ${} USD.".format(asset_tony_stark))
print("The net worth of Stark Industries is ${:,} USD.".format(asset_tony_stark))
print("The net worth of Stark Industries is ${:,.2f} USD.".format(asset_tony_stark))
1
2
3
4
## The net worth of Stark Industries is $12400000000 USD.
## The net worth of Stark Industries is $12,400,000,000 USD.
## The net worth of Stark Industries is $12,400,000,000.00 USD.
1
2
3

R 語言

在 R 語言中透過 sprintf() 搭配 format() 函數做格式化輸出,其中值得注意的是在 R 語言中高量級數字像是 Stark Industries 的淨值,預設是以科學記號呈現的,使用 scientific = FALSE 設定可以取消科學記號呈現格式。

asset_tony_stark <- 12400000000
sprintf("The net worth of Stark Industries is $%s USD.", format(asset_tony_stark, scientific = FALSE))
sprintf("The net worth of Stark Industries is $%s USD.", format(asset_tony_stark, scientific = FALSE, big.mark = ","))
sprintf("The net worth of Stark Industries is $%s USD.", format(asset_tony_stark, scientific = FALSE, big.mark = ",", nsmall = 2))
1
2
3
4
## [1] "The net worth of Stark Industries is $12400000000 USD."
## [1] "The net worth of Stark Industries is $12,400,000,000 USD."
## [1] "The net worth of Stark Industries is $12,400,000,000.00 USD."
1
2
3

值得注意的地方是此刻我們以 % 符號來標記對哪個變數做格式化輸出,而當文字中真實要出現 % 文字時,得以 %% 來標記。

sprintf("相似度有 %s%% 像", 87)
1
## [1] "相似度有 87% 像"
1

擷取部份文字

資料科學團隊常需要利用文字中的某部分的協助判斷程式邏輯,例如身分證字號中的第二個文字,'1' 可以判斷是生理男性,'2' 可以判斷是生理女性;生日中的西元年份則可以協助判斷年齡。

Python

在 Python 中由於文字具備可迭代(iterable)的特性,所以使用中括號搭配索引值與 slicing 的技巧就能夠擷取出部分文字,特別要注意 Python 慣例中索引值由 0 起始、不包含終止值。

shaq = "Shaquille O'Neal"
nickname = shaq[:4]
family_name = shaq[10:]
print(nickname)
print(family_name)
1
2
3
4
5
## Shaq 
## O'Neal
1
2

R 語言

在 R 語言中可以使用 substr() 函數擷取部份文字,利用 startstop 參數來調整。


shaq <- "Shaquille O'Neal"
nickname <- substr(shaq, start = 1, stop = 4)
family_name <- substr(shaq, start = 11, stop = nchar(shaq))
nickname
family_name
1
2
3
4
5
6
## [1] "Shaq"
## [1] "O'Neal"
1
2

轉換為日期時間格式

同樣是日期時間的資訊,以日期時間格式的型別較之以文字型別儲存還具備了額外功能,像是支援運算與格式調整;資料科學團隊時常會利用 strptime 的技巧(String Parse Time)將文字解析成為日期時間;常見的 strptime 符號有:

  • %a:縮寫的星期幾,從 Sun 至 Sat
  • %A:全稱的星期幾,從 Sunday 至 Saturday
  • %b:縮寫的月份,從 Jan 至 Dec
  • %B:全稱的月份,從 January 至 December
  • %d:月份中的第幾天,從 01 至 31
  • %m:以兩位數字表示的月份,從 01 至 12
  • %Y:以四位數字表示的西元年份,從 0 至 9999
  • %H:以兩位數字表示的小時,從 00 至 23
  • %M:以兩位數字表示的分鐘,從 00 至 59
  • %S:以兩位數字表示的秒數,從 00 至 61

Python

在 Python 中可以利用 datetime 模組中的 datetime.strptime() 函數將文字轉換為日期時間格式,並進而利用 datetime 模組中的 timedelta() 函數來運算、利用 .strftime() 方法(意即 String Format Time)來調整格式。

from datetime import datetime, timedelta

first_day_of_2019 = datetime.strptime('2019-01-01', '%Y-%m-%d')
second_day_of_2019 = first_day_of_2019 + timedelta(days = 1)
last_day_of_2018 = first_day_of_2019 - timedelta(days = 1)
print(first_day_of_2019)
print(second_day_of_2019)
print(last_day_of_2018)
print(first_day_of_2019.strftime('%d, %B, %Y %H:%M:%S'))
1
2
3
4
5
6
7
8
9
## 2019-01-01 00:00:00
## 2019-01-02 00:00:00
## 2018-12-31 00:00:00
## 01, January, 2019 00:00:00
1
2
3
4

R 語言

在 R 語言中使用 as.Date() 函數或者 as.POSIXct() 函數來轉換為日期或日期時間格式,strptime 的技巧也能適用。R 語言會以電腦的語系設定決定時區,為了讓顯示結果一致,我將時區設為格林尼治(GMT + 0)而非預設的中原標準時間(GMT + 8)。

first_day_of_2019 <- as.Date('2019-01-01')
second_day_of_2019 <- first_day_of_2019 + 1
last_day_of_2018 <- first_day_of_2019 - 1
first_day_of_2019 <- as.POSIXct(first_day_of_2019)
format(first_day_of_2019, '%Y-%m-%d %H:%M:%S', tz = 'GMT')
format(second_day_of_2019, '%Y-%m-%d %H:%M:%S', tz = 'GMT')
format(last_day_of_2018, '%Y-%m-%d %H:%M:%S', tz = 'GMT')
format(first_day_of_2019, '%d, %B, %Y %H:%M:%S', tz = 'GMT')
1
2
3
4
5
6
7
8
## [1] "2019-01-01 00:00:00"
## [1] "2019-01-02 00:00:00"
## [1] "2018-12-31 00:00:00"
## [1] "01, January, 2019 00:00:00"
1
2
3
4

根據特徵分隔

在前述擷取部分文字的例子中,我們利用索引值將 NBA 球星姓名分開擷取出來,不過,在面對不同 NBA 球星每個人的姓氏、名字的長度都不一致,勢必要用更好的方式。

Python

在 Python 中可以利用 .split() 方法指定一個特徵來將一個文字分隔開來,並依序儲存在 list 之中。

shaq = "Shaquille O'Neal"
print(shaq.split(sep=" "))
shaq = "O'Neal, Shaquille"
print(shaq.split(sep=", "))
1
2
3
4
## ['Shaquille', "O'Neal"]
## ["O'Neal", 'Shaquille']
1
2

R 語言

在 R 語言可以使用 strsplit() 函數指定一個特徵來將一個文字分隔開來,並依序儲存在 list 之中。

shaq <- "Shaquille O'Neal"
strsplit(shaq, split = " ")
shaq <- "O'Neal, Shaquille"
strsplit(shaq, split = ", ")
1
2
3
4
## [[1]]
## [1] "Shaquille" "O'Neal"   

## [[1]]
## [1] "O'Neal"    "Shaquille"
1
2
3
4
5

判斷特徵存在與否及存在之位置

如同在檢索文件或網頁時常用的搜尋功能,我們也常需要在文字中判斷某些特徵或關鍵字是否有出現其中。

Python

在 Python 只要使用 in 運算符號可以判斷是否存在、使用 .find() 方法可以判斷第一個出現的索引值在何處。

shaq = "Shaquille O'Neal"
print('a' in shaq)
print(shaq.find('a'))
1
2
3
## True
## 2
1
2

除了 'Shaquille' 中有出現 'a' 在 'O'Neal' 中也有一個 'a',該如何將所有 'a' 的索引值都找到呢?可以運用一個的 list comprehension 搭配 enumerate() 函數:

shaq = "Shaquille O'Neal"
[idx for idx, val in enumerate(shaq) if val == 'a']
1
2
## [2, 14]
1

另外一個方法 .index() 也能夠達到 .find() 的類似效果,但是我們推薦使用 .find() ,因為它設計若是搜尋不到則回傳 -1,但使用 .index() 方法搜尋不到會產生錯誤。

shaq = "Shaquille O'Neal"
print('z' in shaq)
print(shaq.find('z'))
try ValueError:
  print(shaq.index('z')) # ValueError
except:
  print("找不到")
1
2
3
4
5
6
7
## False
## -1
## 找不到
1
2
3

R 語言

在 R 語言中可以使用 grepl() 函數來判斷是否存在、使用 gregexpr() 函數將所有特徵的索引值都找出來。

shaq <- "Shaquille O'Neal"
grepl(shaq, pattern = "a")
gregexpr(shaq, pattern = "a")[[1]]
1
2
3
## [1] TRUE
## [1]  3 15
## attr(,"match.length")
## [1] 1 1
## attr(,"useBytes")
## [1] TRUE
1
2
3
4
5
6

根據特徵取代

如同在修訂文件時常用的取代功能,我們常需要在文字中將符合某些特徵或關鍵字的部分搜尋出來後再取代為指定文字。

Python

在 Python 中可以使用 .replace() 方法,在其中指定兩個參數,一個是要搜尋的特徵、另一則是要取代的文字。

shaq = "Shaquille O'Neal"
print(shaq.replace('a', 'A'))
1
2
## "ShAquille O'NeAl"
1

R 語言

在 R 語言中使用 sub()gsub() 函數指定兩個參數,pattern 參數指定搜尋特徵、replacement 參數則指定要取代的文字;sub()gsub() 的差別僅在於前者只取代第一個搜尋到的特徵,後者則是取代所有搜尋到的特徵。

shaq <- "Shaquille O'Neal"
sub(shaq, pattern = "a", replacement = "A")
gsub(shaq, pattern = "a", replacement = "A")
1
2
3
## [1] "ShAquille O'Neal"
## [1] "ShAquille O'NeAl"
1
2

正規表達特徵

在分隔、判斷與取代的文字操作中常提及以特徵(pattern)來作為根據,在很多的應用情境中,資料科學團隊需要用一個更廣泛的特徵表達方式,這時就會採用正規表達式(Regular Expression)來支援,常用的正規表達特殊字元有:

  • .:任意文字
  • ^:開頭文字
  • $:結束文字
  • ?:文字出現零次到一次
  • *:文字出現零次到多次
  • +:文字出現一次到多次
  • {m}:文字剛好出現 m 次
  • {m, n}:文字出現次數介於 m 次與 n 次之間(m < n)
  • []:文字組合
  • \:跳脫符號
  • \s:空格

Python

在 Python 中使用 re 模組就可以採取正規表達式做為分隔、判斷與取代的特徵,只要呼叫 re 模組中的 split()findall()sub() 函數即可。

import re

shaq = "Shaquille O'Neal"
print(re.split(pattern="\s+", string=shaq))             # 以空格分隔
print(len(re.findall(pattern="\s+", string=shaq)) > 0)  # 判斷是否有空格
print(re.sub(pattern="\s+", repl=';', string=shaq))     # 將空格取代為分號
1
2
3
4
5
6
## ['Shaquille', "O'Neal"]
## True
## Shaquille;O'Neal
1
2
3

R 語言

R 語言的 strsplit()grepl()gsub() 函數中的 split 參數與 pattern 參數都支援正規表達式,常用的正規表達特殊字元大致相同,只有在使用到 \ 符號時由於 R 語言的特性,必須使用 \\ 符號。

shaq <- "Shaquille O'Neal"
strsplit(shaq, split = "\\s+")                  # 以空格分隔
grepl(shaq, pattern = "\\s+")                   # 判斷是否有空格
gsub(shaq, pattern = "\\s+", replacement = ";") # 將空格取代為分號
1
2
3
4
## [[1]]
## [1] "Shaquille" "O'Neal"   

## [1] TRUE
## [1] "Shaquille;O'Neal"
1
2
3
4
5

對多數資料科學初學者而言,正規表達式並不是在短時間就能靈活運用的技巧,因此我們在延伸閱讀提供了專門探討正規表達式的連結與參考書。

應用文字處理函數至陣列上

如果前述的分隔、判斷與取代這些函數,原本都是處理文字純量(scalar),在希望將這些函數映射到一個陣列上,像是將多位 NBA 球星姓名中的母音(a、e、i、o、u、A、E、I、O、U)移除的操作,必須仰賴像是 map 或者 apply 的技巧。

Python

在 Python 的 re 模組中函數都是以處理文字純量為主的類型,故要實踐將一群球員姓名中的母音(a、e、i、o、u、A、E、I、O、U)取代為空字串,可以透過 map() 函數,值得注意的是 map() 函數輸出的結果是一個 map 物件不能夠很友善地印出來,需要用 list() 函數轉換為一個 list 檢視。

import re

def remove_vowels(x):
  ans = re.sub(pattern="[aeiouAEIOU]+", repl="", string=x)
  return ans

fav_players = ["Steve Nash", "Michael Jordan", "Paul Pierce", "Kevin Garnett", "Shaquille O'Neal"]
print(fav_players)                           # 移除母音前
print(list(map(remove_vowels, fav_players))) # 移除母音後
1
2
3
4
5
6
7
8
9
## ['Steve Nash', 'Michael Jordan', 'Paul Pierce', 'Kevin Garnett', "Shaquille O'Neal"]
## ['Stv Nsh', 'Mchl Jrdn', 'Pl Prc', 'Kvn Grntt', "Shqll 'Nl"]
1
2

R 語言

而 R 語言中的文字函數則皆是以處理文字陣列為主之類型,故要實踐如前述範例的操作可以直接將文字陣列當作輸入。

fav_players <- c("Steve Nash", "Michael Jordan", "Paul Pierce", "Kevin Garnett", "Shaquille O'Neal")
fav_players                                                    # 移除母音前
gsub(fav_players, pattern = "[aeiouAEIOU]+", replacement = "") # 移除母音後
1
2
3
## [1] "Steve Nash"       "Michael Jordan"   "Paul Pierce"      "Kevin Garnett"    "Shaquille O'Neal"
## [1] "Stv Nsh"   "Mchl Jrdn" "Pl Prc"    "Kvn Grntt" "Shqll 'Nl"
1
2

小結

在這個小節中我們簡介在 Python 與 R 語言中如何處理文字,包含建立、量測長度、調整大小寫、去除多餘空格、格式化輸出、擷取部分文字、轉換為日期時間格式、根據特徵分隔、判斷特徵存在與否及存在位置、根據特徵取代、正規表達特徵以及應用文字處理函數至陣列上。

延伸閱讀