Modernizing AWK, a 45-year old language, by adding CSV support

Ben Hoyt

透過加入 CSV 支援,讓 45 年歷史的 AWK 現代化

原文由 Ben Hoyt 發布,訂閱此部落格

我最近為 GoAWK——我開發的 POSIX 相容 AWK 直譯器——加入了對 CSV 檔案的完整支援,我認為這項功能在這個資料至上的時代,能讓 AWK 對開發者與資料分析師變得更加實用。

不管是要產生試算表的輸入資料、分析來自公開資料來源的資料,還是撰寫處理「大數據」的腳本,CSVTSV 檔案在今天都無處不在。至於它是不是最好的格式還有待商榷,但就像有人說的,資料格式只有兩種:一種是大家抱怨的,一種是沒人用的。

自從 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 輸入模式:換句話說,就是忽略標準的欄位與記錄分隔符(FSRS),改用 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 區塊中設定 INPUTMODEOUTPUTMODE 特殊變數。

由於各種原因,我無法在 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 的寫入速度則明顯較快。

以下是使用 goawkfrawk 以及原生 Python 和 Go 進行的一些簡單讀寫效能測試結果。寫入測試的輸出是一個 1GB、350 萬列、20 個欄位(含引號欄位)的 CSV 檔案;讀取測試則使用同一個檔案作為輸入。時間以秒為單位,顯示在配備 SSD 的 64 位元 Linux 筆電上三次執行中的最佳成績:

測試goawkfrawkPythonGo
讀取 1GB CSV3.181.0113.43.22
寫入 1GB CSV5.6413.017.03.24

結論

現在的電腦真的很快,所以你只需要用開發用的筆電或一台相對小型的虛擬機器,就能用 GoAWK 處理大多數的 CSV 資料集,甚至是 GB 等級的資料。誰還需要 Hadoop 呢?

所以,下次處理 CSV 的任務時,請試試 GoAWK。AWK 已經 45 歲了,卻仍被廣泛使用,我希望這項功能能讓它在開發者的資料工具組中變得更加實用。

本文章由 muse-spark-1.2-contributor 進行翻譯

留言