Golang đại pháp – 1.3 Xử lý file
Go
41
golang
39
White

nhaancs viết ngày 02/01/2018

Bài viết được đăng tải đầu tiên tại: http://nhaancs.com/golang-dai-phap-1-3-xu-ly-file/

golang dai phap

Các chương trình làm việc với file như copy, print, search, sort, count có một cấu trúc giống nhau là: lặp qua từng phần của file input, xử lý trên từng phần đó và cho ra kết quả cuối cùng. Trong phần này chúng ta sẽ viết 3 phiên bản khác nhau của một chương trình gọi là dup. Chức năng của dup được lấy ý tưởng từ câu lệnh uniq trong Unix, lệnh uniq dùng để tìm kiếm các dòng bị lặp lại trong file.

Phiên bản đầu tiên của dup in ra các dòng lặp lại kèm với số lần lặp lại. Thông qua chương trình này chúng ta sẽ cùng tìm hiểu câu lệnh if, kiểu dữ liệu map và package bufio (file dup1.go).

// Dup1 in ra tất cả các dòng xuất hiện từ 2 lần trở lên trong file
package main

import (
    "fmt"
    "os"
    "bufio"
)

func main() {
    counts := make(map[string]int)
    input := bufio.NewScanner(os.Stdin)
    for input.Scan() {
        counts[input.Text()]++
    }
    // Bỏ qua các lỗi có thể xảy ra của input.Err()
    for line, n := range counts {
        if n > 1 {
            fmt.Println("%d\t%s\n", n, line)
        }
    }
}

Tạo file test.txt với nội dung:

aaaaaaa
aaaaaaa
bbbbbbb
aaaaaaa

Sau khi build chương trình trên, bạn chạy như sau:

$ cat ./test.txt | ./dup1

Kết quả hiển thị:

3 aaaaaaa

Giống như for, cặp ngoặc tròn không được dùng bao quanh phần điều kiện của lệnh if, nhưng cặp dấu ngoặc nhọn là bắt buộc. Ngoài ra bạn có thể dùng else để viết code xử lý khi điều kiện trả về false.

Một map giữ một tập hợp của các cặp key/value và các phương thức để lưu trữ, truy xuất, kiểm tra từng phần tử trong tập hợp. key phải thuộc các kiểu có thể so sánh với == (string được dùng nhiều nhất), còn value có thể thuộc bất cứ kiểu nào. Trong ví dụ này chúng ta dùng key kiểu stringvalue kiểu int. make là một hàm được cung cấp sẵn của Go dùng để tạo một map rỗng.

Mội lần dup đọc một dòng trong file input thì dòng đó được dùng làm key cho map countsvalue của key đó sẽ được tăng lên một đơn vị.

counts[input.Text()]++

tương đương với hai câu lệnh:

line := input.Text()
counts[line] = counts[line] + 1

Có thể bạn sẽ thắc mắc là nếu lúc đầu key chưa tồn tại trong map thì sao? Điều đó không thành vấn đề vì khi chương trình đọc một dòng mới lần hoàn toàn thì biểu thức counts[line] bên phải sẽ nhận giá trị zero cho kiểu của mình, với int0.

Để in kết quả ra, chúng ta dùng một vòng lặp for range để lặp lần lượt qua các phần tử của map counts. Mỗi lần lặp, range cung cấp một cặp key/value trong map. Thứ tự lặp của map là ngẫu nhiên, không theo một thứ tự xác định.

Tiếp theo đến package bufio, đây là package giúp chúng ta làm cho việc input và output hiệu quả và thuận tiện hơn. Một trong những tính năng khá hữu ích của nó là kiểu Scanner, để đọc input và tách ra thành từng dòng.

Biến input sau thuộc kiểu bufio.Scanner:

input := bufio.NewScanner(os.Stdin)

Mỗi lần gọi input.Scan() đọc dòng tiếp theo và loại bỏ ký tự xuống dòng ở phía cuối, kết quả được trả về qua hàm input.Text(). Hàm input.Scan() trả về false khi không còn dòng nào nữa, ngược lại trả về true.

Hàm fmt.Printf() giống như printf trong C và các ngôn ngữ khác, cung cấp các output được định dạng từ một danh sách các biểu thức. Tham số đầu tiên của nó là một chuỗi cho biết các tham số phía sau sẽ được định dạng như thế nào. Định dạng của mỗi tham số sẽ được qui ước bằng dấu % và theo sau là một ký tự nhất định. Ví dụ %d dùng để định dạng số nguyên theo hệ cơ số 10, %s dùng để định dạng chuỗi.

Bên dưới là danh sách một số mẫu định dạng:

%d: số nguyên hệ 10

%x, %o, %b: số nguyên hệ 16, 8, nhị phân

%f, %g, %e: số thực: 3.141593 3.1415692653598793 3.141593e+00

%t: boolean: true hoặc false

%c: rune (Unicode code point)

%s: string

%q: chuỗi gồm dấu nháy "abc" hay rune 'c'

%v: bất kỳ giá trị nào trong định dạng tự nhiên

%T: kiểu của bất kỳ giá trị nào

%%: dấu phần trăm.

