博客 / 詳情

返回

太讚了,使用 go-pretty 輕鬆美化終端輸出

在學習 Go 電子表格操作庫 Excelize 時,對讀取的數據在控制枱輸出結果顯示不太滿意,在想有沒有相關的美化表格輸出的開源庫,於是搜索一番發現了 go-pretty 這個庫,試用下來功能還挺強大的,這裏記錄一下方便日後查閲。

作者在源代碼中的 https://github.com/jedib0t/go-pretty/tree/main/table 位置列出了庫的一些功能點,並給出了部分示例代碼,在這裏可以初步掌握其用法。此外在源碼 https://github.com/jedib0t/go-pretty/tree/main/cmd 處有完整的示例代碼可供參考。

文章示例以 Titanic 數據集 >) 作為數據源,並通過 Excel 將其轉換成了 XLSX 格式保存在了系統 E 盤中。下面是通過 Excelize 讀取數據的代碼:

package main

import (
    "fmt"
    "os"

    "github.com/jedib0t/go-pretty/v6/table"
    "github.com/jedib0t/go-pretty/v6/text"
    "github.com/xuri/excelize/v2"
)

func main() {
    f, err := excelize.OpenFile("E:\\ExcelDemo\\titanic.xlsx")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer func() {
        if err := f.Close(); err != nil {
            fmt.Println(err)
        }
    }()
    rows, err := f.GetRows("titanic")
    if err != nil {
        fmt.Println(err)
        return
    }

    // 後續代碼...
}

上述代碼中我們讀取的是 titanic 工作簿中的數據,這個是轉換保存後的結果,默認情況下我們創建一個空白的工作簿,它的名稱是 Sheet1

輸出表格數據

在 Go-pretty 中我們通過調用 table.NewWriter() 獲得一個 Table 實例 t,然後調用 t.AppendHeader()t.AppendRow()t.AppendRows()t.AppendFooter() 來分別設置表格頭部,內容和表尾內容,最後調用 t.Render() 完成最終的渲染輸出。

在 Excelize 中通過 f.GetRows()f.GetCols() 分別獲取行和列的數據,返回的是一個包含數據行或列的切片,我們需要把每一行數據通過 table.Row() 包裝後傳入 t.AppendXx() 系列方法中,所以需要遍歷每一個切片數據。

// 實例化一個表格
t := table.NewWriter()
// 標準輸出
t.SetOutputMirror(os.Stdout)

// 獲取表頭數據
tableHeader := make(table.Row, 0)
for _, cell := range rows[0] {
    tableHeader = append(tableHeader, cell)
}
t.AppendHeader(tableHeader)

// 這裏只讀取前5行數據
for _, row := range rows[1:5] {
    innerRow := make(table.Row, 0)
    for _, cell := range row {
        innerRow = append(innerRow, cell)
    }
    t.AppendRow(innerRow)
    t.AppendSeparator()
    // 這裏不用擔心最後一行數據的分隔線會多出一行,不需要額外的判斷
    // if index != len(rows[1:]) {
    //     t.AppendSeparator()
    // }
}
// 設置表尾
t.AppendFooter(table.Row{"乘客ID", "獲救情況", "乘客等級", "姓名", "性別", "年齡", "堂兄弟妹個數", "父母與小孩個數", "船票信息", "票價", "船艙", "登船的港口"})

t.Render()

結果:

Pasted image 20240402175026.png

上面的代碼中我們使用 t.AppendRow() 來添加數據,並在每行數據後面加了一個分隔線,當然我們也可以使用 t.AppendRows() 來添加分組數據,遍歷方式稍有不同:

// 實例化一個表格
t := table.NewWriter()
// 標準輸出
t.SetOutputMirror(os.Stdout)

// 獲取表頭數據
tableHeader := make(table.Row, 0)
for _, cell := range rows[0] {
    tableHeader = append(tableHeader, cell)
}
t.AppendHeader(tableHeader)

tableBody := make([]table.Row, 0)
for _, row := range rows[1:5] {
    innerRow := make(table.Row, 0)
    for _, cell := range row {
        innerRow = append(innerRow, cell)
    }
    tableBody = append(tableBody, innerRow)
}
// 設置表格內容
t.AppendRows(tableBody)
// 設置表尾
t.AppendFooter(table.Row{"乘客ID", "獲救情況", "乘客等級", "姓名", "性別", "年齡", "堂兄弟妹個數", "父母與小孩個數", "船票信息", "票價", "船艙", "登船的港口"})

