Macro para comparação fonética de nomes (Metaphone em português)

Recentemente precisei fazer uma busca por nomes duplicados e também nome similares que foram escritos errados em um arquivo csv.
Para resolver esse problema usei uma macro em python com um algorítimo tipo metaphone, mas em uma versão português brasileiro. Vou compartilhar aqui no forum.

Usei o módulo disponível em: (Créditos ao Autor)

mas precisei fazer algumas alterações no código:
#foi portado do python 3.10 para o python 3.8 que é utilizado atualmente pelo libreoffice 7.5
#foi alterado o código das vogais, para coincidir nomes com essa caracteristica: [espaço Letra D espaço vogal] ex: Joana D Arc; Estrela D Alva …

esse algorítimo pymetaphone-br usa um módulo de terceiro (Unidecode) que precisa ser instalado com o zaz-pip (extensão do libreoffice para instalar módulos oriundos do repositório pypi)


import uno
import itertools
from unidecode import unidecode #https://pypi.org/project/Unidecode/


def main_comparacao_fonetica(*args):
    doc = XSCRIPTCONTEXT.getDocument()
    sheets = doc.Sheets
    plan_main = sheets[0]

    ultima_linha_preenchida = get_last_used_row(plan_main, 0, 0, (1+2+4))
    
    #cria uma matriz (lista de listas, no caso tupla de tuplas) com os nomes que estão no intervalo "A2:A44"
    tag_conteudo = f'''A2:A{ultima_linha_preenchida}'''
    range_conteudo = plan_main[tag_conteudo]
    matriz_nomes = range_conteudo.DataArray


    #cria uma matriz com os códigos gerados pelo metaphone
    matriz_metaphone = []
    for nome in matriz_nomes:
        lista_linha = []
        lista_linha.append(br_metaphone(nome[0]))
        #matriz_metaphone.append(tuple(lista_linha)) #aceita tanto lista como tupla
        matriz_metaphone.append(lista_linha)


    #escreve a matriz_metaphone no intervalo de destino "B2:B44"
    #foi somado 1, pois os dados serão inseridos apartir da linha B2, se fosse B5 somaria 4
    tag_metaphone = f'''B2:B{len(matriz_metaphone)+1}'''
    range_metaphone = plan_main[tag_metaphone]
    #range_metaphone.DataArray = tuple(matriz_metaphone) #aceita tanto lista como tupla
    range_metaphone.DataArray = matriz_metaphone
    return


def get_last_used_row(oSheet, refCol, startRow, flags):
    """Retorna a última linha preenchida por dados na coluna de referência (refCol)

    Flags de tipos de conteúdo:
    https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet_1_1CellFlags.html

    exemplo flags=7 -> 1 + 2 + 4:
    VALUE = 1
    DATETIME = 2
    STRING = 4

    Args:
        oSheet (objeto planilha): doc.Sheets['Planilha1'] ; doc.Sheets[0] ...
        refCol (integer): coluna de referência para pesquisar o intervalo com os dados, ex: coluna A: 0 
        startRow (integer): primeira linha do intervalo onde estão os dados, ex: linha 1: 0
        flags (integer): somatório das Flags de tipos de conteúdo das celúlas

    Returns:
        integer: número da última linha preenchida na coluna refCol
    """
    oCursor = oSheet.createCursor()
    oCursor.gotoEndOfUsedArea(False)
    
    #número da última linha
    LastRow = oCursor.RangeAddress.EndRow

    #oSheet[linha_inicial:(linha_final + 1), coluna_inicial:(coluna_final + 1)].AbsoluteName
    #print(oSheet[0:(999+1), 0:(1+1)].AbsoluteName) #b1:b1000
    #print(oSheet[startRow:(LastRow+1), refCol:(refCol+1)].AbsoluteName)

    #slice de lista == getCellRangeByPosition
    current_selection_range = oSheet[startRow:(LastRow+1), refCol:(refCol+1)].queryContentCells(flags).RangeAddresses[0]

    #última linha preenchida de acordo com a flag indicada
    LastUsedRow = current_selection_range.EndRow + 1

    return LastUsedRow



#####################################
#Funções do Algorítimo Metaphone português brasileiro
#####################################

#https://pypi.org/project/pymetaphone-br/#files
#foi portado do python 3.10 para o python 3.8 que é utilizado atualmente pelo libreoffice 7.5
#foi alterado o código das vogais, para coincidir nomes com essa caracteristica: [espaço Letra D espaço vogal] ex: Joana D Arc; Estrela D Alva ...

def make_upper_clean(word: str):
    original_word = word.upper()

    word = unidecode(original_word)

    # O Unidecode tira a Ç que precisamos para o nosso algoritmos
    pos_cedilha = [i for i, letter in enumerate(original_word) if letter == "Ç"]
    for i in pos_cedilha:
        word = word[:i] + "Ç" + word[i + 1 :]

    # Replace Y with I
    word = word.replace("Y", "I")

    # Remove duplicate consecutive characters if is not R or S without regex
    word_pieces = []
    for i, g in itertools.groupby(word):

        if i not in ["R", "S"]:
            word_pieces.append(i)
        else:
            word_pieces.extend(list(g))

    return "".join(word_pieces)


def is_vowel(char):
    return char in ["A", "E", "I", "O", "U"]


def br_metaphone(word):
    if word is None or word == "":
        return word

    original = make_upper_clean(word)

    length = len(original)

    # Iterate each character
    metaphone = []
    last_char = " "
    current = 0
    while current < length:
        current_char = original[current]
        ahead_char = original[current + 1] if current + 1 < length else " "
        ahead2_char = original[current + 2] if current + 2 < length else " "
        last2_char = original[current - 2] if current - 2 >= 0 else " "
        last3_char = original[current - 3] if current - 3 >= 0 else " "

        if current_char in ["A" , "E" , "I" , "O" , "U"]:
            #if last_char.isspace():
            if last3_char.isspace() and last2_char == 'D' and last_char.isspace(): #-> ' D Vogal' : joana d arc - joana darc, joana d'arc
                pass
            else:
                if last_char.isspace():
                    metaphone.append(current_char)
        elif current_char == "L":
            if ahead_char == "H":
                metaphone.append("1")
            elif is_vowel(ahead_char) or last_char.isspace():
                metaphone.append("L")
        elif current_char in ["T" , "P"]:
            if ahead_char == "H":
                if current_char == "P":
                    metaphone.append("F")
                else:
                    metaphone.append("T")
                current += 1
            metaphone.append(current_char)
        elif current_char in ["B" , "D" , "F" , "J" , "K" , "M" , "V"]:
            metaphone.append(current_char)
        elif current_char == "G":
            if ahead_char == "H":
                if not is_vowel(ahead2_char):
                    metaphone.append("G")
            if ahead_char in ["H", "E", "I"]:
                metaphone.append("J")
            else:
                metaphone.append("G")
        elif current_char == "R":
            if last_char.isspace() or ahead_char.isspace():
                metaphone.append("2")
            elif ahead_char == "R":
                metaphone.append("2")
                current += 1
            elif is_vowel(last_char) and is_vowel(ahead_char):
                metaphone.append("R")
                current += 1
            else:
                metaphone.append("R")
        elif current_char == "Z":
            if ahead_char.isspace():
                metaphone.append("S")
            else:
                metaphone.append("Z")
        elif current_char == "N":
            if ahead_char.isspace():
                metaphone.append("M")
            elif ahead_char == "H":
                metaphone.append("3")
                current += 1
            elif last_char != "N":
                metaphone.append("N")
        elif current_char == "S":
            if ahead_char == "S":
                metaphone.append("S")
                last_char = ahead_char
                current += 1
            elif ahead_char == "H":
                metaphone.append("X")
                current += 1
            elif is_vowel(last_char) and is_vowel(ahead_char):
                metaphone.append("Z")
            elif ahead_char == "C":
                if ahead2_char in ["E", "I"]:
                    metaphone.append("S")
                    current += 2
                elif ahead2_char in ["A", "O", "U"]:
                    metaphone.append("SK")
                    current += 2
                elif ahead2_char == "H":
                    metaphone.append("X")
                    current += 2
                else:
                    metaphone.append("S")
                    current += 1
            else:
                metaphone.append("S")
        elif current_char == "X":
            if ahead_char.isspace():
                metaphone.append("X")
            elif last_char == "E":
                if is_vowel(ahead_char):
                    if last2_char.isspace():
                        metaphone.append("Z")
                    else:
                        if ahead_char in ["E", "I"]:
                            metaphone.append("X")
                            current += 1
                        else:
                            metaphone.append("KS")
                            current += 1
                elif ahead_char == "C":
                    metaphone.append("S")
                    current += 1
                elif ahead_char in ["P", "T"]:
                    metaphone.append("S")
                else:
                    metaphone.append("KS")
            elif is_vowel(last_char):
                if last2_char in ["A", "E", "I", "O", "U", "C", "K", "G", "L", "R", "X"]:
                    metaphone.append("X")
                else:
                    metaphone.append("KS")
            else:
                metaphone.append("X")
        elif current_char == "C":
            if ahead_char in ["E", "I"]:
                metaphone.append("S")
            elif ahead_char == "H":
                if ahead2_char == "R":
                    metaphone.append("K")
                else:
                    metaphone.append("X")
                current += 1
            elif (ahead_char == "Q") or (ahead_char == "K"):
                pass
            else:
                metaphone.append("K")
        elif current_char == "H":
            if last_char.isspace():
                if is_vowel(ahead_char):
                    metaphone.append(ahead_char)
                    current += 1
        elif current_char == "Q":
            metaphone.append("K")
        elif current_char == "W":
            if is_vowel(ahead_char):
                metaphone.append("V")
        elif current_char == "Ç":
            metaphone.append("S")

        last_char = current_char
        current += 1
    return "".join(metaphone)
#####################################

Não é um código que funciona em 100% dos casos, mas já ajuda bastante na buscas por nomes repetidos ou com erros de digitação.

Ex:
David Barros
Davi Barros
Deivid Barros

consegue identificar David Barros e Deivid Barros. Porém não identifica Davi Barros. Esses nomes poderiam ser apenas uma pessoa com três cadastros diferentes.

Mas para confirmar com certeza qualquer duplicidade casdastral sempre temos que ter no mínimo três paramentros para confirmar se são pessoas distintas ou iguais. Ex: Nome, Data de Nascimento, Nome da mãe


no arquivo de exemplo vou usar apenas o nome da pessoa para demonstração.
Na coluna A temos os nomes, na coluna B temos o código metaphone.
Na coluna B apliquei uma formatação condicional que procura por duplicidades, com estilo ERRO (fundo vermelho)
Depois basta filtrar a coluna B pela cor de fundo vermelha que serão exibidos os nomes similares/repetidos identificados pelo metaphone

Obs: Para usar macros em python recomendo essas extensões:
Apso para embutir as macros nos arquivos do libreoffice
https://gitlab.com/jmzambon/apso/-/raw/master/apso.oxt

ZAZPip para instalar os módulos do repositório PyPi
https://git.cuates.net/elmau/zaz-pip/raw/branch/master/extension/ZAZPip_v0.10.2.oxt

como usar a extensão ZAZPip

tutorial em espanhol sobre macros em python no libreoffice, feitos pelo Mauricio Baeza


Arquivo de exemplo:
teste comparação metaphone.ods (27,7,KB)

2 Likes