如何将数字从多列排序或重新排列为多行[固定为4列]?

时间:2018-07-03 03:49:59

标签: linux perl awk sed

我有1个文本文件,即test1.txt。

text1.txt包含以下内容:
输入:

##[A1] [B1] [T1]  [V1] [T2]  [V2] [T3]  [V3] [T4]  [V4]## --> headers
    1  1000    0   100   10   200   20   300   30   400
              40   500   50   600   60   700   70   800
       1010    0   101   10   201   20   301   30   401
              40   501   50   601  
    2  1000    0   110   15   210   25   310   35   410
              45   510   55   610   65   710
       1010    0   150   10   250   20   350   30   450
              40   550  

条件:
每个A1 +(B1 + [Tn + Vn])的A1和B1->
A1应该在1列中。
B1应该在1列中。
T1,T2,T3和T4应该在1列中。
V1,V2,V3和V4应该在1列中。

我如何将其变成如下所示?
需求输出:

##   A1    B1   Tn    Vn ## --> headers

      1  1000    0   100
                10   200
                20   300
                30   400
                40   500
                50   600
                60   700
                70   800
         1010    0   101
                10   201
                20   301
                30   401
                40   501
                50   601
      2  1000    0   110
                15   210
                25   310
                35   410
                45   510
                55   610
                65   710
         1010    0   150
                10   250
                20   350
                30   450
                40   550

这是我当前的代码:
第一次尝试:
输入

cat test1.txt | awk ' { a=$1 b=$2 } { for(i=1; i<=5; i=i+1) { t=substr($0,11+i*10,5) v=substr($0,16+i*10,5) if( t ~ /^\ +[0-9]+$/ || t ~ /^[0-9]+$/ || t ~ /^\ +[0-9]+\ +$/ ){ printf "%7s %7d %8d %8d \n",a,b,t,v } }}' | less

输出:

      1    1000      400        0 
     40     500      800        0 
   1010       0      401        0 
      2    1000      410        0 
   1010       0      450        0

我正在尝试使用简单的awk命令,但仍然无法获得结果。
有人可以帮我吗?

谢谢,
Am

4 个答案:

答案 0 :(得分:4)

与其他地方所说的不同,这根本没有什么棘手的问题,您只是在输入中使用固定宽度的字段,而不是用字符/字符串分隔的字段。

使用GNU awk来让FIELDWIDTHS处理固定宽度的字段,实际上再简单不过了:

$ cat tst.awk
BEGIN {
    # define the width of the input and output fields
    FIELDWIDTHS = "2 4 5 5 6 5 6 5 6 5 6 99"
    ofmt = "%2s%5s%6s%5s%6s%s\n"
}
{
    # strip leading/trailing blanks and square brackets from every field
    for (i=1; i<=NF; i++) {
         gsub(/^[[\s]+|[]\s]+$/,"",$i)
    }
}
NR==1 {
    # print the header line
    printf ofmt, $1, $2, $3, "Tn", "Vn", " "$NF
    next
}
{
    # print every other line
    for (i=4; i<NF; i+=2) {
        printf ofmt, $1, $2, $3, $i, $(i+1), ""
        $1 = $2 = $3 = ""
    }
}

$ awk -f tst.awk file
##   A1    B1   Tn    Vn ## --> headers
      1  1000    0   100
                10   200
                20   300
                30   400
                40   500
                50   600
                60   700
                70   800
         1010    0   101
                10   201
                20   301
                30   401
                40   501
                50   601
      2  1000    0   110
                15   210
                25   310
                35   410
                45   510
                55   610
                65   710
         1010    0   150
                10   250
                20   350
                30   450
                40   550

对于其他问题,您将使用while() { substr() }循环而不是FIELDWIDTHS,这样虽然要多写几行代码,但仍然很琐碎。

以上内容将比等效的shell脚本快几个数量级。参见https://unix.stackexchange.com/questions/169716/why-is-using-a-shell-loop-to-process-text-considered-bad-practice

答案 1 :(得分:3)

这并不容易,因为很难确定何时具有不同的行样式-第1列和第2列中都有值的行,第1列中没有值和第2列中的值的行以及第1列或第2列中没有值。第一步是使此操作更容易— sed进行救援:

