# 文本处理

# 查看文本

有多种命令可以查看文件的内容。

  • 有的文件是采用二进制格式存储的,不能直接阅读。
  • 有的文件是采用 ASCII 码等编码格式存储的,称为文本文件,可以直接阅读。
    • 如果查看文本文件时出现乱码,说明解码时使用的编码格式不对。

本文中,大部分命令的输入参数都声明为 [file] 。如果不指定文件参数 file ,或者指定 - 作为 file ,该命令就会读取 stdin 作为输入。例如:ps | wc -l

# cat

$ cat [file]    # 显示文件的内容
      -n        # 同时显示行号
      -b        # 空白行不记行号
      -E        # 在每行的末尾显示 $
      -T        # 将 Tab 符显示成 ^I
      -v        # 显示不会 print 的特殊字符
  • 不适合查看内容较多的文件,否则会显示好几页终端。
  • 例:
    cat f1 > f2  # 拷贝文件 f1 的内容到文件 f2 中(相当于只 cp 内容)
    

# tac

$ tac [file]     # 从最后一行开始倒序显示文件的内容(与 cat 命令相反)
$ head [file]    # 显示文件开头的 10 行
       -n        # 只显示 n 行
       -c n      # 只显示 n 个字节,可以使用单位 K、M、G、T 等

# tail

$ tail [file]    # 显示文件末尾的 10 行
       -n        # 只显示 n 行
       -c n      # 只显示 n 个字节
       -f n      # 跟踪显示文件的末尾,当末尾增加内容时就显示出来(这会阻塞前台)
       -q        # 查看多个文件时,默认会分别显示文件名,启用该选项则不显示文件名

# more

$ more <file>    # 打开阅读器,显示文件的全部内容(只能向前翻页)

# less

$ less <file>    # 打开阅读器,显示文件的全部内容(可以前后翻页、查找)
       -N        # 显示行号
  • 该阅读器的控制方法与 vim 相似,只是要按 q 键退出。

# 文本排版

# column

$ column [file]...
         -t          # 将每行文本分割成多个字段,从而将输入的多行文本转换成一个列表(会自动上下对齐)
         -s " "      # 指定输入文本的字段分隔符(默认是连续的空白字符)
         -o "  "     # 指定输出列表的字段分隔符(默认是至少两个空格)
  • 例:
    [root@CentOS ~]# echo $PATH
    /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    [root@CentOS ~]# echo $PATH | column -t -s :
    /usr/local/sbin  /usr/local/bin  /usr/sbin  /usr/bin  /sbin  /bin
    [root@CentOS ~]# echo $PATH | column -t -s : -o ', '
    /usr/local/sbin, /usr/local/bin, /usr/sbin, /usr/bin, /sbin, /bin
    

# sort

$ sort [file]...    # 将每行文本排序之后输出
       -k 1         # 按每行的第 1 个字段进行排序
       -t " "       # 指定每行的字段分隔符
       -r           # 反序排列
  • 例:
    [root@CentOS ~]# ls -al | sort -k 9
    total 216
    dr-xr-x---. 22 root root  4096 Dec  6 15:54 .
    dr-xr-xr-x. 19 root root  4096 Dec  9 10:00 ..
    -rw-------   1 root root 41827 Dec  9 09:59 .bash_history
    -rw-r--r--.  1 root root    18 Dec 29  2013 .bash_logout
    -rw-r--r--.  1 root root   176 Dec 29  2013 .bash_profile
    

# jq

$ jq <filter> [file...]   # 读取文本,按 JSON 格式解析,然后输出
    --indent 2            # 输出时,每层缩进的空格数
    --compact-output      # 将输出的 JSON 对象压缩成一行。否则默认会输出多行,自动缩进
  • 安装: yum install jq
  • 例:
    jq . test.json                      # filter 为 . ,这会输出整个 JSON 对象
    cat test.json | jq .links[0]        # 只输出 filter 指定的元素
    cat test.json | jq ,.name,.status   # filter 可以指定多个元素
    cat test.json | jq [.name, .status]                   # 输出为 array 类型
    cat test.json | jq '{name: .name, status: .status}'   # 输出为 object 类型
    cat test.json | jq '"name=\(.name)"'                  # 输出为 string 类型,可以用 \(x) 的格式插入值
    cat test.json | jq 'del(.name, .status)'              # 删除一些字段
    
  • 还可用 Python 进行格式化:
    python -m json.tool <file>
    

# yq

