Skip to main content

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

輸出排版

awk 'BEGIN {
# 報表標題
printf "%-15s %-23s %-7s %-25s %-6s %s\n",
"IP地址", "時間", "方法", "URL", "狀態", "大小";
printf "%s\n", "----------------------------------------------------------------------";
}

{
# 清理和格式化數據
gsub(/\"/, "", $6); # 移除引號
datetime = substr($4, 2) " " substr($5, 1, length($5)-1);

# 截斷長 URL
url = $7;
if (length(url) > 25) {
url = substr(url, 1, 22) "...";
}

# 輸出格式化行
printf "%-15s %-23s %-7s %-25s %-6s %s\n",
$1, datetime, $6, url, $9, $10;
}

END {
printf "%s\n", "----------------------------------------------------------------------";
printf "共處理 %d 條記錄\n", NR;
}' log.txt

輸出 HTML

AWK 的輸出格式化功能讓報表生成和數據呈現更加專業。

awk 'BEGIN {
print "<html><head><style>";
print "table { border-collapse: collapse; width: 100%; }";
print "th, td { padding: 8px; text-align: left; border: 1px solid #ddd; }";
print "th { background-color: #f2f2f2; }";
print "tr:nth-child(even) { background-color: #f9f9f9; }";
print "</style></head><body>";
print "<h2>Nginx 訪問日誌分析</h2>";
print "<table>";
print "<tr><th>IP</th><th>時間</th><th>方法</th><th>URL</th><th>狀態</th><th>大小</th></tr>";
}

{
# 清理數據
gsub(/\"/, "", $6); # 移除 HTTP 方法中的引號

# 格式化日期時間
datetime = substr($4, 2) " " substr($5, 1, length($5)-1);

print "<tr>";
print "<td>" $1 "</td>";
print "<td>" datetime "</td>";
print "<td>" $6 "</td>";
print "<td>" $7 "</td>";
print "<td>" $9 "</td>";
print "<td>" $10 "</td>";
print "</tr>";
}

END {
print "</table>";
print "<p>報表生成時間: " strftime("%Y-%m-%d %H:%M:%S") "</p>";
print "</body></html>";
}' log.txt > access_report.html

echo "已生成 HTML 報表:access_report.html"

輸出:

已生成 HTML 報表:access_report.html

輸出 CSV

生成 CSV 格式輸出:

awk 'BEGIN {
# 加入 UTF-8 BOM(Byte Order Mark)
printf "\xEF\xBB\xBF";

print "IP,時間,方法,URL,狀態碼,響應大小,用戶代理";
FS = " ";
}

{
# 清理字段
gsub(/\"/, "", $6); # 移除方法中的引號

# 提取日期時間並格式化
datetime = $4;
gsub(/[\[\]]/, "", datetime);

# 提取用戶代理
ua = "";
for (i = 12; i <= NF-1; i++) {
ua = ua (i > 12 ? " " : "") $i;
}
gsub(/\"/, "", ua); # 移除引號
gsub(/,/, ";", ua); # 替換逗號以避免 CSV 格式問題

# 輸出 CSV 行
printf "%s,%s,%s,%s,%s,%s,\"%s\"\n",
$1, datetime, $6, $7, $9, $10, ua;
}' log.txt > access_data.csv

echo "已生成 CSV 數據:access_data.csv"

輸出:

已生成 CSV 數據:access_data.csv

複雜資料處理

AWK 可以處理複雜的數據分析任務,包括統計、聚合和關聯分析。

按小時統計訪問量:

需要 gawk。

awk '{
# 從時間戳中提取小時
if (match($4, /:([0-9][0-9]):/, hour)) {
hours[hour[1]]++;
}
}

END {
print "每小時訪問統計:";
for (h in hours) {
printf "%s點: ", h;
for (i = 0; i < hours[h]; i++) printf "■";
printf " (%d)\n", hours[h];
}
}' log.txt

輸出:

每小時訪問統計:
21點: ■■■■■ (5)
09點: ■ (1)

訪問路徑分析:

需要 gawk。

awk '{
# 提取 URL 路徑的第一級目錄
url = $7;
if (url == "/") {
paths["首頁"]++;
} else if (match(url, /\/([^\/]+)/, path)) {
paths[path[1]]++;
}

# 記錄訪問順序
if (last_ip == $1) {
path_seq[path_count++] = url;
} else {
if (path_count > 0) {
print "用戶訪問路徑:";
for (i = 0; i < path_count; i++) {
printf "%s%s", path_seq[i], (i < path_count-1) ? " → " : "\n";
}
}
path_count = 0;
path_seq[path_count++] = url;
last_ip = $1;
}
}

END {
# 打印最後一個用戶的路徑
if (path_count > 0) {
print "用戶訪問路徑:";
for (i = 0; i < path_count; i++) {
printf "%s%s", path_seq[i], (i < path_count-1) ? " → " : "\n";
}
}

print "\n路徑訪問統計:";
for (p in paths) {
printf "%s: %d 次\n", p, paths[p];
}
}' log.txt

計算每個用戶的平均請求大小:

awk '{
# 累加每個 IP 的請求大小
ip_bytes[$1] += $10;
ip_count[$1]++;
}

END {
print "用戶平均請求大小:";
for (ip in ip_bytes) {
avg = ip_bytes[ip] / ip_count[ip];
printf "%s: 總 %d 位元組, %d 請求, 平均 %.1f 位元組\n",
ip, ip_bytes[ip], ip_count[ip], avg;
}
}' log.txt

輸出:

用戶平均請求大小:
192.168.0.101: 總 6257 位元組, 2 請求, 平均 3128.5 位元組
203.0.113.45: 總 10255 位元組, 2 請求, 平均 5127.5 位元組
10.1.2.3: 總 0 位元組, 1 請求, 平均 0.0 位元組
172.16.4.5: 總 732 位元組, 1 請求, 平均 732.0 位元組