2022年5月30日 星期一

Go 開發筆記 - 使用 Regular Expression / Regex / Re

網路上還滿多教學以及文件,其中官方文件 pkg.go.dev/regexp 滿清楚的。也有看到一篇許多人推的 github.com/StefanSchroeder/Golang-Regex-Tutorial ,有中英文。

在此把一些語法筆記一下,方便自己未來快速回憶。

使用筆記:
  • 有兩種 Regular Expression 實現方式 ,其中有 POSIX 關鍵字是 POSIX ERE (egrep) 語法跟效果。
  • 初始化有 Compile 跟 MustCompile (對應的是 CompilePOSIX 和 MustCompilePOSIX) ,其中有 Must 字眼是驗證語法錯誤時,會進入 panics 狀態
使用範例:

% cat main.go
package main

// https://pkg.go.dev/regexp
// https://pkg.go.dev/regexp/syntax

import (
    "fmt"
    "regexp"
)

func main() {
    input := `
<html>
    <head>
        <title>study golang</title>
    </head>
    <body>
        <ul>
            <li>changyy.org</li>
            <li>1234567890</li>
            <li>abcdefg</li>
            <li>abcdefgABCDEFG</li>
            <li>abcdef1234567890gABCDEFG</li>
        </ul>
    </body>
</html>`

    // Style 1: regexp.Match
    {
        matched, err := regexp.Match(`<[A-Za-z]+>(.*?)</[A-Za-z]+>`, []byte(input))
        if err != nil {
            fmt.Println("Style 1 - regexp.Match error:", err)
        } else {
            fmt.Println("Style 1 - regexp.Match: ", matched)
        }
    }

    // Style 2: regexp.Compile + obj.MatchString
    // https://pkg.go.dev/regexp#Regexp.MatchString
    {
        obj, err := regexp.Compile(`<[A-Za-z]+>(.*?)</[A-Za-z]+>`)
        if err != nil {
            fmt.Println("regexp.Compile error:", err)
        } else {
            fmt.Println("Style 2 - case 1: ", obj.MatchString(`<title>Hello World</title>`))
            fmt.Println("Style 2 - case 2: ", obj.MatchString(input))
        }
    }

    // Style 3: regexp.MustCompile + obj.MatchString
    // https://pkg.go.dev/regexp#Regexp.MatchString
    {
        pattern := `<[A-Za-z]+>(.*?)</[A-Za-z]+>`
        obj := regexp.MustCompile(pattern)
        fmt.Println("Style 3 - case 1: ", obj.MatchString(`<title>Hello World</title>`))
        fmt.Println("Style 3 - case 2: ", obj.MatchString(input))
    }

    // Style 4: obj.FindString
    // https://pkg.go.dev/regexp#Regexp.FindString
    {
        pattern := `<[A-Za-z]+>(.*?)</[A-Za-z]+>`
        obj := regexp.MustCompile(pattern)
        fmt.Println("Style 4 - case 1: ", obj.FindString(`<title>Hello World</title>`))
        fmt.Println("Style 4 - case 2: ", obj.FindString(input))
    }

    // Style 5: obj.FindAllString , obj.FindAllStringIndex
    // https://pkg.go.dev/regexp#Regexp.FindAllString
    // https://pkg.go.dev/regexp#Regexp.FindAllStringIndex
    {
        pattern := `<[A-Za-z]+>(.*?)</[A-Za-z]+>`
        obj := regexp.MustCompile(pattern)
        fmt.Println("Style 5 - case 1: ", obj.FindAllString(`<title>Hello World</title>`, -1))
        fmt.Println("Style 5 - case 2: ", obj.FindAllString(input, -1))
        fmt.Println("Style 5 - case 3: ", obj.FindAllStringIndex(`<title>Hello World</title>`, -1))
        fmt.Println("Style 5 - case 4: ", obj.FindAllStringIndex(input, -1))
    }

    // Style 6: obj.FindStringSubmatch , obj.FindStringSubmatchIndex
    // https://pkg.go.dev/regexp#Regexp.FindStringSubmatch
    // https://pkg.go.dev/regexp#Regexp.FindStringSubmatchIndex
    {
        pattern := `<[A-Za-z]+>(.*?)</[A-Za-z]+>`
        obj := regexp.MustCompile(pattern)
        fmt.Println("Style 6 - case 1: ", obj.FindStringSubmatch(`<title>Hello World</title>`))
        fmt.Println("Style 6 - case 2: ", obj.FindStringSubmatch(input))
        fmt.Println("Style 6 - case 3: ", obj.FindStringSubmatchIndex(`<title>Hello World</title>`))
        fmt.Println("Style 6 - case 4: ", obj.FindStringSubmatchIndex(input))
    }

    // Style 7: obj.ReplaceAllString
    // https://pkg.go.dev/regexp#Regexp.ReplaceAllString
    {
        pattern := `<[A-Za-z]+>(.*?)</[A-Za-z]+>`
        obj := regexp.MustCompile(pattern)
        fmt.Println("Style 7 - case 1: ", obj.ReplaceAllString(`<title>Hello World</title>`, "A"))
        fmt.Println("Style 7 - case 2: ", obj.ReplaceAllString(input, "B"))
    }

    // Style 8: obj.Split
    // https://pkg.go.dev/regexp#Regexp.Split
    {
        pattern := `<[A-Za-z]+>(.*?)</[A-Za-z]+>`
        obj := regexp.MustCompile(pattern)
        fmt.Println("Style 8 - case 1: ", len(obj.Split(`<title>Hello World</title>`, -1)))
        fmt.Println("Style 8 - case 2: ", len(obj.Split(input, -1)))
    }

    // Style 9: obj.Longest()
    // https://pkg.go.dev/regexp#Regexp.FindString
    // https://pkg.go.dev/regexp#Regexp.Longest
    {
        pattern := `a(|b)`
        obj := regexp.MustCompile(pattern)
        fmt.Println("Style 9 - case 1: ", obj.FindString(`abb`))
        obj.Longest()
        fmt.Println("Style 9 - case 2: ", obj.FindString(`abb`))
    }
}

