【问题标题】:Interleave text files with a given ratio of lines from file1 to file2以给定的行数从 file1 到 file2 交错文本文件
【发布时间】:2020-05-23 13:21:26
【问题描述】:

我有 2 个文本文件。一个包含 3 倍于另一个的行数。较小的包含标题,我想以 3:1 的比例与较大的文本文件的行交错

例如

小文件:

header1
header2
header3

大文件

lines1.1
lines1.2
lines1.3
lines2.1
lines2.2
lines2.3
lines3.1
lines3.2
lines3.3

变成:

header1
lines1.1
lines1.2
lines1.3
header2
lines2.1
lines2.2
lines2.3
header3
lines3.1
lines3.2
lines3.3

我的问题有一个 shell 解决方案:

new_reads_no="$(wc -l small_file.txt | awk '{print $1}')"
sequence="$(seq 1 $new_reads_no)"

for i in $sequence
do
    start=$((3*($i-1)+1))
    end=$(($start+2))
    awk -v c1=$i 'FNR==c1' small_file.txt >> Output.txt
    awk -v s="$start" -v e="$end" 'NR>=s&&NR<=e' big_file.txt >> Output.txt
done

效果很好。但是,我的小文件是 1000 万行。按照目前的进度,我估计它会在大约一年内完成。

任何帮助加快这一进程将不胜感激。一个简单的、无循环的 shell 衬里,甚至只是另一种语言的快速工具都会很棒。

【问题讨论】:

  • 如果您有GNU sed,请查看sed -e 'R f2' -e 'R f2' -e 'R f2' f1 的性能如何,其中f1 是较小的文件
  • 关于您当前的解决方案为何缓慢,请参阅:unix.stackexchange.com/questions/169716/…
  • @Sundeep 您的链接很有用,但这根本不是这里发生的事情。
  • 完美! sed 线正是我正在寻找的。真正在大约 5 秒内完成了需要一年时间的工作

标签: bash shell text awk


【解决方案1】:

好老paste:

paste -d '\n' fsmall - - - <fbig

概要 paste [-s] [-d list] file ... file
操作数 文件:输入文件的路径名。如果为一个或多个文件指定-,则应使用标准输入;对于- 的每个实例,标准输入应一次读取一行,循环读取。

来源:POSIX paste

这意味着,每个 字符从stdin 读取一行,在这种情况下定义为fbig。三个连字符,表示三行。

好老的awk 没有缓冲:

awk -v r=3 '1;{for(i=1;i<=r;++i) {getline < "-"; print}}' fsmall <fbig

此方法模仿paste-解决方案的想法。它使用getline 来避免小文件的缓冲。这不是很灵活,在使用getline 时应始终小心[参见All about getline]

好老的 awk 带缓冲:

awk -v r=3 '(NR==FNR){b[FNR]=$0;next}(FNR%r==1){print b[++c]}1' fsmall fbig

