AWK入门(Linux中使用)

前言:

小编在学习生信的时候,awk总是不时出现在命令行操作当中,忍无可忍之后决心系统学习awk语法,结果发现awk其实是自成一套编程体系的(天,第一眼确实震惊我!),虽然有很多地方是有借鉴于C的,但是仍然有很多其独特语法所在之处。

本篇教程呢也仅仅是用来服务于生信学习的目的的,虽然对awk的各个方面都有介绍但是本着“深入理解但是够用就行”的学习理念(不喜勿喷),小编尽可能表述清楚我对awk的理解,对于很细节的option也不做深究(因为很可能用不上,要用再查也来得及),希望可以扫除生信应用场景当中的awk学习和理解障碍啦~

Workflow awk的工作流方式

在Linux当中,awk一般是作为一个文本处理工具出现的,所以一般来说给awk的输入是标准输入或者是以文件格式出现的,那么awk的执行的主体部分(后面有介绍),awk是一行一行读取然后操作的,这是awk作为文本处理工具的特点之一。

但是为什么说awk执行的主体部分呢?因为awk的实际的工作流方式是如下的:

+-----------------------+
| 启动 AWK 程序         |
+-----------------------+
           ↓
+-----------------------+
| 执行 BEGIN 块代码     |
| (可选,无则跳过)    |
+-----------------------+
           ↓
+-----------------------+
| 读取输入流的一行      |
+-----------------------+
           ↓
+-----------------------+
| 对该行执行 AWK 指令   |
| (匹配模式、执行动作)|
+-----------------------+
           ↓
+-----------------------+
| 是否到达文件末尾?    |
| ├─ 是 → 进入 END 块  |
| └─ 否 → 返回“读取行”  |
+-----------------------+
           ↓
+-----------------------+
| 执行 END 块代码       |
| (可选,无则跳过)    |
+-----------------------+
           ↓
| 程序结束              |
+-----------------------+

在开始会有一个BEGIN部分的程序,此程序只会在进入awk程序的开始被执行一次,同样的在最后也有一个END部分的程序,此部分程序只会在awk程序结束的时候执行一次,但是请注意这三个部分并不是必须同时存在的,甚至只有有其中任意一部分就可以正常执行。(在后面的例子可以看到很多时候甚至都没有主体部分)

那么小编再把这个工作流简单叙述一遍(可以对照plaintext来理解):首先会先执行BEGIN部分的代码,等到这部分代码结束之后呢,进入到主体部分的代码,这一部分的代码会逐行读取输入,读一行操作一行,然后再读下一行,直到读到最后一行(或者遇到特定指令如nextfile(后面介绍))进入到END部分程序,此部分执行结束之后awk的程序也宣告结束。但是注意一点:对于主体部分的代码可以由多个部分组成,后面提到的/pattern/{commands}只是基本单位而已哦,对于每一行输入流会依次从前到后执行每一块代码。

对于BEGIN,END,都遵循同一个格式:BEGIN{awk-commands}或END{awk-commands},{}里面的是awk的命令行,但是对于文本操作的主阵地BODY这一部分有一些不同(这里不需要BODY标识哦),/pattern/{awk-commands} 前面的pattern可以理解为条件(一般以正则表达式的形式出现),而{}里面的如同BEGIN和END部分是一样的均是操作(awk命令行),只有满足pattern的匹配格式,后面的commands才会在这一行执行

下面是一个简单的例子,具体讲解工作流

下面是一份示例文件(以空格分隔字段(简单理解就是列))

Alice Math 90
Bob English 85
Charlie Math 92
David English 78
Ella Math 88
awk '
BEGIN {
    total = 0    # 总分
    count = 0    # 科目为 Math 的行数
    FS = " "     # 字段分隔符设为空格
} #其实这一块并没有很大必要,因为awk当中如果没有定义total等变量直接使用的话默认值就是0,而且FS字段分隔符(输入文件数据的)也就是space

# Body 部分(带 pattern):匹配科目为 Math 的行
/Math/ {
    print "匹配到 Math 科目行:" $0  # 输出当前行内容
    total += $3                     # 累加分数(这里的$3表示的是第三个字段)
    count++                         # 计数 +1
}

END {
        avg = total / count
        print "Math 科目共", count, "人"
        print "平均分:", avg

}
' scores.txt #这里是输入数据

(格式为awk ‘BEGIN{commands} /pattern/ {commands} END{commands}’)

