文本三剑客之一,awk

Linux 文本三剑客之一 AWK 记录

与其说 AWK 是一个命令行工具,不如说是一门语言。 AWK 是一种使用方便且表现力很强的变成语言,它可以应用在多种不同的计算与数据处理任务中。

基本操作

起于一个简单的例子

现有数据文本 文件名为 emp.data,第一列为员工姓名,第二列为每小时工资,第三列为工作时长

Beth	4.00	0
Dan	3.75	0
Kathy	4.00	10
Mark	5.00	20
Mary	5.50	22
Susie	4.25	18

任务一: 打印员工姓名以及总工资

awk '$3 > 0 {print $1, $2 * $3}' emp.data

上面语句的含义是当第三列大于 0 时执行后面{}内的动作(此处默认以空白符作为列的分割)

AWK 程序结构

pattern {action}
pattern {action}

awk 的基本操作是在由输入行组成的序列中,陆续地扫描每一行,搜索可以被模式 匹配 (match) 的行。匹配 的精确含义依赖于问题中的模式,比如,对于 $3 > 0, 意味着 “条件为真”。每一个输入行轮流被每一个模式测试,每匹配一个模式,对应的动作 (可能包含多个步骤) 就会执行。 然后 下一行被读取, 匹配重新开始。 这个过程会一起持续到所有的输入被读取完毕为止。

运行 AWK 程序

awk program input files

从 files 可以看出 awk 可以同时处理多个文件

awk '$3 == 0 {print $1 }' file1 file2

如果只是执行 awk program 接下来标准输入会作为处理的文本

如果 program 部分比较长的话,可以写在一个文件内,然后通过 -f 指定文件来指定 program 即 -f 来告诉 awk 从文件中提取程序

awk -f program-file optional files

输出

awk 中数据只有两种类型,一种是数值,另一种是字符组成的字符串,awk 从输入中每次读取一行,将行分解为一个一个的字段 ( 默认是将非空白符组成的序列作为字段 ),当前行的第一个字段为 $1 第二个为 $2 以此类推,$0 代表整行

简单打印某些字段

eg: 答应 emp.data 的第一个字段和第三个字段

awk '{print $1, $3}' emp.data

注意在 print 语句中用 , (英语逗号) 作为表达式的分隔符,输出默认以一个空格作为输出的分隔符(都可修改)

字段的数量

内建变量 NF 表示输入的字段数量,$ 符号后可以跟任何表达式,awk 会计算当前行的字段数量,并存储进内建变量 NF 中

注意,是当前输入行

有以下文件

s1 s2 s3 s4
w1 w2 w3
r1 r2 r3 r4 r5

语句如下, 只打印 每一行 的第一个字段和最后一个字段

awk '{ print $1, $NF }' temp_line.data

即使每一行字段数不同,NF 内建变量始终指向最后一个字段

使用字段进行求值

直接看示例程序即可

awk '{ print $1, $2 * $3 }' emp.data

打印行号

在 awk 中内建变量 NR 计算到目前为止读取到的行的数量 eg: 为每一行加上行号

awk '{ print NR, $0 }' emp.data

将文本放入输出之中

注意,print 只是接收参数输出,所以放入字符串也是正常操作,而如 $1 等类似于 awk 内的变量,如同 shell 的 $1
awk '{ print NR, $1, "的总工资为", $2 * $3 }' emp.data

格式化输出

在 awk 中和 c 语言类似也存在 printf,并且基本用法相同

awk ' { printf("test for printf %s is %.2f\n", $1, $2 * $3) } ' emp.data

如果想要输出排序可以使用 sort 命令(默认从小到大排序)

awk ' { printf("%6.2f %s\n", $2 * $3, $0) } ' emp.data | sort -n
# -n 表述取字符串数字的值进行排序

文本选择

除去简单的算术比较如: $2 >= 5 ,$3 == 3,外还可以使用正则表达式进行匹配

