Linux文本处理三剑客之awk学习笔记10:函数

前言

关于函数的基本概念,在学习bash的函数的时候已经大致讲解过了,加上本人大学时期也学习过C语言(虽然都忘记了),因此这里就不再对函数做过多冗余的介绍了。

awk大致将函数分成了自定义函数和内置函数。不过其本质上没有区别,自己写的函数就叫做自定义函数,而官方写好的嵌入在awk本身的我们直接拿来用的函数就叫做内置函数。关于内置函数的介绍请见这里

本博文学习的内容是了解函数,学会如何创建与使用它们。也就是学会自定义函数。虽然内置函数可以拿来直接使用,不需要了解其内部实现。但是学习自定义函数就是在学习函数的基础。

函数的定义

function funcName([arg, ...]){
    ... function body ...
}

func funcName([arg, ...]){
    ... function body ...
}

awk的函数可以定义在代码的任意位置,没有先后顺序之分。例如定义在下面的下划线位置:

awk '_BEGIN{}_main{}_main{}_END{}_' ...

这是因为awk在执行BEGIN代码块执行,awk就会将代码编码成内部格式,而在这一步中就会去识别代码中的函数定义了。这在awk的工作流程中就有讲述到了。

注意:别把函数定义在main代码块中即可。毕竟不是每次内部循环一次就要定义一次函数。

因此可以在任意位置调用任意位置定义好的函数。

# awk 'BEGIN{f()}function f(){print "hello world"}'
hello world

函数的返回值

函数使用return语句来返回返回值。一旦遇到return语句,在return语句后面的函数内部语句就不会执行。

# awk 'func re(){return 100;print "hello world"} BEGIN{a=re();print a;print re()}'
100
100

注意:返回值也可以是字符串。

# awk 'func re(){return "abc";print "hello world"} BEGIN{a=re();print a;print re()}'
abc
abc

如果函数没有return语句或者return语句没有具体的返回值,则返回空字符串。

# awk 'func f(){} BEGIN{print "---"f()"---"}'
------
# awk 'func f(){return} BEGIN{print "---"f()"---"}'
------
# awk 'func f(){return 100} BEGIN{print "---"f()"---"}'
---100---

函数的参数

函数可以不带参数,不过大多数时候是带参数的,这样使得函数在调用时更加灵活。

# cat funcArg.awk
func f(a,b){
    print a
    print b
    return a+b
}

BEGIN{
    x=10;y=20
    res=f(x,y)    # 调用函数时打印了x和y的值,并将返回值赋值给res
    print res
    print f(x,y)    # 在打印函数返回值的同时由于调用了函数,而函数本身包含了打印x和y的值,所以先打印x和y的值,再打印返回值。
}
# awk -f funcArg.awk
10
20
30
10
20
30

使用函数来重复连接字符串。函数接受2个参数,其一是想要串联的字符串,其二是想要串联的次数。

# cat funcCatStr.awk
func cat(str,count){
    for(i=1;i<=count;i++){
        newStr=newStr""str
    }
    return newStr
}
BEGIN{print cat("-",5)}
# awk -f funcCatStr.awk
-----

在编程语言中,函数的参数有两种,形式参数和实际参数。

在函数定义时用来定义函数可接受的参数的参数称为形式参数,简称形参。在函数调用时实际向函数传递的参数成为实际参数,简称实参。

f(x,y){}    # 定义形式参数x和y。
a=10;b=5
f(a,b)    # 传递实际参数a和b。

在计算机英语中,我们使用parameter来表示形参,使用argument来表示实参。如果某种情况下没有实参和形参之分的话,那么parameter和argument都可以用来表示参数之意。

在函数调用时,实参和形参的个数可以不一致。但是,如果实参数量多于形参数量,那么awk会返回警告信息。

