cgicorner.ch

Informationen rund um Perl/CGI

Sie sind hier: Home > Knowledge Base > Dublettenabgleich mit Perl

Dublettenabgleich mit Perl

Perl ist ja vor allem in der Verarbeitung von Textdateien stark. Ein Dubletten-Abgleich von Adressen oder ähnlichem (also doppelte Vorkommnisse filtern) ist deshalb für Perl sehr gut geeignet. Dabei sollen natürlich nicht nur 100%ig identische Zeilen gefiltert werden können, sondern auch sehr ähnliche.

Genau für diesen Zweck habe ich einmal ein Script geschrieben. Grundsätzlich geht davon aus, dass alle Daten in einer Datei durch ein Trennzeichen getrennt untereinander stehen. Danach wird eine Funktion eingesetzt, welche die Daten vereinfacht und so eine höhere Trefferquote erreicht. Einmal konfiguriert findet das Script dann alle Zeilen, welche doppelt vorkommen und schreibt ab dem zweiten Vorkommen in eine andere Datei.

Hier einmal das Script

#/usr/bin/perl
use strict;
my (@buf, %addresses, $key);
open(INPUT,$ARGV[0]);
open(OUTPUT,">".$ARGV[0].".1");
open(DOPPELT,">".$ARGV[0].".doppelt");
while (<INPUT>) {
  chomp;
  $_ =~ s/[\r\n]//g;

  @buf=split(/;/); # split nach ;

  $key="A~".&simplify($buf[2])."~".&simplify($buf[5])."~".&simplify($buf[6]); # Felder 2,5 und 6 werden Key-Felder

  if ($addresses{$key} > 0) { # prüfe, ob Datensatz mit Keyfeld bereits vorhanden
    print DOPPELT $_."\n"; # in DOPPELT-Datei
  } else { # nicht vorhanden, in normale Datei
    print OUTPUT $_."\n";
    $addresses{$key}++; # Key in Datenbank schreiben
  }
}
close(INPUT);
close(OUTPUT);
close(DOPPELT);
 sub simplify { # reduziert Feldinhalt auf wichtige Zeichen um Dublettenfindung zu verbessern
  my $return=$_[0];
  $return =~ s/(ä|Ä)/ae/g;
  $return =~ s/(ö|Ö)/oe/g;
  $return =~ s/(ü|Ü)/ue/g;
  $return =~ s/ç/c/g;
  $return =~ s/(é|è|ê)/e/g;
  $return =~ s/(á|â|à)/a/g;
  $return =~ s/str\./strasse/gi;
  $return =~ s/av\./avenue/gi;
  $return =~ s/^Rte /Route /gi;
  $return =~ s/[^\w]//g;
  return lc($return);
}