eg: 匹配 Susie 这个字符串

/Susie/

逻辑判断

awk 中的逻辑判断与大部分编程语言相同,下面是一个判断例子。

# 第二个字段大于等于 4 或者第三个字段大于等于 20
$2 >= 4 || $3 >= 20

# 取反
!( $2 >= 4 || $3 >= 20 )

BEGIN 与 END

BEGIN 在输入文件的第一行之前被匹配,END 最后一行被处理之后进行匹配。

awk 'BEGIN { print "NAME RATE HOURS"; print "" } \
      { print } ' emp.data

使用 AWK 计算

使用 awk 计算,不仅可以使用内建变量如 NF,还可以使用自定义变量,并且使用变量不需要事先声明。

# 计算工作时常超过15个小时的员工人数。
awk '$3 > 15 { emp = emp + 1} \
    END { print emp, "employees worked more than 15 hours" }' emp.data
  1. 操作文本

搜索每小时工资最高的雇员。

awk '$2 > maxrate { maxrate = $2; maxemp = $1 } \
    END { print "highest hourly rate:", maxrate, "for", maxemp }' emp.data
  1. 字符串拼接

字符串拼接操作直接空格续写字符串即可。下面是将所有名字拼接起来的一个例子。

awk '{ names = names $1 " " } END { print names }' emp.data

names 使用之前的默认值为空字符串,所以不需要显式地初始化。

  1. 内建函数

length 计算字符串中字符的个数

awk '{ nc = nc + length($0) + 1; nw = nw + NF } END { print NR, "lines", nw, "words", nc, "characters" }' emp.data

控制流程语句

  1. if-else 语句

计算每小时工资多于$6.00 的雇员的总报酬与平均报酬。

awk '$2 > 6 { n = n + 1; pay = pay + $2 * $3 } \
END { \
    if (n > 0) \
        print n, "employees, total pay is", pay, "average pay is", pay / n
    else
        print "no employees are paid more than $6/hour" }' emp.data
  1. while 语句

计算一笔钱随着特定的利率增长而增长。

{ i = 1
  while (i <= $3) {
      printf("\t%.2f\n", $1 * (1 + $2) ^ i)
  }
}

此处 ^ 为指数运算。

  1. for 语句

计算工资回报

{ for(i = 1; i <= $3; i = i + 1)
      printf("\t%.2f\n", $1 * (1 + $2) ^ i)
}

awk 中的 if,while,for 都和 c 系列语言的语法相同。

数组

逆序输出输入数据

{ line[NR] = $0 } # 存储每一行
END {i = NR
  while (i > 0) {
    print line[i]
    i = i - 1
  }
}

AWK 语言

将表达式作为模式

表达式的结果如果是 true 或者不为零,字符串不为空,都视为匹配。任意一个表达式都可以用作任意一个运算符的操作数。 如果一个表达式拥有一个数值形式的值,而运算符要求一个字符串值,那么该数值会自动转换成字符串,类似地,当运算符要求一个数值时,字符串被自动转换成数值。

其中字符串的比较,大多数情况是比较 ASCII 码。

字符串匹配模式

  1. regexpr 如果当前输入行包含一段能够被 regexpr 匹配的子字符串,则该模式被匹配。

  1. expression ~ regexpr 如果 expression 的字符串 包含 一段能够被 regexpr 匹配的子字符串,则该模式被匹配。

  1. expression !~ regexpr 如果 expression 的字符串 不包含 一段能够被 regexpr 匹配的子字符串,则该模式被匹配。

下面是一些例子

# 匹配所有第4个字段包含Asia的输入行
$4 ~ /Asia/
# 匹配所有第4个字段不包含Asia的输入行
$4 !~ /Asia/
# 匹配包含 : 字符的输入行(在自己的lunarvim dap配置中用到)
$0 ~ /:/
# 正行匹配的话,上面的就相当于下面的匹配
/:/
  1. 正则表达式