# awk 'func f(a,b){} BEGIN{f(1,2,3)}'
awk: cmd. line:1: warning: function `f' called with more arguments than declared

参数类型冲突

实参和形参的变量类型需要一致,否则会报错。

# 实参是数值变量,而形参是数组。
# awk 'func f(a){a["name"]="alongdidi"} BEGIN{x=10;f(x)}'
awk: cmd. line:1: fatal: attempt to use scalar parameter `a' as an array
# 首先进行函数的调用,实参x被识别为形参中的数组,因此在BEGIN的后续想要将实参x当作数值变量来使用会报错。
# awk 'func f(a){a["name"]="alongdidi"} BEGIN{f(x);x=10}'
awk: cmd. line:1: fatal: attempt to use array `x' in a scalar context

参数的传递方式

首先我们回顾一下bash中的变量的概念,变量的名称,起始是指向某个内存空间的地址,我们引用变量,就是引用对应地址的内存空间中的数据。

在函数参数传递时,有两种传参方式:

  1. 先找到地址对应的内存空间中的数据,将该数据复制一份放入新的内存空间中。将该值作为参数传递就是将形参指向该新内存空间。这种方式叫做按值传递,会产生新的内存空间。
  2. 直接将实参所对应的内存空间的地址传递给形参,使得实参和形参同时指向了同一个内存空间。这种指向应该是基于指针的概念。这种方式叫做按引用传递。

由此可见,按值传递使用不同的内存空间,因此即便实参和形参的变量名称相同,其指向的内存地址也回是不同的。

因此按值传递的函数内部的变量修改不会影响到函数外部,反之亦然。

在awk中,如果传递的参数的变量类型是数值或者字符串,则是按值传递。

# awk 'func f(a){a=10} BEGIN{a=5;print a;f(a);print a}'
5
5
# awk 'func f(a){a="alonggege"} BEGIN{a="alongdidi";print a;f(a);print a}'
alongdidi
alongdidi

按引用传递由于使用了相同的内存空间,并且传参时传递的是内存的地址。因此按引用传递的函数内部的变量修改会影响到函数外部,反之亦然。如果传递的参数的是数组,则是按引用传递。

# awk 'func f(a){a["name"]="alonggege"} BEGIN{a["name"]="alongdidi";print a["name"];f(a);print a["name"]}'
alongdidi
alonggege

函数中变量的作用域

我们先来回顾funcCatStr.awk代码。

# cat funcCatStr.awk
func cat(str,count){
    for(i=1;i<=count;i++){
        newStr=newStr""str
    }
    return newStr
}
BEGIN{print cat("-",5)}
# awk -f funcCatStr.awk
-----

我们新增一部分代码。

func cat(str,count){
    for(i=1;i<=count;i++){
        newStr=newStr""str
    }
    return newStr
}
BEGIN{print cat("-",5);print cat("+",5)}    # 红色字体为新增部分。

此时我们可能会理所应当地认为输出的结果应该是:

-----
+++++

但是:

# awk -f funcCatStr.awk
-----
-----+++++

造成这种结果的原因和变量的作用域有关。在awk中,函数内部定义的变量属于全局变量,因此在函数内部的变量newStr是一个全局变量,经过第一次函数调用以后,它的值是“-----”,函数返回以后,由于它是全局变量,因此该变量不会被释放,在第二次函数调用时会在“----”的基础之上进行操作。

我们可以在第一次函数调用后print看看。

# cat funcCatStr.awk
... ...
BEGIN{print cat("-",5);print newStr;print cat("+",5)}
# awk -f funcCatStr.awk
-----
-----
-----+++++

在awk中,没有显式定义局部变量的关键词。如果希望将某个变量具有局部变量的特性的话,可以将变量置于函数定义时的参数的位置,即形参的位置。

# cat funcCatStr.awk
func cat(str,count    ,newStr){
    for(i=1;i<=count;i++){
        newStr=newStr""str
    }
    return newStr
}
BEGIN{print cat("-",5);print newStr;print cat("+",5)}
# awk -f funcCatStr.awk
-----

+++++

由于newStr并不是真实的参数,放在形参的位置仅仅是因为我们希望使其具备局部变量的特性罢了,因此真实的形参现在前面,作为局部变量的假形参写在后面,并使用多个空格分隔。

如果我们看到一个函数的定义是这样的,我们就应该明白该函数仅支持2个参数,不要向c和d传递参数,因为它们仅仅作为局部变量存在。

func f(a,b    ,c,d){...}

到这里我们应该也会明白所有的形参,无论是作为真实的形参还是局部变量,它们都具备有局部变量的特性。

cat funcCatStr.awk
func cat(str,count    ,newStr){
    for(i=1;i<=count;i++){
        newStr=newStr""str
    }
    return newStr
}
BEGIN{print cat("-",5);print cat("+",5);print str;print count}
[root@c7-server awk]# awk -f funcCatStr.awk
-----
+++++
    # 打印形参str,结果为空字符串。
    # 打印形参count,结果为空字符串。

 

实战

写一个一次性读取文件的所有数据的函数

# cat funcReadFile.awk 
func readFile(file    ,RSBak,data){
    RSBak=RS
    RS="^$"
    if((getline data<file)<=0){
        print "Reading file error!"
        exit 1
    }
    close("c.txt")
    RS=RSBak
    return data
}

/^1/{
    print $0
    content=readFile("c.txt")
    print content
}
# awk -f funcReadFile.awk a.txt 
1   Bob     male    28   abc@qq.com     18023394012
abc
def
ABC
DEF

10  Bruce   female  27   bcbd@139.com   13942943905
abc
def
ABC
DEF

写一个可以重读文件的函数

在处理某个文件的时候,如果遇到某些条件(比如读取到第3行),我们就要求重新读取一遍该文件。

PS:个人觉得这个示例怪怪的,需求都怪怪的。

# cat funcRewind.awk 
func rewind(){
    for(i=ARGC;i>ARGIND;i--){
        ARGV[i]=ARGV[i-1]
    }
    ARGC++
    nextfile
}

NR==3{    # 这里如果改成FNR的话,会陷入死循环。
    print
    rewind()
}

{
    print
}

# awk -f funcRewind.awk a.txt 
ID  name    gender  age  email          phone
1   Bob     male    28   abc@qq.com     18023394012
2   Alice   female  24   def@gmail.com  18084925203
ID  name    gender  age  email          phone
1   Bob     male    28   abc@qq.com     18023394012
2   Alice   female  24   def@gmail.com  18084925203
3   Tony    male    21   aaa@163.com    17048792503
4   Kevin   male    21   bbb@189.com    17023929033
5   Alex    male    18   ccc@xyz.com    18185904230
6   Andy    female  22   ddd@139.com    18923902352
7   Jerry   female  25   exdsa@189.com  18785234906
8   Peter   male    20   bax@qq.com     17729348758
9   Steven  female  23   bc@sohu.com    15947893212
10  Bruce   female  27   bcbd@139.com   13942943905

格式化数组的输出

当我们有一个数组的时候,我们是无法直接使用print语句将其全部输出的。

# awk 'BEGIN{arr["name"]="alongdidi";arr["age"]=29;arr["gender"]="male";print arr}'
awk: cmd. line:1: fatal: attempt to use array `arr' in a scalar context

