Golang: Regular Expressions

Regular Expressions hay thường được gọi với cái tên ngắn gọn và thông dụng hơn là Regex trích nghĩa của từ này trong tiếng Việt là biểu thức chính quy. Biểu thức chính quy được dùng để xử lý chuỗi nâng cao thông qua phương thức riêng của nó. Những biểu thức này có những quy tắc riêng mà bạn bắt buộc phải tuân theo chúng thì nó mới hoạt động được. Nguyên tắc hoạt động của Regex là so khớp dựa vào khuôn mẫu. Ngày nay, trong ngôn ngữ lập trình nào cũng đều hỗ trợ Regex, và Golang cũng không phải là ngôn ngữ ngoại lệ. Trong bài viết này, tôi sẽ giới thiệu với quý bạn đọc cách xử dụng Regex trong Golang.

Golang cung cấp package chính thức giúp hỗ trợ làm việc với Regex đó là regexp. Nếu bạn đã từng làm việc với Regex trong các ngôn ngữ lập trình khác thì bạn nên làm quen với package này. Có một lưu ý nhỏ đó là package này tuân theo chuẩn RE2.

Nếu bạn đã từng code hay ít nhất là đã nghiên cứu qua về ngôn ngữ golang. Bạn sẽ tự đặt ra câu hỏi là trong Golang có một package là strings. Package strings này cũng xử lý và thao tác với chuỗi string. Nó cũng có thể làm được nhiều việc như là searching (Contains, Index), replace(Replace), parse (Split, Join), .. và đôi khi tốc độ xử lý của nó còn nhanh hơn cả so với Regex. Tuy nhiên, những công việc kể trên chỉ là những công việc bình thường không đòi hỏi yêu cầu quá phức tạp. Trong một bài toán, mà bạn cần tìm một chuỗi ký tự không phân biệt chữ hoa hay chữ thường. Lúc này, sử dụng Regex thay cho strings mới là lựa chọn sáng suốt nhất. Vì vậy, tùy vào từng bài toán, tùy vào từng trường hợp cụ thể, nếu package strings là đủ để xử lý thì hãy dùng strings vì nó dễ đọc và dễ hiểu. Còn trong những bài toán phức tạp hơn thì hãy dùng đến Regex. ^^

Hãy tìm hiểu kỹ hơn về package regex

Match

Package regex cung cấp cho chúng ta 3 hàm để kiểm tra xem chuỗi so sánh có giống với chuỗi mẫu ban đầu hay không. Nếu giống nhau thì kết quả trả về sẽ là true, nếu sai thì trả về false.

func Match(pattern string, b []byte) (matched bool, error error)
func MatchReader(pattern string, r io.RuneReader) (matched bool, error error)
func MatchString(pattern string, s string) (matched bool, error error)

Cả 3 hàm này đều kiểm tra xem chuỗi pattern có match với chuỗi input vào hay không. Trả về true nếu match, false trong trường hợp còn lại. Tuy nhiên, nếu bạn Regex của bạn có vấn đề, các hàm này sẽ trả về syntax error. Mỗi hàm có một input truyền vào với kiểu dữ liệu khác nhau, lần lượt là slice kiểu byte, RuneReaderstrings. Hãy xem ví dụ kiểm tra một chuỗi có phải là địa chỉ IP hay không dưới đây.

func IsIP(ip string) (b bool) {
    if m, _ := regexp.MatchString("^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$", ip); !m {
        return false
    }
    return true
}

Các bạn có thể thấy, sử dụng package regexp và các function vô cùng đơn giản. Dưới đây là một ví dụ khác kiểm tra xem dữ liệu input của người dùng nhập vào có hợp lệ hay không?

func main() {
    if len(os.Args) == 1 {
        fmt.Println("Usage: regexp [string]")
        os.Exit(1)
    } else if m, _ := regexp.MatchString("^[0-9]+$", os.Args[1]); m {
        fmt.Println("Number")
    } else {
        fmt.Println("Not number")
    }
}

