antimoda1 commited on
Commit
c1c2970
·
1 Parent(s): 0c818cc

a lot of refactor

Browse files
tests/test_lemmatization.py CHANGED
@@ -11,7 +11,7 @@ from dataclasses import dataclass
11
  sys.path.insert(0, str(Path(__file__).parent.parent))
12
 
13
  from lemmatizer import RussianLemmatizer
14
-
15
 
16
  @dataclass
17
  class TestSearch:
@@ -48,14 +48,13 @@ TESTS_LETTER_E = [
48
  ]
49
 
50
 
51
- def lemmas_in_sentence(query: str, sentence: str, lemmatizer: RussianLemmatizer) -> bool:
52
  """
53
  Проверяет, есть ли леммы из query в sentence после лемматизации.
54
 
55
  Args:
56
  query: поисковое слово
57
  sentence: предложение для проверки
58
- lemmatizer: инстанс лемматизатора
59
 
60
  Returns:
61
  True если есть пересечение лемм
@@ -67,7 +66,7 @@ def lemmas_in_sentence(query: str, sentence: str, lemmatizer: RussianLemmatizer)
67
  return len(query_lemmas & sentence_lemmas) > 0
68
 
69
 
70
- def run_test_suite(test_set_name: str, test_set: list[TestSearch], lemmatizer: RussianLemmatizer) -> tuple[int, int]:
71
  """Запускает набор тестов и возвращает (пройдено, провалено)"""
72
 
73
  print("\n" + "-"*70)
@@ -113,10 +112,7 @@ def test_lemmatization():
113
  print("\n" + "="*70)
114
  print("ТЕСТ ЛЕММАТИЗАЦИИ для русского языка")
115
  print("="*70)
116
-
117
- # Инициализируем лемматизатор один раз
118
- lemmatizer = RussianLemmatizer()
119
-
120
  total_passed = 0
121
  total_failed = 0
122
 
@@ -127,7 +123,7 @@ def test_lemmatization():
127
  ("ОБРАБОТКА Е/Ё", TESTS_LETTER_E),
128
  ]:
129
  if test_set: # Только если есть тесты
130
- passed, failed = run_test_suite(test_name, test_set, lemmatizer)
131
  total_passed += passed
132
  total_failed += failed
133
 
@@ -140,13 +136,4 @@ def test_lemmatization():
140
 
141
 
142
  if __name__ == "__main__":
143
- try:
144
- success = test_lemmatization()
145
- sys.exit(0 if success else 1)
146
- except Exception as e:
147
- print("\n" + "="*70)
148
- print("✗ ОШИБКА ПРИ ВЫПОЛНЕНИИ ТЕСТОВ")
149
- print("="*70)
150
- import traceback
151
- traceback.print_exc()
152
- sys.exit(1)
 
11
  sys.path.insert(0, str(Path(__file__).parent.parent))
12
 
13
  from lemmatizer import RussianLemmatizer
14
+ lemmatizer = RussianLemmatizer()
15
 
16
  @dataclass
17
  class TestSearch:
 
48
  ]
49
 
50
 
51
+ def lemmas_in_sentence(query: str, sentence: str) -> bool:
52
  """
53
  Проверяет, есть ли леммы из query в sentence после лемматизации.
54
 
55
  Args:
56
  query: поисковое слово
57
  sentence: предложение для проверки
 
58
 
59
  Returns:
60
  True если есть пересечение лемм
 
66
  return len(query_lemmas & sentence_lemmas) > 0
67
 
68
 
69
+ def run_test_suite(test_set_name: str, test_set: list[TestSearch]) -> tuple[int, int]:
70
  """Запускает набор тестов и возвращает (пройдено, провалено)"""
71
 
72
  print("\n" + "-"*70)
 
112
  print("\n" + "="*70)
113
  print("ТЕСТ ЛЕММАТИЗАЦИИ для русского языка")
114
  print("="*70)
115
+
 
 
 
116
  total_passed = 0
117
  total_failed = 0
118
 
 
123
  ("ОБРАБОТКА Е/Ё", TESTS_LETTER_E),
124
  ]:
125
  if test_set: # Только если есть тесты
126
+ passed, failed = run_test_suite(test_name, test_set)
127
  total_passed += passed
128
  total_failed += failed
129
 
 
136
 
137
 
138
  if __name__ == "__main__":
139
+ test_lemmatization()
 
 
 
 
 
 
 
 
 
