Adressdubletten abgleich

  • Moin Zusammen,

    hier meine nächste "Auskopplung" aus meinem großen Programm, welches ich evtl. mal vorstelle.
    Dieses Teil werde ich aber auch als eigenstädiges Programm weiterführen.

    Der Grund für die Auskopplung ist folgender:
    Wir arbeiten hier in der Firma mit Adressdaten, die sehr häufig massenweise Adressdubletten enthalten.
    Natürlich sind die Adressen nicht 100% identisch (das wär ja auch einfach...).
    Leider ist ein Dublettenabgleich auf Sicht nicht sehr zuverlässig und kostet auch noch Zeit.
    Natürlich gibt es Programme für sowas. Die Kosten aber Geld und das Teilweise auch nicht zu knapp.
    Und dann bin ich an Vorgaben des Herstellers gebunden.
    Mein Ziel ist aber, eine Excel-Tabelle/Accessdatei/CSV-Datei einfach auf das Programm schieben und kurze
    Zeit später habe ich ein paar Felder dranhängen, in denen die Dubletten markiert sind.

    Genug geschwafelt, Zeit was zu zeigen.

    Die Hauptfunktion habe ich https://autoit.de/hier%20im%20Forum gepostet.

    Das Programm selber

    Spoiler anzeigen

    dubletten.au3

    [autoit]


    #include <sql_func.au3>
    #include <soundex_koelner_phonetik.au3>
    #include <Array.au3>

    [/autoit] [autoit][/autoit] [autoit][/autoit] [autoit][/autoit] [autoit]

    _SQL_Connect(False) ; False=SQLite starten
    ; Tabellen erstellen
    ;Tabelle adressen -------------------------------------------------------------------------------------------------------------------------------------------------
    _SQL_Exec ( "CREATE TABLE `adressen` (`filename` VARCHAR(200) NOT NULL DEFAULT '',firstpage BIGINT NOT NULL DEFAULT 0,`refnr` VARCHAR(10) NULL, "& _
    "volladresse TEXT DEFAULT '', land VARCHAR(20) NOT NULL DEFAULT '', "& _
    "plz VARCHAR(5) NOT NULL DEFAULT '', ort VARCHAR(30) NOT NULL DEFAULT '', strasse VARCHAR(40) NOT NULL DEFAULT '', hausnr VARCHAR(20) NOT NULL DEFAULT '',"& _
    "`zeile8` VARCHAR(50) NOT NULL DEFAULT '', `zeile7` VARCHAR(50) NOT NULL DEFAULT '',`zeile6` VARCHAR(50) NOT NULL DEFAULT '',"& _
    "`zeile5` VARCHAR(50) NOT NULL DEFAULT '',`zeile4` VARCHAR(50) NOT NULL DEFAULT '', `zeile3` VARCHAR(50) NOT NULL DEFAULT '',"& _
    "`zeile2` VARCHAR(50) NOT NULL DEFAULT '',`zeile1` VARCHAR(50) NOT NULL DEFAULT '', adr_gruppe BIGINT UNSIGNED DEFAULT 0, "& _
    "pagecount_set BIGINT UNSIGNED DEFAULT 0);")
    ;Tabelle dubletten -------------------------------------------------------------------------------------------------------------------------------------------------
    _SQL_Exec ( "CREATE TABLE dubletten (dbl_rowid BIGINT UNSIGNED, dbl_sdex_all TEXT NOT NULL DEFAULT '', dbl_sdex_ort VARCHAR(20) NOT NULL DEFAULT '', " & _
    "dbl_sdex_strasse VARCHAR (25) NOT NULL DEFAULT '', dbl_sdex_fullname TEXT NOT NULL DEFAULT '', dbl_land VARCHAR(20) NOT NULL DEFAULT '', "& _
    "dbl_plz VARCHAR(5) NOT NULL DEFAULT '', dbl_stage1 BINGINT UNSIGNED DEFAULT 0, dbl_stage2 BINGINT UNSIGNED DEFAULT 0, "& _
    "dbl_stage3 BINGINT UNSIGNED DEFAULT 0);")
    ;Tabelle dblzeilen -------------------------------------------------------------------------------------------------------------------------------------------------
    _SQL_Exec ( "CREATE TABLE dblzeilen (dblz_rowid BIGINT UNSIGNED, dblz_land VARCHAR(20) NOT NULL DEFAULT '', dblz_plz VARCHAR(5) NOT NULL DEFAULT '',"& _
    "dblz_sdex_ort VARCHAR(20) NOT NULL DEFAULT '', dblz_sdex_strasse VARCHAR (25) NOT NULL DEFAULT '', dblz_sdex_zeile VARCHAR(30) NOT NULL DEFAULT '', "& _
    "dblz_zeile VARCHAR(30) NOT NULL DEFAULT '' );")

    [/autoit] [autoit][/autoit] [autoit][/autoit] [autoit]

    Func SplitSqlZeilen () ; Wenn die Adresse nur in Zeile 8 bis 1 steht: Land, PLZ, Ort, Straße und Hausnummer versuchen zu extrahieren
    Local $aRowData,$sCurSQL,$q,$n,$aRegEx,$iFoundPos
    _SQL_Query("SELECT zeile1,zeile2,zeile3,zeile4,zeile5,zeile6,zeile7,zeile8,ROWID,land,plz,ort,strasse,hausnr,volladresse FROM adressen WHERE plz='' OR ort='' OR strasse='';")
    $sCurSQL=""
    While _SQL_FetchRow($aRowData)
    ; In volladresse alle Zeilen schreiben
    If $aRowData[14]="" Then $sCurSQL&="UPDATE adressen SET volladresse="&_SQLite_FastEscape(_ArrayToString($aRowData,"",0,7))&" WHERE ROWID="&$aRowData[8]&"; "
    $q=7
    While Not StringRegExp($aRowData[$q],"\d{4,} ",0)
    ; Erste Zeile mit einer Zahl mit min. 4 Ziffern gefolgt von einem Leerzeichen suchen
    ; Die Zeile darunter sollte das ausgeschriebene Land sein
    If $aRowData[9]="" Then $sCurSQL&="UPDATE adressen SET land="&_SQLite_FastEscape($aRowData[$q])&" WHERE ROWID="&$aRowData[8]&"; "
    $q-=1
    If $q<0 Then ExitLoop
    WEnd
    If $q>0 Then
    $aRegEx=StringRegExp($aRowData[$q],"\d{4,} ",1)
    If @error=0 Then ; 4stellige Zahl gefunden
    $iFoundPos=@extended
    If $iFoundPos>StringLen($aRegEx[0])+1 Then ;Nanu, vor der PLZ steht noch was => muss ein Länderkennzeichen sein
    If $aRowData[9]="" Then $sCurSQL&="UPDATE adressen SET land="&_SQLite_FastEscape(StringReplace(StringLeft($aRowData[$q],$iFoundPos-1),$aRegEx[0],""))&" WHERE ROWID="&$aRowData[8]&"; "
    EndIf
    If $aRowData[10]="" Then $sCurSQL&="UPDATE adressen SET plz="&_SQLite_FastEscape($aRegEx[0])&" WHERE ROWID="&$aRowData[8]&"; " ; PLZ schreiben
    If $aRowData[11]="" Then $sCurSQL&="UPDATE adressen SET ort="&_SQLite_FastEscape(StringMid($aRowData[$q],$iFoundPos))&" WHERE ROWID="&$aRowData[8]&"; " ; Alles hinter PLZ als ORT schreiben
    $q-=1
    If $q>=0 Then ; Ist da noch eine Zeile, dann muss das die Straße sein
    $aRegEx=StringRegExp($aRowData[$q]," \d{1,}",1) ; Nach einem Leerzeichen gefolgt von min. einer Ziffer suchen => Hausnummer
    If @error=0 Then ; Hausnummer gefunden
    $iFoundPos=StringInStr($aRowData[$q],$aRegEx[0])
    If $aRowData[12]="" Then $sCurSQL&="UPDATE adressen SET strasse="&_SQLite_FastEscape(StringLeft($aRowData[$q],$iFoundPos))&" WHERE ROWID="&$aRowData[8]&"; " ; Straßenamen schreiben
    If $aRowData[13]="" Then $sCurSQL&="UPDATE adressen SET hausnr="&_SQLite_FastEscape(StringMid($aRowData[$q],$iFoundPos))&" WHERE ROWID="&$aRowData[8]&"; " ; Hausnummer schreiben
    Else ; Keine Hausnummer => ganze Zeile als Straße verwenden
    If $aRowData[12]="" Then $sCurSQL&="UPDATE adressen SET strasse="&_SQLite_FastEscape($aRowData[$q])&" WHERE ROWID="&$aRowData[8]&"; " ; Straßenamen schreiben
    EndIf
    EndIf
    EndIf
    ;Alle Zeilen, die verarbeitet wurden, leeren; Wenn keine 4Ziffern mit Leerzeichen gefunden wurden ist es eh keine richtige Adresse
    For $n=7 To $q Step -1
    $sCurSQL&="UPDATE adressen SET zeile"&$n+1&"=''; " ; Zeile leeren
    Next
    EndIf
    WEnd
    _SQL_Exec($sCurSQL)
    EndFunc ; ==> SplitSqlZeilen

    [/autoit] [autoit][/autoit] [autoit]

    Func Gen_SoundEx_From_SqlTab ()
    Local $aRowData,$sCurSQL,$q,$sStr,$sPLZ,$sOrt,$sLand,$n,$sSndEx
    Local Const $sLoeschworte="herrn;herr;frau;firma;familie; kg;co.kg;ggmbh;gmbh; mbh;co.; ohg;z.hd.;c/o;e.v.;gbr;a.g.; ag;e.g.; eg"
    Local $aRplToken=StringSplit($sLoeschworte,";")
    _SQL_Query("SELECT ROWID,zeile1,zeile2,zeile3,zeile4,zeile5,zeile6,zeile7,zeile8,ort,strasse,land,plz,hausnr FROM adressen;")
    While _SQL_FetchRow($aRowData)
    ;Globalen SoundEx für Tabelle:dubletten
    $sStr=_soundex_koelner($aRowData[10])
    $sPLZ=_SQLite_FastEscape($aRowData[12])
    $sOrt=_soundex_koelner($aRowData[9])
    $sLand=_SQLite_FastEscape($aRowData[11])
    $sCurSQL= "INSERT INTO dubletten (dbl_rowid,dbl_land,dbl_plz,dbl_sdex_ort,dbl_sdex_strasse,dbl_sdex_fullname,dbl_sdex_all) VALUES (" & _
    $aRowData[0]&","&$sLand&","&$sPLZ&",'"&$sOrt&"','"&$sStr&"','"&_soundex_koelner(_ArrayToString($aRowData,"",1,8))&"','"& _
    _soundex_koelner(_ArrayToString($aRowData,"",1,10))&"'); "
    ;SoundEx jede Zeile der Adresse für Tabelle:dblzeilen
    For $q=1 To 8
    If StringLen($aRowData[$q])>"" Then
    $aRowData[$q]=StringReplace($aRowData[$q],". ",".")
    $aRowData[$q]=StringReplace($aRowData[$q],", ",",")
    For $n=1 To $aRplToken[0]
    $aRowData[$q]=StringReplace($aRowData[$q],$aRplToken[$n],"")
    Next
    $aRowData[$q]=StringStripWS($aRowData[$q],7)
    $sSndEx=_soundex_koelner($aRowData[$q])
    If StringLen($sSndEx)>0 Then $sCurSQL&="INSERT INTO dblzeilen VALUES ("&$aRowData[0]&","&$sLand&","&$sPLZ&",'"&$sOrt&"','"&$sStr&"','"&$sSndEx&"',"& _
    _SQLite_FastEscape($aRowData[$q])&"); "
    EndIf
    Next
    _SQL_Exec($sCurSQL)
    WEnd
    EndFunc ; ==> Gen_SoundEx_From_SqlTab

    [/autoit] [autoit][/autoit] [autoit]

    Func Mark_Dbl_Grps ($iUseStage=1,$fWriteDblOrder=True)
    Local $aRowData,$sql_query
    ; 1. Stufe : Die Doublettengruppen eintragen, die 100% gleich ("volladresse") sind
    _SQL_Exec("CREATE TABLE dlbgrps AS SELECT volladresse,count(*) as anz FROM adressen GROUP BY volladresse HAVING anz>1;")
    _SQL_Exec("UPDATE dubletten SET dbl_stage1=(SELECT dlbgrps.ROWID FROM dlbgrps INNER JOIN adressen WHERE adressen.volladresse=dlbgrps.volladresse AND dubletten.dbl_rowid=adressen.ROWID);")
    _SQL_Exec("DROP TABLE dlbgrps;")
    ; 2. Stufe : Die Doublettengruppen eintragen, bei denen Land+PLZ+SoundEx-VolleAdresse gleich ist
    _SQL_Exec("CREATE TABLE dlbgrps AS SELECT (dbl_land+dbl_plz+dbl_sdex_fullname) AS token,count(*) AS anz FROM dubletten GROUP BY dbl_land+dbl_plz+dbl_sdex_fullname HAVING anz>1;")
    _SQL_Exec("UPDATE dubletten SET dbl_stage2=(SELECT dlbgrps.ROWID FROM dlbgrps WHERE dubletten.dbl_land+dubletten.dbl_plz+dubletten.dbl_sdex_fullname=dlbgrps.token);")
    _SQL_Exec("DROP TABLE dlbgrps;")
    ; 3. Stufe : Die Doublettengruppen eintragen, bei denen das LAND+PLZ+SoundEx(Strasse+zeile8-1 in einer anderen Zeile) gefunden wird
    _SQL_Exec( "CREATE TABLE dlbgrps AS SELECT (''+dblz_land+dblz_plz+dblz_sdex_strasse+dblz_sdex_zeile) AS token, count(*) as anz, dblz_zeile AS zeile FROM dblzeilen "& _
    "GROUP BY dblz_land+dblz_plz+dblz_sdex_strasse+dblz_sdex_zeile HAVING anz>1;")
    _SQL_Query("SELECT dlbgrps.ROWID,dblzeilen.dblz_rowid FROM dlbgrps INNER JOIN dblzeilen "& _
    "WHERE (''+dblzeilen.dblz_land+dblzeilen.dblz_plz+dblzeilen.dblz_sdex_strasse+dblzeilen.dblz_sdex_zeile)=dlbgrps.token ORDER BY dlbgrps.anz DESC;")
    While _SQL_FetchRow($aRowData)
    _SQL_Exec("UPDATE dubletten SET dbl_stage3="&$aRowData[0]&" WHERE dubletten.dbl_stage3=0 AND dubletten.ROWID="&$aRowData[1]&";")
    WEnd
    _SQL_Exec("DROP TABLE dlbgrps;")
    ; Jetzt die gewünschte Suchtiefe in adressen eintragen
    If $iUseStage>0 And $iUseStage<4 Then
    _SQL_Exec("UPDATE adressen SET adr_gruppe=(SELECT dbl_stage"&$iUseStage&" FROM dubletten WHERE dubletten.dbl_rowid=adressen.ROWID);")
    if $fWriteDblOrder Then _SQL_Exec("UPDATE adressen SET pagecount_set=(SELECT ROWID FROM dubletten WHERE dubletten.dbl_rowid=adressen.ROWID);")
    _SQL_Exec("ALTER TABLE adressen RENAME TO tmp_adressen;")
    _SQL_Exec("CREATE TABLE adressen AS SELECT * FROM tmp_adressen ORDER BY adr_gruppe DESC,pagecount_set;")
    _SQL_Exec("DROP TABLE tmp_adressen;")
    EndIf
    EndFunc ; ==> Mark_Dbl_Grps

    [/autoit]

    Und dazu ein paar "Hilfroutinen"

    Spoiler anzeigen

    sql_func.au3

    [autoit]


    #include-once
    #include <SQLite.au3>
    #include <SQLite.dll.au3>

    [/autoit] [autoit][/autoit] [autoit]

    Global $sql_hConnect
    Global $sql_hQuery

    [/autoit] [autoit][/autoit] [autoit]

    Func _SQL_Connect ($fMysql=False)
    If Not $fMysql Then ; Kein MySQL => SQLite starten
    $sql_hConnect=_SQLite_Startup(@ScriptDir&$_sqlite_dll)
    If @error Then
    MsgBox(16, "SQLite Error", $_sqlite_dll&" konnte nicht geladen werden!")
    Exit
    EndIf
    If _SQLite_LibVersion()<>"3.7.10" Then
    MsgBox(16,"SQLite Error", $_sqlite_dll&" wurde ausgetauscht!"&@CRLF&"Dieses Programm wurde für SQLite Version 3.7.10 programmiert.")
    Exit
    EndIf
    _SQLite_Open() ; :memory:-DB erstellen
    Else ; MySQL-Datenbank öffnen
    Return False
    EndIf
    EndFunc ; ==> _SQL_Connect

    [/autoit] [autoit][/autoit] [autoit]

    Func _SQL_Exec ($sQuery)
    If StringInStr($sql_hConnect,$_sqlite_dll)>0 Then ; Es wurde SQLite-Datenbank gestartet
    If Not _SQLite_Exec(-1,$sQuery)=$SQLITE_OK Then MsgBox(16, "SQLite Error", _SQLite_ErrMsg())
    Else ; Es wurde eine MySQL-Datenbank gestartet
    Return False
    EndIf
    EndFunc ; ==> _SQL_Exec

    [/autoit] [autoit][/autoit] [autoit]

    Func _SQL_Query ($sQuery,$hQuery="")
    If StringInStr($sql_hConnect,$_sqlite_dll)>0 Then ; Es wurde SQLite-Datenbank gestartet
    If Not _SQLite_Query(-1,$sQuery,$hQuery)=$SQLITE_OK Then MsgBox(16, "SQLite Error", _SQLite_ErrMsg())
    If @NumParams=1 Then
    ; Es wurde keine Queryhandle übergeben => Globalen Queryhandle verwenden
    $sql_hQuery=$hQuery
    Else
    Return $hQuery
    EndIf
    Else ; Es wurde eine MySQL-Datenbank gestartet
    Return False
    EndIf
    EndFunc ; ==> _SQL_Exec

    [/autoit] [autoit][/autoit] [autoit]

    Func _SQL_FetchRow (ByRef $aRowdata,$hQuery="")
    If StringInStr($sql_hConnect,$_sqlite_dll)>0 Then ; Es wurde SQLite-Datenbank gestartet
    If @NumParams=1 Then $hQuery=$sql_hQuery
    If Not _SQLite_FetchData($hQuery,$aRowdata,False,False)=$SQLITE_OK Then
    ; Bei einem Fehler ist das Ende erreich => QueryFinalize und False zurück, da keine weiteren Daten kommen
    _SQLite_QueryFinalize($hQuery)
    Return False
    EndIf
    Else ; Es wurde eine MySQL-Datenbank gestartet
    Return False
    EndIf
    Return True
    EndFunc ; ==> _SQL_FetchRow

    [/autoit] [autoit][/autoit] [autoit]

    Func _SQL_FetchSingleRow ($sQuery,ByRef $aRowdata)
    Local $hQuery_hdl,$fResult
    If StringInStr($sql_hConnect,$_sqlite_dll)>0 Then ; Es wurde SQLite-Datenbank gestartet
    $hQuery_hdl=_SQL_Query($sQuery,$hQuery_hdl)
    $fResult=_SQL_FetchRow($aRowdata,$hQuery_hdl)
    _SQLite_QueryFinalize($hQuery_hdl)
    Return $fResult
    Else ; Es wurde eine MySQL-Datenbank gestartet
    Return False
    EndIf
    EndFunc ; ==> _SQL_FetchSingleRow

    [/autoit] [autoit][/autoit] [autoit]

    Func _SQL_FetchSingleField ($sQuery)
    Local $aResult
    If _SQL_FetchSingleRow($sQuery,$aResult) Then Return $aResult[0]
    Return ""
    EndFunc ; ==>_SQL_FetchSingleField

    [/autoit]


    Ich hör die Frage schon: "Wozu denn dieses Doppeltgemoppelte?"
    SQLite ist leider nicht Serverfähig. Wenn ich jetzt aber mit mehreren Leuten einen Dublettenabgleich zugleich(!)
    bearbeite (s.u.), muss ich irgendwo einen Datenserver haben. Da MySQL frei ist, werde ich auch eine MySQL-Schnittstelle einbauen.

    ToDo:

    • eine GUI für das Ganze
    • Import von min. Excel, Access & CSV
    • Export in selbige Formate
    • noch tiefere Unschäfestufen, die dann sogar ...
    • manueller Abgleich der zu unscharfen Dubletten und das ...
    • von mehreren Arbeitsplätzen aus (MySQL)

    Da das "Programm" bis dato nur aus einem Skelett besteht,

    werd den Beitrag wohl noch öfters editieren müssen :huh:


    Aber trotzdem sind mir Verbessungen/Kritik und viel wichtiger Vorschläge und Anregungen sehr willkommen!