Existe-t-il un programme de correspondance de chaînes floues qui fournit un score de correspondance?

17

J'ai une liste de chaînes dans un fichier Aet un fichier B. Je veux prendre chaque chaîne du fichier A et trouver la chaîne la plus similaire dans le fichier B.

Pour cela, je recherche un outil qui propose une comparaison floue.

par exemple:

$ fuzzy_compare "Some string" "Some string"
100

Où 100 est un rapport d'égalité. Par exemple la distance Levenshtein .

Y a-t-il une utilité? Je ne veux pas réinventer la roue.

c0rp
la source
1
J'ai modifié votre question pour améliorer la clarté, mais je l'ai modifiée pour demander de comparer chaque chaîne du fichier A à celles du fichier B et pas seulement la première. J'ai supposé que c'était ce que vous vouliez dire, mais veuillez me corriger si je me trompais.
terdon
@muru non, c'est seulement pour une correspondance floue, l'OP a besoin d'un score.
terdon

Réponses:

23

J'ai trouvé cette page qui fournit des implémentations de l'algorithme de distance Levenshtein dans différentes langues. Ainsi, par exemple en bash, vous pourriez faire:

#!/bin/bash
function levenshtein {
    if [ "$#" -ne "2" ]; then
        echo "Usage: $0 word1 word2" >&2
    elif [ "${#1}" -lt "${#2}" ]; then
        levenshtein "$2" "$1"
    else
        local str1len=$((${#1}))
        local str2len=$((${#2}))
        local d i j
        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
                local del=$((d[(i-1)+str1len*j]+1))
                local ins=$((d[i+str1len*(j-1)]+1))
                local 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
}

while read str1; do
        while read str2; do
                lev=$(levenshtein "$str1" "$str2");
                printf '%s / %s : %s\n' "$str1" "$str2" "$lev"
        done < "$2"
done < "$1"

Enregistrez-le sous ~/bin/levenshtein.sh, rendez-le exécutable ( chmod a+x ~/bin/levenshtein.sh) et exécutez-le sur vos deux fichiers. Par exemple:

$ cat fileA
foo
zoo
bar
fob
baar
$ cat fileB
foo
loo
baar
bob
gaf
$ a.sh fileA fileB
foo / foo : 0
foo / loo : 1
foo / baar : 4
foo / bob : 2
foo / gaf : 3
zoo / foo : 1
zoo / loo : 1
zoo / baar : 4
zoo / bob : 2
zoo / gaf : 3
bar / foo : 3
bar / loo : 3
bar / baar : 1
bar / bob : 2
bar / gaf : 2
fob / foo : 1
fob / loo : 2
fob / baar : 4
fob / bob : 1
fob / gaf : 3
baar / foo : 4
baar / loo : 4
baar / baar : 0
baar / bob : 3
baar / gaf : 3

C'est très bien pour quelques modèles mais cela deviendra très lent pour les fichiers plus gros. Si c'est un problème, essayez l'une des implémentations dans d'autres langues. Par exemple Perl:

#!/usr/bin/perl 
use List::Util qw(min);

sub levenshtein
{
    my ($str1, $str2) = @_;
    my @ar1 = split //, $str1;
    my @ar2 = split //, $str2;

    my @dist;
    $dist[$_][0] = $_ foreach (0 .. @ar1);
    $dist[0][$_] = $_ foreach (0 .. @ar2);

    foreach my $i (1 .. @ar1) {
        foreach my $j (1 .. @ar2) {
            my $cost = $ar1[$i - 1] eq $ar2[$j - 1] ? 0 : 1;
            $dist[$i][$j] = min(
                            $dist[$i - 1][$j] + 1, 
                            $dist[$i][$j - 1] + 1, 
                            $dist[$i - 1][$j - 1] + $cost
                             );
        }
    }

    return $dist[@ar1][@ar2];
}
open(my $fh1, "$ARGV[0]");
open(my $fh2, "$ARGV[1]");
chomp(my @strings1=<$fh1>);
chomp(my @strings2=<$fh2>);

foreach my $str1 (@strings1) {
    foreach my $str2 (@strings2) {
        my $lev=levenshtein($str1, $str2);
        print "$str1 / $str2 : $lev\n";
    }
}

Comme ci-dessus, enregistrez le script sous ~/bin/levenshtein.plet rendez-le exécutable et exécutez-le avec les deux fichiers comme arguments:

~/bin/levenstein.pl fileA fileB

Même dans les très petits fichiers utilisés ici, l'approche Perl est 10 fois plus rapide que celle bash:

$ time levenshtein.sh fileA fileB > /dev/null

real    0m0.965s
user    0m0.070s
sys     0m0.057s

$ time levenshtein.pl fileA fileB > /dev/null
real    0m0.011s
user    0m0.010s
sys     0m0.000s
terdon
la source
Pour ajouter plus d'explications sur les résultats: en citant wikipedia, la distance Levenshtein entre deux mots est le nombre minimum de modifications à un seul caractère (c'est-à-dire des insertions, des suppressions ou des substitutions) nécessaires pour changer un mot en l'autre . Cela signifie que plus le nombre est bas, meilleure est la correspondance. Un nombre de zéro signifie une correspondance parfaite. Notez également que la distance Levenshtein gère les modifications de chaque caractère également, ce qui signifie que "foo" et "Foo" conduisent à la même distance que "foo" et "fox".
scai