如何用Awk在Linux中统计日志文件里每个IP地址的访问次数?

2026-04-29 02:022阅读0评论SEO问题
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计966个文字,预计阅读时间需要4分钟。

如何用Awk在Linux中统计日志文件里每个IP地址的访问次数?

直接使用`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 可能触发截断。

标签:Linux

本文共计966个文字,预计阅读时间需要4分钟。

如何用Awk在Linux中统计日志文件里每个IP地址的访问次数?

直接使用`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 可能触发截断。

标签:Linux