天天看點

為你的 awk 腳本注入 Groovy

作者:硬核老王
為你的 awk 腳本注入 Groovy
awk 和 Groovy 相輔相成,可以建立強大、有用的腳本。

最近我寫了一個使用 Groovy 腳本來清理我的音樂檔案中的标簽的系列。我開發了一個 架構,可以識别我的音樂目錄的結構,并使用它來周遊音樂檔案。在該系列的最後一篇文章中,我從架構中分離出一個實用類,我的腳本可以用它來處理檔案。

這個獨立的架構讓我想起了很多 awk 的工作方式。對于那些不熟悉 awk 的人來說,你學習下這本電子書:

《awk 實用指南》

我從 1984 年開始大量使用 awk,當時我們的小公司買了第一台“真正的”計算機,它運作的是 System V Unix。對我來說,awk 是非常完美的:它有關聯記憶體associative memory——将數組視為由字元串而不是數字來索引的。它内置了正規表達式,似乎專為處理資料而生,尤其是在處理資料列時,而且結構緊湊,易于學習。最後,它非常适合在 Unix 工作流使用,從标準輸入或檔案中讀取資料并寫入到輸出,資料不需要經過其他的轉換就出現在了輸入流中。

說 awk 是我日常計算工具箱中的一個重要部分一點也不為過。然而,在我使用 awk 的過程中,有幾件事讓我感到不滿意。

可能主要的問題是 awk 善于處理以分隔字段呈現的資料,但很奇怪它不善于處理 CSV 檔案,因為 CSV 檔案的字段被引号包圍時可以嵌入逗号分隔符。另外,自 awk 發明以來,正規表達式已經有了很大的發展,我們需要記住兩套正規表達式的文法規則,而這并不利于編寫無 bug 的代碼。一套這樣的規則已經很糟糕了。

由于 awk 是一門簡潔的語言,是以它缺少很多我認為有用的東西,比如更豐富的基礎類型、結構體、

switch

語句等等。

相比之下,Groovy 擁有這些能力:可以使用 OpenCSV 庫,它很擅長處理 CSV 檔案、Java 正規表達式和強大的比對運算符、豐富的基礎類型、類、

switch

語句等等。

Groovy 所缺乏的是簡單的面向管道的概念,即把要處理資料作為一個傳入的流,以及把處理過的資料作為一個傳出的流。

但我的音樂目錄處理架構讓我想到,也許我可以建立一個 Groovy 版本的 awk “引擎”。這就是我寫這篇文章的目的。

安裝 Java 和 Groovy

Groovy 是基于 Java 的,需要先安裝 Java。最新的、合适的 Java 和 Groovy 版本可能都在你的 Linux 發行版的軟體庫中。Groovy 也可以按照 Groovy 首頁上的說明進行安裝。對于 Linux 使用者來說,一個不錯的選擇是SDKMan,它可以用來獲得多個版本的 Java、Groovy 和其他許多相關工具。在這篇文章中,我使用的是 SDK 的版本:

  • Java:OpenJDK 11 的 11.0.12 的開源版本
  • Groovy:3.0.8

使用 Groovy 建立 awk

這裡的基本想法是将打開一個或多個檔案進行處理、将每行分割成字段、以及提供對資料流的通路等複雜情況封裝在三個部分:

  • 在處理資料之前
  • 在處理每行資料時
  • 在處理完所有資料之後

我并不打算用 Groovy 來取代 awk。相反,我隻是在努力實作我的典型用例,那就是:

  • 使用一個腳本檔案而不是在指令行寫代碼
  • 處理一個或多個輸入檔案
  • 設定預設的分隔符為

    |

    ,并基于這個分隔符分割所有行
  • 使用 OpenCSV 完成分割工作(awk 做不到)

架構類

下面是用 Groovy 類實作的 “awk 引擎”:

