awk使用

awk 是一个功能强大的文本处理工具,广泛用于在命令行环境下进行复杂的数据筛选和报告生成。它特别适用于处理结构化文本数据,如日志文件、CSV 文件等。

以下是 awk 格式及其各个组成部分的详解:

一、awk 的基本结构

awk 的基本语法结构如下:

awk 'pattern { action }' input_file
  • pattern(模式):用于匹配输入数据的条件,可以是正则表达式或逻辑表达式。
  • action(动作):匹配到模式的行要执行的操作,通常是打印、修改或计算。

awk 处理每一行输入时,它会检查该行是否匹配模式。如果匹配,则执行动作;否则,跳过该行。

二、awk 的工作流程

  1. 输入分割awk 默认以空白字符(空格或制表符)将每一行分割成多个字段,存储在变量 $1, $2, …, $NF 中。$0 表示整行内容。
  2. 模式匹配:对于每一行,awk 检查是否匹配给定的模式。
  3. 执行动作:如果匹配,则执行与模式相关联的动作。

三、awk 的基本概念

1. 字段和记录

  • 字段(Field):一行文本中的单个数据单元,由分隔符(默认是空白字符)分隔。例如,在 Dec 30 23:50:56 中,Dec$130$223:50:56$3
  • 记录(Record)awk 处理的每一行文本,默认以换行符分割。

2. 内置变量

  • $0:整行文本。
  • $1, $2, …, $NF:行中的第 1、2、…、最后一个字段。
  • NR:已处理的记录(行)数。
  • NF:当前记录的字段数。
  • FS:字段分隔符,默认是空白字符。
  • OFS:输出字段分隔符,默认也是空白字符。
  • RS:记录分隔符,默认是换行符。
  • ORS:输出记录分隔符,默认也是换行符。

3. 操作符和表达式

  • 算术运算符+, -, *, /, %
  • 关系运算符==, !=, <, >, <=, >=
  • 逻辑运算符&&(与),||(或),!(非)

4. 控制结构

  • 条件语句if, else
  • 循环语句for, while, do-while
  • 内置函数split, substr, length, toupper, tolower

四、awk 的详细格式和用法

1. 单行 awk 命令

单行 awk 命令通常用于简单的模式匹配和操作。例如,打印所有包含 "ERROR" 的行:

awk '/ERROR/ { print $0 }' logfile.log

解释

  • /ERROR/:模式,匹配包含 "ERROR" 的行。
  • { print $0 }:动作,打印整行内容。

2. 带条件的 awk 语句

结合多个条件进行筛选。例如,匹配月份为 Dec 并且日期为 30 的行:

awk '$1 == "Dec" && $2 == "30" { print $0 }' logfile.log

解释

  • $1 == "Dec" && $2 == "30":模式,匹配第一个字段为 Dec 且第二个字段为 30 的行。
  • { print $0 }:动作,打印整行内容。

3. 使用内置函数

例如,提取时间部分并进行比较:

awk '$1 == "Dec" && $2 == "30" {
    split($3, t, ":")
    time = t[1] ":" t[2]
    if (time >= "06:20" && time <= "10:30") {
        print $0
    }
}' logfile.log

解释

  • split($3, t, ":"):将第三个字段(假设是时间,如 23:50:56)按 : 分割,存入数组 t
  • time = t[1] ":" t[2]:组合小时和分钟,形成 HH:MM 格式的时间字符串。
  • if (time >= "06:20" && time <= "10:30"):检查时间是否在指定范围内。
  • print $0:如果条件满足,则打印当前行。

4. 多行 awk 脚本

对于复杂的 awk 脚本,通常需要多行编写。可以使用反斜杠 \ 来续行,或将脚本写入一个独立的 .awk 文件。

方法一:使用反斜杠续行
awk '$1 == "Dec" && $2 == "30" { \
    split($3, t, ":"); \
    time = t[1] ":" t[2]; \
    if (time >= "06:20" && time <= "10:30") { \
        print $0 \
    } \
}' logfile.log > filtered.log