这里补充一下:除了我们上面介绍的这种格式,其实可以将‘ ’内的写成一个文件,以下面这种格式运行(当然了,在Linux当中一般是不需要这么复杂的,所以这里就不着重说了)

(格式为awk -f command.awk marks.txt)(其中command.awk就是上面编写的程序)

Built-in Variables 内置变量

我们先运行下面这一个程序然后逐一介绍里面重要的内置变量

awk --dump-variables ''
cat awkvars.out

秉持着理解深入的原则,我们还是先看看上面代码运行的逻辑是什么,首先呢--dump-variables的作用呢是导出(非显示!!!)这个awk程序当中的内置变量和自定义变量,比如说呢我在BEGIN程序里面定义了一个变量Count=1,那么Count就会被这个command导出。那我们注意到在其后是有一个‘ ’,其实这就是空脚本,可以理解为我不改动任何一个配置,所以最后导出的就是最原本的内置变量和默认配置。但是刚才其实也强调了,第一条语句只是把这些变量导出到这样一个文件awkvars.out当中,我们还是需要配合cat来显示

(小小广告,如果cat的具体用法您不清晰的话可以参考小编的另外一篇博客)Unix命令行入门-CSDN博客

我们从头开始,如果跳过说明就是不太重要的参量了 

ARGC表示参数的个数,这里有1个(不是空脚本吗为啥有1个参量?是因为awk本身的shell脚本是字符串格式,它自己就占了一个参量)

ARGV是存储参量的数组(这里真的需要点赞awk的设计,我们在c,c++,Python的当中的数组(或列表)的索引只可以是数字但是在awk格式没有限制(甚至可以是字符串))

CONVFMT(conversion format)其规定了数字的转换格式就是%.6g(也就是数字默认的输出格式就是6位有效数字,以g的格式输出(如果是位数较少就采用一般计数,如果位数较多那么采用科学计数(后面有说明)))

FILENAME:顾名思义,表示的就是当前的文件名,那么这里就是‘’

FS (field separator)(字段分隔符)这里默认的字段分隔符就是单个空格,而且会忽略开头结尾的空格,并且将连续的空格视为一个分隔符(这点很好了),当然了可以在command当中更改的,一般是可以在BEGIN程序部分更改,对于分隔符的更改对于文本处理是一个非常有利且灵活的操作

NF(number of field)表示当前行(record)的字段个数

NR(number of record)表示当前行的编号(从1开始编号注意!)

OFS(output field separator)表示输出格式的字段分隔符,默认是单个space

ORS(output record separator) 表示输出格式行分隔符,默认是\n

RLENGTH (re-length) 表示被pattern匹配的字符串长度(字符为单位)

RS(record separator)表示输入格式的行分隔符,默认是\n

RSTART(re-start)表示被第一个匹配的字符的位置索引(从1开始)

$0直接代指整行,$n代指分隔符分隔之后的各个字段的编号(从1开始)

小小总结一下:其实对于filed separator和record separator无论是input还是output,都可以这么理解:只是我们如何区分和呈现字段与行的方式不同而已,比如我们可以认为我们的输入字段是以,分隔的,(FS=',')那么我们实际文本处理的时候就是会以这种方式去理解字段,同理行

Operators操作符

操作符的基本操作自我感觉是和Python大差不差的,有几个不太一样的小编单独拿出来说一下(或者是容易混淆的)

1. 首先是pre-increment和post-increment的不同之处(decrement请聪明的各位自行举一反三)

awk 'BEGIN { a = 10; b = ++a; printf "a = %d, b = %d\n", a, b }'
#output: a=11,b=11
awk 'BEGIN { a = 10; b = a++; printf "a = %d, b = %d\n", a, b }'
#output: a=11,b=10

其实就是注意到赋值的前后问题,第一个程序是先a加1然后赋值给b,所以a=b=11,第二个程序就是a先赋值给b然后a再++,所以a=11但是b=10(但是为什么要没苦硬吃呢,其实我们在写的时候直接分开来写就好了)

2. 逻辑和&&,逻辑或||,非!

3. condition expression ? statement1 : statement2 在Python当中也有类似语法,如果expression是对的,那么执行statement1,否则执行statement2(三目表达式)

4. 字符串的合并操作还是非常简单的,直接space连接即可

awk 'BEGIN { str1 = "Hello, "; str2 = "AWK"; str3 = str1 str2; print str3 }'

Regular Expressions 正则表达式

这里的正则表达式的应用可以说是文本处理的非常重要的一个环节,所以专门有一篇博客介绍