这会缓冲小文件。当小文件非常大时,这可能会导致性能问题。 (见 Tripleee 的comment

【讨论】:

  • Awk 解决方案将整个较小的文件读入内存;如果你有足够的内存,那很好,但如果你没有,它不会很优雅地降级。
  • @kvantour paste 很好,但是awk 给出了错误的结果。
  • @triplee 正确。我添加了一个无缓冲版本(类似于粘贴的行为)
  • @Ivan 有错别字
  • @kvantour 太棒了
【解决方案2】:

GNU sed

sed -e 'R f2' -e 'R f2' -e 'R f2' f1

其中f1 是较小的文件。 R 命令从给定文件中一次读取一行。由此获得的行被附加在从f1读取的当前行之后

【讨论】:

    【解决方案3】:

    反复重新打开每个输入文件并寻找您上次停止阅读的位置是非常低效的。更糟糕的是,您每次都将 整个 输入文件读到最后,并且一路上只挑出一三行。一旦你打印了你想要的东西,你至少可以exit。但是坚持住。

    这是一个简单的 Python 脚本,它通过简单地保持两个文件打开并随时读取每个文件来满足您的要求。

    with open('small_file.txt') as small, open('big_file.txt') as large:
        for line in small:
            print(line, end='')
            for x in range(3):
                print(large.readline(), end='')
    

    如果您想参数化文件名,请尝试

    import sys
    
    with open(sys.argv[1]) as small, open(sys.argv[2]) as large:
        ...
    

    输出是标准输出,所以如果你将上面的内容保存到path/to/script.py,你可以简单地在shell提示符下运行:

    python3 path/to/script.py small_file.txt big_file.txt >Output.txt
    

    end='' 的使用是一个小技巧,以避免不得不删除换行符并让 print 将其添加回来。

    事后想到,您可以在 shell 脚本中做很多相同的事情;

    while IFS= read -r line; do
        printf '%s\n' "$line"
        for x in 1 2 3; do
            IFS= read -u 3 -r other
            printf '%s\n' "$other"
        done
    done <small_file.txt 3<big_file.txt >Output.txt
    

    但 shell 的 while read -r 循环本质上要慢得多。

    【讨论】:

    • read -u fh 是一个 Bash 扩展。将 Bash 脚本重铸为 POSIX sh 脚本应该不会太难,但它不会很快,所以我们不要打扰。
    • 如果你使用 Bash4+,mapfile 回调非常有效地一次读取所有 3 行并打印标题:while IFS= read -r;do mapfile -tn3 -u3 -C'echo "$REPLY";:' -c3;printf '%s\n' "${MAPFILE[@]}";done &lt;small_file.txt 3&lt;big_file.txt
    【解决方案4】:

    如果您的小文件足够小以适合内存:

    $ awk 'NR==FNR{hdrs[NR]=$0; next} NR%3 == 1{print hdrs[++c]} 1' small big
    header1
    lines1.1
    lines1.2
    lines1.3
    header2
    lines2.1
    lines2.2
    lines2.3
    header3
    lines3.1
    lines3.2
    lines3.3
    

    否则:

    $ awk '(NR%3 == 1) && ((getline hdr < "small") > 0){print hdr} 1' big
    header1
    lines1.1
    lines1.2
    lines1.3
    header2
    lines2.1
    lines2.2
    lines2.3
    header3
    lines3.1
    lines3.2
    lines3.3
    

    请参阅 http://awk.freeshell.org/AllAboutGetline,了解我为什么使用我用来调用 getline 的语法以及为什么如果没有必要最好避免使用它。

    【讨论】:

      【解决方案5】:

      使用 Bash 版本 4 及更高版本:

      while IFS= read -r; do
        # Map 3 lines of big_file.txt without capturing newline character
        mapfile -t -n 3 -u 3
      
        # Output header $REPLY from small_file.txt
        # followed by ${MAPFILE[@]} mapped lines of big_file.txt
        printf '%s\n' "$REPLY" "${MAPFILE[@]}"
      done <small_file.txt 3<big_file.txt
      

      通过回调打印标题行:

      #!/usr/bin/env bash
      
      while IFS= read -r; do
        mapfile -t -n 3 -u 3 -C'echo "$REPLY";:' -c 3
        printf '%s\n' "${MAPFILE[@]}"
      done <small_file.txt 3<big_file.txt
      

      mapfile 用于从文件句柄 3 一次读取 3 行:

      • -t:不捕获换行符。

      • -n 3:将 3 行映射到 MAPFILE 数组中。

      • -u:从指向big_file.txt的文件句柄3读取。

      • -C'echo "$REPLY";:':回调指令。

      • -c 3:Quantum 调用回调(每 3 个映射行)。

      回调:

      • echo "$REPLY":输出$REPLY变量,其中包含从while循环中的small_file.txt读取的行。

      • ::虚拟 NOP 命令使用从mapfile 命令传递给回调的参数。

      mapfile 的回调缺少有关其参数的文档。他们在这里:

      1. 最后映射的数组索引

      2. 最后映射的数组值

      【讨论】:

        【解决方案6】:

        让我们通过ed(1) 加上shell 直接编辑大文件。

        将小文件保存在数组中。

        mapfile -t array < smallfile
        

        mapfile是bash4的特性,bash3可以做。

        array=()
        while IFS= read -r line; do
           array+=("$line")
        done < smallfile
        

        计算大文件中的行数加上我们将插入小文件行的间隔,并将其保存在名为count的数组中

        count=(); for ((i=1;i<=$(wc -l < bigfile); i += 4 )); do count+=("$i"); done
        

        虽然printf '%s\n' '$=' | ed -s bigfile 可以计算大文件中的总行数,但wc 就是为此目的而编写的。 $(wc -l &lt; bigfile)

        通过从小文件复制行将更改写入大文件。

        printf '%s\n' "${count[0]}i" "${array[0]}" . "${count[1]}i" "${array[1]}" . "${count[2]}i" "${array[2]}" . w | ed -s bigfile
        

        【讨论】:

          【解决方案7】:

          因此我没有睡觉。 Sundeep 指出的这个R 命令真的很酷,但实现起来有点令人沮丧。所以我在 sed 的文档中挖掘了一下,发现了这个 first~step

             first~step
                    Match  every  step'th  line starting with line first.  For example, ``sed -n 1~2p'' will print all the odd-numbered lines in the input stream,
                    and the address 2~5 will match every fifth line, starting with the second.  first can be zero; in this case, sed operates as if it were  equal
                    to step.  (This is an extension.)
          

          我已经试过了

          sed '1~3R headers' lines
          

          但结果不如预期

          line1.1
          header1
          line1.2
          line1.3
          line2.1
          header2
          line2.2
          line2.3
          line3.1
          header3
          line3.2
          

          因为R 正在追加行,所以可以通过在行文件中添加额外的第一行来避免这种情况

          sed -i '1s/^/\n/' lines
          

          比我们处理文件

          sed '1~3R headers' lines > output
          

          并从输出中删除这个额外的行

          sed -i '1d' output
          

          但是这种添加删除线的必要性也有点令人沮丧。 有没有更好的办法?

          我想知道 grep 是否足够快?可以试试吗?

          headers=( $(cat headers) )
          for header in ${headers[@]}; {
              echo $header >> output
              digit=${header//[!0-9]/}
              grep .*$digit. lines >> output
          }
          

          好的,哪种方法更快?我已经用这个制作了测试文件

          for i in {1..100}; { echo "header$i" >> headers; }
          for i in {1..100}; { for e in {1..3}; { echo "line$i.$e" >> lines;}; }
          

          100 和 300 行,并测试所有方法

          paste -d '\n' headers - - - <lines
          
          real    0m0,003s
          user    0m0,000s
          sys     0m0,003s
          
          sed -e 'R lines' -e 'R lines' -e 'R lines' headers
          
          real    0m0,003s
          user    0m0,000s
          sys     0m0,003s
          
          awk -v r=3 '1;{for(i=1;i<=r;++i) {getline < "-"; print}}' headers <lines
          awk -v r=3 '(NR==FNR){b[FNR]=$0;next}(FNR%r==1){print b[++c]}1' headers lines
          
          real    0m0,005s
          user    0m0,000s
          sys     0m0,005s
          

          这些都是明显的赢家。这也很快但不正确。

          $ time awk 'NR==FNR{hdrs[NR]=$0; next} NR%3 == 1{print hdrs[++c]} 1' headers lines | head -n7
          line1.1
          line1.2
          header1
          line1.3
          line2.1
          line2.2
          header2
          
          real    0m0,004s
          user    0m0,002s
          sys     0m0,003s
          

          好的,这就是 TS 使用的。

          fun1 () {
              new_reads_no="$(wc -l headers | awk '{print $1}')"
              sequence="$(seq 1 $new_reads_no)"
          
              for i in $sequence
              do
                  start=$((3*($i-1)+1))
                  end=$(($start+2))
                  awk -v c1=$i 'FNR==c1' headers
                  awk -v s="$start" -v e="$end" 'NR>=s&&NR<=e' lines
              done
          }
          
          real    0m0,341s
          user    0m0,219s
          sys     0m0,132s
          

          嗯,这根本不快)

          fun2 () {
              headers=( $(cat headers) )
              for header in ${headers[@]}; {
                  echo $header
                  digit=${header//[!0-9]/}
                  grep .*$digit. lines
              }
          }
          
          real    0m0,167s
          user    0m0,105s
          sys     0m0,068s
          

          我的也失败了,但仍然比第一个更快) 所以我在这里得到了真正需要的东西并做了这个。

          fun3 () {
              exec 3< headers
              exec 4< lines
          
              while read -u3 head do;
                  echo $head
                  for i in {1..3}; {
                      read -u4 line; echo $line
                  }
              done
          
              exec 3<&-
              exec 4<&-
          }
          
          real    0m0,009s
          user    0m0,009s
          sys     0m0,000s
          

          而且速度很快)

          import sys
          
          with open(sys.argv[1]) as small, open(sys.argv[2]) as large:
              for line in small:
                  print(line, end='')
                  for x in range(3):
                      print(large.readline(), end='')
          
          
          real    0m0,021s
          user    0m0,016s
          sys     0m0,004s
          

          Python 也不错。 而且这两个也不错。

          fun4 () {
              while IFS= read -r; do
                # Map 3 lines of big_file.txt without capturing newline character
                mapfile -t -n 3 -u 3
          
                # Output header $REPLY from small_file.txt
                # followed by ${MAPFILE[@]} mapped lines of big_file.txt
                printf '%s\n%s' "$REPLY" "${MAPFILE[@]}"
              done <lines 3<headers
          }
          
          real    0m0,017s
          user    0m0,012s
          sys     0m0,004s
          
          fun5 () {
              while IFS= read -r; do
                mapfile -t -n 3 -u 3 -C'echo "$REPLY";:' -c 3
                printf '%s\n' "${MAPFILE[@]}"
              done <headers 3<lines
          }
          
          real    0m0,011s
          user    0m0,011s
          sys     0m0,000s
          

          【讨论】:

          • 这将与原始解决方案有相同的问题,而且它假设文件的内容是完全规则的,实际上它们可能不是(然后 OP 可以简单地生成输出而不查看这些文件中的任何一个)。
          • @tripleee 它比你的 py 脚本快)但是 kvantour 的 paste 或 Sundeep 的 sed 是无与伦比的
          • 您是否在 10 M 行输入上测试过它?如果您拥有的不仅仅是玩具数据,那么这里有几个正则表达式问题应该会变得很明显(较短的数字序列将匹配较长的数字序列,并且您忘记转义最后一个点;领先的.* 是不必要但无害的)。
          • 是的,更大的文件很糟糕(
          • 要清楚,您的时序测试中没有 awk 解决方案。反复调用 awk 的 shell 循环是一种 shell 解决方案,而不是 awk 解决方案。涉及 shell 循环的任何事情都不会像调用一个命令那样快,而且它通常会更复杂、更不便携且有问题。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2020-06-01
          • 1970-01-01
          • 2016-02-09
          • 2022-06-13
          • 1970-01-01
          • 2021-08-17
          • 1970-01-01
          相关资源
          最近更新 更多