t.Render()

結果:

Pasted image 20240402180245.png

自動合併單元格

通過配置 RowConfigColumnConfig 可以實現自動在水平和垂直合併指定的單元格,並設置其合併後的數據對齊方式。這個功能只適合使用 t.Render() 渲染輸出,對於 CSV/HTML/Markdown/TSV 模式輸出並不支持。

rowConfigAutoMerge := table.RowConfig{AutoMerge: true}
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)

tableHeader := make(table.Row, 0)
for _, cell := range rows[0] {
    tableHeader = append(tableHeader, cell)
}
t.AppendHeader(tableHeader, rowConfigAutoMerge)

for _, row := range rows[1:10] {
    innerRow := make(table.Row, 0)
    for _, cell := range row {
        innerRow = append(innerRow, cell)
    }
    t.AppendRow(innerRow, rowConfigAutoMerge)
    t.AppendSeparator()
}
t.AppendFooter(table.Row{"乘客ID", "獲救情況", "乘客等級", "姓名", "性別", "年齡", "堂兄弟妹個數", "父母與小孩個數", "船票信息", "票價", "船艙", "登船的港口"})
t.SetColumnConfigs([]table.ColumnConfig{
    // 第2列進行合併
    {Number: 2, AutoMerge: true},
})
t.Render()

結果:

Pasted image 20240407160326.png

表格的行和列都可以單獨或者同時設置對齊方式,水平為 Align(示例:Align: text.AlignCenter),垂直為 VAlign(示例:VAlign: VAlignMiddle)。由於輸出的是純文本格式,因此在垂直方向是做不到絕對居中的,我們通過修改數據源列 NAME 的其中一行數據使其文本內容增加以顯示出多行,然後限制這一行的寬度來查看實際效果,發現只有奇數行居中了,這是符合實際的。但是合併後的單元格並沒有跨行居中,只是在其所在單元格同樣以奇數行居中。

t.SetColumnConfigs([]table.ColumnConfig{
    {
        Number:    2,
        AutoMerge: true,
        VAlign:    text.VAlignMiddle,
        Align:     text.AlignCenter,
    },
    {
        Number: 3,
        VAlign: text.VAlignMiddle,
        Align:  text.AlignCenter,
    },
    {
        Number:   4,
        WidthMax: 20,
    },
})

結果:

企業微信截圖_17120561052292.png

<font color="#c00000">注意</font>:在 ColumnConfig 中我們是通過 Number 來指定要配置的列,也可以通過 Name 來設置。如果
同時設置,前都會覆蓋後者。對於 Name 配置需要注意一點的是輸出結果中的表頭顯示是被格式化成全部大寫了,這裏我們需要指定原始的形式,比如最後一行應為:Embarked

此外:在內容為中英文加符合的情況下,輸出結果也會出現不對齊的情況。

上面我們對內容進行了水平和垂直對齊設置,對於表頭和表尾我們可以通過 AlignHeaderAlignFooter 來分別設置每一列的對齊。

水平對齊方式可選值:AlignDefault, AlignLeft, AlignCenter, AlignJustify, AlignRight, AlignAuto,默認為:AlignLeft, 自動情況下,數字向右對齊,其它情況向左對齊。

垂直對齊方式可選值:VAlignDefault, VAlignTop, VAlignMiddleVAlignBottom,默認為:VAlignTop

添加表格標題和描述

通過調用 SetTitle()SetCaption() 來設置標題和描述文字,渲染後標題位於表頭上方一行,不可設置對齊方式(默認左對齊),而描述則位於表格的下方。由於這 2 個方法實際是對 fmt.Printf() 的一個透明封裝,因此我們可以傳多個參數來格式化輸出。

t.SetTitle("泰坦尼克號數據集")
t.SetCaption("%s 數據集作為 kaggle 比賽中的經典數據集,非常適合作為數據分析入門的練手數據,同時網上也有許多分析處理案例。", "Titanic")

結果:

Pasted image 20240403114232.png

自動索引

