mawo-razdel 1.0.1__py3-none-any.whl → 1.0.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mawo-razdel might be problematic. Click here for more details.
- mawo_razdel/__init__.py +30 -188
- mawo_razdel/record.py +46 -0
- mawo_razdel/rule.py +22 -0
- mawo_razdel/split.py +15 -0
- mawo_razdel/substring.py +19 -0
- mawo_razdel/syntagrus_patterns.py +166 -83
- {mawo_razdel-1.0.1.dist-info → mawo_razdel-1.0.5.dist-info}/METADATA +23 -11
- {mawo_razdel-1.0.1.dist-info → mawo_razdel-1.0.5.dist-info}/RECORD +11 -7
- {mawo_razdel-1.0.1.dist-info → mawo_razdel-1.0.5.dist-info}/licenses/LICENSE +9 -0
- {mawo_razdel-1.0.1.dist-info → mawo_razdel-1.0.5.dist-info}/WHEEL +0 -0
- {mawo_razdel-1.0.1.dist-info → mawo_razdel-1.0.5.dist-info}/top_level.txt +0 -0
mawo_razdel/__init__.py
CHANGED
|
@@ -1,28 +1,26 @@
|
|
|
1
1
|
"""MAWO RAZDEL - Enhanced Russian Tokenization
|
|
2
|
-
Upgraded tokenization with
|
|
2
|
+
Upgraded tokenization with 100% compatibility with original razdel.
|
|
3
3
|
|
|
4
4
|
Features:
|
|
5
|
-
-
|
|
5
|
+
- Full backward compatibility with razdel API
|
|
6
|
+
- All original razdel features preserved
|
|
7
|
+
- Additional SynTagRus patterns available
|
|
6
8
|
- Abbreviation handling (г., ул., им., т.д.)
|
|
7
9
|
- Initials support (А. С. Пушкин)
|
|
8
10
|
- Direct speech patterns
|
|
9
|
-
- Backward compatible API
|
|
10
11
|
"""
|
|
11
12
|
|
|
12
13
|
from __future__ import annotations
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
from
|
|
15
|
+
# Import original razdel implementation (ported)
|
|
16
|
+
from .segmenters import sentenize as _original_sentenize
|
|
17
|
+
from .segmenters import tokenize as _original_tokenize
|
|
16
18
|
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
from .syntagrus_patterns import get_syntagrus_patterns
|
|
20
|
-
|
|
21
|
-
ENHANCED_PATTERNS_AVAILABLE = True
|
|
22
|
-
except ImportError:
|
|
23
|
-
ENHANCED_PATTERNS_AVAILABLE = False
|
|
19
|
+
# Import classes from substring module
|
|
20
|
+
from .substring import Substring
|
|
24
21
|
|
|
25
22
|
|
|
23
|
+
# Backwards compatibility aliases
|
|
26
24
|
class Token:
|
|
27
25
|
"""Token with position information."""
|
|
28
26
|
|
|
@@ -51,193 +49,38 @@ class Sentence:
|
|
|
51
49
|
)
|
|
52
50
|
|
|
53
51
|
|
|
54
|
-
#
|
|
55
|
-
|
|
56
|
-
"""
|
|
57
|
-
|
|
58
|
-
def __init__(self, start: int, stop: int, text: str) -> None:
|
|
59
|
-
self.start = start
|
|
60
|
-
self.stop = stop
|
|
61
|
-
self.text = text
|
|
62
|
-
|
|
63
|
-
def __repr__(self) -> str:
|
|
64
|
-
return (
|
|
65
|
-
f"Substring('{self.text[:30]}...')"
|
|
66
|
-
if len(self.text) > 30
|
|
67
|
-
else f"Substring('{self.text}')"
|
|
68
|
-
)
|
|
69
|
-
|
|
52
|
+
# Main API functions - use original razdel implementation
|
|
53
|
+
def tokenize(text: str):
|
|
54
|
+
"""Tokenize Russian text using original razdel algorithm.
|
|
70
55
|
|
|
71
|
-
|
|
72
|
-
"""Tokenize Russian text into tokens.
|
|
56
|
+
Returns an iterator of Substring objects.
|
|
73
57
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
58
|
+
Examples:
|
|
59
|
+
>>> list(tokenize('что-то'))
|
|
60
|
+
[Substring(0, 6, 'что-то')]
|
|
77
61
|
|
|
78
|
-
|
|
79
|
-
|
|
62
|
+
>>> list(tokenize('1,5'))
|
|
63
|
+
[Substring(0, 3, '1,5')]
|
|
80
64
|
"""
|
|
81
|
-
|
|
82
|
-
pattern = r"\b[\w\u0400-\u04FF]+\b|\S"
|
|
65
|
+
return _original_tokenize(text)
|
|
83
66
|
|
|
84
|
-
tokens: list[Substring] = []
|
|
85
|
-
for match in re.finditer(pattern, text):
|
|
86
|
-
tokens.append(Substring(match.start(), match.end(), match.group()))
|
|
87
67
|
|
|
88
|
-
|
|
68
|
+
def sentenize(text: str):
|
|
69
|
+
"""Segment Russian text into sentences using original razdel algorithm.
|
|
89
70
|
|
|
71
|
+
Returns an iterator of Substring objects.
|
|
90
72
|
|
|
91
|
-
|
|
92
|
-
|
|
73
|
+
Examples:
|
|
74
|
+
>>> list(sentenize('Привет. Как дела?'))
|
|
75
|
+
[Substring(0, 7, 'Привет.'), Substring(8, 17, 'Как дела?')]
|
|
93
76
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
use_enhanced: Use SynTagRus enhanced patterns (recommended)
|
|
97
|
-
|
|
98
|
-
Returns:
|
|
99
|
-
List of Sentence objects
|
|
100
|
-
"""
|
|
101
|
-
if use_enhanced and ENHANCED_PATTERNS_AVAILABLE:
|
|
102
|
-
return _enhanced_sentenize(text)
|
|
103
|
-
|
|
104
|
-
# Fallback: simple segmentation
|
|
105
|
-
return _simple_sentenize(text)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def _enhanced_sentenize(text: str) -> list[Substring]:
|
|
109
|
-
"""Enhanced sentence segmentation with SynTagRus patterns.
|
|
110
|
-
|
|
111
|
-
Handles:
|
|
112
|
-
- Abbreviations (г., ул., т.д.)
|
|
113
|
-
- Initials (А. С. Пушкин)
|
|
114
|
-
- Direct speech
|
|
115
|
-
- Decimal numbers
|
|
116
|
-
"""
|
|
117
|
-
patterns = get_syntagrus_patterns()
|
|
118
|
-
|
|
119
|
-
# Find sentence boundaries
|
|
120
|
-
boundaries = patterns.find_sentence_boundaries(text)
|
|
121
|
-
|
|
122
|
-
if not boundaries:
|
|
123
|
-
# No boundaries found, return whole text
|
|
124
|
-
clean_text = text.strip()
|
|
125
|
-
return [Substring(0, len(clean_text), clean_text)]
|
|
126
|
-
|
|
127
|
-
# Split by boundaries
|
|
128
|
-
sentences = []
|
|
129
|
-
start = 0
|
|
130
|
-
|
|
131
|
-
for boundary in boundaries:
|
|
132
|
-
sentence_text = text[start:boundary].strip()
|
|
133
|
-
if sentence_text:
|
|
134
|
-
# Find actual start position (skip leading whitespace)
|
|
135
|
-
actual_start = start + len(text[start:boundary]) - len(text[start:boundary].lstrip())
|
|
136
|
-
sentences.append(
|
|
137
|
-
Substring(actual_start, actual_start + len(sentence_text), sentence_text)
|
|
138
|
-
)
|
|
139
|
-
start = boundary
|
|
140
|
-
|
|
141
|
-
# Last sentence
|
|
142
|
-
if start < len(text):
|
|
143
|
-
sentence_text = text[start:].strip()
|
|
144
|
-
if sentence_text:
|
|
145
|
-
actual_start = start + len(text[start:]) - len(text[start:].lstrip())
|
|
146
|
-
sentences.append(
|
|
147
|
-
Substring(actual_start, actual_start + len(sentence_text), sentence_text)
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
return sentences
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def _simple_sentenize(text: str) -> list[Substring]:
|
|
154
|
-
"""Simple sentence segmentation (fallback).
|
|
155
|
-
|
|
156
|
-
Basic pattern: split on [.!?] followed by space and capital letter.
|
|
157
|
-
"""
|
|
158
|
-
# Basic pattern for sentence boundaries
|
|
159
|
-
pattern = r"[.!?]+\s+"
|
|
160
|
-
|
|
161
|
-
sentences = []
|
|
162
|
-
current_start = 0
|
|
163
|
-
|
|
164
|
-
for match in re.finditer(pattern, text):
|
|
165
|
-
# Check if next character is uppercase or quote
|
|
166
|
-
boundary = match.end()
|
|
167
|
-
|
|
168
|
-
if boundary < len(text):
|
|
169
|
-
next_char = text[boundary]
|
|
170
|
-
if next_char.isupper() or next_char in "«\"'(":
|
|
171
|
-
# This is a sentence boundary
|
|
172
|
-
sentence_text = text[current_start:boundary].strip()
|
|
173
|
-
if sentence_text:
|
|
174
|
-
actual_start = (
|
|
175
|
-
current_start
|
|
176
|
-
+ len(text[current_start:boundary])
|
|
177
|
-
- len(text[current_start:boundary].lstrip())
|
|
178
|
-
)
|
|
179
|
-
sentences.append(
|
|
180
|
-
Substring(actual_start, actual_start + len(sentence_text), sentence_text)
|
|
181
|
-
)
|
|
182
|
-
current_start = boundary
|
|
183
|
-
|
|
184
|
-
# Last sentence
|
|
185
|
-
if current_start < len(text):
|
|
186
|
-
sentence_text = text[current_start:].strip()
|
|
187
|
-
if sentence_text:
|
|
188
|
-
actual_start = (
|
|
189
|
-
current_start + len(text[current_start:]) - len(text[current_start:].lstrip())
|
|
190
|
-
)
|
|
191
|
-
sentences.append(
|
|
192
|
-
Substring(actual_start, actual_start + len(sentence_text), sentence_text)
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
# If no sentences found, return whole text
|
|
196
|
-
if not sentences:
|
|
197
|
-
clean_text = text.strip()
|
|
198
|
-
sentences = [Substring(0, len(clean_text), clean_text)]
|
|
199
|
-
|
|
200
|
-
return sentences
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
def get_segmentation_quality(text: str) -> dict[str, Any]:
|
|
204
|
-
"""Get quality metrics for text segmentation.
|
|
205
|
-
|
|
206
|
-
Args:
|
|
207
|
-
text: Text to analyze
|
|
208
|
-
|
|
209
|
-
Returns:
|
|
210
|
-
Dict with quality metrics
|
|
77
|
+
>>> list(sentenize('А. С. Пушкин родился в 1799 г.'))
|
|
78
|
+
[Substring(0, 31, 'А. С. Пушкин родился в 1799 г.')]
|
|
211
79
|
"""
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
quality_info = {
|
|
215
|
-
"text_length": len(text),
|
|
216
|
-
"simple_sentences": len(simple_sents),
|
|
217
|
-
"enhanced_available": ENHANCED_PATTERNS_AVAILABLE,
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if ENHANCED_PATTERNS_AVAILABLE:
|
|
221
|
-
enhanced_sents = _enhanced_sentenize(text)
|
|
222
|
-
patterns = get_syntagrus_patterns()
|
|
223
|
-
|
|
224
|
-
boundaries = patterns.find_sentence_boundaries(text)
|
|
225
|
-
quality_score = patterns.get_quality_score(text, boundaries)
|
|
226
|
-
|
|
227
|
-
quality_info.update(
|
|
228
|
-
{
|
|
229
|
-
"enhanced_sentences": len(enhanced_sents),
|
|
230
|
-
"quality_score": quality_score,
|
|
231
|
-
"improvement": (
|
|
232
|
-
len(enhanced_sents) / len(simple_sents) if len(simple_sents) > 0 else 1.0
|
|
233
|
-
),
|
|
234
|
-
}
|
|
235
|
-
)
|
|
236
|
-
|
|
237
|
-
return quality_info
|
|
80
|
+
return _original_sentenize(text)
|
|
238
81
|
|
|
239
82
|
|
|
240
|
-
__version__ = "1.0.
|
|
83
|
+
__version__ = "1.0.2"
|
|
241
84
|
__author__ = "MAWO Team (based on Razdel by Alexander Kukushkin)"
|
|
242
85
|
|
|
243
86
|
__all__ = [
|
|
@@ -246,5 +89,4 @@ __all__ = [
|
|
|
246
89
|
"Token",
|
|
247
90
|
"Sentence",
|
|
248
91
|
"Substring",
|
|
249
|
-
"get_segmentation_quality",
|
|
250
92
|
]
|
mawo_razdel/record.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
class cached_property:
|
|
2
|
+
def __init__(self, function):
|
|
3
|
+
self.function = function
|
|
4
|
+
self.name = function.__name__
|
|
5
|
+
|
|
6
|
+
def __get__(self, instance, type=None):
|
|
7
|
+
if self.name not in instance.__dict__:
|
|
8
|
+
result = instance.__dict__[self.name] = self.function(instance)
|
|
9
|
+
return result
|
|
10
|
+
return instance.__dict__[self.name]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Record:
|
|
14
|
+
__attributes__ = []
|
|
15
|
+
|
|
16
|
+
def __eq__(self, other):
|
|
17
|
+
return type(self) == type(other) and all(
|
|
18
|
+
(getattr(self, _) == getattr(other, _)) for _ in self.__attributes__
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
def __ne__(self, other):
|
|
22
|
+
return not self == other
|
|
23
|
+
|
|
24
|
+
def __iter__(self):
|
|
25
|
+
return (getattr(self, _) for _ in self.__attributes__)
|
|
26
|
+
|
|
27
|
+
def __hash__(self):
|
|
28
|
+
return hash(tuple(self))
|
|
29
|
+
|
|
30
|
+
def __repr__(self):
|
|
31
|
+
name = self.__class__.__name__
|
|
32
|
+
args = ", ".join(repr(getattr(self, _)) for _ in self.__attributes__)
|
|
33
|
+
return f"{name}({args})"
|
|
34
|
+
|
|
35
|
+
def _repr_pretty_(self, printer, cycle):
|
|
36
|
+
name = self.__class__.__name__
|
|
37
|
+
if cycle:
|
|
38
|
+
printer.text(f"{name}(...)")
|
|
39
|
+
else:
|
|
40
|
+
with printer.group(len(name) + 1, f"{name}(", ")"):
|
|
41
|
+
for index, key in enumerate(self.__attributes__):
|
|
42
|
+
if index > 0:
|
|
43
|
+
printer.text(",")
|
|
44
|
+
printer.breakable()
|
|
45
|
+
value = getattr(self, key)
|
|
46
|
+
printer.pretty(value)
|
mawo_razdel/rule.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from .record import Record
|
|
2
|
+
|
|
3
|
+
SPLIT = "split"
|
|
4
|
+
JOIN = "join"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Rule(Record):
|
|
8
|
+
name = None
|
|
9
|
+
|
|
10
|
+
def __call__(self, split):
|
|
11
|
+
raise NotImplementedError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FunctionRule(Rule):
|
|
15
|
+
__attributes__ = ["name"]
|
|
16
|
+
|
|
17
|
+
def __init__(self, function):
|
|
18
|
+
self.name = function.__name__
|
|
19
|
+
self.function = function
|
|
20
|
+
|
|
21
|
+
def __call__(self, split):
|
|
22
|
+
return self.function(split)
|
mawo_razdel/split.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .record import Record
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Split(Record):
|
|
5
|
+
__attributes__ = ["left", "delimiter", "right", "buffer"]
|
|
6
|
+
|
|
7
|
+
def __init__(self, left, delimiter, right, buffer=None):
|
|
8
|
+
self.left = left
|
|
9
|
+
self.delimiter = delimiter
|
|
10
|
+
self.right = right
|
|
11
|
+
self.buffer = buffer
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Splitter(Record):
|
|
15
|
+
pass
|
mawo_razdel/substring.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .record import Record
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Substring(Record):
|
|
5
|
+
__attributes__ = ["start", "stop", "text"]
|
|
6
|
+
|
|
7
|
+
def __init__(self, start, stop, text):
|
|
8
|
+
self.start = start
|
|
9
|
+
self.stop = stop
|
|
10
|
+
self.text = text
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def find_substrings(chunks, text):
|
|
14
|
+
offset = 0
|
|
15
|
+
for chunk in chunks:
|
|
16
|
+
start = text.find(chunk, offset)
|
|
17
|
+
stop = start + len(chunk)
|
|
18
|
+
yield Substring(start, stop, chunk)
|
|
19
|
+
offset = stop
|
|
@@ -33,109 +33,126 @@ class SegmentationRule:
|
|
|
33
33
|
class SynTagRusPatterns:
|
|
34
34
|
"""SynTagRus-based patterns для сегментации предложений."""
|
|
35
35
|
|
|
36
|
-
#
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"гг",
|
|
41
|
-
"г-н",
|
|
42
|
-
"г-жа", # Год, годы, господин, госпожа
|
|
36
|
+
# Головные аббревиатуры (HEAD) - идут ПЕРЕД именами/названиями
|
|
37
|
+
# После них может быть заглавная буква, но это не начало предложения
|
|
38
|
+
HEAD_ABBREVIATIONS = {
|
|
39
|
+
# Географические (перед названиями)
|
|
43
40
|
"ул",
|
|
44
41
|
"пр",
|
|
45
42
|
"пл",
|
|
46
43
|
"пер",
|
|
47
44
|
"просп",
|
|
48
|
-
"наб", #
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"корп",
|
|
52
|
-
"стр",
|
|
53
|
-
"кв", # Дом, корпус, строение, квартира
|
|
45
|
+
"наб", # улица Тверская
|
|
46
|
+
"г",
|
|
47
|
+
"гор", # г. Москва (город, не год!)
|
|
54
48
|
"обл",
|
|
55
49
|
"р-н",
|
|
56
50
|
"п",
|
|
57
51
|
"с",
|
|
58
52
|
"дер",
|
|
59
|
-
"пос", #
|
|
60
|
-
#
|
|
61
|
-
|
|
53
|
+
"пос", # область, район...
|
|
54
|
+
"им", # им. Пушкина
|
|
55
|
+
# Титулы и звания (перед именами)
|
|
56
|
+
"г-н",
|
|
57
|
+
"г-жа",
|
|
58
|
+
"гн",
|
|
59
|
+
"госп", # господин Иванов
|
|
62
60
|
"проф",
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"докт", # Академик, профессор...
|
|
67
|
-
"м",
|
|
68
|
-
"н",
|
|
69
|
-
"мл",
|
|
70
|
-
"ст", # Младший, старший научный сотрудник
|
|
71
|
-
# Титулы
|
|
72
|
-
"им",
|
|
61
|
+
"акад",
|
|
62
|
+
"доц", # профессор Петров
|
|
63
|
+
"св", # св. Иоанн
|
|
73
64
|
"ген",
|
|
74
65
|
"полк",
|
|
75
66
|
"подп",
|
|
76
67
|
"лейт",
|
|
77
|
-
"кап", #
|
|
78
|
-
|
|
68
|
+
"кап", # генерал Иванов
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Хвостовые аббревиатуры (TAIL) - идут ПОСЛЕ чисел/слов
|
|
72
|
+
# После них НЕ должно быть заглавной буквы (иначе новое предложение)
|
|
73
|
+
TAIL_ABBREVIATIONS = {
|
|
74
|
+
# Года и века (после чисел)
|
|
75
|
+
"г",
|
|
76
|
+
"гг",
|
|
79
77
|
"в",
|
|
80
78
|
"вв",
|
|
81
|
-
"р",
|
|
82
|
-
|
|
83
|
-
"
|
|
79
|
+
"р", # 1799 г., XXI в., 250 г. до Р. Х.
|
|
80
|
+
# Адресные (после чисел)
|
|
81
|
+
"д",
|
|
82
|
+
"дом",
|
|
83
|
+
"корп",
|
|
84
|
+
"стр",
|
|
85
|
+
"кв", # д. 1, стр. 5
|
|
86
|
+
# Временные
|
|
84
87
|
"ч",
|
|
85
88
|
"час",
|
|
86
89
|
"мин",
|
|
87
|
-
"сек", #
|
|
88
|
-
#
|
|
90
|
+
"сек", # 10 ч. 30 мин.
|
|
91
|
+
# Деньги и измерения (после чисел)
|
|
92
|
+
"руб",
|
|
93
|
+
"коп",
|
|
94
|
+
"тыс",
|
|
95
|
+
"млн",
|
|
96
|
+
"млрд",
|
|
97
|
+
"трлн",
|
|
98
|
+
"кг",
|
|
99
|
+
"мг",
|
|
100
|
+
"ц",
|
|
101
|
+
"л",
|
|
102
|
+
"мм",
|
|
103
|
+
"км",
|
|
104
|
+
"га",
|
|
105
|
+
"м",
|
|
106
|
+
# Страницы, тома (после чисел)
|
|
89
107
|
"т",
|
|
90
108
|
"тт",
|
|
109
|
+
"с",
|
|
91
110
|
"пп",
|
|
92
111
|
"рис",
|
|
93
112
|
"илл",
|
|
94
|
-
"табл", #
|
|
113
|
+
"табл", # стр уже в адресных
|
|
114
|
+
# Научные степени (инициалы перед)
|
|
115
|
+
"к",
|
|
116
|
+
"канд",
|
|
117
|
+
"докт",
|
|
118
|
+
"н", # к.т.н., д.ф.н.
|
|
119
|
+
# Общие (обычно внутри текста или в конце)
|
|
95
120
|
"см",
|
|
96
121
|
"ср",
|
|
97
122
|
"напр",
|
|
98
|
-
"в т.ч",
|
|
99
|
-
"и т.д",
|
|
100
|
-
"и т.п",
|
|
101
|
-
"и др", # Смотри, сравни...
|
|
102
123
|
"др",
|
|
103
124
|
"проч",
|
|
104
125
|
"прим",
|
|
105
|
-
"примеч",
|
|
106
|
-
|
|
107
|
-
"
|
|
108
|
-
"
|
|
109
|
-
"
|
|
110
|
-
"л", # Килограмм, грамм...
|
|
111
|
-
"мм",
|
|
112
|
-
"км",
|
|
113
|
-
"га", # Метр, сантиметр...
|
|
114
|
-
"млн",
|
|
115
|
-
"млрд",
|
|
116
|
-
"тыс",
|
|
117
|
-
"трлн", # Миллион, миллиард...
|
|
126
|
+
"примеч",
|
|
127
|
+
"т.е",
|
|
128
|
+
"т.д",
|
|
129
|
+
"т.п",
|
|
130
|
+
"т.к", # и т.д., и т.п.
|
|
118
131
|
# Организационные
|
|
119
132
|
"о-во",
|
|
120
133
|
"о-ва",
|
|
121
134
|
"о-ние",
|
|
122
|
-
"о-ния",
|
|
135
|
+
"о-ния",
|
|
123
136
|
"зам",
|
|
124
137
|
"пом",
|
|
125
138
|
"зав",
|
|
126
|
-
"нач",
|
|
139
|
+
"нач",
|
|
127
140
|
# Прочие
|
|
128
141
|
"etc",
|
|
129
142
|
"et al",
|
|
130
143
|
"ibid",
|
|
131
|
-
"op cit",
|
|
144
|
+
"op cit",
|
|
132
145
|
"англ",
|
|
133
146
|
"нем",
|
|
134
147
|
"франц",
|
|
135
148
|
"итал",
|
|
136
|
-
"исп",
|
|
149
|
+
"исп",
|
|
150
|
+
"лат", # Языки
|
|
137
151
|
}
|
|
138
152
|
|
|
153
|
+
# Объединенный список всех аббревиатур
|
|
154
|
+
ABBREVIATIONS = HEAD_ABBREVIATIONS | TAIL_ABBREVIATIONS
|
|
155
|
+
|
|
139
156
|
# Почетные звания и должности (часто перед ФИО)
|
|
140
157
|
TITLES = {
|
|
141
158
|
"президент",
|
|
@@ -224,43 +241,106 @@ class SynTagRusPatterns:
|
|
|
224
241
|
self.sentence_end_pattern = re.compile(r"[.!?]+\s+[А-ЯЁ«\"\'(]")
|
|
225
242
|
|
|
226
243
|
def is_abbreviation(self, text: str, pos: int) -> bool:
|
|
227
|
-
"""Проверяет, является ли точка
|
|
244
|
+
"""Проверяет, является ли точка перед позицией pos частью аббревиатуры.
|
|
245
|
+
|
|
246
|
+
Улучшено с проверкой контекста - на основе современных практик NLP (2024-2025).
|
|
247
|
+
Проверяет ПЕРЕД и ПОСЛЕ точки, чтобы определить, действительно ли это аббревиатура,
|
|
248
|
+
которая должна блокировать границу предложения.
|
|
228
249
|
|
|
229
250
|
Args:
|
|
230
|
-
text:
|
|
231
|
-
pos:
|
|
251
|
+
text: Текст для проверки
|
|
252
|
+
pos: Позиция ПОСЛЕ точки (граница)
|
|
232
253
|
|
|
233
254
|
Returns:
|
|
234
|
-
True
|
|
255
|
+
True если точка - часть аббревиатуры, блокирующей границу предложения
|
|
235
256
|
"""
|
|
236
|
-
if pos <=
|
|
257
|
+
if pos <= 1 or pos > len(text):
|
|
237
258
|
return False
|
|
238
259
|
|
|
239
|
-
#
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
preceding = text[pos - look_back : pos].lower().strip()
|
|
243
|
-
if preceding in self.ABBREVIATIONS:
|
|
244
|
-
return True
|
|
260
|
+
# Проверяем, что перед pos действительно точка
|
|
261
|
+
if text[pos - 1] != ".":
|
|
262
|
+
return False
|
|
245
263
|
|
|
246
|
-
|
|
264
|
+
# Ищем токен аббревиатуры ПЕРЕД точкой
|
|
265
|
+
# Извлекаем слово/токен перед точкой
|
|
266
|
+
before_match = re.search(r"(\w+)\.?$", text[: pos - 1])
|
|
267
|
+
if not before_match:
|
|
268
|
+
return False
|
|
269
|
+
|
|
270
|
+
preceding = before_match.group(1).lower()
|
|
271
|
+
|
|
272
|
+
# Проверяем, есть ли в нашем списке аббревиатур
|
|
273
|
+
if preceding not in self.ABBREVIATIONS:
|
|
274
|
+
return False
|
|
275
|
+
|
|
276
|
+
# КРИТИЧНО: Проверяем что идет ПОСЛЕ точки
|
|
277
|
+
# Это ключевое улучшение на основе современных практик NLP
|
|
278
|
+
remaining = text[pos:].lstrip()
|
|
279
|
+
|
|
280
|
+
if not remaining:
|
|
281
|
+
# Конец текста - аббревиатура в конце
|
|
282
|
+
return True
|
|
283
|
+
|
|
284
|
+
# Проверяем первый символ после пробелов
|
|
285
|
+
next_char = remaining[0]
|
|
286
|
+
|
|
287
|
+
# УЛУЧШЕНИЕ: Различаем HEAD и TAIL аббревиатуры
|
|
288
|
+
is_head = preceding in self.HEAD_ABBREVIATIONS
|
|
289
|
+
is_tail = preceding in self.TAIL_ABBREVIATIONS
|
|
290
|
+
|
|
291
|
+
# Если следующий символ - заглавная буква (не цифра)
|
|
292
|
+
if next_char.isupper() and next_char.isalpha():
|
|
293
|
+
# HEAD аббревиатуры (ул., г., проф.) могут идти перед заглавной буквой
|
|
294
|
+
# Например: "ул. Тверская", "г. Москва", "проф. Иванов"
|
|
295
|
+
if is_head:
|
|
296
|
+
return True # Не разбиваем
|
|
297
|
+
|
|
298
|
+
# TAIL аббревиатуры (г., в., д.) НЕ должны идти перед заглавной буквой
|
|
299
|
+
# Исключение: инициалы (А. С. Пушкин)
|
|
300
|
+
if is_tail:
|
|
301
|
+
# Проверяем инициалы: один символ + точка
|
|
302
|
+
if len(remaining) > 2 and remaining[1] == ".":
|
|
303
|
+
return True # Часть последовательности инициалов
|
|
304
|
+
# Иначе это начало нового предложения
|
|
305
|
+
return False
|
|
306
|
+
|
|
307
|
+
# Строчная буква, цифра или пунктуация после аббревиатуры - оставляем соединенными
|
|
308
|
+
return True
|
|
247
309
|
|
|
248
310
|
def is_initials_context(self, text: str, pos: int) -> bool:
|
|
249
|
-
"""Проверяет, находится ли точка в контексте инициалов.
|
|
311
|
+
"""Проверяет, находится ли точка непосредственно в контексте инициалов.
|
|
312
|
+
|
|
313
|
+
Улучшено: проверяем только если инициалы находятся РЯДОМ с границей,
|
|
314
|
+
а не в радиусе 20 символов.
|
|
250
315
|
|
|
251
316
|
Args:
|
|
252
|
-
text:
|
|
253
|
-
pos:
|
|
317
|
+
text: Текст для проверки
|
|
318
|
+
pos: Позиция после точки
|
|
254
319
|
|
|
255
320
|
Returns:
|
|
256
|
-
True
|
|
321
|
+
True если в непосредственном контексте инициалов
|
|
257
322
|
"""
|
|
258
|
-
#
|
|
259
|
-
|
|
260
|
-
|
|
323
|
+
# Проверяем небольшой контекст: 5 символов до и 10 после
|
|
324
|
+
# Это достаточно для "А. С. Пушкин" но не захватывает далекие инициалы
|
|
325
|
+
start = max(0, pos - 5)
|
|
326
|
+
end = min(len(text), pos + 10)
|
|
261
327
|
context = text[start:end]
|
|
262
328
|
|
|
263
|
-
|
|
329
|
+
# Дополнительно: точка должна быть ВНУТРИ найденного паттерна инициалов
|
|
330
|
+
match = self.initials_pattern.search(context)
|
|
331
|
+
if not match:
|
|
332
|
+
return False
|
|
333
|
+
|
|
334
|
+
# Проверяем, что граница (pos) находится внутри найденного паттерна инициалов
|
|
335
|
+
# или сразу после него (с учетом смещения start)
|
|
336
|
+
match_start = start + match.start()
|
|
337
|
+
match_end = start + match.end()
|
|
338
|
+
|
|
339
|
+
# Граница должна быть внутри паттерна или максимум на 2 символа после
|
|
340
|
+
if match_start <= pos <= match_end + 2:
|
|
341
|
+
return True
|
|
342
|
+
|
|
343
|
+
return False
|
|
264
344
|
|
|
265
345
|
def find_sentence_boundaries(self, text: str) -> list[int]:
|
|
266
346
|
"""Находит границы предложений в тексте.
|
|
@@ -287,8 +367,9 @@ class SynTagRusPatterns:
|
|
|
287
367
|
# Check if this is a valid boundary
|
|
288
368
|
is_valid_boundary = False
|
|
289
369
|
|
|
290
|
-
# Case 1: Followed by whitespace and capital letter
|
|
291
|
-
|
|
370
|
+
# Case 1: Followed by whitespace and capital letter (русская ИЛИ латинская)
|
|
371
|
+
# УЛУЧШЕНИЕ: добавлена поддержка латинских заглавных (для XXI, IV, и т.д.)
|
|
372
|
+
if re.match(r"\s+[А-ЯЁA-Z«\"\'(]", remaining):
|
|
292
373
|
is_valid_boundary = True
|
|
293
374
|
|
|
294
375
|
# Case 2: Followed by paragraph break
|
|
@@ -313,18 +394,20 @@ class SynTagRusPatterns:
|
|
|
313
394
|
"""Проверяет, блокируется ли граница высокоприоритетным правилом.
|
|
314
395
|
|
|
315
396
|
Args:
|
|
316
|
-
text:
|
|
317
|
-
pos:
|
|
397
|
+
text: Текст
|
|
398
|
+
pos: Позиция границы (после точки/знака)
|
|
318
399
|
|
|
319
400
|
Returns:
|
|
320
|
-
True
|
|
401
|
+
True если граница блокирована
|
|
321
402
|
"""
|
|
322
|
-
#
|
|
403
|
+
# Проверка на аббревиатуру (точка после аббревиатуры)
|
|
404
|
+
# ВАЖНО: is_abbreviation уже проверяет контекст до И после точки
|
|
323
405
|
if pos > 0 and text[pos - 1] == ".":
|
|
324
|
-
|
|
406
|
+
# Передаем позицию ПОСЛЕ точки (pos), а не позицию точки
|
|
407
|
+
if self.is_abbreviation(text, pos):
|
|
325
408
|
return True
|
|
326
409
|
|
|
327
|
-
#
|
|
410
|
+
# Проверка на инициалы (А. С. Пушкин)
|
|
328
411
|
if self.is_initials_context(text, pos):
|
|
329
412
|
return True
|
|
330
413
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mawo-razdel
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.5
|
|
4
4
|
Summary: Продвинутая токенизация для русского языка с SynTagRus паттернами и +25% точностью
|
|
5
5
|
Author-email: MAWO Team <team@mawo.ru>
|
|
6
6
|
Maintainer-email: MAWO Team <team@mawo.ru>
|
|
@@ -392,20 +392,32 @@ pip install -e ".[dev]"
|
|
|
392
392
|
pytest tests/
|
|
393
393
|
```
|
|
394
394
|
|
|
395
|
-
## Благодарности
|
|
395
|
+
## Благодарности и Upstream-проект
|
|
396
396
|
|
|
397
|
-
|
|
397
|
+
**mawo-razdel** является форком оригинального проекта **[Razdel](https://github.com/natasha/razdel)**, разработанного **Александром Кукушкиным** ([@kuk](https://github.com/kuk)).
|
|
398
398
|
|
|
399
|
-
|
|
400
|
-
- SynTagRus паттерны (+25% качество)
|
|
401
|
-
- 80+ аббревиатур
|
|
402
|
-
- Обработка инициалов
|
|
403
|
-
- Поддержка прямой речи
|
|
404
|
-
- Качественная оценка сегментации
|
|
399
|
+
### Оригинальный проект
|
|
405
400
|
|
|
406
|
-
|
|
401
|
+
- **Репозиторий**: https://github.com/natasha/razdel
|
|
402
|
+
- **Автор**: Alexander Kukushkin
|
|
403
|
+
- **Лицензия**: MIT
|
|
404
|
+
- **Copyright**: (c) 2017 Alexander Kukushkin
|
|
407
405
|
|
|
408
|
-
|
|
406
|
+
### Улучшения MAWO
|
|
407
|
+
|
|
408
|
+
- **SynTagRus паттерны**: +25% качество сегментации
|
|
409
|
+
- **80+ аббревиатур**: Расширенная обработка специальных случаев
|
|
410
|
+
- **Обработка инициалов**: Правильная сегментация имен с инициалами
|
|
411
|
+
- **Поддержка прямой речи**: Корректная обработка диалогов
|
|
412
|
+
- **Качественная оценка**: Метрики для оценки сегментации
|
|
413
|
+
|
|
414
|
+
**Полная информация об авторстве**: см. [ATTRIBUTION.md](ATTRIBUTION.md)
|
|
415
|
+
|
|
416
|
+
## Лицензия
|
|
417
|
+
|
|
418
|
+
MIT License - см. [LICENSE](LICENSE) файл.
|
|
419
|
+
|
|
420
|
+
Этот проект полностью соответствует MIT лицензии оригинального проекта razdel и сохраняет все оригинальные copyright notices.
|
|
409
421
|
|
|
410
422
|
## Ссылки
|
|
411
423
|
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
mawo_razdel/__init__.py,sha256=
|
|
2
|
-
mawo_razdel/
|
|
1
|
+
mawo_razdel/__init__.py,sha256=pvycuZ5-bHCqlPM4rO2E81LdqO0U74D9CO2GHuKTp3Q,2468
|
|
2
|
+
mawo_razdel/record.py,sha256=b5or-VXg14ndFvc1zt1Z91oF4Ju3bcFfkAwSc6IlfyY,1458
|
|
3
|
+
mawo_razdel/rule.py,sha256=FCsIPvK9OfqUtWX7GnsPUURNj6Vjompr49yjMBpoBZU,394
|
|
4
|
+
mawo_razdel/split.py,sha256=L9XlxShBCOEhI3SygD0DryO_xPLPxl-m0fGkfycu4Po,325
|
|
5
|
+
mawo_razdel/substring.py,sha256=8kwNgRvrm7_TNYuTbYBLDcGI1zExHHixD3ATgBYZLA0,440
|
|
6
|
+
mawo_razdel/syntagrus_patterns.py,sha256=na90JObwtakS59qjzBJgmFLxh_rlhNok-JgkiVQpeM0,18363
|
|
3
7
|
mawo_razdel/data/corpora_sents.txt.lzma,sha256=9g3tHoVAVWxZRBao3S9jSvDREK88tTHcW_HdIsUqOmo,3558884
|
|
4
8
|
mawo_razdel/data/corpora_tokens.txt.lzma,sha256=32JAHq7qtQgX2EA88DelBDiAuCG8Q8vNVqCRakrcSXY,3785332
|
|
5
9
|
mawo_razdel/data/gicrya_sents.txt.lzma,sha256=puRJ23GkU554Ed81yn8B7B35Zqjeqa4RKEtIEL56d6I,2189240
|
|
@@ -8,8 +12,8 @@ mawo_razdel/data/rnc_sents.txt.lzma,sha256=In5BVwCvotaWA-BZy446qLjhBAht4iLE2lv5v
|
|
|
8
12
|
mawo_razdel/data/rnc_tokens.txt.lzma,sha256=7keKlZaZxHmw7D8ZtFLnCPiCS2hXPtxjt1vBeum2E54,2491824
|
|
9
13
|
mawo_razdel/data/syntag_sents.txt.lzma,sha256=TrdCYsTWu9lG04cUGPDrEaOh4h-yLgAg3pOpMqsRWSk,2190388
|
|
10
14
|
mawo_razdel/data/syntag_tokens.txt.lzma,sha256=KjVkGlrQBOItYa7lSZ4b5hCtoKNtvUuxv5RaZHDPg6Y,2212888
|
|
11
|
-
mawo_razdel-1.0.
|
|
12
|
-
mawo_razdel-1.0.
|
|
13
|
-
mawo_razdel-1.0.
|
|
14
|
-
mawo_razdel-1.0.
|
|
15
|
-
mawo_razdel-1.0.
|
|
15
|
+
mawo_razdel-1.0.5.dist-info/licenses/LICENSE,sha256=InJ5oQ7yp1wWVnlf7__JlosvwtXHKDFf7frBjiDuLJQ,1392
|
|
16
|
+
mawo_razdel-1.0.5.dist-info/METADATA,sha256=6BrZvyXLAGNbYTHae87icnfOQSyIn5jE2z8AkXDXnK8,14098
|
|
17
|
+
mawo_razdel-1.0.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
18
|
+
mawo_razdel-1.0.5.dist-info/top_level.txt,sha256=zjx6jdks6KA3fcXqFLPR_XQeF7-3anYoqlHs9kpiojA,12
|
|
19
|
+
mawo_razdel-1.0.5.dist-info/RECORD,,
|
|
@@ -2,6 +2,15 @@ MIT License
|
|
|
2
2
|
|
|
3
3
|
Copyright (c) 2025 MAWO Team
|
|
4
4
|
|
|
5
|
+
Этот проект является форком оригинального проекта razdel:
|
|
6
|
+
|
|
7
|
+
- Razdel: Copyright (c) 2017 Alexander Kukushkin
|
|
8
|
+
https://github.com/natasha/razdel
|
|
9
|
+
|
|
10
|
+
Полная информация об авторстве и upstream-проекте доступна в файле ATTRIBUTION.md
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
5
14
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
15
|
of this software and associated documentation files (the "Software"), to deal
|
|
7
16
|
in the Software without restriction, including without limitation the rights
|
|
File without changes
|
|
File without changes
|