Giới thiệu Regular Expression

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.

Các quy tắc cơ bản

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.

  1. . = bất kì kí tự nào

  2. \d = 1 kí tự số bất kì từ 0 đến 9

  3. \D = không phải 1 kí tự số từ 0 đến 9

  4. \w = một kí tự thông thường (a-z, A-Z, 0-9, _)

  5. \W = không phải một kí tự thông thường

  6. \s = một kí tự trắng như: space, tab, newline

  7. \S = không phải một kí tự trắng

  8. \b = khớp nguyên từ (word boundary)

  9. \B = chỉ lấy một phần của từ

  10. \^ = bắt đầu của một chuỗi

  11. $ = kết thúc của một chuỗi

  12. [] = khớp các kí tự trong ngoặc

  13. [^ ] = khớp các kí tự không có trong ngoặc

  14. | = hoặc

  15. ( ) = gom nhóm

  16. * = khớp 0 lần hoặc nhiều hơn

  17. + = khớp ít nhất 1 lần

  18. ? = có hoặc không

  19. {x} = xuất hiện đúng x lần

  20. {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.

Ví dụ áp dụng

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.

Chỉ trích các kí tự số

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.

Loại trừ kí tự số

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.

Loại trừ các kí tự đặc biệt

‘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.

Chỉ lấy 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] "*" "*"

Sự khác nhau giữa ‘b’ và ‘B’

  • ‘b’ lấy trọn từ
  • ‘B’ chỉ lấy một phần của từ.

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.

Bắt đầu và kết thúc chuỗi

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"

Tìm kí tự hoặc số trong đoạn

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)

Kết hợp các quy tắc

Xuất số điện thoại

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.

Trích các địa chỉ mail

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)

Trích các URL

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:

  • Phần đầu bao gồm các tiền tố http hay https, có hoặc không có www. Nó có thể được ghi nhận bằng cách sử dụng quy tắc 15, 18 như sau:

‘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"

Xuất danh bạ

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"

Tổng kết

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