如何用Awk在Linux中统计日志文件里每个IP地址的访问次数?
- 内容介绍
- 文章标签
- 相关推荐
本文共计966个文字,预计阅读时间需要4分钟。
直接使用`awk`命令,根据日志格式输出最频繁的IP地址:
更稳妥的做法是用正则匹配 IPv4 地址(忽略 IPv6):
awk 'match($0, /^[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}/) {print substr($0, RSTART, RLENGTH)}' access.log | sort | uniq -c | sort -nr
-
match()返回非零即匹配成功,RSTART/RLENGTH给出位置和长度 - 不依赖字段分割,避开因日志格式变动导致的
$1错位问题 - 该正则未校验数值范围(如 999.999.999.999),但真实日志中极少出现,权衡可读性与严格性
为什么不用 awk 内置计数而依赖 sort | uniq
awk 完全能自己计数:awk '{ip[$1]++} END{for (i in ip) print ip[i], i}' access.log | sort -nr。但实际中容易踩坑:
- 当 IP 数量超几万,
awk的关联数组在老版本(如 mawk 1.3.4)可能性能骤降,gawk 稍好但内存占用高 -
for (i in ip)遍历顺序不确定,必须靠sort -nr排序,没省掉管道 - 如果日志含注释行(如
#开头)、空行或 header,$1会是空或乱值,需加过滤:!/^#/ && NF>=1 {ip[$1]++}
处理带引号或混合格式的 Nginx 日志
Nginx 默认日志常把 IP 包在双引号里,例如:"192.168.1.1" - - [..]。此时 $1 是 "192.168.1.1",带引号,后续去重会把 "192.168.1.1" 和 192.168.1.1 当作两个 IP。
安全做法是先去引号再取字段:
awk '{$1 = gensub(/^"(.*)"$/, "\1", 1, $1); print $1}' access.log | sort | uniq -c | sort -nr
-
gensub()是 gawk 特有函数,mawk 不支持;若环境只有 mawk,改用sub(/^"/, "", $1); sub(/"$/, "", $1) - 不要写成
gsub(/"/, "", $0)—— 全局替换可能误删 URL 或 UA 中的引号 - 如果日志用 tab 分隔(
log_format显式指定),需加-F' '参数,否则空格分割会崩
统计时漏掉代理 IP 怎么办
真实场景中,X-Forwarded-For 头里的第一个 IP 才是真实客户端,而 $remote_addr 是上一跳代理。若日志已记录该头(如 log_format 含 $http_x_forwarded_for),别只盯着第一列。
假设日志格式为:$remote_addr - $http_x_forwarded_for [$time_local] ...,且 $http_x_forwarded_for 在第 3 字段:
awk '$3 != "-" && $3 != "" {split($3, a, ","); print a[1]} !($3 != "-" && $3 != "") {$1 = gensub(/^"(.*)"$/, "\1", 1, $1); print $1}' access.log | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' | sort | uniq -c | sort -nr
- 优先取
$3的第一个 IP(a[1]),但要防空值或- -
sed清理首尾空格,避免"1.2.3.4 "和"1.2.3.4"被算作不同 IP - 这种逻辑已超出单条
awk表达式舒适区,建议转成小脚本,否则可读性和调试成本陡增
实际跑起来,最易被忽略的是日志轮转和权限:确保 awk 读的是当前活跃日志(不是 access.log.1.gz),且用户有读取权限——尤其是用 sudo journalctl -u nginx 导出的日志,直接管道进 awk 前得确认编码是否为 UTF-8,否则中文 UA 可能触发截断。
本文共计966个文字,预计阅读时间需要4分钟。
直接使用`awk`命令,根据日志格式输出最频繁的IP地址:
更稳妥的做法是用正则匹配 IPv4 地址(忽略 IPv6):
awk 'match($0, /^[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}/) {print substr($0, RSTART, RLENGTH)}' access.log | sort | uniq -c | sort -nr
-
match()返回非零即匹配成功,RSTART/RLENGTH给出位置和长度 - 不依赖字段分割,避开因日志格式变动导致的
$1错位问题 - 该正则未校验数值范围(如 999.999.999.999),但真实日志中极少出现,权衡可读性与严格性
为什么不用 awk 内置计数而依赖 sort | uniq
awk 完全能自己计数:awk '{ip[$1]++} END{for (i in ip) print ip[i], i}' access.log | sort -nr。但实际中容易踩坑:
- 当 IP 数量超几万,
awk的关联数组在老版本(如 mawk 1.3.4)可能性能骤降,gawk 稍好但内存占用高 -
for (i in ip)遍历顺序不确定,必须靠sort -nr排序,没省掉管道 - 如果日志含注释行(如
#开头)、空行或 header,$1会是空或乱值,需加过滤:!/^#/ && NF>=1 {ip[$1]++}
处理带引号或混合格式的 Nginx 日志
Nginx 默认日志常把 IP 包在双引号里,例如:"192.168.1.1" - - [..]。此时 $1 是 "192.168.1.1",带引号,后续去重会把 "192.168.1.1" 和 192.168.1.1 当作两个 IP。
安全做法是先去引号再取字段:
awk '{$1 = gensub(/^"(.*)"$/, "\1", 1, $1); print $1}' access.log | sort | uniq -c | sort -nr
-
gensub()是 gawk 特有函数,mawk 不支持;若环境只有 mawk,改用sub(/^"/, "", $1); sub(/"$/, "", $1) - 不要写成
gsub(/"/, "", $0)—— 全局替换可能误删 URL 或 UA 中的引号 - 如果日志用 tab 分隔(
log_format显式指定),需加-F' '参数,否则空格分割会崩
统计时漏掉代理 IP 怎么办
真实场景中,X-Forwarded-For 头里的第一个 IP 才是真实客户端,而 $remote_addr 是上一跳代理。若日志已记录该头(如 log_format 含 $http_x_forwarded_for),别只盯着第一列。
假设日志格式为:$remote_addr - $http_x_forwarded_for [$time_local] ...,且 $http_x_forwarded_for 在第 3 字段:
awk '$3 != "-" && $3 != "" {split($3, a, ","); print a[1]} !($3 != "-" && $3 != "") {$1 = gensub(/^"(.*)"$/, "\1", 1, $1); print $1}' access.log | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' | sort | uniq -c | sort -nr
- 优先取
$3的第一个 IP(a[1]),但要防空值或- -
sed清理首尾空格,避免"1.2.3.4 "和"1.2.3.4"被算作不同 IP - 这种逻辑已超出单条
awk表达式舒适区,建议转成小脚本,否则可读性和调试成本陡增
实际跑起来,最易被忽略的是日志轮转和权限:确保 awk 读的是当前活跃日志(不是 access.log.1.gz),且用户有读取权限——尤其是用 sudo journalctl -u nginx 导出的日志,直接管道进 awk 前得确认编码是否为 UTF-8,否则中文 UA 可能触发截断。