% go run main.go 
Style 1 - regexp.Match:  true
Style 2 - case 1:  true
Style 2 - case 2:  true
Style 3 - case 1:  true
Style 3 - case 2:  true
Style 4 - case 1:  <title>Hello World</title>
Style 4 - case 2:  <title>study golang</title>
Style 5 - case 1:  [<title>Hello World</title>]
Style 5 - case 2:  [<title>study golang</title> <li>changyy.org</li> <li>1234567890</li> <li>abcdefg</li> <li>abcdefgABCDEFG</li> <li>abcdef1234567890gABCDEFG</li>]
Style 5 - case 3:  [[0 26]]
Style 5 - case 4:  [[27 54] [103 123] [136 155] [168 184] [197 220] [233 266]]
Style 6 - case 1:  [<title>Hello World</title> Hello World]
Style 6 - case 2:  [<title>study golang</title> study golang]
Style 6 - case 3:  [0 26 7 18]
Style 6 - case 4:  [27 54 34 46]
Style 7 - case 1:  A
Style 7 - case 2:  
<html>
    <head>
        B
    </head>
    <body>
        <ul>
            B
            B
            B
            B
            B
        </ul>
    </body>
</html>
Style 8 - case 1:  2
Style 8 - case 2:  7
Style 9 - case 1:  a
Style 9 - case 2:  ab

2022年5月26日 星期四

Go 開發筆記 - 使用 database/sql 通用介面存取資料庫,以 SQLite3 為例

在 Golang 的世界,有定義資料庫存取的通用介面 database/sql ,但貌似官方沒有提供實作而是讓廣大的鄉民開發,並且標記哪些套件是有通過 go-sql-test 驗證的,因此,大部分就是挑哪些有標記的,或是直接看 github 有多熱門也行。

相關文件:
以下就連續動作,筆記一下。

程式碼:

package main

import (
    "log"
    "database/sql"
    _ "github.com/mattn/go-sqlite3"
)

