当前位置: 首页 > 科技观察

将Groovy注入到您的awk脚本中

时间:2023-03-18 14:16:00 科技观察

我最近写了一个系列文章,介绍如何使用Groovy脚本清理我的音乐文件中的标签。我开发了一个框架来识别我的音乐目录的结构并使用它来迭代音乐文件。在本系列的最后一篇文章中,我从我的脚本可以用来处理文件的框架中分离出一个实用程序类。这个独立的框架让我想起了awk的工作原理。对于那些不熟悉awk的人,这是电子书:《awk 实用指南》自1984年以来,我一直在大量使用awk,当时我们的小公司购买了第一台“真正的”计算机,它运行SystemVUnix。对我来说,awk是完美的:它具有关联内存——它将数组视为由字符串而不是数字索引。它内置了正则表达式,似乎是为处理数据而设计的,尤其是在处理数据列时,而且结构紧凑,易于学习。最后,它非常适合在Unix工作流中使用,其中数据从标准输入或文件读取并写入输出,其中数据出现在输入流中而无需额外转换。可以毫不夸张地说awk是我日常计算工具箱的重要组成部分。然而,在我使用awk的过程中,有几件事并不令我满意。可能主要的问题是awk擅长处理以分隔字段形式呈现的数据,但奇怪的是它不擅长处理CSV文件,因为当CSV文件的字段被引号包围时,CSV文件可以嵌入逗号分隔符。还有,自从awk发明以来,正则表达式已经进化了很多,我们需要记住两套正则表达式的语法规则,这不利于写出没有bug的代码。一套这样的规则已经很糟糕了。由于awk是一门简洁的语言,它缺少很多我觉得有用的东西,比如更丰富的基本类型、structs、switch语句等。相比之下,Groovy有这些能力:可以使用OpenCSV库,它非常擅长处理CSV文件,Java正则表达式和强大的匹配运算符,丰富的基本类型,类,switch语句等等。Groovy缺少的是一个简单的面向管道的概念,即将要处理的数据视为传入流,并将处理后的数据视为传出流。但是我的音乐目录处理框架让我想到也许我可以创建awk“引擎”的Groovy版本。这就是我写这篇文章的原因。安装Java和GroovyGroovy是基于Java的,需要先安装Java。最新、合适的Java和Groovy版本可能在您的Linux发行版的软件存储库中。也可以按照Groovy主页上的说明安装Groovy。Linux用户的一个不错选择是SDKMan,它可用于获取多个版本的Java、Groovy和许多其他相关工具。本文中,我使用的SDK版本:Java:OpenJDK11's11.0.12开源版本Groovy:3.0.8使用Groovy创建awk这里的基本思路是打开一个或多个文件进行处理,拆分每个文件的复杂度将行放入字段并提供对数据流的访问被封装在三个部分中:在处理数据之前在处理每一行时在处理所有数据之后我不会使用Groovy来替代awk。相反,我只是试图实现我的典型用例,即:使用脚本文件而不是在命令行上编写代码来处理一个或多个输入文件将默认分隔符设置为||,并基于此delimiter拆分所有行使用OpenCSV进行拆分(awk做不到)框架类这是在Groovy类中实现的“awk引擎”:@Grab('com.opencsv:opencsv:5.6')importcom.opencsv。CSVReaderpublicclassAwkEngine{//对//AlfredAho//PeterWeinberger//BrianKernighan//感谢你的巨大价值//awk给我带来了工作//编程语言ClosureonBeginClosureonEachLineClosureonEndprivateStringfieldSeparatorprivatebooleanisFirstLineHeaderprivateArrayListfileNameListpublicAwkEngine(args){this.fileNameList=argsthis.fieldSeparator="|"}this.isFirstLineHeader=false}publicAwkEngine(args,fieldSeparator){this.fileNameList=argsthis.fieldSeparator=fieldSeparatorthis.isFirstLineHeader=false}publicAwkEngine(args,fieldSeparator,isFirstLineHeader){this.fileNameList=argsthis.fieldSeparator=fieldSeparatorthis.isFirstLineHeader=isFirstLineHeader}publicvoidgo(){this.onBegin()intrecordNumber=0fileNameList.each{fileName->intfileRecordNumber=0newFile(fileName).withReader{reader->defcsvReader=newCSVReader(reader,this.fieldSeparator.charAt(0))if(isFirstLineHeader){defcsvFieldNames=csvReader.readNext()asArrayListcsvReader.each{fieldsByNumber->deffieldsByName=csv字段名。与指数()。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++fileRecordNumberEn++}}onEachLine(fieldsByNumber,recordNumber,fileName,fileRecordNumber)recordNumber++fileRecordNumberEn++}onEachLine}}而这。代码很多,但是很多行因为太长而断掉了(例如,通常你会把第38行和第39行、第41行和第42行等合并起来)让我们逐行看一下。第1行使用@Grab注释从MavenCentral获取本周的5.6OpenCSV库。不需要XML。第2行我导入OpenCSV的CSVReader类第3行,像Java一样,我声明一个公共实用程序类AwkEngine。第11-13行定义脚本使用的GroovyClosure实例作为此类的钩子。与任何Groovy类一样,它们是“默认公共的”,但Groovy将这些字段创建为私有的并在外部引用它们(使用Groovy提供的getter和setter方法)。我将在下面的示例脚本中进一步解释这一点。第14-16行声明了私有字段-字段分隔符、指示文件第一行是否为标题的标志以及文件名列表。第17-31行定义了三个构造函数。第一个接受命令行参数。第二个接收字段的分隔符。第三个接收一个标志,指示第一行是否是标题。第31-67行定义了引擎本身,即go()方法。第33行调用onBegin()闭包(相当于awk的BEGIN{}语句)。第34行将流的recordNumber(相当于awk的NR变量)初始化为0(注意我从00而不是1开始)。第35-65行使用每个{}循环遍历列表中的文件。第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{}之间的部分,不同的是不能输入Execution条件),传入的参数是name:value映射,处理的总行数,文件名和文件处理的行数。第52-53行是处理的总行数和此文件中处理的行数的自动增量。如果第一行不是标题:第56-62行处理每一行。第57-59行调用onEachLine()闭包,传入字段值数组、已处理的总行数、文件名以及该文件中已处理的行数。第60-61行是处理的总行数和此文件中处理的行数的自动增量。第66行调用onEnd()闭包(相当于awk的END{})。这就是这个框架的意义所在。现在您可以编译它了:$groovycAwkEngine.groovy一点注意事项:如果传入的参数不是文件,编译将失败,标准Groovy堆栈跟踪如下所示:Caught:java.io.FileNotFoundException:not-a-file(Nosuchfileordirectory)java.io.FileNotFoundException:not-a-file(Nosuchfileordirectory)atAwkEngine$_go_closure1.doCall(AwkEngine.groovy:46)OpenCSV可能会返回String[]值,而不是和Groovy中的List值一样方便(例如,数组没有每个{})。第41-42行将头字段值数组转换为列表,因此第57行的fieldsByNumber应该也转换为列表。在脚本中使用这个框架下面是一个简单的脚本,它使用AwkEngine来处理冒号分隔和无标题的文件,例如/etc/group:defae=newAwkEngine(args,':')intlineCount=0ae.onBegin={println“inbegin”}ae.onEachLine={fields,recordNumber,fileName,fileRecordNumber->if(lineCount<10)println“fileName$fileNamefields$fields”lineCount++}ae.onEnd={println"inend"println"$lineCountline(s)read"}ae.go()第一行调用构造函数,有两个参数,传入参数列表,定义冒号作为分隔符。第2行定义了一个脚本级变量lineCount来记录处理的行数(注意Groovy闭包不要求外部变量是final)。第3-5行定义了onBegin()闭包,它将字符串“inbegin”打印到标准输出。第6-10行定义了onEachLine()闭包,打印文件名和前10行,不管是否是前10行,总的处理行数lineCount都会递增。第11-14行定义了onEnd()闭包,它打印“inend”字符串和处理的总行数。第15行使用AwkEngine运行脚本。像这样运行脚本:$groovyTest1Awk.groovy/etc/groupinbeginfileName/etc/groupfields[root,x,0,]fileName/etc/groupfields[daemon,x,1,]fileName/etc/groupfields[bin,x,2,]fileName/etc/groupfields[sys,x,3,]fileName/etc/groupfields[adm,x,4,syslog,clh]fileName/etc/groupfields[tty,x,5,]fileName/etc/groupfields[disk,x,6,]fileName/etc/groupfields[lp,x,7,]fileName/etc/groupfields[mail,x,8,]fileName/etc/groupfields[news,x,9,]inend78line(s)read$当然,框架类编译生成的.class文件需要在classpath中,这样才能正常运行。通常你可以将这些class文件用jar打包。我真的很喜欢Groovy对行为委托的支持,这需要其他语言中的各种奇怪技巧。多年来,Java需要匿名类和相当多的额外代码。Lambdas在很大程度上解决了这个问题,但是它们仍然不能引用作用域之外的非final变量。这是另一个更有趣的脚本,它让人想起我对awk的典型使用:defae=newAwkEngine(args,';',true)ae.onBegin={//nothingtodohere}defregionCount=[:]ae.onEachLine={fields,recordNumber,fileName,fileRecordNumber->regionCount[fields.REGION]=(regionCount.containsKey(fields.REGION)?regionCount[fields.REGION]:0)+(fields.PERSONASasInteger)}ae.onEnd={regionCount.each{region,population->println“Region$regionpopulation$population”}}ae.go()第1行调用三个函数的构造函数,true表示这是一个“真正的CSV”文件,第一行是标题。由于它是西班牙语文件,因此它的逗号代表数字的点,标准分隔符是分号。第2-4行定义了onBegin()闭包,它在这里什么也不做。第5行定义了一个(空的)LinkedHashmap,其键类型为String,值类型为Integer。数据文件来自最近的智利人口普查,在此脚本中,您将计算智利每个地区的人口。第6-11行处理文件中的行(180,500行,包括标题)——请注意,在这种情况下,由于您将第1行定义为CSV列的标题,因此字段参数变为LinkedHashMap实例。第7-10行是regionCount地图计数增量,键是REGION字段的值,值是PERSONAS字段的值——注意,与awk不同,在Groovy中你不能使用非-赋值右侧存在映射,期望为空或零。第12-16行,打印每个地区的人口。第17行运行脚本,呼叫awkengine。7区人口1044950地区8区人口1556805区16区人口7112808区9区人口957224区10区人口828708区11区人口103158区12区人口166533区13区人口7112808区14区人口957224区10区人口828708区11区人口103158区12区人口166533区13区人口7112808以上所有内容$60区人口384138区2区2$对于那些喜欢awk但想要更多东西的人,我希望你喜欢这种Groovy方法。