프로그래밍 기술/웹 검색 엔진 만들기

7. 형태소 분석기 만들기

언제나휴일 2017. 12. 7. 10:49
반응형

7. 형태소 분석기 만들기


 

 이번에는 문자열의 내용을 분석하는 형태소 분석기를 만들어봅시다.

 

 형태소 분석기는 원본 내용을 형태소 단위로 분할하는 엔진입니다. 형태소 분석기는 웹 검색 엔진의 일부로 웹 로봇이 수집한 페이지의 내용을 분석하거나 검색 질의를 분석할 때 사용합니다. 형태소 분석기도 라이브러리로 만들기로 했는데 여기에서는 라이브러리 작성과 함께 정상적으로 만들어졌는지 확인하는 예광탄도 만듭시다.

 

7.1 형태소 분석기 예광탄 만들기

 

[그림 7.1] 형태소 분석기 메인 폼 자식 컨트롤 배치

[그림 7.1] 형태소 분석기 메인 폼 자식 컨트롤 배치

번호

컨트롤 이름

컨트롤 유형

설명

1

lb_content_info

Label

정보 표시

2

tbox_content

TextBox

내용 편집 창

3

bnt_parse

Button

내용 분석 버튼

4

gb_result

GroupBox

결과 그룹 박스

5

lb_cnt_info

Label

정보 표시

6

lb_cnt

Label

전체 형태소 개수

7

lv_result

ListView

결과 리스트 뷰

8

ch_name

ColumnHeader

형태소 이름

9

ch_rcnt

ColumnHeader

참조 개수

[ 7.1] 형태소 분석기 메인 폼의 자식 컨트롤

 

 분석하기 버튼을 클릭했을 때 이벤트 핸들러를 작성합시다.

private void btn_parse_Click(object sender, EventArgs e)

 

 이벤트 핸들러에서는 먼저 결과 리스트의 항목을 지워줍니다.

lv_result.Items.Clear();

 

 내용 편집 창에 입력한 문자열을 얻어와서 형태소 분석기에게 분석 요청합니다. 형태소 분석기는 내용 분석 이외의 다른 작업을 진행하지 않으므로 정적 클래스로 작성합시다. 그리고 형태소 분석을 위해 정적 메버드 Parse를 제공합시다.

string content = tbox_content.Text;

List<Morpheme> list = MorphemeParser.Parse(content);

 

 분석 결과의 형태소 개수를 lb_cnt 컨트롤의 Text 속성에 설정합니다. 그리고 결과 항목을 리스트 뷰에 추가합니다.

lb_cnt.Text = list.Count.ToString();

ListViewItem lvi = null;

foreach (Morpheme mo in list)

{

    lvi = new ListViewItem(

        new string[2] { mo.Name, mo.Count.ToString() }

        );

    lv_result.Items.Add(lvi);

}

 

MainForm.cs

using System;

using System.Collections.Generic;

using System.Windows.Forms;

using WSE_Core;

using MorphemeParserLib;

namespace MorphemeParser_예광탄

{

    public partial class MainForm : Form

    {

        public MainForm()

        {

            InitializeComponent();

        }

        private void btn_parse_Click(object sender, EventArgs e)

        {

            lv_result.Items.Clear();

            string content = tbox_content.Text;

            List<Morpheme> list = MorphemeParser.Parse(content);

            lb_cnt.Text = list.Count.ToString();

            ListViewItem lvi = null;

            foreach (Morpheme mo in list)

            {

                lvi = new ListViewItem(

                    new string[2] { mo.Name, mo.Count.ToString() }

                    );

                lv_result.Items.Add(lvi);

            }

        }

    }

}


7.2 형태소 분석기 라이브러리 만들기

 

 이제 형태소 분석기 라이브러리를 만듭시다. 예광탄으로 만든 다음에 정상적으로 동작함을 확인한 후에 라이브러리를 만듭니다. 여기에서는 결과적으로 예광탄에서 만든 내용과 라이브러리가 같으므로 예광탄에서 만드는 과정은 생략할게요.

public static class MorphemeParser

 

 형태소 분석기는 자연어 연구를 토대로 의미있는 최소 단위의 형태소를 분리하면 높은 수준의 형태소 분석기를 만들 수 있습니다.

 

 여기에서는 공백이나 마침표 등의 기호와 자주 사용하는 접두사와 접미사를 기준으로 내용을 분리하는 수준의 형태소 분석기를 만들게요.

 

 원본 내용을 분리할 접두사와 접미사 컬렉션을 선언합시다. 이 부분을 동적으로 추가할 수 있게 정의하면 보다 높은 수준의 형태소 분석기를 만들 수 있습니다.

static string[] filters = {" ",",",".","?","!","\r","\n","!","?",

                              "[","]","<",">","{","}","\"","'"};

static string[] pf_filters = {"","","","","","","",

                                  "에게","입니다","합니다"};

 

 이 책에서는 웹 검색 엔진을 만드는 전체 공정과 각 요소를 만드는 원리를 소개하는 것이며 높은 수준을 지향하는 부분은 여러분의 몫으로 남길게요.

  

 형태소를 분석하여 결과를 반환하는 메서드를 제공합시다.

public static List<Morpheme> Parse(string source)

 

 먼저 결과를 보관할 컬렉션을 생성합니다.

