mecab-ko-core 0.7.2

한국어 형태소 분석 핵심 엔진 - Lattice, Viterbi, 토크나이저
Documentation
//! 컨텍스트 기반 품사 보정 (265개 보정 패스)

use super::types::SejongToken;

mod compound_and_irregular;
mod compound_noun;
mod conjugation;
mod particle_and_ending;
mod pos_reclassification;
mod post_conjugation;
mod sentence_final;
mod sentence_final_endings;
mod suffix_and_dependency;
mod tag_normalization;
mod verb_and_morpheme;
mod verb_splitting;
mod xsv_and_ec_ef;
mod xsv_morpheme_split;

#[cfg(test)]
mod tests;

use compound_and_irregular::apply_compound_and_irregular_corrections;
use compound_noun::apply_compound_noun_corrections;
use conjugation::apply_conjugation_corrections;
use particle_and_ending::apply_particle_and_ending_corrections;
use pos_reclassification::apply_pos_reclassification_corrections;
use post_conjugation::apply_post_conjugation_corrections;
use sentence_final::apply_sentence_final_corrections;
use suffix_and_dependency::apply_suffix_and_dependency_corrections;
use tag_normalization::apply_tag_normalization_corrections;
use verb_and_morpheme::apply_verb_and_morpheme_corrections;

