Spaces:
Sleeping
Sleeping
Upload 2 files
Browse files
src/korean_sentence_splitter.py
ADDED
|
@@ -0,0 +1,601 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
Korean Sentence Splitter (한글 문장 분리기)
|
| 5 |
+
==========================================
|
| 6 |
+
|
| 7 |
+
정규표현식 기반 한국어 문장 분리 라이브러리.
|
| 8 |
+
KSS(Korean Sentence Splitter) 등 기존 연구를 참고하여 구현.
|
| 9 |
+
|
| 10 |
+
주요 특징:
|
| 11 |
+
- 종결어미 기반 문장 분리
|
| 12 |
+
- 구두점(마침표, 느낌표, 물음표 등) 처리
|
| 13 |
+
- 괄호/따옴표 내부 문장 보호
|
| 14 |
+
- 기사 제목 스타일 (명사 종결) 지원
|
| 15 |
+
- 약어 및 숫자 패턴 예외 처리
|
| 16 |
+
|
| 17 |
+
References:
|
| 18 |
+
- https://github.com/hyunwoongko/kss
|
| 19 |
+
- https://github.com/likejazz/korean-sentence-splitter
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
import re
|
| 23 |
+
from typing import List, Optional, Tuple
|
| 24 |
+
from dataclasses import dataclass
|
| 25 |
+
from enum import Enum
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class SplitMode(Enum):
|
| 29 |
+
"""문장 분리 모드"""
|
| 30 |
+
PUNCT_ONLY = "punct" # 구두점 기반만
|
| 31 |
+
NORMAL = "normal" # 일반 모드 (종결어미 + 구두점)
|
| 32 |
+
AGGRESSIVE = "aggressive" # 공격적 분리
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@dataclass
|
| 36 |
+
class SplitterConfig:
|
| 37 |
+
"""문장 분리기 설정"""
|
| 38 |
+
mode: SplitMode = SplitMode.NORMAL
|
| 39 |
+
strip: bool = True
|
| 40 |
+
min_length: int = 2
|
| 41 |
+
preserve_quotes: bool = True
|
| 42 |
+
preserve_brackets: bool = True
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class KoreanSentenceSplitter:
|
| 46 |
+
"""
|
| 47 |
+
한국어 문장 분리기
|
| 48 |
+
|
| 49 |
+
Usage:
|
| 50 |
+
splitter = KoreanSentenceSplitter()
|
| 51 |
+
sentences = splitter.split("안녕하세요. 반갑습니다!")
|
| 52 |
+
# ['안녕하세요.', '반갑습니다!']
|
| 53 |
+
"""
|
| 54 |
+
|
| 55 |
+
# ==================== 종결어미 패턴 ====================
|
| 56 |
+
|
| 57 |
+
# 평서형 종결어미 (Declarative)
|
| 58 |
+
DECLARATIVE_ENDINGS = [
|
| 59 |
+
# 격식체 (Formal) - 합쇼체
|
| 60 |
+
'습니다', '입니다', 'ㅂ니다',
|
| 61 |
+
# 비격식체 (Informal) - 해요체
|
| 62 |
+
'어요', '아요', '여요', '이에요', '예요', '에요', '세요', '셔요',
|
| 63 |
+
'죠', '지요',
|
| 64 |
+
# 해라체 (Plain)
|
| 65 |
+
'한다', '인다', '는다', '운다', '른다',
|
| 66 |
+
'었다', '았다', '였다', '겠다',
|
| 67 |
+
'더라', '더군',
|
| 68 |
+
# 해체 (Casual)
|
| 69 |
+
'해', '야', '네', '군', '구나', '구먼',
|
| 70 |
+
# 기타
|
| 71 |
+
'거든', '거든요', '답니다', '랍니다',
|
| 72 |
+
'데요', '래요', '대요',
|
| 73 |
+
]
|
| 74 |
+
|
| 75 |
+
# 의문형 종결어미 (Interrogative)
|
| 76 |
+
INTERROGATIVE_ENDINGS = [
|
| 77 |
+
'습니까', '입니까', 'ㅂ니까',
|
| 78 |
+
'나요', '가요',
|
| 79 |
+
'니', '냐', '나', '까',
|
| 80 |
+
'은가', '는가', '던가', '을까',
|
| 81 |
+
'을까요',
|
| 82 |
+
'지요', '죠', '지', '잖아', '잖아요',
|
| 83 |
+
]
|
| 84 |
+
|
| 85 |
+
# 명령형 종결어미 (Imperative)
|
| 86 |
+
IMPERATIVE_ENDINGS = [
|
| 87 |
+
'십시오', '세요', '셔요',
|
| 88 |
+
'아라', '어라', '여라', '거라',
|
| 89 |
+
'렴', '려무나',
|
| 90 |
+
]
|
| 91 |
+
|
| 92 |
+
# 청유형 종결어미 (Propositive)
|
| 93 |
+
PROPOSITIVE_ENDINGS = [
|
| 94 |
+
'읍시다', 'ㅂ시다',
|
| 95 |
+
'자', '자요',
|
| 96 |
+
]
|
| 97 |
+
|
| 98 |
+
# 감탄형 종결어미 (Exclamatory)
|
| 99 |
+
EXCLAMATORY_ENDINGS = [
|
| 100 |
+
'구나', '군', '네', '로구나', '는군', '구먼',
|
| 101 |
+
'도다', '로다',
|
| 102 |
+
]
|
| 103 |
+
|
| 104 |
+
# 연결어미 (분리하면 안됨) - 더 포괄적인 목록
|
| 105 |
+
CONNECTIVE_ENDINGS = [
|
| 106 |
+
'는데', '은데', 'ㄴ데',
|
| 107 |
+
'지만', '으나', '나',
|
| 108 |
+
'면서', '으면서', '며', '으며',
|
| 109 |
+
'고', '고서',
|
| 110 |
+
'니까', '으니까',
|
| 111 |
+
'어서', '아서', '여서',
|
| 112 |
+
'려고', '으려고', '러',
|
| 113 |
+
'다가', '었다가', '았다가',
|
| 114 |
+
'도록', '게',
|
| 115 |
+
'자마자',
|
| 116 |
+
'거나', '든지', '든가',
|
| 117 |
+
'려', '으려', '야', '어야', '아야', # 조건/의도
|
| 118 |
+
]
|
| 119 |
+
|
| 120 |
+
# 구두점 패턴
|
| 121 |
+
PUNCT_PATTERN = r'[.!?。!?]'
|
| 122 |
+
|
| 123 |
+
# 약어 패턴
|
| 124 |
+
ABBREV_PATTERNS = [
|
| 125 |
+
r'\d+\.', # 숫자.
|
| 126 |
+
r'[A-Za-z]+\.', # 영문.
|
| 127 |
+
r'\d+\.\d+', # 소수점
|
| 128 |
+
r'\.{2,}', # 연속 마침표
|
| 129 |
+
]
|
| 130 |
+
|
| 131 |
+
# 이모지 패턴
|
| 132 |
+
EMOJI_PATTERN = re.compile(
|
| 133 |
+
"["
|
| 134 |
+
"\U0001F600-\U0001F64F"
|
| 135 |
+
"\U0001F300-\U0001F5FF"
|
| 136 |
+
"\U0001F680-\U0001F6FF"
|
| 137 |
+
"\U0001F1E0-\U0001F1FF"
|
| 138 |
+
"\U00002702-\U000027B0"
|
| 139 |
+
"\U000024C2-\U0001F251"
|
| 140 |
+
"]+",
|
| 141 |
+
flags=re.UNICODE
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
# 한글 패턴
|
| 145 |
+
HANGUL_PATTERN = r'[\uAC00-\uD7A3\u1100-\u11FF\u3130-\u318F]'
|
| 146 |
+
|
| 147 |
+
def __init__(self, config: Optional[SplitterConfig] = None):
|
| 148 |
+
self.config = config or SplitterConfig()
|
| 149 |
+
self._compile_patterns()
|
| 150 |
+
|
| 151 |
+
def _compile_patterns(self):
|
| 152 |
+
"""정규표현식 패턴 컴파일"""
|
| 153 |
+
# 모든 종결어미 결합 (길이순 정렬)
|
| 154 |
+
all_endings = (
|
| 155 |
+
self.DECLARATIVE_ENDINGS +
|
| 156 |
+
self.INTERROGATIVE_ENDINGS +
|
| 157 |
+
self.IMPERATIVE_ENDINGS +
|
| 158 |
+
self.PROPOSITIVE_ENDINGS +
|
| 159 |
+
self.EXCLAMATORY_ENDINGS
|
| 160 |
+
)
|
| 161 |
+
all_endings = sorted(set(all_endings), key=len, reverse=True)
|
| 162 |
+
|
| 163 |
+
# 종결어미 패턴
|
| 164 |
+
endings_str = '|'.join(re.escape(e) for e in all_endings)
|
| 165 |
+
self.ending_pattern = re.compile(rf'({endings_str})$', re.UNICODE)
|
| 166 |
+
|
| 167 |
+
# 연결어미 패턴
|
| 168 |
+
conn_str = '|'.join(re.escape(e) for e in self.CONNECTIVE_ENDINGS)
|
| 169 |
+
self.connective_pattern = re.compile(rf'({conn_str})$', re.UNICODE)
|
| 170 |
+
|
| 171 |
+
# 구두점 패턴
|
| 172 |
+
self.punct_re = re.compile(self.PUNCT_PATTERN)
|
| 173 |
+
|
| 174 |
+
# 약어 패턴
|
| 175 |
+
self.abbrev_pattern = re.compile('|'.join(self.ABBREV_PATTERNS))
|
| 176 |
+
|
| 177 |
+
def split(self, text: str) -> List[str]:
|
| 178 |
+
"""텍스트를 문장 단위로 분리"""
|
| 179 |
+
if not text or not text.strip():
|
| 180 |
+
return []
|
| 181 |
+
|
| 182 |
+
# 전처리
|
| 183 |
+
text = re.sub(r'\s+', ' ', text).strip()
|
| 184 |
+
|
| 185 |
+
# 보호 영역 처리
|
| 186 |
+
text, protected = self._protect_regions(text)
|
| 187 |
+
|
| 188 |
+
# 분리 수행
|
| 189 |
+
if self.config.mode == SplitMode.PUNCT_ONLY:
|
| 190 |
+
sentences = self._split_punct_only(text, protected)
|
| 191 |
+
else:
|
| 192 |
+
sentences = self._split_with_endings(text, protected)
|
| 193 |
+
|
| 194 |
+
# 보호 영역 복원
|
| 195 |
+
sentences = self._restore_regions(sentences, protected)
|
| 196 |
+
|
| 197 |
+
# 후처리
|
| 198 |
+
return self._postprocess(sentences)
|
| 199 |
+
|
| 200 |
+
def _protect_regions(self, text: str) -> Tuple[str, dict]:
|
| 201 |
+
"""괄호/따옴표 내부 보호"""
|
| 202 |
+
protected = {}
|
| 203 |
+
counter = 0
|
| 204 |
+
|
| 205 |
+
def replace_fn(match):
|
| 206 |
+
nonlocal counter
|
| 207 |
+
token = f"__P{counter}__"
|
| 208 |
+
protected[token] = match.group(0)
|
| 209 |
+
counter += 1
|
| 210 |
+
return token
|
| 211 |
+
|
| 212 |
+
if self.config.preserve_quotes:
|
| 213 |
+
# 큰따옴표
|
| 214 |
+
text = re.sub(r'"[^"]*"', replace_fn, text)
|
| 215 |
+
text = re.sub(r'\u201C[^\u201D]*\u201D', replace_fn, text) # ""
|
| 216 |
+
text = re.sub(r'\u300C[^\u300D]*\u300D', replace_fn, text) # 「」
|
| 217 |
+
text = re.sub(r'\u300E[^\u300F]*\u300F', replace_fn, text) # 『』
|
| 218 |
+
# 작은따옴표
|
| 219 |
+
text = re.sub(r"'[^']*'", replace_fn, text)
|
| 220 |
+
|
| 221 |
+
if self.config.preserve_brackets:
|
| 222 |
+
text = re.sub(r'\([^)]*\)', replace_fn, text)
|
| 223 |
+
text = re.sub(r'\[[^\]]*\]', replace_fn, text)
|
| 224 |
+
|
| 225 |
+
return text, protected
|
| 226 |
+
|
| 227 |
+
def _restore_regions(self, sentences: List[str], protected: dict) -> List[str]:
|
| 228 |
+
"""보호 영역 복원"""
|
| 229 |
+
result = []
|
| 230 |
+
for sent in sentences:
|
| 231 |
+
for token, original in protected.items():
|
| 232 |
+
sent = sent.replace(token, original)
|
| 233 |
+
result.append(sent)
|
| 234 |
+
return result
|
| 235 |
+
|
| 236 |
+
def _split_punct_only(self, text: str, protected: dict = None) -> List[str]:
|
| 237 |
+
"""구두점 기반 분리"""
|
| 238 |
+
# 약어 보호
|
| 239 |
+
text, abbrev_map = self._protect_abbrevs(text)
|
| 240 |
+
|
| 241 |
+
# 구두점으로 분리
|
| 242 |
+
parts = re.split(rf'({self.PUNCT_PATTERN}+\s*)', text)
|
| 243 |
+
|
| 244 |
+
sentences = []
|
| 245 |
+
current = ""
|
| 246 |
+
for part in parts:
|
| 247 |
+
current += part
|
| 248 |
+
if self.punct_re.search(part):
|
| 249 |
+
sentences.append(current.strip())
|
| 250 |
+
current = ""
|
| 251 |
+
# 보호 구문(토큰) 뒤의 공백에서도 분리 체크
|
| 252 |
+
elif protected and re.search(r'(__P\d+__)\s*$', current):
|
| 253 |
+
token_match = re.search(r'(__P\d+__)', current)
|
| 254 |
+
if token_match:
|
| 255 |
+
original = protected.get(token_match.group(1), "")
|
| 256 |
+
if self.punct_re.search(original[-2:]):
|
| 257 |
+
sentences.append(current.strip())
|
| 258 |
+
current = ""
|
| 259 |
+
|
| 260 |
+
if current.strip():
|
| 261 |
+
sentences.append(current.strip())
|
| 262 |
+
|
| 263 |
+
# 약어 복원
|
| 264 |
+
return self._restore_abbrevs(sentences, abbrev_map)
|
| 265 |
+
|
| 266 |
+
def _split_with_endings(self, text: str, protected: dict = None) -> List[str]:
|
| 267 |
+
"""종결어미 + 구두점 기반 분리"""
|
| 268 |
+
# 약어 보호
|
| 269 |
+
text, abbrev_map = self._protect_abbrevs(text)
|
| 270 |
+
|
| 271 |
+
sentences = []
|
| 272 |
+
current = ""
|
| 273 |
+
|
| 274 |
+
i = 0
|
| 275 |
+
while i < len(text):
|
| 276 |
+
char = text[i]
|
| 277 |
+
current += char
|
| 278 |
+
|
| 279 |
+
# 구두점 체크
|
| 280 |
+
if self.punct_re.match(char):
|
| 281 |
+
# 연속 구두점 모두 포함 (예: ?!, !!, ...)
|
| 282 |
+
while i + 1 < len(text) and self.punct_re.match(text[i + 1]):
|
| 283 |
+
i += 1
|
| 284 |
+
current += text[i]
|
| 285 |
+
|
| 286 |
+
# 이모지 포함
|
| 287 |
+
while i + 1 < len(text) and self.EMOJI_PATTERN.match(text[i + 1]):
|
| 288 |
+
i += 1
|
| 289 |
+
current += text[i]
|
| 290 |
+
|
| 291 |
+
# 공백까지 포함
|
| 292 |
+
if i + 1 < len(text) and text[i + 1] in ' \t':
|
| 293 |
+
i += 1
|
| 294 |
+
current += text[i]
|
| 295 |
+
|
| 296 |
+
# 약어가 아니면 분리
|
| 297 |
+
if not self._is_abbrev(current.rstrip()):
|
| 298 |
+
sentences.append(current)
|
| 299 |
+
current = ""
|
| 300 |
+
|
| 301 |
+
# 종결어미 체크 (공백 앞)
|
| 302 |
+
elif char == ' ' and len(current) > 2:
|
| 303 |
+
check_text = current.rstrip()
|
| 304 |
+
if self._is_sentence_ending(check_text, protected):
|
| 305 |
+
sentences.append(current)
|
| 306 |
+
current = ""
|
| 307 |
+
|
| 308 |
+
i += 1
|
| 309 |
+
|
| 310 |
+
if current.strip():
|
| 311 |
+
sentences.append(current)
|
| 312 |
+
|
| 313 |
+
# 약어 복원
|
| 314 |
+
return self._restore_abbrevs(sentences, abbrev_map)
|
| 315 |
+
|
| 316 |
+
def _is_sentence_ending(self, text: str, protected: dict = None) -> bool:
|
| 317 |
+
"""종결어미로 끝나는지 확인 (보호 토큰 처리 포함)"""
|
| 318 |
+
if not text:
|
| 319 |
+
return False
|
| 320 |
+
|
| 321 |
+
# 보호 영역(토큰) 체크
|
| 322 |
+
token_match = re.search(r'(__P\d+__)$', text)
|
| 323 |
+
if token_match and protected:
|
| 324 |
+
token = token_match.group(1)
|
| 325 |
+
original = protected.get(token, "")
|
| 326 |
+
if not original:
|
| 327 |
+
return False
|
| 328 |
+
|
| 329 |
+
# 따옴표/괄호 내부 텍스트 추출
|
| 330 |
+
inner = original.strip()
|
| 331 |
+
# 1. 구두점으로 끝나는지 확인 (예: "안녕하세요.")
|
| 332 |
+
if self.punct_re.search(inner[-2:]):
|
| 333 |
+
return True
|
| 334 |
+
|
| 335 |
+
# 2. 종결어미로 끝나는지 확인 (따옴표 제거 후)
|
| 336 |
+
stripped_inner = inner.strip('\'"\"“”‘’「」『』()[]')
|
| 337 |
+
if stripped_inner and self._is_sentence_ending(stripped_inner):
|
| 338 |
+
return True
|
| 339 |
+
return False
|
| 340 |
+
|
| 341 |
+
if len(text) < 2:
|
| 342 |
+
return False
|
| 343 |
+
|
| 344 |
+
# 마지막 단어만 추출 (공백 기준)
|
| 345 |
+
words = text.split()
|
| 346 |
+
if not words:
|
| 347 |
+
return False
|
| 348 |
+
last_word = words[-1]
|
| 349 |
+
|
| 350 |
+
# 한 글자 단어는 종결어미로 판단하지 않음
|
| 351 |
+
if len(last_word) == 1:
|
| 352 |
+
return False
|
| 353 |
+
|
| 354 |
+
# 짧은 종결어미 (1글자) - 명사와 혼동되기 쉬움
|
| 355 |
+
# 이 경우 더 엄격한 검증 필요
|
| 356 |
+
SHORT_ENDINGS = ['자', '해', '야', '네', '군', '니', '냐', '나', '까']
|
| 357 |
+
|
| 358 |
+
# 긴 종결어미 (2글자 이상) - 신뢰도 높음
|
| 359 |
+
LONG_ENDINGS = [e for e in (
|
| 360 |
+
self.DECLARATIVE_ENDINGS +
|
| 361 |
+
self.INTERROGATIVE_ENDINGS +
|
| 362 |
+
self.IMPERATIVE_ENDINGS +
|
| 363 |
+
self.PROPOSITIVE_ENDINGS +
|
| 364 |
+
self.EXCLAMATORY_ENDINGS
|
| 365 |
+
) if len(e) >= 2]
|
| 366 |
+
|
| 367 |
+
# 연결어미로 끝나면 False
|
| 368 |
+
for conn in sorted(self.CONNECTIVE_ENDINGS, key=len, reverse=True):
|
| 369 |
+
if last_word.endswith(conn) and len(last_word) > len(conn):
|
| 370 |
+
return False
|
| 371 |
+
|
| 372 |
+
# 긴 종결어미 체크 (2글자 이상) - 우선 검사
|
| 373 |
+
for ending in sorted(LONG_ENDINGS, key=len, reverse=True):
|
| 374 |
+
if last_word.endswith(ending) and len(last_word) > len(ending):
|
| 375 |
+
return True
|
| 376 |
+
|
| 377 |
+
# 짧은 종결어미 (1글자) - 앞 글자가 용언 어간일 가능성이 높은지 확인
|
| 378 |
+
# 한국어 용언 어간은 대체로 모음으로 끝나지 않음 (받침 있음)
|
| 379 |
+
# 또는 특정 패턴의 어미 활용형
|
| 380 |
+
for ending in SHORT_ENDINGS:
|
| 381 |
+
if last_word.endswith(ending) and len(last_word) >= 2:
|
| 382 |
+
# 어간 추출
|
| 383 |
+
stem = last_word[:-len(ending)]
|
| 384 |
+
if len(stem) == 0:
|
| 385 |
+
continue
|
| 386 |
+
|
| 387 |
+
# 어간 마지막 글자의 받침 확인
|
| 388 |
+
last_stem_char = stem[-1]
|
| 389 |
+
if '\uAC00' <= last_stem_char <= '\uD7A3': # 완성형 한글
|
| 390 |
+
# 받침 여부 확인 (종성이 있는지)
|
| 391 |
+
code = ord(last_stem_char) - 0xAC00
|
| 392 |
+
jongsung = code % 28
|
| 393 |
+
|
| 394 |
+
# 동사/형용사 어간으로 보이는 패턴
|
| 395 |
+
# 예: 먹 + 자 = 먹자 (받침 있음)
|
| 396 |
+
# 가 + 자 = 가자 (받침 없지만 동사)
|
| 397 |
+
# 하 + 자 = 하자 (하다 동사)
|
| 398 |
+
|
| 399 |
+
# "하"로 끝나면 "하다" 동사일 가능성 높음
|
| 400 |
+
if stem.endswith('하') or stem.endswith('되'):
|
| 401 |
+
return True
|
| 402 |
+
|
| 403 |
+
# 받침이 있으면 동사 어간일 가능성 높음
|
| 404 |
+
if jongsung > 0:
|
| 405 |
+
return True
|
| 406 |
+
|
| 407 |
+
# 받침 없는 경우: "가자", "보자" 등 기본 동사
|
| 408 |
+
# 하지만 "피자"와 구분하기 어려움
|
| 409 |
+
# 2글자 단어 + 짧은 종결어미는 보수적으로 처리
|
| 410 |
+
if len(last_word) <= 2:
|
| 411 |
+
return False
|
| 412 |
+
|
| 413 |
+
# 3글자 이상이면 종결어미일 가능성 있음
|
| 414 |
+
return True
|
| 415 |
+
|
| 416 |
+
return False
|
| 417 |
+
|
| 418 |
+
def _protect_abbrevs(self, text: str) -> Tuple[str, dict]:
|
| 419 |
+
"""약어 보호"""
|
| 420 |
+
abbrev_map = {}
|
| 421 |
+
counter = 0
|
| 422 |
+
|
| 423 |
+
def replace_fn(match):
|
| 424 |
+
nonlocal counter
|
| 425 |
+
token = f"__A{counter}__"
|
| 426 |
+
abbrev_map[token] = match.group(0)
|
| 427 |
+
counter += 1
|
| 428 |
+
return token
|
| 429 |
+
|
| 430 |
+
text = self.abbrev_pattern.sub(replace_fn, text)
|
| 431 |
+
return text, abbrev_map
|
| 432 |
+
|
| 433 |
+
def _restore_abbrevs(self, sentences: List[str], abbrev_map: dict) -> List[str]:
|
| 434 |
+
"""약어 복원"""
|
| 435 |
+
result = []
|
| 436 |
+
for sent in sentences:
|
| 437 |
+
for token, original in abbrev_map.items():
|
| 438 |
+
sent = sent.replace(token, original)
|
| 439 |
+
result.append(sent)
|
| 440 |
+
return result
|
| 441 |
+
|
| 442 |
+
def _is_abbrev(self, text: str) -> bool:
|
| 443 |
+
"""약어 여부"""
|
| 444 |
+
if len(text) < 2:
|
| 445 |
+
return False
|
| 446 |
+
last_part = text[-10:] if len(text) > 10 else text
|
| 447 |
+
return bool(self.abbrev_pattern.search(last_part))
|
| 448 |
+
|
| 449 |
+
def _postprocess(self, sentences: List[str]) -> List[str]:
|
| 450 |
+
"""후처리"""
|
| 451 |
+
result = []
|
| 452 |
+
for sent in sentences:
|
| 453 |
+
if self.config.strip:
|
| 454 |
+
sent = sent.strip()
|
| 455 |
+
if len(sent) >= self.config.min_length:
|
| 456 |
+
result.append(sent)
|
| 457 |
+
return result
|
| 458 |
+
|
| 459 |
+
def split_with_type(self, text: str) -> List[dict]:
|
| 460 |
+
"""문장 분리 + 유형 분석"""
|
| 461 |
+
sentences = self.split(text)
|
| 462 |
+
result = []
|
| 463 |
+
|
| 464 |
+
for sent in sentences:
|
| 465 |
+
sent_type = self._detect_type(sent)
|
| 466 |
+
result.append({
|
| 467 |
+
'text': sent,
|
| 468 |
+
'type': sent_type
|
| 469 |
+
})
|
| 470 |
+
|
| 471 |
+
return result
|
| 472 |
+
|
| 473 |
+
def _detect_type(self, sent: str) -> str:
|
| 474 |
+
"""문장 유형 판단"""
|
| 475 |
+
sent = sent.rstrip()
|
| 476 |
+
|
| 477 |
+
if sent.endswith('?') or sent.endswith('\uFF1F'):
|
| 478 |
+
return 'interrogative'
|
| 479 |
+
if sent.endswith('!') or sent.endswith('\uFF01'):
|
| 480 |
+
return 'exclamatory'
|
| 481 |
+
|
| 482 |
+
for e in self.INTERROGATIVE_ENDINGS:
|
| 483 |
+
if sent.endswith(e):
|
| 484 |
+
return 'interrogative'
|
| 485 |
+
|
| 486 |
+
for e in self.IMPERATIVE_ENDINGS:
|
| 487 |
+
if sent.endswith(e):
|
| 488 |
+
return 'imperative'
|
| 489 |
+
|
| 490 |
+
for e in self.PROPOSITIVE_ENDINGS:
|
| 491 |
+
if sent.endswith(e):
|
| 492 |
+
return 'propositive'
|
| 493 |
+
|
| 494 |
+
return 'declarative'
|
| 495 |
+
|
| 496 |
+
|
| 497 |
+
# ==================== 간편 함수 ====================
|
| 498 |
+
|
| 499 |
+
def split_sentences(text: str,
|
| 500 |
+
mode: str = "normal",
|
| 501 |
+
preserve_quotes: bool = True) -> List[str]:
|
| 502 |
+
"""
|
| 503 |
+
간편 문장 분리 함수
|
| 504 |
+
|
| 505 |
+
Args:
|
| 506 |
+
text: 입력 텍스트
|
| 507 |
+
mode: 'punct' (구두점만) / 'normal' (종결어미+구두점)
|
| 508 |
+
preserve_quotes: 따옴표 내부 보호
|
| 509 |
+
|
| 510 |
+
Returns:
|
| 511 |
+
분리된 문장 리스트
|
| 512 |
+
|
| 513 |
+
Example:
|
| 514 |
+
>>> split_sentences("안녕하세요. 반갑습니다!")
|
| 515 |
+
['안녕하세요.', '반갑습니다!']
|
| 516 |
+
"""
|
| 517 |
+
config = SplitterConfig(
|
| 518 |
+
mode=SplitMode(mode),
|
| 519 |
+
preserve_quotes=preserve_quotes
|
| 520 |
+
)
|
| 521 |
+
return KoreanSentenceSplitter(config).split(text)
|
| 522 |
+
|
| 523 |
+
|
| 524 |
+
# ==================== 테스트 ====================
|
| 525 |
+
|
| 526 |
+
def run_tests():
|
| 527 |
+
"""테스트 실행"""
|
| 528 |
+
print("=" * 60)
|
| 529 |
+
print("한글 문장 분리기 테스트")
|
| 530 |
+
print("=" * 60)
|
| 531 |
+
|
| 532 |
+
splitter = KoreanSentenceSplitter()
|
| 533 |
+
|
| 534 |
+
test_cases = [
|
| 535 |
+
# 기본 구두점
|
| 536 |
+
"안녕하세요. 반갑습니다!",
|
| 537 |
+
|
| 538 |
+
# 종결어미 (구두점 없음)
|
| 539 |
+
"오늘 날씨가 좋습니다 내일도 맑을 예정입니다",
|
| 540 |
+
|
| 541 |
+
# 의문문
|
| 542 |
+
"뭐 먹을까요? 저는 피자가 좋아요.",
|
| 543 |
+
|
| 544 |
+
# 따옴표 보호
|
| 545 |
+
'"안녕하세요. 반갑습니다." 라고 말했다.',
|
| 546 |
+
|
| 547 |
+
# 괄호 보호
|
| 548 |
+
"서울(대한민국의 수도. 인구 1000만)은 큰 도시입니다.",
|
| 549 |
+
|
| 550 |
+
# 숫자/소수점
|
| 551 |
+
"3.14는 파이입니다. 원주율이죠.",
|
| 552 |
+
|
| 553 |
+
# 연결어미 vs 종결어미
|
| 554 |
+
"비가 오는데 우산이 없어요 어떡하죠",
|
| 555 |
+
|
| 556 |
+
# 말줄임표
|
| 557 |
+
"그래서... 결국 성공했어요!",
|
| 558 |
+
|
| 559 |
+
# 복잡한 문장 (KSS 예제)
|
| 560 |
+
"회사 동료 분들과 다녀왔는데 분위기도 좋고 음식도 맛있었어요 다만, 강남 토끼정이 강남 쉑쉑버거 골목길로 쭉 올라가야 하는데 다들 쉑쉑버거의 유혹에 넘어갈 뻔 했답니다 강남역 맛집 토끼정의 외부 모습.",
|
| 561 |
+
|
| 562 |
+
# 구어체
|
| 563 |
+
"ㅋㅋㅋ 너무 웃겨 진짜 최고야",
|
| 564 |
+
|
| 565 |
+
# 명사 종결 (기사 제목)
|
| 566 |
+
"주가 급등. 투자자들 환호",
|
| 567 |
+
|
| 568 |
+
# 여러 구두점
|
| 569 |
+
"정말이야?! 믿을 수 없어!!",
|
| 570 |
+
|
| 571 |
+
# 해체
|
| 572 |
+
"오늘 뭐해 나 심심해 놀자",
|
| 573 |
+
|
| 574 |
+
# ���모지
|
| 575 |
+
"오늘 너무 행복해요😊 좋은 하루였어요!",
|
| 576 |
+
]
|
| 577 |
+
|
| 578 |
+
for i, text in enumerate(test_cases, 1):
|
| 579 |
+
print(f"\n[테스트 {i}]")
|
| 580 |
+
print(f"입력: {text}")
|
| 581 |
+
result = splitter.split(text)
|
| 582 |
+
print(f"결과: {result}")
|
| 583 |
+
|
| 584 |
+
# 문장 유형 분석 테스트
|
| 585 |
+
print("\n" + "=" * 60)
|
| 586 |
+
print("문장 유형 분석 테스트")
|
| 587 |
+
print("=" * 60)
|
| 588 |
+
|
| 589 |
+
type_test = "오늘 뭐 먹을까요? 저는 피자 먹고 싶어요. 같이 가자! 얼른 준비해."
|
| 590 |
+
results = splitter.split_with_type(type_test)
|
| 591 |
+
print(f"\n입력: {type_test}")
|
| 592 |
+
for r in results:
|
| 593 |
+
print(f" [{r['type']:15}] {r['text']}")
|
| 594 |
+
|
| 595 |
+
print("\n" + "=" * 60)
|
| 596 |
+
print("테스트 완료!")
|
| 597 |
+
print("=" * 60)
|
| 598 |
+
|
| 599 |
+
|
| 600 |
+
if __name__ == "__main__":
|
| 601 |
+
run_tests()
|
src/subjectless_predicates_122725_v2.py
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
언론 보도 객관성 측정을 위한 무주체 피동형 술어 정규표현식 (v2)
|
| 3 |
+
================================================================================
|
| 4 |
+
|
| 5 |
+
I. 객관성 의심 술어 (DOUBT): 기자 의견으로 여겨질 수 있는 무주체 주관적 술어
|
| 6 |
+
II. 객관성 지지 술어 (SUPPORT): 사실 확인/명시적 출처 기반 술어
|
| 7 |
+
|
| 8 |
+
* 무주체 피동형 술어: 발언/판단의 주체가 문장에 없어 기자 의견으로 읽힐 수 있는 표현
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import re
|
| 12 |
+
from typing import Dict, Pattern, List, Any
|
| 13 |
+
from korean_sentence_splitter import KoreanSentenceSplitter
|
| 14 |
+
|
| 15 |
+
# =============================================================================
|
| 16 |
+
# I. 객관성 의심 술어 (DOUBT):
|
| 17 |
+
# =============================================================================
|
| 18 |
+
|
| 19 |
+
DOUBT_PREDICATES: Dict[str, Pattern] = {
|
| 20 |
+
|
| 21 |
+
# =========================================================================
|
| 22 |
+
# 1. 분석/해석형
|
| 23 |
+
# =========================================================================
|
| 24 |
+
"분석형": re.compile(
|
| 25 |
+
r"(?:"
|
| 26 |
+
# --- 피동형 술어 ---
|
| 27 |
+
r"분석(?:된다|됩니다|됐다|됐습니다|되고\s*있다|되고\s*있습니다)|"
|
| 28 |
+
r"풀이(?:된다|됩니다|됐다|됐습니다|되고\s*있다|되고\s*있습니다)|"
|
| 29 |
+
r"해석(?:된다|됩니다|됐다|됐습니다|되고\s*있다|되고\s*있습니다)|"
|
| 30 |
+
r"진단(?:된다|됩니다|됐다|됐습니다|되고\s*있다|되고\s*있습니다)|"
|
| 31 |
+
r"읽(?:힌다|힙니다|혔다|혔습니다|히고\s*있다|히고\s*있습니다)|"
|
| 32 |
+
# --- 명사형 술어: 분석/풀이/해석/진단 ---
|
| 33 |
+
r"(?:분석|풀이|해석|진단)(?:이다|입니다|이었다|이었습니다|이에요)|"
|
| 34 |
+
r"(?:라는|다는|이라는|란|는|은|인)\s*(?:분석|풀이|해석|진단)(?:이다|입니다|이었다|이었습니다|이에요)|"
|
| 35 |
+
r"(?:라는|다는|이라는|란|는|은|인)?\s*(?:분석|풀이|해석|진단)(?:도\s*있다|도\s*있습니다|도\s*나온다|도\s*나옵니다|도\s*나오고\s*있다|도\s*나오고\s*있습니다)|"
|
| 36 |
+
r"(?:라는|다는|이라는|란|는|은|인)?\s*(?:분석|풀이|해석|진단)(?:이\s*나온다|이\s*나옵니다|이\s*나오고\s*있다|이\s*나오고\s*있습니다|이\s*나왔다|이\s*나왔습니다|이\s*지배적이다|이\s*지배적입니다)"
|
| 37 |
+
r")"
|
| 38 |
+
),
|
| 39 |
+
|
| 40 |
+
# =========================================================================
|
| 41 |
+
# 2. 전망/예측형
|
| 42 |
+
# =========================================================================
|
| 43 |
+
"전망형": re.compile(
|
| 44 |
+
r"(?:"
|
| 45 |
+
# --- 피동형 술어 ---
|
| 46 |
+
r"전망(?:된다|됩니다|됐다|됐습니다|되고\s*있다|되고\s*있습니다)|"
|
| 47 |
+
r"예상(?:된다|됩니다|됐다|됐습니다|되고\s*있다|되고\s*있습니다)|"
|
| 48 |
+
r"예측(?:된다|됩니다|됐다|됐습니다|되고\s*있다|되고\s*있습니다)|"
|
| 49 |
+
r"점쳐(?:진다|집니다|졌다|졌습니다|지고\s*있다|지고\s*있습니다)|"
|
| 50 |
+
r"예견(?:된다|됩니다|됐다|됐습니다|되고\s*있다|되고\s*있습니다)|"
|
| 51 |
+
# --- 명사형 술어 ---
|
| 52 |
+
r"(?:전망|예상|예측)(?:이다|입니다|이었다|이었습니다|이에요)|"
|
| 53 |
+
r"(?:라는|다는|이라는|란|는|은|인)\s*(?:전망|예상|예측|관측)(?:이다|입니다|이었다|이었습니다|이에요)|"
|
| 54 |
+
r"(?:라는|다는|이라는|란|는|은|인)?\s*(?:전망|예상|예측|관측)(?:도\s*있다|도\s*있습니다|도\s*나온다|도\s*나옵니다|도\s*나오고\s*있다|도\s*나오고\s*있습니다|이\s*우세하다|이\s*우세합니다|이\s*지배적이다|이\s*지배적입니다)|"
|
| 55 |
+
r"(?:라는|다는|이라는|란|는|은|인)?\s*(?:전망|예상|예측|관측)(?:이\s*나온다|이\s*나옵니다|이\s*나오고\s*있다|이\s*나오고\s*있습니다|이\s*나왔다|이\s*나왔습니다)"
|
| 56 |
+
r")"
|
| 57 |
+
),
|
| 58 |
+
|
| 59 |
+
# =========================================================================
|
| 60 |
+
# 3. 관측/추정형
|
| 61 |
+
# =========================================================================
|
| 62 |
+
"관측형": re.compile(
|
| 63 |
+
r"(?:"
|
| 64 |
+
# --- 피동형 술어 ---
|
| 65 |
+
r"관측(?:된다|됩니다|됐다|됐습니다|되고\s*있다|되고\s*있습니다)|"
|
| 66 |
+
r"추정(?:된다|됩니다|됐다|됐습니다|되고\s*있다|되고\s*있습니다)|"
|
| 67 |
+
r"추측(?:된다|됩니다|됐다|됐습니다|되고\s*있다|되고\s*있습니다)|"
|
| 68 |
+
r"짐작(?:된다|됩니다|됐다|됐습니다|되고\s*있다|되고\s*있습니다)|"
|
| 69 |
+
# --- 명사형 술어 ---
|
| 70 |
+
r"(?:관측|추정|추측)(?:이다|입니다|이었다|이었습니다|이에요)|"
|
| 71 |
+
r"(?:라는|다는|이라는|란|는|은|인)\s*(?:관측|추정|추측)(?:이다|입니다|이었다|이었습니다|이에요)|"
|
| 72 |
+
r"(?:라는|다는|이라는|란|는|���|인)?\s*(?:관측|추정|추측)(?:도\s*있다|도\s*있습니다|도\s*나온다|도\s*나옵니다|이\s*나온다|이\s*나옵니다|이\s*나오고\s*있다|이\s*나오고\s*있습니다)"
|
| 73 |
+
r")"
|
| 74 |
+
),
|
| 75 |
+
|
| 76 |
+
# =========================================================================
|
| 77 |
+
# 4. 전언/보도형
|
| 78 |
+
# =========================================================================
|
| 79 |
+
"전언형": re.compile(
|
| 80 |
+
r"(?:"
|
| 81 |
+
# --- 피동형 술어 ---
|
| 82 |
+
r"알려(?:진다|집니다|졌다|졌습니다|지고\s*있다|지고\s*있습니다)|"
|
| 83 |
+
r"전해(?:진다|집니다|졌다|졌습니다|지고\s*있다|지고\s*있습니다)|"
|
| 84 |
+
r"보도(?:된다|됩니다|됐다|됐습니다|되고\s*있다|되고\s*있습니다)|"
|
| 85 |
+
r"전달(?:된다|됩니다|됐다|됐습니다|되고\s*있다|되고\s*있습니다)|"
|
| 86 |
+
# 것으로 + 전언
|
| 87 |
+
r"것으로\s*(?:알려졌다|알려졌습니다|알려집니다|알려지고\s*있다|알려지고\s*있습니다|전해졌다|전해졌습니다|전해집니다|전해지고\s*있다|전해지고\s*있습니다)|"
|
| 88 |
+
# --- 명사형 술어 ---
|
| 89 |
+
r"(?:소식|보도|소문)(?:이다|입니다|이었다|이었습니다|이에요)|"
|
| 90 |
+
r"(?:라는|다는|이라는|란|는|은|인)\s*(?:소식|보도|소문)(?:이다|입니다|이었다|이었습니다|이에요)|"
|
| 91 |
+
r"(?:라는|다는)?\s*(?:소식|보도)(?:이\s*전해졌다|이\s*전해졌습니다|이\s*전해지고\s*있다|이\s*전해지고\s*있습니다|이\s*들려온다|이\s*들려옵니다|이\s*들려왔다|이\s*들려왔습니다)|"
|
| 92 |
+
# ~라는 겁니다/것입니다
|
| 93 |
+
r"(?:라는|다는)\s*(?:겁니다|것입니다|것이다|얘기다|얘기입니다|이야기다|이야기입니다)"
|
| 94 |
+
r")"
|
| 95 |
+
),
|
| 96 |
+
|
| 97 |
+
# =========================================================================
|
| 98 |
+
# 5. 평가/판단형
|
| 99 |
+
# =========================================================================
|
| 100 |
+
"평가형": re.compile(
|
| 101 |
+
r"(?:"
|
| 102 |
+
# --- 피동형 술어 ---
|
| 103 |
+
r"평가(?:된다|됩니다|됐다|됐습니다|되고\s*있다|되고\s*있습니다|받고\s*있다|받고\s*있습니다|받았다|받았습니다|받는다|받습니다)|"
|
| 104 |
+
r"판단(?:된다|됩니다|됐다|됐습니다|되고\s*있다|되고\s*있습니다)|"
|
| 105 |
+
r"인식(?:된다|됩니다|됐다|됐습니다|되고\s*있다|되고\s*있습니다)|"
|
| 106 |
+
r"간주(?:된다|됩니다|됐다|됐습니다|되고\s*있다|되고\s*있습니다)|"
|
| 107 |
+
r"여겨(?:진다|집니다|졌다|졌습니다|지고\s*있다|지고\s*있습니다)|"
|
| 108 |
+
# --- 명사형 술어 ---
|
| 109 |
+
r"(?:평가|판단|인식)(?:다|이다|입니다|였다|이었다|이었습니다|이에요)|"
|
| 110 |
+
r"(?:라는|다는|이라는|란|는|은|인)\s*(?:평가|판단|인식)(?:다|이다|입니다|였다|이었다|이었습니다)|"
|
| 111 |
+
r"(?:라는|다는|이라는|란|는|은|인)?\s*(?:평가|판단|인식)(?:도\s*있다|도\s*있습니다|도\s*나온다|도\s*나옵니다|도\s*나오고\s*있다|도\s*나오고\s*있습니다)|"
|
| 112 |
+
r"(?:라는|다는|이라는|란|는|은|인)?\s*(?:평가|판단)(?:가\s*나온다|가\s*나옵니다|가\s*나오고\s*있다|가\s*나오고\s*있습니다|가\s*나왔다|가\s*나왔습니다|이\s*나온다|이\s*나옵니다)|"
|
| 113 |
+
r"(?:라는|다는)?\s*(?:평가|판단)(?:를\s*받고\s*있다|를\s*받고\s*있습니다|를\s*받았다|를\s*받았습니다|를\s*받는다|를\s*받습니다)"
|
| 114 |
+
r")"
|
| 115 |
+
),
|
| 116 |
+
|
| 117 |
+
# =========================================================================
|
| 118 |
+
# 6. 비판/지적형
|
| 119 |
+
# =========================================================================
|
| 120 |
+
"비판형": re.compile(
|
| 121 |
+
r"(?:"
|
| 122 |
+
# --- 피동형 술어 ---
|
| 123 |
+
r"비판(?:된다|됩니다|됐다|됐습니다|되고\s*있다|되고\s*있습니다|받고\s*있다|받고\s*있습니다|받았다|받았습니다|받는다|받습니다)|"
|
| 124 |
+
r"비난(?:된다|됩니다|됐다|됐습니다|되고\s*있다|되고\s*있습니다|받고\s*있다|받고\s*있습니다|받았다|받았습니다|받는다|받습니다)|"
|
| 125 |
+
r"지적(?:된다|됩니다|됐다|됐습니다|되고\s*있다|되고\s*있습니다|받고\s*있다|받고\s*있습니다|받았다|받았습니다|받는다|받습니다)|"
|
| 126 |
+
# --- 명사형 술어 ---
|
| 127 |
+
r"(?:비판|비난|지적)(?:이다|입니다|이었다|이었습니다|이에요)|"
|
| 128 |
+
r"(?:라는|다는|이라는|란|는|은|인)\s*(?:비판|비난|지적)(?:이다|입니다|이었다|이었습니다)|"
|
| 129 |
+
r"(?:라는|다는|이라는|란|는|은|인)?\s*(?:비판|비난|지적)(?:도\s*있다|도\s*있습니다|도\s*나온다|도\s*나옵니다|도\s*나오고\s*있다|도\s*나오고\s*있습니다|도\s*제기됐다|도\s*제기됐습니다)|"
|
| 130 |
+
r"(?:라는|다는|이라는|란|는|은|인)?\s*(?:비판|비��|지적)(?:이\s*나온다|이\s*나옵니다|이\s*나오고\s*있다|이\s*나오고\s*있습니다|이\s*나왔다|이\s*나왔습니다|이\s*제기됐다|이\s*제기됐습니다|이\s*제기되고\s*있다|이\s*제기되고\s*있습니다|이\s*쏟아지고\s*있다|이\s*쏟아지고\s*있습니다|이\s*쏟아졌다|이\s*쏟아졌습니다|이\s*잇따르고\s*있다|이\s*잇따르고\s*있습니다|이\s*잇따랐다|이\s*잇따랐습니다)|"
|
| 131 |
+
r"(?:비판|비난|지적)(?:을\s*받고\s*있다|을\s*받고\s*있습니다|을\s*받았다|을\s*받았습니다|을\s*받는다|을\s*받습니다|을\s*면치\s*못하고\s*있다|을\s*면치\s*못하고\s*있습니다)"
|
| 132 |
+
r")"
|
| 133 |
+
),
|
| 134 |
+
|
| 135 |
+
# =========================================================================
|
| 136 |
+
# 7. 제기/거론형
|
| 137 |
+
# =========================================================================
|
| 138 |
+
"제기형": re.compile(
|
| 139 |
+
r"(?:"
|
| 140 |
+
r"제기(?:된다|됩니다|됐다|됐습니다|되고\s*있다|되고\s*있습니다)|"
|
| 141 |
+
r"거론(?:된다|됩니다|됐다|됐습니다|되고\s*있다|되고\s*있습니다)|"
|
| 142 |
+
r"언급(?:된다|됩니다|됐다|됐습니다|되고\s*있다|되고\s*있습니다)|"
|
| 143 |
+
r"지목(?:된다|됩니다|됐다|됐습니다|되고\s*있다|되고\s*있습니다)|"
|
| 144 |
+
r"논의(?:된다|됩니다|됐다|됐습니다|되고\s*있다|되고\s*있습니다)|"
|
| 145 |
+
r"검토(?:된다|됩니다|됐다|됐습니다|되고\s*있다|되고\s*있습니다)|"
|
| 146 |
+
r"거명(?:된다|됩니다|됐다|됐습니다|되고\s*있다|되고\s*있습니다)"
|
| 147 |
+
r")"
|
| 148 |
+
),
|
| 149 |
+
|
| 150 |
+
# =========================================================================
|
| 151 |
+
# 8. 우려/의혹형
|
| 152 |
+
# =========================================================================
|
| 153 |
+
"우려형": re.compile(
|
| 154 |
+
r"(?:"
|
| 155 |
+
# 우려
|
| 156 |
+
r"우려(?:가|도|를)?\s*(?:있다|있습니다|나온다|나옵니다|나오고\s*있다|나오고\s*있습니다|나왔다|나왔습니다|제기됐다|제기됐습니다|제기되고\s*있다|제기되고\s*있습니다|커지고\s*있다|커지고\s*있습니다|커졌다|커졌습니다|낳고\s*있다|낳고\s*있습니다|낳았다|낳았습니다|높아지고\s*있다|높아지고\s*있습니다)|"
|
| 157 |
+
# 의혹
|
| 158 |
+
r"의혹(?:이|을|도)?\s*(?:있다|있습니다|제기됐다|제기됐습니다|제기되고\s*있다|제기되고\s*있습니다|불거졌다|불거졌습니다|불거지고\s*있다|불거지고\s*있습니다|일고\s*있다|일고\s*있습니다|일었다|일었습니다|사고\s*있다|사고\s*있습니다|샀다|샀습니다|증폭되고\s*있다|증폭되고\s*있습니다|확산되고\s*있다|확산되고\s*있습니다)|"
|
| 159 |
+
# 논란
|
| 160 |
+
r"논란(?:이|도)?\s*(?:있다|있습니다|일고\s*있다|일고\s*있습니다|일었다|일었습니다|되고\s*있다|되고\s*있습니다|됐다|됐습니다|불거졌다|불거졌습니다|불거지고\s*있다|불거지고\s*있습니다|예상된다|예상됩니다|이어지고\s*있다|이어지고\s*있습니다)|"
|
| 161 |
+
# 의문
|
| 162 |
+
r"의문(?:이|도)?\s*(?:있다|있습니다|제기됐다|제기됐습니다|제기되고\s*있다|제기되고\s*있습니다|일고\s*있다|일고\s*있습니다|남는다|남습니다|남아\s*있다|남아\s*있습니다|든다|듭니다)|"
|
| 163 |
+
# 명사형
|
| 164 |
+
r"(?:우려|의혹|논란|의문)(?:다|이다|입니다|이었다|이었습니다|이에요)|"
|
| 165 |
+
r"(?:라는|다는|이라는|란|는|은|인)\s*(?:우려|의혹|논란|의문)(?:다|이다|입니다|이었다|이었습니다)|"
|
| 166 |
+
r"(?:라는|다는)?\s*(?:우려|의혹|논란|의문)(?:도\s*있다|도\s*있습니다|도\s*나온다|도\s*나옵니다|도\s*제기됐다|도\s*제기됐습니다)"
|
| 167 |
+
r")"
|
| 168 |
+
),
|
| 169 |
+
|
| 170 |
+
# =========================================================================
|
| 171 |
+
# 9. 가능성/여지형
|
| 172 |
+
# =========================================================================
|
| 173 |
+
"가능성형": re.compile(
|
| 174 |
+
r"(?:"
|
| 175 |
+
r"가능성(?:이|도|을)?\s*(?:있다|있습니다|크다|큽니다|높다|높습니다|낮다|낮습니다|제기됐다|제기됐습니다|거론되고\s*있다|거론되고\s*있습니다|점쳐지고\s*있다|점쳐지고\s*있습니다|배제할\s*수\s*없다|배제할\s*수\s*없습니다|열려\s*있다|열려\s*있습니다|열렸다|열렸습니다|제기된다|제기됩니다|나온다|나옵니다)|"
|
| 176 |
+
r"개연성(?:이|도)?\s*(?:있다|있습니다|크다|큽니다|높다|높습니다|낮다|낮습니다)|"
|
| 177 |
+
r"여지(?:가|도|를)?\s*(?:있다|있습니다|남아\s*있다|남아\s*있습니다|남겨져\s*있다|남겨져\s*있습니다|남는다|남습니다|남겼다|남겼습니다)|"
|
| 178 |
+
r"(?:라는|다는)?\s*(?:가능성|개연성|여지)(?:도\s*있다|도\s*있습니다|이\s*제기됐다|이\s*제기됐습니다|이\s*나온다|이\s*나옵니다)"
|
| 179 |
+
r")"
|
| 180 |
+
),
|
| 181 |
+
|
| 182 |
+
# =========================================================================
|
| 183 |
+
# 10. 분위기/목소리형
|
| 184 |
+
# =========================================================================
|
| 185 |
+
"분위기형": re.compile(
|
| 186 |
+
r"(?:"
|
| 187 |
+
# 분위기
|
| 188 |
+
r"분위기(?:다|이다|입니다|이었다|이었습니다|가\s*감지되고\s*있다|가\s*감지되고\s*있습니다|가\s*형성되고\s*있다|가\s*형성되고\s*있습니다|가\s*확산되고\s*있다|가\s*확산되고\s*있습니다|가\s*팽배하다|가\s*팽배합니다|가\s*역력하다|가\s*역력합니다)|"
|
| 189 |
+
# 목소리
|
| 190 |
+
r"목소리(?:가|도)?\s*(?:나온다|나옵니다|나오고\s*있다|나오고\s*있습니다|나왔다|나왔습니다|높아지고\s*있다|높아지고\s*있습니다|높아졌다|높아졌습니다|커지고\s*있다|커지고\s*있습니다|커졌다|커졌습니다|있다|있습니다)|"
|
| 191 |
+
# 기대
|
| 192 |
+
r"기대(?:가|를|도)?\s*(?:모아지고\s*있다|모아지고\s*있습니다|모이고\s*있다|모이고\s*있습니다|높아지고\s*있다|높아지고\s*있습니다|커지고\s*있다|커지고\s*있습니다|크다|큽니다|높다|높습니다)|"
|
| 193 |
+
# 관심/이목
|
| 194 |
+
r"(?:관심|이목)(?:이|을)?\s*(?:쏠리고\s*있다|쏠리고\s*있습니다|쏠렸다|쏠렸습니다|집중되고\s*있다|집중되고\s*있습니다|집중됐다|집중됐습니다|모아지고\s*있다|모아지고\s*있습니다)|"
|
| 195 |
+
# 기류/조짐/흐름
|
| 196 |
+
r"(?:기류|조짐|흐름|양상)(?:이|가)?\s*(?:감지되고\s*있다|감지되고\s*있습니다|감지됐다|감지됐습니다|포착되고\s*있다|포착되고\s*있습니다|나타나고\s*있다|나타나고\s*있습니다)"
|
| 197 |
+
r")"
|
| 198 |
+
),
|
| 199 |
+
|
| 200 |
+
# =========================================================================
|
| 201 |
+
# 11. 주장/입장형
|
| 202 |
+
# =========================================================================
|
| 203 |
+
"주장형": re.compile(
|
| 204 |
+
r"(?:"
|
| 205 |
+
# --- 피동형 술어 ---
|
| 206 |
+
r"주장(?:된다|됩니다|됐다|됐습니다|되고\s*있다|되고\s*있습니다)|"
|
| 207 |
+
# --- 명사형 술어 ---
|
| 208 |
+
r"(?:주장|입장|방침|계획|생각|확신|설명|해명)(?:이다|입니다|이었다|이었습니다|이에요)|"
|
| 209 |
+
r"(?:라는|다는|이라는|란|는|은|인)\s*(?:주장|입장|방침|계획|생각|확신|설명|해명)(?:이다|입니다|이었다|이었습니다)|"
|
| 210 |
+
r"(?:라는|다는|이라는|란|는|은|인)?\s*(?:주장|입장)(?:도\s*있다|도\s*있습니다|도\s*나온다|도\s*나옵니다|도\s*나오고\s*있다|도\s*나오고\s*있습니다)|"
|
| 211 |
+
r"(?:라는|다는|이라는|란|는|은|인)?\s*(?:주장|입장|설명|해명)(?:이\s*나온다|이\s*나옵니다|이\s*나오고\s*있다|이\s*나오고\s*있습니다|이\s*나왔다|이\s*나왔습니다)"
|
| 212 |
+
r")"
|
| 213 |
+
),
|
| 214 |
+
|
| 215 |
+
# =========================================================================
|
| 216 |
+
# 12. 시각/견해형
|
| 217 |
+
# =========================================================================
|
| 218 |
+
"시각형": re.compile(
|
| 219 |
+
r"(?:"
|
| 220 |
+
# 단독 사용
|
| 221 |
+
r"(?:시각|견해|관점|자평)(?:이다|다|입니다|이었다|였다|이었습니다|이에요)|"
|
| 222 |
+
# ~라는 + 명사
|
| 223 |
+
r"(?:라는|다는|이라는|란|는|은|인)\s*(?:시각|견해|관점|인식|자평)(?:이다|다|입니다|이었다|였다|이었습니다)|"
|
| 224 |
+
# 명사 + 도 있다/지배적이다
|
| 225 |
+
r"(?:라는|다는|이라는|란|는|은|인)?\s*(?:시각|견해|관점|인식)(?:도\s*있다|도\s*있습니다|도\s*나온다|도\s*나옵니다|이\s*있다|이\s*있습니다|이\s*나온다|이\s*나옵니다|이\s*지배적이다|이\s*지배적입니다|가\s*지배적이다|가\s*지배적입니다|이\s*우세하다|이\s*우세합니다|가\s*우세하다|가\s*우세합니다)"
|
| 226 |
+
r")"
|
| 227 |
+
),
|
| 228 |
+
|
| 229 |
+
# =========================================================================
|
| 230 |
+
# 13. 격찬/혹평형 (극단적 평가)
|
| 231 |
+
# =========================================================================
|
| 232 |
+
"격찬형": re.compile(
|
| 233 |
+
r"(?:"
|
| 234 |
+
r"(?:격찬|찬사|호평)(?:이|을)?\s*(?:쏟아졌다|쏟아졌습니다|쏟아지고\s*있다|쏟아지고\s*있습니다|이어졌다|이어졌습니다|이어지고\s*있다|이어지고\s*있습니다|나왔다|나왔습니다|나오고\s*있다|나오고\s*있습니다|받았다|받았습니다|받고\s*있다|받고\s*있습니다)|"
|
| 235 |
+
r"(?:혹평|악평)(?:이|을)?\s*(?:쏟아졌다|쏟아졌습니다|쏟아지고\s*있다|쏟아지고\s*있습니다|이어졌다|이어졌습니다|이어지고\s*있다|이어지고\s*있습니다|나왔다|나왔습니다|나오고\s*있다|나오고\s*있습니다|받았다|받았습니다|받고\s*있다|받고\s*있습니다)|"
|
| 236 |
+
r"(?:라는|다는)?\s*(?:격찬|찬사|호평|혹평|악평)(?:이다|입니다|이었다|이었습니다)"
|
| 237 |
+
r")"
|
| 238 |
+
),
|
| 239 |
+
|
| 240 |
+
# =========================================================================
|
| 241 |
+
# 15. 관용표현형
|
| 242 |
+
# =========================================================================
|
| 243 |
+
"관용표현형": re.compile(
|
| 244 |
+
r"(?:"
|
| 245 |
+
# ~한 셈이다
|
| 246 |
+
r"[은는인된한했던]\s*셈(?:이다|입니다|이에요|이었다|이었습니다)|"
|
| 247 |
+
r"셈(?:이다|입니다|이에요|이었다|이었습니다)|"
|
| 248 |
+
# ~해야 할 판이다
|
| 249 |
+
r"(?:해야\s*할|하게\s*된|하게\s*됐)\s*판(?:이다|입니다|이에요)|"
|
| 250 |
+
# ~로 보인다/여겨진다/비춰진다
|
| 251 |
+
r"(?:으로|로)\s*(?:보인다|보입니다|보여진다|보여집니다|보이고\s*있다|보이고\s*있습니다)|"
|
| 252 |
+
r"(?:으로|로)\s*(?:여겨진다|여겨집집니다|여겨지고\s*있다|여겨지고\s*있습니다)|"
|
| 253 |
+
r"(?:으로|로)\s*(?:비춰진다|비춰집니다|비쳐진다|비쳐집니다|비쳐지고\s*있다|비쳐지고\s*있습니다)|"
|
| 254 |
+
r"(?:으로|로)\s*(?:받아들여지고\s*있다|받아들여지고\s*있습니다|받아들여진다|받아들여집니다|받아들여졌다|받아들여졌습니다)|"
|
| 255 |
+
# ~것 아니냐는/~지 않겠느냐는
|
| 256 |
+
r"(?:는|은)\s*것\s*아니(?:냐는|냐고|겠냐는|냐며)|"
|
| 257 |
+
r"(?:지|치)\s*않(?:겠느냐는|을까\s*하는|을까\s*싶은|느냐는)|"
|
| 258 |
+
# ~가 아닌가 싶다
|
| 259 |
+
r"(?:가|이)\s*아닌가\s*(?:싶다|싶습니다|하는|하다|합니다)|"
|
| 260 |
+
# ~를 짐작하게/케 한다
|
| 261 |
+
r"(?:을|를)?\s*짐작(?:하게|케)\s*(?:한다|합니다|했다|했습니다)"
|
| 262 |
+
r")"
|
| 263 |
+
),
|
| 264 |
+
|
| 265 |
+
# =========================================================================
|
| 266 |
+
# 16. 완화표현형 (Hedges)
|
| 267 |
+
# =========================================================================
|
| 268 |
+
"완화표현형": re.compile(
|
| 269 |
+
r"(?:"
|
| 270 |
+
# 것으로 + 술어
|
| 271 |
+
r"것으로\s*(?:보인다|보입니다|보여진다|보여집니다)|"
|
| 272 |
+
r"것으로\s*(?:추정된다|추정됩니다|추정되고\s*있다|추정되고\s*있습니다)|"
|
| 273 |
+
r"것으로\s*(?:판단된다|판단됩니다|판단되고\s*있다|판단되고\s*있습니다)|"
|
| 274 |
+
r"것으로\s*(?:분석된다|분석됩니다|분석되고\s*있다|분석되고\s*있습니다)|"
|
| 275 |
+
r"것으로\s*(?:예상된다|예상됩니다|예상되고\s*있다|예상되고\s*있습니다)|"
|
| 276 |
+
r"것으로\s*(?:전망된다|전망됩니다|전망되고\s*있다|전망되고\s*있습니다)|"
|
| 277 |
+
r"것으로\s*(?:관측된다|관측됩니다|관측되고\s*있다|관측되고\s*있습니다)|"
|
| 278 |
+
r"것으로\s*(?:평가된다|평가됩니다|평가되고\s*있다|평가되고\s*있습니다)|"
|
| 279 |
+
r"것으로\s*(?:풀이된다|풀이됩니다|풀이되고\s*있다|풀이되고\s*있습니다)|"
|
| 280 |
+
r"것으로\s*(?:해석된다|해석됩니다|해석되고\s*있다|해석되고\s*있습니다)|"
|
| 281 |
+
r"것으로\s*(?:파악된다|파악됩니다|파악되고\s*있다|파악되고\s*있습니다)|"
|
| 282 |
+
r"것으로\s*(?:나타났다|나타났습니다|나타나고\s*있다|나타나고\s*있습니다)|"
|
| 283 |
+
# 듯 + 술어
|
| 284 |
+
r"듯\s*(?:보인다|보입니다|하다|합니다|싶다|싶습니다)|"
|
| 285 |
+
# ~지도 모른다
|
| 286 |
+
r"[을를]지도?\s*모른(?:다|릅니다)|"
|
| 287 |
+
# ~ㄹ 것 같다
|
| 288 |
+
r"[을를]\s*것\s*같(?:다|습니다)"
|
| 289 |
+
r")"
|
| 290 |
+
),
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
# =============================================================================
|
| 295 |
+
# II. 객관성 지지 술어 (SUPPORT):
|
| 296 |
+
# =============================================================================
|
| 297 |
+
|
| 298 |
+
SUPPORT_PREDICATES: Dict[str, Pattern] = {
|
| 299 |
+
|
| 300 |
+
# =========================================================================
|
| 301 |
+
# 1. 확인/검증형
|
| 302 |
+
# =========================================================================
|
| 303 |
+
"확인형": re.compile(
|
| 304 |
+
r"(?:"
|
| 305 |
+
# 확인/밝혀지다/드러나다
|
| 306 |
+
r"확인(?:됐다|됐습니다|된다|됩니다|되고\s*있다|되고\s*있습니다|했다|했습니다|한다|합니다)|"
|
| 307 |
+
r"밝혀(?:졌다|졌습니다|진다|집니다|지고\s*있다|지고\s*있습니다)|"
|
| 308 |
+
r"드러(?:났다|났습니다|난다|납니다|나고\s*있다|나고\s*있습니다)|"
|
| 309 |
+
r"판명(?:됐다|됐습니다|된다|됩니다|났다|났습니다)|"
|
| 310 |
+
r"입증(?:됐다|됐습니다|된다|됩니다|되고\s*있다|되고\s*있습니다|했다|했습니다)|"
|
| 311 |
+
r"규명(?:됐다|됐습니다|된다|됩니다|되고\s*있다|되고\s*있습니다|했다|했습니다)|"
|
| 312 |
+
r")"
|
| 313 |
+
),
|
| 314 |
+
|
| 315 |
+
# =========================================================================
|
| 316 |
+
# 2. 발견/탐지형
|
| 317 |
+
# =========================================================================
|
| 318 |
+
"발견형": re.compile(
|
| 319 |
+
r"(?:"
|
| 320 |
+
r"발견(?:됐다|됐습니다|된다|됩니다|되고\s*있다|되고\s*있습니다|했다|했습니다|한다|합니다)|"
|
| 321 |
+
r"발각(?:됐다|됐습니다|된다|됩니다)|"
|
| 322 |
+
r"적발(?:됐다|됐습니다|된다|됩니다|되고\s*있다|되고\s*있습니다|했다|했습니다)|"
|
| 323 |
+
r"포착(?:됐다|됐습니다|된다|됩니다|되고\s*있다|되고\s*있습니다|했다|했습니다)|"
|
| 324 |
+
r"감지(?:됐다|됐습니다|된다|됩니다|되고\s*있다|되고\s*있습니다|했다|했습니다)|"
|
| 325 |
+
r"파악(?:됐다|됐습니다|된다|됩니다|되고\s*있다|되고\s*있습니다|했다|했습니다|한다|합니다)"
|
| 326 |
+
r")"
|
| 327 |
+
),
|
| 328 |
+
|
| 329 |
+
# =========================================================================
|
| 330 |
+
# 3. 기록/집계형
|
| 331 |
+
# =========================================================================
|
| 332 |
+
"기록형": re.compile(
|
| 333 |
+
r"(?:"
|
| 334 |
+
r"기록(?:됐다|됐습니다|된다|됩니다|되고\s*있다|되고\s*있습니다|했다|했습니다|한다|합니다)|"
|
| 335 |
+
r"집계(?:됐다|됐습니다|된다|됩니다|되고\s*있다|되고\s*있습니다|했다|했습니다|한다|합니다)|"
|
| 336 |
+
r"조사(?:됐다|됐습니다|된다|됩니다|되고\s*있다|되고\s*있습니다|했다|했습니다|한다|합니다)|"
|
| 337 |
+
r"측정(?:됐다|됐습니다|된다|됩니다|되고\s*있다|되고\s*있습니다|했다|했습니다|한다|합니다)|"
|
| 338 |
+
r"나타(?:났다|났습니다|난다|납니다)"
|
| 339 |
+
r")"
|
| 340 |
+
),
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
# =============================================================================
|
| 345 |
+
# 유틸리티 함수
|
| 346 |
+
# =============================================================================
|
| 347 |
+
|
| 348 |
+
def analyze_objectivity(text: str) -> Dict[str, Any]:
|
| 349 |
+
"""텍스트를 문장 단위로 나누어 객관성 의심/지지 요소를 분석"""
|
| 350 |
+
splitter = KoreanSentenceSplitter()
|
| 351 |
+
sentences = splitter.split(text)
|
| 352 |
+
|
| 353 |
+
doubt_predicates = []
|
| 354 |
+
support_predicates = []
|
| 355 |
+
doubt_sentences = []
|
| 356 |
+
support_sentences = []
|
| 357 |
+
|
| 358 |
+
doubt_sentence_count = 0
|
| 359 |
+
support_sentence_count = 0
|
| 360 |
+
|
| 361 |
+
for sent in sentences:
|
| 362 |
+
sent_doubt_matches = []
|
| 363 |
+
sent_support_matches = []
|
| 364 |
+
|
| 365 |
+
# 중복 방지를 위한 스팬(Span) 기반 체크
|
| 366 |
+
doubt_spans = {}
|
| 367 |
+
support_spans = {}
|
| 368 |
+
|
| 369 |
+
for _, pattern in DOUBT_PREDICATES.items():
|
| 370 |
+
for match in pattern.finditer(sent):
|
| 371 |
+
m_text = match.group(0).strip()
|
| 372 |
+
if m_text:
|
| 373 |
+
doubt_spans[match.span()] = m_text
|
| 374 |
+
|
| 375 |
+
for _, pattern in SUPPORT_PREDICATES.items():
|
| 376 |
+
for match in pattern.finditer(sent):
|
| 377 |
+
m_text = match.group(0).strip()
|
| 378 |
+
if m_text:
|
| 379 |
+
support_spans[match.span()] = m_text
|
| 380 |
+
|
| 381 |
+
# 문장 내 추출 결과 정리
|
| 382 |
+
if doubt_spans:
|
| 383 |
+
sorted_doubt = [doubt_spans[s] for s in sorted(doubt_spans.keys())]
|
| 384 |
+
doubt_predicates.extend(sorted_doubt)
|
| 385 |
+
doubt_sentences.append(sent)
|
| 386 |
+
doubt_sentence_count += 1
|
| 387 |
+
|
| 388 |
+
if support_spans:
|
| 389 |
+
sorted_support = [support_spans[s] for s in sorted(support_spans.keys())]
|
| 390 |
+
support_predicates.extend(sorted_support)
|
| 391 |
+
support_sentences.append(sent)
|
| 392 |
+
support_sentence_count += 1
|
| 393 |
+
|
| 394 |
+
total_sentences = doubt_sentence_count + support_sentence_count
|
| 395 |
+
|
| 396 |
+
return {
|
| 397 |
+
"doubt_predicates": doubt_predicates,
|
| 398 |
+
"support_predicates": support_predicates,
|
| 399 |
+
"doubt_sentences": doubt_sentences,
|
| 400 |
+
"support_sentences": support_sentences,
|
| 401 |
+
"doubt_count": doubt_sentence_count,
|
| 402 |
+
"support_count": support_sentence_count,
|
| 403 |
+
"objectivity_ratio": round(support_sentence_count / total_sentences, 4) if total_sentences > 0 else None
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
|
| 407 |
+
def find_doubt_predicates(text: str) -> Dict[str, List[str]]:
|
| 408 |
+
"""객관성 의심 술어만 반환"""
|
| 409 |
+
results = {}
|
| 410 |
+
for category, pattern in DOUBT_PREDICATES.items():
|
| 411 |
+
matches = pattern.findall(text)
|
| 412 |
+
if matches:
|
| 413 |
+
results[category] = matches
|
| 414 |
+
return results
|
| 415 |
+
|
| 416 |
+
|
| 417 |
+
def find_support_predicates(text: str) -> Dict[str, List[str]]:
|
| 418 |
+
"""객관성 지지 술어만 반환"""
|
| 419 |
+
results = {}
|
| 420 |
+
for category, pattern in SUPPORT_PREDICATES.items():
|
| 421 |
+
matches = pattern.findall(text)
|
| 422 |
+
if matches:
|
| 423 |
+
results[category] = matches
|
| 424 |
+
return results
|
| 425 |
+
|
| 426 |
+
|
| 427 |
+
def print_pattern_summary():
|
| 428 |
+
"""패턴 요약 출력"""
|
| 429 |
+
print("=" * 70)
|
| 430 |
+
print("언론 보도 객관성 측정용 무주체 술어 정규표현식 v2")
|
| 431 |
+
print("=" * 70)
|
| 432 |
+
print()
|
| 433 |
+
print("I. 객관성 의심 술어 (DOUBT) - 무주체 주관적 술어")
|
| 434 |
+
print("-" * 70)
|
| 435 |
+
for i, (name, _) in enumerate(DOUBT_PREDICATES.items(), 1):
|
| 436 |
+
print(f" {i:2d}. {name}")
|
| 437 |
+
print(f"\n 총 {len(DOUBT_PREDICATES)}개 카테고리")
|
| 438 |
+
print()
|
| 439 |
+
print("II. 객관성 지지 술어 (SUPPORT) - 사실 확인/명시적 출처")
|
| 440 |
+
print("-" * 70)
|
| 441 |
+
for i, (name, _) in enumerate(SUPPORT_PREDICATES.items(), 1):
|
| 442 |
+
print(f" {i:2d}. {name}")
|
| 443 |
+
print(f"\n 총 {len(SUPPORT_PREDICATES)}개 카테고리")
|
| 444 |
+
print("=" * 70)
|
| 445 |
+
|
| 446 |
+
|
| 447 |
+
if __name__ == "__main__":
|
| 448 |
+
print_pattern_summary()
|