正则表达式具体匹配就不说了,下面列出了一些元字符。即有特殊含义的字符。

\ ^ . [ ] | ( ) * + ?

在需要匹配元字符自身的情况下需要在前面加上反斜线进行转义。 一组 [] (方括号) 包围的的字符组成的正则表达式称为 字符类(character class) 。这个表达式匹配字符类中的任意一个字符。

eg: [ABC]: 匹配 A 或 B 或 C [A-Z]: 匹配 A 到 Z 的任意一个字母

一个互补的字符类在 [ 之后以 ^ 开始。 eg: [ ^ABC]: 匹配任意一个字符,除了 A 或 B 或 C

在一个字符类中,所有的字符都具有它自身的字面意义 除了反斜线 \,互补类字符开头的 ^,以及两个字符间的 -

在正则表达式中,选择运算符 | 的优先级最低,然后是拼接运算,最后是重复运算符 * + 与 ? 。与算术表达式的规则一样,优先级高的运算符优先处理。

  1. 复合模式

这里说的复合模式其实是使用逻辑运算符(!,&&,|| [优先级从高到低,并且具有惰性求值的特性])合并模式 eg: $4 == "Asia" && $3 > 500

  1. 范围模式

一个范围模式由两个被逗号分开的模式组成,如: pat1,pat2

一个范围模式匹配多个输入行, 这些输入行从匹配 pat1 的行开始, 到匹配 pat2 的行结束, 包括这 两行; pat2 可以与 pat1 匹配到同一行, 这时候模式的范围大小就退化到了一行。

一旦范围的第一个模式匹配到了某个输入行, 那么整个范围模式的匹配就开始了; 如果范围 模式的第二个模式一直都没有匹配到某个输入行, 那么范围模式会一直匹配到输入结束

eg: 匹配的行从包含 Canada 的行开始,到包含 Asia 的行

'/Canada/,/Asia/'

打印程序前五行

awk 'NR == 1, NR == 5 { print $0 }' countries

等价的写法为

awk 'NR <= 5 { print $0 }' countries

动作(Action)

动作即在对应模式得到匹配之后需要执行的语句。如果语句有多条,语句之间用换行符或者分号隔开。动作中可以包括下列语句或关键词。

expression,包括常量,变量,赋值,函数调用等等.

print expression-list

printf(format, expression-list)

if (expression) statements

if (expression) statements else statements

while (expression) statements

for (expression; expression; expression) statements

for (expression in array) statements

do statements while (expression)

break

continue

awk 中的内建变量

变量

意义

默认值

ARGC

命令行参数的个数

-

ARGV

命令行参数数组

-

FILENAME

当前输入文件名

-

FNR

当前输入文件的记录个数

-

FS

控制着输入行的字段分割符

" "

NF

当前记录的字段个数

-

NR

到目前为止读取的记录数量

-

OFMT

数值的输出格式

"%.6g"

OFS

输出字段分割符

" "

ORS

输出的记录分割符

"\n"

RLENGTH

被 match 匹配的字符串的长度

-

RS

控制这输入行的记录分割符

"\n"

RSTART

被函数 match 匹配的字符串的开始

SUBSEP

下标分割符

"\034"

  1. 运算符

一言蔽之,和 C 语言基本相同,甚至++,-- 等操作,前缀后缀也相同。 awk 也可以使用 ?: 三目运算符。awk 也能使用 in 关键字在数组成员上,例如 i in a 表示如果 a[i]存在则为 1 否则为 0。

  1. 内建算术函数

函数

返回值

atan2(y, x)

y/x 的反正切值,定义域在-pi(符号 pi)到 pi 之间

cos(x)

x 的余弦值,x以弧度为单位

exp(x)

x 的指数函数,e x

int(x)

x 的整数部分;当 x>0 时,向 0 取整

log(x)

x 的自然对数(以 e 为底)

rand()

返回一个随机数 r,0 <= r < 1

sin(x)

x 的正弦值,x以弧度为单位

sqrt(x)

x 的方根

srand(x)

x 是 rand()的新的随机数种子

  1. 字符串运算符

awk 中只有一种字符串运算符——拼接。 拼接并没有显式的运算符,通过陆续写出字符串常量,变量,数组元素,函数返回值,与其他表达式,就可以创建一个字符串。

  1. 用作正则表达式的字符串

下面是一个例子:

BEGIN { digits = "^[0-9]+$" }
$2 ~ digits

上面的语句会将第二个字段有且仅有数字的行打印出来。

在一个匹配表达中, 一个被双引号包围的字符串, 可以与一个被斜杠包围的正则 达式互换着使用,例如 "^[0-9]+$" 与 /^[0-9]+$/。

然而有一个例外, 如果被双引号包围的字符串想要匹配一个正则表达式元字符的字面值, 那就要在反斜杠的前面再加一个反斜杠来保护它。 所以$0 ~ /(\+|-)[0-9]+/与$0 ~ "(\\+|-)[0-9]+"等价。

  1. 内建字符串函数

函数

描述

gsub(r, s)

将$0 中所有出现的 r 替换为 s,返回替换的次数

gsub(r, s, t)

将字符串 t 中所有出现的 r 替换为 s,返回替换的次数

index(s, t)

返回字符串 t 在 s 中第一次出现的位置,如果 t 没有出现返回 0

length(s)

返回 s 包含的字符个数

match(s, r)

测试 s 是否包含能被 r 匹配的子串,返回子串的起始位置或 0;设置 RSTART 与 RLENGTH

match(s, r, a)

测试 s 是否包含能被 r 匹配的最左边最长子串,并将结果存入数组 a,但是注意,这里需要在 r 中使用()将需要放入数组的匹配项目标记出来;

split(s, a)

用 FS 将 s 分割到数组 a 中,返回字段的个数

split(s, a, fs)

用 fs 将 s 分割到数组 a 中,返回字段的个数

sprintf(fmt, expr-list)

根据字符串 fmt 返回格式化后的 expr-list

sub(r, s)

将$0 的最左最长的,能被 r 匹配的字符串替换为 s,返回替换的次数

sub(r, s, t)

将 t 的最左最长的,能被 r 匹配的字符串替换为 s,返回替换的次数

substr(s, p)

返回 s 中位置从 p 开始的后缀

substr(s, p, n)

返回 s 中从位置 p 开始的,长度为 n 的子字符串

awk 中,未初始化的变量默认初始值为数值 0 或空字符串 ""。不存在的,或显式为空的字段具有字符串值 "",它们不是数值,但是当强制转换为数值时,将会是 0。

有两种惯用的语法可以将表达式从一种类型转换为另一种类型:

  1. number "" 将空字符串拼接到 number 可以将它强制转换为字符串。

  1. string + 0 给字符串加上零可以把它强制转换成数值。

eg: 字符串转为数值类型

awk 'BEGIN { print "1E2"+0, "12E" + 0, "E12" + 0, "1X2Y3" + 0 }' countries

结果为字符串中最长的看起来像数值的前缀的值。

数值的字符串形式需要根据 OFMT 转换后才会输出。

awk 'BEGIN { print 1E2 "", 12E-2 "", E12 "", 1.23456789 "" }' countries
  1. 流程语句

一言蔽之,基本与 c 语言相同,不同的是可以使用 next 关键字直接开始输入主循环的下一次迭代。exit 或者 exit expression 表示马上执行 END 动作;如果已经在 END 内,则退出程序,将 expression 作为程序退出的返回状态。

  1. 数组

awk 中的数组与变量一样不需要声明,但是下标是字符串类型。也由此 awk 数组被称为关联数组,即很多语言中的 map。

打印 Asia 与 Europe 的总人口

awk '/Asia/ { pop["Asia"] += $3 }
/Europe/ { pop["Europe"] += $3 }
END { print "Asian population is", pop["Asia"], "million."
    print "European population is", pop["Europe"], "million."
}' countries

删除数组中的元素可以使用 delete 关键字。

delete array["index"]

for 循环可以使用 in 来得到数组下标。

for (i in array)
    statements;

eg: 删除数组中的所有元素。

for (i in array)
    delete array[i]
  1. 多维数组

多维数组表示

Arr[i, j]

测试多维下标是否是某个数组成员

if ((i, j) in arr) ...
注意数组的元素不能是数组

用户自定义函数

自定义函数形式

function name(parameter-list) {
    statements
}

函数可以具有返回值,使用和大部分语言一样使用 return 返回值。注意 return 以及后面的值是可选的。 一个函数定义可以出现在任何 模式-动作 语句可以出现的地方。

eg: 计算参数最大值

function max(m, n) {
    return m > n ? m : n
}
注意,调用函数时,函数名与左括号之间不能有空白。函数传递的参数是实际传入值的拷贝,但是数组传递不是,可以在函数内修改数组内容。 函数中没有声明的变量都是全局变量,只有出现在函数参数列表中的变量才是局部的。

输出

输出可以使用 shell 内置的命令 print 以及 printf,使用方法基本和 c 语言的 printf 相同,print 也是直接打印字符串,不同的表达式用逗号隔开。

其中可以使用管道符和重定向符来将输出结果放入管道或者重定向到别的命令。 eg:

print expression,expression... >> filename
printf(format,expression,expression...) > filename

注意,当 print 后的 参数有运算关系时需要使用括号将表达式包住

可以使用 close(filename)或者 close(command),来断开 print 与 filename 或者 command 之间的连接。

system(command) 执行 command;函数返回值是 command 的退出状态。 eg:

BEGIN { res=system("ls -al")
  print res
}

此时 res 为 0,系统执行 ls -al 命令。

  1. 输出到文件

{ print($1, $3) > ($3 > 100 ? "bigpop" : "smallpop") }

此处文件名需要用双引号括起来,不然会被认为是变量。

与之前需要用括号括起来的规则进行对比,为了防止 > (重定向符号) 与 > (大于符号) 类似的情况发生歧义,所以在表达运算关系时需要在对应的表达式外侧加上括号。

eg: 将所有行输入到以第一个字段命名的文件中。

{ print > $1 }

表达重定向关系

{ print $1, $2 > $3 }

表达算术关系

{ print $1, ($2 > $3) }

输入

输入形式有如下 eg:

awk 'program' data

grep 'Asia' countries | awk 'program'

awk -f prog_filename data
  1. 输入分隔符

内建变量 FS 的默认值是 " ",也就是一个空格符。当 FS 具有这个特定值时,输入字段按照空格和 (或) 制表符分割,前导的空格与制表符会被丢弃。

如果字符串的长度多于一个字符,那么它会被当成一个正则表达式。当前输入行中,与该正则表达式匹配的最左,最长,非空且不重叠的子字符串变成字段分隔符。下面是一个例子。

BEGIN { FS=",[ \t]*|[ \t]+" }
  1. getline 函数

函数 getline 可以从当前输入行,或文件,或管道,读取输入。getline 抓取下一个记录,按照通常的方式把记录分割成一个个的字段。它会设置 NF,NR,和 FNR; 如果存在一个记录,返回 1,若遇到文件末尾,返回 0,发生错误时返回 -1 (例如打开文件失败)。

表达式 getline x 读取下一条记录到变量 x 中,并递增 NR 与 FNR,不会对记录进行分割,所以不会设置 NF。

从文件 file 读取输入。 它不会对 NR 与 FNR 产生影响, 但是会执行字段分割, 并且设置 NF。

getline <"file"

从 file 读取下一条记录, 存到变量 x 中。记录不会被分割成字段, 变量 NF, NR, 与 FNR 都不会被修改。

getline x <"file"

表达式

被设置的变量

getline

$0, NF, NR, FNR

getline var

var, NR, FNR

getline <file

$0, NF

getline var <file

var

cmd 管道符 getline

$0, NF

cmd 管道符 getline var

var

注意,这里的 cmd 命令需要使用双引号包起来。 eg: 统计一共有多少用户登录了系统(类 Unix 系统)

{ while("who" | getline)
        n++;
    print n;
}

注意: 使用重定向和 geline 来获取文件内容,可能会有文件不存在问题。例如下面的语句。

while (getline <"file") ...

当文件不存在时,getline 返回-1,但是只要是非 0 数都为 true,这将造成死循环!!!

  1. 命令行参数

命令行参数可以通常 awk 的内建数组 ARGV 来访问, 内建变量 ARGC 的值是参数的个数再加 1. 对于下面命令行

awk -f profile a v=1 b

ARGC 的值是 4, ARGV[0]含有 awk,ARGV[1]含有 a,ARGV[2]含有 v=1,ARGV[3]含有 b。ARGC 之所以会比参数的个数多 1,是因为命令的名字 awk 也被当作参数之一,存放在索引为 0 的位置,就像 C 程序那样。

与其他程序交互

使用 system 函数,也可以使用 awk 实现一些命令行工具。(后期找个工具实现一下)。

数据处理

列求和

{ for (i = 1; i <= NF; i++) {
    col[i] += $i
    # 目前为止,字段数的最大值
    if (NF > maxfld)
        maxfld = NF
}}
END { for (i = 1; i <= maxfld; i++) {
        printf("%g%s", col[i], i < maxfld ? "\t" : "\n")
    }

}

写一个程序,这个程序读取一个条目–数额对列表,对列表中的每一个条目,累加它的数额; 在结束时, 打印条目以及它的总数额,条目按照字母顺序排列。

awk 程序

{ itemm[$1] += $2
}
END { for(i in itemm) {
       	printf ("%s: %d\n", i, itemm[i]) | "sort"
    }
}

对应文件

haa 45
hbb 23
hcc 78
haa 89
hcc 90
hbb 2

运行

awk -f 3_4.awk 3_4.data

# 运行结果
# haa 134
# hbb  25
# hcc 168

给每三个数字后添加逗号,检查逗号是否放置正确。

data:

0
-1
-12.34
12345
-1234567.89
-123.
-123456

program:


{ printf("%-12s %20s %20s\n", $0, addcomma($0), sumcomma($0) ? "true" : "false") }
function addcomma(x, num) {
	if (x < 0)
		return "-" addcomma(-x)
	num = sprintf("%.2f", x)
	# num is dddddd.dd
	while (num ~ /[0-9]{4}/)
    # 注意此处 & 符号表示之前匹配的内容
		sub(/[0-9]{3}[,.]/, ",&", num)
	return num
}

function sumcomma(num) {
	while (num ~ /,[0-9]{3},/)
		sub(/,[0-9]{4}/, "", num)
	if (num !~ /,/)
		return 1
	return 0
}

注意上面的 ",&",&表示之前匹配到的字符。

运行结果

awk -f addcomma.awk addcomma.data

#           0            0.0  true
#          -1           -1.0  true
#      -12.34         -12.34  true
#       12345      12,345.00  true
# -1234567.89  -1,234,567.89  true
#        -123         -123.0  true
#     -123456    -123,456.00  true

总结

看似awk是一个处理文本的命令行,实则是一门语言。它给终端下文本的处理等带来了极大的便利。得找个时间干点什么哈哈哈,继续玩玩awk,但是目前的awk学习与使用就暂且告一段落啦。

(顺带说一句,Org-Mode确实不赖,就是markdown导出一般般)

加油哇!-- 来自一位Linux爱好者。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值