现在我们编写一个自定义的函数,来输出这个数组的数据,输出的格式如下。

{
    arr["name"]="alongdidi"
    arr["age"]=29
    arr["gender"]="male"
}
# cat funcA2S.awk
func a2s(arr    ,str){
    for(i in arr){
        str=str""(sprintf("\tarr[\"%s\"]=%s\n",i,arr[i]))
    }
    return "{\n"str"}"
}

BEGIN{
    arr["name"]="alongdidi"
    arr["age"]=29
    arr["gender"]="male"
    print a2s(arr)
}
# awk -f funcA2S.awk
{
    arr["age"]=29
    arr["name"]=alongdidi
    arr["gender"]=male
}

识别文件名带等于号的文件

一般来说,如果出现了这种CLI,那么awk会将a=b识别变量赋值,倘若“a=b”真的是一个文件的话,我们只需要在为其带上相对路径即可。

awk -f xxx.awk a=b a.txt c.txt
awk -f xxx.awk ./a=b a.txt c.txt
# awk '{print}' a=b
^C
# awk '{print}' ./a=b
aaa
bbb
ccc

思路:

  • 所有的CLI的参数保存在ARGV中,因此从中寻找文件名形似变量赋值的文件。
  • 变量赋值的规律:
    • 等于号。
    • 变量名包含数字、字母和下划线并且只能以字母或者下划线打头。
  • 找到后替换ARGV中对应的参数。
  • 要提供一个开关选项,毕竟不是每次都会遇到文件名形如变量赋值的形式。
# cat funcRecogAssignFile.awk
func recog(argv,argc    ,i){
    for(i=1;i<argc;i++){
        if(argv[i]~/^[[:alpha:]_][[:alnum:]_]*=.*/){
            argv[i]="./"argv[i]
        }
    }
}

BEGIN{
    if(open){
        recog(ARGV,ARGC)
    }
}

{print}
# awk -f funcRecogAssignFile.awk a=b
^C
# awk -v open=1 -f funcRecogAssignFile.awk a=b
aaa
bbb
ccc

识别时间

在学习了时间类内置函数以后,我们可以基于已有的时间类内置函数来识别一些在日志文件中常见的时间格式,将其转换为epoch值(即时间戳)。这样有助于我们的后续深入的运维工作。

注意:如果只需要精确到天的时间比对的话,可以简单这么写:

awk '/2019-11-08/{print}' access.log
sed -nr '/2019-11-08/p' access.log

在运维工作中,一般遇到的日志文件中的时间格式一般都形如以下两种:

2019-11-11T03:42:42+08:00
Sat 26. Jan 15:36:24 CET 2013

这种日期时间格式无法直接拿来比较,必须先转换成epoch值。但是这种格式也无法直接被mktime()转换成epoch值,需要先做处理。