Widmen wir uns zuerst der Funktion "simplify": diese dient dazu, aus einem Adressteil eine stark vereinfachte Form hinzukriegen. Dazu werden Umlaute, französische Sonderzeichen und Abkürzungen wie "str." für Strasse umgewandelt. Ebenfalls werden alle Leer- und Sonderzeichen eliminiert ($return =~ s/[^\w]//g;, also alles ausser A-Z und 0-9 durch nichts ersetzen) und alle Zeichen in Kleinbuchstaben geschrieben (return lc($return);).

Aus "François Légère" wird somit "francoislegere", was auch auf "Francois Légère", "Francois Legère","François LEGERE" oder "  françois    Légère" zutrifft. Somit können also schon einige unterschiedliche Schreibweisen ausgeschaltet werden. Diese Umsetzungstabelle kann natürlich noch beliegig ergänzt werden. Man kann sich dies noch für weitere Sonderzeichen vorstellen, aber auch, dass jedes einzelne Wort alphabetisch sortiert ausgegeben wird (also aus "Reto Meier" "meierreto" entsteht, so dass Schreibweisen von "Reto Meier" und "Meier, Reto" identisch wären). Die erzeugte Ausgabe von "simplify" wird in keiner Datei gespeichert, sondern dient nur scriptintern der Vereinfachung. Die Adresszeile können also auch "zerstört" werden.

Nun widmen wir uns dem Hauptscript: der Aufruf erfolgt mit duplicate.pl filename.txt, wobei filename.txt natürlich frei gewählt werden kann. Diese Datei (1. Argument = $ARGV[0]) wird lesend geöffnet. Danach werden zwei neue Dateien erstellt: filename.txt.1 mit den einzelnen Adressen und filename.txt.doppelt mit den doppelten Adressen (wobei das erste Vorkommnis der Adresse in die .1 Datei geschrieben wird und erst das 2., 3., ... Vorkommnisse in die .doppelt-Datei).

Die Eingabedatei wird nun Zeilenweise verarbeitet (while (<INPUT>) {). Zuerst werden Zeilenumbrüche entfernt ( chomp; $_ =~ s/[\r\n]//g;) und die Felder dann nach einem Trennzeichen (hier ein Strichpunkt, andere Zeichen wären aber auch möglich) in @buf aufgesplittet (@buf=split(/;/);).

Nun kommt "simplify" zum Einsatz: jetzt wird der eindeutige Kontroll-Schlüssel der Adresse erstellt, welcher nur die Felder berücksichtigt, welche abgeglichen werden sollen (in unserem Fall das zweite, fünfte und sechste Feld, beginnend bei 0). Der eindeutige Key zu jeder Adresse wird während der Laufzeit im RAM behalten. Mit heutigem Speicher funktioniert dieses Prinzip aber auch bei mehreren Millionen von Adressen eigentlich immer einwandfrei.

Nun wird geprüft, ob eine Adresse mit dem eindeutigen Key bereits existiert (if ($addresses{$key} > 0) {). Falls ja: wird die Zeile in die .doppelt-Datei geschrieben, falls nicht, wird der Z&auuml;hler zu dieser Adresszeile erhöht ($addresses{$key}++;) und die Zeile in die .1-Datei geschrieben.

Als konkretes Beispiel hier nun eine Demo-Datei:

1;Herr;Muster;Hans;c/o Fritz Müller;Mustergasse 99;9999;Mustershausen
2;Frau;Wölfe;Belinda;;Hauptstrasse 77;7777;Meine Stadt
3;Herr;Müller;Fritz;;Mühle 88;8888;Testeshausen
4;Herr;Muster;Hans;;Mustergasse 98;9999;Mustershausen
5;Frau;Woelfe;B.;;Hauptstr. 77;7777;Meine Stadt
6;Herr;MUSTER;Hans;;Mustergasse 99;9999;Musters-Hausen

Hier noch etwas schöner dargestellt, zur besseren Erklärung:

Nr. Anrede  Name    Vorname  Zusatz            Strasse          PLZ   Ort
1   Herr    Muster  Hans     c/o Fritz Müller  Mustergasse 99   9999  Mustershausen
2   Frau    Wölfe   Belinda                    Hauptstrasse 77  7777  Meine Stadt
3   Herr    Müller  Fritz                      Mühle 88         8888  Testeshausen
4   Herr    Muster  Hans                       Mustergasse 98   9999  Mustershausen
5   Frau    Woelfe  B.                         Hauptstr. 77     7777  Meine Stadt
6   Herr    MUSTER  Hans                       Mustergasse 99   9999  Musters-Hausen

Wir lesen jetzt also die erste Zeile ein, nach der "simplify" Funktion (bei der nur Nachname, Strasse und PLZ berücksichtigt werden) sieht der Wert von $key so aus: A~muster~mustergasse99~9999. Dieser Schlüssel existiert bisher noch nicht, wird also erstellt und die Zeile in die .1-Datei geschrieben.

Zeile 2 besteht aus A~woelfe~hauptstrasse77~777, existiert noch nicht, also .1 Datei und Schlüssel erstellen. Gleiches gilt auch für Zeile 3 (A~mueller~muehle88~8888) und 4 (A~muster~mustergasse98~9999; hier stimmt die Hausnummer nicht).

Bei Zeile 5 erhalten wir den Schlüssel A~woelfe´hauptstrasse77~7777 und der Schlüssel existiert bereits und wird deshalb als doppelt erkannt und in entsprechende Datei geschrieben. Dies obwohl Adressnummer und Vorname nicht übereinstimmen (wurde nicht berüchsichtigt) und Name und Ort unterschiedliche Schreibweisen aufwiesen.

Gleiches gilt auch für Zeile 6 (A~muster~mustergasse99~9999): hier wird ein doppeltes Vorkommen erkannt, obwohl Adressnummer (nicht berücksichtigt), Name (Gross-/Kleinschreibung durch "simplify" angepasst), Zusatz (nicht berücksichtigt) und Ort (nicht berücksichtigt) nicht übereinstimmen.

Genanntes Script-Beispiel ist also universell durch eigene Spaltenanordnung, andere Trennzeichen und angepasste Fehlertoleranz erweiterbar und eignet sich sehr gut zur Erkennung von doppelten Datensätzen.

Autor: Jürg Sommer, knowledge@cgicorner.ch

[ zurück ]