通過調用 t.SetAutoIndex(true) 來激活自動索引功能,我們取數據第 10-15 條,顯示結果如下:

Pasted image 20240403114959.png

限制行寬度和列寬度

如果行的內容過長的話,只會換行,不會出現水平滾動條,因此 Go-pretty 提供了 t.SetAllowedRowLength() 方法來限制寬度,超出的部分在每行以 ~ 來顯示。對於列的限制我們只需要在 table.ColumnConfig 中設置 MinWidthMaxWidth 即可,內容會自動換行。

t.SetAllowedRowLength(120)
t.SetColumnConfigs([]table.ColumnConfig{
    {Number: 4, WidthMax: 16},
})

結果:

Pasted image 20240403123652.png

這裏需要注意的是默認的換行方式為 text.WrapText,如果需要根據內容選擇不同的換行方式(官方支持三種 WrapHard/WrapSoft/WrapText),可以在 WidthMaxEnforcer 配置中進行判斷和指定,具體如何選用需要根據文字內容顯示的結果合理選用。

t.SetColumnConfigs([]table.ColumnConfig{
    {
        Number:   4,
        WidthMax: 16,
        WidthMaxEnforcer: func(col string, maxLen int) string {
            // 根據 col 和 maxLen 來選擇合適的換行方式
            return text.WrapText(col, maxLen)
        },
    },
})

三種換行方式對比如下:

Pasted image 20240403123112.png

分頁顯示

對於數據較多的場景,可以使用分頁功能,按指定的大小,來分頁顯示,只需要設置 t.SetPageSize() 即可。

Pasted image 20240408121509.png

數據排序

我們可以按指定的列進行排序,比如先按年齡排序,然後相同年齡再按票價進行排序。這裏需要使用到 t.SortBy() 方法。排序方式有:Asc, AscAlphaNumeric, AscNumeric, AscNumericAlpha, Dsc, DscAlphaNumeric, DscNumericDscNumericAlpha 8 種方式。

t.SortBy([]table.SortBy{
    {Name: "Age", Mode: table.Asc},
    {Name: "Fare", Mode: table.AscNumeric},
})

排序後的結果:

Pasted image 20240403152635.png

設置單元格顏色

t.SetColumnConfigs() 中配置 table.ColumnConfigColors, ColorsHeader, ColorsFooter 可以實現對錶頭、內容和表尾前景後和背景設置。

t.SetColumnConfigs([]table.ColumnConfig{
    {
        Name: "Pclass",
        Colors: text.Colors{
            text.FgHiBlue,
        },
        ColorsHeader: text.Colors{
            text.BgCyan,
        },
    },
    {
        Name: "Sex",
        Colors: text.Colors{
            text.BgHiGreen,
            text.FgHiYellow,
        },
    },
})

結果如下:

Pasted image 20240403154712.png

單元格格式化

很多場景下我們需要對數據作轉換,根據條件以不同的方式來顯示數據,對髒數據設置默認值等。我們可以通過 Transformer, TransformerFooterTransformerHeader 來對錶頭、內容和表尾進行轉換。下面我們對性別用顏色加圖標替換顯示:

t.SetColumnConfigs([]table.ColumnConfig{
    {
        Name: "Sex",
        Transformer: func(val interface{}) string {
            if val == "male" {
                return text.Colors{text.FgBlue}.Sprintf("♂")
            } else if val == "female" {
                return text.Colors{text.FgRed}.Sprintf("♀")
            } else {
                return ""
            }
        },
    },
})

結果如下:

Pasted image 20240403160204.png

切換主題

Go-pretty 默認提供了很多主題和不同的邊框類型。由於文章最開始提及的官方文檔中有很詳細的説明,這裏就不再贅述了,而且很多時候我們並不需要,直接上效果圖:

t.SetStyle(table.StyleColoredBright)

結果如下:

Pasted image 20240403161025.png

輸出不同的文檔格式

上面所有的示例都是在控制枱中輸出格式化後的表格數據,我們也可以調用相應的函數格式化輸出為 CSV/TSV/HTML Table/Markdown Table 的表格描述方式。

  • t.RenderCSV() 輸出 CSV,以逗號分隔數據的純文本
  • t.RenderTSV() 輸出 TSV,即以製表符分隔數據的純文本
  • t.RenderHTML() 輸出 HTML 表格表示
  • t.RenderMarkdown() 輸出 Markdown 表格表示