正则表达式入门-CSDN博客(也是小编自己写的,自己做个广告嘻嘻~)

在这基础上,简单注意一下我们的pattern需要用/regex/包裹的哦,除此之外,请看下面一个例子

awk '$1 ~ /apple/ || $3 ~ /apple/ { print }' file.txt

我们可以不限制字段(没有$n)此时我们也不需要写~,但是如果我们需要限制匹配字段的话就需要如上面所示的语法了(pattern使用也是很灵活的,可以配合逻辑表达式) 

Array 数组

小编自以为数组是awk与其他编程语言不同之处,可以称得上是awk的highlight了(可能是因为小编见识过的编程语言不多所以敢如此放言),基本语法如下

array_name[index] = value

前面简单介绍过awk的index是可以取任何格式的,可以是正常的整数甚至可以是字符串(是不是很nice!),比如说如果我要生成这一个数组

100   200   300
400   500   600
700   800   900

如果按照Python的做法,得列表里面套列表对吧(或者用numpy多维数组括号不停套)

但是在awk,直接这么写就好了 array['1,1']=100,世界一下子晴朗了~

这里只是简单介绍一下基本语法

Control Flow 控制流

其实基本的控制流的关键if,else等还是没变的,这里就举一个例子就好,聪慧的大家自行体会(如果有编程基础的话,没有的话可能需要再查一下哦!)(当然也可以写在同一行)

awk 'BEGIN {
   a = 30;
   
   if (a==10)
   print "a = 10";
   else if (a == 20)
   print "a = 20";
   else if (a == 30)
   print "a = 30";
   else
   print "a = 40";
}'

(习惯用Python的同学可能需要注意一下这里的分号表示不同语句哦还有这里是else if)

Loops 循环

对于循环和C语言其实几乎没有变动,但是为了严谨起见,咱们还是把各个循环语句过一遍

For循环

awk 'BEGIN { for (i=1;i<=5;++i) print i}'

For除了这一种方式控制,还可以 for i in a (print i) (那么这里的i其实遍历的是数组a的索引,如果要访问对应的Value的话直接a[i]即可)

While循环

awk' BEGIN
{ i=1;
  while(i<6)
{ print i;
  i++
}}'

Do-While Loop

awk 'BEGIN {
i=1
do {
    print i
    ++i
   }
while (i<6) }'

对于Break,continue就不再多说了,功能是一样的

对于exit表示退出程序,其后可以加一个constant如exit(10)来表示因为什么退出程序的

Built-in Functions 内建函数

这一块小编会尽可能讲的详细一点,可能在后面的文本处理当中会用到

1. 计算函数,这一块小编就简单过一遍哈~,因为基本一致

atan2(y,x)算arctangent(y/x)

cos(x)(弧度表示)

sin(x)(弧度表示)

exp(x)自然指数次幂

int(x)取整函数

log(x)自然对数为底对数

rand() 随机生成0-1内的数(左闭右开)

srand()设立种子值,保证可重复性

sqrt(x)开平方根

2. 字符串函数(字符串必须要用双引号包裹哦~)

(这里参数位置其实小编也没有记清楚,实际运用的时候可以再来查看)

asort 对数组的值排序

asort(arr [, d [, how] ]),[]表示可选,前面一个arr表示需要排序的数组,d(optional)表示排序之后存放值的数组,how表示希望按什么标准排序(这里可以用内建函数哦,这里不展开了)

这里强调一下有没有d的区别(强烈建议还是有的),如果不给目的数组的参数,那么最后asort的结果就会直接存放在原数组的位置,索引变成了排序序号,所以我们最初的索引信息没有了,但是如果给了目的数组,那么就把目的数组作为存放排序结果的地方,原来是数组就不会改变

asorti(arr [, d [, how] ])

这是对数组的索引进行排序,其他地方和asort相同略过

gsub(regex,sub,string)

全局替换,把一切满足regex(正则表达式)的字符串部分全部替换为sub(给定的字符串),string是我们操作的对象,比如

awk 'BEGIN { string="hello, awk"; gsub ("awk","World",string) print string}'

(注意直接在原字符串上更改)

index(string,sub)

判断子串存在的位置,如果sub是str的子串,那么返回sub第一个字符的索引,否则返回0

length(string)

返回字符串的长度

match(string,regex)

返回第一个匹配regex的最长子串的第一个字符索引,如果没有返回0

split(string,arr,regex)

将regex作为分隔符分割str将分割的字段储存在arr当中