mktime("YYYY MM DD HH MM SS [DST]"[,utc-flag])

因此我们可以自定义两个函数来将上面的两种格式转换为epoch值。

str1ToTime()

2019-11-11T03:42:42+08:00

思路:

  1. 将字符串转换成mktime()可识别的格式“Y M D H m S”。注意:需要使用sprintf()来构建这种格式。
  2. 然后使用mktime()输出时间戳。
# cat str1ToTime.awk
func str1ToTime(str    ,newStr,Y,M,D,H,m,S,arr){
    newStr=gensub("[-:T+]+"," ","g",str) # 2019 11 11 03 42 42 08 00
    split(newStr,arr)
    Y=arr[1]
    M=arr[2]
    D=arr[3]
    H=arr[4]
    m=arr[5]
    S=arr[6]
    # print mktime(Y M D H m S)
    # Do not write like this, otherwise mktime() return -1!!!
    # Use sprintf() instead!!!
    return mktime(sprintf("%d %d %d %d %d %d",Y,M,D,H,m,S))
}

BEGIN{
    print str1ToTime("2019-11-11T03:42:42+08:00")
    print str1ToTime("2021-11-11T03:42:42+08:00")
    print (str1ToTime("2019-11-11T03:42:42+08:00") < str1ToTime("2021-11-11T03:42:42+08:00"))
}

# awk -f str1ToTime.awk
1573414962
1636573362
1

需要注意,日期和时间信息存入数组以后不能直接拿来用,必须要使用sprintf()转换才可以。

mktime(Y M D H m S)    # 这样会使得mktime()接收6个参数,实际上它只能接收2个,其中一个还是可选的。
mktime("Y M D H m S")    # 这样写的话,就无法变量替换了,而是识别了字符串字面量。

str2ToTime()

Sat 26. Jan 15:36:24 CET 2013

思路:

  • 与str1ToTime()类似,区别在于需要识别月份“Jan”,因此需要事先写一个映射函数。
# cat str2ToTime.awk
func str2ToTime(str    ,Y,M,D,H,m,S,arr){
    patsplit(str,arr,"[[:alnum:]]+")
    Y=arr[8]
    M=monthMap(arr[3])
    D=arr[2]
    H=arr[4]
    m=arr[5]
    S=arr[6]
    return mktime(sprintf("%d %d %d %d %d %d",Y,M,D,H,m,S))
}

func monthMap(mon    ,mrr){
    mrr["Jan"]=1
    mrr["Feb"]=2
    mrr["Mar"]=3
    mrr["Apr"]=4
    mrr["May"]=5
    mrr["Jun"]=6
    mrr["Jul"]=7
    mrr["Aug"]=8
    mrr["Sep"]=9
    mrr["Oct"]=10
    mrr["Nov"]=11
    mrr["Dec"]=12
    return mrr[mon]
}

BEGIN{
    print str2ToTime("Sat 26. Jan 15:36:24 CET 2013")
    print mktime("2013 01 26 15 36 24")
}
# awk -f str2ToTime.awk
1359185784
1359185784

str3ToTime()

这个映射函数可以进行简单优化,使其不用占据那么多行数据。

每一个月份的简写字符串占用3个字符。基于这个规律,我们可以使用index()加上简单的数学计算来推算月份简写字符串对应的月份数。

# cat str3ToTime.awk
func str3ToTime(str    ,Y,M,D,H,m,S,arr){
    patsplit(str,arr,"[[:alnum:]]+")
    Y=arr[8]
    M=monthMap(arr[3])
    D=arr[2]
    H=arr[4]
    m=arr[5]
    S=arr[6]
    return mktime(sprintf("%d %d %d %d %d %d",Y,M,D,H,m,S))
}

func monthMap(mon){
    return (index("JanFebMarAprMayJunJulAugSepOctNovDec",mon)+2)/3
}

BEGIN{
    print str3ToTime("Sat 26. Jan 15:36:24 CET 2013")
    print mktime("2013 01 26 15 36 24")
}
# awk -f str3ToTime.awk
1359185784
1359185784

真实日志文件

原博主【骏马金龙】提供了2个日志文件,内容基本一致,除了日志字段,我们就各取对应的5行来处理吧。

日志文件放百度网盘了,提取码是jtlg。