注意:單元格合併僅支持通用輸出 t.Render()

輸出列表數據

go-pretty/list 提供了數據列表格式化輸出的一些功能,就像我們在 HTML 中使用 <ul><li>fqgx</li></ul> 一樣,當然我們也可以直接渲染輸出為 HTML 表示。

要想生成一個列表,可以通過 l := list.NewWriter() 或者 l := list.List{} 直接實例化來初始化。然後就可以通過 l.AppendItem() 來添加列表項數據,使用 l.Indent() / l.UnIndent() / UnIndentAll() 來增加/減少/重置所有縮進。

默認樣式

默認輸出樣式其實和 HTML 中的 <ul> 是一樣的。

package main

import (
    "fmt"

    "github.com/jedib0t/go-pretty/v6/list"
)

func main() {
    l := list.List{}
    l.AppendItem("Item 1")
    l.AppendItem("Item 2")
    l.AppendItem("Item 3")
    l.Indent()
    l.AppendItem("Item 4")
    l.AppendItem("Item 5")
    l.UnIndent()
    l.AppendItem("Item 6")
    l.AppendItem("Item 7")
    fmt.Println(l.Render())
    // fmt.Println(list.RenderHTML())
}

// 結果:
// * Item 1
// * Item 2
// * Item 3
//   * Item 4
//   * Item 5
// * Item 6
// * Item 7

其它可選樣式

除了默認的效果外,官方還提供了多種候選樣式。

l.SetStyle(list.StyleBulletCircle)
// ● Item 1
// ● Item 2
// ● Item 3
//   ● Item 4
//   ● Item 5
// ● Item 6
// ● Item 7

l.SetStyle(list.StyleBulletFlower)
// ✽ Item 1
// ✽ Item 2
// ✽ Item 3
//   ✽ Item 4
//   ✽ Item 5
// ✽ Item 6

l.SetStyle(list.StyleBulletSquare)
// ■ Item 1
// ■ Item 2
// ■ Item 3
//   ■ Item 4
//   ■ Item 5
// ■ Item 6
// ■ Item 7

l.SetStyle(list.StyleBulletStar)
// ★ Item 1
// ★ Item 2
// ★ Item 3
//   ★ Item 4
//   ★ Item 5
// ★ Item 6
// ★ Item 7

l.SetStyle(list.StyleConnectedBold)
// ┏━ Item 1
// ┣━ Item 2
// ┣━ Item 3
// ┃  ┣━ Item 4
// ┃  ┗━ Item 5
// ┣━ Item 6
// ┗━ Item 7

l.SetStyle(list.StyleConnectedDouble)
// ╔═ Item 1
// ╠═ Item 2
// ╠═ Item 3
// ║  ╠═ Item 4
// ║  ╚═ Item 5
// ╠═ Item 6
// ╚═ Item 7

l.SetStyle(list.StyleConnectedLight)
// ┌─ Item 1
// ├─ Item 2
// ├─ Item 3
// │  ├─ Item 4
// │  └─ Item 5
// ├─ Item 6
// └─ Item 7

l.SetStyle(list.StyleConnectedRounded)
// ╭─ Item 1
// ├─ Item 2
// ├─ Item 3
// │  ├─ Item 4
// │  ╰─ Item 5
// ├─ Item 6
// ╰─ Item 7

l.SetStyle(list.StyleMarkdown)
// * Item 1
// * Item 2
// * Item 3
//   * Item 4
//   * Item 5
// * Item 6
// * Item 7

自定義樣式

如果默認的樣式可能滿足不了你的需求,作者還提供了靈活的自定義設置。

下面是一個簡單的示例:

package main

import (
    "fmt"

    "github.com/jedib0t/go-pretty/v6/list"
    "github.com/jedib0t/go-pretty/v6/text"
)