func main() {
    db, err := sql.Open("sqlite3", "/tmp/sqlite3.db")
    if err != nil {
        log.Fatalln(err)
    }
    defer db.Close()

    //
    // http://go-database-sql.org/modifying.html
    //
    // via db.Exec with checking the error message only
    if _, err := db.Exec(`
        CREATE TABLE IF NOT EXISTS account (
            uid INTEGER PRIMARY KEY AUTOINCREMENT,
            username VARCHAR(64) NULL
        );
    `); err != nil {
        log.Println(err)
    }

    // Insert via db.Prepare
    if stmt, err := db.Prepare("INSERT INTO account(username) VALUES(?)"); err == nil {
        if res, err := stmt.Exec("changyy.org"); err != nil {
            log.Println("Insert Exec Error:", err)
        } else if lastId, err := res.LastInsertId() ; err != nil {
            log.Println("Get LastInsertId Error:", err)
        } else if rowCount, err := res.RowsAffected() ; err != nil {
            log.Println("Get RowsAffected Error:", err)
        } else {
            log.Println("Insert Done, Last Insert Id:", lastId, ", RowsAffected: ", rowCount)

            // Update via db.Prepare
            if stmt, err := db.Prepare("UPDATE account SET username = ? WHERE uid = ?"); err != nil {
                log.Println("Update Prepqre Error:", err)
            } else if res, err := stmt.Exec("blog.changyy.org", lastId); err != nil {
                log.Println("Update Exec Error:", err)
            } else if rowCount, err := res.RowsAffected() ; err != nil {
                log.Println("Get RowsAffected Error:", err)
            } else {
                log.Println("Update RowsAffected: ", rowCount)
            }
        }
    } else {
        log.Println("Prepqre Insert Error:", err)
    }

    //
    // http://go-database-sql.org/retrieving.html
    // https://pkg.go.dev/database/sql#DB.Query
    //
    // via db.Query with sql.Rows and error mesasge
    rows, err := db.Query("SELECT * FROM account")
    if err != nil {
        log.Println(err)
    } else {
        defer rows.Close()
        log.Println("Result:")
        for rows.Next() {
            var id int
            var username string
            if err := rows.Scan(&id, &username) ; err == nil {
                log.Println(id, username)
            } else {
                log.Println(err)
            }
        }
        if err := rows.Err() ; err != nil {
            log.Println(err)
        }
    }
}

執行:

% go run main.go    
2022/05/25 20:44:26 Insert Done, Last Insert Id: 1 , RowsAffected:  1
2022/05/25 20:44:26 Update RowsAffected:  1
2022/05/25 20:44:26 Result:
2022/05/25 20:44:26 1 blog.changyy.org

% file /tmp/sqlite3.db
/tmp/sqlite3.db: SQLite 3.x database, last written using SQLite version 3038005, file counter 3, database pages 3, cookie 0x1, schema 4, UTF-8, version-valid-for 3

% sqlite3 /tmp/sqlite3.db .schema
CREATE TABLE account (
            uid INTEGER PRIMARY KEY AUTOINCREMENT,
            username VARCHAR(64) NULL
        );
CREATE TABLE sqlite_sequence(name,seq);

2022年5月22日 星期日

Go 開發筆記 - 使用 golang.org/x/oauth2 與 Facebook 登入 / Google OAuth 串接

最近評估網站是否從 PHP 翻到 Golang ,研究了一下關於串接 OAuth2 相關部分。早年在串 FB 登入時,都是直接使用 Facebook PHP SDK ,雖然都知道底層還是 OAuth2 ,但不免還是擔心要串時很麻煩(主要是很懶再刻一份)。稍微研究了一下,原來有 golang.org/x/oauth2 套件可以用,裡頭有支援了各式各家的登入機制,非常方便。

接著反而開始複習起來 Facebook 登入 該怎樣處理,過程:
  • 建立一個 FB 應用程式 developers.facebook.com/apps/
  • 設定 FB 登入相關事宜,包括應用程式網域(添加 localhost)、FB 登入用戶端 OAuth 設定,如 有效的 OAuth 重新導向 URI
  • 處理相關雜事