Trong ví dụ này, tôi đã dùng hàm Match(Reader|String) để check content có hợp lệ hay không. Các sử dụng cũng không hề khó.

Filter

Trong mục trên, chúng tà đã tìm hiểu về Match. Match có thể kiểm tra nội dung nhưng lại không thể trích xuất (cut), lọc (filter) hay thu thập (collect) một chuỗi từ chuỗi pattern. Giả sử chúng ta cần phải viết một chương trình để thu thập dữ liệu. Đây là một ví dụ khi bạn muốn lọc hoặc cắt dữ liệu

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "regexp"
    "strings"
)

func main() {
    resp, err := http.Get("https://www.google.com")
    if err != nil {
        fmt.Println("http get error.")
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("http read error")
        return
    }

    src := string(body)

    // Convert HTML tags to lower case.
    re, _ := regexp.Compile("\\<[\\S\\s]+?\\>")
    src = re.ReplaceAllStringFunc(src, strings.ToLower)

    // Remove STYLE.
    re, _ = regexp.Compile("\\<style[\\S\\s]+?\\</style\\>")
    src = re.ReplaceAllString(src, "")

    // Remove SCRIPT.
    re, _ = regexp.Compile("\\<script[\\S\\s]+?\\</script\\>")
    src = re.ReplaceAllString(src, "")

    // Remove all HTML code in angle brackets, and replace with newline.
    re, _ = regexp.Compile("\\<[\\S\\s]+?\\>")
    src = re.ReplaceAllString(src, "\n")

    // Remove continuous newline.
    re, _ = regexp.Compile("\\s{2,}")
    src = re.ReplaceAllString(src, "\n")

    fmt.Println(strings.TrimSpace(src))
}

Trong ví dụ này, tôi xử dụng function Compile trong bước đầu tiên. Nó kiểm tra tính hợp lệ của Regex bạn truyền vào. Nếu regex hợp lệ sẽ trả lại regex để parsing content trong các bước tiếp theo. Dưới đây là một số function để parsing regex của bạn

func Compile(expr string) (*Regexp, error)
func CompilePOSIX(expr string) (*Regexp, error)
func MustCompile(str string) *Regexp
func MustCompilePOSIX(str string) *Regexp

Sự khác nhau giữa hàm CompilePOSIX và hàm CompileCompilePOSIX sẽ tìm kiếm chuỗi ký tự dài nhất bên trái còn Compile là chỉ tìm kiếm bên trái. Ví dụ với chuỗi "aa09aaa88aaaa" và regex là [a-z]{2,4} thì CompilePOSIX sẽ trả về kết quả là ""aaaa" còn hàm Compile chỉ trả về kết quả là "aa". Nếu thêm tiền tố Must vào trước nữa có nghĩa là panic chương trình khi Regex không chính xác. return error trong các trường hợp khác.

Tới đây, chúng ta đã biết làm cách nào để tạo mới một regex. Hãy xem các hàm mà struct Regexp cung cấp cho chúng ta giúp chúng ta thao tác dễ dàng hơn.

func (re *Regexp) Find(b []byte) []byte
func (re *Regexp) FindAll(b []byte, n int) [][]byte
func (re *Regexp) FindAllIndex(b []byte, n int) [][]int
func (re *Regexp) FindAllString(s string, n int) []string
func (re *Regexp) FindAllStringIndex(s string, n int) [][]int
func (re *Regexp) FindAllStringSubmatch(s string, n int) [][]string
func (re *Regexp) FindAllStringSubmatchIndex(s string, n int) [][]int
func (re *Regexp) FindAllSubmatch(b []byte, n int) [][][]byte
func (re *Regexp) FindAllSubmatchIndex(b []byte, n int) [][]int
func (re *Regexp) FindIndex(b []byte) (loc []int)
func (re *Regexp) FindReaderIndex(r io.RuneReader) (loc []int)
func (re *Regexp) FindReaderSubmatchIndex(r io.RuneReader) []int
func (re *Regexp) FindString(s string) string
func (re *Regexp) FindStringIndex(s string) (loc []int)
func (re *Regexp) FindStringSubmatch(s string) []string
func (re *Regexp) FindStringSubmatchIndex(s string) []int
func (re *Regexp) FindSubmatch(b []byte) [][]byte
func (re *Regexp) FindSubmatchIndex(b []byte) []int