Chuỗi định dạng trong dup1 còn chứ các ký tự tab \t, xuống dòng \n. Theo qui ước, các hàm dùng để format có tên kết thúc với ký tự f như log.Printf, fmt.Errorf, fmt.Printf, ... Những hàm có tên kết thúc với ln giống như Println định dạng các tham số của nó với %v và kết thúc bằng ký tự xuống dòng.

Phiên bản hai của dup (file dup2.go) có thể đọc từ standard input như trước hoặc từ một danh sách các tên file:

// Dup2 in ra tất cả các dòng xuất hiện từ 2 lần trở lên trong file
// đọc từ stdin hay danh sách file name truyền vào
package main 

import (
    "os"
    "fmt"
    "bufio"
)

func main() {
    counts := make(map[string]int)
    files := os.Args[1:]

    if len(files) == 0 {
        countLines(os.Stdin, counts)
    } else {
        for _, arg := range files {
            f, err := os.Open(arg)
            if err != nil {
                fmt.Fprintf(os.Stderr, "dup 2: %v\n", err)
                continue
            }
            countLines(f, counts)
            f.Close()
        }
    }

    for line, n := range counts {
        if n > 1 {
            fmt.Println("%d\t%s\n", n, line)
        }
    }
}

func countLines(f *os.File, counts map[string]int) {
    input := bufio.NewScanner(f)
    for input.Scan() {
        counts[input.Text()]++
    }
    // Bỏ qua các lỗi có thể xảy ra của input.Err()
}

Hàm os.Open trả về hai giá trị. Đầu tiên là file được mở (kiểu *os.File). Giá trị thứ hai là giá trị thuộc kiểu error của Go.

Nếu err bằng nil (nil là một giá trị có sẵn của Go) thì file được mở thành công. Sau khi đọc file, hàm Close đóng file lại và giải phóng tất cả các resource.

Ngược lại nếu err không bằng nil thì nghĩa là có điều gì đó sai sai đã xảy ra. Trong trường hợp này thì biến err sẽ cho chúng ta biết đang có vấn đề gì. Khi có lỗi err, chúng ta in thông báo lỗi với định dạng %v và câu lệnh conntinue tiếp tục vòng lặp tiếp theo.

Như bạn có thể thấy là chúng ta gọi function countLines() trước khi khai báo. Trong Go bạn có thể khai báo function theo bất kỳ thức tự nào.

Một map chỉ chứa liên kết (reference) tới cấu trúc dữ liệu (data structure) tạo ra bởi make. Khi truyền map vào trong function thì function đó nhận một bản sao của liên kết nhưng bản sao đó vẫn trỏ tới cùng một data structure bên dưới. Vì vậy mội thay đổi của map trong function đều được lưu lại. Trong ví dụ của chúng ta, các giá trị được chèn vào map counts trong countLines() đều được thấy trong main().

Phiên bản hai của dup thực thi trong chế độ "streaming" khi mà iput được đọc và tách thành các dòng khi cần thiết. Cách tiếp cận khác là đọc toàn bộ input, lưu vào trong memory, tách input thành từng dòng trong một lần, sau đó xử lý qua các dòng. Đây là các mà phiên bản 3 của dup (file dup3.go) hoạt động. Phiên bản này giới thiệu hàm ReadFile (package io/ioutil) dùng để đọc toàn bộ nội dung của file và hàm strings.Split để tách một chuỗi thành một slice của các chuỗi con (ngược lại với strings.Join).

Để cho đơn giản, phiên bản 3 này chỉ đọc các file theo tên được cung cấp (không dùng standard input) và đoạn code đếm dòng được đem trở lại main:

package main 

import(
    "fmt"
    "os"
    "strings"
    "io/ioutil"
)

func main() {
    counts := make(map[string]int)
    for _, filename := range os.Args[1:] {
        data, err := ioutil.ReadFile(filename)
        if err != nil {
            fmt.Fprintf(os.Stderr, "dup3: %v\n", err)
            continue
        }

        for _, line := range strings.Split(string(data), "\n") {
            counts[line]++
        }
    }

    for line, n := range counts {
        if n > 1 {
            fmt.Printf("%d\t%s\n", n, line)
        }
    }
}

ReadFile trả về byte slice nên cần được chuyển đổi về string (string(data)) để có thể truyển vào strings.Split.

Bên dưới, bufio.Scanner, ioutil.ReadFileioutil.WriteFile dùng hàm ReadWrite của *os.File, nhưng rất hiếm khi có lập trình viên nào truy cập trực tiếp tới các hàm bậc thấp như vậy. Các hàm bậc cao trong các package như bufio hay io/ioutil dễ dùng hơn.

Bài tập cho bạn:

Exercise 1.4: Chỉnh sửa dup2 để in ra tên của tất cả các file có dòng bị trùng lặp.

Reference: The Go Programming Language

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

nhaancs

11 bài viết.
3 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Bài viết liên quan
White
17 0
Crawl dữ liệu Crawl là một vấn đề hay gặp trong quá trình làm software. Ví dụ lấy tin tức, tin giảm giá, vé xem phim... là những dạng của crawl. Mộ...
Thach Le viết gần 2 năm trước
17 0
White
41 7
Là một người thường xuyên đọc Quora, có một điểm cá nhân tôi thấy rất ấn tượng ở quora chính là khả năng autocomplete với tốc độ ánh sáng. (Ảnh) ...
huydx viết 2 tháng trước
41 7
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
11 bài viết.
3 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!