Chapter 04

文本处理

Linux 文本处理的核心工具——grep、sed、awk 的完整用法,管道与重定向的底层原理,以及组合工具解决实际问题的思路

核心概念:标准流与管道原理

理解 Linux 文本处理的基础,是理解标准流(Standard Streams)的概念。每个进程在启动时默认打开三个文件描述符,这是管道和重定向的底层基础。

stdin(标准输入,fd=0)
进程读取输入的默认来源。默认连接到终端键盘。可以用 < file 重定向为文件输入,或通过管道从另一个进程的 stdout 接收数据。grep、awk、sed 等命令既能接受文件参数,也能从 stdin 读取。
stdout(标准输出,fd=1)
进程写入正常输出的默认目标。默认连接到终端屏幕。用 > file 重定向到文件(覆盖),>> file 追加,或通过管道传给下一命令的 stdin。
stderr(标准错误,fd=2)
进程写入错误信息的独立通道。默认也显示在终端屏幕上,但与 stdout 独立,管道 | 默认不传递 stderr。可用 2>/dev/null 丢弃错误,2>&1 将 stderr 合并到 stdout。
管道(Pipe,|)
内核提供的进程间通信机制。cmd1 | cmd2 创建一个内核缓冲区,cmd1 的 stdout 写入缓冲区,cmd2 的 stdin 从缓冲区读取。两个命令并行执行,不是 cmd1 执行完才执行 cmd2。管道满了写端阻塞,管道空了读端阻塞。
流编辑器(Stream Editor)
按行处理文本流的程序类型。grep/sed/awk 都是流编辑器——它们逐行读取输入、处理、输出,不需要将整个文件加载到内存,因此可以处理任意大的文件。这是"一切皆文件"设计哲学的具体体现。
# 重定向操作符速查
cmd > file.txt           # stdout 重定向到文件(覆盖)
cmd >> file.txt          # stdout 重定向到文件(追加)
cmd < file.txt           # stdin 从文件读取
cmd 2> error.log         # stderr 重定向到文件
cmd 2>&1                 # stderr 合并到 stdout(顺序很重要!)
cmd > output.txt 2>&1   # stdout 和 stderr 都写入文件(常见写法)
cmd >& output.txt        # bash 简写:等同于 cmd > output.txt 2>&1
cmd 2>/dev/null          # 丢弃错误输出(/dev/null 是"黑洞")
cmd >/dev/null 2>&1      # 完全静默(stdout 和 stderr 都丢弃)

# Here Document:多行输入(不用写临时文件)
cat <<EOF
Hello World
第一行内容
第二行内容
EOF

# Here String:单行字符串作为 stdin
grep "pattern" <<< "some string to search"

# tee:同时输出到屏幕和文件("T形管道")
./build.sh 2>&1 | tee build.log   # 屏幕实时看,同时保存日志
ls -la | tee /tmp/list.txt | wc -l # 保存列表的同时统计行数
2>&1 的顺序陷阱

cmd > file 2>&1(正确)和 cmd 2>&1 > file(错误)的行为不同。Shell 从左到右处理重定向:正确写法先把 stdout 指向文件,再把 stderr 指向 stdout(此时 stdout 已指向文件),所以两者都进文件。错误写法先把 stderr 指向 stdout(此时 stdout 还是终端),再把 stdout 指向文件,导致 stderr 仍然打印到终端。

查看文件内容

cat(Concatenate)
读取文件并将内容输出到 stdout。名字来源于"连接"——原始设计是连接多个文件并输出。适合小文件;大文件请用 less,避免终端被海量输出淹没。
less(分页查看器)
替代 more 的分页查看器(less is more 的双关)。在查看大文件时只加载当前屏幕内容,内存占用极小。支持向上/向下滚动、正则搜索、跳转到指定行号,是查看日志的首选工具。
tail -f(Follow 模式)
持续监控文件末尾,有新内容时立即显示。通过 inotify 内核事件(Linux)或轮询实现。-f 跟踪文件描述符(日志轮转后失效),-F 跟踪文件名(日志轮转后自动重新打开新文件)。
# cat:连接并输出文件内容
cat file.txt                  # 显示完整内容(小文件用)
cat -n file.txt               # -n:显示行号
cat -A file.txt               # -A:显示不可见字符($ 表示行尾,^I 表示 Tab,排查编码问题)
cat -s file.txt               # -s:压缩连续空行为单个空行
cat file1.txt file2.txt       # 合并多个文件输出
cat file1.txt >> file2.txt    # 将 file1 追加到 file2(注意 >> 不是 >)

