透過加入 CSV 支援,讓 45 年歷史的 AWK 現代化
我最近為 GoAWK——我開發的 POSIX 相容 AWK 直譯器——加入了對 CSV 檔案的完整支援,我認為這項功能在這個資料至上的時代,能讓 AWK 對開發者與資料分析師變得更加實用。
不管是要產生試算表的輸入資料、分析來自公開資料來源的資料,還是撰寫處理「大數據」的腳本,CSV 與 TSV 檔案在今天都無處不在。至於它是不是最好的格式還有待商榷,但就像有人說的,資料格式只有兩種:一種是大家抱怨的,一種是沒人用的。
自從 2018 年釋出 GoAWK 後不久,在 Hacker News 上看到一則留言,我就一直在考慮為 GoAWK 加入 CSV 支援:
哈。我正想著,如果有個能原生理解 CSV 檔的工具裡可以用上 awk 程式語言,那該有多實用。突然間這件事感覺可行多了!
當時我給了一個不置可否的回應:
有趣的觀點。我也想過 AWK 應該要有個能正確解析 CSV 引號的模式。也許我會加個 -csv 選項(或者當 FS 是 ‘,’ 時就自動啟用——不過這樣就無法向下相容了)。
好吧,現在我們真的有這樣的模式了!最後定案的命令列選項是 -i csv(「輸入 CSV」),也算八九不離十了。
特別感謝安特衛普大學圖書館贊助了這項功能。他們是我所知兩個主要使用 GoAWK 的團隊或專案之一——另一個是 Benthos 串流處理器。
但,為什麼需要這個?
我們到底為什麼需要這個?遺憾的是,標準的 AWK 沒有辦法處理帶有引號欄位的 CSV 檔案,而這對於處理真實世界的 CSV 檔案至關重要。
你可以把欄位分隔符設為逗號(-F, 或 FS=","),但這完全是權宜之計,只要一碰到引號欄位就會出錯。例如:
$ cat quoted.csv
"Smith, Bob",42
$ awk -F, '{ print $1 }' quoted.csv
"Smith # you want to print the first field: Smith, Bob目前有幾種用 AWK 處理 CSV 的變通方法,例如 Gawk 的 FPAT 功能、各種 Gawk 的 CSV 擴充套件,或是 Adam Gordon Bell 的 csvquote 工具——你得先執行它來轉換輸入再跑 awk,之後再執行 csvquote -u 來還原轉換。
還有 frawk,這是 Eli Rosenthal 開發、原生支援 CSV 的優秀工具,可惜它在一定程度上偏離了 POSIX 標準的 AWK。(Frawk 啟發了 GoAWK 的部分 CSV 支援,包含 -i 與 -o 命令列選項。)
所以我認為,在一個與 POSIX 相容的 AWK 版本中,確實需要完整的 CSV 支援,而 GoAWK 正好提供了這一點。
在深入介紹之前,讓我們先用 GoAWK 的 CSV 輸入模式來修正上面範例中的引號問題:
$ goawk -i csv '{ print $1 }' quoted.csv
Smith, Bob # that's better!功能
CSV 輸入模式。 -i csv 選項會讓 GoAWK 使用 CSV 輸入模式:換句話說,就是忽略標準的欄位與記錄分隔符(FS 與 RS),改用 CSV 解析。也有 TSV 模式,或是可以使用自訂的分隔字元。
此外,如果你使用 -H 選項,GoAWK 會將輸入的第一列作為欄位名稱,並提供實用的 @"named-field" 語法,讓你可以用名稱而非編號來取得欄位。
例如,如果我們有一個 states.csv 檔案,內容如下:
"State","Abbreviation"
"Alabama","AL"
"Alaska","AK"
"Arizona","AZ"
...我們可以用這個指令稿只輸出州名縮寫:
$ goawk -i csv -H '{ print @"Abbreviation" }' states.csv
AL
AK
AZ
...或者,要計算名稱中包含「New」的州有幾個:
$ goawk -i csv -H '@"State" ~ /New/ { n++ } END { print n }' states.csv
4~ 是標準 AWK 中的正則表達式比對運算子,所以這段程式碼的意思是:對於 State 欄位符合正則表達式 New 的每一筆記錄,將 n 加一;最後印出 n。
CSV 輸出模式。 -o csv 命令列參數會讓 GoAWK 使用 CSV 輸出模式:這會讓帶有一個或多個參數的 print 以正確的 CSV 編碼來輸出。
例如,要將 states.csv 轉為 TSV,可以使用以下指令(這裡我們沒有使用 -H,所以會包含標題列):
$ goawk -i csv -o tsv '{ print $1, $2 }' states.csv
State Abbreviation
Alabama AL
Alaska AK
Arizona AZ
...另外,我們還擁有標準 AWK 的所有功能,所以能做的事情還有很多。
閱讀 CSV 文件以取得完整細節與更多範例。
實作說明
實作約有 2000 行程式碼,包含完整的測試。
當然也有 Go API,讓你可以透過 interp.Config 結構來設定選項,以啟用 CSV 輸入或輸出模式。你也可以在 BEGIN 區塊中設定 INPUTMODE 或 OUTPUTMODE 特殊變數。
由於各種原因,我無法在 CSV 輸入模式中直接使用 encoding/csv.Reader,但我在 csvSplitter 中重用了標準函式庫的程式碼結構,並將其改寫為一個 bufio.SplitFunc,它會解析欄位並將整列作為 token 提供。
除了我自己撰寫的大量測試之外,我還在我的實作上執行了標準函式庫中 csv.Reader 測試的相關子集,以確保沒有出錯,並且仍符合 RFC 4180。
至於 CSV 輸出模式,我則能直接使用 encoding/csv.Writer(不過寫入 CSV 比讀取簡單得多)。詳情請參閱 writeCSV 函式。
效能
我還沒有花太多時間在效能上。我打算找個時間好好做效能分析,但就目前而言,它的表現已經「夠好了」:與直接使用 Go 的 encoding/csv 套件不相上下,而且比 Python 的 csv 模組快得多。在我的筆電上,它大約三秒就能讀取一 GB 複雜的 CSV 輸入。
與 frawk 相比,CSV 的讀取速度明顯較慢,不過(有點令人意外的是)CSV 的寫入速度則明顯較快。
以下是使用 goawk 與 frawk 以及原生 Python 和 Go 進行的一些簡單讀寫效能測試結果。寫入測試的輸出是一個 1GB、350 萬列、20 個欄位(含引號欄位)的 CSV 檔案;讀取測試則使用同一個檔案作為輸入。時間以秒為單位,顯示在配備 SSD 的 64 位元 Linux 筆電上三次執行中的最佳成績:
| 測試 | goawk | frawk | Python | Go |
|---|---|---|---|---|
| 讀取 1GB CSV | 3.18 | 1.01 | 13.4 | 3.22 |
| 寫入 1GB CSV | 5.64 | 13.0 | 17.0 | 3.24 |
結論
現在的電腦真的很快,所以你只需要用開發用的筆電或一台相對小型的虛擬機器,就能用 GoAWK 處理大多數的 CSV 資料集,甚至是 GB 等級的資料。誰還需要 Hadoop 呢?
所以,下次處理 CSV 的任務時,請試試 GoAWK。AWK 已經 45 歲了,卻仍被廣泛使用,我希望這項功能能讓它在開發者的資料工具組中變得更加實用。
隨機一篇部落格
留言
登入後參與討論