【问题标题】:Replace '\n' with white space only when the occurrence of '\t' is more than a number仅当 '\t' 的出现多于一个数字时,才将 '\n' 替换为空格
【发布时间】:2017-08-22 21:27:41
【问题描述】:

我有数以万计的制表符分隔的数据文件,每个都喜欢:

a0\ta1\ta2\ta3\ta4\ta5\ta6\ta7\ta8\ta9\n
b0\tb1\tb2\tb3\tb4\tb5\tb6\tb7\tb8\tb9\n
...

但是,偶尔会有一些文件包含(随机)格式错误的行,例如:

a0\ta1\ta2\ta3_0\n
a3_1\ta4\ta5\ta6\ta7\ta8\ta9\n
b0\tb1\tb2_0\n
b2_1\tb3\tb4\tb5\tb6\tb7\tb8\tb9\n
...

其中a3_0a3_1b2_0b2_1 resp.)是 a3b2 resp.)的一部分,最初由空格分隔。仅当行太短或\t 太少时,我才想用空格替换行尾的每个\n。目前5 似乎是一个安全阈值。

我经常用sed做一些修改,比上面的要简单很多。我想知道sed 或其他一些命令(如awk?我还需要学习)是否可以用于快速处理(因为我有很多文件)。谢谢。

【问题讨论】:

  • 因为这是一个关于寻找具有良好性能特征的解决方案的问题——文件有多大?那里有多少?您是否更关心单个文件中的启动时间或吞吐量? (如果它们很短,则为每个文件启动一个工具实例可能会花费更多;如果它们很长,则使用效率低/选择不当的工具来提高吞吐量会花费更多。
  • +Charles Duffy 每个文件都很小,只有几KB。但我有成千上万个。
  • 嗯。从吞吐量的角度来看,awk 的性能往往比sed 好得多。但是,除非您使用的 GNU awk 比 4.1.0 新,否则它没有就地编辑支持——您有哪个版本?
  • (另外,对于小文件来说,原生 bash 通常是一个可以接受的选择——它在每行的基础上速度很慢,但如果写得好,实际上不会花费在工具启动时间上的开销)。跨度>
  • 要清楚,顺便说一句,你的意思是 literal \t\n 字符串,每个两个字符,第一个字符是反斜杠?或者您是否使用\t 作为选项卡的替代品并使用\n 作为换行符?

标签: awk sed


【解决方案1】:

使用 GNU awk 处理多字符 RSRT(以及后来的 -i infileENDFILE)并使用逗号而不是制表符来提高可见性:

$ cat file
a0,a1,a2,a3,a4,a5,a6,a7,a8,a9
b0,b1,b2,b3,b4,b5,b6,b7,b8,b9
a0,a1,a2,a3_0
a3_1,a4,a5,a6,a7,a8,a9
b0,b1,b2_0
b2_1,b3,b4,b5,b6,b7,b8,b9

$ awk -v RS='([^,]*,){9}[^\n]*\n' '{$0=RT; sub(/\n$/,"") gsub(/\n/," ")} 1' file
a0,a1,a2,a3,a4,a5,a6,a7,a8,a9
b0,b1,b2,b3,b4,b5,b6,b7,b8,b9
a0,a1,a2,a3_0 a3_1,a4,a5,a6,a7,a8,a9
b0,b1,b2_0 b2_1,b3,b4,b5,b6,b7,b8,b9

上面的 [ab-] 使用 RS 将每条记录(而不是记录分隔符)描述为一系列 10 个逗号分隔的字段,以换行符结尾,然后在打印前在每条记录中适当地替换换行符。

只需将 RS='([^,]*,){9}[^\n]*\n' 更改为 RS='([^\t]*\t){9}[^\n]*\n' 即可使用制表符分隔而不是逗号分隔的字段。

要对所有文件进行更改,请添加-i inplace:

awk -i inplace -v RS='...' '...' *

或:

find ... -exec awk -i inplace -v RS='...' '...' {} +

实际上,您甚至不必对 RS 进行硬编码,只要每个输入文件中至少有 1 行完整的行,该工具就可以解决:

$ awk -F',' '
    BEGIN { ARGV[ARGC] = ARGV[ARGC-1]; ARGC++ }
    NR==FNR { n=(NF>n?NF:n); next }
    ENDFILE { RS="([^"FS"]*"FS"){"n-1"}[^\n]*\n" }
    { $0=RT; sub(/\n$/,"") gsub(/\n/," "); print }
' file
a0,a1,a2,a3,a4,a5,a6,a7,a8,a9
b0,b1,b2,b3,b4,b5,b6,b7,b8,b9
a0,a1,a2,a3_0 a3_1,a4,a5,a6,a7,a8,a9
b0,b1,b2_0 b2_1,b3,b4,b5,b6,b7,b8,b9

只需将-F',' 更改为-F'\t' 即可使用制表符分隔。

使用 POSIX awks 仅供参考,上述两个 gawk 脚本的最接近等效项是:

$ awk '
    { rec=rec $0 RS }
    END{
        while ( match(rec,/([^,]*,){9}[^\n]*\n/) ) {
            tgt = substr(rec,RSTART,RLENGTH)
            sub(/\n$/,"",tgt)
            gsub(/\n/," ",tgt)
            print tgt
            rec = substr(rec,RSTART+RLENGTH)
        }
    }
' file
a0,a1,a2,a3,a4,a5,a6,a7,a8,a9
b0,b1,b2,b3,b4,b5,b6,b7,b8,b9
a0,a1,a2,a3_0 a3_1,a4,a5,a6,a7,a8,a9
b0,b1,b2_0 b2_1,b3,b4,b5,b6,b7,b8,b9

和:

awk -F',' '
    { rec=rec $0 RS; n=(NF>n?NF:n) }
    END{
        while ( match(rec,"([^"FS"]*"FS"){"n-1"}[^\n]*\n") ) {
            tgt = substr(rec,RSTART,RLENGTH)
            sub(/\n$/,"",tgt)
            gsub(/\n/," ",tgt)
            print tgt
            rec = substr(rec,RSTART+RLENGTH)
        }
    }
' file
a0,a1,a2,a3,a4,a5,a6,a7,a8,a9
b0,b1,b2,b3,b4,b5,b6,b7,b8,b9
a0,a1,a2,a3_0 a3_1,a4,a5,a6,a7,a8,a9
b0,b1,b2_0 b2_1,b3,b4,b5,b6,b7,b8,b9

请注意,那些在主处理开始之前将整个文件读入单个字符串的人,因此如果您的文件太大而无法放入内存但您已经告诉我们每个文件“非常小”,他们就会失败,这样应该不是问题。

要覆盖输入文件,最简单的方法总是:

awk '{...}' file > tmp && mv tmp file

但在这种情况下,您也可以这样做:

awk '{...} END{... print tgt > ARGV[1] ...}' file

这在这种情况下有效,因为 awk 在开始 END 部分之前已经完成了对输入文件的读取。不要在脚本的其他地方尝试。

【讨论】:

  • 我会去这里,但 OP 表示担心限制对当前 gawk 版本的支持(请参阅有关问题的评论历史记录)。
  • 啊,是的,我明白了——我现在在末尾添加了一个 POSIX 等效项。
【解决方案2】:

假设您将以下脚本命名为repiece

#!/usr/bin/env bash

IFS=$'\t'       # use tab separators throughout this script
rIFS=,          # except to avoid field coalescing, use commas
pieces_needed=5 # adjust this to taste

for arg; do
  tempfile="${arg}.tmp-$$" # vulnerable to symlink attacks; use mktemp instead if untrusted
                           # users have write access to current directory.
  deferred=( )
  {
    while IFS="$rIFS" read -r -a pieces; do
      if (( ( ${#deferred[@]} + ${#pieces[@]} ) < pieces_needed )); then
        deferred+=( "${pieces[@]}" )
      elif (( ${#deferred[@]} )); then
        # separate last piece of deferred and first of pieces with a space
        all_pieces=( "${deferred[@]} ${pieces[@]}" )
        printf '%s\n' "${all_pieces[*]}"
        deferred=( )
      else
        printf '%s\n' "${pieces[*]}"
      fi
    done
    # if we have anything deferred for the last line, print it now
    (( ${#deferred[@]} )) && printf '%s\n' "${deferred[*]}"
  } < <(tr -- "$IFS" "$rIFS" <"$arg") >"$tempfile"
  mv -- "$tempfile" "$arg"
done

...您可以调用尽可能少的调用来处理所有文件,如下所示:

# if your files end in .tsv
find . -type f -name '*.tsv' -exec ./repiece {} +

【讨论】:

  • 谢谢。但是在我使用你的脚本之后似乎有两个错误。首先,\n 被替换为\t 而不是空格。其次,我的文件中有空字段,例如\t\t\t\t\ta5\ta6\ta7\ta8\ta9。该脚本已删除所有进行中的\t(尽管我想保留它们)。我还注意到有时两条线会连在一起,即使它们已经足够长了。
  • Re: 空标签被移除,这实际上是一个相当粘的标签,足以让我在几年前问我自己的问题——见read in bash on tab-delimited file without empty fields collapsing。那里最简单的答案需要临时使用非空格字符来代替制表符——例如,如果我使用逗号,您是否对这些在数据中的任何地方都不作为文字存在感到满意?
  • (一个选项卡 is 是空格的一种形式,顺便说一句,所以这个答案 is 已经在另一个计数上响应了——但是如果你想要一个非制表符空间,这也是可行的)。
  • 我已经编辑以解决空字段问题。至于“有时两条线连接在一起,即使它们已经足够长了”——请提供一个复制器。
  • 现在看来运行良好。只是两个小问题。如果行尾有\t\n,则将删除最后一个\t。但这没关系,因为它不会影响我以后的处理。 , 不是那么安全(这让我之前很痛苦),所以我创建了rIFS=,,,。感谢您的努力。
【解决方案3】:

在 awk 中,在空格和 之间更改 ORS

$ awk '
BEGIN { 
    FS=OFS="\t"       # set field separators
    RS=ORS="\n"       # set record separators
}
NF<=5 {               # if below or at threshold
    ORS=" "           # redefine output record separator
}
{
    print             # print record with ORS
    ORS="\n"          # reset ORS back to newline
}' file
a0      a1      a2      a3      a4      a5      a6      a7      a8      a9
b0      b1      b2      b3      b4      b5      b6      b7      b8      b9
a0      a1      a2      a3_0 a3_1       a4      a5      a6      a7      a8      a9
b0      b1      b2_0 b2_1       b3      b4      b5      b6      b7      b8      b9

使用 shell 脚本处理多个文件:

$ for f in file1 file2 ; do awk ... $f > new-$f ; done

如果需要,请引用 $f

【讨论】:

  • 为什么只引用“如果需要”?用你知道无法处理所有可能的输入数据的做法编写代码似乎很不合常理,不是吗? (我经常提到这一点,但我见过的最糟糕的数据丢失事件是由文件名中的“不可能”通配符引起的——由于错误将随机内存内容转储到用于创建所述名称的缓冲区中——到被扩展)。
  • 了解您的数据。
  • “了解您的数据”意味着了解未来。这意味着要知道您正在阅读的内容的尚未编写的制作者将拥有的所有可能的错误。这意味着了解尚未发生的每项规范更改。你鼓励人们做不可能的事,并告诉他们应该做可能的事,以替代遵循最佳实践和编写无条件健壮的代码。
  • 当然。我并不是建议这里的人们为一切做好准备,并对他们未来所做的一切负责。 如果需要,请引用 是我的免责声明。在这种情况下,我尝试向 op 展示如何使用 awk 来完成他想要做的事情。我什至跳过了awk ... $f &gt; tmp &amp;&amp; mv -i tmp $f,这样硬件就不会覆盖他的数据。如果他想要样板的 bash 脚本,我不会回答这个问题,我会很高兴地把它留给你,先生。
  • 如果我要在仅使用编写每个部分时预期的数据编写的片段之上构建一个系统,那么整个较大的系统很脆弱,需要大量的、不合理的测试工作来扩展:当编写该组件时的假设发生变化时,它的每个部分都容易崩溃。假设确实会改变——这是在现实世界中构建、部署和维护软件的一部分。
【解决方案4】:

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

sed ':a;s/\t/&/9;t;N;s/\n/ /;ta' file

如果当前行中的制表符少于 9 个,则追加下一行并将换行符替换为空格。重复直到 9 个或更多选项卡。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-05-30
    • 1970-01-01
    • 1970-01-01
    • 2021-06-15
    • 1970-01-01
    • 2020-03-31
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多