15 - 文本处理
15 - 文本处理
15.1 cut —— 列提取
# 基本用法
echo "apple,banana,cherry" | cut -d',' -f2 # banana
# 多个字段
echo "a:b:c:d:e" | cut -d':' -f1,3,5 # a:c:e
# 字段范围
echo "a:b:c:d:e" | cut -d':' -f2-4 # b:c:d
# 字符位置
echo "Hello World" | cut -c1-5 # Hello
# 从后面截取
echo "Hello World" | cut -c7- # orld
# 反向选择
echo "a:b:c:d" | cut -d':' --complement -f2 # a:c:d
# 实际应用:提取 /etc/passwd 信息
cut -d':' -f1,3,6 /etc/passwd | head -5
# 提取 IP 地址
echo "192.168.1.1 - - [10/May/2026:13:45:30]" | cut -d' ' -f1
15.2 tr —— 字符转换
# 大小写转换
echo "Hello World" | tr '[:lower:]' '[:upper:]' # HELLO WORLD
echo "HELLO" | tr '[:upper:]' '[:lower:]' # hello
# 删除字符
echo "Hello 123 World 456" | tr -d '[:digit:]' # Hello World
# 压缩重复字符
echo "Hello World" | tr -s ' ' # Hello World
# 替换字符
echo "hello-world" | tr '-' '_' # hello_world
# 删除换行符
cat file.txt | tr -d '\n'
# 将换行转为空格
cat list.txt | tr '\n' ' '
# 补集(complement)
echo "Hello 123" | tr -cd '[:digit:]\n' # 123
# 删除非字母数字
echo "Hello, World! 123" | tr -cd '[:alnum:]\n' # HelloWorld123
# Tab 转空格
expand file.txt | tr -s ' '
# ROT13 加密
echo "Hello" | tr 'A-Za-z' 'N-ZA-Mn-za-m' # Uryyb
# 删除重复字符
echo "aabbcc" | tr -s 'a-z' # abc
15.3 sort —— 排序
# 基本排序(字典序)
echo -e "banana\napple\ncherry" | sort
# 数字排序
echo -e "100\n20\n3" | sort -n
# 逆序
echo -e "a\nc\nb" | sort -r
# 按指定字段排序
echo -e "3\tapple\n1\tbanana\n2\tcherry" | sort -t$'\t' -k1 -n
# 去重排序
echo -e "a\nb\na\nc\nb" | sort -u
# 按文件大小排序
ls -lS /tmp/ | sort -k5 -n -r
# 多字段排序(先按第2列字典序,再按第3列数字序)
echo -e "A 3 10\nB 1 20\nA 1 30" | sort -k2,2 -k3,3n
# 忽略大小写
echo -e "Banana\napple\nCherry" | sort -f
# 月份排序
echo -e "Jan\nMar\nFeb" | sort -M
# 随机排序
echo -e "a\nb\nc\nd" | sort -R
# 人类可读大小排序
du -sh /var/log/* 2>/dev/null | sort -rh | head -10
# 大文件排序(外部排序)
sort -T /tmp/big_sort -S 1G large_file.txt > sorted.txt
15.4 uniq —— 去重与统计
# ⚠️ uniq 只处理相邻的重复行,通常需要先 sort
echo -e "a\na\nb\nb\na" | sort | uniq # a b
# 统计出现次数
echo -e "a\na\nb\nb\na" | sort | uniq -c # 3 a 2 b
# 只显示重复行
echo -e "a\na\nb\nc" | sort | uniq -d # a
# 只显示非重复行
echo -e "a\na\nb\nc" | sort | uniq -u # b c
# 忽略前 N 个字段
echo -e "1 a\n2 a\n3 b" | sort -k2 | uniq -f1 -c
# 实际应用:统计日志中最常见的错误
grep "ERROR" /var/log/syslog | sort | uniq -c | sort -rn | head -10
# 统计访问最多的 IP
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10
15.5 join —— 连接文件
# 两个已排序文件按公共字段连接
cat > users.txt << 'EOF'
1 张三
2 李四
3 王五
EOF
cat > orders.txt << 'EOF'
1 订单A 100元
1 订单B 200元
2 订单C 150元
3 订单D 300元
EOF
# 默认按第一列连接
join users.txt orders.txt
# 指定连接字段
join -1 1 -2 1 users.txt orders.txt
# 输出未匹配的行
join -a1 -a2 users.txt orders.txt # 全外连接
join -v1 users.txt orders.txt # 只输出第一个文件未匹配的
# 指定分隔符
join -t$'\t' file1.txt file2.txt
# 从指定字段输出
join -o 1.2 2.2 2.3 users.txt orders.txt
15.6 paste —— 并行合并
# 按列合并文件
cat > names.txt << 'EOF'
张三
李四
王五
EOF
cat > scores.txt << 'EOF'
90
85
95
EOF
paste names.txt scores.txt
# 指定分隔符
paste -d',' names.txt scores.txt
# 合并为一行
paste -sd',' names.txt # 张三,李四,王五
# 交错合并
paste - - < names.txt # 每两行合并为一行
# 实际应用:将多个文件按列合并
paste <(cut -d',' -f1 data.csv) <(cut -d',' -f3 data.csv) > result.csv
15.7 comm —— 比较已排序文件
# 比较两个已排序文件
cat > file1.txt << 'EOF'
apple
banana
cherry
date
EOF
cat > file2.txt << 'EOF'
banana
date
elderberry
fig
EOF
# 输出三列:只在file1 | 只在file2 | 两者都有
comm file1.txt file2.txt
# 只显示共有行
comm -12 file1.txt file2.txt
# 只显示 file1 独有
comm -23 file1.txt file2.txt
# 只显示 file2 独有
comm -13 file1.txt file2.txt
15.8 wc —— 统计
# 行数
wc -l file.txt
# 字数
wc -w file.txt
# 字节数
wc -c file.txt
# 字符数(多字节字符)
wc -m file.txt
# 全部统计
wc file.txt
# 从标准输入
echo "hello world" | wc -w # 2
# 统计目录下文件数量
find . -name "*.sh" -type f | wc -l
# 统计代码行数(排除空行和注释)
find . -name "*.sh" -type f -exec cat {} + | grep -cvE '^\s*#|^\s*$'
15.9 awk —— 文本处理语言
# 基本打印
echo -e "a\tb\tc" | awk '{print $2}' # b
# 字段分隔符
awk -F: '{print $1, $3}' /etc/passwd | head -5
# 条件过滤
awk -F: '$3 >= 1000 {print $1}' /etc/passwd
# 内置变量
# NR: 行号 NF: 字段数 FS: 输入分隔符 OFS: 输出分隔符
awk 'NR <= 5 {print NR, NF, $0}' /etc/passwd
# BEGIN/END 块
awk -F: 'BEGIN {print "用户列表"} {print $1} END {print "总计:", NR, "个用户"}' /etc/passwd
# 求和
echo -e "10\n20\n30" | awk '{sum+=$1} END {print "总和:", sum}'
# 格式化输出
awk -F: '{printf "%-20s %5d\n", $1, $3}' /etc/passwd
# 多文件处理
awk 'FNR==1 {print "---", FILENAME, "---"} {print}' file1.txt file2.txt
# 字符串函数
echo "hello world" | awk '{print toupper($0)}' # HELLO WORLD
echo "hello world" | awk '{print length($0)}' # 11
# 数组
echo -e "a\nb\nc\na\nb\na" | awk '{count[$1]++} END {for(k in count) print k, count[k]}'
15.10 sed —— 流编辑器
# 替换
echo "Hello World" | sed 's/World/Bash/' # Hello Bash
echo "aaa" | sed 's/a/b/g' # bbb(全局替换)
# 删除行
sed '3d' file.txt # 删除第3行
sed '/pattern/d' file.txt # 删除匹配行
sed '1,5d' file.txt # 删除1-5行
# 插入和追加
sed '3a\新行内容' file.txt # 第3行后追加
sed '3i\新行内容' file.txt # 第3行前插入
# 就地编辑
sed -i 's/old/new/g' file.txt # 直接修改文件
sed -i.bak 's/old/new/g' file.txt # 修改并备份
# 范围操作
sed '10,20s/foo/bar/g' file.txt # 只在10-20行替换
# 多命令
sed -e 's/foo/bar/' -e 's/baz/qux/' file.txt
# 使用不同的分隔符
sed 's|/usr/local|/opt|' file.txt
sed 's#path/to#/new/path#' file.txt
15.11 业务场景:日志分析工具集
#!/bin/bash
# log_tools.sh —— 日志分析工具集
set -euo pipefail
# 统计 HTTP 状态码分布
analyze_status_codes() {
local logfile="$1"
echo "HTTP 状态码分布:"
awk '{print $9}' "$logfile" | sort | uniq -c | sort -rn | \
while read -r count code; do
case "$code" in
2*) prefix="✅ 成功" ;;
3*) prefix="↪️ 重定向" ;;
4*) prefix="⚠️ 客户端错误" ;;
5*) prefix="🔴 服务端错误" ;;
*) prefix="❓ 未知" ;;
esac
printf " %6d %s HTTP %s\n" "$count" "$prefix" "$code"
done
}
# 统计最慢的请求
analyze_slow_requests() {
local logfile="$1"
local threshold="${2:-1.0}"
echo "慢请求(响应时间 > ${threshold}s):"
awk -v thresh="$threshold" '$NF > thresh {print $7, $NF"s"}' "$logfile" | \
sort -t' ' -k2 -rn | head -20
}
# 统计每小时请求数
analyze_hourly_traffic() {
local logfile="$1"
echo "每小时请求分布:"
awk '{
split($4, dt, ":")
hour = dt[2]
count[hour]++
} END {
for (h in count) printf " %s:00 %6d 请求\n", h, count[h]
}' "$logfile" | sort
}
# 统计最活跃的 IP
analyze_top_ips() {
local logfile="$1"
local limit="${2:-10}"
echo "Top $limit 最活跃 IP:"
awk '{print $1}' "$logfile" | sort | uniq -c | sort -rn | head -"$limit" | \
while read -r count ip; do
printf " %8d %s\n" "$count" "$ip"
done
}
# 统计请求路径
analyze_top_paths() {
local logfile="$1"
local limit="${2:-10}"
echo "Top $limit 请求路径:"
awk '{print $7}' "$logfile" | sort | uniq -c | sort -rn | head -"$limit" | \
while read -r count path; do
printf " %8d %s\n" "$count" "$path"
done
}
# 提取错误日志
extract_errors() {
local logfile="$1"
local keyword="${2:-error}"
echo "错误日志 (关键词: $keyword):"
grep -i "$keyword" "$logfile" | tail -20
}
# 主函数
main() {
local logfile="${1:?用法: $0 <日志文件>}"
if [[ ! -f "$logfile" ]]; then
echo "文件不存在: $logfile" >&2
exit 1
fi
echo "========================================"
echo " 日志分析报告"
echo " 文件: $logfile"
echo " 时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo "========================================"
echo ""
analyze_status_codes "$logfile"
echo ""
analyze_hourly_traffic "$logfile"
echo ""
analyze_top_ips "$logfile" 10
echo ""
analyze_top_paths "$logfile" 10
}
main "$@"
15.12 注意事项
| 陷阱 | 说明 | 解决方案 |
|---|
sort 默认字典序 | 10 < 2 | 使用 -n 数字排序 |
uniq 只去相邻重复 | 需要先排序 | sort file | uniq |
cut 不支持多分隔符 | 只能用单字符 | 使用 awk |
tr 不接受文件参数 | 只从 stdin 读取 | tr ... < file |
join 文件必须排序 | 未排序结果不对 | 先 sort |
awk 字段从1开始 | 不是从0开始 | $1 是第一个字段 |
15.13 扩展阅读