# less:分页查看大文件
less /var/log/syslog
# 操作快捷键:
#   j/k 或 ↓/↑    逐行滚动
#   f/b 或 PgDn/Up  翻页(forward/backward)
#   g / G            跳到开头/结尾
#   /pattern         向后搜索(Enter确认,n 下一个,N 上一个)
#   ?pattern         向前搜索
#   &pattern         只显示匹配行(过滤模式)
#   :100             跳到第 100 行
#   q                退出
less +F /var/log/app.log      # 以 tail -f 模式打开(Ctrl+C 切换到普通模式)
less +G /var/log/syslog       # 打开时直接跳到文件末尾
less -S file.txt              # 长行不折行(横向滚动),查看宽 CSV 时有用
less -N file.txt              # 显示行号

# head/tail:查看文件头尾
head -n 20 file.txt           # 前 20 行(默认 10 行)
head -c 100 file.txt          # 前 100 字节(-c = bytes)
tail -n 50 file.txt           # 最后 50 行
tail -f /var/log/syslog       # 实时追踪日志(运维必备!)
tail -F /var/log/app.log      # -F 即使文件被轮转也继续追踪新文件
tail -n +100 file.txt         # 从第 100 行开始显示(+N 表示从第N行起)

# 组合:实时过滤日志
tail -F /var/log/nginx/error.log | grep --line-buffered "ERROR"
# --line-buffered:强制行缓冲(管道默认块缓冲,会延迟显示)

grep:文本搜索利器

grep(Global Regular Expression Print)是 Linux 最常用的文本搜索工具。它逐行扫描输入,将匹配正则表达式的行打印到 stdout。grep 有三种正则引擎:BRE(基础正则,默认)、ERE(扩展正则,-E)、PCRE(Perl 兼容,-P)。

BRE vs ERE 的区别
基础正则(BRE)中,+ ? | () 需要用 \ 转义才有特殊含义;扩展正则(ERE,-E)中这些字符直接有特殊含义,\+ 反而是字面量加号。现代脚本建议统一使用 grep -E,更直观。
锚定符(Anchors)
^ 匹配行首(行起始位置),$ 匹配行尾(行结束位置)。^$ 匹配空行。注意 ^ 在字符集 [^...] 内是"取反",含义完全不同。\b(ERE/PCRE)匹配单词边界,用于精确匹配单词(如 \broot\b 不会匹配 "uprooted")。
# ─────── 基础搜索 ────────────────────────────
grep "error" /var/log/syslog          # 搜索包含 error 的行(大小写敏感)
grep -i "error" /var/log/syslog       # -i:大小写不敏感
grep -v "debug" /var/log/app.log      # -v:反向匹配(排除含 debug 的行)
grep -n "error" file.txt              # -n:显示匹配行的行号
grep -c "error" file.txt              # -c:只统计匹配行数(不显示内容)
grep -l "import os" *.py              # -l:只显示包含匹配的文件名(列表)
grep -L "import os" *.py              # -L:显示不含匹配的文件名
grep -w "root" /etc/passwd            # -w:精确单词匹配(不匹配 "rootfs" 或 "groot")
grep -x "exact line" file.txt         # -x:整行完全匹配
grep -m 5 "error" app.log             # -m 5:找到5处即停止(大文件提速)

# ─────── 递归搜索 ────────────────────────────
grep -r "TODO" ./src/                 # -r:递归搜索目录
grep -r --include="*.py" "TODO" .     # 只搜索 .py 文件
grep -r --exclude="*.min.js" "func" . # 排除压缩文件
grep -r --exclude-dir=".git" "API_KEY" .  # 排除 .git 目录(避免搜索版本历史)

# ─────── 上下文显示 ──────────────────────────
grep -A 3 "ERROR" app.log             # 显示匹配行及之后 3 行(After)
grep -B 3 "ERROR" app.log             # 显示匹配行及之前 3 行(Before)
grep -C 3 "ERROR" app.log             # 显示匹配行及前后各 3 行(Context)

