每第 i 个字符打印子串

Print substrings every ith character

我有一些文件,我想以 "sliding window" 的方式分成子字符串,以 1 个字符为增量。每个文件只有一行,我可以像这样打印子字符串:

input="file.txt"
awk '{print substr(,1,21)}' $input


awk '{print substr(,2,21)}' $input

分别给我以下输出。

AATAAGGTGCCTGATTAAA-G   
ATAAGGTGCCTGATTAAA-GG

输入文件包含大约 17k 个字符,我设法尝试执行一个 for 循环来计算字符数,并在 for 循环中尝试上述命令,如下所示:

count=`wc -c ${input} |cut -d' ' -f1`
for num in `seq ${count}`
   do
awk '{print substr(,$num,21)}' $input
   done

但是这个 returns 空输出。我还想 运行 它作为一个 bash 脚本,在命令行中指定输入和子字符串的大小以及输出文件,如:

script.sh input_file.txt 21 output.txt

我试过了,但还是不行。

  input=
  kmer=
  output=
  count=`wc -c ${input} |cut -d' ' -f1`
  for num in `seq ${count}`
    do
 awk '{print substr(,$num,$kmer)}' $input > $output
  done

关于我做错了什么的任何提示?我是 awk 的新手...

#!/usr/bin/env bash 

input=
kmer=
output=

data=$(<"$input")

for ((i=0;i<${#data};i++)); do
    echo "${data:i:kmer}"
done > "$output"

它仅使用 substring expansion,引用手册:

${parameter:offset:length}

This is referred to as Substring Expansion. It expands to up to length characters of the value of parameter starting at the character specified by offset.


使用gawk

awk -v num="$kmer" '{for(i=1;i<=length([=11=]);i++) print substr([=11=],i,num)}' "$input" > "$output"

这是一个快得多的解决方案。速度差异显着:测试 17k 个字符和 30 个字符 window:第一个解决方案 ~10s~0.01s 为第二个解决方案。

您也可以使用 GNU sed 执行此操作,如下所示:

echo -n "123456789" | sed -r ':loop h;s/.//3g;p;x; s/.//; t loop'
12
23 
34
45
56
67
78
89 
9

3g 是 "sliding window" 尺寸 + 1。

要处理文件中的数据而不是 STDIN,只需在 sed 命令后指定它:

sed -r ':loop h;s/.//3g;p;x; s/.//; t loop' myfile
$ echo {1..9} | tr -d ' ' |   # create test data
  awk -v len=3 '{n=length([=10=]); for(i=1;i<=n-len+1;i++) print substr([=10=],i,len)}'

123
234
345
456
567
678
789

关于您的特定问题,代码段:

awk '{print substr(,$num,21)}' $input

存在一个问题,即单引号内的内容 而不是 受 shell 变量扩展的影响。这可以通过以下方式看到:

pax$ num=42 && echo '$num'
$num
pax$ num=42 && echo "$num"
42

因此 $num 而不是 替换为 shell 变量的值。

从上面也可以看出,你可以使用双引号,允许扩展,但是你需要转义</code>到<em>防止</em> 它的扩展。我通常发现将 shell 变量转换为 <code>awk 变量更容易,如下所示:

awk -vnum=$num '{print substr(,num,21)}' $input

以下代码片段显示了此操作:

pax$ num=42 && awk 'END{print $num}' </dev/null

pax$ num=42 && awk -v num=$num 'END{print num}' </dev/null
42

然而,外部程序的 17,000 次调用将是相当低效的,您最好编译一些东西,或者,如果您必须使用脚本,它可以完全在 bash 本身完成。下面的代码展示了如何做到这一点,重要的一点在 time ( ) 块中,其他一切都只是设置测试数据、计时和清理。

# Create test data.

(
    for i in {1..1000} ; do
        echo -n "abcdefghijklmnop-"
    done
) >inputdata.txt

# Time the execution.

time (
    char17k="$(cat inputdata.txt)"
    echo ${#char17k}
    for ((i = 0; i < ${#char17k}; i++)) ; do
        echo ${char17k:i:21}
    done

)

# Clean up.

rm -rf inputdata.txt

在我的系统上,这会在大约 10 秒内完成。 17,000 awk 次调用所花费的时间大约是它的三倍,即使没有做任何有用的工作也是如此:

pax$ time (for in in {1..17000} ; do awk '{}' </dev/null ; done )
real    0m30.649s
user    0m5.196s
sys     0m4.848s

当然,您可以通过让 awk 完成 所有 工作来获得甚至 更多 的速度。将上面代码中time ( )块的内容替换为:

awk '{for (i = 1; i < length([=16=]); i++) {print substr([=16=], i, 21)}}' inputdata.txt

给出了更令人印象深刻的(大约十分之一秒):

real    0m0.121s
user    0m0.008s
sys     0m0.016s

需要 perl 中的条目吗?

#! /bin/env perl

use strict;
use warnings;

my $data;
my $offset = 0;
my $window = shift or die "Use: [=10=] {windowSize} [ < ] infile [ > outfile ]\n";

{ local $/;
  $data = <>;
}

print "$_\n" while $_ = substr $data, $offset++, $window;

exit;

可以压缩成一行,但即使使用严格和警告 &c...

$: wc -c src
17000 src

$: time ./slide 21 src
!"#$%&'()*+,-./012345
"#$%&'()*+,-./0123456
#$%&'()*+,-./01234567
$%&'()*+,-./012345678

. . .

WXYZ[\
XYZ[\
YZ[\
Z[\
[\
\

real    0m0.029s
user    0m0.004s
sys     0m0.021s