matchum 0.1.0__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.
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: matchum
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 규칙 기반 오프라인 한국어 맞춤법 검사 라이브러리
|
|
5
|
+
Author: 서주원 (OMEGA SYSTEMS)
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/sjw0328/matchum
|
|
8
|
+
Project-URL: Issues, https://github.com/sjw0328/matchum/issues
|
|
9
|
+
Keywords: korean,spell-checker,맞춤법,한국어,hanspell,nlp
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Natural Language :: Korean
|
|
13
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
19
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
matchum.py,sha256=Ehx-Fjdd5zMUfzjnmb5ZYLQ2UE0JlrlcHhqQ5LN5bNw,29952
|
|
2
|
+
matchum-0.1.0.dist-info/licenses/LICENSE,sha256=NndS3B606IBWT6fyT16XKSudGuTcpkzcbSaSj06OjMA,1064
|
|
3
|
+
matchum-0.1.0.dist-info/METADATA,sha256=-aYL9tQhtAhOz3kUPGqxJyf81k8SqjKD2zbgnkbbkpU,719
|
|
4
|
+
matchum-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
5
|
+
matchum-0.1.0.dist-info/entry_points.txt,sha256=xHXNUe7NfYmrV5ncaXM1JyOaGEF5jgJe9MK4BQUuOIs,41
|
|
6
|
+
matchum-0.1.0.dist-info/top_level.txt,sha256=6K5pyLRVOwLYSX0bJkxuPmqOt1hhqkUsdjoraTsEAHc,8
|
|
7
|
+
matchum-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sjw0328
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
matchum
|
matchum.py
ADDED
|
@@ -0,0 +1,830 @@
|
|
|
1
|
+
"""matchum (맞춤) — 규칙 기반 오프라인 한국어 맞춤법 검사 라이브러리.
|
|
2
|
+
|
|
3
|
+
>>> from matchum import check
|
|
4
|
+
>>> check("몇일 뒤에 연락할께요").checked
|
|
5
|
+
'며칠 뒤에 연락할게요'
|
|
6
|
+
|
|
7
|
+
MIT License. 있는 그대로(AS IS) 제공되며 어떠한 보증도 하지 않습니다.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
import sys
|
|
16
|
+
import time
|
|
17
|
+
from abc import ABC, abstractmethod
|
|
18
|
+
from collections import OrderedDict
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from enum import Enum
|
|
21
|
+
from types import SimpleNamespace
|
|
22
|
+
from typing import Callable, Dict, Iterable, Iterator, List, Optional, Sequence, Tuple, Union
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ============================================================================
|
|
26
|
+
# 한글 음절 유틸리티
|
|
27
|
+
# ============================================================================
|
|
28
|
+
|
|
29
|
+
CHO = list("ㄱㄲㄴㄷㄸㄹㅁㅂㅃㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎ")
|
|
30
|
+
JUNG = list("ㅏㅐㅑㅒㅓㅔㅕㅖㅗㅘㅙㅚㅛㅜㅝㅞㅟㅠㅡㅢㅣ")
|
|
31
|
+
JONG = [""] + list("ㄱㄲㄳㄴㄵㄶㄷㄹㄺㄻㄼㄽㄾㄿㅀㅁㅂㅄㅅㅆㅇㅈㅊㅋㅌㅍㅎ")
|
|
32
|
+
|
|
33
|
+
_BASE = 0xAC00
|
|
34
|
+
_LAST = 0xD7A3
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def is_syllable(ch: str) -> bool:
|
|
38
|
+
"""완성형 한글 음절 한 글자인지."""
|
|
39
|
+
return len(ch) == 1 and _BASE <= ord(ch) <= _LAST
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def decompose(ch: str) -> Optional[Tuple[str, str, str]]:
|
|
43
|
+
"""음절을 (초성, 중성, 종성)으로 분해. 한글 음절이 아니면 None."""
|
|
44
|
+
if not is_syllable(ch):
|
|
45
|
+
return None
|
|
46
|
+
code = ord(ch) - _BASE
|
|
47
|
+
return CHO[code // 588], JUNG[(code % 588) // 28], JONG[code % 28]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def compose(cho: str, jung: str, jong: str = "") -> str:
|
|
51
|
+
"""(초성, 중성, 종성)을 음절로 조합."""
|
|
52
|
+
return chr(_BASE + CHO.index(cho) * 588 + JUNG.index(jung) * 28 + JONG.index(jong))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def jongseong(ch: str) -> str:
|
|
56
|
+
"""종성(받침). 받침이 없거나 한글이 아니면 빈 문자열."""
|
|
57
|
+
parts = decompose(ch)
|
|
58
|
+
return parts[2] if parts else ""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def has_jongseong(ch: str) -> bool:
|
|
62
|
+
return bool(jongseong(ch))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ============================================================================
|
|
66
|
+
# 결과 자료형
|
|
67
|
+
# ============================================================================
|
|
68
|
+
|
|
69
|
+
class Level(str, Enum):
|
|
70
|
+
ERROR = "error" # 문맥과 무관하게 틀린 표기 — 기본적으로 자동 교정
|
|
71
|
+
WARNING = "warning" # 문맥에 따라 맞을 수도 있는 표기 — 기본적으로 보고만 함
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class Category(str, Enum):
|
|
75
|
+
SPELLING = "spelling" # 맞춤법
|
|
76
|
+
SPACING = "spacing" # 띄어쓰기
|
|
77
|
+
STYLE = "style" # 표준어·문체 권고
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class Correction:
|
|
82
|
+
start: int # 원문 기준 시작 인덱스
|
|
83
|
+
end: int # 원문 기준 끝 인덱스 (exclusive)
|
|
84
|
+
original: str
|
|
85
|
+
suggestion: str
|
|
86
|
+
rule_id: str
|
|
87
|
+
message: str
|
|
88
|
+
level: Level = Level.ERROR
|
|
89
|
+
category: Category = Category.SPELLING
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
93
|
+
class Result:
|
|
94
|
+
original: str
|
|
95
|
+
checked: str
|
|
96
|
+
corrections: List[Correction] = field(default_factory=list)
|
|
97
|
+
time: float = 0.0
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def errors(self) -> int:
|
|
101
|
+
return len(self.corrections)
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def ok(self) -> bool:
|
|
105
|
+
return not self.corrections
|
|
106
|
+
|
|
107
|
+
def as_dict(self) -> Dict:
|
|
108
|
+
return {
|
|
109
|
+
"original": self.original,
|
|
110
|
+
"checked": self.checked,
|
|
111
|
+
"errors": self.errors,
|
|
112
|
+
"time": self.time,
|
|
113
|
+
"corrections": [
|
|
114
|
+
{
|
|
115
|
+
"start": c.start,
|
|
116
|
+
"end": c.end,
|
|
117
|
+
"original": c.original,
|
|
118
|
+
"suggestion": c.suggestion,
|
|
119
|
+
"rule": c.rule_id,
|
|
120
|
+
"message": c.message,
|
|
121
|
+
"level": c.level.value,
|
|
122
|
+
"category": c.category.value,
|
|
123
|
+
}
|
|
124
|
+
for c in self.corrections
|
|
125
|
+
],
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ============================================================================
|
|
130
|
+
# 규칙 베이스
|
|
131
|
+
# ============================================================================
|
|
132
|
+
#
|
|
133
|
+
# 규칙 베이스 클래스.
|
|
134
|
+
#
|
|
135
|
+
# 규칙 하나 = 오류 패턴 하나. 새 규칙은 아래 세 가지 중 하나로 만든다.
|
|
136
|
+
# - WordRule : 항상 틀린 고정 표기 → 고정 교정어 (사전형, 대부분의 규칙)
|
|
137
|
+
# - RegexRule : 정규식 + 교정 함수 (어미·띄어쓰기 등 패턴형)
|
|
138
|
+
# - Rule : 직접 find() 구현 (형태소 분석 등이 필요한 고급 규칙)
|
|
139
|
+
|
|
140
|
+
# 한글 음절 앞에 붙어 있지 않을 것 (단어 시작 경계)
|
|
141
|
+
WORD_START = r"(?<![가-힣])"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class Rule(ABC):
|
|
145
|
+
id: str = ""
|
|
146
|
+
message: str = ""
|
|
147
|
+
level: Level = Level.ERROR
|
|
148
|
+
category: Category = Category.SPELLING
|
|
149
|
+
|
|
150
|
+
@abstractmethod
|
|
151
|
+
def find(self, text: str) -> Iterator[Correction]:
|
|
152
|
+
...
|
|
153
|
+
|
|
154
|
+
def _make(self, start: int, end: int, text: str, suggestion: str,
|
|
155
|
+
message: Optional[str] = None) -> Correction:
|
|
156
|
+
return Correction(
|
|
157
|
+
start=start, end=end, original=text[start:end], suggestion=suggestion,
|
|
158
|
+
rule_id=self.id, message=message or self.message,
|
|
159
|
+
level=self.level, category=self.category,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _covered(text: str, start: int, end: int, word: str) -> bool:
|
|
164
|
+
"""text 안에서 word가 [start, end) 구간을 통째로 덮는 위치에 있는지."""
|
|
165
|
+
for st in range(max(0, end - len(word)), start + 1):
|
|
166
|
+
if text.startswith(word, st):
|
|
167
|
+
return True
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class RegexRule(Rule):
|
|
172
|
+
"""정규식 매치마다 fix(match)로 교정어를 만든다. fix가 None을 반환하면 건너뜀.
|
|
173
|
+
|
|
174
|
+
exceptions: 매치 구간을 덮는 정상 단어 목록 (예: '것' 띄어쓰기 규칙의 '날것', '들것').
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
def __init__(self, id: str, pattern: str, fix: Callable[[re.Match], Optional[str]],
|
|
178
|
+
message: str, level: Level = Level.ERROR,
|
|
179
|
+
category: Category = Category.SPELLING,
|
|
180
|
+
exceptions: Iterable[str] = ()):
|
|
181
|
+
self.id = id
|
|
182
|
+
self.pattern = re.compile(pattern)
|
|
183
|
+
self.fix = fix
|
|
184
|
+
self.message = message
|
|
185
|
+
self.level = level
|
|
186
|
+
self.category = category
|
|
187
|
+
self.exceptions = tuple(exceptions)
|
|
188
|
+
|
|
189
|
+
def find(self, text: str) -> Iterator[Correction]:
|
|
190
|
+
for m in self.pattern.finditer(text):
|
|
191
|
+
if any(_covered(text, m.start(), m.end(), w) for w in self.exceptions):
|
|
192
|
+
continue
|
|
193
|
+
suggestion = self.fix(m)
|
|
194
|
+
if suggestion is not None and suggestion != m.group(0):
|
|
195
|
+
yield self._make(m.start(), m.end(), text, suggestion)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class WordRule(RegexRule):
|
|
199
|
+
"""고정 오표기 → 고정 교정어. word_start=True면 단어 첫머리에서만 잡는다(합성어 오탐 방지)."""
|
|
200
|
+
|
|
201
|
+
def __init__(self, id: str, wrong: str, right: str, message: str = "",
|
|
202
|
+
word_start: bool = False, level: Level = Level.ERROR,
|
|
203
|
+
category: Category = Category.SPELLING,
|
|
204
|
+
exceptions: Iterable[str] = ()):
|
|
205
|
+
prefix = WORD_START if word_start else ""
|
|
206
|
+
self.wrong, self.right = wrong, right
|
|
207
|
+
super().__init__(
|
|
208
|
+
id=id,
|
|
209
|
+
pattern=prefix + re.escape(wrong),
|
|
210
|
+
fix=lambda m, r=right: r,
|
|
211
|
+
message=message or f"'{right}'이(가) 맞는 표기입니다.",
|
|
212
|
+
level=level,
|
|
213
|
+
category=category,
|
|
214
|
+
exceptions=exceptions,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def word_rules(prefix: str, table: Iterable[tuple], **kwargs) -> list:
|
|
219
|
+
"""(오표기, 교정어[, 옵션dict]) 표를 WordRule 목록으로 변환."""
|
|
220
|
+
rules = []
|
|
221
|
+
for i, row in enumerate(table, 1):
|
|
222
|
+
wrong, right, *rest = row
|
|
223
|
+
opts = {**kwargs, **(rest[0] if rest else {})}
|
|
224
|
+
rules.append(WordRule(f"{prefix}.{i:03d}", wrong, right, **opts))
|
|
225
|
+
return rules
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# ============================================================================
|
|
229
|
+
# 사전형 규칙
|
|
230
|
+
# ============================================================================
|
|
231
|
+
#
|
|
232
|
+
# 사전형 규칙: 고정 오표기 → 고정 교정어.
|
|
233
|
+
#
|
|
234
|
+
# 행 하나가 규칙 하나다. 형식: (오표기, 교정어[, 옵션])
|
|
235
|
+
# 옵션 키: word_start(단어 첫머리에서만), exceptions(매치를 덮는 정상 단어), message
|
|
236
|
+
#
|
|
237
|
+
# 기준은 현행 한글 맞춤법·표준어 규정·외래어 표기법. 교정 쌍과 메시지는 직접 작성한다
|
|
238
|
+
# (외부 사전·자료의 해설문이나 예문을 복사하지 않는다).
|
|
239
|
+
#
|
|
240
|
+
# 새 행을 넣을 때 확인할 것
|
|
241
|
+
# 1. 오표기가 다른 정상 단어의 일부로 나오지 않는가 → 나오면 word_start / exceptions
|
|
242
|
+
# 2. 문맥에 따라 맞을 수도 있는가 → 그렇다면 _CONTEXT 표(WARNING)에 넣는다
|
|
243
|
+
# 3. tests/ 에 교정 케이스와 오탐 방지 케이스를 추가한다
|
|
244
|
+
|
|
245
|
+
WS = {"word_start": True}
|
|
246
|
+
|
|
247
|
+
# ---------------------------------------------------------------------------
|
|
248
|
+
# 되/돼, 활용형
|
|
249
|
+
# ---------------------------------------------------------------------------
|
|
250
|
+
_DOE = [
|
|
251
|
+
("됬", "됐"),
|
|
252
|
+
("됀", "된"),
|
|
253
|
+
("됄", "될"),
|
|
254
|
+
("되요", "돼요"),
|
|
255
|
+
("되서", "돼서"),
|
|
256
|
+
("되야", "돼야"),
|
|
257
|
+
("돼었", "되었"),
|
|
258
|
+
("돼고", "되고"),
|
|
259
|
+
("돼는", "되는"),
|
|
260
|
+
("돼면", "되면"),
|
|
261
|
+
("돼니", "되니"),
|
|
262
|
+
("않되", "안 되"),
|
|
263
|
+
("뵈요", "봬요"),
|
|
264
|
+
("뵜", "뵀"),
|
|
265
|
+
("사겼", "사귀었"),
|
|
266
|
+
("바꼈", "바뀌었"),
|
|
267
|
+
("헤매이", "헤매"),
|
|
268
|
+
("되뇌이", "되뇌"),
|
|
269
|
+
("설레이", "설레"),
|
|
270
|
+
("끼여들", "끼어들"),
|
|
271
|
+
("알맞는", "알맞은"),
|
|
272
|
+
("걸맞는", "걸맞은"),
|
|
273
|
+
("삼가해", "삼가"),
|
|
274
|
+
("삼가하", "삼가"),
|
|
275
|
+
("꺽어", "꺾어"),
|
|
276
|
+
("꺽었", "꺾었"),
|
|
277
|
+
("꺽인", "꺾인"),
|
|
278
|
+
("꺽고", "꺾고"),
|
|
279
|
+
("들어났", "드러났"),
|
|
280
|
+
("무릎쓰", "무릅쓰"),
|
|
281
|
+
("쑥쓰럽", "쑥스럽"),
|
|
282
|
+
("애띤", "앳된"),
|
|
283
|
+
("널부러", "널브러"),
|
|
284
|
+
("희안하", "희한하"),
|
|
285
|
+
("희안한", "희한한"),
|
|
286
|
+
("어의없", "어이없"),
|
|
287
|
+
("어의가 없", "어이가 없"),
|
|
288
|
+
("어줍잖", "어쭙잖"),
|
|
289
|
+
("닥달", "닦달"),
|
|
290
|
+
("핼쓱", "핼쑥"),
|
|
291
|
+
("아니예요", "아니에요"),
|
|
292
|
+
("아니였", "아니었"),
|
|
293
|
+
("십시요", "십시오"),
|
|
294
|
+
("슴니다", "습니다"),
|
|
295
|
+
("읍니다", "습니다"),
|
|
296
|
+
("거에요", "거예요"),
|
|
297
|
+
("뭐에요", "뭐예요"),
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
# ---------------------------------------------------------------------------
|
|
301
|
+
# 부사 -이/-히, 기타 부사
|
|
302
|
+
# ---------------------------------------------------------------------------
|
|
303
|
+
_ADVERB = [
|
|
304
|
+
("일일히", "일일이"),
|
|
305
|
+
("깨끗히", "깨끗이"),
|
|
306
|
+
("곰곰히", "곰곰이"),
|
|
307
|
+
("틈틈히", "틈틈이"),
|
|
308
|
+
("번번히", "번번이"),
|
|
309
|
+
("꼼꼼이", "꼼꼼히"),
|
|
310
|
+
("솔직이", "솔직히"),
|
|
311
|
+
("가만이", "가만히"),
|
|
312
|
+
("일찌기", "일찍이"),
|
|
313
|
+
("더우기", "더욱이"),
|
|
314
|
+
("생각컨데", "생각건대"),
|
|
315
|
+
("요컨데", "요컨대"),
|
|
316
|
+
("바라건데", "바라건대"),
|
|
317
|
+
("비로서", "비로소"),
|
|
318
|
+
("금새", "금세", {"word_start": True, "exceptions": ["금새우"]}),
|
|
319
|
+
("괜시리", "괜스레"),
|
|
320
|
+
("아뭏든", "아무튼"),
|
|
321
|
+
("하옇든", "하여튼"),
|
|
322
|
+
("어쨋든", "어쨌든"),
|
|
323
|
+
("어떻든간에", "어떻든 간에"),
|
|
324
|
+
("오랫만", "오랜만"),
|
|
325
|
+
("오랜동안", "오랫동안"),
|
|
326
|
+
("엇그제", "엊그제"),
|
|
327
|
+
("웬지", "왠지"),
|
|
328
|
+
("왠일", "웬일"),
|
|
329
|
+
("왠만", "웬만"),
|
|
330
|
+
("어떻해", "어떡해"),
|
|
331
|
+
("어떻하", "어떡하"),
|
|
332
|
+
("어떻케", "어떻게"),
|
|
333
|
+
("어떡게", "어떻게"),
|
|
334
|
+
("몇일", "며칠"),
|
|
335
|
+
("몇 일", "며칠"),
|
|
336
|
+
("어물쩡", "어물쩍"),
|
|
337
|
+
]
|
|
338
|
+
|
|
339
|
+
# ---------------------------------------------------------------------------
|
|
340
|
+
# 명사·어휘
|
|
341
|
+
# ---------------------------------------------------------------------------
|
|
342
|
+
_NOUN = [
|
|
343
|
+
("역활", "역할"),
|
|
344
|
+
("설겆이", "설거지"),
|
|
345
|
+
("내노라", "내로라"),
|
|
346
|
+
("안밖", "안팎", WS),
|
|
347
|
+
("우뢰", "우레", WS),
|
|
348
|
+
("짜집기", "짜깁기"),
|
|
349
|
+
("짜투리", "자투리"),
|
|
350
|
+
("뒤치닥거리", "뒤치다꺼리"),
|
|
351
|
+
("귀뜸", "귀띔"),
|
|
352
|
+
("눈쌀", "눈살"),
|
|
353
|
+
("오지랍", "오지랖"),
|
|
354
|
+
("쑥맥", "숙맥"),
|
|
355
|
+
("쉽상", "십상"),
|
|
356
|
+
("폭팔", "폭발"),
|
|
357
|
+
("뇌졸증", "뇌졸중"),
|
|
358
|
+
("구렛나루", "구레나룻"),
|
|
359
|
+
("귓볼", "귓불"),
|
|
360
|
+
("굼뱅이", "굼벵이"),
|
|
361
|
+
("개거품", "게거품"),
|
|
362
|
+
("제작년", "재작년"),
|
|
363
|
+
("육계장", "육개장"),
|
|
364
|
+
("김치찌게", "김치찌개"),
|
|
365
|
+
("된장찌게", "된장찌개"),
|
|
366
|
+
("부대찌게", "부대찌개"),
|
|
367
|
+
("순두부찌게", "순두부찌개"),
|
|
368
|
+
("떡볶기", "떡볶이"),
|
|
369
|
+
("깍뚜기", "깍두기"),
|
|
370
|
+
("곱배기", "곱빼기"),
|
|
371
|
+
("쭈꾸미", "주꾸미"),
|
|
372
|
+
("어리버리", "어리바리"),
|
|
373
|
+
# 사이시옷
|
|
374
|
+
("윗층", "위층"),
|
|
375
|
+
("윗쪽", "위쪽"),
|
|
376
|
+
("뒷쪽", "뒤쪽"),
|
|
377
|
+
("뒷풀이", "뒤풀이"),
|
|
378
|
+
("뒷처리", "뒤처리"),
|
|
379
|
+
("뒷편", "뒤편"),
|
|
380
|
+
("윗어른", "웃어른"),
|
|
381
|
+
("햇님", "해님"),
|
|
382
|
+
("갯수", "개수"),
|
|
383
|
+
("촛점", "초점"),
|
|
384
|
+
("홧병", "화병"),
|
|
385
|
+
("싯가", "시가"),
|
|
386
|
+
("머릿말", "머리말"),
|
|
387
|
+
("인삿말", "인사말"),
|
|
388
|
+
# 암수
|
|
389
|
+
("숫놈", "수놈"),
|
|
390
|
+
("숫닭", "수탉"),
|
|
391
|
+
("숫캐", "수캐"),
|
|
392
|
+
("숫소", "수소"),
|
|
393
|
+
# 한자 성어
|
|
394
|
+
("희노애락", "희로애락"),
|
|
395
|
+
("홀홀단신", "혈혈단신"),
|
|
396
|
+
("산수갑산", "삼수갑산"),
|
|
397
|
+
("절대절명", "절체절명"),
|
|
398
|
+
("동거동락", "동고동락"),
|
|
399
|
+
("야밤도주", "야반도주"),
|
|
400
|
+
("토사광란", "토사곽란"),
|
|
401
|
+
("유도심문", "유도신문"),
|
|
402
|
+
("염치불구", "염치 불고"),
|
|
403
|
+
("일사분란", "일사불란"),
|
|
404
|
+
("포복졸도", "포복절도"),
|
|
405
|
+
("성대묘사", "성대모사"),
|
|
406
|
+
("환골탈퇴", "환골탈태"),
|
|
407
|
+
("고정관렴", "고정관념"),
|
|
408
|
+
# -율/-률 (모음·ㄴ 뒤 '율', 그 밖 '률')
|
|
409
|
+
("확율", "확률"),
|
|
410
|
+
("비률", "비율"),
|
|
411
|
+
("합격율", "합격률"),
|
|
412
|
+
("성공율", "성공률"),
|
|
413
|
+
("출석율", "출석률"),
|
|
414
|
+
("경쟁율", "경쟁률"),
|
|
415
|
+
("불량율", "불량률"),
|
|
416
|
+
("시청율", "시청률"),
|
|
417
|
+
("점유률", "점유율"),
|
|
418
|
+
("이자률", "이자율"),
|
|
419
|
+
("할인률", "할인율"),
|
|
420
|
+
("백분률", "백분율"),
|
|
421
|
+
("에매", "애매", WS),
|
|
422
|
+
]
|
|
423
|
+
|
|
424
|
+
# ---------------------------------------------------------------------------
|
|
425
|
+
# 외래어 표기 (현장 표기가 굳은 경우가 많아 WARNING)
|
|
426
|
+
# ---------------------------------------------------------------------------
|
|
427
|
+
_LOANWORD = [
|
|
428
|
+
("메세지", "메시지"),
|
|
429
|
+
("악세사리", "액세서리"),
|
|
430
|
+
("악세서리", "액세서리"),
|
|
431
|
+
("컨텐츠", "콘텐츠"),
|
|
432
|
+
("쥬스", "주스"),
|
|
433
|
+
("쵸콜릿", "초콜릿"),
|
|
434
|
+
("초콜렛", "초콜릿"),
|
|
435
|
+
("케익", "케이크"),
|
|
436
|
+
("까페", "카페"),
|
|
437
|
+
("화이팅", "파이팅"),
|
|
438
|
+
("리더쉽", "리더십"),
|
|
439
|
+
("멤버쉽", "멤버십"),
|
|
440
|
+
("파트너쉽", "파트너십"),
|
|
441
|
+
("인턴쉽", "인턴십"),
|
|
442
|
+
("어플리케이션", "애플리케이션"),
|
|
443
|
+
("바베큐", "바비큐"),
|
|
444
|
+
("쇼파", "소파"),
|
|
445
|
+
("비지니스", "비즈니스"),
|
|
446
|
+
("데이타", "데이터"),
|
|
447
|
+
("렌트카", "렌터카"),
|
|
448
|
+
("런닝", "러닝"),
|
|
449
|
+
("플랭카드", "플래카드"),
|
|
450
|
+
("워크샵", "워크숍"),
|
|
451
|
+
("앵콜", "앙코르"),
|
|
452
|
+
("심볼", "심벌"),
|
|
453
|
+
("로보트", "로봇"),
|
|
454
|
+
("타겟", "타깃"),
|
|
455
|
+
("팜플렛", "팸플릿"),
|
|
456
|
+
("자켓", "재킷"),
|
|
457
|
+
("쟈켓", "재킷"),
|
|
458
|
+
("알콜", "알코올"),
|
|
459
|
+
("스케쥴", "스케줄"),
|
|
460
|
+
("썬크림", "선크림"),
|
|
461
|
+
("써비스", "서비스"),
|
|
462
|
+
("라이센스", "라이선스"),
|
|
463
|
+
("엔돌핀", "엔도르핀"),
|
|
464
|
+
("메뉴얼", "매뉴얼"),
|
|
465
|
+
("메니저", "매니저"),
|
|
466
|
+
("매니아", "마니아"),
|
|
467
|
+
("플래쉬", "플래시"),
|
|
468
|
+
("프리젠테이션", "프레젠테이션"),
|
|
469
|
+
("디렉토리", "디렉터리"),
|
|
470
|
+
("컨셉", "콘셉트"),
|
|
471
|
+
("져지", "저지"),
|
|
472
|
+
("쉐이크", "셰이크"),
|
|
473
|
+
("쉐프", "셰프"),
|
|
474
|
+
("레포트", "리포트", WS),
|
|
475
|
+
("카라멜", "캐러멜"),
|
|
476
|
+
("샌달", "샌들"),
|
|
477
|
+
("부페", "뷔페"),
|
|
478
|
+
("돈까스", "돈가스"),
|
|
479
|
+
("나레이션", "내레이션"),
|
|
480
|
+
("뱃지", "배지"),
|
|
481
|
+
("앙케이트", "앙케트"),
|
|
482
|
+
("밧데리", "배터리"),
|
|
483
|
+
("밧테리", "배터리"),
|
|
484
|
+
]
|
|
485
|
+
|
|
486
|
+
# ---------------------------------------------------------------------------
|
|
487
|
+
# 문맥 의존 (대부분 오용이지만 정상 용법이 있음 → WARNING, 자동 교정 안 함)
|
|
488
|
+
# ---------------------------------------------------------------------------
|
|
489
|
+
_CONTEXT = [
|
|
490
|
+
("바램", "바람", {"word_start": True,
|
|
491
|
+
"message": "'소망'의 뜻이라면 '바람'이 맞습니다. ('색이 바래다'의 활용이면 정상)"}),
|
|
492
|
+
("설레임", "설렘", {"message": "명사형은 '설렘'이 맞습니다. (상품명이면 무시)"}),
|
|
493
|
+
("문안하게", "무난하게", {"message": "'무난하다'의 뜻이라면 '무난하게'가 맞습니다."}),
|
|
494
|
+
("구지", "굳이", {"word_start": True,
|
|
495
|
+
"message": "'고집스럽게'의 뜻이라면 '굳이'가 맞습니다. (지명·인명이면 무시)"}),
|
|
496
|
+
("일부로", "일부러", {"word_start": True,
|
|
497
|
+
"message": "'고의로'의 뜻이라면 '일부러'가 맞습니다. ('일부분으로'의 뜻이면 정상)"}),
|
|
498
|
+
("잊혀지", "잊히", {"message": "이중 피동입니다. '잊히다'를 권장합니다."}),
|
|
499
|
+
("되어지", "되", {"message": "이중 피동입니다. '되다'를 권장합니다."}),
|
|
500
|
+
("보여지", "보이", {"message": "이중 피동입니다. '보이다'를 권장합니다."}),
|
|
501
|
+
]
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
WORD_RULES = (
|
|
505
|
+
word_rules("doe", _DOE)
|
|
506
|
+
+ word_rules("adv", _ADVERB)
|
|
507
|
+
+ word_rules("noun", _NOUN)
|
|
508
|
+
+ word_rules("loan", _LOANWORD, level=Level.WARNING, category=Category.STYLE)
|
|
509
|
+
+ word_rules("ctx", _CONTEXT, level=Level.WARNING)
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
# ============================================================================
|
|
514
|
+
# 어미 규칙
|
|
515
|
+
# ============================================================================
|
|
516
|
+
|
|
517
|
+
# 어절 끝 또는 '요'·문장부호가 뒤따를 때
|
|
518
|
+
_END = r"(?=요|[\s.,!?~\"')\]]|$)"
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def _if_rieul(repl):
|
|
522
|
+
"""앞 음절 받침이 ㄹ일 때만 교정하는 fix 생성기."""
|
|
523
|
+
return lambda m: repl(m) if jongseong(m.group(1)) == "ㄹ" else None
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
ENDING_RULES = [
|
|
527
|
+
# -ㄹ께 → -ㄹ게 (할께요, 갈께) '함께'는 받침 ㅁ이라 제외
|
|
528
|
+
RegexRule("ending.001", r"([가-힣])께" + _END,
|
|
529
|
+
_if_rieul(lambda m: m.group(1) + "게"),
|
|
530
|
+
"'-ㄹ게'로 적습니다. (소리는 [께]로 나도 '-ㄹ게')"),
|
|
531
|
+
|
|
532
|
+
# -ㄹ꺼(에요) → -ㄹ 거(예요)
|
|
533
|
+
RegexRule("ending.002", r"([가-힣])꺼(에요)?(?=[야예요니면지라]|[\s.,!?~]|$)",
|
|
534
|
+
_if_rieul(lambda m: m.group(1) + " 거" + ("예요" if m.group(2) else "")),
|
|
535
|
+
"의존명사 '거'로 적고 띄어 씁니다. (할 거야, 할 거예요)"),
|
|
536
|
+
|
|
537
|
+
# -ㄹ껄 → -ㄹ걸
|
|
538
|
+
RegexRule("ending.003", r"([가-힣])껄" + _END,
|
|
539
|
+
_if_rieul(lambda m: m.group(1) + "걸"),
|
|
540
|
+
"'-ㄹ걸'로 적습니다."),
|
|
541
|
+
|
|
542
|
+
# -ㄹ런지 → -ㄹ는지
|
|
543
|
+
RegexRule("ending.004", r"([가-힣])런지",
|
|
544
|
+
_if_rieul(lambda m: m.group(1) + "는지"),
|
|
545
|
+
"'-ㄹ는지'가 맞습니다."),
|
|
546
|
+
|
|
547
|
+
# -ㄹ려고/-ㄹ려면 (하다·보다 계열의 잘못된 ㄹ 첨가)
|
|
548
|
+
RegexRule("ending.005", r"(할|볼|될)려(고|면|는|던)",
|
|
549
|
+
lambda m: {"할": "하", "볼": "보", "될": "되"}[m.group(1)]
|
|
550
|
+
+ "려" + m.group(2),
|
|
551
|
+
"'-려고'·'-려면' 앞에 ㄹ을 덧붙이지 않습니다. (하려고, 보려면)"),
|
|
552
|
+
|
|
553
|
+
# -을려고 → -으려고 (먹을려고, 읽을려면)
|
|
554
|
+
RegexRule("ending.006", r"을려(고|면|는|던)",
|
|
555
|
+
lambda m: "으려" + m.group(1),
|
|
556
|
+
"'-으려고'가 맞습니다."),
|
|
557
|
+
|
|
558
|
+
# 있슴/없슴/했슴 → 있음/없음/했음 (뒤에 '니다'가 오면 습니다 규칙이 처리)
|
|
559
|
+
RegexRule("ending.007", r"(있|없|했|겠|었|았|였)슴(?!니)",
|
|
560
|
+
lambda m: m.group(1) + "음",
|
|
561
|
+
"명사형 어미는 '-음'입니다. (있음, 했음)"),
|
|
562
|
+
|
|
563
|
+
# 단독으로 쓰인 '않' + 공백 + 용언 → '안' (않 된다, 않 해)
|
|
564
|
+
RegexRule("ending.008", r"(?<![가-힣])않(?= [가-힣])",
|
|
565
|
+
lambda m: "안",
|
|
566
|
+
"용언 앞에서 부정 부사는 '안'입니다. ('않'은 '-지 않다'에만 씁니다)"),
|
|
567
|
+
|
|
568
|
+
# -지 안았다/안습니다 → -지 않았다
|
|
569
|
+
RegexRule("ending.009", r"지 안(았|습|는다|아요|아서|으면|는)",
|
|
570
|
+
lambda m: "지 않" + m.group(1),
|
|
571
|
+
"'-지 않다'로 적습니다."),
|
|
572
|
+
|
|
573
|
+
# 단독 '왠' + 공백 → '웬' ('왠'은 '왠지'에만 씁니다)
|
|
574
|
+
RegexRule("ending.010", r"(?<![가-힣])왠(?= [가-힣])",
|
|
575
|
+
lambda m: "웬",
|
|
576
|
+
"'어찌 된'의 뜻이면 '웬'입니다. ('왠'은 '왠지'에만 씁니다)"),
|
|
577
|
+
|
|
578
|
+
# 댓가 → 대가 (댓가지는 정상)
|
|
579
|
+
RegexRule("ending.011", r"댓가(?!지)",
|
|
580
|
+
lambda m: "대가",
|
|
581
|
+
"'대가(代價)'가 맞습니다."),
|
|
582
|
+
|
|
583
|
+
]
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
# ============================================================================
|
|
587
|
+
# 띄어쓰기 규칙
|
|
588
|
+
# ============================================================================
|
|
589
|
+
|
|
590
|
+
def _dep(rule_id, noun, after, exceptions, message):
|
|
591
|
+
"""-ㄹ + 의존명사(noun) [+ after] 붙여쓰기 → 띄어쓰기."""
|
|
592
|
+
def fix(m):
|
|
593
|
+
if jongseong(m.group(1)) != "ㄹ":
|
|
594
|
+
return None
|
|
595
|
+
return f"{m.group(1)} {noun}{m.group(2)}"
|
|
596
|
+
return RegexRule(rule_id, rf"([가-힣]){noun}({after})", fix, message,
|
|
597
|
+
category=Category.SPACING, exceptions=exceptions)
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def _fix_su(m):
|
|
601
|
+
pre, sp1, sp2, verb = m.group(1), m.group(2), m.group(3), m.group(4)
|
|
602
|
+
if jongseong(pre) != "ㄹ" or (sp1 and sp2):
|
|
603
|
+
return None
|
|
604
|
+
return f"{pre} 수 {verb}"
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
SPACING_RULES = [
|
|
608
|
+
# 할 수 있다/없다 (앞뒤 띄어쓰기 모두 점검)
|
|
609
|
+
RegexRule("spacing.001", r"([가-힣])( ?)수( ?)(있|없)", _fix_su,
|
|
610
|
+
"의존명사 '수'는 띄어 씁니다. (할 수 있다)",
|
|
611
|
+
category=Category.SPACING),
|
|
612
|
+
|
|
613
|
+
_dep("spacing.002", "것", "", ["날것", "물것", "들것", "별것", "탈것", "날것들"],
|
|
614
|
+
"의존명사 '것'은 띄어 씁니다. (할 것)"),
|
|
615
|
+
|
|
616
|
+
_dep("spacing.003", "때", "(?!까치)", ["물때", "썰물때", "밀물때", "끝물때"],
|
|
617
|
+
"'때'는 앞말과 띄어 씁니다. (갈 때)"),
|
|
618
|
+
|
|
619
|
+
_dep("spacing.004", "줄", "(?= ?(?:알|몰|모))", ["글줄", "날줄", "물줄", "밧줄"],
|
|
620
|
+
"의존명사 '줄'은 띄어 씁니다. (할 줄 안다)"),
|
|
621
|
+
|
|
622
|
+
]
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
# ============================================================================
|
|
626
|
+
# 검사 엔진
|
|
627
|
+
# ============================================================================
|
|
628
|
+
|
|
629
|
+
DEFAULT_RULES = [*WORD_RULES, *ENDING_RULES, *SPACING_RULES]
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
class Checker:
|
|
633
|
+
"""
|
|
634
|
+
>>> Checker().check("오랫만에 만나서 반가웠어요. 내일 연락할께요.").checked
|
|
635
|
+
'오랜만에 만나서 반가웠어요. 내일 연락할게요.'
|
|
636
|
+
"""
|
|
637
|
+
|
|
638
|
+
def __init__(self, rules: Optional[Sequence[Rule]] = None,
|
|
639
|
+
ignore: Iterable[str] = (),
|
|
640
|
+
disabled: Iterable[str] = (),
|
|
641
|
+
categories: Optional[Iterable[Category]] = None):
|
|
642
|
+
self.rules: List[Rule] = list(DEFAULT_RULES if rules is None else rules)
|
|
643
|
+
self.ignore = set(ignore) # 사용자 사전: 교정하지 않을 원문 표기
|
|
644
|
+
self.disabled = set(disabled) # 끌 규칙 ID
|
|
645
|
+
self.categories = set(categories) if categories else None
|
|
646
|
+
|
|
647
|
+
# --- 확장 API -------------------------------------------------------
|
|
648
|
+
def add_rule(self, rule: Rule) -> "Checker":
|
|
649
|
+
self.rules.append(rule)
|
|
650
|
+
return self
|
|
651
|
+
|
|
652
|
+
# --- 검사 ---------------------------------------------------------
|
|
653
|
+
def find(self, text: str) -> List[Correction]:
|
|
654
|
+
"""겹침이 해소된 교정 목록 (원문 위치 순)."""
|
|
655
|
+
found: List[Correction] = []
|
|
656
|
+
for rule in self.rules:
|
|
657
|
+
if rule.id in self.disabled:
|
|
658
|
+
continue
|
|
659
|
+
if self.categories and rule.category not in self.categories:
|
|
660
|
+
continue
|
|
661
|
+
for c in rule.find(text):
|
|
662
|
+
if c.original in self.ignore:
|
|
663
|
+
continue
|
|
664
|
+
found.append(c)
|
|
665
|
+
return _resolve_overlaps(found)
|
|
666
|
+
|
|
667
|
+
def check(self, text: str, apply_warnings: bool = False) -> Result:
|
|
668
|
+
t0 = time.perf_counter()
|
|
669
|
+
corrections = self.find(text)
|
|
670
|
+
to_apply = [c for c in corrections
|
|
671
|
+
if apply_warnings or c.level is Level.ERROR]
|
|
672
|
+
checked = _apply(text, to_apply)
|
|
673
|
+
return Result(original=text, checked=checked, corrections=corrections,
|
|
674
|
+
time=time.perf_counter() - t0)
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def _resolve_overlaps(cs: List[Correction]) -> List[Correction]:
|
|
678
|
+
# 시작 위치 순, 같으면 더 긴 매치·ERROR 우선. 앞에서 채택된 범위와 겹치면 버림.
|
|
679
|
+
cs.sort(key=lambda c: (c.start, -(c.end - c.start), c.level is not Level.ERROR))
|
|
680
|
+
out: List[Correction] = []
|
|
681
|
+
last_end = -1
|
|
682
|
+
for c in cs:
|
|
683
|
+
if c.start >= last_end:
|
|
684
|
+
out.append(c)
|
|
685
|
+
last_end = c.end
|
|
686
|
+
return out
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def _apply(text: str, cs: List[Correction]) -> str:
|
|
690
|
+
parts, pos = [], 0
|
|
691
|
+
for c in cs: # 이미 위치 순·비겹침
|
|
692
|
+
parts.append(text[pos:c.start])
|
|
693
|
+
parts.append(c.suggestion)
|
|
694
|
+
pos = c.end
|
|
695
|
+
parts.append(text[pos:])
|
|
696
|
+
return "".join(parts)
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
_default: Optional[Checker] = None
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def check(text: str, apply_warnings: bool = False) -> Result:
|
|
703
|
+
"""기본 규칙으로 검사하는 단축 함수."""
|
|
704
|
+
global _default
|
|
705
|
+
if _default is None:
|
|
706
|
+
_default = Checker()
|
|
707
|
+
return _default.check(text, apply_warnings=apply_warnings)
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
# ============================================================================
|
|
711
|
+
# py-hanspell 호환 계층
|
|
712
|
+
# ============================================================================
|
|
713
|
+
#
|
|
714
|
+
# py-hanspell 호환 계층.
|
|
715
|
+
#
|
|
716
|
+
# 기존 코드의 import 한 줄만 바꾸면 네이버 의존 없이 동작하도록 하는 것이 목적.
|
|
717
|
+
#
|
|
718
|
+
# # from hanspell import spell_checker
|
|
719
|
+
# from matchum.compat.hanspell import spell_checker
|
|
720
|
+
#
|
|
721
|
+
# result = spell_checker.check("안녕 하세요. 몇일 뒤에 봐요.")
|
|
722
|
+
# result.checked
|
|
723
|
+
# result.as_dict()
|
|
724
|
+
|
|
725
|
+
class CheckResult:
|
|
726
|
+
PASSED = 0
|
|
727
|
+
WRONG_SPELLING = 1
|
|
728
|
+
WRONG_SPACING = 2
|
|
729
|
+
AMBIGUOUS = 3
|
|
730
|
+
STATISTICAL_CORRECTION = 4
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
class Checked:
|
|
734
|
+
def __init__(self, result=False, original="", checked="", errors=0, words=None, time=0.0):
|
|
735
|
+
self.result = result
|
|
736
|
+
self.original = original
|
|
737
|
+
self.checked = checked
|
|
738
|
+
self.errors = errors
|
|
739
|
+
self.words = words or OrderedDict()
|
|
740
|
+
self.time = time
|
|
741
|
+
|
|
742
|
+
def as_dict(self):
|
|
743
|
+
return {
|
|
744
|
+
"result": self.result, "original": self.original, "checked": self.checked,
|
|
745
|
+
"errors": self.errors, "words": self.words, "time": self.time,
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
def only_checked(self):
|
|
749
|
+
return self.checked
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def _word_status(r) -> "OrderedDict[str, int]":
|
|
753
|
+
# 교정문 어절별 상태 (py-hanspell 형식 근사)
|
|
754
|
+
changed_cat = {}
|
|
755
|
+
for c in r.corrections:
|
|
756
|
+
changed_cat[c.suggestion.strip()] = (
|
|
757
|
+
CheckResult.WRONG_SPACING if c.category is Category.SPACING
|
|
758
|
+
else CheckResult.WRONG_SPELLING
|
|
759
|
+
)
|
|
760
|
+
words = OrderedDict()
|
|
761
|
+
for w in r.checked.split():
|
|
762
|
+
status = CheckResult.PASSED
|
|
763
|
+
for frag, cat in changed_cat.items():
|
|
764
|
+
if frag and frag.split()[0] in w:
|
|
765
|
+
status = cat
|
|
766
|
+
break
|
|
767
|
+
words[w] = status
|
|
768
|
+
return words
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
def _hanspell_check(sentences: Union[str, List[str]]):
|
|
772
|
+
if isinstance(sentences, list):
|
|
773
|
+
return [_hanspell_check(s) for s in sentences]
|
|
774
|
+
r = check(sentences)
|
|
775
|
+
return Checked(result=True, original=r.original, checked=r.checked,
|
|
776
|
+
errors=r.errors, words=_word_status(r), time=r.time)
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
spell_checker = SimpleNamespace(check=_hanspell_check)
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
# ============================================================================
|
|
783
|
+
# 명령줄 도구
|
|
784
|
+
# ============================================================================
|
|
785
|
+
#
|
|
786
|
+
# 명령줄 도구.
|
|
787
|
+
#
|
|
788
|
+
# matchum "몇일 뒤에 연락할께요"
|
|
789
|
+
# matchum -f 원고.txt --json
|
|
790
|
+
# echo "할수있다" | matchum
|
|
791
|
+
|
|
792
|
+
def main(argv=None) -> int:
|
|
793
|
+
p = argparse.ArgumentParser(prog="matchum", description="오프라인 한국어 맞춤법 검사")
|
|
794
|
+
p.add_argument("text", nargs="?", help="검사할 문장 (생략 시 표준입력)")
|
|
795
|
+
p.add_argument("-f", "--file", help="검사할 텍스트 파일 (UTF-8)")
|
|
796
|
+
p.add_argument("--json", action="store_true", help="JSON으로 출력")
|
|
797
|
+
p.add_argument("--apply-warnings", action="store_true", help="WARNING 수준도 교정문에 반영")
|
|
798
|
+
args = p.parse_args(argv)
|
|
799
|
+
|
|
800
|
+
if args.file:
|
|
801
|
+
with open(args.file, encoding="utf-8") as fh:
|
|
802
|
+
text = fh.read()
|
|
803
|
+
elif args.text is not None:
|
|
804
|
+
text = args.text
|
|
805
|
+
else:
|
|
806
|
+
text = sys.stdin.read()
|
|
807
|
+
|
|
808
|
+
r = Checker().check(text, apply_warnings=args.apply_warnings)
|
|
809
|
+
|
|
810
|
+
if args.json:
|
|
811
|
+
print(json.dumps(r.as_dict(), ensure_ascii=False, indent=2))
|
|
812
|
+
else:
|
|
813
|
+
print(r.checked)
|
|
814
|
+
for c in r.corrections:
|
|
815
|
+
mark = "!" if c.level.value == "error" else "?"
|
|
816
|
+
print(f" {mark} {c.original} → {c.suggestion} ({c.message}) [{c.rule_id}]",
|
|
817
|
+
file=sys.stderr)
|
|
818
|
+
return 1 if r.corrections else 0
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
__version__ = "0.1.0"
|
|
823
|
+
__all__ = ["Checker", "check", "Rule", "RegexRule", "WordRule", "word_rules",
|
|
824
|
+
"Category", "Correction", "Level", "Result", "spell_checker", "CheckResult",
|
|
825
|
+
"DEFAULT_RULES"]
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
if __name__ == "__main__":
|
|
829
|
+
raise SystemExit(main())
|
|
830
|
+
|