← cd ../posts

文本流水线:cut / awk / sed / sort / uniq 全家桶

2026-06-20

这是 Linux 系列的第 7 篇。上一篇讲怎么"找到文件",这一篇讲"找到之后怎么处理"——shell 真正的杀手锏。

0. 一份样本数据

为方便练手,假设你有一份 access.log(Nginx 日志格式):

192.168.1.10 - - [22/Apr/2026:10:15:32 +0800] "GET /api/posts HTTP/1.1" 200 4521
192.168.1.10 - - [22/Apr/2026:10:15:33 +0800] "GET /static/logo.png HTTP/1.1" 200 12345
10.0.0.55    - - [22/Apr/2026:10:15:33 +0800] "POST /api/login HTTP/1.1" 401 89
192.168.1.10 - - [22/Apr/2026:10:15:34 +0800] "GET /api/posts/1 HTTP/1.1" 200 2341
...

下面所有命令都基于这种"每行一条记录,列用空格分"的数据。


1. cut:按列切,最简单的那个

# 按字符位置切
$ cut -c 1-7 file.txt       # 每行前 7 个字符

# 按分隔符切
$ cut -d ',' -f 2 data.csv         # 取第 2 列(逗号分隔)
$ cut -d ',' -f 1,3 data.csv       # 取第 1 和第 3 列
$ cut -d ',' -f 2- data.csv        # 从第 2 列到结尾

# 日志取 IP(默认分隔符其实是 Tab,要显式指定空格)
$ cut -d ' ' -f 1 access.log

cut -d ' ' 只把单个空格当分隔符。日志里两个空格连一起会切错——这种场景该上 awk


2. awk:行 + 列 + 表达式的瑞士军刀

awk 是一门小型编程语言(M. Aho, P. Weinberger, B. Kernighan 三个名字首字母),但 99% 的日常用法只是它的皮毛:

# 取第 N 列(awk 自动按空白切,多个空格当一个)
$ awk '{print $1}' access.log

# 多列
$ awk '{print $1, $7, $9}' access.log

# 改分隔符
$ awk -F ',' '{print $2}' data.csv         # CSV
$ awk -F ':' '{print $1}' /etc/passwd      # 用户名

# 加条件(过滤)
$ awk '$9 == 401' access.log              # 只看 401 行
$ awk '$9 >= 500' access.log              # 5xx 错误
$ awk '$10 > 10000' access.log            # body 大于 10KB

# 算总和(统计第 10 列总流量)
$ awk '{sum += $10} END {print sum}' access.log

# 算平均
$ awk '{sum += $10; count++} END {print sum/count}' access.log

# 计数(多少条记录)
$ awk 'END {print NR}' access.log         # NR = 行号变量

# 唯一值(不去重也能算)
$ awk '!seen[$1]++' access.log            # 第 1 列去重输出

awk 的核心:模式 + 动作

模式 { 动作 }
  • 模式空了 → 每行都跑动作
  • 动作空了 → 默认 print
  • 多个模式可堆叠
# 多条规则
$ awk '
  /ERROR/ { errors++ }
  /WARN/  { warnings++ }
  END     { print "errors:", errors, "warnings:", warnings }
' app.log

awk 5 个内置变量记下来就够用了

变量 含义
$0 整行
$1, $2, … 第 N 列
NF 当前行的列数($NF = 最后一列)
NR 当前是第几行
FS 输入分隔符(-F 设置)

3. sed:流式编辑器,主要用来替换

sed 99% 的用途是替换删行

# s/A/B/ 替换(默认每行只换第一个)
$ sed 's/old/new/' file.txt

# 加 g:每行所有出现都换
$ sed 's/old/new/g' file.txt

# 在原文件改(-i = in place)
$ sed -i 's/old/new/g' file.txt

# Mac 上 -i 要给个空字符串作备份后缀
$ sed -i '' 's/old/new/g' file.txt

# 按行号删
$ sed '5d' file.txt          # 删第 5 行
$ sed '1,10d' file.txt       # 删 1-10 行
$ sed '/^#/d' file.conf      # 删所有以 # 开头的行(去注释)

# 按行号打印
$ sed -n '5,15p' file.txt    # 只打印 5-15 行
$ sed -n '/ERROR/p' app.log  # 只打印含 ERROR 的(等于 grep)

# 多条规则
$ sed -e 's/foo/bar/g' -e 's/baz/qux/g' file.txt

# 替换里用捕获组
$ echo 'name=John' | sed 's/name=\(.*\)/hello \1/'
hello John