@Grab('com.opencsvpencsv:5.6') import com.opencsv.CSVReader public class AwkEngine { // With admiration and respect for // Alfred Aho // Peter Weinberger // Brian Kernighan // Thank you for the enormous value // brought my job by the awk // programming language Closure onBegin Closure onEachLine Closure onEnd private String fieldSeparator private boolean isFirstLineHeader private ArrayList           

雖然這看起來是相當多的代碼,但許多行是因為太長換行了(例如,通常你會合并第 38 行和第 39 行,第 41 行和第 42 行,等等)。讓我們逐行看一下。

第 1 行使用

@Grab

注解從Maven Central擷取 OpenCSV 庫的 5.6 本周。不需要 XML。

第 2 行我引入了 OpenCSV 的

CSVReader

第 3 行,像 Java 一樣,我聲明了一個

public

實用類

AwkEngine

第 11-13 行定義了腳本所使用的 Groovy 閉包執行個體,作為該類的鈎子。像任何 Groovy 類一樣,它們“預設是

public

”,但 Groovy 将這些字段建立為

private

,并對其進行外部引用(使用 Groovy 提供的 getter 和 setter 方法)。我将在下面的示例腳本中進一步解釋這個問題。

第 14-16 行聲明了

private

字段 —— 字段分隔符,一個訓示檔案第一行是否為标題的标志,以及一個檔案名的清單。

第 17-31 行定義了三個構造函數。第一個接收指令行參數。第二個接收字段的分隔符。第三個接收訓示第一行是否為标題的标志。

第 31-67 行定義了引擎本身,即

go

方法。

第 33 行調用了

onBegin

閉包(等同于 awk 的

BEGIN {}

語句)。

第 34 行初始化流的

recordNumber

(等同于 awk 的

NR

變量)為 0(注意我這裡是從 00 而不是 1 開始的)。

第 35-65 行使用

each

{}

來循環處理清單中的檔案。

第 36 行初始化檔案的

fileRecordNumber

(等同于 awk 的

FNR

變量)為 0(從 0 而不是 1 開始)。

第 37-64 行擷取一個檔案對應的

Reader

執行個體并處理它。

第 38-39 行擷取一個

CSVReader

執行個體。

第 40 行檢測第一行是否為标題。

如果第一行是标題,那麼在 41-42 行會從第一行擷取字段的标題名字清單。

第 43-54 行處理其他的行。

第 44-48 行把字段的值複制到

name:value

的映射中。

第 49-51 行調用

onEachLine

閉包(等同于 awk 程式

BEGIN {}

END {}

之間的部分,不同的是,這裡不能輸入執行條件),傳入的參數是

name:value

映射、處理過的總行數、檔案名和該檔案處理過的行數。

第 52-53 行是處理過的總行數和該檔案處理過的行數的自增。

如果第一行不是标題:

第 56-62 行處理每一行。

第 57-59 調用

onEachLine

閉包,傳入的參數是字段值的數組、處理過的總行數、檔案名和該檔案處理過的行數。

第 60-61 行是處理過的總行數和該檔案處理過的行數的自增。

第 66 行調用

onEnd

閉包(等同于 awk 的

END {}

)。

這就是該架構的内容。現在你可以編譯它:

$ groovyc AwkEngine.groovy
           

一點注釋:

如果傳入的參數不是一個檔案,編譯就會失敗,并出現标準的 Groovy 堆棧跟蹤,看起來像這樣:

Caught: java.io.FileNotFoundException: not-a-file (No such file or directory)
java.io.FileNotFoundException: not-a-file (No such file or directory)
at AwkEngine$_go_closure1.doCall(AwkEngine.groovy:46)
           

OpenCSV 可能會傳回

String

值,不像 Groovy 中的

List

值那樣友善(例如,數組沒有

each {}

)。第 41-42 行将标題字段值數組轉換為 list,是以第 57 行的

fieldsByNumber

可能也應該轉換為 list。

在腳本中使用這個架構

下面是一個使用

AwkEngine

來處理

/etc/group

之類由冒号分隔并沒有标題的檔案的簡單腳本:

def ae = new AwkEngine(args, ':')
int lineCount = 0
ae.onBegin = {
    println “in begin”
}
ae.onEachLine = { fields, recordNumber, fileName, fileRecordNumber ->
    if (lineCount            

第 1 行 調用的有兩個參數的構造函數,傳入了參數清單,并定義冒号為分隔符。

第 2 行定義一個腳本級的變量

lineCount

,用來記錄處理過的行數(注意,Groovy 閉包不要求定義在外部的變量為

final

)。

第 3-5 行定義

onBegin

閉包,在标準輸出中列印出 “in begin” 字元串。

第 6-10 行定義

onEachLine

閉包,列印檔案名和前 10 行字段,無論是否為前 10 行,處理過的總行數

lineCount

都會自增。

第 11-14 行定義

onEnd

閉包,列印 “in end” 字元串和處理過的總行數。

第 15 行運作腳本,使用

AwkEngine

像下面一樣運作一下腳本:

$ groovy Test1Awk.groovy /etc/group
in begin
fileName /etc/group fields [root, x, 0, ]
fileName /etc/group fields [daemon, x, 1, ]
fileName /etc/group fields [bin, x, 2, ]
fileName /etc/group fields [sys, x, 3, ]
fileName /etc/group fields [adm, x, 4, syslog,clh]
fileName /etc/group fields [tty, x, 5, ]
fileName /etc/group fields [disk, x, 6, ]
fileName /etc/group fields [lp, x, 7, ]
fileName /etc/group fields [mail, x, 8, ]
fileName /etc/group fields [news, x, 9, ]
in end
78 line(s) read
$
           

當然,編譯架構類生成的

.class

檔案需要在 classpath 中,這樣才能正常運作。通常你可以用

jar

把這些 class 檔案打包起來。

我非常喜歡 Groovy 對行為委托的支援,這在其他語言中需要各種詭異的手段。許多年來,Java 需要匿名類和相當多的額外代碼。Lambda 已經在很大程度上解決了這個問題,但它們仍然不能引用其範圍之外的非 final 變量。

下面是另一個更有趣的腳本,它很容易讓人想起我對 awk 的典型使用方式:

def ae = new AwkEngine(args, ';', true)
ae.onBegin = {
    // nothing to do here
}
def regionCount = [:]
    ae.onEachLine = { fields, recordNumber, fileName, fileRecordNumber ->
        regionCount[fields.REGION] =
            (regionCount.containsKey(fields.REGION) ?
                regionCount[fields.REGION] : 0) +
            (fields.PERSONAS as Integer)
}
ae.onEnd = {
    regionCount.each { region, population ->
        println “Region $region population $population”
    }
}
ae.go
           

第 1 行調用了三個函數的構造方法,

true

表示這是“真正的 CSV” 檔案,第一行為标題。由于它是西班牙語的檔案,是以它的逗号表示數字的

,标準的分隔符是分号。

第 2-4 行定義

onBegin

閉包,這裡什麼也不做。

第 5 行定義一個(空的)

LinkedHashmap

,鍵是 String 類型,值是 Integer 類型。資料檔案來自于智利最近的人口普查,你要在這個腳本中計算出智利每個地區的人口數量。

第 6-11 行處理檔案中的行(加上标題一共有 180,500 行)—— 請注意在這個案例中,由于你定義 第 1 行為 CSV 列的标題,是以

fields

參數會成為

LinkedHashMap

執行個體。

第 7-10 行是

regionCount

映射計數增加,鍵是

REGION

字段的值,值是

PERSONAS

字段的值 —— 請注意,與 awk 不同,在 Groovy 中你不能在指派操作的右邊使用一個不存在的映射而期望得到空值或零值。

第 12-16 行,列印每個地區的人口數量。

第 17 行運作腳本,調用

AwkEngine

像下面一樣運作一下腳本:

$ groovy Test2Awk.groovy ~/Downloads/Censo2017/ManzanaEntidad_CSV/Censo*csv
Region 1 population 330558
Region 2 population 607534
Region 3 population 286168
Region 4 population 757586
Region 5 population 1815902
Region 6 population 914555
Region 7 population 1044950
Region 8 population 1556805
Region 16 population 480609
Region 9 population 957224
Region 10 population 828708
Region 11 population 103158
Region 12 population 166533
Region 13 population 7112808
Region 14 population 384837
Region 15 population 226068
$
           

以上為全部内容。對于那些喜歡 awk 但又希望得到更多的東西的人,我希望你能喜歡這種 Groovy 的方法。

via: https://opensource.com/article/22/9/awk-groovy

作者:Chris Hermansen選題:lkxed譯者:lxbwolf校對:wxy

本文由 LCTT原創編譯,Linux中國榮譽推出