func main() {
    l := list.List{}
    funkyStyle := list.Style{
        CharItemSingle:   "✅",
        CharItemTop:      "✅",
        CharItemFirst:    "✅",
        CharItemMiddle:   "❎",
        CharItemVertical: "  ",
        CharItemBottom:   "✅",
        CharNewline:      "\n",
        Format:           text.FormatUpper,
        LinePrefix:       "",
        Name:             "styleTest",
    }
    l.SetStyle(funkyStyle)
    l.AppendItem("Item 1")
    l.AppendItem("Item 2")
    l.AppendItem("Item 3")
    l.Indent()
    l.AppendItem("Item 4")
    l.AppendItem("Item 5")
    l.UnIndent()
    l.AppendItem("Item 6")
    l.AppendItem("Item 7")
    fmt.Println(l.Render())
}

// ✅ ITEM 1
// ❎ ITEM 2
// ❎ ITEM 3
//   ✅ ITEM 4
//   ✅ ITEM 5
// ❎ ITEM 6
// ✅ ITEM 7

上述代碼中我們使用 text.FormatUpper 來將選項名稱格式化成大寫字母,可選擇的值還有

  • FormatLower - 全部小寫
  • FormatTitle - 標題首字母大宇
  • FormatDefault - 默認效果,不格式化

其它渲染方式

除了默認的渲染方式 l.Render(),還支持輸出為 HTML,使用 l.RenderHTML() 以及 Markdown, 使用 l.RenderMarkdown()。在輸出 HTML 時,我們還可以使用 l.SetHTMLCSSClass("ul") 來指定 <ul> 元素的樣式。

package main

import (
    "fmt"

    "github.com/jedib0t/go-pretty/v6/list"
)

func main() {
    l := list.List{}
    l.AppendItem("Item 1")
    l.AppendItem("Item 2")
    l.AppendItem("Item 3")
    l.Indent()
    l.AppendItem("Item 4")
    l.AppendItem("Item 5")
    l.UnIndent()
    l.AppendItem("Item 6")
    l.AppendItem("Item 7")
    l.SetHTMLCSSClass("ul")
    fmt.Println(l.RenderHTML())
}

// <ul class="ul">    
//   <li>Item 1</li>  
//   <li>Item 2</li>  
//   <li>Item 3</li>  
//   <ul class="ul-1">
//     <li>Item 4</li>
//     <li>Item 5</li>
//   </ul>
//   <li>Item 6</li>  
//   <li>Item 7</li>  
// </ul>

進度條

Go 社區中有很多進度條相關的庫,例如:

  • schollz/progressbar: A really basic thread-safe progress bar for Golang applications (github.com)
  • vbauerster/mpb: multi progress bar for Go cli applications (github.com)
  • cheggaaa/pb: Console progress bar for Golang (github.com)

顯然沒有使用過以上列出的 3 個庫,但是從其官方介紹來看和 Go-pretty 提供的進度條相對比在功能上還是相對較簡單,但是 Go-pretty 的作者並沒有給出一個入門使用文檔,而是在源文件中給出了一個複雜的示例,初看時會一頭霧水,理解後會發現這個進度條真的很美觀,很強大。

作者介紹其進度條有以下功能點:

  • 同時跟蹤一個或多個任務
  • Render() 過程中動態添加一個或多個任務跟蹤器
  • 當沒有更多的跟蹤器時,選擇讓 Writer 自動停止渲染或者手動使用 stop() 停止
  • 將輸出重定向到 io.Writer 對象 (如 os.StdOut)
  • 完全可自定義的樣式
  • 許多現成的樣式
  • 使用 StyleColors 為跟蹤器的各個部分着色
  • 使用 StyleOptions 自定義跟蹤器的渲染方式

實際效果推薦看官方倉庫中的 Gif 圖。

進度條效果非常適合來演示文件下載的進度,下面給出一個示例:下載指定 URL 的資源,創建臨時目錄存放下載的資源,資源下載完成後刪除臨時目錄並存放至指定目錄中。

作者 Go 新手:代碼就不作封裝了...
package main

import (
    "fmt"
    "io"
    "net/http"
    "net/url"
    "os"
    "path/filepath"
    "time"

    "github.com/jedib0t/go-pretty/v6/progress"
    "github.com/jedib0t/go-pretty/v6/text"
    tsize "github.com/kopoli/go-terminal-size"
)

type Downloader struct {
    Total    int64  // 文件大小
    Current  int64  // 當前接收的字節數
    Last     int64  // 上一次接收的字節數
    filename string // 文件名
    pw       progress.Writer
    tracker  *progress.Tracker
}

