Regular Expression (tạm dịch là biểu thức chính quy) có thể hiểu là một chuỗi các ký tự khớp với một mẫu trong một đoạn văn bản hoặc một tệp văn bản. Nó được sử dụng trong khai thác văn bản trong rất nhiều ngôn ngữ lập trình. Các ký tự của biểu thức chính quy khá giống nhau trong tất cả các ngôn ngữ. Nhưng các chức năng trích lọc, định vị, phát hiện và thay thế có thể khác nhau ở các ngôn ngữ khác nhau.
Trong bài viết này, tôi sẽ minh họa cách sử dụng biểu thức chính quy để khai tác văn bản trên hai ngôn ngữ là R và Stata thông qua 20 quy tắc cơ bản sau.
Sau đây là 20 quy tắc cơ bản về biểu thức chính quy được áp dụng phổ biến trong khai thác văn bản.
. = bất kì kí tự nào
\d = 1 kí tự số bất kì từ 0 đến 9
\D = không phải 1 kí tự số từ 0 đến 9
\w = một kí tự thông thường (a-z, A-Z, 0-9, _)
\W = không phải một kí tự thông thường
\s = một kí tự trắng như: space, tab, newline
\S = không phải một kí tự trắng
\b = khớp nguyên từ (word boundary)
\B = chỉ lấy một phần của từ
\^ = bắt đầu của một chuỗi
$ = kết thúc của một chuỗi
[] = khớp các kí tự trong ngoặc
[^ ] = khớp các kí tự không có trong ngoặc
| = hoặc
( ) = gom nhóm
* = khớp 0 lần hoặc nhiều hơn
+ = khớp ít nhất 1 lần
? = có hoặc không
{x} = xuất hiện đúng x lần
{x, y} = khoảng giá trị cần so khớp (GTLN, GTNN)
Với các bạn lần đầu tìm hiểu về biểu thức chính quy thì có lẽ bạn chưa hình dung lắm về cách sử dụng các quy tắc này. Nhưng bạn không phải lo lắng hay chờ đợi lâu. Phần bên dưới sẽ minh họa chi tiết cách sử dụng 20 quy tắc này thông qua các ví dụ cụ thể trước khi kết hợp chúng vào việc khai thác văn bản.
Giả sử tôi có chuỗi ch như sau:
ch <- c('Nancy Smith',
'is there any solution?',
".[{(^$|?*+",
"coreyms.com",
"321-555-4321",
"123.555.1234",
"123*555*1234"
)
Trên R sử dụng hàm str_extract_all() để trích các kí tự
từ một chuỗi thỏa mãn tiêu chí cần trích. Hàm có hai đối số là đối tượng
chuỗi và phần tử được trích.
library(stringr)
str_extract_all(ch, "\\d")
## [[1]]
## character(0)
##
## [[2]]
## character(0)
##
## [[3]]
## character(0)
##
## [[4]]
## character(0)
##
## [[5]]
## [1] "3" "2" "1" "5" "5" "5" "4" "3" "2" "1"
##
## [[6]]
## [1] "1" "2" "3" "5" "5" "5" "1" "2" "3" "4"
##
## [[7]]
## [1] "1" "2" "3" "5" "5" "5" "1" "2" "3" "4"
Trong chuỗi ch ban đầu, bốn chuỗi đầu tiên không có bất
kì kí tự số nào. Ba chuỗi cuối là các số điện thoại và biểu thức trích “
đã lọc được những kí tự số này.
Ngược lại với “, sử dụng”” để trích tất cả các kí tự dấu chấm, các kí tự đặc biệt nhưng ngoại trừ các kí tự số (từ 0 đến 9).
str_extract_all(ch, "\\D")
## [[1]]
## [1] "N" "a" "n" "c" "y" " " "S" "m" "i" "t" "h"
##
## [[2]]
## [1] "i" "s" " " "t" "h" "e" "r" "e" " " "a" "n" "y" " " "s" "o" "l" "u" "t" "i"
## [20] "o" "n" "?"
##
## [[3]]
## [1] "." "[" "{" "(" "^" "$" "|" "?" "*" "+"
##
## [[4]]
## [1] "c" "o" "r" "e" "y" "m" "s" "." "c" "o" "m"
##
## [[5]]
## [1] "-" "-"
##
## [[6]]
## [1] "." "."
##
## [[7]]
## [1] "*" "*"
Kết quả là các kí tự ở bốn chuỗi đầu tiên được trích hết, và ba chuỗi cuối cùng chỉ những kí tự đặc biệt như “-”, “.” hay “*” được trích.
‘w’ sẽ lọc tất cả các kí tự bao gồm a-z, A-Z, 0–9, và kí tự gạch chân ‘_’
str_extract_all(ch, "\\w")
## [[1]]
## [1] "N" "a" "n" "c" "y" "S" "m" "i" "t" "h"
##
## [[2]]
## [1] "i" "s" "t" "h" "e" "r" "e" "a" "n" "y" "s" "o" "l" "u" "t" "i" "o" "n"
##
## [[3]]
## character(0)
##
## [[4]]
## [1] "c" "o" "r" "e" "y" "m" "s" "c" "o" "m"
##
## [[5]]
## [1] "3" "2" "1" "5" "5" "5" "4" "3" "2" "1"
##
## [[6]]
## [1] "1" "2" "3" "5" "5" "5" "1" "2" "3" "4"
##
## [[7]]
## [1] "1" "2" "3" "5" "5" "5" "1" "2" "3" "4"
Nhưng nó loại trừ các kí tự đặc biệt.
Ngược lại, ‘W’ chỉ lấy các kí tự đặc biệt
str_extract_all(ch, "\\W")
## [[1]]
## [1] " "
##
## [[2]]
## [1] " " " " " " "?"
##
## [[3]]
## [1] "." "[" "{" "(" "^" "$" "|" "?" "*" "+"
##
## [[4]]
## [1] "."
##
## [[5]]
## [1] "-" "-"
##
## [[6]]
## [1] "." "."
##
## [[7]]
## [1] "*" "*"
Ví dụ:
st <- "This is Bliss"
str_extract_all(st, "\\bis")
## [[1]]
## [1] "is"
str_extract_all(st, "\\Bis")
## [[1]]
## [1] "is" "is"
Chúng ta thấy trong chuỗi “This is Bliss” thì chuỗi ‘is’ xuất hiện 3 nơi nhưng từ ‘is’ thì chỉ có một và ‘is’ là xuất hiện trong từ khác thì có hai ‘This’ và ‘Bliss’. Quy tắc “” sẽ trích các nội dung này.
Sử dụng quy tắc ‘^’ và ‘$’ để so khớp kí tự ở vị trí bắt đầu và kết thúc của một chuỗi.
Ví dụ: tìm chuỗi được kết thúc bởi dấu chấm than (!) trong chuỗi
sts bên dưới:
sts <- c("This is me",
"That my house",
"Hello, world!")
str_extract_all(sts, "!$")
## [[1]]
## character(0)
##
## [[2]]
## character(0)
##
## [[3]]
## [1] "!"
Để hiển thị chuỗi đã tìm thấy, tôi sử dụng hàm
str_detect
sts[str_detect(sts, "!$")]
## [1] "Hello, world!"
Hoặc tìm câu bắt đầu với từ ‘This’
sts[str_detect(sts, "^This")]
## [1] "This is me"
hay bắt đầu với kí tự “T”
sts[str_detect(sts, "^T")]
## [1] "This is me" "That my house"
Ví dụ:
Trích tất cả số trong đoạn từ 2 đến 4 (bao gồm cả hai biên)
str_extract_all(ch, "[2-4]")
## [[1]]
## character(0)
##
## [[2]]
## character(0)
##
## [[3]]
## character(0)
##
## [[4]]
## character(0)
##
## [[5]]
## [1] "3" "2" "4" "3" "2"
##
## [[6]]
## [1] "2" "3" "2" "3" "4"
##
## [[7]]
## [1] "2" "3" "2" "3" "4"
hoặc trích các kí tự “-” hoặc “.”
str_extract_all(ch, "[-.]")
## [[1]]
## character(0)
##
## [[2]]
## character(0)
##
## [[3]]
## [1] "."
##
## [[4]]
## [1] "."
##
## [[5]]
## [1] "-" "-"
##
## [[6]]
## [1] "." "."
##
## [[7]]
## character(0)
Bạn có thể sử dụng nhiều dần tiêu chí ‘\d’ cùng dấu ngăn cách (-, .
và *) để trích các số điện thoại từ chuỗi ch trong ví dụ
trên. Tuy nhiên, một cách nhanh hơn là sử dụng quy tắc 19 {x} để trích
các kí tự số lặp lại x lần trong chuỗi.
str_extract(ch, "\\d{3}.\\d{3}.\\d{4}")
## [1] NA NA NA NA "321-555-4321"
## [6] "123.555.1234" "123*555*1234"
Ở đây, kí tự ‘.’ là bất kì kí tự nào (quy tắc 1) chứ không phải là chỉ kí tự (.). Nếu chỉ muốn hiển thị duy nhất kí tự (.) này thì bạn sử dụng thêm dấu (\) phía trước.
Một ví dụ khác, tôi có một chuỗi ph gồm các số điện
thoại và tôi muốn trích từng số điện thoại từ chuỗi này.
ph <- c("543-325-1278",
"900-123-7865",
"421.235.9845",
"453*2389*4567",
"800-565-1112",
"900 234 4356"
)
Nhận thấy các số điện thoại được định dạng theo một mô thức {3 kí tự số}{dấu ngăn cách có thể là ‘-’ ‘.’ hoặc khoảng trắng}{3 kí tự số tiếp theo} và cuối cùng là {4 kí tự số}. Vì vậy, tôi sử dụng biểu thức chính quy như sau:
str_extract(ph, "\\d{3}[- .]\\d{3}[- .]\\d{4}")
## [1] "543-325-1278" "900-123-7865" "421.235.9845" NA "800-565-1112"
## [6] "900 234 4356"
Mặc dù đã sử dụng kí tự khoảng trắng trong [- .] nhưng
kết quả trả về vẫn thiếu trường hợp “900 234 4356”.
Tôi có thể khắc phục vấn đề này bằng cách sử dụng quy tắc 15 (nhóm các kí tự)
str_extract(ph, "\\d{3}([- .])\\d{3}([- .])\\d{4}")
## [1] "543-325-1278" "900-123-7865" "421.235.9845" NA "800-565-1112"
## [6] "900 234 4356"
Hoặc nếu muốn sử dụng kết hợp quy tắc 18 với việc thiết lập ‘?’ phía sau nhóm thì thực hiện như sau:
str_extract(ph, "\\d{3}([- .])?\\d{3}([- .])?\\d{4}")
## [1] "543-325-1278" "900-123-7865" "421.235.9845" NA "800-565-1112"
## [6] "900 234 4356"
Ngoài ra, một vấn đề nữa các số điện thoại ở US không bắt đầu bằng 0
hoặc 1 đứng đầu. Do vậy, tôi viết lại biểu thức chính quy p
và áp dụng lọc như sau:
p <- "([2-9][0-9]{2})([- .]?)([0-9]{3})([- .])?([0-9]{4})"
str_extract(ph, p)
## [1] "543-325-1278" "900-123-7865" "421.235.9845" NA "800-565-1112"
## [6] "900 234 4356"
Trong biểu thức trên, () được dùng để mô tả nhóm các kí
tự (quy tắc 15).
Biểu thức chính quy trên được diễn giải dễ hiểu từ các thành phần sau:
Nhóm đầu tiên “([2–9][0–9]{2})”:
‘[2–9]’ thể hiện các số từ 2 đến 9
‘[0–9]{2}’ thể hiện các số từ 0 đến 9, được lập lại 2 lần
Nhóm thứ hai “([- .]?)”:
‘[- .]’ có thể là ‘-’ hoặc ‘.’ hay khoảng trắng
‘?’ cho biết ‘-’ ‘.’ hay khoảng trắng là các tùy chọn, nếu để trống cũng không sao.
Nhóm thứ ba ([0-9]{3}): các số từ 0 đến 9, lặp lại 3 lần
Nhóm thứ tư “([- .]?)”: ý nghĩa tương tự nhóm 2
Nhóm thứ năm ([0-9]{4}): các số từ 0 đến 9, lặp lại 4 lần.
Bây giờ chúng ta muốn lọc các số điện thoại bắt đầu là 800 hoặc 900 thì thiết lập biểu thức chính quy như sau:
p <- "[89]00([- .])\\d{3}([- .])\\d{4}"
str_extract_all(ph, p)
## [[1]]
## character(0)
##
## [[2]]
## [1] "900-123-7865"
##
## [[3]]
## character(0)
##
## [[4]]
## character(0)
##
## [[5]]
## [1] "800-565-1112"
##
## [[6]]
## [1] "900 234 4356"
Ở đây [89] có nghĩa kí tự đầu có thể là 8 hoặc 9.
Việc trích các địa chỉ email thường phức tạp hơn so với số điện
thoại. Bởi vì một địa chỉ email có thể chứa chữ hoa, chữ thường, chữ số,
ký tự đặc biệt, mọi thứ. Thử thực hành trích địa chỉ email từ văn bản
emails sau:
emails <- c("RashNErel@gmail.com",
"rash.nerel@regen04.net",
"rash_48@uni.edu",
"rash_48_nerel@STB.org")
Một email với hai kí tự đặc biệt ngăn cách là ‘@’ và ‘.’ sẽ phân địa chỉ thành ba phần: phần trước kí tự ‘@’, phần sau dấu ‘@’ và trước kí tự ‘.’ và cuối cùng là phần còn lại. Ở đây, chỉ xét kí tự đặc biệt ‘@’ và ‘.’ xuất hiện đầu tiên trong chuỗi theo thứ tự từ trái qua phải.
Phần đầu tiên bao gồm các kí tự viết thường, [a-z], viết hoa, [A-Z], các kí tự số, [0–9] và một số kí tự đặc biệt như ‘.’ và ’_’. Do vậy, biểu thức chính quy có thể được viết là “[a-zA-Z0–9-.]+”. Ở đây, dấu ‘+’ cho biết các kí tự trên có thể xuất hiện nhiều hơn 1 lần (quy tắc 17). Bởi vì chúng ta không biết các kí tự xuất hiện bao nhiêu lần nên không thể sử dụng quy tắc 19.
Phần thứ hai cũng tương tự phần đầu nên có thể được phát hiện qua biểu thức: “[a-zA-Z0–9-.]+”
Phần thứ ba là phần còn lại. Bởi vì tôi đã thêm dấu ‘+’ sau phần thứ hai nên nó có thể nhận bất kì kí tự nào xuất hiện sau đó.
Tổng hợp lại, để trích các địa chỉ email từ văn bản
emails trên, tôi xây dựng một biểu thức chính quy như
sau:
p <- "[a-zA-Z0-9-.]+@[a-zA_Z0-9]+"
str_extract_all(emails, p)
## [[1]]
## [1] "RashNErel@gmail"
##
## [[2]]
## [1] "rash.nerel@regen04"
##
## [[3]]
## [1] "48@uni"
##
## [[4]]
## character(0)
Kết quả vẫn tốt khi tôi không đề cập đến các từ ‘com’, ‘net’, ‘edu’, ‘org’ như trong ví dụ. Tuy nhiên, vì mục đích mở rộng sau này, tôi nhận diện và trích xuất bằng cách sử dụng quy tắc 14 (hoặc) kết hợp quy tắc 15 (gom nhóm): “(com|edu|net|org”) như sau:
p <- "[a-zA-Z0-9-.]+@[a-zA_Z0-9]+\\.(com|edu|net|org)"
str_extract_all(emails, p)
## [[1]]
## [1] "RashNErel@gmail.com"
##
## [[2]]
## [1] "rash.nerel@regen04.net"
##
## [[3]]
## [1] "48@uni.edu"
##
## [[4]]
## character(0)
Việc trích các URL cũng tương tự như với số điện thoại, mặc dù có đôi
chút bất tiện do các địa chỉ có thể bắt đầu với chuẩn http hay https, có
hoặc không có www.. Thử thực hành trích các URL từ văn bản
urls sau:
urls <- c("https://regenerativetoday.com",
"http://setf.ml",
"https://www.yahoo.com",
"http://studio_base.net"
)
Một URL sẽ bao gồm 3 phần:
‘https?’: có hoặc không kí tự ‘s’
“(www\.)?”: có hoặc không từ ‘www.’
Phần thứ hai là tên miền có thể là bất kì kí tự thông thường nào được lặp lại nhiều hơn một lần –> sử dụng: “\w+”
Phần thứ ba là sau tên miền: có thể có một hoặc nhiều hơn một dấu chấm “.” và theo sau là các kí tự thông thường khác –> sử dụng: “\.\w+”
Tổng hợp lại, tôi viết biểu thức chính quy để trích các URL từ văn
bản urls như sau:
p <- "https?://(www\\.)?\\w+\\.\\w+"
str_extract_all(urls, p)
## [[1]]
## [1] "https://regenerativetoday.com"
##
## [[2]]
## [1] "http://setf.ml"
##
## [[3]]
## [1] "https://www.yahoo.com"
##
## [[4]]
## [1] "http://studio_base.net"
Trong trường hợp chỉ muốn lấy các tên miền ‘.com’ hoặc ‘.net’ thì bổ sung thêm nhóm (com|net) vào biểu thức chính quy trên như sau:
p <- "https?://(www\\.)?(\\w+)(\\.)+(com|net)"
str_extract_all(urls, p)
## [[1]]
## [1] "https://regenerativetoday.com"
##
## [[2]]
## character(0)
##
## [[3]]
## [1] "https://www.yahoo.com"
##
## [[4]]
## [1] "http://studio_base.net"
Cuối cùng là ví dụ kết hợp các quy tắc để xuất các danh bạ từ một văn
bản name sau:
name <- c("Mr. Jon",
"Mrs. Jon",
"Mr Ron",
"Ms. Reene",
"Ms Julie")
Một danh bạ bao gồm bí danh Mr, Mrs, Ms (–> sử dụng: “M(r|s|rs)”)
có thể có hoặc không dấu ‘.’ đi kèm (–> sử dụng: “\.?”), theo sau là
một khoảng trắng (–> sử dụng: “\s”). Sau đó là tên, với kí tự đầu
tiên viết hoa (–> sử dụng: “[A-Z]”), theo sau là các kí tự thông
thường (–> sử dụng: “\w”). Sử dụng dấu ’’ có nghĩa các kí tự
trước đó có thể không xuất hiện hoặc xuất hiện nhiều lần. Kết hợp lại,
biểu thức chính quy sau sẽ trích các danh bạ từ văn bản
name như sau
p <- "M(r|s|rs)\\.?[A-Z\\s]\\w*"
str_extract_all(name, p)
## [[1]]
## [1] "Mr. Jon"
##
## [[2]]
## [1] "Mrs. Jon"
##
## [[3]]
## [1] "Mr Ron"
##
## [[4]]
## [1] "Ms. Reene"
##
## [[5]]
## [1] "Ms Julie"
Như vậy, bạn đã cùng tôi đã tìm hiểu cách sử dụng biểu thức chính quy qua 20 quy tắc cơ bản, cũng như thực hành qua các ví dụ thú vị về trích xuất số điện thoại, địa chỉ mail, lấy các URL, hay một danh bạ nào đó. Bạn có thể thực hành lại nhiều lần để nắm vững các quy tắc, cũng như sử dụng kết hợp các quy tắc. Việc khai thác các mẫu hình văn bản phức tạp khác cũng bắt đầu từ các mẫu hình cơ bản với các quy tắc cơ bản trên.
Tham khảo: regenerative