将输出通过管道传递到带有多个输入的Bash函数

3
这是我想做的事情:我想使用bash测量两个字符串之间的Levensthein距离。我找到了一个LD here的实现。
现在,假设我有一些玩具数据,如下所示:
1    The brown fox jumped    The green fox jumped
0    The red fox jumped    The green fox jumped
1    The gray fox jumped    The green fox jumped

假设这个内容存储在 data.test 中。

然后我通过一个简单的 awk 命令进行过滤,过滤掉以 1 开头的行,如下所示:

awk -F '\t' '{if ($1>0) print $2,t,$3}' data.test

这个简单命令的第一个输出将会是:
The brown fox jumped    The green fox jumped

我现在想要测量这两个句子之间的Levensthein距离,通过将此输出直接传输到该函数(从上面的链接中提取):
function levenshtein {
    if (( $# != 2 )); then
        echo "Usage: $0 word1 word2" >&2
    elif (( ${#1} < ${#2} )); then
        levenshtein "$2" "$1"
    else
        local str1len=${#1}
        local str2len=${#2}
        local d

        for i in $( seq 0 $(( (str1len+1)*(str2len+1) )) ); do
            d[i]=0
        done

        for i in $( seq 0 $str1len );   do
            d[i+0*str1len]=$i
        done

        for j in $( seq 0 $str2len );   do
            d[0+j*(str1len+1)]=$j
        done

        for j in $( seq 1 $str2len ); do
            for i in $( seq 1 $str1len ); do
                [ "${1:i-1:1}" = "${2:j-1:1}" ] && local cost=0 || local cost=1
                del=$(( d[(i-1)+str1len*j]+1 ))
                ins=$(( d[i+str1len*(j-1)]+1 ))
                alt=$(( d[(i-1)+str1len*(j-1)]+cost ))
                d[i+str1len*j]=$( echo -e "$del\n$ins\n$alt" | sort -n | head -1 )
            done
        done
        echo ${d[str1len+str1len*(str2len)]}
    fi
}

我知道你可以做到这一点,但是我遇到了两个需要传递的参数和传递序列的问题,所以卡住了。
我尝试使用this的建议的各种版本,该建议主张抓取输入如下:
function levenshtein {
    # Grab input.
    declare input1=${1:-$(</dev/stdin)};
    declare input2=${2:-$(</dev/stdin)};
.
.
.
}

这是我无法完全让其正常工作的部分。


用awk会快得多。快速谷歌搜索可以找到https://rosettacode.org/wiki/Levenshtein_distance#AWK,https://awkology.wordpress.com/2012/01/23/levenshtein-distance/等网站。 - Ed Morton
我能像问题演示的那样将内容输入到awk版本中吗? - Astrid
我在问题中没有看到它被演示,但是 awk 可以从管道或文件中读取输入。 - Ed Morton
哦,我的意思是说我问题的“演示意图”,即将输出传递给一个函数。 - Astrid
3个回答

7
您根本不需要使用 awk
while IFS=$'\t' read num first second; do
    [[ $num -gt 0 ]] || continue
    levenshtein "$first" "$second"
done < data.txt

(正确的,awk在处理大文件时比bash更快,但如果您首先在bash中实现Levenshtein算法,速度可能不是问题。)


另外,使用“元组”作为键来使用关联数组可以实现一个更简单(虽然测试最少)的实现,不需要那么多的索引算术。

levenshtein () {
  if (( ${#1} < ${#2} )); then
    levenshtein "$2" "$1"
    return
  fi

  local str1len str2len cost m a b i j
  local -A d

  str1len=${#1}
  str2len=${#2}
  for ((i=0;i<=strlen1;i++)); do
    d[$i,0]=0
  done

  for ((j=0;j<=strlen2;j++)); do
    d[0,$j]=0
  done

  for ((j=1; j<=str2len; j++)); do
    for ((i=1; i<=str1len; i++)); do
      a=${1:i-1:1}
      b=${2:j-1:1}
      [ "$a" = "$b" ] && cost=0 || cost=1
      del=$(( $d[$((i-1)),$j] + 1 ))
      ins=$(( $d[$i,$((j-1))] + 1 ))
      alt=$(( $d[$((i-1)),$((j-1))] + cost ))

      # Compute the min without forking
      m=$del; ((ins < m)) && m=$ins; ((alt < m)) && m=$alt

      d[$i,$j]=$m
    done
  done
  echo ${d[$str1len,$str2len]}
} 

关于速度的评论,你有什么建议吗?我用bash做这件事,因为我认为它比python快得多,但这基本上是唯一的原因。还有其他更快的东西吗?(另外,非常好的答案) - Astrid
你为什么认为bash比Python更快?bash旨在运行其他程序,而不是自己进行计算。 - chepner
嗯,在Pandas中处理数十万个句子对(在我的情况下是几GB的数据)可能会导致计算机崩溃。我听说有人主张使用bash作为这种大型操作的替代方案。 - Astrid
你可能正在耗尽内存;你没有崩溃是因为Python比“bash”“慢”。你要么误解了这些“人”所说的话,要么在听信极度不准确的人的建议。 - chepner
我会再次尝试使用Pandas选项,谢谢。 - Astrid

1

我支持Chepner的答案,但如果你发现自己陷入需要解决这个问题的境地,那也不是太难。

# Awk script refactored slightly for aesthetics
pair=$(awk -F '\t' '$1>0 {print $2 "\t" $3}' data.test)
levenshtein "${pair%$'\t*'}" "${pair#$'*\t'}"

稍微解释一下:

  • levenshtein 的两个参数都用双引号括起来。
  • 每个参数都包含一个参数替换;
    • ${variable%pattern} 生成的是variable的值,其中匹配pattern的任何后缀都被删除
    • ${variable#pattern} 生成的是variable的值,其中匹配pattern的任何前缀都被删除
    • 这两者都匹配尽可能短的pattern。如果你有一个包含多个字段的字符串,你可能需要使用##%%变体,它们分别从值的前面或后面截取最长的适用pattern
  • $'\t' 是一个包含制表符的C风格字符串
  • pattern 中还包含一个在制表符前面或后面的 *,以便根据需要删除制表符前面或后面的所有内容,以获得由制表符分隔的字符串中的第一个或第二个值。

1

如果在调用awk之前在bash中导出Levenshtein函数,可以通过export -f levenshtein轻松地逐行调用awk函数: awk -F '\t' '$1>0 {system("levenshtein \""$2"\" \""$3"\"")}'


这是一种非常好的、简洁的方法,谢谢。 - Astrid

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