func (d *Downloader) Write(p []byte) (int, error) {
    n := len(p)
    d.Current += int64(n)
    increment := d.Current - d.Last
    d.Last = d.Current

    d.tracker.Increment(increment)

    if d.Current == d.Total {
        d.tracker.Total = 0
        d.tracker.MarkAsDone()
    }

    return n, nil
}

func DownloadFile(imgPath string, url string) error {
    // 創建目錄
    tempDir, err := os.MkdirTemp(".", "dir")
    if err != nil {
        return err
    }
    defer os.Remove(tempDir)

    // 創建臨時文件
    tmpFile, err := os.CreateTemp(tempDir, imgPath)
    if err != nil {
        return err
    }
    defer tmpFile.Close()
    defer os.Remove(tmpFile.Name())

    // 下載文件
    resp, err := http.Get(url)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    counter := &Downloader{}
    counter.Total = resp.ContentLength
    counter.Last = 0
    counter.filename = imgPath

    width := 100
    if size, err := tsize.GetSize(); err == nil {
        width = size.Width
    }

    counter.pw = progress.NewWriter()
    counter.pw.SetTrackerLength(width / 3)
    counter.pw.SetMessageLength(width / 2)
    counter.pw.SetAutoStop(true)
    counter.pw.SetStyle(progress.StyleDefault)
    counter.pw.SetUpdateFrequency(time.Millisecond * 100)
    counter.pw.Style().Colors = progress.StyleColorsExample
    counter.pw.Style().Options.DoneString = text.FgBlue.Sprint("下載完成")
    counter.pw.Style().Options.ErrorString = text.FgRed.Sprint("下載失敗")
    counter.pw.Style().Options.PercentFormat = "%4.1f%%"
    counter.pw.Style().Visibility.ETA = true
    counter.pw.Style().Visibility.Pinned = true

    go counter.pw.Render()

    message := fmt.Sprintf("Downloading %s", counter.filename)
    units := progress.UnitsBytes
    tracker := progress.Tracker{
        Total:      counter.Total,
        Message:    message,
        Units:      units,
        DeferStart: false,
    }
    counter.pw.AppendTracker(&tracker)
    counter.tracker = &tracker

    if _, err := io.Copy(tmpFile, io.TeeReader(resp.Body, counter)); err != nil {
        tmpFile.Close()
        os.Remove(tmpFile.Name())
        return err
    }

    for counter.pw.IsRenderInProgress() {
        if counter.pw.LengthActive() == 0 {
            counter.pw.Stop()
        }
    }

    // 確保臨時文件的內容已經刷新到磁盤
    err = tmpFile.Sync()
    if err != nil {
        panic(err)
    }

    tmpFile.Close()

    // 將臨時文件重命名為最終文件
    if err = os.Rename(tmpFile.Name(), filepath.Join("image", imgPath)); err != nil {
        return err
    }
    return nil
}

func main() {
    fmt.Println("Download Started")

    fileUrl := "http://212.183.159.230/512MB.zip"
    parsedUrl, err := url.Parse(fileUrl)
    if err != nil {
        panic(err)
    }
    path := filepath.Base(parsedUrl.Path)
    err = DownloadFile(path, fileUrl)
    if err != nil {
        panic(err)
    }

    fmt.Println("Download Finished")
}

效果截圖:

Pasted image 20240415184242.png

建議仔細研究一下官方關於進度條的示例,然後再去 Github 上看看別人怎麼封裝使用的,作者本身就是初學者,所以沒有相關項目經驗,只能初窺門徑,能簡單用起來,作個 Demo 效果。

後續隨着學習的深入和經驗的積累,再分享具體項目中使用。

寫在最後

感謝庫作者 jedib0t ,在寫作文章時發現了一些 Bug,作者很快就回復修復了。

  • Paging result not expected · Issue #312 · jedib0t/go-pretty (github.com)
  • Paging is set when row/column cell merge is set at the same time, missing some cell data · Issue #315 · jedib0t/go-pretty (github.com)

文章參考:

  • 【Go】用Go在命令行輸出好看的表格_go-pretty-CSDN博客
  • 編程日記:分享開源庫go-pretty - 掘金 (juejin.cn)
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.