/// 컨텍스트 기반 품사 보정
///
/// 체언(NNG, NNP, NP) 뒤의 어미(EF)를 조사로 보정
#[allow(clippy::too_many_lines)]
pub(super) fn apply_context_corrections(tokens: &mut Vec<SejongToken>) {
    // 185차: 첫 번째 토큰이 "하/XSV"인 경우 VV로 변환
    // "하니까 보니까" = "하/VV 니까/EC 보/VV 니까/EC"
    // 문장 시작 부분의 "하다"는 독립 동사
    if !tokens.is_empty() && tokens[0].surface == "" && tokens[0].pos == "XSV" {
        tokens[0].pos = "VV".to_string();
    }

    // 187차: "서울특별시청" → "서울/NNP 특별시/NNG 청/NNG" 분리
    // "서울특별시/NNP 청/NNG"로 분석된 경우 처리
    let mut i = 0;
    while i < tokens.len() {
        if tokens[i].surface == "서울특별시" && tokens[i].pos == "NNP" {
            // "서울특별시"를 "서울/NNP", "특별시/NNG"로 분리
            let original_start = tokens[i].start_pos;
            let original_end = tokens[i].end_pos;
            let original_surface = tokens[i].surface.clone();
            let original_pos = tokens[i].pos.clone();

            tokens[i] = SejongToken::from_split(
                "서울",
                "NNP",
                original_start,
                original_start + "서울".len(),
                &original_surface,
                &original_pos,
            );
            tokens.insert(
                i + 1,
                SejongToken::from_split(
                    "특별시",
                    "NNG",
                    original_start + "서울".len(),
                    original_end,
                    &original_surface,
                    &original_pos,
                ),
            );
            i += 2;
            continue;
        }
        i += 1;
    }

    // 188차: "그래/VV" → "그러/VV" 표면형 정규화
    // "왜 그래요" = "왜/MAG 그러/VV 어요/EF"
    // "그러다"의 ㅓ→ㅐ 축약 복원
    for token in tokens.iter_mut() {
        if token.surface == "그래" && token.pos == "VV" {
            token.surface = "그러".to_string();
        }
    }

    // 193차: ETN 표면형 정규화 (ᄆ → ㅁ)
    // "달리/VV ᄆ/ETN" → "달리/VV ㅁ/ETN"
    // 초성 ㅁ(U+1106)을 자모 ㅁ(U+3141)으로 정규화
    for token in tokens.iter_mut() {
        if token.pos == "ETN" && token.surface == "\u{1106}" {
            token.surface = "".to_string();
        }
    }

    // 194차: "따라/NNB + 서/VV + 어/EC" → "따라서/MAG" 병합
    // "그래서 따라서" = "그래서/MAJ 따라서/MAG"
    let mut i = 0;
    while i + 2 < tokens.len() {
        if tokens[i].surface == "따라"
            && tokens[i].pos == "NNB"
            && tokens[i + 1].surface == ""
            && tokens[i + 1].pos == "VV"
            && tokens[i + 2].surface == ""
            && tokens[i + 2].pos == "EC"
        {
            tokens[i].surface = "따라서".to_string();
            tokens[i].pos = "MAG".to_string();
            tokens[i].end_pos = tokens[i + 2].end_pos;
            tokens.remove(i + 2);
            tokens.remove(i + 1);
            i += 1;
            continue;
        }
        i += 1;
    }

    // 191차: NNG 뒤의 "아/IC" → "아/JX" 변환
    // "야 얘들아" = "야/IC 얘들/NNG 아/JX"
    // 명사 뒤의 "아"는 호격이 아닌 보조사 (sample.tsv 기준)
    for i in 1..tokens.len() {
        if tokens[i].surface == "" && tokens[i].pos == "IC" && tokens[i - 1].pos == "NNG" {
            tokens[i].pos = "JX".to_string();
        }
    }

    // 247차: "하/XSV + 여/XSN" → "하/XSV + 어/EC" 변환
    // "호출하여" = "호출/NNG 하/XSV 어/EC"
    // MeCab이 "하여"를 "하/XSV 여/XSN"으로 분석하지만 세종 기준은 "어/EC"
    for i in 0..tokens.len().saturating_sub(1) {
        if tokens[i].surface == ""
            && tokens[i].pos == "XSV"
            && tokens[i + 1].surface == ""
            && tokens[i + 1].pos == "XSN"
        {
            tokens[i + 1].surface = "".to_string();
            tokens[i + 1].pos = "EC".to_string();
        }
    }

    // 192~202차: 복합명사/형태소 병합·분리 (192차, 196차, 200차, 197차, 202차, 198차, 199차, 190차)
    apply_compound_noun_corrections(tokens);

    // 207~256차: POS 재분류 (부사→명사, NNP→NNG, NNG→IC, VV→VA 등)
    apply_pos_reclassification_corrections(tokens);

    // 257차: "VA + ㅁ/ETN + NNG" → "명사화/NNG + NNG" 병합
    // "나쁨 수준" = "나쁨/NNG 수준/NNG"
    // 형용사가 명사형 어미와 결합하여 명사로 사용될 때
    let mut i = 0;
    while i + 2 < tokens.len() {
        if tokens[i].pos == "VA"
            && tokens[i + 1].surface == ""
            && tokens[i + 1].pos == "ETN"
            && tokens[i + 2].pos == "NNG"
        {
            // "나쁘/VA ㅁ/ETN 수준/NNG" → "나쁨/NNG 수준/NNG"
            let start = tokens[i].start_pos;
            let end = tokens[i + 1].end_pos;
            // 원본 표면형 재구성 (나쁘 + ㅁ = 나쁨)
            let surface = format!("{}", tokens[i].surface);
            // ㅡ로 끝나면 ㅁ 붙이기
            let merged_surface = if tokens[i].surface.ends_with("") {
                "나쁨".to_string()
            } else {
                surface
            };
            tokens[i] = SejongToken::new(&merged_surface, "NNG", start, end);
            tokens.remove(i + 1);
            continue;
        }
        i += 1;
    }

    // 249차: "어디/NP + 서/JKB" → "어디/NP + 에서/JKB"
    // "어디서" = "어디/NP 에서/JKB" (sample.tsv 기준)
    // MeCab이 "서/JKB"로 분석하지만 세종 기준은 "에서/JKB"
    for i in 0..tokens.len().saturating_sub(1) {
        if tokens[i].surface == "어디"
            && tokens[i].pos == "NP"
            && tokens[i + 1].surface == ""
            && tokens[i + 1].pos == "JKB"
        {
            tokens[i + 1].surface = "에서".to_string();
        }
    }

    // 250차: "EP + 늘/VV + ㄴ데/EC" → "EP + 는데/EC"
    // "되었는데" = "되/VV 었/EP 는데/EC"
    // MeCab이 "는데"를 "늘/VV + ㄴ데/EC"로 잘못 분석하는 경우 수정
    let mut i = 0;
    while i + 2 < tokens.len() {
        if tokens[i].pos == "EP"
            && tokens[i + 1].surface == ""
            && tokens[i + 1].pos == "VV"
            && tokens[i + 2].surface == "ㄴ데"
            && tokens[i + 2].pos == "EC"
        {
            // "늘/VV + ㄴ데/EC" → "는데/EC" 병합
            let start = tokens[i + 1].start_pos;
            let end = tokens[i + 2].end_pos;
            tokens[i + 1] = SejongToken::new("는데", "EC", start, end);
            tokens.remove(i + 2);
            i += 2;
            continue;
        }
        i += 1;
    }

    // 251차: "그/NP + 동안/NNG" → "그동안/NNG" 병합
    // "그동안" = "그동안/NNG" (sample.tsv 기준)
    // MeCab이 "그/NP 동안/NNG"으로 분리하는 경우 병합
    let mut i = 0;
    while i + 1 < tokens.len() {
        if tokens[i].surface == ""
            && tokens[i].pos == "NP"
            && tokens[i + 1].surface == "동안"
            && tokens[i + 1].pos == "NNG"
        {
            let start = tokens[i].start_pos;
            let end = tokens[i + 1].end_pos;
            tokens[i] = SejongToken::new("그동안", "NNG", start, end);
            tokens.remove(i + 1);
            continue;
        }
        i += 1;
    }

    // 209~223차: 빈 POS/XR 태그 정규화 (SL, NNG 부여)
    apply_tag_normalization_corrections(tokens);

    // 210차: MAJ → VV + EC 분리 (문맥 기반)
    // "하지만 가지만" → "하/VV 지만/EC 가/VV 지만/EC" (연결어미 패턴)
    // "하지만 그러나" → "하지만/MAJ 그러나/MAJ" (접속부사 나열 - MAJ 유지)
    // 214차: 인접 MAJ가 있으면 유지, 없으면 분리
    let standalone_maj = [
        "그러나",
        "그래서",
        "따라서",
        "그리고",
        "또한",
        "그런데",
        "또는",
        "혹은",
    ];
    let mut i = 0;
    while i < tokens.len() {
        if tokens[i].pos == "MAJ" && tokens[i].surface.ends_with("지만") {
            // 인접한 토큰이 접속부사인지 확인 (앞/뒤)
            let prev_is_maj = if i > 0 {
                tokens[i - 1].pos == "MAJ"
                    || standalone_maj.contains(&tokens[i - 1].surface.as_str())
            } else {
                false
            };
            let next_is_maj = if i + 1 < tokens.len() {
                tokens[i + 1].pos == "MAJ"
                    || standalone_maj.contains(&tokens[i + 1].surface.as_str())
            } else {
                false
            };

            // 접속부사 나열인 경우 MAJ 유지
            if prev_is_maj || next_is_maj {
                i += 1;
                continue;
            }

            let surface = &tokens[i].surface;
            if surface.len() > "지만".len() {
                let stem = &surface[..surface.len() - "지만".len()];
                let start = tokens[i].start_pos;
                let end = tokens[i].end_pos;
                let stem_end = start + stem.len();
                tokens[i] = SejongToken::new(stem, "VV", start, stem_end);
                tokens.insert(i + 1, SejongToken::new("지만", "EC", stem_end, end));
                i += 2;
                continue;
            }
        }
        i += 1;
    }

    // 211차: NNG + 만/JX → VV + EC 분리
    // "가지만" = "가지/NNG 만/JX" → MeCab이 잘못 분석
    // sample.tsv 기준 "가/VV 지만/EC"로 처리해야 함
    // 동사 어간 + 지만 패턴
    let mut i = 0;
    while i < tokens.len() {
        if i + 1 < tokens.len()
            && tokens[i].pos == "NNG"
            && tokens[i + 1].surface == ""
            && tokens[i + 1].pos == "JX"
        {
            // "가지" + "만" → "가" + "지만" 확인
            let surface = &tokens[i].surface;
            if surface.ends_with("") && surface.len() > "".len() {
                let stem = &surface[..surface.len() - "".len()];
                // 동사 어간인지 확인 (가, 보, 하 등)
                let verb_stems = ["", "", "", "", "", "", ""];
                if verb_stems.contains(&stem) {
                    let start = tokens[i].start_pos;
                    let end = tokens[i + 1].end_pos;
                    let stem_end = start + stem.len();
                    tokens[i] = SejongToken::new(stem, "VV", start, stem_end);
                    tokens[i + 1] = SejongToken::new("지만", "EC", stem_end, end);
                    i += 2;
                    continue;
                }
            }
        }
        i += 1;
    }

    apply_particle_and_ending_corrections(tokens);

    // 24~67차: 동사/형용사 분리, 어미 정규화, 접두사·의존명사 보정
    apply_verb_and_morpheme_corrections(tokens);

    // 228~244차: 복합어 병합 및 불규칙 활용 보정
    apply_compound_and_irregular_corrections(tokens);

    // 167~172차, 68~85차: 접미사·의존명사·파생어·목적 연결어미 보정
    apply_suffix_and_dependency_corrections(tokens);

    // 174~219차: 동사/형용사 활용 보정 (XSA 변환, VA 병합, 불규칙 어간 복원)
    apply_conjugation_corrections(tokens);

    // 220차, 86차, 87차, 87-2차, 88차, 205차 보정
    apply_post_conjugation_corrections(tokens);

    // 89~259차: 문장 종결·EC/EF 변환 보정
    apply_sentence_final_corrections(tokens);
}