$ yq <expression> [file...]
    -I <int>    # --indent ,设置输出时的缩进,默认为 2
    -i          # --inplace ,将输出保存到源文件中。此时不支持读取 stdin 作为输入
  • yq 命令的用法与 jsq 类似,但支持解析 YAML、JSON、XML 文本格式。
  • 安装:
    wget https://github.com/mikefarah/yq/releases/download/v4.19.1/yq_linux_amd64 -O /usr/bin/yq
    chmod +x /usr/bin/yq
    
  • 例:
    yq '.a[0].b' test.yaml              # 输出指定字段
    cat test.yaml | yq -                # 读取 stdin
    yq -i '.a="hello"' test.yaml        # 替换一个字段的值
    yq '.a[]' test.yaml -s '.name'      # 将 list 元素分别保存到不同文件中,文件名引用其中的 .name 字段
    
  • yq 命令不支持通过 $ 读取 shell 环境变量。而要使用以下语法:
    yq -i ".a=env(PWD)"  test.yaml      # 引用 shell 环境变量,自动保存为字符串、数值、数组等数据类型
    yq -i ".a=strenv(PWD)"  test.yaml   # 引用 shell 环境变量,总是保存为字符串类型
    
  • 如果目标文件中包含多个用 --- 分隔的 YAML 文档,则会被 yq 分别处理。

# 统计分析

# wc

$ wc [file]...
     -l        # 统计行数
     -c        # 统计字节数
     -m        # 统计字符数

# uniq

$ uniq [file]...  # 去掉文本中重复出现的行,再显示
      -c          # 统计每行重复出现的次数,显示在每行左侧
      -d          # 仅显示重复出现的行
      -u          # --unique ,仅显示没有重复的行
      -i          # --ignore-case ,忽略大小写
  • uniq 识别重复的行时,这些行必须相邻,因此通常先用 sort 将文本排序,再用 uniq 去重。如下:
    [root@CentOS ~]# cat f1                   # 查看文件的原内容
    Hello World
    Hello
    Hello World
    Hello
    [root@CentOS ~]# uniq f1                  # 此时重复的行没有相邻,因此 uniq 不能识别
    Hello World
    Hello
    Hello World
    Hello
    [root@CentOS ~]# cat f1 | sort            # 将文本排序
    Hello
    Hello
    Hello World
    Hello World
    [root@CentOS ~]# cat f1 | sort | uniq     # 将文本排序后去重
    Hello
    Hello World
    [root@CentOS ~]# cat f1 | sort | uniq -c  # 统计每行重复出现的次数
          2 Hello
          2 Hello World
    

# diff

$ diff
       <file1> <file2>  # 逐行比较两个文本文件的内容差异(如果没有差异,则显示为空)
            -q          # 安静模式(如果有差异,则不显示具体差异,只显示一行提示)
            -y          # 在屏幕左右侧分别显示两个文件的内容,类似 GUI
            -b          # 忽略空格(多个连续空格视作一个空格)
            -B          # 忽略空行
            -i          # 忽略大小写的差异
            -w          # 忽略所有空白字符
       <dir1> <dir2>    # 比较两个目录。找出哪些文件名只存在单个目录中;找出哪些文件名存在两个目录中,但是内容不同
            -q          # 安静模式
            -r          # 递归比较(默认不会比较子目录)
  • 安装: yum install diffutils
  • 例:
    cat f2 | diff f1 -  # 通过 stdin 传入一个文件进行比较
    
    [root@CentOS ~]# diff -y f1 f2
    A                     |
    B                     |
    C                     |
    D                       D
                          > E
                          > F
                          > G
    [root@CentOS ~]# diff f1 f2
    1,3c1,3
    < A
    < B
    < C
    ---
    >
    >
    >
    4a5,7
    > E
    > F
    > G
    
    diff 的显示结果表示了从文件 1 到文件 2 的变化。
    • 例如,1,3c1,3 表示从文件 1 的第 1~3 行到文件 2 的第 1~3 行有变化。
    • a 表示 add ,c 表示 change ,d 表示 delete 。
    • --- 是两个文件内容的分隔线。

# hexdump

$ hexdump <file>...     # 将文件内容显示成十六进制值(两个字节为一组)
            -c          # 显示成 ASCII 码
            -C          # 显示成十六进制值 + ASCII 码(每个字节为一组)
            -n 4        # 最多显示 4 个字节
            -s  4       # 从第 4 个字节开始显示
  • 例:

    [root@CentOS ~]# echo 'Hello World! How are you?' > f1
    [root@CentOS ~]# hexdump f1
    0000000 6548 6c6c 206f 6f57 6c72 2164 4820 776f     # 第一排包含从地址 0000000 到 000000F 处的 16 个字节
    0000010 6120 6572 7920 756f 0a3f
    000001a                                             # 最后一排表示该文件的总字节数
    [root@CentOS ~]# hexdump -C f1
    00000000  48 65 6c 6c 6f 20 57 6f  72 6c 64 21 20 48 6f 77  |Hello World! How|
    00000010  20 61 72 65 20 79 6f 75  3f 0a                    | are you?.|
    000001a
    [root@CentOS ~]# hexdump -n 1 f1
    0000000 0048
    0000001
    [root@CentOS ~]# hexdump -n 2 f1
    0000000 6548
    0000002
    
    • 上例中,地址 0000000、0000001 处的字节内容分别为 48、65 ,对应的 ASCII 字符分别为 H、e 。
    • 显示 . 代表不能解码成 ASCII 码中的可显示字符。
  • xxd 命令与之类似:

    [root@CentOS ~]# echo Hello | xxd
    0000000: 4865 6c6c 6f0a                           Hello.
    