# ─────── 正则表达式 ──────────────────────────
# BRE(默认)特殊字符:. * ^ $ [] \
grep "^ERROR" app.log                 # ^ 行首锚定
grep "\.log$" filelist.txt            # $ 行尾锚定(. 需转义)
grep "err.r" app.log                  # . 匹配任意单字符(包括空格、数字)
grep "colou*r" text.txt               # * 前一字符 0 次或多次(u 可以出现 0 次)

# ERE(-E 或 egrep)额外支持 + ? | () {} 无需转义
grep -E "error|warning|critical" app.log  # | 或运算
grep -E "colou?r" text.txt            # ? 前一字符 0 次或 1 次(color 或 colour)
grep -E "[0-9]+" numbers.txt          # + 前一字符 1 次或多次
grep -E "[0-9]{3}-[0-9]{4}" phones.txt   # {} 精确重复次数
grep -E "^\s*$" file.txt              # 匹配空白行(\s 匹配空格/Tab)
grep -E "^[A-Z][a-z]+" names.txt      # 首字母大写的单词
grep -E "https?://[^\s]+" urls.txt    # 匹配 URL(http 或 https)

# PCRE(-P)支持 \d \w \s 等 Perl 正则
grep -P "\d{4}-\d{2}-\d{2}" log.txt   # \d 匹配数字(比 [0-9] 更简洁)
grep -P "(?<=GET )/\S+" access.log    # 向后查找(lookbehind),提取 URL 路径

# 实用组合:统计日志中各级别错误数量
grep -cE "^\[ERROR\]" app.log; grep -cE "^\[WARN\]" app.log
grep 搜索二进制文件

grep 遇到二进制文件(如编译产物 .pyc、图片)时默认输出 "Binary file matches",而不显示匹配内容。用 grep -a(--text)将二进制文件当文本处理,或用 strings binary_file | grep pattern 提取可打印字符后再搜索。在递归搜索代码目录时,应加 --exclude="*.pyc" 排除编译产物,否则可能得到噪音输出。

sed:流编辑器

sed(Stream EDitor)按行读取输入,对每行执行编辑操作(替换/删除/插入/提取),然后输出。sed 内部有一个模式空间(Pattern Space)——当前处理的行的缓冲区,和一个保持空间(Hold Space)——可跨行存储数据的缓冲区。大多数日常使用只需掌握替换和删除。

# ─────── 替换操作 ────────────────────────────
# 基本语法:sed 's/旧文本/新文本/标志'
# s = substitute(替换命令)
# 标志:g = global(全行),i = 忽略大小写,数字N = 替换第N次出现
sed 's/foo/bar/' file.txt              # 每行只替换第一次出现
sed 's/foo/bar/g' file.txt             # g:全局替换所有出现
sed 's/foo/bar/gi' file.txt            # g+i:全局且大小写不敏感
sed 's/foo/bar/2' file.txt             # 只替换每行第 2 次出现

# -i:直接修改文件(in-place),不输出到 stdout
sed -i 's/foo/bar/g' file.txt          # Linux 直接修改(GNU sed)
sed -i.bak 's/foo/bar/g' file.txt      # 修改文件并保留 .bak 备份(推荐!)

# 使用不同分隔符(替换内容含 / 时避免转义地狱)
sed 's|/old/path|/new/path|g' config.txt   # 用 | 替代 /
sed 's#http://old.com#https://new.com#g' urls.txt  # 用 # 替代 /

# 引用捕获组(& 代表整个匹配,\1 代表第1个括号)
sed 's/\(hello\) \(world\)/\2 \1/' file.txt  # BRE:交换两个单词的顺序
sed -E 's/(hello) (world)/\2 \1/' file.txt   # ERE(-E):同上,括号不需转义
sed 's/[0-9]*/(&)/' file.txt                 # & 代表匹配内容:给数字加括号

# 多命令(-e 或分号)
sed 's/foo/bar/g; s/baz/qux/g' file.txt
sed -e 's/foo/bar/g' -e 's/baz/qux/g' file.txt