# cat access1.log
111.202.100.141 - - [2019-11-07T03:11:02+08:00] "GET /robots.txt HTTP/1.1" 301 169 "-" "Sogou web spider/4.0(+http://www.sogou.com/docs/help/webmasters.htm#07)" "-"
111.202.100.141 - - [2019-11-07T03:11:02+08:00] "GET /videos/index/ HTTP/1.1" 301 169 "-" "Sogou web spider/4.0(+http://www.sogou.com/docs/help/webmasters.htm#07)" "-"
50.7.235.2 - - [2019-11-07T03:11:32+08:00] "GET /robots.txt HTTP/1.1" 301 169 "-" "Mozilla/5.0 (Windows NT 6.1; rv:60.0) Gecko/20100101 Firefox/60.0" "-"
50.7.235.2 - - [2019-11-07T03:11:33+08:00] "GET / HTTP/1.1" 301 169 "-" "Mozilla/5.0 (Windows NT 6.1; rv:60.0) Gecko/20100101 Firefox/60.0" "-"
54.36.149.32 - - [2019-11-07T03:15:03+08:00] "GET /robots.txt HTTP/1.1" 301 169 "-" "Mozilla/5.0 (compatible; AhrefsBot/6.1; +http://ahrefs.com/robot/)" "-"

# cat access2.log
111.202.100.141 - - [07/Nov/2019:03:11:02+08:00] "GET /robots.txt HTTP/1.1" 301 169 "-" "Sogou web spider/4.0(+http://www.sogou.com/docs/help/webmasters.htm#07)" "-"
111.202.100.141 - - [07/Nov/2019:03:11:02+08:00] "GET /videos/index/ HTTP/1.1" 301 169 "-" "Sogou web spider/4.0(+http://www.sogou.com/docs/help/webmasters.htm#07)" "-"
50.7.235.2 - - [07/Nov/2019:03:11:32+08:00] "GET /robots.txt HTTP/1.1" 301 169 "-" "Mozilla/5.0 (Windows NT 6.1; rv:60.0) Gecko/20100101 Firefox/60.0" "-"
50.7.235.2 - - [07/Nov/2019:03:11:33+08:00] "GET / HTTP/1.1" 301 169 "-" "Mozilla/5.0 (Windows NT 6.1; rv:60.0) Gecko/20100101 Firefox/60.0" "-"
54.36.149.32 - - [07/Nov/2019:03:15:03+08:00] "GET /robots.txt HTTP/1.1" 301 169 "-" "Mozilla/5.0 (compatible; AhrefsBot/6.1; +http://ahrefs.com/robot/)" "-"

目标:输出“2019-11-07 03:11:32”之后的日志信息。

针对access1.log:

# cat access1.awk
BEGIN{
    compareTime=mktime("2019 11 07 03 11 32")
}

{
    patsplit($4,arr,"[[:digit:]]{1,4}")
    Y=arr[1]
    M=arr[2]
    D=arr[3]
    H=arr[4]
    m=arr[5]
    S=arr[6]
    time=mktime(sprintf("%d %d %d %d %d %d",Y,M,D,H,m,S))
    if(time>compareTime){
        print
    }
}
# awk -f access1.awk access1.log 
50.7.235.2 - - [2019-11-07T03:11:33+08:00] "GET / HTTP/1.1" 301 169 "-" "Mozilla/5.0 (Windows NT 6.1; rv:60.0) Gecko/20100101 Firefox/60.0" "-"
54.36.149.32 - - [2019-11-07T03:15:03+08:00] "GET /robots.txt HTTP/1.1" 301 169 "-" "Mozilla/5.0 (compatible; AhrefsBot/6.1; +http://ahrefs.com/robot/)" "-"

针对access2.log:

# cat access2.awk
func monthMap(mon){
    return (index("JanFebMarAprMayJunJulAugSepOctNovDec",mon)+2)/3
}

BEGIN{
    compareTime=mktime("2019 11 07 03 11 32")
}

{
    patsplit($4,arr,"[[:alnum:]]{1,4}")
    Y=arr[3]
    M=monthMap(arr[2])
    D=arr[1]
    H=arr[4]
    m=arr[5]
    S=arr[6]
    time=mktime(sprintf("%d %d %d %d %d %d",Y,M,D,H,m,S))
    if(time>compareTime){
        print
    }
}
# awk -f access2.awk access2.log 
50.7.235.2 - - [07/Nov/2019:03:11:33+08:00] "GET / HTTP/1.1" 301 169 "-" "Mozilla/5.0 (Windows NT 6.1; rv:60.0) Gecko/20100101 Firefox/60.0" "-"
54.36.149.32 - - [07/Nov/2019:03:15:03+08:00] "GET /robots.txt HTTP/1.1" 301 169 "-" "Mozilla/5.0 (compatible; AhrefsBot/6.1; +http://ahrefs.com/robot/)" "-"

 

posted @ 2021-02-02 16:45  阿龙弟弟  阅读(554)  评论(0编辑  收藏  举报
回到顶部