(这里比一般的功能强大多了,因为有regex的加持,所以可以同时识别多个分隔符)

sub(regex,sub,string)

是gsub的缩版,仅仅更改第一个匹配regex的子串为sub

substr(string,start,length)

(其实有点类似Python的切片)是返回string子串的操作,从start索引开始(含)截取length个字符,如果不写length,那么直接提取到string的最后一个字符

tolower(string)

把string里面所有的字母全部改为小写

toupper(string)

把string里面所有的字母全部改为大写

3. 其他内建函数

close(x)

一般是用来关闭双管道的,请看下面一段程序(#后是对此行命令的解释,程序可能需要自己手打)(getline不懂的后面有详细介绍哦,请勿焦虑~)

 awk 'BEGIN {
   cmd = "tr [a-z] [A-Z]"  
   print "hello, world !!!" |& cmd 
    
   
   close(cmd, "to") 
   cmd |& getline out 
   print out; 
   
   close(cmd);  
}'

#tr 是翻译的命令,此时相当于cd就是此翻译的命令字符串
#双管道有两个方向,一个方向是常规的数据写入命令,还有另外一个方向就是读得执行命令的结果,这里的结果必须需要getline来手动强迫换行读取
#第一个close用来关闭数据写入的通道,否则命令会等待继续输入
#用getline将执行cmd的结果储存在out文件内
#cmd是存储量Shell的一个命令字符串,双管道的操作必须要把命令转化为字符串格式(和C语言底层执行逻辑有关)
#关闭管道释放资源

delete arr[index]

删除数组元素

getline 

直接读取下一行(如果没有getline对于一般body部分会自动读取,但是如果有getline,就会因此跳过第一行从第二行开始读取),在双管道当中读取命令的输出这一块是必备的

next

next一般式放在非循环程序中用来跳过本行剩余命令的(在循环当中,next的作用和continue一样)

nextfile

用来跳过本文件剩余命令的

system()

()内是某个command,system会试图执行这个command,然后将执行是否正常进行的结果返回(如果正常执行那么会返回0,如果没有正常执行就会返回非零值)

Getline 详细讲解

小编发现getline这个命令出现频率还是很高的,为了之后在Linux遇到可以不慌,特意整理了一些关于getline的必备知识点

其实getline最简单或者是最本质的理解就是对于输入流的读取,只是根据输入流来源的不同其的功能也有些许差别

1.首先是应用于一般无管道的command当中(后面会说)

echo '1\n2\n3\n4\n5\n' | awk 'BEGIN{getline} {print $0}'
echo '1\n2\n3\n4\n5\n' | awk '{getline} {print $0}'

咱们首先来看这两个程序(这里管道只是一般用法,不要在意)

第一个程序的getline是放在 BEGIN程序内部的,我们介绍过BEGIN是工作流的最开始,是在BODY 部分才会程序才会主动读取输入,所以BEGIN这里的getline读取的是第一行输入,但是并没有任何操作,那么接着进入主程序,由于第一行已经在BEGIN部分被getline读取,所以主程序从第二行开始执行,最后打印结果是2 3 4 5(换行符省略)

我们再来看第二个程序和第一个程序之间有什么不同,这里的getline是放在了主程序里面。我们一进入主程序,程序已经读取了第一行输入(这里还没执行getline),然后呢我们立马就执行了getline,我们读到了第二行,第一行的数据被我们覆盖了,那么第一次print的其实是第二行的结果2,然后再回到主程序开头(因为输入还没读到最后一行),主程序读到了第三行,但是一旦我们执行了getline,第三行数据有被第四行数据覆盖,所以我们打印的是4,最后我们主程序读到第五行,但是无脑的getline继续向下读,结果是空行,所以最后没有打印。总而言之结果就是2 4

我们初步了解之后,我们可以总结:主程序和getline是共同控制读取到第几行的,主程序是在一开始进入即读(可以理解为在{处开始),而getline是需要执行这个命令开始读

在明白了这个道理之后,我们看到一个稍微加点料的例子

echo '1\n2\n3\n4\n5\n' | awk '{getline next_line} {print $0}'

 稍稍变化一下,在getline后面加上一个参数来接收此时getline读到的行,结果会有什么不同吗?

运行一下发现,结果是1 3 5,为啥呢?

原来给getline加上一个参量就可以使得把getline读到的该行存储到next_line当中,从而不覆盖主程序读取的$n的数据,所以$0表示的仍然是主程序读到的,而在之前的例子当中,getline后面没有参量,那么getline就会将自己读到的行覆盖主程序的$n信息,所以输出的是getline该行的$n。

但是仍然要注意一点,我们只讲到覆盖的问题,getline仍然是控制了程序读到哪里了,下一次主程序读取的仍然是getline读取的下一行(尤其注意哦~)

2. 好了,我们现在再来看有管道的getline是如何使用的,我们先暂且借用下内建函数当中close的例子

 awk 'BEGIN {
   cmd = "tr [a-z] [A-Z]"  
   print "hello, world !!!" |& cmd 
    
   
   close(cmd, "to") 
   cmd |& getline out 
   print out; 
   
   close(cmd);  
}'