List<Morpheme> mlist = new List<Morpheme>();

 

 원본 내용을 접두사 필터 컬렉션을 기준으로 분할합니다.

string[] elems = source.Split(filters,

                     StringSplitOptions.RemoveEmptyEntries);

 

 분할한 문자열 컬렉션의 각 항목을 다시 분석합니다. 그리고 결과를 반환합니다. 각 항목을 분석하는 부분은 별도의 메서드로 작성합시다.

foreach (string elem in elems)

{

    ParseElem(elem, mlist);

}

return mlist;

 

 분할한 항목을 분석하는 메서드를 작성합시다.

private static void ParseElem(string elem, List<Morpheme> mlist)

 

 입력 인자로 받은 문자열의 끝이 접미사 컬렉션에 있는 요소와 일치하는지 확인하여 일치하면 접미사를 뺀 요소를 만듭니다. 일치하지 않으면 결과에 추가합니다. 이를 위해 세부적인 작업을 수행하는 부분은 별도의 메서드로 정의합시다.

foreach (string pf_filter in pf_filters)

{

    if (CheckPostfix(pf_filter, elem))

    {

        MakeElem(pf_filter, elem, mlist);

        return;

    }

}

RecordElem(elem, mlist); 

 

 먼저 분석할 요소의 끝에 접미사가 있는지 확인하는 메서드를 작성합시다.

private static bool CheckPostfix(string pf_filter, string elem)

 

 먼저 요소에 접미사 문자열이 포함한다면 두 개의 문자열의 길이의 차를 이용하여 부분 문자열을 얻어옵니다. 만약 얻어온 부분 문자열이 접미사와 같으면 참을 반환합니다. 그렇지 않으면 거짓을 반환합니다.

if (elem.Contains(pf_filter))

{

    int pos = elem.Length - pf_filter.Length;

    string sub = elem.Substring(pos);

    if (sub == pf_filter)

    {

        return true;

    }

}

return false;

 

 요소의 접미사를 뺀 나머지 요소를 결과에 추가하는 메서드를 작성합시다.

private static void MakeElem(string pf_filter, string elem, List<Morpheme> mlist)

 

 요소의 길이에서 접미사의 길이를 뺀 값을 이용하여 부분 문자열을 얻어옵니다.

int pos = elem.Length - pf_filter.Length;

string sub = elem.Substring(0, pos);

 

 얻어온 부분 문자열을 결과에 추가합니다.

RecordElem(sub, mlist);

 

 결과에 추가하는 메서드를 작성합시다.

private static void RecordElem(string elem, List<Morpheme> mlist)

 

 결과 목록에 있는 각 항목의 이름이 추가할 내용과 일치하면 참조 개수를 1 증가하고 반환합니다.

foreach (Morpheme mo in mlist)

{

    if (mo.Name == elem)

    {

        mo.Count++;

        return;

    }

}

 

 일치하는 형태소가 없다면 새로운 형태소를 생성하여 결과 컬렉션에 추가합니다.

Morpheme mor = new Morpheme(elem, 1);

mlist.Add(mor);

 

MorphemeParser.cs

using System;

using System.Collections.Generic;

using WSE_Core;

namespace MorphemeParserLib

{

    /// <summary>

    /// 형태소 분석기 - 정적 클래스

    /// </summary>

    public static class MorphemeParser

    {

        static string[] filters = {" ",",",".","?","!","\r","\n","!","?",

                                      "[","]","<",">","{","}","\"","'"};

        static string[] pf_filters = {"","","","","","","",

                                     "에게","입니다","합니다"};

        /// <summary>

        /// 형태소 분석 정적 메서드

        /// </summary>

        /// <param name="source">원본</param>

        /// <returns>형태소 컬렉션</returns>

        public static List<Morpheme> Parse(string source)

        {

            List<Morpheme> mlist = new List<Morpheme>();

            string[] elems = source.Split(filters,

                StringSplitOptions.RemoveEmptyEntries);

            foreach (string elem in elems)

            {

                ParseElem(elem, mlist);

            }

            return mlist;

        }

         private static void ParseElem(string elem, List<Morpheme> mlist)

        {

            foreach (string pf_filter in pf_filters)

            {

                if (CheckPostfix(pf_filter, elem))

                {

                    MakeElem(pf_filter, elem, mlist);

                    return;

                }

            }

 

            RecordElem(elem, mlist);

        }

 

        private static void RecordElem(string elem, List<Morpheme> mlist)

        {

            foreach (Morpheme mo in mlist)

            {

                if (mo.Name == elem)

                {

                    mo.Count++;

                    return;

                }

            }

 

            Morpheme mor = new Morpheme(elem, 1);

            mlist.Add(mor);

        }

 

        private static void MakeElem(string pf_filter, string elem, List<Morpheme> mlist)

        {

            int pos = elem.Length - pf_filter.Length;

            string sub = elem.Substring(0, pos);

            RecordElem(sub, mlist);

        }

 

        private static bool CheckPostfix(string pf_filter, string elem)

        {

            if (elem.Contains(pf_filter))

            {

                int pos = elem.Length - pf_filter.Length;

                string sub = elem.Substring(pos);

                if (sub == pf_filter)

                {

                    return true;

                }

            }

            return false;

        }

    }

}

 

반응형