# 筛选文本

# grep

$ grep <pattern> [file]...  # 筛选出文件中匹配 pattern 的每行文本
       [-e pattern]...      # --regexp ,指定基本语法的正则表达式(这是默认类型)
       [-E pattern]...      # --extended-regexp ,指定扩展语法的正则表达式
       [-P pattern]...      # --perl-regexp ,指定 Perl 语法的正则表达式
       -a                   # 将二进制文件也当作 text 格式筛选,否则默认不能处理二进制文件
       -i                   # --ignore-case ,忽略大小写
       -r                   # --recursive ,递归检索指定目录下的所有文件

       -c                   # --count ,只显示匹配的总行数
       -H                   # --with-filename ,输出每个匹配结果时显示文件名
       -h                   # --no-filename ,输出每个匹配结果时不显示文件名。默认情况下,如果只读取一个文件或 stdin ,则启用 -h 选项,否则启用 -H 选项
       -o                   # --only-matching ,只显示每行中与 pattern 匹配的子字符串,而不是整行
       -q                   # --quit ,不显示匹配结果。此时只能根据命令返回码,判断匹配是否成功
       -n                   # --line-number ,增加显示每行的行号
       -v                   # 反向匹配

       -A <int>             # --after-context ,增加显示在匹配的行之后的 n 行
       -B <int>             # --before-context ,增加显示在匹配的行之前的 n 行
       -C <int>             # --context ,增加显示在匹配的行之后和之前的 n 行
       -o -P '.{0,10}pattern.{0,10}'  # 只显示在与 pattern 匹配的子字符串的前后 10 个字符

       -l                   # 如果文件内容匹配,则只显示文件名
       -L                   # 只显示不匹配的文件名
  • 如果 grep 的匹配结果不为空,则返回码为 0 ,表示匹配成功(只要有一行文本匹配成功即可)。否则为非 0 ,表示匹配失败。
  • 例:
    grep -n root /etc/passwd
    grep -v '^$' /etc/passwd                  # 去掉空行
    grep README.md -e hello -e world          # 可以指定多个 pattern ,只要文本匹配任一 pattern 就会被筛选出来
    grep -P '[\p{Han}]'                       # 匹配汉字
    
    grep -cr DEBUG . | grep -v :0             # 递归检索指定目录下的所有文件
    find . -name *.py | xargs grep -c DEBUG   # 先找出一组文件,再筛选文本
    

# cut

$ cut [file]...
      -f 1-4        # 显示每行的第 1 至 4 个字段
      -f 1 -d ","   # 显示每行的第 1 个字段,以 , 作为字段分隔符
      -b "1 2 3"    # 显示每行的第几个字符

# awk

$ awk [option] [expression] [file]...
      -F ,                        # 设置字段分隔符(默认是一个或多个空格、Tab)
  • awk 可编写复杂的表达式,是一种脚本语言。
    • awk 表达式要用单引号包住,避免转义。
    • 正则表达式 pattern 要用 / / 包住。
    • awk 表达式可以简写,例如 $1=="root" {print $3}' 的完整写法是:
      if ( $1 == "root" )
      {
         print($3)    # 函数的括号可以省略
      }
      
  • awk 表达式示例:
    # 筛选显示的内容
    '{print $3}'                # 显示每行的第 3 个字段
    '{print NR, $(NF-1)}'       # 显示每行的行号(从 1 开始编号)、倒数第二个字段
    '$3="hello"; print'         # 设置第 3 个字段的值,然后打印全部内容
    'NR>1 && NR<=10'            # 显示行号在指定范围的行
    '/^\S+/ {print $2,$3}'      # 要求当前行与 pattern 正则匹配,才显示
    '$1=="root" {print $3}'     # 要求第一个字段等于指定字符串,才显示
    '/Hello/,/^\s\s/'           # 查找两个 pattern 匹配的行,显示它们之间的所有行。其中的逗号 , 是 awk 语法的分隔符
    
    # 引用 shell 变量
    -v a=$A -v b=$B '{print $a}'
    
    # 进行运算
    '{sum+=$1} END {print sum / 1024}'  # 计算第一个字段的总和
    
    [root@CentOS ~]# echo "1.76e-06"| awk '{printf "%f", $0 }'    # 控制输出格式为小数,默认小数点后保留 6 位
    0.000002
    [root@CentOS ~]# echo "1.76e-04"| awk '{printf "%.2f", $0 }'  # 控制小数点后保留 2 位
    0.00
    
  • awk 的内置变量:
    • $0 :当前行的全部字段。
    • $1 :当前行的第一个字段,以此类推。
      • 如果 $n 超过当前行的最大字段数,则获取的值为空。
    • NF :当前行的最大字段数。
    • NR :当前行数。
  • awk 命令不支持直接修改文件,可采用以下方式:
    awk '{$3=$2; print}' f1 > f2
    rm -f f1
    mv f2 f1