awk と Groovy は強力で便利なスクリプトを作成するために互いに補完し合っています。
このスタンドアローン・フレームワークは、awkがどのように動くかをよく思い出させてくれます。awkをよく知らない人は、この電子書籍で勉強してください:
私の小さな会社が最初の「本物の」コンピュータを購入し、System V Unixを走らせるようになった1984年以来、私はawkをよく使っています。配列を数値ではなく文字列でインデックスされたものとして扱うことができます。データを扱うために作られたような正規表現が組み込まれていて、特にデータの列を扱うときには、コンパクトで学びやすい。最後に、標準入力やファイルからデータを読み込んで出力に書き出すようなUnixワークフローでの使用に適しています。
awk は私の日常的なコンピューティングツールボックスの重要な一部と言っても過言ではありません。しかし、awk を使っていて困ったことがいくつかあります。
おそらく主な問題は、awk は区切りフィールドで表示されるデータを扱うのは得意なのですが、フィールドを引用符で囲むとカンマ区切りが埋め込まれてしまう CSV ファイルを扱うのが妙に苦手だということでしょう。また、正規表現は awk の発明以来大きく進化しており、正規表現の構文規則を 2 セット覚える必要があるのは、バグのないコードを書くためには不都合です。そのような規則が 1 セットあるだけでも十分悪いことです。
awk は簡潔な言語なので、豊富な基本型や構造体、switch 文など、私が便利だと思うものの多くが欠けています。
対照的に、Groovyには、CSVファイルを扱うのが得意な OpenCSVライブラリへのアクセス、Java正規表現と強力なマッチング演算子、豊富な基本型セット、クラス、switch文など、これらすべての機能があります。
Groovyに欠けているのは、処理されるデータを受信ストリームとして、処理されたデータを送信ストリームとしてという単純なパイプライン指向の概念です。
JavaとGroovyのインストール
JavaとGroovyの最新の適切なバージョンは、Linuxディストリビューションのリポジトリにあるかもしれません。Groovyは、 OpenCSV 指示に従ってインストールすることもできます。Groovyは、Groovyホームページの指示に従ってインストールすることもできます。Linuxユーザーにとって良い選択肢は、複数のバージョンのJava、Groovy、および他の多くの関連ツールを入手するために使用できる SDKManです。この記事では、SDKバージョンを使用します:
- Groovy: 3.0.8
Groovyでawkを作る
ここでの基本的な考え方は、処理のために1つまたは複数のファイルを開き、各行をフィールドに分割し、データストリームへのアクセスを提供するという複雑さを、3つの部分にカプセル化することです:
- データを扱うとき
- データの各行を処理するとき
- すべてのデータを処理した後
- コマンドラインでコードを書く代わりにスクリプトファイルを使用します。
- 1つ以上の入力ファイルの処理
- デフォルトの区切り文字を , に設定し、この区切り文字に基づいてすべての行を分割します。
- OpenCSVを使ったセグメンテーションの完了
フレームワーククラス
Groovyクラスで実装した "awkエンジン "を示します:
@Grab('com.opencsv:opencsv:5.6')import com.opencsv.CSVReaderpublic 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 languageClosure onBeginClosure onEachLineClosure onEndprivate String fieldSeparatorprivate boolean isFirstLineHeaderprivate ArrayList<String> fileNameListpublic AwkEngine(args) {this.fileNameList = argsthis.fieldSeparator = "|"this.isFirstLineHeader = falsepublic AwkEngine(args, fieldSeparator) {this.fileNameList = argsthis.fieldSeparator = fieldSeparatorthis.isFirstLineHeader = falsepublic AwkEngine(args, fieldSeparator, isFirstLineHeader) {this.fileNameList = argsthis.fieldSeparator = fieldSeparatorthis.isFirstLineHeader = isFirstLineHeaderpublic void go() {this.onBegin()int recordNumber = 0fileNameList.each { fileName ->int fileRecordNumber = 0new File(fileName).withReader { reader ->def csvReader = new CSVReader(reader,this.fieldSeparator.charAt(0))if (isFirstLineHeader) {def csvFieldNames = csvReader.readNext() asArrayList<String>csvReader.each { fieldsByNumber ->def fieldsByName = csvFieldNames.withIndex().collectEntries { name, index ->[name, fieldsByNumber[index]]this.onEachLine(fieldsByName,recordNumber, fileName,fileRecordNumber)recordNumber++fileRecordNumber++} else {csvReader.each { fieldsByNumber ->this.onEachLine(fieldsByNumber,recordNumber, fileName,fileRecordNumber)recordNumber++fileRecordNumber++this.onEnd()
かなりのコード量に見えますが、長すぎるため改行している行が多いのです。一行ずつ見ていきましょう。
1行目は@Grabアノテーションを使用して、 Groovy 5.6週目のOpenCSVライブラリをフェッチします。XMLは必要ありません。
2行目でOpenCSVの CSVReader クラスを紹介しています。
3行目では、Javaと同じように、パブリック・ユーティリティ・クラス AwkEngine宣言しています。
11~13行目では、スクリプトがクラスのフックとして使用するGroovyクロージャのインスタンスを定義しています。他のGroovyクラスと同様に、これらは「デフォルトでpublic」ですが、Groovyはこれらのフィールドをprivateとして作成し、外部から参照します。これについては、以下のサンプルスクリプトで詳しく説明します。
14-16行目では、プライベート・フィールド、フィールド・セパレーター、ファイルの最初の行がタイトルであるかどうかを示すフラグ、ファイル名のリストを宣言しています。
17~31行目では3つのコンストラクターを定義しています。1つ目はコマンドライン引数を受け取ります。2番目はフィールド区切り文字を受け取ります。3番目は最初の行が見出しかどうかを示すフラグを受け取ります。
31行目から67行目では、エンジンそのもの、go()メソッドを定義しています。
33行目は onBegin() クロージャを呼び出します。
34行目はストリームの recordNumber0に初期化します。
35~65行目では、each {}を使ってリスト内のファイルをループしています。
36行目はファイルの fileRecordNumber0に初期化します。
37~64行目でファイルに対応する Reader インスタンスを取得し、処理します。
38-39行目で CSVReader インスタンスを取得します。
40行目は最初の行が見出しかどうかを検出します。
最初の行がタイトルの場合、フィールドのタイトル名のリストは最初の行から41-42行目で取得されます。
43~54行目は他の行を処理します。
44~48行目は、フィールドの値を name:value マッピングにコピーします。
49~51行目でonEachLine()クロージャーを呼び出し、name:valueのマッピング、処理された行の総数、ファイル名、そのファイルで処理された行数を渡します。
52行目から53行目は、そのファイルの総処理行数と処理行数のインクリメントです。
最初の行がタイトルでない場合
56行目から62行目までは、それぞれの行を扱います。
onEachLine() 渡されるパラメータは、フィールド値の配列、処理された行数の合計、ファイル名、ファイル内で処理された行数です。
60行目から61行目は、処理された行数の合計と、そのファイルの処理行数の増分です。
66行目は onEnd() クロージャを呼び出しています。
これがフレームワークです。これでコンパイルできます:
$ 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値ほど便利ではありません。41~42行目でヘッダーフィールド値の配列をリストに変換しているので、57行目のfieldsByNumberもリストに変換する必要があるでしょう。
スクリプトにこのフレームワークを使用します。
コロンで区切られ、タイトルのない /etc/group ようなファイルを処理するAwkEngineを使った簡単なスクリプトを紹介します:
def ae = new AwkEngine(args, ':')int lineCount = 0ae.onBegin = {println "in begin”ae.onEachLine = { fields, recordNumber, fileName, fileRecordNumber ->if (lineCount < 10)println 「ファイル名$fileName fields $fields”lineCount++ae.onEnd = {println 「最後に”println $lineCount line(s) read”ae.go()
行目 2つの引数で呼び出されるコンストラクタには、引数のリストが渡され、セパレータとしてコロンが定義されます。
2行目では、処理された行数を記録するスクリプトレベル変数 lineCount を定義しています。
3-5行目は onBegin() クロージャを定義し、"in begin "という文字列を標準出力に表示します。
6~10行目でonEachLine()クロージャを定義し、ファイル名と最初の10行のフィールドを表示し、最初の10行かどうかに関係なく、処理された行の総数をインクリメントします。
11~14行目でonEnd()クロージャを定義し、"in end "文字列と処理された行の総数を表示します。
15行目は AwkEngineスクリプトを実行します。
以下のようなスクリプトを実行してください:
$ groovy Test1Awk.groovy /etc/groupin beginfileName /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, ]78 line(s) read
もちろん、フレームワークのクラスをコンパイルすることで生成された.classファイルは、正しく動作するためにクラスパスに存在する必要があります。通常はこれらのクラスファイルをjarにパッケージできます。
私はGroovyの振る舞いの委譲のサポートがとても好きです。ラムダはこの問題を解決するために長い道のりを歩んできましたが、それでもスコープ外の非終端変数を参照することはできません。
もうひとつ、もっと面白いスクリプトを紹介しましょう。これは私が awk を使うときの典型的な方法を彷彿とさせるものです:
def ae = new AwkEngine(args, ';', true)ae.onBegin = {// nothing to do heredef 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 population $population”ae.go()
最初の行は3つの関数のコンストラクタを呼び出し、これが "本物のCSV "ファイルであることを示すtrue、そして最初の行はタイトルです。スペイン語のファイルなので、数字のドットはカンマで、標準のセパレータはセミコロンです。
2-4行目では onBegin() クロージャを定義していますが、ここでは何もしません。
5行目では、String型のキーとInteger型の値を持つLinkedHashmapを定義しています。データファイルは最新のチリの国勢調査によるもので、このスクリプトではチリの各地域の人口を計算します。
7行目から10行目はregionCountマッピングのカウントアップで、keyはREGIONフィールドの値、valueはPERSONASフィールドの値です。awkとは異なり、Groovyでは代入操作の右辺に存在しないマッピングを使用することはできず、NULLやゼロの値を得ることはできないことに注意してください。
12行目から16行目にかけて、各地域の人数を印刷してください。
17行目はスクリプトを実行し、 AwkEngine.
以下のようなスクリプトを実行してください:
$ groovy Test2Awk.groovy ~/Downloads/Censo2017/ManzanaEntidad_CSV/Censo*csvRegion 1 populationRegion 2 populationRegion 3 populationRegion 4 populationRegion 5 populationRegion 6 populationRegion 7 populationRegion 8 populationRegion 16 populationRegion 9 populationRegion 10 populationRegion 11 populationRegion 12 populationRegion 13 populationRegion 14 populationRegion 15 population
以上です。awkは好きだけどもっと何か欲しいという方には、このGroovyのアプローチを楽しんでいただければと思います。





