awk 关联数组增长很快

awk associative array grows fast

我有一个文件将数字分配给 md5sums,如下所示:

0   0000001732816557DE23435780915F75
1   00000035552C6F8B9E7D70F1E4E8D500
2   00000051D63FACEF571C09D98659DC55
3   0000006D7695939200D57D3FBC30D46C
4   0000006E501F5CBD4DB56CA48634A935
5   00000090B9750D99297911A0496B5134
6   000000B5AEA2C9EA7CC155F6EBCEF97F
7   00000100AD8A7F039E8F48425D9CB389
8   0000011ADE49679AEC057E07A53208C1

另一个文件每行包含三个 md5sums,如下所示:

00000035552C6F8B9E7D70F1E4E8D500    276EC96E149571F8A27F4417D7C6BC20    9CFEFED8FB9497BAA5CD519D7D2BB5D7
00000035552C6F8B9E7D70F1E4E8D500    44E48C092AADA3B171CE899FFC6943A8    1B757742E1BF2AA5DB6890E5E338F857

我想要的是用第一个文件的整数替换第二个文件中的第一个和第三个md5sums。目前我正在尝试以下 awk 脚本:

awk '{OFS="\t"}FNR==NR{map[]=;next}
{print map[],,map[]}' mapping.txt relation.txt

问题是尽管第一个文件在硬盘上只有 5.7g,但脚本需要超过 16g 的内存。

如果你没有足够的内存来存储第一个文件,那么你需要写这样的东西来为第二个文件中的每个值查找第一个文件:

awk 'BEGIN{OFS="\t"}
{
    val1 = val3 = ""
    while ( (getline line < "mapping.txt") > 0 ) {
        split(line,flds)
        if (flds[2] == ) {
            val1 = flds[1]
        }
        if (flds[2] == ) {
            val3 = flds[1]
        }
        if ( (val1 != "") && (val3 != "") ) {
            break
        }
    }
    close("mapping.txt")

    print val1,,val3

}' relation.txt

会很慢。如果你愿意,你可以添加一个 N getline-d 行的缓存来加速它。

这个问题是可以解决的,如下(file1.txt是整数和md5sums的文件,而file2.txt是三列md5sums的文件)​​:

#!/bin/sh
# First sort each of file 1 and the first and third columns of file 2 by MD5
awk '{ print  "\t" }' file1.txt | sort >file1_n.txt
# Before we sort the file 2 columns, we number the rows so we can put them
# back into the original order later
cut -f1 file2.txt | cat -n - | awk '{ print  "\t" }' | sort >file2_1n.txt
cut -f3 file2.txt | cat -n - | awk '{ print  "\t" }' | sort >file2_3n.txt
# Now do a join between them, extract the two columns we want, and put them back in order
join -t'    ' file2_1n.txt file1_n.txt | awk '{ print  "\t" }' | sort -n | cut -f2 >file2_1.txt
join -t'    ' file2_3n.txt file1_n.txt | awk '{ print  "\t" }' | sort -n | cut -f2 >file2_3.txt
cut -f2 file2.txt | paste file2_1.txt - file2_3.txt >file2_new1.txt

对于 file1.txtfile2.txt 各有 100 万行的情况,此解决方案和 Ed Morton 的 awk-only 解决方案在我的系统上花费的时间大致相同.无论使用何种方法,我的系统都需要很长时间才能解决 1.4 亿行的问题,但我 运行 一个包含 1000 万行文件的测试用例。

我假设依赖于 sort 的解决方案(在需要时自动使用临时文件)对于大量行来说应该更快,因为它将是 O(N log N) 运行time,而如果两个文件的大小相似,则为输入的每一行重新读取映射文件的解决方案将是 O(N^2)。

计时结果

我对两个候选解决方案的性能关系的假设对于我尝试过的测试用例来说是错误的。在我的系统上,基于 sort 的解决方案和仅基于 awk 的解决方案对于 100 万行和 1000 万行输入文件中的每一个都花费了相似的时间(在 30% 以内),其中awk-唯一的解决方案在每种情况下都更快。当然,当输入文件大小再增加 10 倍以上时,我不知道这种关系是否成立。

St运行gely,1000 万行问题花费的时间大约是 运行 的 10 倍,这两种解决方案都是 100 万行问题,这让我很困惑,因为我预计不会- 两种解决方案与文件长度的线性关系。

如果文件的大小导致 awk 运行 内存不足,则要么使用其他工具,要么完全采用其他方法。

