重复数据但行数不同的连续排序
Posted
技术标签:
【中文标题】重复数据但行数不同的连续排序【英文标题】:Successive sorting of repeating data but different number of lines 【发布时间】:2021-09-25 16:47:46 【问题描述】:我有一个格式如下的数据:
2
1
A 11.364500 48.395199 40.160500 5
B 35.067699 30.944700 24.155300 4
4
2
A 26.274099 38.533298 32.624298 4
B 36.459000 29.662701 17.806299 5
A 15.850300 28.130899 24.753901 4
A 32.098000 33.665699 20.372499 4
5
3
A 17.501801 44.279202 8.005670 5
B 35.853001 43.095901 17.402901 4
B 1.326100 17.127600 39.600300 4
A 9.837760 41.103199 13.062300 5
B 31.686800 44.997501 16.619499 4
3
4
B 31.274700 8.726580 25.267599 4
A 19.032400 41.384701 19.456301 5
B 19.441900 24.286400 6.961680 4
1
5
B 48.973999 15.508400 5.099710 4
6
6
A 42.586601 21.343800 23.280701 5
B 30.145800 13.256200 30.713800 4
B 29.186001 44.353699 9.057160 4
A 39.311199 27.371201 35.473999 5
B 31.437799 30.415001 37.454399 4
B 48.501999 1.277730 21.900600 4
...
数据由多个重复的数据块组成。每个数据块的格式相同,如下:
First line = Number of lines of data block
Second line = ID of data block
From third line = Actual data lines (with number of lines designated in the first line)
例如,如果我们看到第一个数据块:
2 (=> This data block has 2 number of lines)
1 (=> This data block's ID is 1 (very first data block))
A 11.364500 48.395199 40.160500 5
B 35.067699 30.944700 24.155300 4
(Third and fourth lines of the data block are actual data.
The integer of the first line, the number of lines for this data block, is 2, so the actual data consist of 2 lines.
All other data blocks follow the same format. )
-
前两行是每个重复数据块的标题。第一行指定数据块的行数,第二行是数据块的ID。
然后真正的数据从每个数据块的第三行开始。正如我所解释的,“真实数据”的行数在第一行中指定为整数。
因此,每个数据块的总行数将为 number_of_lines+2。在本例中,数据块 1 的总行数为 4,数据块 2 花费 6 行...
这种格式重复 100,000 次。换句话说,我在单个文本文件中有 100,000 个数据块,就像这个例子一样。我可以提供数据块的总数。
我要做的是按第四列对每个数据块的“实际数据行”进行排序,并以与原始数据文件相同的格式打印出数据。我希望从上面的示例中实现的效果如下:
2
1
A 11.364500 48.395199 40.160500 5
B 35.067699 30.944700 24.155300 4
4
2
A 26.274099 38.533298 32.624298 4
A 15.850300 28.130899 24.753901 4
A 32.098000 33.665699 20.372499 4
B 36.459000 29.662701 17.806299 5
5
3
B 1.326100 17.127600 39.600300 4
B 35.853001 43.095901 17.402901 4
B 31.686800 44.997501 16.619499 4
A 9.837760 41.103199 13.062300 5
A 17.501801 44.279202 8.005670 5
3
4
B 31.274700 8.726580 25.267599 4
A 19.032400 41.384701 19.456301 5
B 19.441900 24.286400 6.961680 4
1
5
B 48.973999 15.508400 5.099710 4
6
6
B 31.437799 30.415001 37.454399 4
A 39.311199 27.371201 35.473999 5
B 30.145800 13.256200 30.713800 4
A 42.586601 21.343800 23.280701 5
B 48.501999 1.277730 21.900600 4
B 29.186001 44.353699 9.057160 4
...
我知道在 Linux 中使用第 4 列对数据进行排序的典型命令是:
sort -gk4 data.txt > data_4sort.txt
但是,在这种情况下,我需要对每个数据块进行排序。另外,每个数据块的行数并不统一,但每个数据块的数据行数不同。
我真的不知道我该如何解决这个问题。我正在考虑使用带有 for 循环和/或 awk、sed、split 等的 shell 脚本和 sort 命令。但是我什至不确定如何使用 for 循环来解决这个问题,或者是否真的需要 for 循环和 shell 脚本来执行这种逐块排序。
【问题讨论】:
【参考方案1】:这可以使用awk
和sort
的组合来完成。你使用awk
来驱动整个过程,就像一个非常简单的状态机,它遵循以下规则:
动作基本上是针对每一行的。
简而言之: 对于短行,只需打印即可。 否则(长行),仅使用该行创建新的排序文件,将状态设置为长。 否则(处于长状态): 如果行长,只需将其附加到排序文件即可。 否则(短行),排序文件,打印短行,将状态设置为短行。最后,如果需要,对排序文件进行排序/打印/删除(因为在最后一个块之后不会有从长行到短行的最终转换,所以这个操作会解决这个问题)。
该处理按以下方式实现,一个调用awk
来完成工作的shell 脚本:
awk '
function sort()
close(tmp_file)
system("sort -rnk4 "tmp_file"; rm -rf "tmp_file)
BEGIN tmp_file = "/tmp/tmp_file"
state == 0 && NF == 1 print ; next
state == 0 && NF != 1 print >tmp_file ; state = 1 ; next
state == 1 && NF != 1 print >>tmp_file ; next
state == 1 && NF == 1 sort() ; print ; state = 0 ; next
END if (state == 1) sort()
' input_file
您也可以不使用足够高级的awk
变体的临时文件来执行此操作,该变体允许管道:
awk 'NF > 1 print | "sort -rnk4"; next
close("sort -rnk4"); print
' input_file
这样做的好处是一个更简单、更短的awk
程序,它隐式地完成了前面脚本中的大部分内容。
在您的输入文件上运行任一个这些脚本会根据要求生成按第四列(最终浮点数)排序的每个单独分组:
2
1
A 11.364500 48.395199 40.160500 5
B 35.067699 30.944700 24.155300 4
4
2
A 26.274099 38.533298 32.624298 4
A 15.850300 28.130899 24.753901 4
A 32.098000 33.665699 20.372499 4
B 36.459000 29.662701 17.806299 5
5
3
B 1.326100 17.127600 39.600300 4
B 35.853001 43.095901 17.402901 4
B 31.686800 44.997501 16.619499 4
A 9.837760 41.103199 13.062300 5
A 17.501801 44.279202 8.005670 5
3
4
B 31.274700 8.726580 25.267599 4
A 19.032400 41.384701 19.456301 5
B 19.441900 24.286400 6.961680 4
1
5
B 48.973999 15.508400 5.099710 4
6
6
B 31.437799 30.415001 37.454399 4
A 39.311199 27.371201 35.473999 5
B 30.145800 13.256200 30.713800 4
A 42.586601 21.343800 23.280701 5
B 48.501999 1.277730 21.900600 4
B 29.186001 44.353699 9.057160 4
【讨论】:
创建显式临时文件有好处吗? IE。为什么不只在 awk 中使用print | "sort ..."
?是性能问题还是可移植性问题?
谢谢。我还有一个要求。事实上,“真实数据”的第一列可以有 2 个字母字符,如“AA”或“AB”。抱歉,我忘记在示例中提及这一点。这种情况下需要在awk/sort命令中修改什么?
@exsonic01: 什么都不需要改变,“长”行完全按原样写入文件,排序只是对字段 4 进行反向数字排序。
关于 an advanced enough awk variant
- 在任何 awk 中的行为方式都相同。
这会很慢,因为您要生成一个子shell 来为每个输入块调用一次“排序”。你在做shell awk FOR each line DO system shell sort ENDFOR
。在 awk 中不生成子壳会更有效,而只需执行 shell awk | sort
或类似操作。如果在脚本开始之前就存在 /tmp/tmp_file
,它也会破坏 - 最好调用 mktemp
来创建任何临时文件。【参考方案2】:
一个选项可以在不显式创建临时文件的情况下通过管道传递到以下命令。
awk '
BEGIN
cmd = "sort -k4,4nr"
if(NF > 1)
print | cmd
else
close(cmd)
print
' input_file
注意:由于@Ed Morton 的有用提示,已更改为数字排序并将 cmd 移动到变量
输出:
2
1
A 11.364500 48.395199 40.160500 5
B 35.067699 30.944700 24.155300 4
4
2
A 26.274099 38.533298 32.624298 4
A 15.850300 28.130899 24.753901 4
A 32.098000 33.665699 20.372499 4
B 36.459000 29.662701 17.806299 5
5
3
B 1.326100 17.127600 39.600300 4
B 35.853001 43.095901 17.402901 4
B 31.686800 44.997501 16.619499 4
A 9.837760 41.103199 13.062300 5
A 17.501801 44.279202 8.005670 5
3
4
B 31.274700 8.726580 25.267599 4
A 19.032400 41.384701 19.456301 5
B 19.441900 24.286400 6.961680 4
1
5
B 48.973999 15.508400 5.099710 4
6
6
B 31.437799 30.415001 37.454399 4
A 39.311199 27.371201 35.473999 5
B 30.145800 13.256200 30.713800 4
A 42.586601 21.343800 23.280701 5
B 48.501999 1.277730 21.900600 4
B 29.186001 44.353699 9.057160 4
【讨论】:
不错的答案,唯一的缺点是它会很慢,因为 awk 必须分拆一个子外壳来调用sort
每个输入记录一次。仅供参考,在任何 awk 中的行为方式都相同。我建议将其设为BEGIN cmd="sort -k4,4r" and then
print | cmd` 和 close(cmd)
以避免将来发生任何更改,即您在一个地方更改命令而忘记在另一个地方更改它并最终出现神秘错误。
@EdMorton 谢谢你的解释。您能否指出有关其工作原理的文档?直觉上很难理解它如何在每一行上调用一个新的排序,因为排序本质上应该一次需要处理多个行才能工作。但是,这对我来说可能不直观,因为我是 unix 工具的新手 :)
它不会对每一行调用一个新的排序,而是对每条记录调用一个新的排序(即具有 > 1 个字段的行块)。在您的代码中显示print | "sort -k4,4r"
,如果没有这样的调用“sort”的子shell正在运行,那么awk会生成一个新的子shell来调用“sort”,但是如果这样的子shell已经在运行,那么它只是将当前行通过管道传递给它.现在看看你的else
行close("sort -k4,4r")
正是这样做的,它关闭管道以“排序”从而杀死子shell。所以下次if (NF > 1)
变为真时,awk 必须重新生成一个子shell。
在gnu.org/software/gawk/manual/gawk.html#I_002fO-Functions上有更多信息。
如果不是 100% 清楚,请随时提问。【参考方案3】:
使用 awk 准备文本以进行排序,然后将其通过管道传递给 1 次对 sort
的调用,而不会产生子 shell,也不会创建临时文件:
$ cat tst.sh
#!/usr/bin/env bash
awk '
BEGIN OFS="\t"; recNr=1
NF < pNF ++recNr
print recNr, NF, NR, $0
pNF = NF
' "$@:--" |
sort -k1,1n -k2,2n -k7,7nr -k3,3n |
cut -f4-
$ ./tst.sh file
2
1
A 11.364500 48.395199 40.160500 5
B 35.067699 30.944700 24.155300 4
4
2
A 26.274099 38.533298 32.624298 4
A 15.850300 28.130899 24.753901 4
A 32.098000 33.665699 20.372499 4
B 36.459000 29.662701 17.806299 5
5
3
B 1.326100 17.127600 39.600300 4
B 35.853001 43.095901 17.402901 4
B 31.686800 44.997501 16.619499 4
A 9.837760 41.103199 13.062300 5
A 17.501801 44.279202 8.005670 5
3
4
B 31.274700 8.726580 25.267599 4
A 19.032400 41.384701 19.456301 5
B 19.441900 24.286400 6.961680 4
1
5
B 48.973999 15.508400 5.099710 4
6
6
B 31.437799 30.415001 37.454399 4
A 39.311199 27.371201 35.473999 5
B 30.145800 13.256200 30.713800 4
A 42.586601 21.343800 23.280701 5
B 48.501999 1.277730 21.900600 4
B 29.186001 44.353699 9.057160 4
这是上述 OPs 输入文件在执行速度上的第 3 次运行时间差异:
$ time ./tst.sh file >/dev/null
real 0m0.105s
user 0m0.030s
sys 0m0.091s
当前的 awk 答案会为每个要排序的输入块生成一个子shell:
@IceCreamTucan's answer:
$ cat tst2.sh
#!/usr/bin/env bash
awk '
if(NF > 1)
print | "sort -k4,4nr"
else
close("sort -k4,4nr")
print
' "$@:--"
$ time ./tst2.sh file >/dev/null
real 0m0.405s
user 0m0.120s
sys 0m0.121s
@paxdiablo's answer:
$ cat tst3.sh
#!/usr/bin/env bash
awk '
function sort()
close(tmp_file)
system("sort -rnk4 "tmp_file"; rm -rf "tmp_file)
BEGIN tmp_file = "/tmp/tmp_file"
state == 0 && NF == 1 print ; next
state == 0 && NF != 1 print >tmp_file ; state = 1 ; next
state == 1 && NF != 1 print >>tmp_file ; next
state == 1 && NF == 1 sort() ; print ; state = 0 ; next
END if (state == 1) sort()
' "$@:--"
$ time ./tst3.sh file >/dev/null
real 0m0.804s
user 0m0.060s
sys 0m0.396s
【讨论】:
我会给你一个投票,Ed,它比我的性能更好(考虑到 OP 规定了 100,000 个块,这可能很重要)。干杯。【参考方案4】:我知道,老问题,但是...另一个想法,使用更新的gawk
能力更高效...
使用gawk
及其predefined array scanning orders 的一个想法(即,无需外部sort
)...
awk '
BEGIN delete arr # hack to pre-declare variable arr as an array
function sort_block()
if ( length(arr) > 0 ) # if array is not empty then ...
PROCINFO["sorted_in"]="@ind_num_desc" # desginate sorting by index/descending and then ...
for (i in arr) # loop through array ...
print arr[i] # printing arry entries
delete arr # clear array
NF==1 sort_block()
print
NF>1 arr[$4]=$0
next
END sort_block() # flush anything still in array
' block.dat
针对 OPs 样本数据集运行此操作会生成:
2
1
A 11.364500 48.395199 40.160500 5
B 35.067699 30.944700 24.155300 4
4
2
A 26.274099 38.533298 32.624298 4
A 15.850300 28.130899 24.753901 4
A 32.098000 33.665699 20.372499 4
B 36.459000 29.662701 17.806299 5
5
3
B 1.326100 17.127600 39.600300 4
B 35.853001 43.095901 17.402901 4
B 31.686800 44.997501 16.619499 4
A 9.837760 41.103199 13.062300 5
A 17.501801 44.279202 8.005670 5
3
4
B 31.274700 8.726580 25.267599 4
A 19.032400 41.384701 19.456301 5
B 19.441900 24.286400 6.961680 4
1
5
B 48.973999 15.508400 5.099710 4
6
6
B 31.437799 30.415001 37.454399 4
A 39.311199 27.371201 35.473999 5
B 30.145800 13.256200 30.713800 4
A 42.586601 21.343800 23.280701 5
B 48.501999 1.277730 21.900600 4
B 29.186001 44.353699 9.057160 4
在笔记本电脑级 i7(即,这绝不是快速系统)上的 VM 内的 cygwin
环境中重复运行此代码可提供以下最佳时机:
real 0m0.026s
user 0m0.000s
sys 0m0.015s
【讨论】:
以上是关于重复数据但行数不同的连续排序的主要内容,如果未能解决你的问题,请参考以下文章