我们之前说到close在这里是起到关闭管道的作用以释放资源,我们在这里再深入了解一下

首先cmd是命令的字符串,我们将"hello, world !!!"的输入传给cmd这个命令,然后cmd的输入就可以终止了,所以我们close(cmd,'to')这一步我们只关闭输入通道,但是对于cmd的输出通道我们还是没有关闭的

在cmd |& getline out这里,其实是将cmd执行的结果(理解为水)全部放在管道(理解为水龙头)当中,此时cmd由于输入结束,所以不会再产生任何的水了,产生的所有的的水就全部放在了管道当中(作为进一步处理的对象,所以可以当成是另一级输入)。但是程序不会主动地从管道中获取输入,所以必须要借助getline,得到管道当中的‘水’,并将其存储在out当中。在这一步结束之后,管道中的水已经没了,所以我们可以关闭管道(如果不关闭,管道自身已经没有输送功能,但是也不可以给其他子进程使用所以会浪费资源)。但是需要非常注意的一点是,如果cmd产生的是多行输出怎么办,也就是管道当中的水我们用一次getline运不完,这个时候我们就需要用到循环来操作

 awk 'BEGIN {
   cmd = "tr [a-z] [A-Z]"  
   print "hello, world !!!" |& cmd 
   close(cmd, "to") 
 while( cmd |& getline out) 
   {print out} 
   
   close(cmd);  
}'

那么这里我们不断用循环读取管道当中的内容,然后等到读取结束之后,关闭管道(注意必须是循环结束之后关闭,因为如果在循环内部关闭,那么管道内部还有水但是没有获取)

getline的理解就讲到这里,只是希望之后见到getline的时候可以理解其的作用,然后慢慢模仿写法

User Defined Functions 自定义函数

自定义函数的方式和Python是一致的,可以仿照,这里不赘述了(当然了仅仅是如何定义和调用是一致的,对于函数里面的内容就参考本博客其他内容了)

Pretty Printing 打印

打印这一块基本上是和C语言是一致的,做一个简要介绍

print可以用于简单打印,比如直接打印一个string之类的,但是涉及到格式化打印还是要交给printf,下面也即是着重介绍printf的打印

基本语法: printf format variable-list

基本转义符
\n换行符
\t水平制表符
\v垂直制表符
\b将光标移至前一个字符
\r将光标移至此行开头

简单例子如下:

awk 'BEGIN { printf "HAHAHA 1\b HAHAHA 2\bHAHAHA 3\bHAHAHA 4\n" }'
#输出 HAHAHA HAHAHA HAHAHA HAHAHA 4

和C一致,printf打印需要格式说明符(Format Specifier)

格式说明符
%c单个字符(遇到数字那么就是认为是单个字符对应的ASCII值)
%s字符串
%d整数
%f浮点数,如以86.2300呈现
%e科学计数法,如以8.62300E+01
%g根据数的大小和位数选择是f还是e表示
%o八进制,如printf‘%o’,10输出12
%x十六进制,如printf‘%x’,10输出A
%u转变为无符号数

对于要表示%本来的字符,需要两次转义%%

输出的选择参数(宽度和对齐格式)

Width:对于宽度的调控是将Width放在格式说明符%和。。之间

如printf 'Num=%10d' 即右对齐10字符宽度以space填充

(如果想要用0填充,那么即写printf 'Num=%010d' )

默认是右对齐,如果想要左对齐,那么用-

printf 'Num=%-10d'即左对齐10宽度

注意:如果是负号也会占一个字符哦~

总结

本篇只是简单系统性介绍了awk的基本语法,可以当成是一个introduction类型的内容,内容覆盖Linux生信基本用法应该是没有问题滴,但是在Linux熟练掌握awk还需要不断练习和学习呀~

小编水平有限,如果错误不足之处请多指教友好交流哦~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值