$ sed 's/[[:space:]]\{1,\}$//
s/^....../&|/
s/|....../&|/
:a
s/|\(  *[0-9][0-9]* \)\( *[^|]\)/|\1|\2/
t a' data
    1 | 1000 |   0 |  100 |  10 |  200 |  20 |  300 |  30 |  400
      |      |  40 |  500 |  50 |  600 |  60 |  700 |  70 |  800
      | 1010 |   0 |  101 |  10 |  201 |  20 |  301 |  30 |  401
      |      |  40 |  501 |  50 |  601
    2 | 1000 |   0 |  110 |  15 |  210 |  25 |  310 |  35 |  410
      |      |  45 |  510 |  55 |  610 |  65 |  710
      | 1010 |   0 |  150 |  10 |  250 |  20 |  350 |  30 |  450
      |      |  40 |  550
$

第一行删除任何结尾的空格,以避免混淆。接下来的两个表达式处理固定宽度的列1和2(每个列6个字符)。下一行创建标签a;替代者找到一个管道|,一些空格,一些数字,一个空格以及一些不包含管道的尾随材料;并在中间插入管道。如果完成替换,t a会跳回到标签。

有了这一点,使用awk字段分隔符就可以轻松管理|。 这很冗长,但似乎可以解决问题:

awk -F '|' '
$1 > 0 { printf "%5d  %4d  %3d  %3d\n", $1, $2, $3, $4
         for (i = 5; i <= NF; i += 2) { printf "%5s  %4s  %3d  %3d\n", "", "", $i, $(i+1) }
         next
       }
$2 > 0 { printf "%5s  %4d  %3d  %3d\n", "", $2, $3, $4
         for (i = 5; i <= NF; i += 2) { printf "%5s  %4s  %3d  %3d\n", "", "", $i, $(i+1) }
         next
       }
       { for (i = 3; i <= NF; i += 2) { printf "%5s  %4s  %3d  %3d\n", "", "", $i, $(i+1) }
         next
       }'

输出:

    1  1000    0  100
              10  200
              20  300
              30  400
              40  500
              50  600
              60  700
              70  800
       1010    0  101
              10  201
              20  301
              30  401
              40  501
              50  601
    2  1000    0  110
              15  210
              25  310
              35  410
              45  510
              55  610
              65  710
       1010    0  150
              10  250
              20  350
              30  450
              40  550

如果需要删除标题,请将1d;添加到sed脚本的开头。

答案 2 :(得分:1)

这可能对您有用(GNU sed):

sed -r '1d;s/^(.{11}).{11}/&\n\1/;s/^((.{5}).*\n)\2/\1     /;s/^(.{5}(.{6}).*\n.{5})\2/\1      /;/\S/P;D' file

删除第一行(如果需要标题,请参见下文)。键域占据前11个字符(第一个键为5个字符,后6个为字符),数据域占据下11个字符。在每对数据域之前插入换行符和键域。比较相邻行上的键,如果重复则用空格替换。不要打印任何空白行。

如果需要标题,请使用以下内容:

sed -r '1{s/\[[^]]+\]\s*//5g;y/[]/  /;s/1/n/3g;s/B/ B/;G;b};s/^(.{11}).{11}/&\n\1/;s/^((.{5}).*\n)\2/\1     /;s/^(.{5}(.{6}).*\n.{5})\2/\1      /;/\S/P;D' file

这会在第一行进行额外的格式化,以删除多余的标题[],将1替换为n,添加额外的对齐空间和随后的空行

更多。通过将输入文件的第二行用作数据模板,可以创建不具有任何hard coded值的sed脚本:

sed -r '2!d;s/\s*\S*//3g;s/.\>/&\n/;h;s/[^\n]/./g;G;s/[^\n.]/ /g;s#(.*)\n(.*)\n(.*)\n(.*)#1d;s/^(\1\2)\1\2/\&\\n\\1/;s/^((\1).*\\n)\\2/\\1\3/;s/^(\1(\2).*\\n\1)\\2/\\1\4/;/\\S/P;D#' file |
sed -r -f - file

将从模板创建的脚本作为文件通过管道传递给sed的第二次调用,并针对原始文件运行以产生所需的输出。

同样,如果需要,标题可以被格式化:

sed -r '2!d;s/\s*\S*//3g;s/.\>/&\n/;h;s/[^\n]/./g;G;s/[^\n.]/ /g;s#(.*)\n(.*)\n(.*)\n(.*)#s/^(\1\2)\1\2/\&\\n\\1/;s/^((\1).*\\n)\\2/\\1\3/;s/^(\1(\2).*\\n\1)\\2/\\1\4/;/\\S/P;D#' file |
sed -r -e '1{s/\[[^]]+\]\s*//5g;y/[]/  /;s/1/n/3g;s/B/ B/;G;b}' -f - file

通过从输入文件的第二行提取前四个字段,可以创建四个变量。两个正则表达式和两个值。这些变量可用于构建sed脚本。

sed脚本是根据从模板中提取的字符串创建的,并且所产生的变量也是字符串,因此可以将它们连接起来以产生其他新的正则表达式和新值等

答案 3 :(得分:0)

这是一个相当棘手的问题,可以通过多种方法来解决。无论是bashperl还是awk,您都需要以某种半通用的方式处理字段数,而不仅仅是对示例中的值进行硬编码。

使用bash,只要您可以依赖所有行中偶数个字段(除了具有唯一初始值的行(例如1010),就可以容纳一个字段对于具有1, 2等的行,您知道初始输出将包含4-fields;对于具有1010等的行,您知道输出将包含初始3-fields。对于其余的值,您只需输出

棘手的部分是处理 alignment 。这是printf的地方,它允许您使用"%*s"形式的参数设置 field-width ,其中转换说明符期望下一个参数为{{1} }值,指定 field-width ,后跟字符串转换本身的参数。这需要一些体操,但是您可以在bash本身中执行以下操作:

(注意:进行编辑以匹配您的输出标题格式)

integer

使用重定向将输入文件重定向到脚本(而不是读取文件)(如果您只想提供文件名,则重定向文件以提供输出#!/bin/bash declare -i nfields wd=6 ## total no. fields, printf field-width modifier while read -r line; do ## read each line (preserve for header line) arr=($line) ## separate into array first=${arr[0]} ## check for '#' in first line for header if [ "${first:0:1}" = '#' ]; then nfields=$((${#arr[@]} - 2)) ## no. fields in header printf "## A1 B1 Tn Vn ## --> headers\n" ## new header continue fi fields=${#arr[@]} ## fields in line case "$fields" in $nfields ) ## fields -eq nfiles? cnt=4 ## handle 1st 4 values in line printf " " for ((i=0; i < cnt; i++)); do if [ "$i" -eq '2' ]; then printf "%*s" "5" "${arr[i]}" else printf "%*s" "$wd" "${arr[i]}" fi done echo for ((i = cnt; i < $fields; i += 2)); do ## handle rest printf "%*s%*s%*s\n" "$((2*wd))" " " "$wd" "${arr[i]}" "$wd" "${arr[$((i+1))]}" done ;; $((nfields - 1)) ) ## one less than nfields cnt=3 ## handle 1st 3 values printf " %*s%*s" "$wd" " " for ((i=0; i < cnt; i++)); do if [ "$i" -eq '1' ]; then printf "%*s" "5" "${arr[i]}" else printf "%*s" "$wd" "${arr[i]}" fi done echo for ((i = cnt; i < $fields; i += 2)); do ## handle rest if [ "$i" -eq '0' ]; then printf "%*s%*s%*s\n" "$((wd+1))" " " "$wd" "${arr[i]}" "$wd" "${arr[$((i+1))]}" else printf "%*s%*s%*s\n" "$((2*wd))" " " "$wd" "${arr[i]}" "$wd" "${arr[$((i+1))]}" fi done ;; * ) ## all other lines format as pairs for ((i = 0; i < $fields; i += 2)); do printf "%*s%*s%*s\n" "$((2*wd))" " " "$wd" "${arr[i]}" "$wd" "${arr[$((i+1))]}" done ;; esac done 循环)

使用/输出示例

while read...

$ bash text1format.sh <dat/text1.txt ## A1 B1 Tn Vn ## --> headers 1 1000 0 100 10 200 20 300 30 400 40 500 50 600 60 700 70 800 1010 0 101 10 201 20 301 30 401 40 501 50 601 2 1000 0 110 15 210 25 310 35 410 45 510 55 610 65 710 1010 0 150 10 250 20 350 30 450 40 550 awk之间,bash通常会更快,但是在这里使用格式化输出时,它可能比平时更近。仔细研究一下,如果您有任何疑问,请告诉我。