注意

  • 每一行的末尾使用 \ 表示续行。
  • 确保 \ 后没有额外的空格。
方法二:使用 .awk 脚本文件
  1. 创建 filter_logs.awk 文件

    $1 == "Dec" && $2 == "30" {
        split($3, t, ":")
        time = t[1] ":" t[2]
        if (time >= "06:20" && time <= "10:30") {
            print $0
        }
    }
    
  2. 执行 awk 脚本

    awk -f filter_logs.awk logfile.log > filtered.log
    

解释

  • -f filter_logs.awk:指定要执行的 awk 脚本文件。

5. 使用 BEGINEND

BEGIN 块在处理任何输入行之前执行,常用于初始化操作。END 块在所有输入行处理完毕后执行,常用于总结或输出结果。

示例:统计匹配行的总数:

awk 'BEGIN { count=0 }
    $1 == "Dec" && $2 == "30" {
        split($3, t, ":")
        time = t[1] ":" t[2]
        if (time >= "06:20" && time <= "10:30") {
            count++
            print $0
        }
    }
    END { print "Total matched lines:", count }' logfile.log > filtered.log

解释

  • BEGIN { count=0 }:初始化计数器。
  • 中间部分:筛选并计数。
  • END { print "Total matched lines:", count }:输出总计数。

五、awk 常用内置函数

1. split

用于将字符串按指定分隔符拆分成数组。

语法

split(string, array, separator)

示例

split($3, t, ":")

将第三字段按 : 分割,存储在数组 t 中。

2. substr

用于提取字符串的子串。

语法

substr(string, start, length)
  • string:目标字符串。
  • start:起始位置(1-based)。
  • length:(可选)子串长度。

示例

substr($3, 1, 5)

提取第三字段的前 5 个字符。

3. length

返回字符串的长度。

语法

length(string)

示例

len = length($3)

获取第三字段的长度。

4. touppertolower

将字符串转换为大写或小写。

语法

toupper(string)
tolower(string)

示例

month = toupper($1)

将第一个字段转换为大写字母。

六、awk 示例详解

示例1:过滤特定日期和时间范围的日志

假设日志文件内容如下:

Dec 30 05:50:56 core-1 sudo: Command1
Dec 30 06:25:45 core-1 sudo: Command2
Dec 30 09:00:00 core-1 sudo: Command3
Dec 30 10:31:10 core-1 sudo: Command4
Dec 29 08:00:00 core-1 sudo: Command5

目标:过滤出 Dec 30 日期,时间在 06:2010:30 之间的日志行。

命令

awk '$1 == "Dec" && $2 == "30" {
    split($3, t, ":")
    time = t[1] ":" t[2]
    if (time >= "06:20" && time <= "10:30") {
        print $0
    }
}' logfile.log > filtered.log

执行结果(filtered.log):

Dec 30 06:25:45 core-1 sudo: Command2
Dec 30 09:00:00 core-1 sudo: Command3

示例2:统计特定条件下的字段总和

假设日志中的某个字段表示数值,需统计符合条件的数值总和。

日志内容

Dec 30 06:25:45 core-1 sudo: Value=100
Dec 30 09:00:00 core-1 sudo: Value=200
Dec 30 10:31:10 core-1 sudo: Value=300

命令

awk '$1 == "Dec" && $2 == "30" {
    split($0, a, "Value=")
    if (length(a) > 1) {
        sum += a[2]
    }
}
END { print "Total Value:", sum }' logfile.log

输出

Total Value: 600

解释

  • split($0, a, "Value="):按 Value= 分割整行,数值部分存放在 a[2]
  • sum += a[2]:累加数值到 sum 变量。
  • END { print "Total Value:", sum }:输出总和。

示例3:替换文件中的特定模式

将日志中的某个词替换为其他词,例如将 "sudo" 替换为 "ADMIN"