# ─────── 删除操作 ────────────────────────────
# d 命令:删除模式空间中的内容(跳过该行不输出)
sed '/^#/d' config.txt                 # 删除注释行(# 开头)
sed '/^\s*$/d' file.txt                # 删除空白行(只含空格/Tab 的行)
sed '2,5d' file.txt                    # 删除第 2 到第 5 行
sed '$d' file.txt                      # $ 代表最后一行,删除最后一行
sed '1d' file.txt                      # 删除第一行(如 CSV 的标题行)
sed '/^$/d; /^#/d' file.txt            # 同时删除空行和注释(两个条件)

# ─────── 提取操作 ────────────────────────────
# -n 抑制默认输出,p 命令打印模式空间(常见组合:-n + p 只输出匹配行)
sed -n '10,20p' file.txt               # 只显示第 10-20 行
sed -n '/ERROR/p' app.log              # 只显示包含 ERROR 的行(等同 grep "ERROR")
sed -n '/START/,/END/p' file.txt       # 显示 START 到 END 之间的行

# ─────── 实战示例 ────────────────────────────
# 批量替换配置文件中的 IP(.bak 保留备份)
sed -i.bak 's/192\.168\.1\.10/10\.0\.0\.5/g' /etc/hosts

# 去除文件中所有行尾空格
sed -i 's/[[:space:]]*$//' *.txt

# 在第 3 行之后插入新行
sed '3a\新增的一行内容' file.txt       # a = append(在指定行后追加)
sed '3i\插入在第3行之前' file.txt      # i = insert(在指定行前插入)
sed -i 在 macOS 上的兼容性问题

macOS 自带的 BSD sed 的 -i 选项必须提供备份后缀(即使是空字符串):sed -i '' 's/foo/bar/g' file。而 Linux 的 GNU sed 允许 -i 不带后缀。如果脚本需要跨平台运行,可以安装 GNU sed(brew install gnu-sed)或统一使用 sed -i.bak(兼容两个平台)。

awk:文本数据处理

awk 是一种完整的文本处理语言(得名于设计者 Aho、Weinberger、Kernighan 的姓氏首字母)。它将输入分割为记录(Records,默认按行)和字段(Fields,默认按空格/Tab),非常适合处理结构化文本(日志、CSV、命令输出)。

awk 的工作循环
awk 执行流程:① 运行 BEGIN { } 块(初始化,仅一次);② 逐行读取输入,对每行执行匹配的 pattern { action } 块;③ 运行 END { } 块(汇总,仅一次)。BEGIN/END 中无输入行,不能使用 $1 等字段变量。
内置变量
$0 = 整行;$1~$NF = 第 1 到第 NF 列;NF = 当前行字段总数;NR = 全局行号;FNR = 当前文件的行号(处理多文件时有用);FS = 输入分隔符(默认空白字符);OFS = 输出分隔符(默认空格);RS = 记录分隔符(默认换行)。
字段分隔符(FS)的特殊行为
默认 FS 为空白字符时,awk 会忽略行首/行尾的空格,并将连续多个空格视为单个分隔符(智能分隔)。当 FS 设为单个具体字符(如 -F:)时,连续分隔符会产生空字段,行首/行尾的分隔符也会产生空字段。
# ─────── 基础列提取 ──────────────────────────
awk '{print $1}' file.txt              # 打印第 1 列
awk '{print $1, $3}' file.txt          # 打印第 1 和第 3 列(空格分隔)
awk '{print $NF}' file.txt             # 打印最后一列($NF = $字段数)
awk '{print $(NF-1)}' file.txt         # 打印倒数第 2 列
awk -F: '{print $1}' /etc/passwd       # -F 指定输入分隔符(冒号分割用户名)
awk -F, '{print $2}' data.csv          # CSV 处理:取第 2 列
awk -F'\t' '{print $1, $2}' data.tsv   # Tab 分隔

# 控制输出分隔符(OFS)
awk -F: 'BEGIN{OFS=","} {print $1,$3}' /etc/passwd   # 用逗号分隔输出(: 输入 → , 输出)

# ─────── 条件过滤 ────────────────────────────
awk '$3 > 100 {print $0}' data.txt     # 第 3 列大于 100 的行
awk '$1 == "alice" {print}' file.txt   # 第 1 列等于 "alice" 的行
awk '/ERROR/ {print NR": "$0}' app.log # 匹配 ERROR 的行(附行号)
awk 'NR>=10 && NR<=20' file.txt        # 显示第 10-20 行(无 action 默认 print)
awk 'NR%2 == 0' file.txt               # 只打印偶数行
awk '$0 !~ /^#/' config.txt            # 排除注释行(!~ 不匹配)

