hangeul 0.1.0__tar.gz
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.
- hangeul-0.1.0/PKG-INFO +95 -0
- hangeul-0.1.0/README.md +87 -0
- hangeul-0.1.0/pyproject.toml +9 -0
- hangeul-0.1.0/setup.cfg +4 -0
- hangeul-0.1.0/src/hangeul/__init__.py +68 -0
- hangeul-0.1.0/src/hangeul/check.py +53 -0
- hangeul-0.1.0/src/hangeul/constants.py +62 -0
- hangeul-0.1.0/src/hangeul/core.py +160 -0
- hangeul-0.1.0/src/hangeul/josa.py +103 -0
- hangeul-0.1.0/src/hangeul/number.py +54 -0
- hangeul-0.1.0/src/hangeul/search.py +45 -0
- hangeul-0.1.0/src/hangeul/similarity.py +42 -0
- hangeul-0.1.0/src/hangeul.egg-info/PKG-INFO +95 -0
- hangeul-0.1.0/src/hangeul.egg-info/SOURCES.txt +21 -0
- hangeul-0.1.0/src/hangeul.egg-info/dependency_links.txt +1 -0
- hangeul-0.1.0/src/hangeul.egg-info/requires.txt +1 -0
- hangeul-0.1.0/src/hangeul.egg-info/top_level.txt +1 -0
- hangeul-0.1.0/tests/test_check.py +56 -0
- hangeul-0.1.0/tests/test_core.py +31 -0
- hangeul-0.1.0/tests/test_josa.py +34 -0
- hangeul-0.1.0/tests/test_num2han.py +22 -0
- hangeul-0.1.0/tests/test_search.py +18 -0
- hangeul-0.1.0/tests/test_similarity.py +10 -0
hangeul-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hangeul
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: pytest>=9.0.3
|
|
8
|
+
|
|
9
|
+
# hangeul
|
|
10
|
+
|
|
11
|
+
파이썬 환경에서 한글을 다루기 위한 현대적인 툴킷입니다.
|
|
12
|
+
|
|
13
|
+
기존의 파이썬용 한글 라이브러리들은 최신 개발 환경(Type Hinting, Python 3.10+ 등)에 최적화되어 있지 않거나 기능이 파편화되어 있었습니다. hangeul은 조합/분해 오토마타를 기반으로 실무에서 즉시 사용 가능한 강력한 기능들을 통합하여 제공합니다.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 핵심 강점
|
|
18
|
+
|
|
19
|
+
* **조사(Josa) 처리**: 단순히 받침 유무만 체크하지 않고 영문 약어(SDK, IBM 등)의 발음과 숫자(7, 8 등)의 종성 발음을 인식하여 조사를 선택합니다.
|
|
20
|
+
* **자소 단위 유사도 매칭**: 단어 단위가 아닌 초/중/종성 단위의 편집 거리(Levenshtein Distance)를 계산하여 오타 교정에 최적화된 유사도 점수를 제공합니다.
|
|
21
|
+
* **오토마타 기반 조합/분해**: 연음 법칙을 고려한 조합 로직을 통해 `ㄱㅏㅂㅅㅇㅣ`를 `값이`로 정확히 조립합니다.
|
|
22
|
+
* **Zero Dependency**: 외부 라이브러리 의존성 없이 파이썬 표준 라이브러리만으로 동작합니다.
|
|
23
|
+
* **Modern Development**: 전 기능에 대한 타입 힌팅을 지원하며, `uv` 및 최신 파이썬 환경에 최적화되어 있습니다.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 주요 기능 및 예제
|
|
28
|
+
|
|
29
|
+
### 1. 조사(Josa) 결합
|
|
30
|
+
단순 문자열 비교가 아닌 발음 기반의 처리를 수행합니다.
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import hangeul
|
|
34
|
+
|
|
35
|
+
# 영문 약어 발음 인식
|
|
36
|
+
hangeul.attach("SDK", "은/는") # "SDK는" (에스디케이 - 받침 없음)
|
|
37
|
+
hangeul.attach("IBM", "이/가") # "IBM이" (아이비엠 - ㅁ받침)
|
|
38
|
+
|
|
39
|
+
# 숫자 종성 인식
|
|
40
|
+
hangeul.attach("7", "으로/로") # "7로" (칠 - ㄹ받침 예외)
|
|
41
|
+
hangeul.attach("10", "을/를") # "10을" (십 - ㅂ받침)
|
|
42
|
+
|
|
43
|
+
# 괄호 제거 후 판별
|
|
44
|
+
hangeul.attach("애플(Apple)", "이/가") # "애플(Apple)이"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. 조합 및 분해 (Assemble / Disassemble)
|
|
48
|
+
한국어 음운 규칙을 따르는 조립과 완전 분해를 지원합니다.
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
# 조합 (연음 법칙 적용)
|
|
52
|
+
hangeul.assemble("ㄱㅏㅂㅅㅇㅣ") # "값이"
|
|
53
|
+
|
|
54
|
+
# 분해 (겹받침/복합모음 완전 분리)
|
|
55
|
+
hangeul.disassemble("값") # "ㄱㅏㅂㅅ"
|
|
56
|
+
hangeul.disassemble("과") # "ㄱㅗㅏ"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 3. 유사도 분석 및 검색
|
|
60
|
+
오타에 강한 검색 기능을 구현할 때 유용합니다.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
# 자소 단위 유사도 (0.0 ~ 1.0)
|
|
64
|
+
# '값'과 '갑'은 글자 단위에선 0점이지만 자소 단위에선 0.75점입니다.
|
|
65
|
+
hangeul.jamo_similarity("값", "갑") # 0.75
|
|
66
|
+
|
|
67
|
+
# 초성 검색 및 부분 일치
|
|
68
|
+
hangeul.matches("서강대학교", "ㅅㄱㄷ") # True
|
|
69
|
+
hangeul.matches("한글", "ㅎㅏㄴ") # True
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 4. 숫자 한글 변환
|
|
73
|
+
숫자 단위를 분석하여 관습적인 한글 읽기 방식으로 변환합니다.
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
hangeul.num2han(1234567) # "백이십삼만사천오백육십칠"
|
|
77
|
+
hangeul.num2han(10000) # "만" (일만 -> 만 축약 적용)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 설치
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pip install hangeul
|
|
86
|
+
# or
|
|
87
|
+
uv add hangeul
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## 환경
|
|
91
|
+
* Python 3.10 이상 권장
|
|
92
|
+
* 의존성 없음 (Standard Library Only)
|
|
93
|
+
|
|
94
|
+
## 기여 및 문의
|
|
95
|
+
버그 리포트나 기능 제안은 이슈 트래커를 이용해 주세요. High Cohesion, Low Coupling 원칙을 준수하며 관리됩니다.
|
hangeul-0.1.0/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# hangeul
|
|
2
|
+
|
|
3
|
+
파이썬 환경에서 한글을 다루기 위한 현대적인 툴킷입니다.
|
|
4
|
+
|
|
5
|
+
기존의 파이썬용 한글 라이브러리들은 최신 개발 환경(Type Hinting, Python 3.10+ 등)에 최적화되어 있지 않거나 기능이 파편화되어 있었습니다. hangeul은 조합/분해 오토마타를 기반으로 실무에서 즉시 사용 가능한 강력한 기능들을 통합하여 제공합니다.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 핵심 강점
|
|
10
|
+
|
|
11
|
+
* **조사(Josa) 처리**: 단순히 받침 유무만 체크하지 않고 영문 약어(SDK, IBM 등)의 발음과 숫자(7, 8 등)의 종성 발음을 인식하여 조사를 선택합니다.
|
|
12
|
+
* **자소 단위 유사도 매칭**: 단어 단위가 아닌 초/중/종성 단위의 편집 거리(Levenshtein Distance)를 계산하여 오타 교정에 최적화된 유사도 점수를 제공합니다.
|
|
13
|
+
* **오토마타 기반 조합/분해**: 연음 법칙을 고려한 조합 로직을 통해 `ㄱㅏㅂㅅㅇㅣ`를 `값이`로 정확히 조립합니다.
|
|
14
|
+
* **Zero Dependency**: 외부 라이브러리 의존성 없이 파이썬 표준 라이브러리만으로 동작합니다.
|
|
15
|
+
* **Modern Development**: 전 기능에 대한 타입 힌팅을 지원하며, `uv` 및 최신 파이썬 환경에 최적화되어 있습니다.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 주요 기능 및 예제
|
|
20
|
+
|
|
21
|
+
### 1. 조사(Josa) 결합
|
|
22
|
+
단순 문자열 비교가 아닌 발음 기반의 처리를 수행합니다.
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import hangeul
|
|
26
|
+
|
|
27
|
+
# 영문 약어 발음 인식
|
|
28
|
+
hangeul.attach("SDK", "은/는") # "SDK는" (에스디케이 - 받침 없음)
|
|
29
|
+
hangeul.attach("IBM", "이/가") # "IBM이" (아이비엠 - ㅁ받침)
|
|
30
|
+
|
|
31
|
+
# 숫자 종성 인식
|
|
32
|
+
hangeul.attach("7", "으로/로") # "7로" (칠 - ㄹ받침 예외)
|
|
33
|
+
hangeul.attach("10", "을/를") # "10을" (십 - ㅂ받침)
|
|
34
|
+
|
|
35
|
+
# 괄호 제거 후 판별
|
|
36
|
+
hangeul.attach("애플(Apple)", "이/가") # "애플(Apple)이"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 2. 조합 및 분해 (Assemble / Disassemble)
|
|
40
|
+
한국어 음운 규칙을 따르는 조립과 완전 분해를 지원합니다.
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
# 조합 (연음 법칙 적용)
|
|
44
|
+
hangeul.assemble("ㄱㅏㅂㅅㅇㅣ") # "값이"
|
|
45
|
+
|
|
46
|
+
# 분해 (겹받침/복합모음 완전 분리)
|
|
47
|
+
hangeul.disassemble("값") # "ㄱㅏㅂㅅ"
|
|
48
|
+
hangeul.disassemble("과") # "ㄱㅗㅏ"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 3. 유사도 분석 및 검색
|
|
52
|
+
오타에 강한 검색 기능을 구현할 때 유용합니다.
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
# 자소 단위 유사도 (0.0 ~ 1.0)
|
|
56
|
+
# '값'과 '갑'은 글자 단위에선 0점이지만 자소 단위에선 0.75점입니다.
|
|
57
|
+
hangeul.jamo_similarity("값", "갑") # 0.75
|
|
58
|
+
|
|
59
|
+
# 초성 검색 및 부분 일치
|
|
60
|
+
hangeul.matches("서강대학교", "ㅅㄱㄷ") # True
|
|
61
|
+
hangeul.matches("한글", "ㅎㅏㄴ") # True
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 4. 숫자 한글 변환
|
|
65
|
+
숫자 단위를 분석하여 관습적인 한글 읽기 방식으로 변환합니다.
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
hangeul.num2han(1234567) # "백이십삼만사천오백육십칠"
|
|
69
|
+
hangeul.num2han(10000) # "만" (일만 -> 만 축약 적용)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 설치
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install hangeul
|
|
78
|
+
# or
|
|
79
|
+
uv add hangeul
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 환경
|
|
83
|
+
* Python 3.10 이상 권장
|
|
84
|
+
* 의존성 없음 (Standard Library Only)
|
|
85
|
+
|
|
86
|
+
## 기여 및 문의
|
|
87
|
+
버그 리포트나 기능 제안은 이슈 트래커를 이용해 주세요. High Cohesion, Low Coupling 원칙을 준수하며 관리됩니다.
|
hangeul-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
2
|
+
|
|
3
|
+
from .check import (
|
|
4
|
+
is_han,
|
|
5
|
+
is_pure_han,
|
|
6
|
+
has_batchim,
|
|
7
|
+
can_cho,
|
|
8
|
+
can_jung,
|
|
9
|
+
can_jong,
|
|
10
|
+
)
|
|
11
|
+
from .core import (
|
|
12
|
+
assemble,
|
|
13
|
+
disassemble,
|
|
14
|
+
qwerty2han,
|
|
15
|
+
)
|
|
16
|
+
from .josa import (
|
|
17
|
+
get_josa,
|
|
18
|
+
attach,
|
|
19
|
+
)
|
|
20
|
+
from .search import (
|
|
21
|
+
matches,
|
|
22
|
+
get_chosung,
|
|
23
|
+
search_filter,
|
|
24
|
+
)
|
|
25
|
+
from .constants import (
|
|
26
|
+
CHOSOENG,
|
|
27
|
+
JUNGSOENG,
|
|
28
|
+
JONGSOENG,
|
|
29
|
+
)
|
|
30
|
+
from .similarity import jamo_similarity
|
|
31
|
+
from .number import num2han
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
__all__: list[str] = [
|
|
35
|
+
# Metadata
|
|
36
|
+
"__version__",
|
|
37
|
+
|
|
38
|
+
# Check API
|
|
39
|
+
"is_han",
|
|
40
|
+
"is_pure_han",
|
|
41
|
+
"has_batchim",
|
|
42
|
+
"can_cho",
|
|
43
|
+
"can_jung",
|
|
44
|
+
"can_jong",
|
|
45
|
+
|
|
46
|
+
# Core API
|
|
47
|
+
"assemble",
|
|
48
|
+
"disassemble",
|
|
49
|
+
"qwerty2han",
|
|
50
|
+
|
|
51
|
+
# Josa API
|
|
52
|
+
"get_josa",
|
|
53
|
+
"josa",
|
|
54
|
+
"attach",
|
|
55
|
+
|
|
56
|
+
# Search API
|
|
57
|
+
"matches",
|
|
58
|
+
"get_chosung",
|
|
59
|
+
"search_filter",
|
|
60
|
+
|
|
61
|
+
# Constants
|
|
62
|
+
"CHOSOENG",
|
|
63
|
+
"JUNGSOENG",
|
|
64
|
+
"JONGSOENG",
|
|
65
|
+
|
|
66
|
+
"jamo_similarity",
|
|
67
|
+
"num2han"
|
|
68
|
+
]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import unicodedata
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
from .constants import 초성, 중성, 종성
|
|
5
|
+
|
|
6
|
+
_NormalizationForm = Literal[
|
|
7
|
+
"NFC", "NFD", "NFKC", "NFKD",
|
|
8
|
+
"nfc", "nfd", "nfkc", "nfkd"
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
def normalize(text: str, form: _NormalizationForm = "NFC") -> str:
|
|
12
|
+
"""
|
|
13
|
+
텍스트를 유니코드 정규화 형식으로 변환합니다.
|
|
14
|
+
기본값은 NFC(완성형)이며, macOS 환경과의 호환성을 위해 사용합니다.
|
|
15
|
+
"""
|
|
16
|
+
return unicodedata.normalize(form.upper(), text)
|
|
17
|
+
|
|
18
|
+
def is_han(text: str, allow_space: bool = True, allow_punctuation: bool = False) -> bool:
|
|
19
|
+
if not isinstance(text, str) or not text:
|
|
20
|
+
return False
|
|
21
|
+
|
|
22
|
+
for char in text:
|
|
23
|
+
if ('가' <= char <= '힣' or 'ㄱ' <= char <= 'ㅣ'):
|
|
24
|
+
continue
|
|
25
|
+
if allow_space and char.isspace():
|
|
26
|
+
continue
|
|
27
|
+
if allow_punctuation and unicodedata.category(char).startswith('P'):
|
|
28
|
+
continue
|
|
29
|
+
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
return True
|
|
33
|
+
|
|
34
|
+
def is_pure_han(text: str) -> bool:
|
|
35
|
+
if not text:
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
return all('가' <= c <= '힣' or 'ㄱ' <= c <= 'ㅣ' for c in text)
|
|
39
|
+
|
|
40
|
+
def can_cho(char: str) -> bool:
|
|
41
|
+
return char in 초성
|
|
42
|
+
|
|
43
|
+
def can_jung(char: str) -> bool:
|
|
44
|
+
return char in 중성
|
|
45
|
+
|
|
46
|
+
def can_jong(char: str) -> bool:
|
|
47
|
+
return char in 종성
|
|
48
|
+
|
|
49
|
+
def has_batchim(char: str) -> bool:
|
|
50
|
+
if len(char) != 1 or not ('가' <= char <= '힣'):
|
|
51
|
+
return False
|
|
52
|
+
return (ord(char) - 0xAC00) % 28 > 0
|
|
53
|
+
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from typing import Final
|
|
2
|
+
from types import MappingProxyType
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
초성: Final[tuple[str, ...]] = (
|
|
6
|
+
'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ',
|
|
7
|
+
'ㅂ', 'ㅃ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ',
|
|
8
|
+
'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ',
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
중성: Final[tuple[str, ...]] = (
|
|
12
|
+
'ㅏ', 'ㅐ', 'ㅑ', 'ㅒ', 'ㅓ', 'ㅔ', 'ㅕ', 'ㅖ',
|
|
13
|
+
'ㅗ', 'ㅘ', 'ㅙ', 'ㅚ', 'ㅛ', 'ㅜ', 'ㅝ', 'ㅞ',
|
|
14
|
+
'ㅟ', 'ㅠ', 'ㅡ', 'ㅢ', 'ㅣ'
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
종성: Final[tuple[str, ...]] = (
|
|
18
|
+
'', 'ㄱ', 'ㄲ', 'ㄳ', 'ㄴ', 'ㄵ', 'ㄶ', 'ㄷ',
|
|
19
|
+
'ㄹ', 'ㄺ', 'ㄻ', 'ㄼ', 'ㄽ', 'ㄾ', 'ㄿ', 'ㅀ',
|
|
20
|
+
'ㅁ', 'ㅂ', 'ㅄ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅊ',
|
|
21
|
+
'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
겹받침맵: MappingProxyType[tuple[str, str], str] = MappingProxyType({
|
|
25
|
+
('ㄱ', 'ㅅ'): 'ㄳ', ('ㄴ', 'ㅈ'): 'ㄵ', ('ㄴ', 'ㅎ'): 'ㄶ', ('ㄹ', 'ㄱ'): 'ㄺ',
|
|
26
|
+
('ㄹ', 'ㅁ'): 'ㄻ', ('ㄹ', 'ㅂ'): 'ㄼ', ('ㄹ', 'ㅅ'): 'ㄽ', ('ㄹ', 'ㅌ'): 'ㄾ',
|
|
27
|
+
('ㄹ', 'ㅍ'): 'ㄿ', ('ㄹ', 'ㅎ'): 'ㅀ', ('ㅂ', 'ㅅ'): 'ㅄ'
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
복합모음맵: MappingProxyType[tuple[str, str], str] = MappingProxyType({
|
|
31
|
+
('ㅗ', 'ㅏ'): 'ㅘ', ('ㅗ', 'ㅐ'): 'ㅙ', ('ㅗ', 'ㅣ'): 'ㅚ',
|
|
32
|
+
('ㅜ', 'ㅓ'): 'ㅝ', ('ㅜ', 'ㅔ'): 'ㅞ', ('ㅜ', 'ㅣ'): 'ㅟ', ('ㅡ', 'ㅣ'): 'ㅢ'
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
쿼티한글맵: MappingProxyType[str, str] = MappingProxyType({
|
|
36
|
+
'q': 'ㅂ', 'w': 'ㅈ', 'e': 'ㄷ', 'r': 'ㄱ', 't': 'ㅅ',
|
|
37
|
+
'a': 'ㅁ', 's': 'ㄴ', 'd': 'ㅇ', 'f': 'ㄹ', 'g': 'ㅎ',
|
|
38
|
+
'z': 'ㅋ', 'x': 'ㅌ', 'c': 'ㅊ', 'v': 'ㅍ',
|
|
39
|
+
|
|
40
|
+
'y': 'ㅛ', 'u': 'ㅕ', 'i': 'ㅑ', 'o': 'ㅐ', 'p': 'ㅔ',
|
|
41
|
+
'h': 'ㅗ', 'j': 'ㅓ', 'k': 'ㅏ', 'l': 'ㅣ',
|
|
42
|
+
'b': 'ㅠ', 'n': 'ㅜ', 'm': 'ㅡ',
|
|
43
|
+
|
|
44
|
+
'Q': 'ㅃ', 'W': 'ㅉ', 'E': 'ㄸ', 'R': 'ㄲ', 'T': 'ㅆ',
|
|
45
|
+
'O': 'ㅒ', 'P': 'ㅖ'
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
CHOSOENG: Final[tuple[str, ...]] = 초성
|
|
49
|
+
JUNGSOENG: Final[tuple[str, ...]] = 중성
|
|
50
|
+
JONGSOENG: Final[tuple[str, ...]] = 종성
|
|
51
|
+
DOUBLE_JONG_MAP: MappingProxyType[tuple[str, str], str] = 겹받침맵
|
|
52
|
+
COMPOUND_JUNG_MAP: MappingProxyType[tuple[str, str], str] = 복합모음맵
|
|
53
|
+
QWERTY_HANGEUL_MAP: MappingProxyType[str, str] = 쿼티한글맵
|
|
54
|
+
|
|
55
|
+
__all__: list[str] = [
|
|
56
|
+
"CHOSOENG",
|
|
57
|
+
"JUNGSOENGS",
|
|
58
|
+
"JONGSOENGS",
|
|
59
|
+
"DOUBLE_JONG_MAP",
|
|
60
|
+
"COMPOUND_JUNG_MAP",
|
|
61
|
+
"QWERTY_HANGEUL_MAP"
|
|
62
|
+
]
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from .constants import (
|
|
2
|
+
초성, 중성, 종성,
|
|
3
|
+
겹받침맵, 복합모음맵, 쿼티한글맵
|
|
4
|
+
)
|
|
5
|
+
|
|
6
|
+
# 성능을 위한 인덱스 맵 미리 생성 (O(1) lookup)
|
|
7
|
+
_CHO_IDX = {char: i for i, char in enumerate(초성)}
|
|
8
|
+
_JUNG_IDX = {char: i for i, char in enumerate(중성)}
|
|
9
|
+
_JONG_IDX = {char: i for i, char in enumerate(종성)}
|
|
10
|
+
_SPLIT_JONG_MAP = {v: k for k, v in 겹받침맵.items()}
|
|
11
|
+
_SPLIT_JUNG_MAP = {v: k for k, v in 복합모음맵.items()}
|
|
12
|
+
|
|
13
|
+
def qwerty2han(text: str) -> str:
|
|
14
|
+
"""영문 쿼티 문자열을 한글 자모 문자열로 변환합니다."""
|
|
15
|
+
return "".join(쿼티한글맵.get(char, char) for char in text)
|
|
16
|
+
|
|
17
|
+
def assemble(text: str) -> str:
|
|
18
|
+
"""자소 단위로 분리된 문자열을 완성형 한글로 조합합니다."""
|
|
19
|
+
cho_idx, jung_idx, jong_idx = -1, -1, 0
|
|
20
|
+
result: list[str] = []
|
|
21
|
+
|
|
22
|
+
def make_syllable(c, r, t):
|
|
23
|
+
return chr(0xAC00 + (c * 21 * 28) + (r * 28) + t)
|
|
24
|
+
|
|
25
|
+
def flush():
|
|
26
|
+
nonlocal cho_idx, jung_idx, jong_idx
|
|
27
|
+
if cho_idx != -1 and jung_idx != -1:
|
|
28
|
+
result.append(make_syllable(cho_idx, jung_idx, jong_idx))
|
|
29
|
+
elif cho_idx != -1:
|
|
30
|
+
result.append(초성[cho_idx])
|
|
31
|
+
elif jung_idx != -1:
|
|
32
|
+
result.append(중성[jung_idx])
|
|
33
|
+
cho_idx, jung_idx, jong_idx = -1, -1, 0
|
|
34
|
+
|
|
35
|
+
for char in text:
|
|
36
|
+
# 1. 자음 처리
|
|
37
|
+
if char in _CHO_IDX or char in _JONG_IDX:
|
|
38
|
+
if cho_idx != -1 and jung_idx != -1:
|
|
39
|
+
if jong_idx == 0: # 현재 받침 없음
|
|
40
|
+
if char in _JONG_IDX:
|
|
41
|
+
jong_idx = _JONG_IDX[char]
|
|
42
|
+
else: # 종성 불가 자음 (ㄸ, ㅃ, ㅉ)
|
|
43
|
+
flush()
|
|
44
|
+
cho_idx = _CHO_IDX.get(char, -1)
|
|
45
|
+
else: # 현재 받침 있음 -> 겹받침 가능성 확인
|
|
46
|
+
cur_jong = 종성[jong_idx]
|
|
47
|
+
if (cur_jong, char) in 겹받침맵:
|
|
48
|
+
jong_idx = _JONG_IDX[겹받침맵[(cur_jong, char)]]
|
|
49
|
+
else: # 겹받침 불가 -> 새 글자로
|
|
50
|
+
flush()
|
|
51
|
+
if char in _CHO_IDX:
|
|
52
|
+
cho_idx = _CHO_IDX[char]
|
|
53
|
+
else:
|
|
54
|
+
result.append(char)
|
|
55
|
+
else: # 초성만 있거나 새로 시작
|
|
56
|
+
if cho_idx != -1: flush()
|
|
57
|
+
if char in _CHO_IDX:
|
|
58
|
+
cho_idx = _CHO_IDX[char]
|
|
59
|
+
else:
|
|
60
|
+
result.append(char)
|
|
61
|
+
|
|
62
|
+
# 2. 모음 처리
|
|
63
|
+
elif char in _JUNG_IDX:
|
|
64
|
+
# 받침이 있는 상태에서 모음이 오면 연음 처리 (예: 값이 -> 갑시)
|
|
65
|
+
if jong_idx != 0:
|
|
66
|
+
cur_jong_char = 종성[jong_idx]
|
|
67
|
+
if cur_jong_char in _SPLIT_JONG_MAP: # 겹받침인 경우 분리
|
|
68
|
+
f_jong, s_cho = _SPLIT_JONG_MAP[cur_jong_char]
|
|
69
|
+
result.append(make_syllable(cho_idx, jung_idx, _JONG_IDX[f_jong]))
|
|
70
|
+
cho_idx, jung_idx, jong_idx = _CHO_IDX[s_cho], _JUNG_IDX[char], 0
|
|
71
|
+
else: # 일반 받침은 다음 초성으로 이동
|
|
72
|
+
result.append(make_syllable(cho_idx, jung_idx, 0))
|
|
73
|
+
cho_idx, jung_idx, jong_idx = _CHO_IDX.get(cur_jong_char, -1), _JUNG_IDX[char], 0
|
|
74
|
+
|
|
75
|
+
elif cho_idx != -1: # 초성만 있는 상태에서 모음
|
|
76
|
+
if jung_idx == -1:
|
|
77
|
+
jung_idx = _JUNG_IDX[char]
|
|
78
|
+
else: # 복합 모음 가능성 (ㅗ + ㅏ = ㅘ)
|
|
79
|
+
cur_jung = 중성[jung_idx]
|
|
80
|
+
if (cur_jung, char) in 복합모음맵:
|
|
81
|
+
jung_idx = _JUNG_IDX[복합모음맵[(cur_jung, char)]]
|
|
82
|
+
else:
|
|
83
|
+
flush()
|
|
84
|
+
jung_idx = _JUNG_IDX[char]
|
|
85
|
+
else: # 초성 없는 모음 단독
|
|
86
|
+
if jung_idx != -1:
|
|
87
|
+
cur_jung = 중성[jung_idx]
|
|
88
|
+
if (cur_jung, char) in 복합모음맵:
|
|
89
|
+
jung_idx = _JUNG_IDX[복합모음맵[(cur_jung, char)]]
|
|
90
|
+
flush()
|
|
91
|
+
else:
|
|
92
|
+
flush()
|
|
93
|
+
jung_idx = _JUNG_IDX[char]
|
|
94
|
+
else:
|
|
95
|
+
jung_idx = _JUNG_IDX[char]
|
|
96
|
+
|
|
97
|
+
# 3. 기타 문자 (숫자, 영문, 특수문자 등)
|
|
98
|
+
else:
|
|
99
|
+
flush()
|
|
100
|
+
result.append(char)
|
|
101
|
+
|
|
102
|
+
flush()
|
|
103
|
+
return "".join(result)
|
|
104
|
+
|
|
105
|
+
def disassemble(text: str) -> str:
|
|
106
|
+
"""
|
|
107
|
+
완성형 한글 문자열을 개별 자소 단위로 완전히 분리합니다.
|
|
108
|
+
(예: '값' -> 'ㄱㅏㅂㅅ', '과' -> 'ㄱㅗㅏ')
|
|
109
|
+
"""
|
|
110
|
+
result: list[str] = []
|
|
111
|
+
|
|
112
|
+
for char in text:
|
|
113
|
+
# 1. 완성형 한글인 경우 (가 ~ 힣)
|
|
114
|
+
if '가' <= char <= '힣':
|
|
115
|
+
num = ord(char) - 0xAC00
|
|
116
|
+
c = num // 588
|
|
117
|
+
r = (num % 588) // 28
|
|
118
|
+
t = num % 28
|
|
119
|
+
|
|
120
|
+
# 초성 추가
|
|
121
|
+
result.append(초성[c])
|
|
122
|
+
|
|
123
|
+
# 중성 분리 (ㅘ -> ㅗ, ㅏ)
|
|
124
|
+
jung_char = 중성[r]
|
|
125
|
+
if jung_char in _SPLIT_JUNG_MAP:
|
|
126
|
+
result.extend(_SPLIT_JUNG_MAP[jung_char])
|
|
127
|
+
else:
|
|
128
|
+
result.append(jung_char)
|
|
129
|
+
|
|
130
|
+
# 종성 분리 (ㄳ -> ㄱ, ㅅ)
|
|
131
|
+
if t > 0:
|
|
132
|
+
jong_char = 종성[t]
|
|
133
|
+
if jong_char in _SPLIT_JONG_MAP:
|
|
134
|
+
result.extend(_SPLIT_JONG_MAP[jong_char])
|
|
135
|
+
else:
|
|
136
|
+
result.append(jong_char)
|
|
137
|
+
|
|
138
|
+
# 2. 이미 분리된 자제나 기타 문자인 경우
|
|
139
|
+
else:
|
|
140
|
+
if char in _SPLIT_JUNG_MAP:
|
|
141
|
+
result.extend(_SPLIT_JUNG_MAP[char])
|
|
142
|
+
elif char in _SPLIT_JONG_MAP:
|
|
143
|
+
result.extend(_SPLIT_JONG_MAP[char])
|
|
144
|
+
else:
|
|
145
|
+
result.append(char)
|
|
146
|
+
|
|
147
|
+
return "".join(result)
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
# 1. 복합 문자 분리 테스트
|
|
151
|
+
print(disassemble("값이")) # 결과: "ㄱㅏㅂㅅㅇㅣ"
|
|
152
|
+
|
|
153
|
+
# 2. 역변환 (Round-trip) 테스트
|
|
154
|
+
raw = "값이"
|
|
155
|
+
dis = disassemble(raw)
|
|
156
|
+
ass = assemble(dis)
|
|
157
|
+
print(f"{raw} -> {dis} -> {ass}") # 결과: 값이 -> ㄱㅏㅂㅅㅇㅣ -> 값이
|
|
158
|
+
|
|
159
|
+
# 3. 복합 모음 테스트
|
|
160
|
+
print(disassemble("왜 뭐가"))
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from types import MappingProxyType
|
|
3
|
+
from typing import Final
|
|
4
|
+
|
|
5
|
+
# 1. 상수 정의
|
|
6
|
+
_JOSA_MAP: Final = MappingProxyType({
|
|
7
|
+
"이/가": ("가", "이"),
|
|
8
|
+
"은/는": ("는", "은"),
|
|
9
|
+
"을/를": ("를", "을"),
|
|
10
|
+
"와/과": ("와", "과"),
|
|
11
|
+
"아/야": ("야", "아"),
|
|
12
|
+
"나/이나": ("나", "이나"),
|
|
13
|
+
"란/이란": ("란", "이란"),
|
|
14
|
+
"랑/이랑": ("랑", "이랑"),
|
|
15
|
+
"라/이라": ("라", "이라"),
|
|
16
|
+
"예요/이에요": ("예요", "이에요"),
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
# 로 계열 조사는 'ㄹ' 받침 예외 규칙을 동일하게 공유함
|
|
20
|
+
_RO_JOSAS: Final[tuple[str, ...]] = ("로/으로", "로서/으로서", "로써/으로써", "로부터/으로부터")
|
|
21
|
+
|
|
22
|
+
# 영문 알파벳 끝글자 발음 데이터 (약어 처리용)
|
|
23
|
+
_ALPHABET_BATTCHIM: MappingProxyType = MappingProxyType({
|
|
24
|
+
'A': '에이', 'B': '비', 'C': '씨', 'D': '디', 'E': '이',
|
|
25
|
+
'F': '에프', 'G': '지', 'H': '에이치', 'I': '아이', 'J': '제이',
|
|
26
|
+
'K': '케이', 'L': '엘', 'M': '엠', 'N': '엔', 'O': '오',
|
|
27
|
+
'P': '피', 'Q': '큐', 'R': '알', 'S': '에스', 'T': '티',
|
|
28
|
+
'U': '유', 'V': '브이', 'W': '더블유', 'X': '엑스', 'Y': '와이', 'Z': '제트'
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
def _get_jong_idx(char: str) -> int:
|
|
32
|
+
"""한글 음절의 종성 인덱스를 반환합니다."""
|
|
33
|
+
if '가' <= char <= '힣':
|
|
34
|
+
return (ord(char) - 0xAC00) % 28
|
|
35
|
+
return -1
|
|
36
|
+
|
|
37
|
+
def _analyze_last_char(word: str) -> tuple[bool, int]:
|
|
38
|
+
"""
|
|
39
|
+
단어의 마지막 글자를 분석하여 (받침_여부, 종성_인덱스)를 반환합니다.
|
|
40
|
+
괄호, 숫자, 영문 약어 처리를 포함합니다.
|
|
41
|
+
"""
|
|
42
|
+
if not word:
|
|
43
|
+
return False, -1
|
|
44
|
+
|
|
45
|
+
# 1. 괄호 제거: "단어(Content)" -> "단어" 추출
|
|
46
|
+
base_word = re.sub(r'\(.*\)$', '', word).strip()
|
|
47
|
+
if not base_word:
|
|
48
|
+
base_word = word
|
|
49
|
+
last = base_word[-1]
|
|
50
|
+
|
|
51
|
+
# 2. 한글 분석
|
|
52
|
+
if '가' <= last <= '힣':
|
|
53
|
+
idx = _get_jong_idx(last)
|
|
54
|
+
return idx > 0, idx
|
|
55
|
+
|
|
56
|
+
# 3. 숫자 분석 (0, 1, 3, 6, 7, 8 은 받침 있음 / 1, 7, 8 은 'ㄹ' 계열)
|
|
57
|
+
if last.isdigit():
|
|
58
|
+
has_bat = last in "013678"
|
|
59
|
+
return has_bat, (8 if last in "178" else (1 if has_bat else 0))
|
|
60
|
+
|
|
61
|
+
# 4. 영문 대문자 약어 분석 (예: SDK, AI, SQL)
|
|
62
|
+
if base_word.isupper() and 'A' <= last <= 'Z':
|
|
63
|
+
pronunciation = _ALPHABET_BATTCHIM.get(last, "")
|
|
64
|
+
if pronunciation:
|
|
65
|
+
idx = _get_jong_idx(pronunciation[-1])
|
|
66
|
+
return idx > 0, idx
|
|
67
|
+
|
|
68
|
+
# 5. 기타 (일반 영문 단어, 특수문자 등)는 받침 없음으로 간주
|
|
69
|
+
return False, 0
|
|
70
|
+
|
|
71
|
+
def get_josa(word: str, pattern: str) -> str:
|
|
72
|
+
"""
|
|
73
|
+
단어와 조사 패턴을 분석하여 적절한 조사를 선택합니다.
|
|
74
|
+
"""
|
|
75
|
+
if not word:
|
|
76
|
+
return ""
|
|
77
|
+
|
|
78
|
+
has_bat, jong_idx = _analyze_last_char(word)
|
|
79
|
+
norm_pattern = "/".join(sorted(pattern.split("/")))
|
|
80
|
+
|
|
81
|
+
match norm_pattern:
|
|
82
|
+
# '로' 계열 특수 처리 (받침이 없거나 'ㄹ' 받침인 경우 '로')
|
|
83
|
+
case p if p in _RO_JOSAS:
|
|
84
|
+
parts = pattern.split('/') # 사용자 입력 순서 유지
|
|
85
|
+
# 보통 (로/으로) 또는 (으로/로) 순서임
|
|
86
|
+
ro, euro = (parts[0], parts[1]) if len(parts[0]) < len(parts[1]) else (parts[1], parts[0])
|
|
87
|
+
return ro if not has_bat or jong_idx == 8 else euro
|
|
88
|
+
|
|
89
|
+
# 일반 조사 처리
|
|
90
|
+
case p if p in _JOSA_MAP:
|
|
91
|
+
vowel_case, consonant_case = _JOSA_MAP[p]
|
|
92
|
+
return consonant_case if has_bat else vowel_case
|
|
93
|
+
|
|
94
|
+
# 맵에 없는 패턴은 (받침있음/받침없음) 순서로 가정하여 처리
|
|
95
|
+
case _:
|
|
96
|
+
parts = pattern.split('/')
|
|
97
|
+
if len(parts) != 2:
|
|
98
|
+
raise ValueError(f"지원하지 않는 조사 패턴입니다: {pattern}")
|
|
99
|
+
return parts[0] if has_bat else parts[1]
|
|
100
|
+
|
|
101
|
+
def attach(word: str, pattern: str) -> str:
|
|
102
|
+
"""단어에 조사를 결합합니다."""
|
|
103
|
+
return f"{word}{get_josa(word, pattern)}"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from typing import Final
|
|
2
|
+
|
|
3
|
+
_NUM_MAP: Final = {
|
|
4
|
+
"0": "", "1": "일", "2": "이", "3": "삼", "4": "사",
|
|
5
|
+
"5": "오", "6": "육", "7": "칠", "8": "팔", "9": "구"
|
|
6
|
+
}
|
|
7
|
+
_UNIT_MAP: Final = ["", "십", "백", "천"]
|
|
8
|
+
_BLOCK_MAP: Final = ["", "만", "억", "조", "경", "해"]
|
|
9
|
+
|
|
10
|
+
def num2han(number: int | str) -> str:
|
|
11
|
+
"""
|
|
12
|
+
숫자를 한글 읽기 방식으로 변환합니다.
|
|
13
|
+
예: 1234 -> "천이백삼십사", 10001 -> "만일"
|
|
14
|
+
"""
|
|
15
|
+
num_str = str(number).strip().replace(",", "")
|
|
16
|
+
if not num_str.isdigit():
|
|
17
|
+
if not num_str: return ""
|
|
18
|
+
raise ValueError(f"숫자만 입력 가능합니다: {num_str}")
|
|
19
|
+
|
|
20
|
+
if int(num_str) == 0:
|
|
21
|
+
return "영"
|
|
22
|
+
|
|
23
|
+
# 4자리씩 chunk로 쪼갬
|
|
24
|
+
length = len(num_str)
|
|
25
|
+
full_str = num_str.zfill(((length - 1) // 4 + 1) * 4)
|
|
26
|
+
chunks = [full_str[i:i+4] for i in range(0, len(full_str), 4)]
|
|
27
|
+
|
|
28
|
+
result = []
|
|
29
|
+
num_chunks = len(chunks)
|
|
30
|
+
|
|
31
|
+
for i, chunk in enumerate(chunks):
|
|
32
|
+
chunk_res = ""
|
|
33
|
+
for j, digit in enumerate(chunk):
|
|
34
|
+
if digit == "0":
|
|
35
|
+
continue
|
|
36
|
+
|
|
37
|
+
unit = _UNIT_MAP[3 - j]
|
|
38
|
+
val = _NUM_MAP[digit]
|
|
39
|
+
|
|
40
|
+
# '일십', '일백', '일천' -> '십', '백', '천'으로 축약 (단, 일의 자리는 유지)
|
|
41
|
+
if val == "일" and unit != "":
|
|
42
|
+
chunk_res += unit
|
|
43
|
+
else:
|
|
44
|
+
chunk_res += val + unit
|
|
45
|
+
|
|
46
|
+
if chunk_res:
|
|
47
|
+
block_unit = _BLOCK_MAP[num_chunks - 1 - i]
|
|
48
|
+
# '일만' 대신 '만'이 자연스러운 경우 처리
|
|
49
|
+
if chunk_res == "일" and block_unit == "만":
|
|
50
|
+
result.append(block_unit)
|
|
51
|
+
else:
|
|
52
|
+
result.append(chunk_res + block_unit)
|
|
53
|
+
|
|
54
|
+
return "".join(result)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from .constants import 초성
|
|
2
|
+
from .core import disassemble
|
|
3
|
+
|
|
4
|
+
def get_chosung(text: str) -> str:
|
|
5
|
+
"""
|
|
6
|
+
문자열에서 초성만 추출합니다.
|
|
7
|
+
한글이 아닌 문자는 그대로 유지합니다.
|
|
8
|
+
"""
|
|
9
|
+
result = []
|
|
10
|
+
for char in text:
|
|
11
|
+
if '가' <= char <= '힣':
|
|
12
|
+
# 초성 인덱스 계산: (유니코드 - 0xAC00) // (21 * 28)
|
|
13
|
+
idx = (ord(char) - 0xAC00) // 588
|
|
14
|
+
result.append(초성[idx])
|
|
15
|
+
else:
|
|
16
|
+
result.append(char)
|
|
17
|
+
return "".join(result)
|
|
18
|
+
|
|
19
|
+
def matches(target: str, query: str, choseong_only: bool = False) -> bool:
|
|
20
|
+
"""
|
|
21
|
+
초성 검색 및 자소 단위 검색을 수행합니다.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
target: 검색 대상 (예: "서강대학교")
|
|
25
|
+
query: 검색어 (예: "ㅅㄱㄷ" 또는 "서강")
|
|
26
|
+
choseong_only: True일 경우 검색어가 초성이 아니더라도 강제로 초성 비교를 수행
|
|
27
|
+
"""
|
|
28
|
+
if not query:
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
# 1. 검색어가 순수 초성인지 확인 (또는 강제 초성 모드)
|
|
32
|
+
is_query_chosung = choseong_only or all(c in 초성 for c in query)
|
|
33
|
+
|
|
34
|
+
if is_query_chosung:
|
|
35
|
+
# 대상의 초성과 쿼리를 비교
|
|
36
|
+
return query in get_chosung(target)
|
|
37
|
+
|
|
38
|
+
# 2. 완성형/자소가 섞인 경우 자소 단위 분해 검색 (예: "ㅎㅏㄴㄱ" 검색 시 "한글" 매칭)
|
|
39
|
+
return disassemble(query) in disassemble(target)
|
|
40
|
+
|
|
41
|
+
def search_filter(targets: list[str], query: str) -> list[str]:
|
|
42
|
+
"""
|
|
43
|
+
리스트에서 검색어에 맞는 항목들을 필터링하여 반환합니다.
|
|
44
|
+
"""
|
|
45
|
+
return [t for t in targets if matches(t, query)]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from .core import disassemble
|
|
2
|
+
|
|
3
|
+
def _levenshtein_distance(s1: str, s2: str) -> int:
|
|
4
|
+
"""두 문자열 간의 편집 거리(Levenshtein Distance)를 계산합니다."""
|
|
5
|
+
if len(s1) < len(s2):
|
|
6
|
+
return _levenshtein_distance(s2, s1)
|
|
7
|
+
|
|
8
|
+
if not s2:
|
|
9
|
+
return len(s1)
|
|
10
|
+
|
|
11
|
+
previous_row = range(len(s2) + 1)
|
|
12
|
+
for i, c1 in enumerate(s1):
|
|
13
|
+
current_row = [i + 1]
|
|
14
|
+
for j, c2 in enumerate(s2):
|
|
15
|
+
insertions = previous_row[j + 1] + 1
|
|
16
|
+
deletions = current_row[j] + 1
|
|
17
|
+
substitutions = previous_row[j] + (c1 != c2)
|
|
18
|
+
current_row.append(min(insertions, deletions, substitutions))
|
|
19
|
+
previous_row = current_row
|
|
20
|
+
|
|
21
|
+
return previous_row[-1]
|
|
22
|
+
|
|
23
|
+
def jamo_similarity(str1: str, str2: str) -> float:
|
|
24
|
+
"""
|
|
25
|
+
두 문자열을 자소 단위로 분해하여 유사도(0.0 ~ 1.0)를 측정합니다.
|
|
26
|
+
1.0에 가까울수록 두 문자열이 유사함을 의미합니다.
|
|
27
|
+
"""
|
|
28
|
+
if str1 == str2:
|
|
29
|
+
return 1.0
|
|
30
|
+
|
|
31
|
+
# 자소 단위로 완전히 분해 (예: '값' -> 'ㄱㅏㅂㅅ')
|
|
32
|
+
s1_dis = disassemble(str1)
|
|
33
|
+
s2_dis = disassemble(str2)
|
|
34
|
+
|
|
35
|
+
if not s1_dis or not s2_dis:
|
|
36
|
+
return 0.0
|
|
37
|
+
|
|
38
|
+
distance = _levenshtein_distance(s1_dis, s2_dis)
|
|
39
|
+
max_len = max(len(s1_dis), len(s2_dis))
|
|
40
|
+
|
|
41
|
+
# 유사도 계산 공식: 1 - (거리 / 최대 길이)
|
|
42
|
+
return 1.0 - (distance / max_len)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hangeul
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: pytest>=9.0.3
|
|
8
|
+
|
|
9
|
+
# hangeul
|
|
10
|
+
|
|
11
|
+
파이썬 환경에서 한글을 다루기 위한 현대적인 툴킷입니다.
|
|
12
|
+
|
|
13
|
+
기존의 파이썬용 한글 라이브러리들은 최신 개발 환경(Type Hinting, Python 3.10+ 등)에 최적화되어 있지 않거나 기능이 파편화되어 있었습니다. hangeul은 조합/분해 오토마타를 기반으로 실무에서 즉시 사용 가능한 강력한 기능들을 통합하여 제공합니다.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 핵심 강점
|
|
18
|
+
|
|
19
|
+
* **조사(Josa) 처리**: 단순히 받침 유무만 체크하지 않고 영문 약어(SDK, IBM 등)의 발음과 숫자(7, 8 등)의 종성 발음을 인식하여 조사를 선택합니다.
|
|
20
|
+
* **자소 단위 유사도 매칭**: 단어 단위가 아닌 초/중/종성 단위의 편집 거리(Levenshtein Distance)를 계산하여 오타 교정에 최적화된 유사도 점수를 제공합니다.
|
|
21
|
+
* **오토마타 기반 조합/분해**: 연음 법칙을 고려한 조합 로직을 통해 `ㄱㅏㅂㅅㅇㅣ`를 `값이`로 정확히 조립합니다.
|
|
22
|
+
* **Zero Dependency**: 외부 라이브러리 의존성 없이 파이썬 표준 라이브러리만으로 동작합니다.
|
|
23
|
+
* **Modern Development**: 전 기능에 대한 타입 힌팅을 지원하며, `uv` 및 최신 파이썬 환경에 최적화되어 있습니다.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 주요 기능 및 예제
|
|
28
|
+
|
|
29
|
+
### 1. 조사(Josa) 결합
|
|
30
|
+
단순 문자열 비교가 아닌 발음 기반의 처리를 수행합니다.
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import hangeul
|
|
34
|
+
|
|
35
|
+
# 영문 약어 발음 인식
|
|
36
|
+
hangeul.attach("SDK", "은/는") # "SDK는" (에스디케이 - 받침 없음)
|
|
37
|
+
hangeul.attach("IBM", "이/가") # "IBM이" (아이비엠 - ㅁ받침)
|
|
38
|
+
|
|
39
|
+
# 숫자 종성 인식
|
|
40
|
+
hangeul.attach("7", "으로/로") # "7로" (칠 - ㄹ받침 예외)
|
|
41
|
+
hangeul.attach("10", "을/를") # "10을" (십 - ㅂ받침)
|
|
42
|
+
|
|
43
|
+
# 괄호 제거 후 판별
|
|
44
|
+
hangeul.attach("애플(Apple)", "이/가") # "애플(Apple)이"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. 조합 및 분해 (Assemble / Disassemble)
|
|
48
|
+
한국어 음운 규칙을 따르는 조립과 완전 분해를 지원합니다.
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
# 조합 (연음 법칙 적용)
|
|
52
|
+
hangeul.assemble("ㄱㅏㅂㅅㅇㅣ") # "값이"
|
|
53
|
+
|
|
54
|
+
# 분해 (겹받침/복합모음 완전 분리)
|
|
55
|
+
hangeul.disassemble("값") # "ㄱㅏㅂㅅ"
|
|
56
|
+
hangeul.disassemble("과") # "ㄱㅗㅏ"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 3. 유사도 분석 및 검색
|
|
60
|
+
오타에 강한 검색 기능을 구현할 때 유용합니다.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
# 자소 단위 유사도 (0.0 ~ 1.0)
|
|
64
|
+
# '값'과 '갑'은 글자 단위에선 0점이지만 자소 단위에선 0.75점입니다.
|
|
65
|
+
hangeul.jamo_similarity("값", "갑") # 0.75
|
|
66
|
+
|
|
67
|
+
# 초성 검색 및 부분 일치
|
|
68
|
+
hangeul.matches("서강대학교", "ㅅㄱㄷ") # True
|
|
69
|
+
hangeul.matches("한글", "ㅎㅏㄴ") # True
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 4. 숫자 한글 변환
|
|
73
|
+
숫자 단위를 분석하여 관습적인 한글 읽기 방식으로 변환합니다.
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
hangeul.num2han(1234567) # "백이십삼만사천오백육십칠"
|
|
77
|
+
hangeul.num2han(10000) # "만" (일만 -> 만 축약 적용)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 설치
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pip install hangeul
|
|
86
|
+
# or
|
|
87
|
+
uv add hangeul
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## 환경
|
|
91
|
+
* Python 3.10 이상 권장
|
|
92
|
+
* 의존성 없음 (Standard Library Only)
|
|
93
|
+
|
|
94
|
+
## 기여 및 문의
|
|
95
|
+
버그 리포트나 기능 제안은 이슈 트래커를 이용해 주세요. High Cohesion, Low Coupling 원칙을 준수하며 관리됩니다.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/hangeul/__init__.py
|
|
4
|
+
src/hangeul/check.py
|
|
5
|
+
src/hangeul/constants.py
|
|
6
|
+
src/hangeul/core.py
|
|
7
|
+
src/hangeul/josa.py
|
|
8
|
+
src/hangeul/number.py
|
|
9
|
+
src/hangeul/search.py
|
|
10
|
+
src/hangeul/similarity.py
|
|
11
|
+
src/hangeul.egg-info/PKG-INFO
|
|
12
|
+
src/hangeul.egg-info/SOURCES.txt
|
|
13
|
+
src/hangeul.egg-info/dependency_links.txt
|
|
14
|
+
src/hangeul.egg-info/requires.txt
|
|
15
|
+
src/hangeul.egg-info/top_level.txt
|
|
16
|
+
tests/test_check.py
|
|
17
|
+
tests/test_core.py
|
|
18
|
+
tests/test_josa.py
|
|
19
|
+
tests/test_num2han.py
|
|
20
|
+
tests/test_search.py
|
|
21
|
+
tests/test_similarity.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pytest>=9.0.3
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
hangeul
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import unicodedata
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from hangeul.check import (
|
|
6
|
+
is_han, is_pure_han, has_batchim, can_cho, can_jung, can_jong, normalize
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_is_pure_han():
|
|
11
|
+
assert is_pure_han("가나다") is True
|
|
12
|
+
assert is_pure_han("ㄱㄴㄷ") is True
|
|
13
|
+
assert is_pure_han("가나12") is False
|
|
14
|
+
assert is_pure_han("Hello") is False
|
|
15
|
+
|
|
16
|
+
def test_is_han_options():
|
|
17
|
+
# 공백 허용 테스트
|
|
18
|
+
assert is_han("가나 다", allow_space=True) is True
|
|
19
|
+
assert is_han("가나 다", allow_space=False) is False
|
|
20
|
+
|
|
21
|
+
# 문장 부호 허용 테스트
|
|
22
|
+
assert is_han("안녕!", allow_punctuation=True) is True
|
|
23
|
+
assert is_han("안녕!", allow_punctuation=False) is False
|
|
24
|
+
|
|
25
|
+
# 혼합 테스트
|
|
26
|
+
assert is_han("반가워, 친구야!", allow_space=True, allow_punctuation=True) is True
|
|
27
|
+
|
|
28
|
+
def test_has_batchim():
|
|
29
|
+
assert has_batchim("강") is True
|
|
30
|
+
assert has_batchim("가") is False
|
|
31
|
+
assert has_batchim(" ") is False
|
|
32
|
+
assert has_batchim("ㄱ") is False
|
|
33
|
+
|
|
34
|
+
def test_can_functions():
|
|
35
|
+
assert can_cho("ㄱ") is True
|
|
36
|
+
assert can_cho("ㅏ") is False
|
|
37
|
+
assert can_jung("ㅏ") is True
|
|
38
|
+
assert can_jong("ㄳ") is True
|
|
39
|
+
assert can_jong("ㄸ") is False
|
|
40
|
+
|
|
41
|
+
def test_normalize():
|
|
42
|
+
nfd_text = unicodedata.normalize('NFD', '한글')
|
|
43
|
+
# 눈으로 보기엔 같지만 길이는 다름 (ㅎ, ㅏ, ㄴ, ㄱ, ㅡ, ㄹ = 6)
|
|
44
|
+
assert len(nfd_text) == 6
|
|
45
|
+
# NFC로 정규화
|
|
46
|
+
nfc_text = normalize(nfd_text, form='NFC')
|
|
47
|
+
assert len(nfc_text) == 2
|
|
48
|
+
assert nfc_text == '한글'
|
|
49
|
+
|
|
50
|
+
nfc_text = '한글'
|
|
51
|
+
nfd_text = normalize(nfc_text, form='NFD')
|
|
52
|
+
assert len(nfd_text) == 6
|
|
53
|
+
|
|
54
|
+
with pytest.raises(Exception):
|
|
55
|
+
normalize(123)
|
|
56
|
+
normalize(None)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from hangeul.core import assemble, disassemble, qwerty2han
|
|
2
|
+
|
|
3
|
+
def test_qwerty2han():
|
|
4
|
+
assert qwerty2han("gksrmf") == "ㅎㅏㄴㄱㅡㄹ"
|
|
5
|
+
assert qwerty2han("R r") == "ㄲ ㄱ"
|
|
6
|
+
assert qwerty2han("123!@#") == "123!@#"
|
|
7
|
+
|
|
8
|
+
def test_assemble_basic():
|
|
9
|
+
assert assemble("ㄱㅏ") == "가"
|
|
10
|
+
assert assemble("ㄱㅏㄴ") == "간"
|
|
11
|
+
assert assemble("ㄱㅏㅂㅅ") == "값"
|
|
12
|
+
assert assemble("ㅗㅏ") == "ㅘ"
|
|
13
|
+
|
|
14
|
+
def test_assemble_liaison():
|
|
15
|
+
"""연음 법칙 및 겹받침 분리 테스트"""
|
|
16
|
+
assert assemble("ㄱㅏㅂㅅㅇㅣ") == "값이"
|
|
17
|
+
assert assemble("ㄱㅏㅂㅅㅣ") == "갑시"
|
|
18
|
+
assert assemble("ㅇㅏㄴㅈㅇㅏ") == "앉아"
|
|
19
|
+
assert assemble("ㅇㅗㅐㅇㅏㄴㄷㅗㅣ") == "왜안되"
|
|
20
|
+
|
|
21
|
+
def test_disassemble():
|
|
22
|
+
assert disassemble("한글") == "ㅎㅏㄴㄱㅡㄹ"
|
|
23
|
+
assert disassemble("값") == "ㄱㅏㅂㅅ"
|
|
24
|
+
assert disassemble("과") == "ㄱㅗㅏ"
|
|
25
|
+
assert disassemble("왜") == "ㅇㅗㅐ"
|
|
26
|
+
|
|
27
|
+
def test_round_trip():
|
|
28
|
+
"""조합 후 분해했을 때 원본과 동일한지 확인"""
|
|
29
|
+
targets = ["한글", "값이", "앉아", "도전", "학교"]
|
|
30
|
+
for t in targets:
|
|
31
|
+
assert assemble(disassemble(t)) == t
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from hangeul.josa import attach, get_josa
|
|
2
|
+
|
|
3
|
+
def test_josa_basic():
|
|
4
|
+
assert attach("사과", "이/가") == "사과가"
|
|
5
|
+
assert attach("수박", "이/가") == "수박이"
|
|
6
|
+
assert attach("바다", "은/는") == "바다는"
|
|
7
|
+
assert attach("강", "은/는") == "강은"
|
|
8
|
+
|
|
9
|
+
def test_josa_ro_exception():
|
|
10
|
+
"""'로/으로'의 ㄹ 받침 예외 테스트"""
|
|
11
|
+
assert attach("강", "으로/로") == "강으로"
|
|
12
|
+
assert attach("물", "으로/로") == "물로"
|
|
13
|
+
assert attach("길", "으로/로") == "길로"
|
|
14
|
+
|
|
15
|
+
def test_josa_number():
|
|
16
|
+
"""숫자 발음 기반 조사 테스트"""
|
|
17
|
+
assert attach("1", "이/가") == "1이" # 일-이
|
|
18
|
+
assert attach("2", "이/가") == "2가" # 이-가
|
|
19
|
+
assert attach("3", "은/는") == "3은" # 삼-은
|
|
20
|
+
assert attach("7", "로/으로") == "7로" # 칠-로 (ㄹ받침)
|
|
21
|
+
assert attach("8", "로/으로") == "8로" # 팔-로 (ㄹ받침)
|
|
22
|
+
assert attach("10", "을/를") == "10을" # 십-을
|
|
23
|
+
|
|
24
|
+
def test_josa_alphabet_acronym():
|
|
25
|
+
"""영문 대문자 약어 테스트"""
|
|
26
|
+
assert attach("IBM", "이/가") == "IBM이" # 아이비엠(ㅁ받침)
|
|
27
|
+
assert attach("AI", "이/가") == "AI가" # 에이아이(받침없음)
|
|
28
|
+
assert attach("SDK", "은/는") == "SDK는" # 에스디케이(받침없음)
|
|
29
|
+
assert attach("SQL", "로/으로") == "SQL로" # 에스큐엘(ㄹ받침)
|
|
30
|
+
|
|
31
|
+
def test_josa_parentheses():
|
|
32
|
+
"""괄호 처리 테스트"""
|
|
33
|
+
assert attach("애플(Apple)", "이/가") == "애플(Apple)이"
|
|
34
|
+
assert attach("삼성(Samsung)", "은/는") == "삼성(Samsung)은"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from hangeul.number import num2han
|
|
3
|
+
|
|
4
|
+
def test_num2han():
|
|
5
|
+
# 기본 변환
|
|
6
|
+
assert num2han(1234) == "천이백삼십사"
|
|
7
|
+
assert num2han(1010) == "천십"
|
|
8
|
+
|
|
9
|
+
# 만 단위 축약 (일만 -> 만)
|
|
10
|
+
assert num2han(10000) == "만"
|
|
11
|
+
assert num2han(15000) == "만오천"
|
|
12
|
+
|
|
13
|
+
# 억 단위 (일억 유지)
|
|
14
|
+
assert num2han(100000000) == "일억"
|
|
15
|
+
|
|
16
|
+
# 큰 숫자 및 0 처리
|
|
17
|
+
assert num2han(0) == "영"
|
|
18
|
+
assert num2han(200010) == "이십만십"
|
|
19
|
+
|
|
20
|
+
# 에러 처리
|
|
21
|
+
with pytest.raises(ValueError):
|
|
22
|
+
num2han("123a")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from hangeul.search import matches, search_filter
|
|
2
|
+
|
|
3
|
+
def test_chosung_matches():
|
|
4
|
+
target = "서강대학교"
|
|
5
|
+
assert matches(target, "ㅅㄱㄷ") is True
|
|
6
|
+
assert matches(target, "ㅅㄱㄷㅎㄱ") is True
|
|
7
|
+
assert matches(target, "ㅎㅇㄷ") is False
|
|
8
|
+
|
|
9
|
+
def test_jaso_matches():
|
|
10
|
+
target = "한글"
|
|
11
|
+
assert matches(target, "ㅎㅏㄴ") is True
|
|
12
|
+
assert matches(target, "ㅎㅏㄴㄱㅡㄹ") is True
|
|
13
|
+
assert matches(target, "ㅎㅡㄹ") is False
|
|
14
|
+
|
|
15
|
+
def test_search_filter():
|
|
16
|
+
list_data = ["사과", "수박", "복숭아", "포도"]
|
|
17
|
+
assert search_filter(list_data, "ㅅㅂ") == ["수박"]
|
|
18
|
+
assert search_filter(list_data, "ㅅ") == ["사과", "수박", "복숭아"]
|