强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Bash 脚本编写教程 / 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 扩展阅读