命令

awk '{ gsub(/sudo/, "ADMIN"); print }' logfile.log > modified.log

解释

  • gsub(/sudo/, "ADMIN"):将当前行中的所有 "sudo" 替换为 "ADMIN"
  • print:打印替换后的行。

七、awk 的高级用法

1. 使用自定义字段分隔符

默认情况下,awk 使用空白字符分隔字段。可以通过 -F 选项指定自定义分隔符。

示例:使用逗号作为字段分隔符(例如处理 CSV 文件)。

awk -F ',' '{ print $1, $2 }' file.csv

解释

  • -F ',':指定逗号 , 为字段分隔符。

2. 打印特定字段

仅打印某些字段而非整行内容。

示例:打印第 1 和第 3 字段。

awk '{ print $1, $3 }' logfile.log

3. 条件语句和循环

结合 if 语句和循环进行复杂的操作。

示例:打印包含特定关键字的行,并统计其出现次数。

awk '/ERROR/ { 
    print $0
    count++
}
END { print "Total ERROR lines:", count }' logfile.log

4. 自定义输出字段分隔符

使用 OFS 变量自定义输出时的字段分隔符。

示例:将输出字段用逗号分隔。

awk 'BEGIN { OFS="," }
    { print $1, $2, $3 }' logfile.log > output.csv

八、 调试 awk 脚本

在编写复杂的 awk 脚本时,可能需要逐步调试以确保正确性。以下是一些调试技巧:

1. 打印中间变量

在脚本中添加 print 语句以检查变量值。

示例

awk '$1 == "Dec" && $2 == "30" {
    split($3, t, ":")
    time = t[1] ":" t[2]
    print "Extracted time:", time
    if (time >= "06:20" && time <= "10:30") {
        print $0
    }
}' logfile.log

2. 使用条件断点

只在特定条件下执行调试信息。

示例

awk '$1 == "Dec" && $2 == "30" && $3 == "09:00:00" {
    print "Debug: Found specific time at line:", NR
    print $0
}

九、 处理多种日志格式

日志文件的格式可能多种多样,以下是一些常见的日志格式处理方法:

1. 带有年份的日志格式

示例日志

2024 Dec 30 06:25:45 core-1 sudo: Command2

过滤命令

$1 == "2024" && $2 == "Dec" && $3 == "30" {
    split($4, t, ":")
    time = t[1] ":" t[2]
    if (time >= "06:20" && time <= "10:30") {
        print $0
    }
}' logfile.log > filtered.log

解释

  • 这里字段顺序不同,需要根据实际情况调整字段索引。

2. 使用不同的时间格式

示例日志

Dec 30 06:25:45.123 core-1 sudo: Command2

处理带有毫秒的时间

$1 == "Dec" && $2 == "30" {
    split($3, t, ":")
    time = t[1] ":" t[2]
    if (time >= "06:20" && time <= "10:30") {
        print $0
    }
}' logfile.log > filtered.log

注意

  • 即使时间包含毫秒,提取前两部分 (HH:MM) 进行比较依然有效。

十、 综合实例

结合之前的内容,以下是一个较为综合的 awk 脚本示例,用于过滤特定日期和时间范围的日志,并输出指定字段。

目标

  • 筛选出 Dec 30 日期,时间在 06:2010:30 之间的日志行。
  • 打印主机名和命令部分。

日志示例

Dec 30 06:25:45 core-1 sudo: Command2
Dec 30 09:00:00 core-1 sudo: Command3
Dec 30 10:31:10 core-1 sudo: Command4
Dec 29 08:00:00 core-1 sudo: Command5

命令

awk '
$1 == "Dec" && $2 == "30" {
    split($3, t, ":")
    time = t[1] ":" t[2]
    if (time >= "06:20" && time <= "10:30") {
        # 假设主机名在第4个字段,命令在第6个字段之后
        host = $4
        # 提取命令部分(从第6个字段到行尾)
        command = ""
        for (i = 6; i <= NF; i++) {
            command = command $i " "
        }
        # 去除末尾的空格
        sub(/ $/, "", command)
        print "Host:", host, "| Command:", command
    }
}' logfile.log > filtered.log

