Unix/Linux 的 awk 指令使用(下)
本文是 awk 指令教學的下篇,範例 log.txt
請見上篇。
如果有指令不能執行,請安裝 gnu 版本的 awk,macos 請使用
brew install gawk
安裝並改為 gawk。
📘 AWK 中階篇 2
與 shell 命令結合
AWK 可以使用 system()
函數執行 shell 命令,也可通過 |&
與外部程序交互。
在 AWK 中執行系統命令:
awk '{
command = "echo " $1 " | grep -o \"[0-9]\\+\"";
while (command | getline result) {
print "IP數字部分:", result
}
close(command)
}' log.txt | head -3
輸出:
IP數字部分: 192
IP數字部分: 168
IP數字部分: 0
將 AWK 處理結果存入檔案:
awk 'BEGIN {system("echo 日誌分析報告 > report.txt")}
$9 == "403" {system("echo 發現禁止訪問: " $1 " 嘗試訪問 " $7 " >> report.txt")}
END {system("echo 分析完成 >> report.txt; cat report.txt")}' log.txt
輸出:
日誌分析報告
發現禁止訪問: 172.16.4.5 嘗試訪問 /api/user/321
分析完成
使用 date 命令格式化日期:
awk '{
split($4, dt, "[:+/]");
cmd = "date -d \"" dt[2] "/" dt[3] "/" dt[4] " " dt[5] ":" dt[6] ":" dt[7] "\" +\"%Y-%m-%d %H:%M:%S\"";
cmd | getline formatted_date;
close(cmd);
print "格式化日期:", formatted_date, "IP:", $1
}' log.txt | head -2
字串函數
AWK 提供了豐富的字串處理函數:
length(str)
:返回字串長度index(str, substr)
:返回子字串在字串中的位置substr(str, pos, len)
:從字串中提取子字串gsub(regexp, replacement, target)
:全局替換sub(regexp, replacement, target)
:替換第一個匹配match(str, regexp)
:測試字串是否匹配正則表達式split(str, arr, delimiter)
:拆分字串到陣列
提取 HTTP 方法(去除引號):
awk '{
method = $6;
gsub(/\"/, "", method);
print "HTTP方法:", method;
}' log.txt
輸出:
HTTP方法: GET
HTTP方法: GET
HTTP方法: POST
HTTP方法: GET
HTTP方法: DELETE
HTTP方法: POST
使用 substr 提取日期部分:
awk '{
date_str = $4;
date_only = substr(date_str, 2, 10);
print "日期:", date_only;
}' log.txt
輸出:
日期: 17/May/202
日期: 17/May/202
日期: 17/May/202
日期: 17/May/202
日期: 17/May/202
日期: 18/May/202
分割 URL 路徑:
awk '{
url = $7;
count = split(url, parts, "/");
if (count > 1) {
print "URL第一段:", parts[1] ? parts[1] : "(空)";
print "URL第二段:", parts[2] ? parts[2] : "(空)";
}
}' log.txt
數學函數
AWK 提供了許多數學函數:
int(x)
:取整數部分sqrt(x)
:平方根exp(x)
:指數log(x)
:自然對數sin(x)
,cos(x)
,atan2(y,x)
:三角函數rand()
:0到1之間的隨機數srand(x)
:設置隨機數種子
計算響應大小的統計信息:
awk 'BEGIN {min=999999; max=0; total=0}
{
size = $10 + 0; # 轉換為數字
total += size;
if (size < min && size > 0) min = size;
if (size > max) max = size;
}
END {
avg = int(total/NR);
print "最小響應:", min, "位元組";
print "最大響應:", max, "位元組";
print "平均響應:", avg, "位元組";
print "總響應:", total, "位元組";
}' log.txt
輸出:
最小響應: 423 位元組
最大響應: 9832 位元組
平均響應: 2874 位元組
總響應: 17244 位元組
對數值進行四捨五入:
awk '{
kb = $10 / 1024;
rounded_kb = int(kb * 100 + 0.5) / 100;
if ($10 > 0) {
print $7, "大小:", rounded_kb, "KB";
}
}' log.txt
自定義函數
AWK 允許定義自己的函數,增強程式的模組性和可讀性:
function 函數名(參數1, 參數2, ...) {
語句;
return 返回值;
}
定義函數解析 HTTP 狀態碼:
awk '
function status_desc(code) {
if (code >= 200 && code < 300) return "成功";
else if (code >= 300 && code < 400) return "重定向";
else if (code >= 400 && code < 500) return "客戶端錯誤";
else if (code >= 500) return "伺服器錯誤";
else return "未知";
}
{
status = $9;
desc = status_desc(status);
print "請求:", $7, "狀態:", status, "-", desc;
}' log.txt
輸出:
請求: / 狀態: 200 - 成功
請求: /dashboard 狀態: 200 - 成功
請求: /login 狀態: 302 - 重定向
請求: /assets/css/style.css 狀態: 304 - 重定向
請求: /api/user/321 狀態: 403 - 客戶端錯誤
請求: /api/upload 狀態: 201 - 成功
計算兩個時間點之間的差值(秒):
awk '
function time_to_seconds(time_str) {
split(time_str, t, ":");
return t[1] * 3600 + t[2] * 60 + t[3];
}
function extract_time(datetime) {
# 從 [17/May/2025:21:14:12 格式提取時間
match(datetime, /[0-9]+:[0-9]+:[0-9]+/);
return substr(datetime, RSTART, RLENGTH);
}
NR == 1 {first_time = extract_time($4)}
{
current_time = extract_time($4);
seconds_diff = time_to_seconds(current_time) - time_to_seconds(first_time);
if (seconds_diff >= 0) {
print "請求間隔:", seconds_diff, "秒";
} else {
# 跨天
print "請求間隔: 跨天訪問";
}
}' log.txt
📘 AWK 中階篇 3
陣列使用
AWK 中的陣列是動態的,無需事先聲明大小。陣列索引可以是數字,也可以是字串。
基本陣列使用:
awk 'BEGIN {
# 初始化陣列
methods[1] = "GET";
methods[2] = "POST";
methods[3] = "PUT";
methods[4] = "DELETE";
print "支援的 HTTP 方法:";
for (i = 1; i <= 4; i++) {
print i, methods[i];
}
}' log.txt
輸出:
支援的 HTTP 方法:
1 GET
2 POST
3 PUT
4 DELETE
從日誌中填充陣列:
awk '{
# 存儲每行的 HTTP 方法
gsub(/\"/, "", $6);
methods[NR] = $6;
# 存儲每行的狀態碼
codes[NR] = $9;
} END {
print "請求方法與狀態碼對照:";
for (i = 1; i <= NR; i++) {
print i, methods[i], codes[i];
}
}' log.txt
輸出:
請求方法與狀態碼對照:
1 GET 200
2 GET 200
3 POST 302
4 GET 304
5 DELETE 403
6 POST 201
使用陣列收集統計信息:
awk '{
# 計算每種狀態碼的數量
status_counts[$9]++;
} END {
print "各狀態碼出現次數:";
for (status in status_counts) {
print status, status_counts[status];
}
}' log.txt
輸出:
各狀態碼出現次數:
201 1
200 2
302 1
304 1
403 1
關聯陣列
AWK 的陣列實際上都是關聯陣列(associative array)或稱為哈希表(hash table),即索引可以是任何字串而非僅限於數字。
用 IP 地址作為索引:
awk '{
# 為每個 IP 地址計數
ip_counts[$1]++;
} END {
print "各 IP 地址請求次數:";
for (ip in ip_counts) {
print ip, ip_counts[ip];
}
}' log.txt
輸出:
各 IP 地址請求次數:
192.168.0.101 2
203.0.113.45 2
10.1.2.3 1
172.16.4.5 1
多維關聯陣列(使用複合索引):
awk '{
# 記錄每個 IP 地址的每種 HTTP 方法的請求次數
http_method = $6;
gsub(/\"/, "", http_method);
# 使用複合索引
ip_method_counts[$1 SUBSEP http_method]++;
} END {
print "IP地址和請求方法組合統計:";
for (combo in ip_method_counts) {
# 分解複合索引
split(combo, parts, SUBSEP);
ip = parts[1];
method = parts[2];
print ip, method, ip_method_counts[combo];
}
}' log.txt
輸出:
IP地址和請求方法組合統計:
192.168.0.101 GET 2
203.0.113.45 POST 2
10.1.2.3 GET 1
172.16.4.5 DELETE 1
使用關聯陣列統計 URL 訪問量:
awk '{
# 計算每個 URL 的訪問次數
urls[$7]++;
# 同時記錄每個 URL 的總響應大小
url_sizes[$7] += $10;
} END {
print "URL 訪問統計:";
print "URL\t次數\t總大小\t平均大小";
for (url in urls) {
avg_size = url_sizes[url] / urls[url];
printf "%s\t%d\t%d\t%.1f\n", url, urls[url], url_sizes[url], avg_size;
}
}' log.txt
多檔案處理
AWK 可以同時處理多個檔案,使用特殊變數 FILENAME
可以獲取當前處理的檔案名。
假設將日誌分割為兩個檔案:
# 先將日誌分割為兩個檔案以便演示
awk 'NR <= 3 {print > "access_part1.log"} NR > 3 {print > "access_part2.log"}' log.txt
# 然後處理這兩個檔案
awk '{
print FILENAME, "行", NR, ":", $1, $9;
# 每個檔案的行數單獨計數
file_lines[FILENAME]++;
} END {
print "各檔案行數:";
for (file in file_lines) {
print file, file_lines[file];
}
}' access_part1.log access_part2.log
輸出:
access_part1.log 行 1 : 192.168.0.101 200
access_part1.log 行 2 : 192.168.0.101 200
access_part1.log 行 3 : 203.0.113.45 302
access_part2.log 行 1 : 10.1.2.3 304
access_part2.log 行 2 : 172.16.4.5 403
access_part2.log 行 3 : 203.0.113.45 201
各檔案行數:
access_part1.log 3
access_part2.log 3
使用 FNR 和 NR 區分不同檔案中的記錄:
awk '{
# FNR 是每個檔案中的記錄號
# NR 是所有檔案的總記錄號
print "總行號:", NR, "檔案行號:", FNR, "檔案:", FILENAME, "IP:", $1;
} END {
print "總處理", NR, "行";
}' access_part1.log access_part2.log
輸出:
總行號: 1 檔案行號: 1 檔案: access_part1.log IP: 192.168.0.101
總行號: 2 檔案行號: 2 檔案: access_part1.log IP: 192.168.0.101
總行號: 3 檔案行號: 3 檔案: access_part1.log IP: 203.0.113.45
總行號: 4 檔案行號: 1 檔案: access_part2.log IP: 10.1.2.3
總行號: 5 檔案行號: 2 檔案: access_part2.log IP: 172.16.4.5
總行號: 6 檔案行號: 3 檔案: access_part2.log IP: 203.0.113.45
總處理 6 行
比較兩個檔案中的 IP 地址差異:
awk '
FILENAME == "access_part1.log" {ip_part1[$1] = 1}
FILENAME == "access_part2.log" {ip_part2[$1] = 1}
END {
print "僅在第一個檔案中出現的 IP:";
for (ip in ip_part1) {
if (!(ip in ip_part2)) print ip;
}
print "僅在第二個檔案中出現的 IP:";
for (ip in ip_part2) {
if (!(ip in ip_part1)) print ip;
}
print "同時出現在兩個檔案中的 IP:";
for (ip in ip_part1) {
if (ip in ip_part2) print ip;
}
}' access_part1.log access_part2.log
分割與合併操作
AWK 可以將輸入分割成多個檔案,或將多個檔案合併。
根據狀態碼將日誌分割為不同檔案:
awk '{
# 根據狀態碼將記錄分類到不同檔案
if ($9 >= 200 && $9 < 300) {
print > "2xx_success.log";
} else if ($9 >= 300 && $9 < 400) {
print > "3xx_redirect.log";
} else if ($9 >= 400 && $9 < 500) {
print > "4xx_client_error.log";
} else {
print > "other_status.log";
}
}' log.txt
# 檢查生成的檔案
awk 'END {print "2xx 成功請求數:", NR}' 2xx_success.log
awk 'END {print "3xx 重定向請求數:", NR}' 3xx_redirect.log
awk 'END {print "4xx 客戶端錯誤請求數:", NR}' 4xx_client_error.log
輸出:
2xx 成功請求數: 3
3xx 重定向請求數: 2
4xx 客戶端錯誤請求數: 1
按照 IP 地址分割日誌:
awk '{
# 為每個 IP 地址創建單獨的日誌檔案
print > $1 "_requests.log";
}' log.txt
# 檢查每個 IP 的日誌數量
awk 'END {print "192.168.0.101 請求數:", NR}' 192.168.0.101_requests.log
awk 'END {print "203.0.113.45 請求數:", NR}' 203.0.113.45_requests.log
輸出:
192.168.0.101 請求數: 2
203.0.113.45 請求數: 2
合併多個檔案並添加來源標記:
awk '{
print FILENAME ":", $0;
}' 2xx_success.log 3xx_redirect.log | head -3
輸出:
2xx_success.log: 192.168.0.101 - - [17/May/2025:21:14:12 +0000] "GET / HTTP/1.1" 200 1536 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" "-"
2xx_success.log: 192.168.0.101 - - [17/May/2025:21:16:44 +0000] "GET /dashboard HTTP/1.1" 200 4721 "https://example.com/" "Mozilla/5.0 (iPhone; CPU iPhone OS 16_4 like Mac OS X)" "-"
2xx_success.log: 203.0.113.45 - john [18/May/2025:09:01:11 +0000] "POST /api/upload HTTP/1.1" 201 9832 "https://example.com/upload" "curl/7.68.0" "-"
📘 分析 Nginx 日誌的實際案例
AWK 的進階字串處理能力讓複雜的文本轉換變得簡單。以下是一些更深入的技巧和應用。
解析日期格式
awk '
function parse_month(mon) {
months["Jan"] = 1; months["Feb"] = 2; months["Mar"] = 3;
months["Apr"] = 4; months["May"] = 5; months["Jun"] = 6;
months["Jul"] = 7; months["Aug"] = 8; months["Sep"] = 9;
months["Oct"] = 10; months["Nov"] = 11; months["Dec"] = 12;
return months[mon];
}
{
# 從 [17/May/2025:21:14:12 +0000] 提取日期時間
if (match($4, /\[([0-9]+)\/([A-Za-z]+)\/([0-9]+):([0-9]+):([0-9]+):([0-9]+)/, dt)) {
day = dt[1];
month = parse_month(dt[2]);
year = dt[3];
hour = dt[4];
minute = dt[5];
second = dt[6];
printf "ISO格式日期時間: %s-%02d-%02d %s:%s:%s\n",
year, month, day, hour, minute, second;
timestamp = mktime(sprintf("%s %02d %02d %s %s %s",
year, month, day, hour, minute, second));
print "Unix 時間戳:", timestamp;
}
}' log.txt | head -4
輸出:
ISO格式日期時間: 2025-05-17 21:14:12
Unix 時間戳: 1747516452
ISO格式日期時間: 2025-05-17 21:16:44
Unix 時間戳: 1747516604