# ─────── 统计计算 ────────────────────────────
awk '{sum += $3} END {print "总和:", sum}' data.txt
awk '{sum += $3; count++} END {
  printf "总和: %d\n平均: %.2f\n", sum, sum/count
}' data.txt

awk '{if($1 > max) max=$1} END {print "最大值:", max}' data.txt

# ─────── BEGIN / END 块 ──────────────────────
awk '
BEGIN {
  print "===== 报告开始 ====="  # 处理前执行,可初始化变量、打印标题
  FS=":"                        # 在 BEGIN 中设置 FS(等同 -F:)
  count=0
}
/bash$/ {                       # 只处理以 bash 结尾的行(bash 用户)
  count++
  print $1, "使用 bash"
}
END {
  print "===== 共", count, "个用户使用 bash ====="
}
' /etc/passwd

# ─────── 实战:分析 Nginx 访问日志 ──────────
# 统计 TOP5 访问 IP
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -5

# 统计各 HTTP 状态码数量
awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn

# 计算平均响应时间(假设第 10 列是响应时间)
awk '{sum+=$10; count++} END {printf "平均响应时间: %.2f ms\n", sum/count}' access.log

# 找出响应时间超过 1 秒(1000ms)的慢请求
awk '$10 > 1000 {print $1, $7, $10"ms"}' access.log | sort -k3 -rn | head -20
awk 字段分隔符的空字段陷阱

awk -F: 处理 /etc/passwd 时,如果某行有两个连续的冒号(如 alice::1000),中间是空字段,$2 为空字符串,$3 才是 1000。而默认空白分隔符模式下连续空格合并为一个,不会产生空字段。遇到多列数据对不齐的问题,先检查分隔符的一致性。

sort / uniq / wc / cut / tr

# ─────── sort:排序 ──────────────────────────
# 默认按字典序(字符串比较)排序——注意 "10" < "9"!
# 处理数字必须加 -n,否则顺序错误
sort file.txt                  # 字典序升序
sort -r file.txt               # -r:反向(降序)
sort -n numbers.txt            # -n:按数字大小(而非字典序)
sort -h sizes.txt              # -h:按人类可读大小排序(1K < 1M < 1G)
sort -u file.txt               # -u:排序并去重(相当于 sort | uniq)
sort -k2 file.txt              # -k2:按第 2 列排序(默认字典序)
sort -k2 -n -r file.txt        # 按第 2 列数字降序
sort -k1,1 -k2,2n file.txt     # 主键第 1 列字典序,次键第 2 列数字序
sort -t: -k3 -n /etc/passwd    # 以 : 分隔,按第 3 列(UID)数字排序
sort --parallel=4 huge_file.txt  # 并行排序(利用多核)

# ─────── uniq:去重 ──────────────────────────
# 重要:uniq 只删除相邻的重复行!使用前必须排序!
uniq file.txt                  # 去除相邻重复行(不排序则效果不完整)
sort file.txt | uniq            # 正确用法:先排序再去重
uniq -c file.txt               # -c:在行首显示出现次数
uniq -d file.txt               # -d:只显示重复行(出现多次的)
uniq -u file.txt               # -u:只显示唯一行(出现恰好一次的)
sort file.txt | uniq -c | sort -rn | head  # 频率统计经典组合(最高频在前)
sort -i file.txt | uniq -i      # -i:忽略大小写去重("Apple" = "apple")

# ─────── wc:统计 ────────────────────────────
wc -l file.txt                 # -l:统计行数(Lines)
wc -w file.txt                 # -w:统计单词数(Words,按空白分隔)
wc -c file.txt                 # -c:统计字节数(Characters/bytes)
wc -m file.txt                 # -m:统计字符数(多字节字符与字节数不同!)
wc file.txt                    # 同时显示行数、单词数、字节数
wc -l *.py | sort -n           # 统计所有 .py 文件行数并排序
find . -name "*.py" | wc -l    # 统计目录中 .py 文件总数