sed 命令可能会成功使用更少的内存。这个想法是读取索引文件并创建一个执行重新映射的 sed 脚本,然后在生成的 sedscript 上调用 sed。

下面的 bash 脚本就是这个想法的实现。它包括一些 STDERR 输出以帮助跟踪进度。我喜欢为大型数据集问题或其他类型的耗时处理生成进度跟踪输出。

该脚本已经在一小部分数据上进行了测试;它可能 处理您的数据。请试一试。

#!/bin/bash

# md5-indexes.txt
# 0   0000001732816557DE23435780915F75
# 1   00000035552C6F8B9E7D70F1E4E8D500
# 2   00000051D63FACEF571C09D98659DC55
# 3   0000006D7695939200D57D3FBC30D46C
# 4   0000006E501F5CBD4DB56CA48634A935
# 5   00000090B9750D99297911A0496B5134
# 6   000000B5AEA2C9EA7CC155F6EBCEF97F
# 7   00000100AD8A7F039E8F48425D9CB389
# 8   0000011ADE49679AEC057E07A53208C1

# md5-data.txt
# 00000035552C6F8B9E7D70F1E4E8D500    276EC96E149571F8A27F4417D7C6BC20    9CFEFED8FB9497BAA5CD519D7D2BB5D7
# 00000035552C6F8B9E7D70F1E4E8D500    44E48C092AADA3B171CE899FFC6943A8    1B757742E1BF2AA5DB6890E5E338F857

# Goal replace field 1 and field 3 with indexes to md5 checksums from md5-indexes

md5_indexes='md5-indexes.txt'
md5_data='md5-data.txt'

talk()  { echo 1>&2 "$*" ; }
talkf() { printf 1>&2 "$@" ; }
track() {
  local var="" interval=""
  local val
  eval "val=$$var"
  if (( interval == 0 || val % interval == 0 )); then
    shift 2
    talkf "$@"
  fi
  eval "(( $var++ ))"   # increment the counter
}

# Build a sedscript to translate all occurances of the 1st & 3rd MD5 sums into their
# corresponding indexes

talk "Building the sedscript from the md5 indexes.."

sedscript=/tmp/$$.sed

linenum=0
lines=`wc -l <$md5_indexes`
interval=$(( lines / 100 ))

while read index md5sum ; do
  track linenum $interval "..$linenum"
  echo "s/^[[:space:]]*[[:<:]]$md5sum[[:>:]]/$index/" >>$sedscript
  echo "s/[[:<:]]$md5sum[[:>:]]$/$index/"            >>$sedscript
done <$md5_indexes
talk ''

sedlength=`wc -l <$sedscript`

talkf "The sedscript is %d lines\n" $sedlength

cmd="sed -E -f $sedscript -i .bak $md5_data"
talk "Invoking: $cmd"

$cmd

changes=`diff -U 0 $md5_data.bak $md5_data | tail +3 | grep -c '^+'`

talkf "%d lines changed in $md5_data\n" $changes

exit

这是两个文件:

cat md5-indexes.txt
0   0000001732816557DE23435780915F75
1   00000035552C6F8B9E7D70F1E4E8D500
2   00000051D63FACEF571C09D98659DC55
3   0000006D7695939200D57D3FBC30D46C
4   0000006E501F5CBD4DB56CA48634A935
5   00000090B9750D99297911A0496B5134
6   000000B5AEA2C9EA7CC155F6EBCEF97F
7   00000100AD8A7F039E8F48425D9CB389
8   0000011ADE49679AEC057E07A53208C1

cat md5-data.txt
00000035552C6F8B9E7D70F1E4E8D500    276EC96E149571F8A27F4417D7C6BC20    9CFEFED8FB9497BAA5CD519D7D2BB5D7
00000035552C6F8B9E7D70F1E4E8D500    44E48C092AADA3B171CE899FFC6943A8    1B757742E1BF2AA5DB6890E5E338F857

这是示例 运行:

$ ./md5-reindex.sh
Building the sedscript from the md5 indexes..
..0..1..2..3..4..5..6..7..8
The sedscript is 18 lines
Invoking: sed -E -f /tmp/83800.sed -i .bak md5-data.txt
2 lines changed in md5-data.txt

最后生成的文件:

$ cat md5-data.txt
1    276EC96E149571F8A27F4417D7C6BC20    9CFEFED8FB9497BAA5CD519D7D2BB5D7
1    44E48C092AADA3B171CE899FFC6943A8    1B757742E1BF2AA5DB6890E5E338F857