jonghhhh commited on
Commit
46b19e8
·
verified ·
1 Parent(s): 0736c27

Delete src

Browse files
src/korean_sentence_splitter.py DELETED
@@ -1,601 +0,0 @@
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/streamlit_app.py DELETED
@@ -1,169 +0,0 @@
1
- import streamlit as st
2
- import re
3
- from typing import List, Tuple, Dict
4
- from subjectless_predicates_122725_v2 import DOUBT_PREDICATES, SUPPORT_PREDICATES, analyze_objectivity
5
- from korean_sentence_splitter import KoreanSentenceSplitter
6
-
7
- def highlight_objectivity(text: str) -> str:
8
- if not text:
9
- return ""
10
-
11
- splitter = KoreanSentenceSplitter()
12
- sentences = splitter.split(text)
13
-
14
- highlighted_sentences = []
15
-
16
- for sent in sentences:
17
- matches = []
18
-
19
- # Find doubt matches
20
- for cat, pattern in DOUBT_PREDICATES.items():
21
- for match in pattern.finditer(sent):
22
- matches.append((match.start(), match.end(), "doubt", cat))
23
-
24
- # Find support matches
25
- for cat, pattern in SUPPORT_PREDICATES.items():
26
- for match in pattern.finditer(sent):
27
- matches.append((match.start(), match.end(), "support", cat))
28
-
29
- # Sort matches by start position, then by length (descending) to handle potential overlaps
30
- matches.sort(key=lambda x: (x[0], -(x[1] - x[0])))
31
-
32
- # Filter overlapping matches (keep the longest or first)
33
- filtered_matches = []
34
- if matches:
35
- last_end = -1
36
- for start, end, mtype, cat in matches:
37
- if start >= last_end:
38
- filtered_matches.append((start, end, mtype, cat))
39
- last_end = end
40
-
41
- # Build highlighted sentence
42
- last_idx = 0
43
- h_sent = ""
44
- for start, end, mtype, cat in filtered_matches:
45
- # Add text before match
46
- h_sent += sent[last_idx:start]
47
-
48
- # Add highlighted match
49
- match_text = sent[start:end]
50
- if mtype == "doubt":
51
- color = "#ffcccc" # Light red
52
- border = "#ff0000"
53
- h_sent += f'<span style="background-color: {color}; border-bottom: 2px solid {border};" title="{cat}">{match_text}</span>'
54
- else:
55
- color = "#ccffcc" # Light green
56
- border = "#00aa00"
57
- h_sent += f'<span style="background-color: {color}; border-bottom: 2px solid {border};" title="{cat}">{match_text}</span>'
58
-
59
- last_idx = end
60
-
61
- h_sent += sent[last_idx:]
62
- highlighted_sentences.append(h_sent)
63
-
64
- return " ".join(highlighted_sentences)
65
-
66
- def clear_input():
67
- st.session_state.main_text_input = ""
68
-
69
- def main():
70
- st.set_page_config(page_title="뉴스 객관성 술어 분석기", layout="wide")
71
-
72
- st.title("📰 뉴스 객관성 분석기: 술어 성격 탐지 및 추출")
73
-
74
- st.markdown("""
75
- 뉴스 객관성 평가를 위해 술어의 성격을 탐지하고 추출하는 기능을 수행합니다.
76
- - <span style="background-color: #ffcccc; border-bottom: 2px solid #ff0000;">빨간색 표시</span>: **객관성 의심 (Objectivity Doubt)** - 발언의 주체가 불분명하여 기자의 주관이 개입되었을 가능성이 높은 표현
77
- - <span style="background-color: #ccffcc; border-bottom: 2px solid #00aa00;">녹색 표시</span>: **객관성 지지 (Objectivity Support)** - 사실 확인이나 구체적인 출처/데이터를 바탕으로 한 객관적 표현
78
- """, unsafe_allow_html=True)
79
-
80
- # 세션 상태 초기화
81
- if 'main_text_input' not in st.session_state:
82
- st.session_state.main_text_input = """오늘 주가가 크게 오를 것으로 전망된다. 투자자들 사이에서는 이번 상승세가 당분간 이어질 것이라는 분석이 지배적이다. 반면 일각에서는 거품이라는 지적도 나온다. 실제로 통계청 집계에 따르면 지난달 수출은 역대 최고치를 기록했다. 정부 관계자는 경제 지표가 개선되고 있음이 확인됐다고 밝혔다."""
83
-
84
- col1, col2 = st.columns([1, 1])
85
-
86
- with col1:
87
- st.subheader("입력 텍스트")
88
- # key를 통해 세션 상태와 직접 연결
89
- input_text = st.text_area("분석할 기사 내용을 입력하세요.", height=400,
90
- placeholder="여기에 기사 내용을 붙여넣으세요...",
91
- key="main_text_input")
92
-
93
- btn_col1, btn_col2, _ = st.columns([1, 1, 3])
94
- analyze_clicked = btn_col1.button("분석", type="primary")
95
- # 초기화 버튼에 콜백 함수 적용
96
- btn_col2.button("초기화", on_click=clear_input)
97
-
98
- # 분석 버튼 클릭 시 결과 표시
99
- if analyze_clicked and input_text:
100
- with col2:
101
- st.subheader("분석 결과")
102
- with st.spinner("분석 중..."):
103
- highlighted_html = highlight_objectivity(input_text)
104
- st.markdown(f'<div style="line-height: 1.8; font-size: 1.1em; border: 1px solid #ddd; padding: 20px; border-radius: 5px; background-color: white;">{highlighted_html}</div>', unsafe_allow_html=True)
105
-
106
- # 통계 요약
107
- stats = analyze_objectivity(input_text)
108
-
109
- st.write("---")
110
- s_col1, s_col2, s_col3 = st.columns(3)
111
- s_col1.metric("의심 문장", stats["doubt_count"])
112
- s_col2.metric("지지 문장", stats["support_count"])
113
- if stats["objectivity_ratio"] is not None:
114
- st.progress(stats["objectivity_ratio"])
115
- st.write(f"**객관성 지표:** {stats['objectivity_ratio']:.2%}")
116
-
117
- with st.expander("검출된 술어 상세 목록"):
118
- tab1, tab2 = st.tabs(["의심 술어 (DOUBT)", "지지 술어 (SUPPORT)"])
119
- with tab1:
120
- if stats["doubt_predicates"]:
121
- st.write(", ".join(set(stats["doubt_predicates"])))
122
- else:
123
- st.write("발견되지 않음")
124
- with tab2:
125
- if stats["support_predicates"]:
126
- st.write(", ".join(set(stats["support_predicates"])))
127
- else:
128
- st.write("발견되지 않음")
129
-
130
- st.write("---")
131
- st.subheader("💡 설명서: 술어 분류 체계")
132
- st.markdown("`subjectless_predicates_122725_v2.py` 참조")
133
-
134
- desc_col1, desc_col2 = st.columns(2)
135
-
136
- with desc_col1:
137
- st.markdown("### 🔴 객관성 의심 술어 (DOUBT): 무주체 피동형")
138
- st.markdown("""
139
- | 대분류 | 설명 | 주요 예시 |
140
- | :--- | :--- | :--- |
141
- | **분석/해석형** | 사건의 의미를 주관적으로 풀이 | 분석된다, 해석된다, ~라는 분석이다 |
142
- | **전망/예측형** | 불확실한 미래를 단정적으로 추측 | 전망된다, 예상된다, 점쳐진다 |
143
- | **관측/추정형** | 뚜렷한 근거 없이 미루어 짐작 | 관측된다, 추정된다, 추측된다 |
144
- | **전언/보도형** | 출처를 흐리며 말을 전달 | 알려졌다, 전해졌다, ~라는 소식이다 |
145
- | **평가/판단형** | 가치 판단이 개입된 서술 | 평가된다, 여겨진다, ~라는 판단이다 |
146
- | **비판/지적형** | 특정 입장에서 부정적으로 언급 | 비판받는다, 지적된다, 논란이 일고 있다 |
147
- | **제기/거론형** | 화제를 수면 위로 올리는 서술 | 제기된다, 거론된다, 언급된다 |
148
- | **우려/의혹형** | 부정적 가능성을 강조 | 우려가 나온다, 의혹이 제기됐다 |
149
- | **가능성형** | 여지를 두는 표현 | 가능성이 크다, 배제할 수 없다 |
150
- | **분위기형** | 주변 상황을 추상적으로 묘사 | 분위기다, 목소리가 높다, 기류가 감지된다 |
151
- | **주장/입장형** | 특정인의 말을 무주체로 전달 | 주장된다, ~라는 입장이다 |
152
- | **시각/견해형** | 관점을 제시 | 시각이다, 견해가 지배적이다 |
153
- | **격찬/혹평형** | 극단적인 감정적 평가 | 찬사가 쏟아졌다, 혹평을 받았다 |
154
- | **관용표현형** | 객관성을 흐리는 상투적 표현 | ~인 셈이다, ~로 보인다, 아닌가 싶다 |
155
- | **완화표현형** | 단정을 피하려는 회피성 표현 | ~듯 보인다, ~일 것 같다 |
156
- """)
157
-
158
- with desc_col2:
159
- st.markdown("### 🟢 객관성 지지 술어 (SUPPORT)")
160
- st.markdown("""
161
- | 대분류 | 설명 | 주요 예시 |
162
- | :--- | :--- | :--- |
163
- | **확인/검증형** | 사실 관계가 분명히 밝혀짐 | 확인됐다, 밝혀졌다, 드러났다, 입증됐다 |
164
- | **발견/탐지형** | 구체적인 실체를 찾아냄 | 발견됐다, 적발됐다, 파악됐다 |
165
- | **기록/집계형** | 수치나 데이터에 기반한 서술 | 기록됐다, 집계됐다, 나타났다, 조사됐다 |
166
- """)
167
-
168
- if __name__ == "__main__":
169
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/subjectless_predicates_122725_v2.py DELETED
@@ -1,448 +0,0 @@
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()