# ─────── cut:按列提取 ───────────────────────
cut -d: -f1 /etc/passwd        # 以 : 分隔,取第 1 列(用户名)
cut -d: -f1,6 /etc/passwd      # 取第 1 和第 6 列(用户名和主目录)
cut -d, -f2-4 data.csv         # 取第 2 到第 4 列(范围)
cut -c1-10 file.txt            # 取每行的前 10 个字符(按字符位置)
cut -c1-80 wide_file.txt       # 截断长行到 80 字符(查看宽文件)

# ─────── tr:字符替换/删除 ───────────────────
# tr 不接受文件参数,只能从 stdin 读取,且只操作单个字符(不支持字符串)
echo "Hello World" | tr 'a-z' 'A-Z'   # 转换为大写
echo "Hello World" | tr 'A-Z' 'a-z'   # 转换为小写
echo "hello   world" | tr -s ' '       # -s:压缩连续重复字符(多空格→单空格)
echo "hello123" | tr -d '0-9'          # -d:删除指定字符集中的所有字符
cat file.txt | tr '\n' ','             # 将换行符替换为逗号(多行→单行)
echo "hello\nworld" | tr -d '\n'       # 删除所有换行符

# ─────── diff:文件差异比较 ──────────────────
diff file1.txt file2.txt               # 基础差异显示(< 是 file1,> 是 file2)
diff -u file1.txt file2.txt            # -u:统一格式(和 git diff 相同格式)
diff -r dir1/ dir2/                    # -r:递归比较目录
diff --color file1.txt file2.txt       # 彩色显示差异
diff -w file1.txt file2.txt            # -w:忽略空白字符差异
diff -i file1.txt file2.txt            # -i:忽略大小写差异
diff <(sort a.txt) <(sort b.txt)       # 进程替换:比较两个命令的输出
sort 的字典序 vs 数字序陷阱

sort 默认按字典序排序,这意味着 "100" < "20" < "9"(因为比较第一个字符 1 < 2 < 9)。凡是涉及数字比较(文件大小、行数、时间戳等数字)都必须加 -n。类似的,uniq 只去除相邻重复行——如果文件未排序,重复行分散在不同位置,uniq 不会合并它们。正确用法始终是 sort | uniq 而非单独使用 uniq

管道组合:Linux 的超级武器

Unix 哲学的精髓:每个工具做好一件事,通过管道将它们组合解决复杂问题。管道让数据在工具之间流动,而不需要创建临时文件。

# ─────── 日志分析 ────────────────────────────

# 从 access.log 中提取 404 错误的 URL 并排行
grep ' 404 ' /var/log/nginx/access.log \  # 筛选 404 响应行
  | awk '{print $7}' \                    # 提取 URL(第 7 列)
  | sort \                                # 排序(uniq 需要)
  | uniq -c \                             # 统计重复次数
  | sort -rn \                            # 按次数降序排列
  | head -10                              # 只显示前 10 个

# 统计过去 1 小时的错误日志数量(假设日志含时间戳)
grep "$(date -d '1 hour ago' +'%Y-%m-%d %H')" /var/log/app.log | grep -c "ERROR"

# 查找日志中响应时间超过 500ms 的请求(第 10 列为响应时间)
awk '$10 > 500' /var/log/nginx/access.log | wc -l

# ─────── 文件与目录分析 ──────────────────────

# 找出占用最多磁盘空间的 10 个目录
du -sh */ | sort -rh | head -10

# 统计每种文件类型的数量
find . -type f | sed 's/.*\.//' | sort | uniq -c | sort -rn

# 统计代码行数(不含空行和注释)
find . -name "*.py" -type f \
  | xargs grep -v "^\s*#\|^\s*$" \   # 排除注释行和空行
  | wc -l

# ─────── 代码库分析 ──────────────────────────

# 查找所有 Python 文件中的 TODO/FIXME 并显示文件名和行号
grep -rn "# TODO\|# FIXME" . --include="*.py"

# 批量替换所有 Python 文件中的旧导入路径(先预览再执行)
grep -rl "from oldpackage import" . --include="*.py" | head -5  # 先预览
grep -rl "from oldpackage import" . --include="*.py" \
  | xargs sed -i 's/from oldpackage import/from newpackage import/g'