输出(filtered.log):

Host: core-1 | Command: sudo: Command2 
Host: core-1 | Command: sudo: Command3 

解释

  • split($3, t, ":"):分割时间字段,提取小时和分钟。
  • 时间范围判断。
  • 提取主机名和命令部分:
    • 主机名假设在第4个字段(具体根据日志格式调整)。
    • 命令部分从第6个字段开始,使用循环拼接。

好的,我来用一个详细的逐步案例,把整个 awk 转换流程拆解地更细致,更通俗好懂,不仅包含“处理每列的原理”,还会让你每一步都能与实际输入、输出一一对应!


数据示例(原始数据)

假设你有个表格(可能是 Excel 另存为 tsv/文本),内容如下(用制表符\t做分隔):

数据库名	表名
db1	tb11 tb12 tb13
	tb14	   tb15
db2	"tb21	tb22"
db3	'tb31    tb32'
	db33
db4	
db5	tb51

数据拆解说明:

  • 第一行为表头
  • 有的库的表名在一行内写了多个(空格、tab混合)
  • 有的数据库名单元格为空(实际是继承上一行)
  • 有的表名带有双引号、单引号或windows回车
  • 有的行数据库名有,但表名为空,需跳过

awk 脚本拆解详案

#!/usr/bin/env bash
set -euo pipefail

SRC_FILE=${1:?请指定源 .xlsx 文件}
DST_FILE=${2:-"${SRC_FILE%.*}_一对一.tsv"}
SHEET_IDX=${3:-1}

TMP_TSV=$(mktemp)

# --- 1. xlsx ➜ TSV (内部换行 → 空格)---------------------------
python3 -m xlsx2csv --no-line-breaks -s "$SHEET_IDX" \
       -d $'\t' "$SRC_FILE" >"$TMP_TSV"

# --- 2. 拆分一对多 --------------------------------------------
awk -F'\t' -v OFS='\t' '
NR==1 { print; next }                         # 表头
{
  db=$1
  if (db!="") current_db=db                  # 记录当前数据库
  tbl=$2
  if (tbl=="") next                          # 空行跳过

  gsub(/\r/,"",tbl)                          # 去 \r
  gsub(/^["'\'']|["'\'']$/,"",tbl)           # 去首尾引号

  # 把「一个或多个空白」作为分隔符(兼容空格 / 制表符)
  n=split(tbl,arr,/[[:space:]]+/)
  for(i=1;i<=n;i++)
    if(arr[i]!="") print current_db,arr[i]
}
' "$TMP_TSV" >"$DST_FILE"

rm -f "$TMP_TSV"
echo "✅ 完成!结果行数: $(wc -l <"$DST_FILE"),文件: $DST_FILE"

运行:

[root@node4 ~/工具]# sh 1.sh 工作簿1.xlsx 工作簿1_一对一.csv 1
✅ 完成!结果行数: 6426,文件: 工作簿1_一对一.csv

1. 设置输入输出格式

awk -F'\t' -v OFS='\t' '...'
  • 设置输入的字段为tab分隔,print每列输出时也自动用tab分隔
  • 保证与Excel等数据工具兼容

2. 保留表头

NR==1 { print; next }
  • 处理到第一行时,直接输出(因为是“数据库名 表名”),不做下面的数据处理
  • 只影响表头,不影响正文

打印后光标停在第二行


3. 数据库名变量记录和继承

db = $1
if(db != "") current_db = db
  • 每处理一行,如果第一列(库名)不为空,就用当前行库名更新变量current_db
  • 如果本行库名为空,current_db就继续用上一行的
  • 这样兼容Excel合并单元格导出,能让tb14、tb15这些没填库名的行全归属于db1
