為你的 awk 腳本注入 Groovy

最近我寫(xiě)了一個(gè)使用 Groovy 腳本來(lái)清理我的音樂(lè)文件中的標(biāo)簽的系列。我開(kāi)發(fā)了一個(gè) ??框架??,可以識(shí)別我的音樂(lè)目錄的結(jié)構(gòu),并使用它來(lái)遍歷音樂(lè)文件。在該系列的最后一篇文章中,我從框架中分離出一個(gè)實(shí)用類(lèi),我的腳本可以用它來(lái)處理文件。
這個(gè)獨(dú)立的框架讓我想起了很多 awk 的工作方式。對(duì)于那些不熟悉 awk 的人來(lái)說(shuō),你學(xué)習(xí)下這本電子書(shū):
我從 1984 年開(kāi)始大量使用 awk,當(dāng)時(shí)我們的小公司買(mǎi)了第一臺(tái)“真正的”計(jì)算機(jī),它運(yùn)行的是 System V Unix。對(duì)我來(lái)說(shuō),awk 是非常完美的:它有關(guān)聯(lián)內(nèi)存associative memory——將數(shù)組視為由字符串而不是數(shù)字來(lái)索引的。它內(nèi)置了正則表達(dá)式,似乎專(zhuān)為處理數(shù)據(jù)而生,尤其是在處理數(shù)據(jù)列時(shí),而且結(jié)構(gòu)緊湊,易于學(xué)習(xí)。最后,它非常適合在 Unix 工作流使用,從標(biāo)準(zhǔn)輸入或文件中讀取數(shù)據(jù)并寫(xiě)入到輸出,數(shù)據(jù)不需要經(jīng)過(guò)其他的轉(zhuǎn)換就出現(xiàn)在了輸入流中。
說(shuō) awk 是我日常計(jì)算工具箱中的一個(gè)重要部分一點(diǎn)也不為過(guò)。然而,在我使用 awk 的過(guò)程中,有幾件事讓我感到不滿(mǎn)意。
可能主要的問(wèn)題是 awk 善于處理以分隔字段呈現(xiàn)的數(shù)據(jù),但很奇怪它不善于處理 CSV 文件,因?yàn)?CSV 文件的字段被引號(hào)包圍時(shí)可以嵌入逗號(hào)分隔符。另外,自 awk 發(fā)明以來(lái),正則表達(dá)式已經(jīng)有了很大的發(fā)展,我們需要記住兩套正則表達(dá)式的語(yǔ)法規(guī)則,而這并不利于編寫(xiě)無(wú) bug 的代碼。??一套這樣的規(guī)則已經(jīng)很糟糕了??。
由于 awk 是一門(mén)簡(jiǎn)潔的語(yǔ)言,因此它缺少很多我認(rèn)為有用的東西,比如更豐富的基礎(chǔ)類(lèi)型、結(jié)構(gòu)體、??switch?? 語(yǔ)句等等。
相比之下,Groovy 擁有這些能力:可以使用 ??OpenCSV 庫(kù)???,它很擅長(zhǎng)處理 CSV 文件、Java 正則表達(dá)式和強(qiáng)大的匹配運(yùn)算符、豐富的基礎(chǔ)類(lèi)型、類(lèi)、??switch?? 語(yǔ)句等等。
Groovy 所缺乏的是簡(jiǎn)單的面向管道的概念,即把要處理數(shù)據(jù)作為一個(gè)傳入的流,以及把處理過(guò)的數(shù)據(jù)作為一個(gè)傳出的流。
但我的音樂(lè)目錄處理框架讓我想到,也許我可以創(chuàng)建一個(gè) Groovy 版本的 awk “引擎”。這就是我寫(xiě)這篇文章的目的。
安裝 Java 和 Groovy
Groovy 是基于 Java 的,需要先安裝 Java。最新的、合適的 Java 和 Groovy 版本可能都在你的 Linux 發(fā)行版的軟件庫(kù)中。Groovy 也可以按照 ??Groovy 主頁(yè)??? 上的說(shuō)明進(jìn)行安裝。對(duì)于 Linux 用戶(hù)來(lái)說(shuō),一個(gè)不錯(cuò)的選擇是 ??SDKMan??,它可以用來(lái)獲得多個(gè)版本的 Java、Groovy 和其他許多相關(guān)工具。在這篇文章中,我使用的是 SDK 的版本:
- Java:OpenJDK 11 的 11.0.12 的開(kāi)源版本
- Groovy:3.0.8
使用 Groovy 創(chuàng)建 awk
這里的基本想法是將打開(kāi)一個(gè)或多個(gè)文件進(jìn)行處理、將每行分割成字段、以及提供對(duì)數(shù)據(jù)流的訪問(wèn)等復(fù)雜情況封裝在三個(gè)部分:
- 在處理數(shù)據(jù)之前
- 在處理每行數(shù)據(jù)時(shí)
- 在處理完所有數(shù)據(jù)之后
我并不打算用 Groovy 來(lái)取代 awk。相反,我只是在努力實(shí)現(xiàn)我的典型用例,那就是:
- 使用一個(gè)腳本文件而不是在命令行寫(xiě)代碼
- 處理一個(gè)或多個(gè)輸入文件
- 設(shè)置默認(rèn)的分隔符為?
?|??,并基于這個(gè)分隔符分割所有行 - 使用 OpenCSV 完成分割工作(awk 做不到)
框架類(lèi)
下面是用 Groovy 類(lèi)實(shí)現(xiàn)的 “awk 引擎”:
雖然這看起來(lái)是相當(dāng)多的代碼,但許多行是因?yàn)樘L(zhǎng)換行了(例如,通常你會(huì)合并第 38 行和第 39 行,第 41 行和第 42 行,等等)。讓我們逐行看一下。
第 1 行使用 ??@Grab??? 注解從 ??Maven Central?? 獲取 OpenCSV 庫(kù)的 5.6 本周。不需要 XML。
第 2 行我引入了 OpenCSV 的 ??CSVReader?? 類(lèi)
第 3 行,像 Java 一樣,我聲明了一個(gè) ??public??? 實(shí)用類(lèi) ??AwkEngine??。
第 11-13 行定義了腳本所使用的 Groovy 閉包實(shí)例,作為該類(lèi)的鉤子。像任何 Groovy 類(lèi)一樣,它們“默認(rèn)是 ??public???”,但 Groovy 將這些字段創(chuàng)建為 ??private??,并對(duì)其進(jìn)行外部引用(使用 Groovy 提供的 getter 和 setter 方法)。我將在下面的示例腳本中進(jìn)一步解釋這個(gè)問(wèn)題。
第 14-16 行聲明了 ??private?? 字段 —— 字段分隔符,一個(gè)指示文件第一行是否為標(biāo)題的標(biāo)志,以及一個(gè)文件名的列表。
第 17-31 行定義了三個(gè)構(gòu)造函數(shù)。第一個(gè)接收命令行參數(shù)。第二個(gè)接收字段的分隔符。第三個(gè)接收指示第一行是否為標(biāo)題的標(biāo)志。
第 31-67 行定義了引擎本身,即 ??go()?? 方法。
第 33 行調(diào)用了 ??onBegin()??? 閉包(等同于 awk 的 ??BEGIN {}?? 語(yǔ)句)。
第 34 行初始化流的 ??recordNumber???(等同于 awk 的 ??NR?? 變量)為 0(注意我這里是從 00 而不是 1 開(kāi)始的)。
第 35-65 行使用 ??each??? ??{}?? 來(lái)循環(huán)處理列表中的文件。
第 36 行初始化文件的 ??fileRecordNumber???(等同于 awk 的 ??FNR?? 變量)為 0(從 0 而不是 1 開(kāi)始)。
第 37-64 行獲取一個(gè)文件對(duì)應(yīng)的 ??Reader?? 實(shí)例并處理它。
第 38-39 行獲取一個(gè) ??CSVReader?? 實(shí)例。
第 40 行檢測(cè)第一行是否為標(biāo)題。
如果第一行是標(biāo)題,那么在 41-42 行會(huì)從第一行獲取字段的標(biāo)題名字列表。
第 43-54 行處理其他的行。
第 44-48 行把字段的值復(fù)制到 ??name:value?? 的映射中。
第 49-51 行調(diào)用 ??onEachLine()??? 閉包(等同于 awk 程序 ??BEGIN {}??? 和 ??END {}??? 之間的部分,不同的是,這里不能輸入執(zhí)行條件),傳入的參數(shù)是 ??name:value?? 映射、處理過(guò)的總行數(shù)、文件名和該文件處理過(guò)的行數(shù)。
第 52-53 行是處理過(guò)的總行數(shù)和該文件處理過(guò)的行數(shù)的自增。
如果第一行不是標(biāo)題:
第 56-62 行處理每一行。
第 57-59 調(diào)用 ??onEachLine()?? 閉包,傳入的參數(shù)是字段值的數(shù)組、處理過(guò)的總行數(shù)、文件名和該文件處理過(guò)的行數(shù)。
第 60-61 行是處理過(guò)的總行數(shù)和該文件處理過(guò)的行數(shù)的自增。
第 66 行調(diào)用 ??onEnd()??? 閉包(等同于 awk 的 ??END {}??)。
這就是該框架的內(nèi)容。現(xiàn)在你可以編譯它:
一點(diǎn)注釋?zhuān)?/p>
如果傳入的參數(shù)不是一個(gè)文件,編譯就會(huì)失敗,并出現(xiàn)標(biāo)準(zhǔn)的 Groovy 堆棧跟蹤,看起來(lái)像這樣:
OpenCSV 可能會(huì)返回 ??String[]??? 值,不像 Groovy 中的 ??List??? 值那樣方便(例如,數(shù)組沒(méi)有 ??each {}???)。第 41-42 行將標(biāo)題字段值數(shù)組轉(zhuǎn)換為 list,因此第 57 行的 ??fieldsByNumber?? 可能也應(yīng)該轉(zhuǎn)換為 list。
在腳本中使用這個(gè)框架
下面是一個(gè)使用 ??AwkEngine??? 來(lái)處理 ??/etc/group?? 之類(lèi)由冒號(hào)分隔并沒(méi)有標(biāo)題的文件的簡(jiǎn)單腳本:
第 1 行 調(diào)用的有兩個(gè)參數(shù)的構(gòu)造函數(shù),傳入了參數(shù)列表,并定義冒號(hào)為分隔符。
第 2 行定義一個(gè)腳本級(jí)的變量 ??lineCount???,用來(lái)記錄處理過(guò)的行數(shù)(注意,Groovy 閉包不要求定義在外部的變量為 ??final??)。
第 3-5 行定義 ??onBegin()?? 閉包,在標(biāo)準(zhǔn)輸出中打印出 “in begin” 字符串。
第 6-10 行定義 ??onEachLine()??? 閉包,打印文件名和前 10 行字段,無(wú)論是否為前 10 行,處理過(guò)的總行數(shù) ??lineCount?? 都會(huì)自增。
第 11-14 行定義 ??onEnd()?? 閉包,打印 “in end” 字符串和處理過(guò)的總行數(shù)。
第 15 行運(yùn)行腳本,使用 ??AwkEngine??。
像下面一樣運(yùn)行一下腳本:
當(dāng)然,編譯框架類(lèi)生成的 ??.class??? 文件需要在 classpath 中,這樣才能正常運(yùn)行。通常你可以用 ??jar?? 把這些 class 文件打包起來(lái)。
我非常喜歡 Groovy 對(duì)行為委托的支持,這在其他語(yǔ)言中需要各種詭異的手段。許多年來(lái),Java 需要匿名類(lèi)和相當(dāng)多的額外代碼。Lambda 已經(jīng)在很大程度上解決了這個(gè)問(wèn)題,但它們?nèi)匀徊荒芤闷浞秶獾姆?final 變量。
下面是另一個(gè)更有趣的腳本,它很容易讓人想起我對(duì) awk 的典型使用方式:
第 1 行調(diào)用了三個(gè)函數(shù)的構(gòu)造方法,??true??? 表示這是“真正的 CSV” 文件,第一行為標(biāo)題。由于它是西班牙語(yǔ)的文件,因此它的逗號(hào)表示數(shù)字的??點(diǎn)??,標(biāo)準(zhǔn)的分隔符是分號(hào)。
第 2-4 行定義 ??onBegin()?? 閉包,這里什么也不做。
第 5 行定義一個(gè)(空的)??LinkedHashmap??,鍵是 String 類(lèi)型,值是 Integer 類(lèi)型。數(shù)據(jù)文件來(lái)自于智利最近的人口普查,你要在這個(gè)腳本中計(jì)算出智利每個(gè)地區(qū)的人口數(shù)量。
第 6-11 行處理文件中的行(加上標(biāo)題一共有 180,500 行)—— 請(qǐng)注意在這個(gè)案例中,由于你定義 第 1 行為 CSV 列的標(biāo)題,因此 ??fields??? 參數(shù)會(huì)成為 ??LinkedHashMap<String,String>?? 實(shí)例。
第 7-10 行是 ??regionCount??? 映射計(jì)數(shù)增加,鍵是 ??REGION??? 字段的值,值是 ??PERSONAS?? 字段的值 —— 請(qǐng)注意,與 awk 不同,在 Groovy 中你不能在賦值操作的右邊使用一個(gè)不存在的映射而期望得到空值或零值。
第 12-16 行,打印每個(gè)地區(qū)的人口數(shù)量。
第 17 行運(yùn)行腳本,調(diào)用 ??AwkEngine?? 。
像下面一樣運(yùn)行一下腳本:
以上為全部?jī)?nèi)容。對(duì)于那些喜歡 awk 但又希望得到更多的東西的人,我希望你能喜歡這種 Groovy 的方法。