# 找出最大的 Python 文件(代码行数最多)
find . -name "*.py" | xargs wc -l | sort -rn | head -5

# ─────── 系统管理 ────────────────────────────

# 查看哪些进程占用了最多内存(前 5)
ps aux | sort -k4 -rn | head -5 | awk '{printf "%-20s %5.1f%%\n", $11, $4}'

# 统计每个用户的进程数量
ps aux | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn

# 查看当前系统最消耗 CPU 的 10 个进程
ps aux --sort=-%cpu | head -11 | awk 'NR>1 {printf "%-30s %s%%\n", $11, $3}'

# 列出所有已打开端口(不含注释和空行)
ss -tlnp | grep LISTEN | awk '{print $4}' | cut -d: -f2 | sort -n

# ─────── 文本转换技巧 ───────────────────────

# 将 CSV 的某列数据去重排序后输出(取第 2 列的唯一值)
cut -d, -f2 data.csv | sort -u

# 合并两个有序文件(类似 SQL JOIN)
join -t: -1 1 -2 1 <(sort /etc/passwd) <(sort /etc/shadow) | cut -d: -f1,2

# 统计文本文件中各个单词的出现频率(词频统计)
cat text.txt | tr '[:upper:]' '[:lower:]' \  # 全部小写
  | tr -cs 'a-z' '\n' \                       # 非字母字符替换为换行(分词)
  | sort \                                     # 排序
  | uniq -c \                                  # 计数
  | sort -rn \                                 # 按频率降序
  | head -20                                   # TOP20 词频

xargs:命令行参数构造

xargs 将 stdin 的内容转换为命令的参数。管道只能传递数据到 stdin,但很多命令(如 cp、mv、chmod)不接受 stdin,需要命令行参数——这时就需要 xargs 做"桥梁"。

# 基础用法:将 stdin 作为参数追加到命令后
echo "file1 file2 file3" | xargs rm           # 等同于 rm file1 file2 file3
find . -name "*.tmp" | xargs rm               # 删除所有 .tmp 文件
find . -name "*.py" | xargs wc -l             # 统计所有 .py 文件行数

# -I {} 替换字符串(处理每个参数时执行独立命令)
find . -name "*.log" | xargs -I {} cp {} /backup/    # 逐一复制每个日志文件
cat urls.txt | xargs -I {} curl -s {} -o /dev/null   # 逐一测试 URL 是否可达

# -0 和 find -print0:处理文件名中含空格的情况
# 默认 xargs 按空白字符分割,文件名含空格会被错误分割
find . -name "*.txt" -print0 | xargs -0 grep "pattern"   # 用 NULL 作为分隔符
find . -name "* *" -print0 | xargs -0 rm                 # 安全删除含空格的文件名

# -n N:每次传递最多 N 个参数(控制批次大小)
find . -name "*.py" | xargs -n 10 grep "TODO"     # 每次处理 10 个文件

# -P N:并行执行 N 个进程(利用多核加速)
find . -name "*.jpg" | xargs -P 4 -I {} convert {} -quality 85 {}.opt.jpg
# 用 ImageMagick 并行压缩图片(4个并发进程)
xargs 参数长度限制与空文件名问题

系统对命令行参数总长度有限制(通常 2MB,getconf ARG_MAX 查看),find 结果过多时直接 find | xargs rm 可能报"Argument list too long"。xargs 会自动将参数分批传递,不会超过限制,这是它优于 for f in $(find ...) 的原因之一。对于含空格、换行、特殊字符的文件名,始终使用 find -print0 | xargs -0 的组合。

本章小结

Linux 文本处理的核心要点:① 理解标准流(stdin/stdout/stderr)和管道的工作原理——管道是并行的,不是顺序的;② grep -E(ERE)比 BRE 更直观,日常优先使用;③ sed -i.bak 修改文件前保留备份,永远是好习惯;④ awk 擅长结构化文本处理,-F 指定分隔符,BEGIN/END 做初始化和汇总;⑤ sort | uniq -c | sort -rn 是频率统计的万能公式;⑥ sort 处理数字必须加 -n;⑦ find -print0 | xargs -0 是处理含空格文件名的标准做法;⑧ 管道组合的威力在于用简单工具解决复杂问题。