Trên đây, bao gồm 18 phương thức với chức năng giống nhau nhưng được dùng cho các trường hợp input khác nhau. Vì vậy, chúng ta có thể đơn giản hóa danh sách các hàm bên trên bằng cách k quan tâm đến kiểu dữ liệu truyền vào như sau:

func (re *Regexp) Find(b []byte) []byte
func (re *Regexp) FindAll(b []byte, n int) [][]byte
func (re *Regexp) FindAllIndex(b []byte, n int) [][]int
func (re *Regexp) FindAllSubmatch(b []byte, n int) [][][]byte
func (re *Regexp) FindAllSubmatchIndex(b []byte, n int) [][]int
func (re *Regexp) FindIndex(b []byte) (loc []int)
func (re *Regexp) FindSubmatch(b []byte) [][]byte
func (re *Regexp) FindSubmatchIndex(b []byte) []int

Ví dụ:

package main

import (
    "fmt"
    "regexp"
)

func main() {
    a := "I am learning Go language"

    re, _ := regexp.Compile("[a-z]{2,4}")

    // Find the first match.
    one := re.Find([]byte(a))
    fmt.Println("Find:", string(one))

    // Find all matches and save to a slice, n less than 0 means return all matches, indicates length of slice if it's greater than 0.
    all := re.FindAll([]byte(a), -1)
    fmt.Println("FindAll", all)

    // Find index of first match, start and end position.
    index := re.FindIndex([]byte(a))
    fmt.Println("FindIndex", index)

    // Find index of all matches, the n does same job as above.
    allindex := re.FindAllIndex([]byte(a), -1)
    fmt.Println("FindAllIndex", allindex)

    re2, _ := regexp.Compile("am(.*)lang(.*)")

    // Find first submatch and return array, the first element contains all elements, the second element contains the result of first (), the third element contains the result of second ().
    // Output: 
    // the first element: "am learning Go language"
    // the second element: " learning Go ", notice spaces will be outputed as well.
    // the third element: "uage"
    submatch := re2.FindSubmatch([]byte(a))
    fmt.Println("FindSubmatch", submatch)
    for _, v := range submatch {
        fmt.Println(string(v))
    }

    // Same as FindIndex().
    submatchindex := re2.FindSubmatchIndex([]byte(a))
    fmt.Println(submatchindex)

    // FindAllSubmatch, find all submatches.
    submatchall := re2.FindAllSubmatch([]byte(a), -1)
    fmt.Println(submatchall)

    // FindAllSubmatchIndex,find index of all submatches.
    submatchallindex := re2.FindAllSubmatchIndex([]byte(a), -1)
    fmt.Println(submatchallindex)
}

Chúng ta đã biết, package regexp có 3 hàm matching. Chún hoạt động tương tự như exported functions

func (re *Regexp) Match(b []byte) bool
func (re *Regexp) MatchReader(r io.RuneReader) bool
func (re *Regexp) MatchString(s string) bool

Cách để replace string

func (re *Regexp) ReplaceAll(src, repl []byte) []byte
func (re *Regexp) ReplaceAllFunc(src []byte, repl func([]byte) []byte) []byte
func (re *Regexp) ReplaceAllLiteral(src, repl []byte) []byte
func (re *Regexp) ReplaceAllLiteralString(src, repl string) string
func (re *Regexp) ReplaceAllString(src, repl string) string
func (re *Regexp) ReplaceAllStringFunc(src string, repl func(string) string) string

Đến đây, các bạn đã nắm được cơ bản nội dung của package regexp trong Golang. Các bạn hãy nghiên cứu các ví dụ khác để có thể hiểu kỹ hơn về cách xử dụng nhé. Chúc mọi người thành công. ^^