tests/test_vocabular.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Тест поиска терминов с учётом падежей"""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+ sys.path.insert(0, str(Path(__file__).parent.parent))
6
+ from vocabulary.parse_vocabulary import VOCABULARY_MANAGER
7
+
8
+ TESTS_CASES = {
9
+ 'Газель газель Газель газели Газелей': set(('ГАЗель', )),
10
+ 'Икаруса-280 Икарус-260 Икарусом-260 Икарусов-280': set(('Икарус-260', 'Икарус-280')),
11
+ 'Купили ещё автобусов': set(),
12
+ 'Икарус-2600': set(),
13
+ 'Иккарус-260': set(),
14
+ 'Ока': set(('Ока', )),
15
+ 'Окой': set(('Ока', )),
16
+ 'Окружная дорога': set(),
17
+ }
18
+
19
+ for text, terms in TESTS_CASES.items():
20
+ assert VOCABULARY_MANAGER.find_terms(text) == set((el.lower() for el in terms)), breakpoint()
vocabulary/parse_vocabulary.py CHANGED
@@ -1,18 +1,50 @@
 
1
  from typing import Optional
2
-
3
-
4
- def parse_term(term_raw: str) -> tuple[str, str, Optional[str]]:
5
- """Извлекает лемму термина, корень и окончание из строки термина"""
6
- if "[" in term_raw and "]" in term_raw:
7
- bracket_pos = term_raw.index("[")
8
- stem = term_raw[:bracket_pos]
9
- ending = term_raw[bracket_pos + 1: term_raw.index("]")]
10
- else:
11
- stem = term_raw
12
- ending = None
13
- # важно, что не пустую строку! Есть пустое окончание, есть отсутствие окончания у несклоняемых мслов
14
- term_lemma = term_raw.replace("[", "").replace("]", "")
15
- return term_lemma, stem, ending
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
 
18
  class ParserVocabulary:
@@ -26,10 +58,10 @@ class ParserVocabulary:
26
  Определение термина
27
 
28
  - vocabulary: термин (без скобок) -> определение
29
- - stems: корень -> информация об окончании
30
  """
31
  self.vocabulary: dict[str, str] = {}
32
- self.stems: dict[str, dict[str, str]] = {} # корень -> информация об окончании
33
 
34
  with open(filepath, 'r', encoding='utf-8') as f:
35
  lines = f.readlines()
@@ -71,38 +103,10 @@ class ParserVocabulary:
71
  i += 1
72
 
73
  def __parse_term_raw(self, term_raw, definition):
74
- term_lemma, stem, ending = parse_term(term_raw)
75
- self.vocabulary[term_lemma] = definition
 
76
 
77
- # Сохраняем информацию о корне для гибкого поиска
78
- if stem and ending is not None:
79
- if stem.lower() not in self.stems:
80
- self.stems[stem.lower()] = {
81
- 'canonical': term_lemma,
82
- 'ending': ending
83
- }
84
-
85
- def extract_stem(self, word: str):
86
- """
87
- Проверяет, совпадает ли слово с каким-то корнем из stems_dict
88
- с допустимым окончанием (может отличаться на 0-2 буквы)
89
-
90
- Args:
91
- word: слово для проверки (в нижнем регистре)
92
-
93
- Returns:
94
- Канонический вид термина или None
95
- """
96
- for stem, info in self.stems.items():
97
- if word.startswith(stem):
98
- # Вычисляем окончание в слове
99
- word_ending = word[len(stem):]
100
- expected_ending = info['ending']
101
-
102
- if abs(len(word_ending) - len(expected_ending)) <= 2:
103
- return info['canonical']
104
- return None
105
-
106
  def find_terms(self, text: str) -> set[str]:
107
  """Возвращает список терминов в тексте (учитывая словоформы)
108
 
@@ -112,15 +116,20 @@ class ParserVocabulary:
112
  Returns:
113
  set: Множество найденных терминов из vocabulary в порядке их появления
114
  """
115
- found_terms = set() # Для отслеживания дубликатов
116
 
117
- text_lower = text.lower()
 
118
 
119
- for word in text_lower:
120
- res = self.extract_stem(word)
121
- if res:
122
- found_terms.add(word)
123
- return found_terms
 
 
 
 
124
 
125
  def wrap_prompt(self, retrieved_text: str, query_text: str):
126
  tokens_from_query = self.find_terms(query_text)
