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 要資料的架構。

2022年5月7日 星期六

Crypto Metal VISA Cards 與 Spotify Premium Family Plan

圖:Crypto.com 官網截圖 @ 2022-05-07

申請幾個月了,一直偷懶沒寫來筆記 XD 當初在 2021 年底觀望了 Crypto 信用卡,想說來了解這個行業,算了一下風險來試試看吧!

接著則是使用 Spotify - Premium Family Plan 再辦理優惠 cc 看了一下網路上常簡介的方式,共有兩種:

  1. 透過新加坡 VPN,接著將到 Spotify 官網切換國家地區至新加坡,並且使用 Crypto.com Metal VISA Card 來支付
  2. 先綁定一張台灣的信用卡支付(或是體驗方案)Premium Family 方案,接著再設法換信用卡到 Metal VISA Card

因為我本身早就有在用 Spotify 付費了,直接升級到 Premium Family Plan ,Spotify 佛心地說,等到下次支付週期才收 Premium Family Plan 費用。這時的我,就是使用 Spotify 台灣,便利的邀請家人加入方案。

下一刻,則是要更換 Metal VISA CARD,其實輸入卡號綁定都會顯示失敗 XD 最後忘記是不是把網址改成 hk 去綁定才搞定的,還是我也自行換成新加坡地區?比較特別的是我一直維持在 Spotify Taiwan,但經歷過第一次 Crypto Metal VISA Card 支付後,Spotify 顯示的國家地區就真的被切換到新加坡了。

當 Spotify 顯示的國家變成新加坡時,不方便的地方有兩個:

  • 那些初次使用 Spotify 的人,通常習慣聽台灣相關的歌曲的,這時只能在慢慢 "養" 一下帳號,像是聽聽 "前 50 名 - 臺灣" 來練練資料庫,而原先已經用一陣子的,推論就不太會有什麼感覺
  • 當管理者想要添加對方加入到 Spotify Premium Family Plan 時,就必須要求對方國家更改到新加坡才行

至於刷 Metal VISA Card 的部分,約莫一小時內,會看到以 Crypto.com 的平台幣 CRO 回饋進帳,我的習慣就是立馬再轉成 USDC (甚至在匯入到 Metal VISA CARD 的 GSD 儲值)。

由於 Crypto - Metal VISA Card 是 Debit Card 機制,是必須裡頭有錢才可以刷的。這時就得看自己打算怎樣規劃,因為 Crypto.com 有提供新戶初次匯款的優惠(30天內免手續費),我是規劃了一筆 USDC 鎖幣,可以有小額的利息費,做一些應用調整(若匯差轉換的損失等),現況純粹拿來研究小東西,不會是我的主力刷卡用途。

有興趣的需要多多了解一下未來要付卡費時,想走的流程。例如用其他信用卡在 Crypto.com 刷虛擬幣 (被收信用卡國外手續費+Crypto.com 平台手續費) -> 用虛擬幣轉法幣 GSD (Crypto.com 交易手續費) -> 完成 Metal VISA Card 儲值。而大部分好像是推薦匯款到 USDC 指定帳戶,可能是最優惠的路線。

目前是 2022-05-07 ,順勢聊一下 Crypto.com 的平台幣 CRO 的價格變化:


從圖大概可以了解,從 2021-11-23 至今 2022-05-07 已經打折再打折 XD 而我是在 2022 年三月去使用的。因為辦 Metal VISA Card 需要質押對應數量的 CRO 平台幣,等於把一筆錢押在裡頭的。這筆錢若用 Spotify Premium Family Plan 來支付的話,大概兩年可以回收的,所以風險低不少(當然,莫忘 Crypto.com 修改福利又是另一個風險)。此外,最近 FED 已宣佈不少資訊(升息、QE退場縮表),通常 2017 年經歷過幣圈的都會很抖,不知平台幣的變化會多少,例如近一個月 CRO 從 0.45 跌到 0.27 了,快打對折了 XD 只能說穩穩有 Spotify Premium Family Plan 回饋,其實就不太擔心(?)


此外,這幾天 Crypto.com 頻頻動作,像發了數封 EDM 邀請大戶們不要賣幣以及提信用卡優惠(?),說真的也是穩定幣價的不二法門。相信之後還得面對正式的縮表,以及以太坊2.0升級 XD 

圖:2022-05-06 EDM

圖:2022-05-03 EDM

當初我看到他的商業模式小試身手:
  • 質押CRO享受信用卡回饋
  • 質押CRO等同有穩定自家幣價
最後證明,事實上波動仍是很大的 XD 此外,有高手是採用放空 CRO 平台幣的方式來故意讓 CRO 變成穩定幣,這就得各自評估風險了

最後再聊一下關於 "鎖幣賺利息",目前最長是三個月為一期。而年利率有 6% 不等,有興趣可以用穩定幣 USDC / USDT 體驗看看。而這個 % 變化主要還是來自於虛擬幣交易市場的供需問題,當前景大好時,很多人會借錢買幣(當沖?),這時借貸活動踴躍時,這個利息 % 就會變高,據說 2021 極為活躍時,可以來到年利率 15% 以上的水準(有的交易所為了吸引人,就是提高這%數,這時也得擔心該交易所穩不穩,會不會倒),因此鎖幣賺利息的來源,也是市場供需的。

圖:Crypto app 內的鎖幣賺利息 USDC 的年利率