-
Notifications
You must be signed in to change notification settings - Fork 0
/
corrector.py
182 lines (142 loc) · 5.94 KB
/
corrector.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
"""Logica del corrector de palabras."""
import re
from typing import Protocol
from pathlib import Path
import csv
from dotenv import dotenv_values
from maybe import Maybe
config = dotenv_values(".env")
# Clases de modelo
class WordStatistics:
"""Coleccion de palabras. Contiene funciones que actuan sobre esta
colecion para ofrecer informacion estadistica.
"""
def __init__(self, words: dict[str, int]):
self.words = words
self.letters = list(set("".join(words.keys())))
self._size = None
@property
def size(self) -> int:
"""Tamño del conjunto de datos.
No es lo mismo que la cantidad de las palabras
"""
if not self._size:
self._size = sum(self.words.values())
return self._size
def get_freq_abs(self, word: str) -> Maybe[int]:
return Maybe(self.words.get(word))
def get_freq_rel(self, word: str) -> Maybe[float]:
return self.get_freq_abs(word).map(lambda x: x/self.size)
def add_word(self, word: str):
if self.words.get(word) is not None:
self.words[word] += 1
else:
self.words[word] = 1
class Corrector(Protocol):
def spell_check(self, word: str) -> list[str]: ...
def add_word(self, word: str): ...
class ModelLoader(Protocol):
"""Protocolo para definir un cargador de modelo de estadisticas de palabras"""
def get_model(self) -> WordStatistics:
"""
Obtiene el modelo de estadisticas de palabras.
Returns
-------
WordStatistics
Instancia de WordStatistics que contiene las estadisticas de palabras cargadas
"""
...
class ModelUnloader(Protocol):
def save_model(self, word_statistics: WordStatistics): ...
class NorvigCorrector:
"""Corrector de palabras basado en el algoritmo de correccion de Norvig"""
def __init__(self, word_statistics: WordStatistics, saver: ModelUnloader):
"""
Inicializa el corrector con las estadisticas de palabras
Parameters
----------
word_statistics : WordStatistics
Estadisticas de palabras utilizadas para la correccion
saver: ModelUnloader
Encargado de guardar el modelo de lenguaje.
"""
self.word_statistics = word_statistics
self.saver = saver
def add_word(self, word: str):
"""Añade una palabra al diccionario.
Parameters
----------
word: str
Palabra a añadir.
"""
self.word_statistics.add_word(word)
self.saver.save_model(self.word_statistics)
def edits1(self, word: str):
"""
Genera las ediciones 1 de la palabra a la vez
Parameters
----------
word : str
Palabra de la cual se generaran las ediciones
Returns
-------
set
Conjunto que contiene todas las ediciones a una distancia de edicion de 1 de la palabra
"""
# Genera una lista de tuplas de sub-palabras de la palabra `word`
# las cuales se dividen en pedazos. el + 1 es por que el slicing
# parara en i, lo cual no incluira el final de la lista si solo se toma la longitud
# Ejemplo: word = sazon
# splits = [("", "sazon"), ("s", "azon"), ("sa", "zon"), ("saz", "on"), ("sazo", "n"), ("sazon", "")]
splits = [(word[:i], word[i:]) for i in range(len(word) + 1)]
# Elimina una palabra. Hace esto saltandose una letra del lado
# derecho de las divisiones anterior y concatenando con el lado derecho
deletes = [L + R[1:] for L, R in splits if R]
# Transpone dos letras que estan adayacentes entre si.
# Usa el mismo truco que el anterior
transposes = [L + R[1] + R[0] + R[2:] for L, R in splits if len(R) > 1]
replaces = [L + c + R[1:] for L, R in splits if R for c in self.word_statistics.letters]
inserts = [L + c + R for L, R in splits for c in self.word_statistics.letters]
return set(deletes + transposes + replaces + inserts)
def edits2(self, word):
"All edits that are two edits away from `word`."
return (e2 for e1 in self.edits1(word) for e2 in self.edits1(e1))
def candidates(self, word):
"Generate possible spelling corrections for word."
return (
self.known([word])
or self.known(self.edits1(word))
or self.known(self.edits2(word))
or [word]
)
def known(self, words):
"The subset of `words` that appear in the dictionary of WORDS."
return set(w for w in words if w in self.word_statistics.words)
def spell_check(self, word: str) -> list[str]:
return [max(self.candidates(word), key=lambda x: self.word_statistics.get_freq_rel(x).value or -1)]
class CsvModelLoader:
def __init__(self, model_path: str | Path):
self.model_path = model_path
def only_words(self, word):
return (
Maybe(re.search(r"\w+", word)).map(lambda x: x.group()).value
and word not in "1234567890"
)
def get_model(self) -> WordStatistics:
with open(self.model_path, encoding="utf8") as model_file:
# Este conjunto de datos usa la comilla como character
model_reader = csv.reader(model_file, delimiter="\t", quotechar=None)
model_reader = filter(lambda line: self.only_words(line[0]), model_reader)
# Salta el encabezado
next(model_reader)
return WordStatistics(
{line[0]: int(line[1]) for line in model_reader}
)
def save_model(self, word_statistics: WordStatistics):
header = [
"Palabras", "Frecuencias"
]
with open(self.model_path, mode="w", encoding="utf8") as model_file:
model_writer = csv.writer(model_file, delimiter="\t", quotechar=None)
model_writer.writerow(header)
model_writer.writerows([k, v] for k, v in word_statistics.words.items())