举例说明:
$1current_db
1db1db1
2db1 (继承)
3db2db2
4db3db3
5db3 (继承)

4. 表名为空直接跳过本行

tbl = $2
if(tbl == "") next
  • 当表名这一列没写,整行不处理(有些数据库没有表)

5. 清理脏数据

gsub(/\r/, "", tbl)                               # 去掉回车符
gsub(/^["'\'']|["'\'']$/, "", tbl)               # 去掉首尾单双引号
  • 有可能表名"tb21"这种带引号
  • 有可能Windows拷贝的数据结尾有\r和\n(awk在linux下要清理\r)
例子:
原内容清理后
“tb21 tb22”tb21 tb22
‘tb31 tb32’tb31 tb32
tb51\rtb51

6. 拆分一行多个表名

n = split(tbl, arr, /[[:space:]]+/)
  • 一个及以上任意空白字符(空格、tab、全角空格等)做分隔,把tbl拆成数组arr
  • n为分割后实际表数
  • 也能移除多余的间隔
例子:
tblarr拆解n
tb11 tb12 tb13arr[1]=tb11 arr[2]=tb12 arr[3]=tb133
tb31 tb32arr[1]=tb31 arr[2]=tb322
tb21 tab22arr[1]=tb21 arr[2]=tb222
tb33arr[1]=tb331

7. 最终输出每行表及其库名

for(i=1; i<=n; i++)
  if(arr[i]!="") print current_db, arr[i]
  • arr如果有空白元素不用(比如有俩tab,split会自动处理掉)
  • 逐个输出,每行为一个“库名+表名”,而不是一行多个表名!

整个处理流程举例(每行演示)

原数据处理步骤说明输出行
数据库名 表名首行NR=1,直接输出数据库名 表名
db1 tb11 tb12 tb13current_db=db1,tbl=tb11 tb12 tb13
处理拆分后为[tb11,tb12,tb13]
逐行输出
db1 tb11
db1 tb12
db1 tb13
tb14 tb15current_db继承db1,tbl=tb14 tb15
分割后[tb14,tb15]
逐行输出
db1 tb14
db1 tb15
db2 “tb21 tb22”去掉引号后tbl=tb21 tab22
current_db=db2,拆分[tb21,tb22]
db2 tb21
db2 tb22
db3 ‘tb31 tb32’去掉引号后tbl=tb31 tb32
current_db=db3,分割[tb31,tb32]
db3 tb31
db3 tb32
tb33current_db继承db3,tbl=tb33
只一项
输出
db3 tb33
db4tbl空,next跳过-
db5 tb51单一项,输出db5 tb51

最终输出内容

数据库名	表名
db1	tb11
db1	tb12
db1	tb13
db1	tb14
db1	tb15
db2	tb21
db2	tb22
db3	tb31
db3	tb32
db3	tb33
db5	tb51

总结与价值

  1. 兼容Excel/TSV类复杂整理表,库名不重复填写也没问题;
  2. 多表名挤在一行可一键拆分,彻底变"一对一成一行"模式;
  3. 清洗引号、回车、特殊空白符等,保证数据规整;
  4. 输出清爽可直接做数据库批量操作/脚本化处理/表格录入;
  5. 只需微调字段序号即可适配任意"数据库-表名"格式。

十一、 常见问题与解决方法

1. 多行 awk 脚本出现语法错误

问题
在命令行中直接输入多行 awk 脚本时,可能因为命令行解析器的限制而导致语法错误。

解决方法

  • 将脚本写成单行,用分号 ; 分隔各部分。
  • 使用续行符 \ 进行换行。
  • 将脚本写入独立的 .awk 文件,再使用 -f 选项执行。

示例

单行脚本:

awk '$1 == "Dec" && $2 == "30" { split($3, t, ":"); time = t[1] ":" t[2]; if (time >= "06:20" && time <= "10:30") print $0 }' logfile.log > filtered.log

续行符脚本: