应用替换N次

3

我有一个字符串

data1_data2_data3_data4@data5,data6

有时候,data5包含下划线作为字段分隔符,这很丑陋。

我想用类似以下的方式读取这些数据片段:

IFS="_@," read d1 d2 d3 d4 d5 d6 <<< "$input"

当data5中包含下划线时,问题就出现了。为了解决这个问题,我想用逗号(和@符号)替换前三个下划线。到目前为止,我发现最简单的方法是使用sed:

sed 's/_/,/; s/_/,/; s/_/,/; s/@/,/' <<< "$input"

但是重复三次相同的替换似乎效率很低。如果我需要重复5000次会怎样呢?

有没有办法告诉sed重复某个替换一定次数?

为了完整起见,这是示例输入:

input="data1_data2_data3_data4@d_a_t_a_5,data6"
IFS="," read d1 d2 d3 d4 d5 d6 <<< "$input"

期望输出结果:

d1=="data1"
d2=="data2"
d3=="data3"
d4=="data4"
d5=="d_a_t_a_5"
d6=="data6"

1
嗨,Poshi。我们如何区分 data1_data2d_a_t_a_5 中的下划线?后一种情况中的下划线是如何与周围字段区分开来的? - TrebledJ
2
如果您正在寻求效率,请避免使用缓慢的bash read内置命令。那么使用Perl如何? - Nahuel Fouilleul
1
@TrebuchetMS,data{1..4}没有下划线。在“@”之前,下划线是字段分隔符。在“@”之后,逗号是字段分隔符。 - Poshi
1
@Poshi:但是你在问题中提到“当data4包含下划线时出现问题”,所以你可能在“@”之前也有“_”,而你不想对其进行拆分。 - anubhava
1
理想情况下,应该修复首先产生这种令人恼火的格式的任何流程或工具。 - tripleee
显示剩余6条评论
5个回答

1
您可以在进程替换中使用此awk
input="data1_data2_data3_data4@d_a_t_a_5,data6"

IFS=, read d1 d2 d3 d4 d5 d6 < <(awk -F@ -v OFS=, -v n=3 '{
while (i++<n) sub(/_/, ",", $1)} 1' <<< "$input")

# check variable values
declare -p d1 d2 d3 d4 d5 d6

declare -- d1="data1"
declare -- d2="data2"
declare -- d3="data3"
declare -- d4="data4"
declare -- d5="d_a_t_a_5"
declare -- d6="data6"
  • awk 命令使用 @ 作为字段分隔符。
  • awk 命令将 第一个字段中的 _ 替换为 ,,且替换次数为 精确的 n 次。

这个解决方案对于5000个假设很好,但它远离我正在寻找的简单易懂的一行代码。此外,它不仅转换第一个N个下划线,而是所有直到“@”为止,但这可以很容易地解决。无论如何,您的代码解决了手头的问题,但比只重复三次sed替换要冗长得多 :-( - Poshi
哈哈哈!好的重写。我认为这是我接近圣杯的最接近的一次了。我很难过,因为没有标志告诉解释器重复替换N次,我必须手动处理字段。 - Poshi
但我同意你的观点,它比你想象中的更冗长 :-) - anubhava

1
使用 awk。
$ input="data1_data2_data3_data4@d_a_t_a_5,data6"
$ awk -v RS='[@\n]' '{ if(NR % 2){ gsub(/_/, ","); ORS = "," } else ORS = "\n" } 1' <<< "$input"
data1,data2,data3,data4,d_a_t_a_5,data6

0

一种选择是使用 shell 扩展手动拆分,${var%%pat} 可以去除最大的后缀匹配 pat,${var#pat} 可以去除最小的前缀匹配 pat。

while IFS= read line; do
    tmpline=$line
    d1=${tmpline%%_*} tmpline=${tmpline#*_}
    d2=${tmpline%%_*} tmpline=${tmpline#*_}
    d3=${tmpline%%_*} tmpline=${tmpline#*_}
    d4=${tmpline%%@*} tmpline=${tmpline#*@}
    d5=${tmpline%%,*} tmpline=${tmpline#*,}
    d6=${tmpline}

    printf "%s\n" "d1=$d1" "d2=$d2" "d3=$d3" "d4=$d4" "d5=$d5" "d6=$d6"
done <<< "$input"

或者为了避免bash读取速度慢的问题,手动分割行。
tmpinput=$input
while [[ $tmpinput ]]; do
    if [[ $tmpinput = *$'\n'* ]]; then
        tmpline=${tmpinput%%$'\n'*} tmpinput=${tmpinput#*$'\n'}
    else
        tmpline=${tmpinput} tmpinput=''
    fi

    d1=${tmpline%%_*} tmpline=${tmpline#*_}
    d2=${tmpline%%_*} tmpline=${tmpline#*_}
    d3=${tmpline%%_*} tmpline=${tmpline#*_}
    d4=${tmpline%%@*} tmpline=${tmpline#*@}
    d5=${tmpline%%,*} tmpline=${tmpline#*,}
    d6=${tmpline}

    printf "%s\n" "d1=$d1" "d2=$d2" "d3=$d3" "d4=$d4" "d5=$d5" "d6=$d6"
done 

“如果我需要重复它5000次会发生什么?” - oguz ismail
1
这里比调用sed更高效,因为扩展是内置的而不是产生新的进程,但瓶颈在于读取内置函数。 - Nahuel Fouilleul
1
考虑效率是个好点子,但这不是重点。我试图为手头的问题编写最紧凑和清晰的代码。输入始终为单行,通常约40字节。这是为了交互式工作而设计的。我相信操作员不会担心多等0.1秒,但我担心如果有一天我需要处理类似的更长字符串。 - Poshi
在你的问题中,你问是否必须重复5000次,最紧凑的是sed版本,缺点是每次调用都会启动一个新进程,awk或perl也是如此。 - Nahuel Fouilleul
没错,这就是为什么我只使用一个包含所有替换的 sed 进程来开始操作 :-) - Poshi
也许我误解了,5000次是在一行上,我以为是在5000个不同的行上。 - Nahuel Fouilleul

0
在中,我会使用正则表达式。
$ cat input
one_two_three_fourpt1_fourpt2@fivept1_fivept2,six
$ regex='([^_]+)_([^_]+)_([^_]+)_(.+)@([^,]+).(.*)'
$ while IFS= read -r line; do
> [[ $line =~ $regex ]]
> done < input
$ printf '%s\n' "${BASH_REMATCH[@]}"
one_two_three_fourpt1_fourpt2@fivept1_fivept2,six
one
two
three
fourpt1_fourpt2
fivept1_fivept2
six

BASH_REMATCH 的第零个元素包含整个匹配;其余元素从左侧开始包含各个捕获组。

或者,您可以使用 read 先在 @ 处分割,然后再根据需要使用 _, 分割两半。

$ IFS="@" read -r first second <<< "$line"
$ IFS=_ read -r f1 f2 f3 f4 <<< "$first"
$ IFS=, read -r f5 f6 <<< "$second"

由于第二次调用read只有4个参数,f4将包含第三个_之后的任何内容,而不会在其他_上进行进一步的字段拆分。


在支持更有效迭代文件内容的语言中,可以使用类似的正则表达式和两级分割方案,如 Nahuel Fouilleul 指出的那样,bash 的速度并不快。(read 逐字节读取其输入,而不是一次读取整个块,以避免读取比必要多的字节来消耗恰好一行输入。)

你仍然需要逐行迭代文件(我猜我应该在答案中包含这一点)。 - chepner
我的意思是:如果你需要对每一行重复执行这个转换5000次怎么办?我试图避免正则表达式的重复使用。在问题中显示的一行代码比这两行代码更简单、更清晰,但很可惜。 - Poshi
1
这个 line 有5000个不同的字段?我肯定会转向另一种语言;bash 不适合这种数据处理!你可以在所有 _ 上分割,然后重新连接被分割的任何尾部。 (以 Python 为例,类似于 fields = line.split('_'); fields[5000] = '_'.join(fields[5001:]) - chepner

0

如果您有一个以上的带有@._._....的字段,
您可以尝试使用以下awk命令:

echo "data1_data2@d_a_t_a_17,data3_data4@d_a_t_a_5,data6_data7" |
awk '
{
i = split ( $0 , a , "_" )
for ( j = 1 ; j <= i ; j++ )
  if ( a[j] !~ /@/ )
    print "d" ++k "==\"" a[j] "\""
  else
    {
      split ( a[j] , b , "@" )
      print "d" ++k "==\"" b[1] "\""
      sub ( ".*@" , "" , a[j] )
      while ( a[j] !~ "," )
        {
          c = c a[j] "_"
          j++
        }
        split ( a[j] , b , "," )
        c = c b[1]
        print "d" ++k "==\"" c "\""
        a[j] = b[2]
        j--
        c = ""
    }
}'

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接