結果處理相關雜事反而耗掉最多時間,包括:
  • FB應用程式要儲存時,還得弄個 隱私政策網址 跟 用戶資料刪除 網頁
  • FB登入相關,要求都走 https 溝通,變成要研究 golang gin 如何跑 https web server 出來、憑證該怎樣產生等
  • 寫完程式後,體驗流程後,想弄個 github 筆記一下且降低程式碼變動,開始規劃如何靠 YAML 檔案來抽換設定檔
大概就是如此,花了不少時間。最後的效果純粹驗證支援 FB 登入是可行的,收工 XD

2022年5月21日 星期六

Switch 防塵 防塵箱 / 防塵套 / 防塵布

圖:不到60元的小箱子,尺寸 325x167x155mm

前陣子因為信用卡回饋折扣,敗了台 Switch ,順便當作防疫包升級。恰好這後疫情時代,Switch跟健身環不再是難買的,以及本身也不打算拿著 Switch 小螢幕玩,就買了大概 8500 的舊機(因折扣平台限制,不能挑其他便宜的地方),下一刻就是再買原廠手把、健身環大冒險,以及下載 "遊戲盒子" 隨時關注想收藏的數位版遊戲,整個入坑太深,當初信用卡折扣早已用光 XD

回過頭來,在想是不是要買個防塵的東西,結果找了下有人專門在做壓克力板也算精美,價格大概三四百。由於我多買了充電的設備,size若要的話,已經需要訂製才行。

此外,我覺得還是要留意散熱,不太適合一直罩住的。單純買個蓋子避免灰塵即可!原本想買一些硬塑膠板自己製作,結果室友神指示,就買了不錯的小籃子頂替。推敲了一下,最適合的此寸高度是 15cm 的,寬(深)也 15cm,剩下長 30cm就夠用,就是找 15x15x30 的小籃子,網路上就隨便找一個不用百元的,搞定!

其中買了三款箱子,尺寸分別是:

  • 325x167x155mm (商品編號 P5-0255)
    • 非常剛好,但缺點是全密封
  • 169x283x130mm (商品編號 KGB-201)
    • 原本期待這個是最佳的,非密封可散熱,但發現13cm不夠高,推論還是switch稍微撐著它
  • 168x281x119mm (商品編號 KGB-102)
    • 順手測一下,最後也不適用,當作收納籃使用

2022年5月16日 星期一

Go 開發筆記 - 地圖服務 查詢剩餘快篩劑販售地點 為例




之前一直想練習 Golang 卻一直偷懶,這次就找個題目練了一下。包括如何取得網路上的資料,以及處理 csv 的流程。然而,最後想找 Heroku 來發布,就...全部改成 HTML+JS 處理而已,但還是留點紀錄吧。

若單純想要使用 政府資料開放平臺 - 健保特約機構防疫家用快篩剩餘數量明細 來作為範例時,其實可以只有一個 HTML 檔案就搞定,因為政府資料開放平臺下載 CSV 的那隻,支援大家隨意呼叫?我是用 Golang 寫完 backend api 後,為了發布到 Heroku 備忘時,就乾脆多寫個直接呼叫的版本

// https://data.gov.tw/dataset/152408
% curl -I https://data.nhi.gov.tw/resource/Nhi_Fst/Fstdata.csv
HTTP/1.1 200 OK
...
Access-Control-Allow-Origin: *
...

而 Golang 的部分,單純使用 gin framework 而已,主程式極短,單純吐 HTML 出去。

最後 templates/index.tmpl 就是 HTML/CSS/JS 的領域而已,分別使用了

- bootstrap@5.1.3
- leaflet@1.8.0
- leaflet.markercluster@1.5.3
- jquery@3.6.0
- jquery-csv@1.0.21

就簡單的桌面版型交給 Bootstrap (對的,沒有mobile版型),地圖則是使用 OpenStreetMap 服務,搭配 leaflet js sdk 管理,最後再用個 jQuery 處理 api 詢問,以及 jquery-csv 處理 CSV 格式。

原本用 Golang 開發 api 時,做了一些 api cache 管理跟 csv parsing 等,因發布到 Heroku 時,簡化成純前端任務就沒在使用了。不然在 Golang 還有規劃避免太頻繁詢問 data.nhi.gov.tw 要資料的架構。