# 现代版(-E 用扩展正则,括号不用转义)
$ echo 'name=John' | sed -E 's/name=(.*)/hello \1/'

一个真实例子:把 nginx 日志里的 IP 全部替换成 X.X.X.X 脱敏

$ sed -E 's/^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/X.X.X.X/' access.log

4. sort + uniq:排序 + 去重 + 计数

这俩总是配合用——uniq 只会折叠相邻的重复行,所以前面一定要 sort 让相同的挨在一起:

# 基本去重
$ sort file.txt | uniq

# 计数(出现次数)
$ sort file.txt | uniq -c

# 只显示重复的
$ sort file.txt | uniq -d

# 只显示出现一次的
$ sort file.txt | uniq -u

# 按数字排序
$ sort -n file.txt          # 升序
$ sort -rn file.txt         # 降序

# 按某列排序
$ sort -k 3 -n file.txt     # 按第 3 列数字排
$ sort -k 3,3 -n            # 只按第 3 列(默认 k3 是"从第 3 列到结尾",区别明显)

# 按"人类大小"排序(K/M/G)
$ du -h /var/* | sort -hr

# 大文件排序(自动用磁盘做归并)
$ sort -S 1G big.log        # 给 1G 内存

5. 组合食谱:5 个 1 行小程序

① 访问最多的 5 个 IP

$ awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -5

② 各 HTTP 状态码分布

$ awk '{print $9}' access.log | sort | uniq -c | sort -rn
   12055 200
    1340 404
     234 500
      89 401

③ 每小时请求量

$ awk '{print substr($4, 14, 2)}' access.log | sort | uniq -c
   132 09
   589 10
   912 11
  1024 12

substr($4, 14, 2)[22/Apr/2026:10:15:32 的第 14 个字符开始取 2 个 = 10(小时)。

④ 哪些 URL 最慢(假设第 11 列是响应时长 ms)

$ awk '{print $11, $7}' access.log | sort -rn | head -10

⑤ 统计每个用户的登录失败次数

$ grep 'Failed password' /var/log/auth.log \
    | awk '{print $9}' \
    | sort | uniq -c | sort -rn

6. tr:字符级转换(轻量补充)

# 大小写转换
$ echo 'HELLO' | tr 'A-Z' 'a-z'        # hello

# 删字符
$ echo 'hello world' | tr -d ' '       # helloworld

# 压缩重复字符
$ echo 'aaabbb' | tr -s 'a'            # abbb (a 被压成一个)

# 换行符转空格(让多行变一行)
$ cat file | tr '\n' ' '

7. column:列对齐排版(输出更美)

最后处理完一堆数据,肉眼看会很挤。column -t 自动对齐:

$ ps aux | head -5 | column -t | head -5
USER  PID  %CPU  %MEM  VSZ      RSS    TTY    STAT  START  TIME  COMMAND
root  1    0.0   0.4   168568   12808  ?      Ss    Apr20  0:21  /sbin/init
root  2    0.0   0.0   0        0      ?      S     Apr20  0:00  [kthreadd]
...

肉眼立刻顺眼很多。


8. 一个综合实战:从访问日志生成 5 分钟概况

#!/bin/bash
LOG=/var/log/nginx/access.log

echo "=== 总请求数 ==="
wc -l < "$LOG"

echo
echo "=== Top 5 IP ==="
awk '{print $1}' "$LOG" | sort | uniq -c | sort -rn | head -5

echo
echo "=== 状态码分布 ==="
awk '{print $9}' "$LOG" | sort | uniq -c | sort -rn

echo
echo "=== Top 10 URL ==="
awk '{print $7}' "$LOG" | sort | uniq -c | sort -rn | head -10

echo
echo "=== 总流量 ==="
awk '{sum += $10} END {printf "%.2f MB\n", sum/1024/1024}' "$LOG"

25 行。换成 Python 至少 50 行还要 import。


9. 现在做一件事

在你自己的机器上拿一份真实文本(任意一个 log、CSV、history 输出),跑这 4 条:

# 1. 总行数
$ wc -l file

# 2. 最长的 5 行
$ awk '{print length, $0}' file | sort -rn | head -5

# 3. 出现最多的第一列
$ awk '{print $1}' file | sort | uniq -c | sort -rn | head

# 4. 改写后保存
$ sed 's/old/new/g' file > file.new

熟悉了节奏就可以告别 Excel + Python 的"杀鸡用牛刀"了。


下一篇redirection——> >> 2>&1 < <<< 这一堆符号到底什么意思,怎么 1 行里把 stdout / stderr / 文件 三方喂给不同地方。