@@ -148,12 +157,8 @@ class ParserVocabulary:
148
  ОТВЕТ:"""
149
 
150
 
 
151
  import os
152
  current_dir = os.path.dirname(os.path.abspath(__file__))
153
  vocab_file = os.path.join(current_dir, 'vocabulary.md')
154
  VOCABULARY_MANAGER = ParserVocabulary(vocab_file)
155
-
156
-
157
- if __name__ == '__main__':
158
- print(VOCABULARY_MANAGER.vocabulary)
159
- print(VOCABULARY_MANAGER.stems)
 
1
+ import re
2
  from typing import Optional
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass
7
+ class TermPattern:
8
+ """Класс для представления шаблона термина с окончанием"""
9
+ raw_pattern: str
10
+
11
+ def __post_init__(self):
12
+ """Разбор шаблон при создании"""
13
+ self.base_part, self.ending_part, self.suffix = self._parse_pattern(self.raw_pattern)
14
+
15
+ def lemma(self):
16
+ return self.raw_pattern.replace('[', '').replace(']', '')
17
+
18
+ def _parse_pattern(self, pattern: str) -> tuple[str, Optional[str], str, bool]:
19
+ """Разбирает шаблон на составные части.
20
+ Возвращает (основа, окончание, суффикс, был_ли_дефис)
21
+ """
22
+ # Ищем окончание в квадратных скобках
23
+ match = re.search(r'\[(.*?)\]', pattern)
24
+
25
+ if match:
26
+ ending = match.group(1)
27
+ start, end = match.span()
28
+ prefix = pattern[:start]
29
+ suffix = pattern[end:]
30
+ return prefix.lower(), ending.lower(), suffix.lower()
31
+ return pattern, None, ''
32
+
33
+ def matches(self, word: str) -> bool:
34
+ """Проверяет, соответствует ли слово шаблону"""
35
+ word = word.lower()
36
+ if self.ending_part is None:
37
+ # Если окончания нет, слово должно точно совпадать
38
+ return word == self.lemma().lower()
39
+ if not word.startswith(self.base_part):
40
+ return False
41
+ word_without_base = word[len(self.base_part):]
42
+ if not self.suffix:
43
+ return abs((len(word_without_base)-len(self.ending_part))) <= 2
44
+ if not word_without_base.endswith(self.suffix):
45
+ return False
46
+ word_without_base_and_suffix = word_without_base[:-len(self.suffix)]
47
+ return abs((len(word_without_base_and_suffix)-len(self.ending_part))) <= 2
48
 
49
 
50
  class ParserVocabulary:
 
58
  Определение термина
59
 
60
  - vocabulary: термин (без скобок) -> определение
61
+ - patterns: список объектов TermPattern для проверки словоформ
62
  """
63
  self.vocabulary: dict[str, str] = {}
64
+ self.patterns: list[TermPattern] = [] # список шаблонов для проверки
65
 
66
  with open(filepath, 'r', encoding='utf-8') as f:
67
  lines = f.readlines()
 
103
  i += 1
104
 
105
  def __parse_term_raw(self, term_raw, definition):
106
+ pattern = TermPattern(term_raw)
107
+ self.vocabulary[pattern.lemma().lower()] = definition
108
+ self.patterns.append(pattern)
109
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  def find_terms(self, text: str) -> set[str]:
111
  """Возвращает список терминов в тексте (учитывая словоформы)
112
 
 
116
  Returns:
117
  set: Множество найденных терминов из vocabulary в порядке их появления
118
  """
119
+ found_lemmas = set() # Для отслеживания дубликатов
120
 
121
+ # Очищаем текст от знаков препинания и разбиваем на слова
122
+ words = re.findall(r'\b\w+(?:[.-]\w+)*\b', text.lower())
123
 
124
+ for word in words:
125
+ # проверяем по паттернам
126
+ for pattern in self.patterns:
127
+ if pattern.matches(word):
128
+ lemma = pattern.lemma().lower()
129
+ assert lemma in self.vocabulary, breakpoint()
130
+ found_lemmas.add(lemma)
131
+ break # Слово найдено, переходим к следующему
132
+ return found_lemmas
133
 
134
  def wrap_prompt(self, retrieved_text: str, query_text: str):
135
  tokens_from_query = self.find_terms(query_text)
 
157
  ОТВЕТ:"""
158
 
159
 
160
+ # Создаём VOCABULARY_MANAGER, чтобы импортировать его при импортах модуля
161
  import os
162
  current_dir = os.path.dirname(os.path.abspath(__file__))
163
  vocab_file = os.path.join(current_dir, 'vocabulary.md')
164
  VOCABULARY_MANAGER = ParserVocabulary(vocab_file)
 
 
 
 
 
vocabulary/vocabulary.md CHANGED
@@ -118,10 +118,10 @@
118
  ### Икарус[]-180
119
  Автобус особо большого класса (с гармошкой).
120
 
121
- ### Икарус[]-260, Икарус[] 260
122
  Автобус производства венгерской фирмы Ikarus второго поколения.
123
 
124
- ### Икарус[]-280, Икарус[] 280, Икарус[]
125
  Автобус особо большого класса (с гармошкой), долгое время эксплуатировавшийся в Рязани и ставший символом рязанского автобуса.
126
 
127
  ### ЛиАЗ[]-5292, ЛиАЗ[]
 
118
  ### Икарус[]-180
119
  Автобус особо большого класса (с гармошкой).
120
 
121
+ ### Икарус[]-260
122
  Автобус производства венгерской фирмы Ikarus второго поколения.
123
 
124
+ ### Икарус[]-280, Икарус[]
125
  Автобус особо большого класса (с гармошкой), долгое время эксплуатировавшийся в Рязани и ставший символом рязанского автобуса.
126
 
127
  ### ЛиАЗ[]-5292, ЛиАЗ[]